@sap/cds
Version:
SAP Cloud Application Programming Model - CDS for Node.js
174 lines (149 loc) • 6.86 kB
JavaScript
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