UNPKG

@resin/pinejs

Version:

Pine.js is a sophisticated rules-driven API engine that enables you to define rules in a structured subset of English. Those rules are used in order for Pine.js to generate a database schema and the associated [OData](http://www.odata.org/) API. This make

182 lines (161 loc) 6.14 kB
Promise = require 'bluebird' { ODataParser } = require '@resin/odata-parser' { OData2AbstractSQL } = require '@resin/odata-to-abstract-sql' memoize = require 'memoizee' memoizeWeak = require 'memoizee/weak' _ = require 'lodash' { BadRequestError, ParsingError, TranslationError } = require './errors' deepFreeze = require 'deep-freeze' env = require '../config-loader/env' permissions = require './permissions' sbvrUtils = require './sbvr-utils' exports.BadRequestError = BadRequestError exports.ParsingError = ParsingError exports.TranslationError = TranslationError # Converts a value to its string representation and tries to parse is as an # OData bind exports.parseId = (b) -> ODataParser.matchAll(String(b), 'ExternalKeyBind') exports.memoizedParseOdata = memoizedParseOdata = do -> odataParser = ODataParser.createInstance() parseOdata = (url) -> odata = odataParser.matchAll(url, 'Process') # if we parse a canAccess action rewrite the resource to ensure we # do not run the resource hooks if odata.tree.property?.resource == 'canAccess' odata.tree.resource = odata.tree.resource + '#' + odata.tree.property.resource return odata _memoizedParseOdata = memoize( parseOdata primitive: true max: env.cache.parseOData.max ) return (url) -> if _.includes(url, '$') # If we're doing a complex url then skip caching due to # of permutations return parseOdata(url) else # Else if it's simple we can easily skip the parsing as we know we'll get a high % hit rate # We deep clone to avoid mutations polluting the cache return _.cloneDeep(_memoizedParseOdata(url)) memoizedGetOData2AbstractSQL = memoizeWeak( (abstractSqlModel) -> odata2AbstractSQL = OData2AbstractSQL.createInstance() odata2AbstractSQL.setClientModel(abstractSqlModel) return odata2AbstractSQL ) memoizedOdata2AbstractSQL = do -> _memoizedOdata2AbstractSQL = memoizeWeak( (abstractSqlModel, odataQuery, method, bodyKeys, existingBindVarsLength) -> try odata2AbstractSQL = memoizedGetOData2AbstractSQL(abstractSqlModel) abstractSql = odata2AbstractSQL.match(odataQuery, 'Process', [method, bodyKeys, existingBindVarsLength]) # We deep freeze to prevent mutations, which would pollute the cache deepFreeze(abstractSql) return abstractSql catch e if e instanceof permissions.PermissionError throw e console.error('Failed to translate url: ', JSON.stringify(odataQuery, null, '\t'), method, e, e.stack) throw new TranslationError('Failed to translate url') normalizer: (abstractSqlModel, [ odataQuery, method, bodyKeys, existingBindVarsLength ]) -> return JSON.stringify(odataQuery) + method + bodyKeys + existingBindVarsLength max: env.cache.odataToAbstractSql.max ) return (request) -> { method, odataQuery, odataBinds, values } = request abstractSqlModel = sbvrUtils.getAbstractSqlModel(request) # Sort the body keys to improve cache hits { tree, extraBodyVars, extraBindVars } = _memoizedOdata2AbstractSQL(abstractSqlModel, odataQuery, method, _.keys(values).sort(), odataBinds.length) _.assign(values, extraBodyVars) odataBinds.push(extraBindVars...) return tree exports.metadataEndpoints = metadataEndpoints = ['$metadata', '$serviceroot'] notBadRequestOrParsingError = (e) -> not ((e instanceof BadRequestError) or (e instanceof ParsingError)) exports.parseOData = (b) -> Promise.try -> if b._isChangeSet csReferences = new Map() # We sort the CS set once, we must assure that requests which reference # other requests in the changeset are placed last. Once they are sorted # Map will guarantee retrival of results in insertion order sortedCS = _.sortBy b.changeSet, (el) -> !(el.url[0] == '/') Promise.reduce(sortedCS, parseODataChangeset, csReferences) .then (csReferences) -> Array.from(csReferences.values()) else { url, apiRoot } = splitApiRoot(b.url) odata = memoizedParseOdata(url) return { method: b.method url vocabulary: apiRoot resourceName: odata.tree.resource odataBinds: odata.binds odataQuery: odata.tree values: b.data custom: {} _defer: false } .catch SyntaxError, (e) -> throw new BadRequestError("Malformed url: '#{b.url}'") .catch notBadRequestOrParsingError, (e) -> console.error('Failed to parse url: ', b.method, b.url, e, e.stack) throw new ParsingError("Failed to parse url: '#{b.url}'") parseODataChangeset = (csReferences, b) -> contentId = mustExtractHeader(b, 'content-id') if csReferences.has(contentId) throw new BadRequestError('Content-Id must be unique inside a changeset') if b.url[0] == '/' { url, apiRoot } = splitApiRoot(b.url) odata = memoizedParseOdata(url) defer = false else url = b.url odata = memoizedParseOdata(url) { bind } = odata.tree.resource [ tag, id ] = odata.binds[bind] # Use reference to collect information ref = csReferences.get(id) if _.isUndefined(ref) throw new BadRequestError('Content-Id refers to a non existent resource') apiRoot = ref.vocabulary # Update resource with actual resourceName odata.tree.resource = ref.resourceName defer = true parseResult = { method: b.method url vocabulary: apiRoot resourceName: odata.tree.resource odataBinds: odata.binds odataQuery: odata.tree values: b.data custom: {} id: contentId _defer: defer } csReferences.set(contentId, parseResult) return csReferences splitApiRoot = (url) -> url = url.split('/') apiRoot = url[1] if !apiRoot? throw new ParsingError('No such api root: ' + apiRoot) url = '/' + url[2...].join('/') return { url: url, apiRoot: apiRoot } mustExtractHeader = (body, header) -> h = body.headers[header]?[0] if _.isUndefined h throw new BadRequestError("#{header} must be specified") return h exports.translateUri = (request) -> if request.abstractSqlQuery? return request isMetadataEndpoint = request.resourceName in metadataEndpoints or request.method is 'OPTIONS' if !isMetadataEndpoint abstractSqlQuery = memoizedOdata2AbstractSQL(request) request = _.clone(request) request.abstractSqlQuery = abstractSqlQuery return request return request