UNPKG

@sap/cds

Version:

SAP Cloud Application Programming Model - CDS for Node.js

217 lines (187 loc) 7 kB
const cds = require ('../index'), { uuid } = cds.utils const async_events = { succeeded:1, failed:1, done:1, commit:1 } const locale = require('../i18n/locale') const { EventEmitter } = require('events') /** * This is the base class for `cds.Events` and `cds.Requests`, * providing the transaction context nature to all instances. * Calling `srv.tx()` without args to start new transacions * creates direct instances of this base class. */ class EventContext { /** Creates a new instance that inherits from cds.context */ static for (_,_as_root) { const ctx = new this (_) const base = cds.context if (base) { if (_as_root) ctx._set('_propagated', Object.create(base, { timestamp: { value: undefined } })) else { ctx._set('_propagated', base) if (!ctx.context) ctx._set('context', base.context) // all transaction handling works with root contexts if (!ctx.tx && base.tx) ctx.tx = base.tx } } return ctx } constructor(_={}) { Object.defineProperty (this, '_', { value:_, writable:true }) Object.assign (this, _) } /** @type <T>( property: string, value: T ) => T */ _set (property, value) { Object.defineProperty (this, property, { value, writable:true }) return value } // // Emitting and listening to succeeded / failed / done events // get emitter() { return this.context._emitter || this.context._set('_emitter', new EventEmitter) } async emit (event,...args) { const emitter = this.context._emitter; if (!emitter) return if (event in async_events) for (const each of emitter.listeners(event)) await each.call (this, ...args) else return emitter.emit (event,...args) } on (event, listener) { return this.emitter.on (event, listener.bind(this)) } once (event, listener) { return this.emitter.once (event, listener.bind(this)) } before (event, listener) { return this.emitter.prependListener (event, listener.bind(this)) } // // The following properties are inherited from root contexts, if exist... // set context(c) { if (c) this._set('context', this._set('_propagated', c)) } get context() { return this } set id(id) { if (id) super.id = id } get id() { return super.id = this._propagated.id || this.headers[ 'x-correlation-id' ] || uuid() } set tenant(t) { if (t) super.tenant = t } get tenant() { return super.tenant = this._propagated.tenant || this._.req?.tenant } set user(u) { super.user = cds.User(u) } get user() { return super.user = this._propagated.user || cds.User (this._.req?.user) } set locale(l) { if (l) super.locale = super._locale = l } get locale() { return super.locale = this._propagated.locale || locale.from(this._.req) //||this.http?.req) } get _locale() { return super._locale = this._propagated._locale || locale.header(this._.req) //||this.http?.req) || this.hasOwnProperty('locale') && this.locale // eslint-disable-line no-prototype-builtins } get _features() { return super._features = this._propagated._features || Features.for (this._.req?.features || this.user.features) } get features() { return super.features = this._features || Features.none } set features(v) { super.features = Features.for(v) || Features.none } get model() { const m = this._propagated.model return this._set('model',m) } set model(m) { super.model = m } get timestamp() { return super.timestamp = this._propagated.timestamp || new Date } set headers(h) { if (h) super.headers = h } get headers() { // REVISIT: isn't "this._.req?.headers" deprecated? shouldn't it be "this.http?.req?.headers"? let headers = this._.req?.headers if (!headers) { headers={} const outer = this._propagated.headers if (outer) for (let each of EventContext.propagateHeaders) { if (each in outer) headers[each] = outer[each] } } return super.headers = headers } set http(rr) { if (!this._.req) Object.assign(this._, rr) super.http = rr } get http() { return super.http = this._propagated.http || this._.req && this._.res && { req:this._.req, res:this._.res } } /** * This sets an EventContext's / Message's / Request's tx object, i.e. * the service which this request is passed on for execution. * In response to that, the instance will be wired up to and inherit * context properties from tx.context. */ set tx (tx) { Object.defineProperty (this,'tx',{value:tx}) //> allowed only once! const root = tx.context?.context if (root && root !== this) { if (!this.hasOwnProperty('context')) this.context = root // eslint-disable-line no-prototype-builtins } } get _tx() { return this.tx } // REVISIT: for compatibility to bade usages of req._tx } class Features { /** * Returns an instance of this class for different supported input variants to specify features: * * - an `array` of feature names of features to be enabled * - a `string` with a comma-separated list of feature names * - the string `'*'` to generically enable _all_ features * - an `object` with feature names as keys and boolean values true/false * - `null` or `undefined` or an _empty_ `string` or `array` or `object` to indicate no features * * Note that the returned instance is effectively an object that has all enabled feature * names as keys with the value true. In particualar, that means if the input is an object, * with some flags set to false, the returned instance will not have these keys at all. * * Hence, users of cds.context.features can simply check for the presence of a feature * with the like of `if ('foo' in cds.context.features)`... * @returns {Features} */ static for (x) { if (x == null) return if (x === '*') return this.all if (Array.isArray(x)) ; //> go on below else if (typeof x === 'object') x = Object.keys(x).filter(k => x[k]) else if (typeof x === 'string') x = x.split(',') if (x.length) return Object.assign (new this, x.reduce((o,f)=>{o[f]=true;return o},{})) } get given() { return true } get $hash() { let h = Object.keys(this).join(','); Object.defineProperty(this,'$hash',{value:h}); return h } includes(t) { return t in this } has(t) { return t in this } map(..._) { return Object.keys(this).map(..._) } find(filter) { for (let t in this) if (filter(t)) return t } some(filter) { for (let t in this) if (filter(t)) return true } every(filter) { for (let t in this) if (!filter(t)) return false } static all = new Proxy ({'*':true},{ has:() => true, get:(_,p) => p === '$hash' ? '*' : true }) static none = new class none extends Features { get given(){ return false } get $hash(){ return '' } } } EventContext.prototype._set('_propagated', Object.seal({})) EventContext.propagateHeaders = [ 'x-correlation-id' ] module.exports = EventContext