@sap/cds 6.0.4 → 6.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (120) hide show
  1. package/CHANGELOG.md +128 -18
  2. package/apis/cds.d.ts +11 -7
  3. package/apis/log.d.ts +48 -0
  4. package/apis/ql.d.ts +72 -15
  5. package/bin/build/buildTaskHandler.js +5 -2
  6. package/bin/build/constants.js +4 -1
  7. package/bin/build/provider/buildTaskHandlerEdmx.js +11 -39
  8. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +13 -32
  9. package/bin/build/provider/buildTaskHandlerInternal.js +56 -4
  10. package/bin/build/provider/buildTaskProviderInternal.js +22 -14
  11. package/bin/build/provider/hana/index.js +8 -7
  12. package/bin/build/provider/java/index.js +18 -8
  13. package/bin/build/provider/mtx/index.js +7 -4
  14. package/bin/build/provider/mtx/resourcesTarBuilder.js +64 -35
  15. package/bin/build/provider/mtx-extension/index.js +57 -0
  16. package/bin/build/provider/mtx-sidecar/index.js +46 -18
  17. package/bin/build/provider/nodejs/index.js +34 -13
  18. package/bin/deploy/to-hana/cfUtil.js +7 -2
  19. package/bin/serve.js +7 -4
  20. package/lib/compile/{index.js → cds-compile.js} +0 -0
  21. package/lib/compile/extend.js +15 -5
  22. package/lib/compile/minify.js +1 -15
  23. package/lib/compile/parse.js +1 -1
  24. package/lib/compile/resolve.js +2 -2
  25. package/lib/compile/to/srvinfo.js +6 -4
  26. package/lib/{deploy.js → dbs/cds-deploy.js} +7 -6
  27. package/lib/env/{index.js → cds-env.js} +1 -17
  28. package/lib/env/{requires.js → cds-requires.js} +24 -3
  29. package/lib/env/defaults.js +7 -1
  30. package/lib/env/schemas/cds-package.json +11 -0
  31. package/lib/env/schemas/cds-rc.json +605 -0
  32. package/lib/index.js +19 -16
  33. package/lib/log/{errors.js → cds-error.js} +1 -1
  34. package/lib/log/{index.js → cds-log.js} +0 -0
  35. package/lib/ql/SELECT.js +1 -1
  36. package/lib/ql/{index.js → cds-ql.js} +0 -0
  37. package/lib/req/context.js +35 -7
  38. package/lib/req/locale.js +5 -1
  39. package/lib/{serve → srv}/adapters.js +23 -19
  40. package/lib/{connect → srv}/bindings.js +0 -0
  41. package/lib/{connect/index.js → srv/cds-connect.js} +1 -1
  42. package/lib/{serve/index.js → srv/cds-serve.js} +0 -0
  43. package/lib/{serve → srv}/factory.js +1 -1
  44. package/lib/{serve/Service-api.js → srv/srv-api.js} +14 -6
  45. package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +3 -2
  46. package/lib/{serve/Service-handlers.js → srv/srv-handlers.js} +10 -0
  47. package/lib/{serve/Service-methods.js → srv/srv-methods.js} +10 -8
  48. package/lib/srv/srv-models.js +206 -0
  49. package/lib/{serve/Transaction.js → srv/srv-tx.js} +6 -1
  50. package/lib/utils/{tests.js → cds-test.js} +2 -2
  51. package/lib/utils/cds-utils.js +146 -0
  52. package/lib/utils/index.js +2 -145
  53. package/lib/utils/jest.js +43 -0
  54. package/lib/utils/resources/index.js +14 -24
  55. package/lib/utils/resources/tar.js +18 -41
  56. package/libx/_runtime/auth/index.js +13 -10
  57. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +7 -19
  58. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -4
  59. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -4
  60. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -4
  61. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -4
  62. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -2
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +6 -19
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -4
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -3
  69. package/libx/_runtime/cds-services/services/utils/differ.js +4 -0
  70. package/libx/_runtime/cds-services/util/errors.js +1 -29
  71. package/libx/_runtime/common/i18n/messages.properties +2 -1
  72. package/libx/_runtime/common/perf/index.js +10 -15
  73. package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
  74. package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
  75. package/libx/_runtime/common/utils/template.js +1 -1
  76. package/libx/_runtime/db/Service.js +2 -14
  77. package/libx/_runtime/db/expand/expandCQNToJoin.js +28 -25
  78. package/libx/_runtime/db/generic/input.js +4 -0
  79. package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
  80. package/libx/_runtime/extensibility/activate.js +47 -47
  81. package/libx/_runtime/extensibility/add.js +19 -13
  82. package/libx/_runtime/extensibility/addExtension.js +17 -13
  83. package/libx/_runtime/extensibility/defaults.js +25 -30
  84. package/libx/_runtime/extensibility/linter/allowlist_checker.js +373 -0
  85. package/libx/_runtime/extensibility/linter/annotations_checker.js +113 -0
  86. package/libx/_runtime/extensibility/linter/checker_base.js +20 -0
  87. package/libx/_runtime/extensibility/linter/namespace_checker.js +180 -0
  88. package/libx/_runtime/extensibility/linter.js +32 -0
  89. package/libx/_runtime/extensibility/push.js +78 -21
  90. package/libx/_runtime/extensibility/service.js +29 -12
  91. package/libx/_runtime/extensibility/token.js +56 -0
  92. package/libx/_runtime/extensibility/validation.js +6 -9
  93. package/libx/_runtime/fiori/generic/new.js +0 -11
  94. package/libx/_runtime/hana/Service.js +0 -1
  95. package/libx/_runtime/hana/conversion.js +12 -1
  96. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +4 -3
  97. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -0
  98. package/libx/_runtime/hana/pool.js +6 -10
  99. package/libx/_runtime/hana/search2Contains.js +0 -5
  100. package/libx/_runtime/hana/search2cqn4sql.js +1 -0
  101. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  102. package/libx/_runtime/messaging/service.js +11 -6
  103. package/libx/_runtime/remote/utils/data.js +5 -0
  104. package/libx/_runtime/sqlite/Service.js +0 -1
  105. package/libx/odata/afterburner.js +79 -2
  106. package/libx/odata/cqn2odata.js +9 -7
  107. package/libx/odata/grammar.pegjs +157 -76
  108. package/libx/odata/index.js +9 -3
  109. package/libx/odata/parser.js +1 -1
  110. package/libx/odata/utils.js +39 -5
  111. package/libx/rest/RestAdapter.js +1 -2
  112. package/libx/rest/middleware/delete.js +4 -5
  113. package/libx/rest/middleware/parse.js +3 -2
  114. package/package.json +3 -3
  115. package/server.js +1 -1
  116. package/srv/extensibility-service.cds +6 -3
  117. package/srv/model-provider.cds +3 -1
  118. package/srv/model-provider.js +84 -104
  119. package/srv/mtx.js +7 -1
  120. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +0 -240
