UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

182 lines (156 loc) 8.31 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() { return super.entities = this.reflect (d => d.kind === 'entity') } get events() { return super.events = this.reflect (d => d.kind === 'event') } get types() { return super.types = this.reflect (d => !d.kind || d.kind === 'type') } get actions() { return super.actions = this.reflect (d => d.kind === 'action' || d.kind === 'function') } reflect (filter) { return this.model?.childrenOf (this.namespace, filter) || [] } } /** * This class provides the API used by service providers to add event handlers. * It inherits the ConsumptionAPI and ReflectionAPI. */ class Service extends ReflectionAPI { 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 {( entity?, path?, 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.') } } 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