UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

264 lines (227 loc) 11.7 kB
const EventHandlers = require('./srv-handlers') const Request = require('../req/request') const Event = require('../req/event') const cds = require('../index') /** * This class constitutes the API used by service consumers to send requests, and emit events. * * - `dispatch()` - is the central method ultimately called by all the other methods below. * - `emit()` - is the central method of the **Messaging API** to emit asynchronous event messages. * - `send()` - is the central method of the **Request API** to send synchronous requests. * - `run()` - is the central method of the **Querying API** to execute queries. * * The other methods, like `read`, `create`, `update`, `delete`, are **CRUD-style** syntactical * sugar variants provided for convenience, or **REST-style** like `get`, `put`, `post`, `patch`. */ class ConsumptionAPI { async dispatch() {} emit (event, data, headers) { if (is_object(event)) return this.dispatch (event instanceof Event ? event : new Event(event)) else return this.dispatch (new Event ({ event, data, headers })) } send (...args) { const req = _req4 (...args) return this.dispatch (req) } schedule (...args) { const req = _req4 (...args), {ms4} = cds.utils return { after (t,u) { (req.queue ??= {}).after = ms4(t,u); return this }, every (t,u) { (req.queue ??= {}).every = ms4(t,u); return this }, then: (r,e) => cds.queued(this).send(req).then(r,e) } } get (...args) { return is_rest(args[0]) ? this.send('GET', ...args) : this.read (...args) } put (...args) { return is_rest(args[0]) ? this.send('PUT', ...args) : this.update (...args) } post (...args) { return is_rest(args[0]) ? this.send('POST', ...args) : this.create (...args) } patch (...args) { return is_rest(args[0]) ? this.send('PATCH', ...args) : this.update (...args) } delete (...args) { return is_rest(args[0]) ? this.send('DELETE',...args) : DELETE.from (...args).bind(this) } /** * Queries can be passed as one of the following: * - a CQL tagged template string, which is converted into an instance of `cds.ql.Query` * - a CQN object, or an array of such * - a native SQL string, with binging parameters in the second argument `data` */ run (query, data) { if (query.raw) [ query, data ] = [ cds.ql (...arguments) ] const req = new Request ({ query, data }) return this.dispatch (req) } read (...args) { return is_query(args[0]) ? this.run(...args) : SELECT.read(...args).bind(this) } insert (...args) { return INSERT(...args).bind(this) } create (...args) { return INSERT.into(...args).bind(this) } update (...args) { return UPDATE.entity(...args).bind(this) } upsert (...args) { return UPSERT(...args).bind(this) } exists (...args) { return SELECT.one([1]).from(...args).bind(this) } /** * Streaming API variant of .run(). Subclasses should override this to support real streaming. */ foreach (query, data, callback) { if (!callback) [ data, callback ] = [ undefined, data ] return this.run (query, data) .then (rows => rows.forEach(callback) || rows) } // Internal-only API to free resources when tenants offboard /** @protected */ disconnect(tenant) {} // eslint-disable-line no-unused-vars } /** * This class provides API used by service providers to reflect * their service definitions from a given model. */ class ReflectionAPI extends ConsumptionAPI { /** @param {import('../core/linked-csn').LinkedCSN} csn */ set model (csn) { super.model = csn ? cds.compile.for.nodejs(csn) : undefined } /** @type import('../core/classes').service */ get definition() { const defs = this.model?.definitions; if (!defs) return super.definition = undefined return super.definition = defs[this.options.service] || defs[this.name] } get namespace() { return super.namespace = this.definition?.name || this.model?.namespace || !this.isDatabaseService && !/\W/.test(this.name) && this.name || undefined } get entities() { if (!this.model) return super.entities = [] // REVISIT: remove _compat_texts_entities with cds^10 const filter = this.model._compat_texts_entities ? d => d.kind === 'entity' : d => d.kind === 'entity' && !d.name.endsWith('.texts') const entities = this.reflect(filter) return super.entities = deconstructable_and_iterable(entities, 'entities', this) } get events() { if (!this.model) return super.events = [] const events = this.reflect(d => d.kind === 'event') return super.events = deconstructable_and_iterable(events, 'events', this) } get types() { if (!this.model) return super.types = [] const types = this.reflect(d => !d.kind || d.kind === 'type') return super.types = deconstructable_and_iterable(types, 'types', this) } get actions() { if (!this.model) return super.actions = [] const actions = this.reflect(d => d.kind === 'action' || d.kind === 'function') return super.actions = deconstructable_and_iterable(actions, 'actions', this) } reflect (filter) { return this.model?.childrenOf (this.namespace, filter) || [] } } const deconstructable_and_iterable = (it, what, srv) => { // REVISIT: remove deprecated function API with cds^10 const compat_function_api = Object.assign(compat_function_factory(what, srv, it), it) // srv.* is both deconstructable and iterable return Object.setPrototypeOf(compat_function_api, it) } const compat_function_factory = (api, srv, it) => cds.utils.deprecated (ns => { return !ns || ns === srv.namespace ? it : srv.model[api]?.(ns) || {} }, { kind: 'API', old: `srv.${api}()`, use: api === 'entities' ? `cds.${api}()` : undefined }) /** * This class provides the API used by service providers to add event handlers. * It inherits the ConsumptionAPI and ReflectionAPI. */ class Service extends ReflectionAPI { /** * @param {string} name * @param {import('../core/linked-csn').LinkedCSN} model */ constructor (name, model, options) { super() if (typeof name === 'object') [ model, options, name = _service_in(model) ] = [ name, model ] this.name = name || new.target.name // i.e. when called without any arguments this.options = options ??= {} if (options.kind) this.kind = options.kind // shortcut, e.g. for 'sqlite', ... if (model) this.model = model this.handlers = new EventHandlers(this) this.decorate() } init(){ return this } //> essentially a constructor without arguments // Handler registration API prepend (fn) { return this.handlers.prepend.call (this,fn) } /** @typedef {( event, entity?, handler:(req:import('../req/request'))=>{})=> Service} boa */ /** @type boa */ before (...args) { return this.handlers.register (this, 'before', ...args) } /** @type boa */ on (...args) { return this.handlers.register (this, 'on', ...args) } /** @type boa */ after (...args) { return this.handlers.register (this, 'after', ...args) } reject (e, path) { return this.handlers.register (this, '_initial', e, path, r => r.reject (405, `Event "${r.event}" not allowed for entity "${r.path}".`) )} // Overrriding `srv.run()` to additionally allow running a function in a managed transaction. run (fn) { if (typeof fn !== 'function') return super.run (...arguments) if (this.context) return fn(this) // if this is already a tx -> run fn with this const ctx = cds.context, tx = ctx?.tx // is there an (open) outer tx? ... if (!tx || tx._done === 'committed') return this.tx(fn) // no -> run fn with root tx if (tx._done !== 'rolled back') return fn(this.tx(ctx)) // yes -> run fn with nested tx else throw this.tx._is_done (tx._done) // throw if outer tx was rolled back } // Inofficial APIs - for internal use only /** @protected */ static _is_service_class = true //> for factory /** @protected */ get endpoints() { return super.endpoints = cds.service.protocols.endpoints4(this) } /** @protected */ set endpoints(p) { super.endpoints = p } /** @protected */ get path() { return super.path = cds.service.protocols.path4(this) } /** @protected */ set path(p) { super.path = p } // Deprecated APIs - kept for backwards compatibility /** @deprecated */ get _handlers() { return this.handlers } /** @deprecated */ get operations() { return this.actions } /** @deprecated */ get transaction() { return this.tx } /** @deprecated */ get isExtensible() { return this.model === cds.model && !this.name?.startsWith('cds.xt.') } get resolve() { if (this._resolve) return this._resolve const { resolveView, getTransition } = require('../../libx/_runtime/common/utils/resolveView') const PERSISTENCE_TABLE = '@cds.persistence.table' const _isPersistenceTable = target => Object.prototype.hasOwnProperty.call(target, PERSISTENCE_TABLE) && target[PERSISTENCE_TABLE] const _defaultAbort = tx => e => e._service?.name === tx.definition?.name this._resolve = (query, abortCondition) => { const ctx = cds.context const model = ctx?.model || this.model return resolveView(query, model, this, abortCondition || _defaultAbort(this)) } // REVISIT: Remove argument `skipForbiddenViewCheck` once we get rid of composition tree this._resolve.transitions = (query, abortCondition, skipForbiddenViewCheck) => { const target = query && typeof query === 'object' ? cds.infer.target(query) || query?._target : undefined const _tx = typeof tx === 'function' ? cds.context?.tx : this const event = query?.INSERT ? 'INSERT' : query?.UPDATE ? 'UPDATE' : query?.DELETE ? 'DELETE' : undefined return getTransition(target, _tx, skipForbiddenViewCheck, event, { abort: abortCondition ?? (this.isDatabaseService ? this.resolve._abortDB : _defaultAbort(this)) }) } this._resolve.resolve4db = query => { return this.resolve(query, this, this.resolve.abortDB) } // REVISIT: Remove once we get rid of composition tree this._resolve.table = target => { if (target.query?._target && !_isPersistenceTable(target)) { return this.resolve.table(target.query._target) } return target } // REVISIT: Remove once we get rid of old db this._resolve.abortDB = target => { return !!(_isPersistenceTable(target) || !target.query?._target) } this._resolve.transitions4db = (query, skipForbiddenViewCheck) => { return this.resolve.transitions(query, this.resolve.abortDB, skipForbiddenViewCheck) } return this._resolve } } const { dispatch, handle } = require('./srv-dispatch') Service.prototype.dispatch = dispatch Service.prototype.handle = handle Service.prototype.tx = require('./srv-tx') Service.prototype.decorate = require('./srv-methods') const is_object = x => typeof x === 'object' const is_query = x => x?.bind || Array.isArray(x) && !x.raw const is_rest = x => typeof x === 'string' && x[0] === '/' const _service_in = m => cds.linked(m).services?.[0]?.name || cds.error.expected `${{model:m}} to be a CSN with a single service definition` const _req4 = (event, path, data, headers) => { if (is_query(event)) return new Request({ query: event, data: path, headers }) if (is_object(event)) return event instanceof Request ? event : new Request(event) if (is_object(path)) return new Request (path.is_linked //... ? { method:event, entity:path, data, headers } : { method:event, data:path, headers:data }) else return new Request({ method:event, path, data, headers }) } exports = module.exports = Service exports.Service = Service