package/lib/ql/SELECT.js CHANGED
@@ -116,7 +116,7 @@ module.exports = class SELECT extends Whereable {
116
116
 
117
117
  limit (rows, offset) {
118
118
  if (is_number(rows) || rows) this.SELECT.limit = rows.rows ? rows : { rows: {val:rows} }
119
- if (is_number(offset)) this.SELECT.limit.offset = { val: offset }
119
+ if (is_number(offset)) (this.SELECT.limit = (this.SELECT.limit || {})) .offset = { val: offset }
120
120
  return this
121
121
  }
122
122
 
File without changes
@@ -104,6 +104,23 @@ class EventContext {
104
104
  || this.hasOwnProperty('locale') && this.locale // eslint-disable-line no-prototype-builtins
105
105
  }
106
106
 
107
+ get _features() {
108
+ return super._features = this._propagated._features || _features4 (this.http?.req?.features || this.user?.features || this.http?.req?.user?.features)
109
+ }
110
+ get features() {
111
+ return super.features = this._features || noFeatures
112
+ }
113
+ set features(v) {
114
+ super.features = _features4(v)
115
+ }
116
+
117
+ get model() {
118
+ return super.model = this._propagated.model || this.http?.req.__model // IMPORTANT: Never use that anywhere else
119
+ }
120
+ set model(m) {
121
+ super.model = m
122
+ }
123
+
107
124
  get timestamp() {
108
125
  return super.timestamp = this._propagated.timestamp || new Date
109
126
  }
@@ -151,18 +168,29 @@ class EventContext {
151
168
  }
152
169
  }
