UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

174 lines (149 loc) 6.86 kB
const cds = require('..') class EventHandlers { /** @type {EventHandler[]} */ before = [] /** @type {EventHandler[]} */ on = [] /** @type {EventHandler[]} */ after = [] /** @type {EventHandler[]} */ _error = [] /** @param {Service} srv */ prepend (srv, fn) { const {handlers} = srv, _new = srv.handlers = new EventHandlers const x = fn.call (srv,srv) // NOTE: we need the doubled await to compensate usages of srv.prepend() with missing awaits !!! if (x?.then) throw cds.error `srv.prepend() doesn't accept asynchronous functions anymore` for (let each in _new) if (_new[each].length) handlers[each].unshift(..._new[each]) srv.handlers = handlers } //-------------------------------------------------------------------------- /** Registers event handlers. This is the central method to register handlers, * used by all respective public API methods, i.e. .on/before/after/reject. * @import Service from './cds.Service' * @import Request from '../req/request' * @param {Service} srv * @param {'on'|'before'|'after'} phase * @param {string|string[]} event * @param {string|string[]} path * @param {(req:Request)=>{}} handler */ register (srv, phase, event, path, handler) { if (!handler) [ handler, path ] = [ path, '*' ] // argument path is optional if (typeof handler !== 'function') cds.error.expected `${{handler}} to be a function` if (handler._is_stub) { cds.log().warn (`\n WARNING: You are trying to register a frameworks-generated stub method for custom action/function '${event}' in implementation of service '${srv.name}'. We're ignoring that as we already registered the according handler. Please fix your implementation, i.e., just don't register that handler. `) return srv } // Canonicalize path argument if (!path || path === '*') path = undefined else if (is_array(path)) { for (let each of path) this.register (srv, phase, event, each, handler) return srv } else if (typeof path === 'object') { if (path.is_singular && event === 'READ') event = 'each' //> srv.after ('READ', Book, ...) w/ Book is a singular def from cds-typer path = path.name || cds.error.expected `${{path}} to be a string or an entity's CSN definition` } else if (typeof path === 'string') { if (!srv.isDatabaseService && !path.startsWith(srv.name+'.')) path = `${srv.name}.${path}` } // Canonicalize event argument... if (!event || event === '*') event = undefined // Recurse for array of events else if (is_array(event)) { for (let each of event) this.register (srv, phase, each, path, handler) return srv } // SAVE Foo is a shorthand for [ CREATE, UPSERT, UPDATE ] Foo else if (event === 'SAVE') { //> special handling for SAVE // SAVE Foo.drafts is mapped to [ CREATE, UPSERT, UPDATE ] Foo, when called from draftActivate() if (path?.endsWith('.drafts')) { handler = save_draft_handler_for (phase, handler) path = path.slice(0,-7) // Foo.drafts -> Foo } for (let each of ['CREATE','UPSERT','UPDATE']) this.register (srv, phase, each, path, handler) return srv } // WRITE Foo is a shorthand for [ CREATE, UPSERT, UPDATE ] Foo else if (event === 'WRITE') { for (let each of ['CREATE','UPSERT','UPDATE']) this.register (srv, phase, each, path, handler) return srv } // srv.after ('each', ...) is a shorthand for after READ w/ handler looping over results else if (phase === 'after' && ( event === 'each' //> srv.after ('each', Book, b => ...) // event 'each' => READ each || event === 'READ' && /^\(?each\b/.test(handler) //> srv.after ('READ', Book, each => ...) // handler's first param is named 'each' )) { event = 'READ' // override event='each' to 'READ' handler = foreach_handler_for (handler) } // If event is an object, its assumed to be an action definition else if (typeof event === 'object') { // extract action name from an action definition's fqn event = event.name && /[^.]+$/.exec(event.name)[0] || cds.error.expected `${{event}} to be a string or an action's CSN definition` } // Allow HTTP method names as aliases for CRUD events else event = events[event] || event // Finally register with a filter function to match requests to be handled const handlers = event === 'error' ? this._error : this[phase] handlers.push (new EventHandler (phase, event, path, handler)) if (phase === 'on') cds.emit('subscribe',srv,event) //> inform messaging service return srv } } class EventHandler { constructor (phase, event, path, handler) { const h = { [phase]: event || '*' } if (path) h.path = path h.handler = async(handler) Object.defineProperty (h, 'for', {value: this.for(event,path) }) return h } /** Factory for the actual filter method this.for, assigned above */ for (event, path) { if (event && path) return req => event === req.event && (path === req.path || path === req.entity) if (event) return req => event === req.event if (path) return req => path === req.path || path === req.entity else return req => !req.event?.includes('/#') } } const is_array = Array.isArray /** * Turns a handler function into an async function, if not already. * This is necessary to avoid uncaught exceptions when sync handlers are executed with Promise.all(). * See cap/cds/pull/5864 for more details and discussion on this topic. * See also tests/runtime/srv-dispatch.test.js. */ const async = handler => { if (handler.constructor.name === 'AsyncFunction') return handler const async = async function (...args) { return handler.call(this, ...args) } return Object.defineProperty (async, 'name', { value: handler.name }) } const save_draft_handler_for = (phase, handler) => { switch (phase) { case 'before': return function (req) { if (req._?.event === 'draftActivate') return handler.call (this, req) } case 'after': return function (res, req) { if (req._?.event === 'draftActivate') return handler.call (this, res, req) } case 'on': return function (req, next) { if (req._?.event === 'draftActivate') return handler.call (this, req, next) else return next() } default: cds.error `SAVE not supported in ${phase} handlers` }} const foreach_handler_for = (handler) => function (rows,req) { if (!rows) return if (is_array(rows)) return rows.forEach (r => handler.call(this,r,req)) else return handler.call(this,rows,req) } const events = { SELECT: 'READ', GET: 'READ', PUT: 'UPDATE', POST: 'CREATE', PATCH: 'UPDATE', INSERT: 'CREATE', DISCARD: 'CANCEL', //> REVISIT: Shouldn't we switch to DISCARD everywhere? } module.exports = EventHandlers