@sap/cds 6.0.2 → 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.
- package/CHANGELOG.md +153 -19
- package/apis/cds.d.ts +11 -7
- package/apis/log.d.ts +48 -0
- package/apis/ql.d.ts +72 -15
- package/bin/build/buildTaskHandler.js +5 -2
- package/bin/build/constants.js +4 -1
- package/bin/build/provider/buildTaskHandlerEdmx.js +11 -39
- package/bin/build/provider/buildTaskHandlerFeatureToggles.js +13 -32
- package/bin/build/provider/buildTaskHandlerInternal.js +56 -4
- package/bin/build/provider/buildTaskProviderInternal.js +22 -14
- package/bin/build/provider/hana/index.js +8 -7
- package/bin/build/provider/java/index.js +18 -8
- package/bin/build/provider/mtx/index.js +7 -4
- package/bin/build/provider/mtx/resourcesTarBuilder.js +64 -35
- package/bin/build/provider/mtx-extension/index.js +57 -0
- package/bin/build/provider/mtx-sidecar/index.js +46 -18
- package/bin/build/provider/nodejs/index.js +34 -13
- package/bin/build/util.js +6 -4
- package/bin/deploy/to-hana/cfUtil.js +7 -2
- package/bin/deploy/to-hana/hana.js +6 -3
- package/bin/serve.js +8 -13
- package/lib/compile/{index.js → cds-compile.js} +0 -0
- package/lib/compile/extend.js +15 -5
- package/lib/compile/minify.js +1 -15
- package/lib/compile/parse.js +1 -1
- package/lib/compile/resolve.js +2 -2
- package/lib/compile/to/srvinfo.js +6 -4
- package/lib/{deploy.js → dbs/cds-deploy.js} +8 -8
- package/lib/env/{index.js → cds-env.js} +1 -17
- package/lib/env/{requires.js → cds-requires.js} +24 -3
- package/lib/env/defaults.js +7 -1
- package/lib/env/schemas/cds-package.json +11 -0
- package/lib/env/schemas/cds-rc.json +605 -0
- package/lib/index.js +20 -17
- package/lib/log/{errors.js → cds-error.js} +1 -1
- package/lib/log/{index.js → cds-log.js} +0 -0
- package/lib/ql/SELECT.js +1 -1
- package/lib/ql/{index.js → cds-ql.js} +0 -0
- package/lib/req/cds-context.js +1 -1
- package/lib/req/context.js +35 -7
- package/lib/req/locale.js +5 -1
- package/lib/{serve → srv}/adapters.js +23 -19
- package/lib/{connect → srv}/bindings.js +0 -0
- package/lib/{connect/index.js → srv/cds-connect.js} +1 -1
- package/lib/{serve/index.js → srv/cds-serve.js} +1 -1
- package/lib/{serve → srv}/factory.js +2 -3
- package/lib/{serve/Service-api.js → srv/srv-api.js} +14 -6
- package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +3 -2
- package/lib/{serve/Service-handlers.js → srv/srv-handlers.js} +10 -0
- package/lib/{serve/Service-methods.js → srv/srv-methods.js} +10 -8
- package/lib/srv/srv-models.js +206 -0
- package/lib/{serve/Transaction.js → srv/srv-tx.js} +6 -1
- package/lib/utils/{tests.js → cds-test.js} +2 -2
- package/lib/utils/cds-utils.js +146 -0
- package/lib/utils/index.js +2 -136
- package/lib/utils/jest.js +43 -0
- package/lib/utils/resources/index.js +14 -24
- package/lib/utils/resources/tar.js +18 -41
- package/libx/_runtime/auth/index.js +13 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +9 -20
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +19 -7
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +8 -11
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +6 -19
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +8 -5
- package/libx/_runtime/cds-services/services/utils/differ.js +4 -0
- package/libx/_runtime/cds-services/util/errors.js +1 -29
- package/libx/_runtime/common/constants/events.js +1 -3
- package/libx/_runtime/common/i18n/messages.properties +2 -1
- package/libx/_runtime/common/perf/index.js +10 -15
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
- package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
- package/libx/_runtime/common/utils/template.js +1 -1
- package/libx/_runtime/db/Service.js +2 -14
- package/libx/_runtime/db/expand/expandCQNToJoin.js +28 -25
- package/libx/_runtime/db/generic/input.js +4 -0
- package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
- package/libx/_runtime/extensibility/activate.js +47 -47
- package/libx/_runtime/extensibility/add.js +19 -13
- package/libx/_runtime/extensibility/addExtension.js +17 -13
- package/libx/_runtime/extensibility/defaults.js +25 -30
- package/libx/_runtime/extensibility/linter/allowlist_checker.js +373 -0
- package/libx/_runtime/extensibility/linter/annotations_checker.js +113 -0
- package/libx/_runtime/extensibility/linter/checker_base.js +20 -0
- package/libx/_runtime/extensibility/linter/namespace_checker.js +180 -0
- package/libx/_runtime/extensibility/linter.js +32 -0
- package/libx/_runtime/extensibility/push.js +78 -21
- package/libx/_runtime/extensibility/service.js +29 -12
- package/libx/_runtime/extensibility/token.js +56 -0
- package/libx/_runtime/extensibility/validation.js +6 -9
- package/libx/_runtime/fiori/generic/activate.js +0 -4
- package/libx/_runtime/fiori/generic/edit.js +1 -9
- package/libx/_runtime/fiori/generic/new.js +3 -28
- package/libx/_runtime/fiori/generic/patch.js +6 -7
- package/libx/_runtime/fiori/generic/prepare.js +11 -18
- package/libx/_runtime/fiori/generic/read.js +11 -1
- package/libx/_runtime/fiori/utils/handler.js +0 -17
- package/libx/_runtime/hana/Service.js +0 -1
- package/libx/_runtime/hana/conversion.js +12 -1
- package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +4 -3
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -0
- package/libx/_runtime/hana/pool.js +6 -10
- package/libx/_runtime/hana/search2Contains.js +0 -5
- package/libx/_runtime/hana/search2cqn4sql.js +1 -0
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +18 -19
- package/libx/_runtime/messaging/file-based.js +1 -0
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/messaging/service.js +11 -6
- package/libx/_runtime/remote/utils/client.js +6 -2
- package/libx/_runtime/remote/utils/data.js +5 -0
- package/libx/_runtime/sqlite/Service.js +0 -1
- package/libx/odata/afterburner.js +79 -2
- package/libx/odata/cqn2odata.js +9 -7
- package/libx/odata/grammar.pegjs +161 -77
- package/libx/odata/index.js +9 -3
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +39 -5
- package/libx/rest/RestAdapter.js +1 -2
- package/libx/rest/middleware/delete.js +4 -5
- package/libx/rest/middleware/parse.js +3 -2
- package/package.json +3 -3
- package/server.js +1 -1
- package/srv/extensibility-service.cds +6 -3
- package/srv/model-provider.cds +3 -1
- package/srv/model-provider.js +84 -104
- package/srv/mtx.js +7 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +0 -240
|
@@ -60,7 +60,7 @@ exports.expected = ([,type], arg) => {
|
|
|
60
60
|
//
|
|
61
61
|
|
|
62
62
|
exports._duplicate_cds = (...locations) => {
|
|
63
|
-
const { local } = require('../utils')
|
|
63
|
+
const { local } = require('../utils/cds-utils')
|
|
64
64
|
throw error `Duplicate @sap/cds/common!
|
|
65
65
|
|
|
66
66
|
There are duplicate versions of @sap/cds loaded from these locations:
|
|
File without changes
|
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
|
package/lib/req/cds-context.js
CHANGED
|
@@ -30,7 +30,7 @@ module.exports = new class extends AsyncLocalStorage {
|
|
|
30
30
|
if (typeof o === 'function') [fn,o] = [o,fn] //> for compatibility
|
|
31
31
|
if (o instanceof EventContext) throw cds.error `The passed options must not be an instance of cds.EventContext.`
|
|
32
32
|
const fx = ()=>{
|
|
33
|
-
const tx = cds.tx(o) // create a new detached transaction for each run of the background job
|
|
33
|
+
const tx = cds.tx({...o}) // create a new detached transaction for each run of the background job
|
|
34
34
|
return cds._context.run (tx, ()=> Promise.resolve(fn(tx))
|
|
35
35
|
.then (tx.commit, e => tx.rollback(_error(e, cds)))
|
|
36
36
|
.then (res => Promise.all(em.listeners('succeeded').map(each => each(res))))
|
package/lib/req/context.js
CHANGED
|
@@ -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
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
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('
|
|
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
|
|
@@ -63,7 +63,7 @@ module.exports = function cds_serve (som, _options) { // NOSONAR
|
|
|
63
63
|
// skip all services marked to be ignored
|
|
64
64
|
d['@cds.ignore'] || d['@cds.serve.ignore'] ||
|
|
65
65
|
// skip external services, unless asked to mock them and unbound
|
|
66
|
-
d['@cds.external'] || required[d.name]?.external && (!o.mocked || required[d.name].credentials)
|
|
66
|
+
(d['@cds.external'] || required[d.name]?.external) && (!o.mocked || required[d.name].credentials)
|
|
67
67
|
))
|
|
68
68
|
if (services.length > 1 && o.at) {
|
|
69
69
|
throw cds.error `You cannot specify 'path' for multiple services`
|
|
@@ -1,9 +1,8 @@
|
|
|
1
|
-
const { pathToFileURL } = require('url') // on Windows, must use a file: URL for ESM import
|
|
2
1
|
const cds = require('..'), { path, isfile } = cds.utils
|
|
3
2
|
const paths = Array.from (new Set ([ cds.root, ...require.resolve.paths('x') ]))
|
|
4
3
|
const DEBUG = cds.debug('srv.factory'); DEBUG && DEBUG ({ 'cds.root':cds.root, paths })
|
|
5
4
|
|
|
6
|
-
/** @typedef {import('./
|
|
5
|
+
/** @typedef {import('./srv-api')} Service @type { (()=>Service) & (new()=>Service) } */
|
|
7
6
|
const ServiceFactory = function (name, model, options) { //NOSONAR
|
|
8
7
|
|
|
9
8
|
const o = { ...options } // avoid changing shared options
|
|
@@ -48,7 +47,7 @@ const _require = (it,d) => {
|
|
|
48
47
|
}
|
|
49
48
|
}
|
|
50
49
|
DEBUG && DEBUG({resolved})
|
|
51
|
-
return
|
|
50
|
+
return cds.utils._import(resolved)
|
|
52
51
|
}
|
|
53
52
|
|
|
54
53
|
const _function = (impl) => !_is_class(impl) ? impl : (srv) => {
|
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
const cds = require('..'), { Event, Request } = cds
|
|
2
|
-
const add_methods_to = require ('./
|
|
2
|
+
const add_methods_to = require ('./srv-methods')
|
|
3
3
|
|
|
4
4
|
|
|
5
|
-
class Service extends require('./
|
|
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 `${{
|
|
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('./
|
|
111
|
-
Service.prototype.
|
|
117
|
+
const { dispatch, handle } = require('./srv-dispatch')
|
|
118
|
+
Service.prototype.tx = require('./srv-tx')
|
|
112
119
|
Service.prototype.handle = handle
|
|
113
|
-
Service.prototype.
|
|
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('./
|
|
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 (
|
|
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
|
-
))
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
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('./
|
|
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)
|