153
170
  get _tx() { return this.tx } // REVISIT: for compatibility to bade usages of req._tx
154
-
155
-
156
- /** REVISIT: remove -> @deprecated */
157
- set _model(m){ Object.defineProperty(this,'_model',{value:m}) }
158
- get _model() {
159
- return this._model = this.tx && this.tx.model || this._propagated._model
160
- }
161
171
  }
162
172
 
163
173
  const _TENANT_LOCALE = { tenant:1, locale:2 }
164
174
  const _anonymous = new cds.User.default
165
175
 
176
+ const _features4 = features => { // normalizes features to an object
177
+ if (!features) return
178
+ if (features === '*') return allFeatures
179
+ const o = (
180
+ Array.isArray(features) ? features.reduce((fts,f)=>{ fts[f] = true; return fts },{}) :
181
+ typeof features === 'object' ? Object.fromEntries (Object.entries(features).filter(([,v])=>v)) :
182
+ (''+features).split(',').reduce((fts,f)=>{ fts[f] = true; return fts },{})
183
+ )
184
+ return Object.defineProperty (o,'$hash',$hash)
185
+ }
186
+ const $hash = {
187
+ get() { return this.$hash = Object.keys(this).join(',') },
188
+ set(v){ Object.defineProperty(this,'$hash',{value:v}) },
189
+ configurable:true
190
+ }
191
+ const allFeatures = new Proxy ({'*':true},{ has:() => true, get:(_,p) => p === '$hash' ? '*' : true })
192
+ const noFeatures = {__proto__:{ $hash:'' }}
193
+
166
194
  EventContext.prototype._set('_propagated', Object.seal({}))
167
195
  EventContext.propagateHeaders = [ 'x-correlation-id' ]
168
196
  module.exports = EventContext
package/lib/req/locale.js CHANGED
@@ -4,7 +4,11 @@ const INCLUDE_LIST = i18n.preserved_locales.reduce((p,n)=>{
4
4
  p[n]=n; p[n.toUpperCase()]=n; return p
5
5
  },{
6
6
  en_US_x_saptrc: 'en_US_saptrc',
7
- en_US_x_sappsd: 'en_US_sappsd'
7
+ en_US_x_sappsd: 'en_US_sappsd',
8
+ en_US_x_saprigi: 'en_US_saprigi',
9
+ '1Q': 'en_US_saptrc',
10
+ '2Q': 'en_US_sappsd',
11
+ '3Q': 'en_US_saprigi'
8
12
  })
9
13
 
10
14
  const from_req = req => req && (
@@ -32,22 +32,11 @@ class ProtocolAdapter {
32
32
  /**
33
33
  * Mounts the adapter to an express app.
34
34
  */
35
- in (app, at) {
36
- const LOG = cds.log(), DEBUG = cds.debug('server')
37
- const srv = this.service, path = at || srv.path
38
- app.use (path+'/webapp/', (_,res)=> res.sendStatus(404))
39
- lib.auth (srv, app, srv.options)
40
- lib.perf (app)
41
- app.use (path, (req,_,next) => {
42
- if (req.user?.tenant && !cds.context?.tenant) cds.context = { user: req.user } // REVISIT: should move to auth middleware
43
- LOG && LOG (req.method, decodeURI(req.originalUrl), req.body||'')
44
- if (/\$batch/.test(req.url)) req.on ('dispatch', (req) => {
45
- LOG && LOG ('>', req.event, decodeURI(req._path), req._query||'')
46
- if (DEBUG && req.query) DEBUG (req.query)
47
- })
48
- next()
49
- })
50
- app.use (path, this)
35
+ in (app) {
36
+ const srv = this.service
37
+ app.use (srv.path+'/webapp/', (_,res)=> res.sendStatus(404))
38
+ const cds_context_model = require('./srv-models')
39
+ app.use (srv.path, logger, lib.perf, lib.auth(srv), cds_context_model.middleware4(srv), this)
51
40
  return srv
52
41
  }
53
42
 
@@ -72,10 +61,25 @@ class ProtocolAdapter {
72
61
 
73
62
  const _protocol4 = (srv) => {
74
63
  const {to} = srv.options; if (to) return to
75
- const def = srv.definition
76
- return !def ? default_protocol : def['@protocol'] || def['@rest'] && 'rest' || def['@odata'] && 'odata_v4' || default_protocol
64
+ return _protocol4Service(srv.definition)
77
65
  }
78
66
 
67
+ const _protocol4Service = (service) => {
68
+ return !service ? default_protocol : service['@protocol'] || service['@rest'] && 'rest' || service['@odata'] && 'odata_v4' || default_protocol
69
+ }
70
+
71
+
72
+ const LOG = cds.log(), DEBUG = cds.debug('server')
73
+ const logger = function cap_req_logger (req,_,next) {
74
+ LOG && LOG (req.method, decodeURI(req.originalUrl), req.body||'')
75
+ if (/\$batch/.test(req.url)) req.on ('dispatch', (req) => {
76
+ LOG && LOG ('>', req.event, decodeURI(req._path), req._query||'')
77
+ if (DEBUG && req.query) DEBUG (req.query)
78
+ })
79
+ next()
80
+ }
81
+
82
+
79
83
  const default_protocol = 'odata_v4'
80
84
  const _prototype = Object.getOwnPropertyDescriptors (ProtocolAdapter.prototype)
81
- module.exports = { ProtocolAdapter }
85
+ module.exports = { ProtocolAdapter, _protocol4Service }
File without changes
@@ -18,7 +18,7 @@ const connect = module.exports = async function cds_connect (options) {
18
18
  * or with options configured in cds.env.requires.<datasource>.
19
19
  * @param {string} [datasource]
20
20
  * @param {{ kind?:String, impl?:String }} [options]
21
- * @returns { Promise<import('../serve/Service-api')> }
21
+ * @returns { Promise<import('./srv-api')> }
22
22
  */
23
23
  connect.to = async (datasource, options) => {
24
24
  let Service = cds.service.factory, _done = x=>x
File without changes
@@ -2,7 +2,7 @@ const cds = require('..'), { path, isfile } = cds.utils
2
2
  const paths = Array.from (new Set ([ cds.root, ...require.resolve.paths('x') ]))
3
3
  const DEBUG = cds.debug('srv.factory'); DEBUG && DEBUG ({ 'cds.root':cds.root, paths })
4
4
 
5
- /** @typedef {import('./Service-api')} Service @type { (()=>Service) & (new()=>Service) } */
5
+ /** @typedef {import('./srv-api')} Service @type { (()=>Service) & (new()=>Service) } */
6
6
  const ServiceFactory = function (name, model, options) { //NOSONAR
7
7
 
8
8
  const o = { ...options } // avoid changing shared options
@@ -1,13 +1,13 @@
1
1
  const cds = require('..'), { Event, Request } = cds
2
- const add_methods_to = require ('./Service-methods')
2
+ const add_methods_to = require ('./srv-methods')
3
3
 
4
4
 
5
- class Service extends require('./Service-handlers') {
5
+ class Service extends require('./srv-handlers') {
6
6
 
7
7
  constructor (name, model, o) {
8
8
  if (is_object(name)) {
9
9
  [ model, o ] = [ name, model ]
10
- let srv = cds.linked(model).services[0] || cds.error.expected `${{arg0:name}} to be a CSN with a service definition`
10
+ let srv = cds.linked(model).services[0] || cds.error.expected `${{model}} passed as first argument to be a CSN with a single service definition`
11
11
  name = srv.name
12
12
  }
13
13
  super (name || new.target.name) .options = o || (o={})
@@ -94,6 +94,13 @@ class Service extends require('./Service-handlers') {
94
94
  get events() { return super.events = _reflect (this, d => d.kind === 'event') }
95
95
  get types() { return super.types = _reflect (this, d => !d.kind || d.kind === 'type') }
96
96
 
97
+ /**
98
+ * Flag to control whether this service is extensible.
99
+ * Can be overridden by subclasses.
100
+ * REVISIT cds.xt name check should move to respective services
101
+ */
102
+ get isExtensible() { return !this.name?.startsWith('cds.xt.') && this.model === cds.model}
103
+
97
104
  /**
98
105
  * Subclasses may override this to free private resources
99
106
  */
@@ -107,10 +114,11 @@ class Service extends require('./Service-handlers') {
107
114
 
108
115
  }
109
116
 
110
- const { dispatch, handle } = require('./Service-dispatch')
111
- Service.prototype.dispatch = dispatch
117
+ const { dispatch, handle } = require('./srv-dispatch')
118
+ Service.prototype.tx = require('./srv-tx')
112
119
  Service.prototype.handle = handle
113
- Service.prototype.transaction = Service.prototype.tx = require('./Transaction')
120
+ Service.prototype.dispatch = dispatch
121
+ Service.prototype.transaction = Service.prototype.tx
114
122
  Service.prototype._implicit_next = cds.env.features.implicit_next
115
123
  Service.prototype._is_service_instance = Service._is_service_class = true //> for factory
116
124
  module.exports = Service
@@ -3,7 +3,7 @@ const cds = require ('../index')
3
3
  /**
4
4
  * The default implementation of the `srv.dispatch(req)` ensures everything
5
5
  * is prepared before calling `srv.handle(req)`
6
- * @typedef {import('./Service-api')} Service
6
+ * @typedef {import('./srv-api')} Service
7
7
  * @typedef {import('../req/request')} Request
8
8
  * @this {Service}
9
9
  * @param {Request} req
@@ -24,7 +24,7 @@ exports.dispatch = async function dispatch (req) { //NOSONAR
24
24
 
25
25
  // Handle batches of queries
26
26
  if (_is_array(req.query))
27
- return Promise.all (req.query.map (q => this.dispatch ({query:q,__proto__:req,context:req})))
27
+ return Promise.all (req.query.map (q => this.dispatch ({query:q,__proto__:req,context:req.context})))
28
28
 
29
29
  // Ensure target and fqns
30
30
  if (!req.target) _ensure_target (this,req)
@@ -107,6 +107,7 @@ const _ensure_target = (srv,req) => {
107
107
 
108
108
  const _ensure_fqn = (x,p,srv, name = x[p]) => {
109
109
  if (typeof name === 'string') {
110
+ if (srv instanceof cds.DatabaseService) return
110
111
  if (srv.model && name in srv.model.definitions) return
111
112
  if (name.startsWith(srv.namespace)) return
112
113
  if (name.endsWith('_drafts')) return // REVISIT: rather fix test/fiori/localized-draft.test.js ?
@@ -1,4 +1,5 @@
1
1
  const cds = require('..'), {expected} = cds.error
2
+ const LOG = cds.log()
2
3
 
3
4
  class EventHandlers {
4
5
 
@@ -41,6 +42,15 @@ const _register = function (srv, phase, event, path, handler) { //NOSONAR
41
42
 
42
43
  if (!handler) [ handler, path ] = [ path ] // argument path is optional
43
44
  if (typeof handler !== 'function') expected `${{handler}} to be a function`
45
+ if (handler._is_stub) {
46
+ LOG.warn (`\n
47
+ WARNING: You are trying to register a frameworks-generated stub method for
48
+ custom action/function '${event}' in implementation of service '${srv.name}'.
49
+ We're ignoring that as we already registered the according handler.
50
+ Please fix your implementation, i.e., just don't register that handler.
51
+ `)
52
+ return srv
53
+ }
44
54
 
45
55
  // Canonicalize event argument
46
56
  if (!event || event === '*') event = undefined
@@ -3,19 +3,21 @@ const LOG = cds.log('cds-app-service-methods')
3
3
 
4
4
 
5
5
  module.exports = (srv) => {
6
- if (!( //> we only support that for app services
6
+ if (srv.model && ( //> we only support that for app services
7
7
  srv instanceof cds.ApplicationService ||
8
8
  srv instanceof cds.RemoteService ||
9
9
  srv._add_stub_methods
10
- )) return
11
- for (const each of srv.operations) {
12
- add_handler_for (srv, each)
13
- }
14
- for (const each of srv.entities) {
15
- for (const a in each.actions) {
16
- add_handler_for (srv, each.actions[a])
10
+ )) {
11
+ for (const each of srv.operations) {
12
+ add_handler_for (srv, each)
13
+ }
14
+ for (const each of srv.entities) {
15
+ for (const a in each.actions) {
16
+ add_handler_for (srv, each.actions[a])
17
+ }
17
18
  }
18
19
  }
20
+ return srv
19
21
  }
20
22
 
21
23
  const add_handler_for = (srv, def) => {
@@ -0,0 +1,206 @@
1
+ // REVISIT: all handler caches (incl. templates) need to go into cached models -> eviction
2
+ // REVISIT: all edmx caches also have to be hooked in here
3
+
4
+ const cds = require ('../index')
5
+ const LOG = cds.log()
6
+
7
+ const {normalizeError} = require('../../libx/_runtime/common/error/frontend')
8
+ const getError = require('../../libx/_runtime/common/error')
9
+
10
+ /**
11
+ * Implements a static cache for all tenant/features-specific models.
12
+ * Cache keys are strings of the form `<tenant>:<comma-separated-features>`.
13
+ * The base model `cds.model` is also in the cache with key `':'`, i.e.,
14
+ * with undefined tenant and no activated features.
15
+ */
16
+ class ExtendedModels {
17
+
18
+ /**
19
+ * Returns an express middleware to be used with the given srv to set cds.context.model.
20
+ * Uses `this.model4(...)` to get the respective tenant/features-specific models.
21
+ * @returns {(req,res)=>{}}
22
+ */
23
+ static middleware4 (srv) {
24
+ if (!(srv instanceof cds.ApplicationService)) return [] //> no middleware to add // REVISIT: move to `srv.isExtensible`
25
+ if (srv.name?.startsWith('cds.xt.')) return [] //> no middleware to add // REVISIT: move to `srv.isExtensible`
26
+ else return async (req,res,next) => {
27
+ if (!req.user?.tenant) return next()
28
+ const ctx = cds.context = cds.EventContext.for({ http: { req, res } })
29
+ ctx.user = req.user // REVISIT: should move to auth middleware?
30
+ try {
31
+ ctx.model = req.__model = await this.model4 (ctx.tenant, ctx.features)
32
+ } catch (e) {
33
+ LOG.error (Object.assign(e, { message: 'Unable to get service from service map due to error: ' + e.message }))
34
+
35
+ // return 503 to client
36
+ // REVISIT: Error handling!
37
+ const { error } = normalizeError(getError(Object.assign(e, { statusCode: 503 })), req)
38
+
39
+ return res.status(503).json({ error })
40
+ }
41
+ next()
42
+ }
43
+ }
44
+
45
+
46
+ /**
47
+ * Returns the model to use for given tenant and features.
48
+ * Loaded models are cached, with eviction on inactivity of tenants,
49
+ * and automatically refreshed when new extensions are made.
50
+ * @returns a CSN compiled.for.nodejs and cds.linked
51
+ */
52
+ static async model4 (tenant, features) {
53
+
54
+ const {cache} = ExtendedModels, key = cache.key4 (tenant, features)
55
+ const cached = cache.at(key); if (cached) return cached
56
+ if (key === ':') return cache.add (':', cds.compile.for.nodejs(cds.model))
57
+ else return cache[key] = (async()=>{ // temporarily add promise to cache to avoid race conditions...
58
+
59
+ // If tenant doesn't have extensions check cache with tenant = undefined
60
+ const _has_extensions = tenant && extensibility && await _is_extended(tenant)
61
+ if (!_has_extensions) {
62
+ let k = cache.key4 (tenant = undefined, features)
63
+ let cached = cache.at(k); if (cached) return cached
64
+ else if (k === ':') return cache.add (':', cds.compile.for.nodejs(cds.model))
65
+ }
66
+
67
+ // None cached -> obtain and cache specific model from ModelProvider
68
+ return await _get_model4 (tenant, Object.keys(features))
69
+
70
+ })()
71
+ .then (m => cache.add(key,m)) // replace promise in cache by real model
72
+ .catch (e => { delete cache[key]; throw e })
73
+ }
74
+
75
+
76
+ /**
77
+ * Constructs and returns a cache key for given tenant and features.
78
+ * @param {string} tenant string or `undefined` as obtained from `cds.context.tenant`
79
+ * @param {object} features object as obtained from `cds.context.features`
80
+ * @returns {string} of the form `<tenant>:<comma-separated-features>`
81
+ */
82
+ key4 (tenant, features) {
83
+ return `${tenant||''}:${features.$hash}`
84
+ }
85
+
86
+
87
+ /**
88
+ * Returns the currently cached model, or a promised one.
89
+ * Promises are added to the cache to avoid race conditions with parallel requests.
90
+ * This implementation regularly checks for new extensions, and transparently
91
+ * refreshes cached models if so.
92
+ * This method is overridden with a simple `return this[key]` when extensibility
93
+ * is switched off.
94
+ * @param {string} key as obtained through `cache.key4(t,f)`
95
+ * @returns { LinkedCSN | Promise<LinkedCSN> }
96
+ */
97
+ at (key) {
98
+ const model = this[key]; if (!model) return
99
+ if (model.then) return model //> promised model to avoid race conditions
100
+
101
+ const {_cached} = model, interval = ExtendedModels.checkInterval
102
+ if (Date.now() - _cached.touched < interval) return model //> checked recently
103
+
104
+ else return this[key] = (async()=>{ // temporarily replace cache entry by promise to avoid race conditions...
105
+
106
+ const has_new_extensions = await cds.db.exists('cds.xt.Extensions') .where ({
107
+ timestamp: { '>': new Date(_cached.touched).toISOString() } // REVISIT: better store epoc time in db?
108
+ // REVISIT: GAP: CAP runtime should allow Date objects + Date.now() for all date+time types !
109
+ })
110
+ if (has_new_extensions) { // new extensions arrived -> refresh model in cache
111
+ let [ tenant = undefined, toggles ] = key.split(':')
112
+ return _get_model4 (tenant, toggles.split(','))
113
+ } else { // no new extensions...
114
+ _cached.touched = Date.now() // check again in 1 min or so
115
+ return model // keep cached model in cache
116
+ }
117
+
118
+ })()
119
+ .then (m => this.add(key,m)) // replace promise in cache by real model
120
+ .catch (e => { delete this[key]; throw e })
121
+ }
122
+
123
+
124
+ /**
125
+ * Adds a model into the cache under the given key.
126
+ * Only use that method to add loaded models, while using direct assignment
127
+ * to add promises, e.g. `cache[key] = promised_model`.
128
+ * @param {string} key as obtained through `cache.key4(t,f)`
129
+ * @param {LinkedCSN} model the loaded and linked model
130
+ * @returns the given `model`
131
+ */
132
+ add (key, model, touched = Date.now()) {
133
+ if (model) {
134
+ if (!model._cached) Object.defineProperty (model,'_cached',{ value:{key,touched} })
135
+ return this[key] = model
136
+ }
137
+ }
138
+
139
+
140
+ /**
141
+ * When started, regularly evicts models for inactive tenants.
142
+ */
143
+ startSentinel(){
144
+ this.sentinel = setInterval (()=>{ for (let [key,m] of Object.entries(this)) {
145
+ if (!m._cached) continue // `m` can also be `this.sentinel`
146
+ if (Date.now() - m._cached.touched > ExtendedModels.sentinelInterval) {
147
+ delete this [key]
148
+ }
149
+ }}, ExtendedModels.sentinelInterval)
150
+ cds.on('shutdown', ()=> clearInterval(this.sentinel))
151
+ }
152
+
153
+
154
+ /** The cache instance used by `middleware4()` and `model4()`. */
155
+ static cache = new ExtendedModels
156
+
157
+ /** Time interval in ms to check for new extensions and refresh models, if so. */
158
+ static checkInterval = cds.requires.extensibility?.tenantCheckInterval || cds.mtx?.tenantCheckInterval || 1 * 60 * 1000
159
+
160
+ /** Time interval in ms after which to evict models for inactive tenants. */
161
+ static sentinelInterval = cds.requires.extensibility?.evictionInterval || 3600*1000
162
+
163
+ }
164
+ module.exports = ExtendedModels
165
+
166
+
167
+
168
+ // ---------------------------------------------------------------------------
169
+ // Support for old MTX
170
+
171
+ const old_mtx = cds.mtx
172
+ if (old_mtx) {
173
+ if (!cds.requires.extensibility) cds.requires.extensibility = true // REVISIT: extensibility was always true in old MTX?
174
+ ExtendedModels.prototype.at = function (key) { return this[key] }
175
+ old_mtx.eventEmitter.on (old_mtx.events.TENANT_UPDATED, async (tenant='') => {
176
+ delete ExtendedModels.cache [tenant+':']
177
+ })
178
+ }
179
+
180
+
181
+ // ---------------------------------------------------------------------------
182
+ // Optimizations for single-tenancy modes
183
+
184
+ const extensibility = cds.requires.extensibility
185
+ if (!extensibility) {
186
+ ExtendedModels.prototype.at = function (key) { return this[key] }
187
+ if (!cds.requires.toggles) ExtendedModels.middleware4 = ()=> []
188
+ }
189
+
190
+
191
+ // helper to get model for tenant/features
192
+ const _is_extended = old_mtx ? t => old_mtx.isExtended(t) : extensibility ? ()=> cds.db.exists('cds.xt.Extensions') : ()=> false
193
+ const _get_model4 = old_mtx ? async (tenant) => {
194
+ const isExtended = tenant && await old_mtx.isExtended(tenant) // REVISIT: avoid await
195
+ if (isExtended) return old_mtx.getCsn(tenant) .then (cds.compile.for.nodejs)
196
+ } : (tenant, toggles) => {
197
+ const { 'cds.xt.ModelProviderService':mps } = cds.services
198
+ return mps.getCsn (tenant, toggles) .then (cds.compile.for.nodejs)
199
+ }
200
+
201
+
202
+ // ---------------------------------------------------------------------------
203
+ // Optimizations for single-tenancy modes
204
+
205
+ if (cds.requires.multitenancy && typeof global.it === 'undefined') ExtendedModels.cache.startSentinel()
206
+ // REVISIT: how to do ^that^ correctly with jest?
@@ -5,7 +5,7 @@ class RootContext extends EventContext {}
5
5
  /**
6
6
  * This is the implementation of the `srv.tx(req)` method. It constructs
7
7
  * a new Transaction as a derivate of the `srv` (i.e. {__proto__:srv})
8
- * @returns { Transaction & import('./Service-api') }
8
+ * @returns { Transaction & import('./srv-api') }
9
9
  * @param { EventContext } ctx
10
10
  */
11
11
  module.exports = function tx (ctx,fn) { const srv = this
@@ -48,6 +48,7 @@ class Transaction {
48
48
  static for (srv,root) {
49
49
  let txs = root.transactions
50
50
  if (!txs) Object.defineProperty(root, 'transactions', {value: txs = new Map})
51
+ if (srv._real_srv) srv = srv._real_srv // REVISIT: srv._real_srv is a qirty hack for current Okra Adapters
51
52
  let tx = txs.get (srv)
52
53
  if (!tx) txs.set (srv, tx = new this (srv,root))
53
54
  return tx
@@ -58,6 +59,10 @@ class Transaction {
58
59
  const proto = new.target.prototype
59
60
  tx.commit = proto.commit.bind(tx)
60
61
  tx.rollback = proto.rollback.bind(tx)
62
+ if (srv.isExtensible) {
63
+ const m = cds.context?.model
64
+ if (m) tx.model = m
65
+ }
61
66
  return _init(tx)
62
67
  }
63
68
 
@@ -25,7 +25,7 @@ class Test extends require('./axios') {
25
25
  this.server = server
26
26
  this.url = url
27
27
  })
28
- try { return cds.exec (cmd, ...args, '--port','0') }
28
+ try { return cds.exec (cmd, ...args, ...(args.includes('--port') ? [] : ['--port', '0'])) }
29
29
  catch (e) { if (is_mocha) console.error(e) } // eslint-disable-line no-console
30
30
  })
31
31
 
@@ -97,7 +97,7 @@ function support_jest_and_mocha() {
97
97
  }
98
98
  after(()=>{
99
99
  delete global.cds
100
- for (let k in require.cache) delete require.cache[k]
100
+ for (let k in require.cache) delete require.cache[k] // REVISIT: Whay are we doing that?
101
101
  })
102
102
  } else if (is_jest) { // it's jest
103
103
  global.before = (msg,fn) => global.beforeAll(fn||msg)