@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.
- package/CHANGELOG.md +128 -18
- 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/deploy/to-hana/cfUtil.js +7 -2
- package/bin/serve.js +7 -4
- 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} +7 -6
- 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 +19 -16
- 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/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} +0 -0
- package/lib/{serve → srv}/factory.js +1 -1
- 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 -145
- 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 +7 -19
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -4
- 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 +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -3
- 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/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/new.js +0 -11
- 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/outbox/utils.js +1 -1
- package/libx/_runtime/messaging/service.js +11 -6
- 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 +157 -76
- 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
package/srv/model-provider.js
CHANGED
|
@@ -1,19 +1,40 @@
|
|
|
1
|
-
|
|
2
|
-
const
|
|
1
|
+
const { packTarArchive, unpackTarArchive } = require('../lib/utils/resources')
|
|
2
|
+
const { collectFiles } = require('../libx/_runtime/extensibility/utils')
|
|
3
|
+
const fs = require('fs').promises
|
|
4
|
+
const os = require('os')
|
|
3
5
|
const path = require('path')
|
|
4
6
|
|
|
5
|
-
const cds = require('../
|
|
7
|
+
const cds = require('../lib')
|
|
8
|
+
const conf = cds.requires['cds.xt.ModelProviderService'] || cds.requires.kinds['cds.xt.ModelProviderService']
|
|
9
|
+
const main = conf.root ? new class { //> we're running in sidecar -> use env of main app
|
|
10
|
+
get env() { return super.env = cds.env.for ('cds', this.root) }
|
|
11
|
+
get root() { return super.root = path.resolve (cds.root, conf.root) }
|
|
12
|
+
get requires() { return super.requires = this.env.requires }
|
|
13
|
+
cache = {} //> for cds.resolve()
|
|
14
|
+
} : { //> not in sidecar
|
|
15
|
+
requires: cds.requires,
|
|
16
|
+
root: cds.root,
|
|
17
|
+
env: cds.env,
|
|
18
|
+
}
|
|
19
|
+
const fts = main.env.features.folders
|
|
6
20
|
const DEBUG = cds.debug('mtx')
|
|
7
|
-
|
|
8
|
-
const { packTarArchive, unpackTarArchive } = require('../lib/utils/resources')
|
|
9
|
-
const { collectFiles } = require('../libx/_runtime/extensibility/utils')
|
|
21
|
+
if (DEBUG) cds.once('served', ()=> DEBUG ('model provider options:', conf))
|
|
10
22
|
|
|
11
23
|
|
|
12
|
-
|
|
24
|
+
module.exports = class ModelProviderService extends cds.ApplicationService {
|
|
13
25
|
|
|
14
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Overload `.on` to decorate handlers to set `cds.context.tenant` based on incoming arg `tenant`.
|
|
28
|
+
*/
|
|
29
|
+
on (event, handler) {
|
|
30
|
+
return super.on(event, (req) => {
|
|
31
|
+
if (req.data.tenant) cds.context = { tenant: req.data.tenant }
|
|
32
|
+
// REVISIT: might not be correct when called via ExtensibilityService.add(...)
|
|
33
|
+
//> const tenant = req.tenant || (req.user.is('internal-user') && req.data.tenant)
|
|
34
|
+
return handler(req)
|
|
35
|
+
})
|
|
36
|
+
}
|
|
15
37
|
|
|
16
|
-
get _add_stub_methods(){ return true }
|
|
17
38
|
init() {
|
|
18
39
|
|
|
19
40
|
// REVISIT: We should do the enforcement only in production
|
|
@@ -24,13 +45,20 @@ module.exports = class ModelProviderService extends cds.Service {
|
|
|
24
45
|
// if (!requires.some(r => req.user.is(r))) return req.reject({ code:403 })
|
|
25
46
|
// })
|
|
26
47
|
|
|
27
|
-
this.
|
|
28
|
-
|
|
48
|
+
this._in_sidecar = (conf.kind === 'in-sidecar') // REVISIT: eliminate this 'private API' if possible
|
|
49
|
+
|
|
50
|
+
this.on('getCsn', req => _getCsn(req))
|
|
51
|
+
this.on('getExtCsn', req => {
|
|
52
|
+
if (!main.requires.extensibility) return req.reject(400, 'Missing extensibility parameter')
|
|
53
|
+
return _getCsn (req, true)
|
|
54
|
+
})
|
|
55
|
+
|
|
29
56
|
this.on('getEdmx', async req => {
|
|
57
|
+
const { res } = req._; if (res) res.set('Content-Type', 'application/xml')
|
|
30
58
|
const { service, locale, flavor } = req.data
|
|
31
|
-
|
|
59
|
+
delete req.data.flavor // we need to delete the OData 'flavor' argument, as getCsn has a different CSN `flavor` argument
|
|
60
|
+
const csn = await _getCsn(req)
|
|
32
61
|
const edmx = cds.compile.to.edmx(csn, { service, flavor })
|
|
33
|
-
const {res} = req._; if (res) res.set('Content-Type', 'application/xml')
|
|
34
62
|
return cds.localize(csn, locale, edmx)
|
|
35
63
|
})
|
|
36
64
|
|
|
@@ -39,125 +67,77 @@ module.exports = class ModelProviderService extends cds.Service {
|
|
|
39
67
|
// REVISIT: Works only w/o encoding parameter. Default encoding is 'utf8'.
|
|
40
68
|
// try { return await cds.utils.read('resources.tgz') }
|
|
41
69
|
// root is defined in cds.requires, in case of the sidecar scenario it is set to "_main"
|
|
42
|
-
try { return await
|
|
70
|
+
try { return await fs.readFile (path.resolve (main.root, 'resources.tgz')) }
|
|
43
71
|
catch(e) { if (e.code !== 'ENOENT') throw e }
|
|
44
72
|
const files = Object.keys(await cds.deploy.resources(['*', cds.env.features.folders]))
|
|
45
73
|
if (!files.length) return req.reject(404)
|
|
46
74
|
if (req._.res) req._.res.set('content-type', 'application/octet-stream; charset=binary')
|
|
47
|
-
return packTarArchive(files, cds.root)
|
|
75
|
+
return packTarArchive (files, cds.root)
|
|
48
76
|
})
|
|
49
77
|
|
|
50
78
|
this.on('getExtResources', async req => {
|
|
51
|
-
if (!
|
|
52
|
-
const rs = await
|
|
79
|
+
if (!main.requires.extensibility) return req.reject(400, 'Missing extensibility parameter')
|
|
80
|
+
const rs = await SELECT('sources').from('cds.xt.Extensions')
|
|
53
81
|
if (!rs.length) return req.reject(404, 'Missing extensions')
|
|
54
82
|
if (rs.every(row => row.sources === null)) return req.reject(404, 'Extension resources not found')
|
|
55
83
|
if (req._.res) req._.res.set('content-type', 'application/octet-stream; charset=binary')
|
|
56
|
-
const
|
|
84
|
+
const tmp = await fs.realpath(os.tmpdir())
|
|
85
|
+
const dir = await fs.mkdtemp(tmp + path.sep + 'tar-') // REVISIT: hase to be exactly this folder -> weird
|
|
57
86
|
try {
|
|
58
|
-
await Promise.all (rs.map (row => unpackTarArchive(Buffer.from(
|
|
59
|
-
return await packTarArchive(collectFiles(
|
|
87
|
+
await Promise.all (rs.map (row => unpackTarArchive (Buffer.from(row.sources), dir, false))) // REVISIT: use pipe instead
|
|
88
|
+
return await packTarArchive (collectFiles (dir, ['.csv', '.properties']), dir) // REVISIT: better tar APIs -> collectFiles should not be required
|
|
60
89
|
} finally {
|
|
61
|
-
await
|
|
90
|
+
await fs.rm (dir, { recursive: true, force: true }).catch(()=>{})
|
|
62
91
|
}
|
|
63
92
|
})
|
|
64
93
|
|
|
65
|
-
this.on('isExtended', async req =>
|
|
94
|
+
this.on('isExtended', async req => {
|
|
95
|
+
if (!main.requires.extensibility) return false
|
|
96
|
+
if (!req.data.tenant && main.requires.multitenancy) return false
|
|
97
|
+
const one = await SELECT.one(1).from('cds.xt.Extensions')
|
|
98
|
+
return !!one
|
|
99
|
+
})
|
|
66
100
|
|
|
67
101
|
this.on('getExtensions', async req => {
|
|
68
|
-
if (!
|
|
69
|
-
|
|
70
|
-
if (!exts.length) return req.reject(404, 'Missing extensions')
|
|
71
|
-
|
|
72
|
-
const csn = { extensions: [], definitions: {} }
|
|
73
|
-
exts.forEach(ext => {
|
|
74
|
-
const extCsn = JSON.parse(ext.csn)
|
|
75
|
-
csn.extensions.push(...extCsn.extensions)
|
|
76
|
-
if (extCsn.definitions) csn.definitions = Object.assign(extCsn.definitions, csn.definitions)
|
|
77
|
-
})
|
|
78
|
-
|
|
79
|
-
return csn
|
|
102
|
+
if (!main.requires.extensibility) return req.reject(400, 'Missing extensibility parameter')
|
|
103
|
+
return await _getExtensions4(req.data.tenant) || req.reject(404, 'Missing extensions')
|
|
80
104
|
})
|
|
81
105
|
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Conf vars required by getCsn ------------------------------------------
|
|
91
|
-
// eslint-disable-next-line one-var
|
|
92
|
-
const conf = cds.env.requires['cds.xt.ModelProviderService'] || cds.env.requires.kinds['cds.xt.ModelProviderService']
|
|
93
|
-
DEBUG && DEBUG ('ModelProviderService options:', conf)
|
|
94
|
-
const options = {}
|
|
95
|
-
if (conf.root) {
|
|
96
|
-
options.root = path.resolve (cds.root, conf.root)
|
|
97
|
-
options.env = cds.env.for('cds', options.root)
|
|
98
|
-
options.cache = {}
|
|
99
|
-
}
|
|
100
|
-
const fts = cds.env.features.folders
|
|
101
|
-
if (conf.kind !== 'in-sidecar') this._nodejs_models = {}
|
|
106
|
+
/** Implementation for getCsn */
|
|
107
|
+
async function _getCsn (req, checkExt) {
|
|
108
|
+
const { tenant, toggles, base, flavor, for:javaornode } = req.data
|
|
109
|
+
const extensions = !base && await _getExtensions4 (req.data.tenant)
|
|
110
|
+
if (!extensions && checkExt) req.reject(404, 'Missing extensions')
|
|
102
111
|
|
|
103
|
-
/** The implementation for getCsn */
|
|
104
|
-
async function _getCsn (req, checkExt, cache) {
|
|
105
|
-
const { toggles, for:javaornode } = req.data
|
|
106
112
|
const features = !toggles ? [] : toggles === '*' || toggles.includes('*') ? [fts] : toggles.map (f => fts.replace('*',f))
|
|
107
|
-
const
|
|
108
|
-
if (checkExt && !isExtended) req.reject(404, 'Missing extensions')
|
|
109
|
-
const hash = `${isExtended ? req.data.tenant : undefined}:${features.join(',')}`
|
|
110
|
-
if (javaornode && cache && cache[hash]) return cache[hash]
|
|
111
|
-
|
|
112
|
-
const models = cds.resolve(['*',...features], options)
|
|
113
|
-
if (!models) return
|
|
113
|
+
const models = cds.resolve (['*',...features], main); if (!models) return
|
|
114
114
|
|
|
115
|
-
DEBUG && DEBUG ('loading models from', models)
|
|
116
|
-
let csn = await cds.load(models)
|
|
117
|
-
if (
|
|
118
|
-
if (
|
|
115
|
+
DEBUG && DEBUG ('loading models for', { tenant, toggles } ,'from', models.map (cds.utils.local))
|
|
116
|
+
let csn = await cds.load (models, { flavor, silent:true })
|
|
117
|
+
if (csn.meta?.flavor === 'inferred') csn = cds.minify (csn)
|
|
118
|
+
if (extensions) csn = cds.extend (csn) .with (extensions)
|
|
119
|
+
if (javaornode) csn = cds.compile.for[javaornode] (csn)
|
|
119
120
|
|
|
120
121
|
// Dirty hack for cds.localize in Node sidecar setup
|
|
121
|
-
Object.defineProperty(csn,'$sources',{ value:csn.$sources, enumerable:true })
|
|
122
|
-
|
|
123
|
-
if (javaornode && cache) cache[hash] = csn
|
|
124
|
-
|
|
122
|
+
Object.defineProperty (csn,'$sources',{ value:csn.$sources, enumerable:true })
|
|
125
123
|
return csn
|
|
126
124
|
}
|
|
127
125
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
const exts = await run4 (tenant, SELECT('csn').from('cds.xt.Extensions'))
|
|
141
|
-
if (!exts.length) return csn
|
|
126
|
+
/** Implementation for getExtensions */
|
|
127
|
+
async function _getExtensions4 (tenant) {
|
|
128
|
+
if (!main.requires.extensibility || !tenant && main.requires.multitenancy) return
|
|
129
|
+
const exts = await SELECT('csn').from('cds.xt.Extensions'); if (!exts.length) return
|
|
130
|
+
const merged = { extensions: [], definitions: {} }
|
|
131
|
+
for (let each of exts) {
|
|
132
|
+
let {definitions,extensions} = JSON.parse(each.csn)
|
|
133
|
+
if (definitions) Object.assign (merged.definitions, definitions)
|
|
134
|
+
if (extensions) merged.extensions.push (...extensions)
|
|
135
|
+
}
|
|
136
|
+
return merged
|
|
137
|
+
}
|
|
142
138
|
|
|
143
|
-
const all = { definitions: {}, extensions: [] }
|
|
144
|
-
for (const each of exts) {
|
|
145
|
-
const ext = JSON.parse(each.csn)
|
|
146
|
-
if (ext.definitions) Object.assign(all.definitions, ext.definitions)
|
|
147
|
-
if (ext.extensions) all.extensions.push(...ext.extensions)
|
|
148
139
|
}
|
|
149
|
-
const extended = cds.compile({
|
|
150
|
-
'base.csn': cds.compile.to.json(csn),
|
|
151
|
-
'ext.csn': cds.compile.to.json(all)
|
|
152
|
-
})
|
|
153
|
-
extended.$sources = csn.$sources // required to load resources like i18n later on
|
|
154
|
-
|
|
155
|
-
return extended
|
|
156
|
-
}
|
|
157
140
|
|
|
158
|
-
|
|
159
|
-
if (!cds.requires.extensibility) return false
|
|
160
|
-
if (!tenant && cds.requires.multitenancy) return false
|
|
161
|
-
const one = await run4 (tenant, SELECT.one(1).from('cds.xt.Extensions'))
|
|
162
|
-
return !!one
|
|
141
|
+
get isExtensible () { return false } // REVISIT: Do we want to keep this?
|
|
163
142
|
}
|
|
143
|
+
module.exports.prototype._add_stub_methods = true
|
package/srv/mtx.js
CHANGED
|
@@ -1,7 +1,13 @@
|
|
|
1
1
|
const cds = require ('../lib')
|
|
2
|
+
const DEBUG = cds.debug('mtx')
|
|
3
|
+
|
|
2
4
|
class MTXServices extends cds.Service { async init(){
|
|
3
|
-
if (cds.mtx)
|
|
5
|
+
if (cds.mtx) {
|
|
6
|
+
DEBUG && DEBUG ('bootstrapping old MTX...')
|
|
7
|
+
return cds.mtx.in (cds.app) // old mtx
|
|
8
|
+
}
|
|
4
9
|
// else...
|
|
10
|
+
DEBUG && DEBUG ('bootstrapping MTX services...')
|
|
5
11
|
let sources = []
|
|
6
12
|
if (cds.requires.multitenancy) {
|
|
7
13
|
if (!('cds.xt.DeploymentService' in cds.requires)) {
|
|
@@ -1,240 +0,0 @@
|
|
|
1
|
-
const cds = require('../../../cds')
|
|
2
|
-
const LOG = cds.log('odata')
|
|
3
|
-
|
|
4
|
-
const { normalizeError } = require('../../../common/error/frontend')
|
|
5
|
-
|
|
6
|
-
const _run4 = (tenant, query) => (tenant ? cds.tx({ tenant }, tx => tx.run(query)) : query)
|
|
7
|
-
const getError = require('../../../common/error')
|
|
8
|
-
|
|
9
|
-
// REVISIT: this is always active, even without mtx -> do that better
|
|
10
|
-
module.exports = class Dispatcher {
|
|
11
|
-
/**
|
|
12
|
-
* Constructs an Dispatcher for cds service.
|
|
13
|
-
* New OData services will be created.
|
|
14
|
-
*
|
|
15
|
-
* @param service
|
|
16
|
-
*/
|
|
17
|
-
constructor(service) {
|
|
18
|
-
this._serviceName = service.definition.name
|
|
19
|
-
this._options = service.options
|
|
20
|
-
|
|
21
|
-
this._extMap = new Map()
|
|
22
|
-
this._extMap.set(getModelHash(), createOdataService(service))
|
|
23
|
-
this._intervalMap = new Map()
|
|
24
|
-
this._intervalMap.set(undefined, Date.now())
|
|
25
|
-
|
|
26
|
-
if (cds.mtx) {
|
|
27
|
-
cds.mtx._nodejs_models = {}
|
|
28
|
-
cds.mtx.eventEmitter.on(cds.mtx.events.TENANT_UPDATED, async tenant => {
|
|
29
|
-
this._extMap.delete(getModelHash(tenant))
|
|
30
|
-
delete cds.mtx._nodejs_models[tenant]
|
|
31
|
-
})
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
this._tenantCheckInterval = cds.requires.extensibility && cds.requires.extensibility.tenantCheckInterval
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
_deleteService(tenant) {
|
|
38
|
-
const hash = getModelHash(tenant)
|
|
39
|
-
for (const entry of this._extMap.entries()) {
|
|
40
|
-
if (entry[0].startsWith(hash)) {
|
|
41
|
-
this._extMap.delete(entry[0])
|
|
42
|
-
}
|
|
43
|
-
}
|
|
44
|
-
this._intervalMap.delete(tenant)
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
async _checkTenantExt(tenant, last) {
|
|
48
|
-
try {
|
|
49
|
-
const rs = await _run4(tenant, SELECT(1).from('cds.xt.Extensions').where('timestamp >=', last))
|
|
50
|
-
if (!this._mps) this._mps = await cds.connect.to('cds.xt.ModelProviderService')
|
|
51
|
-
if (rs.length) {
|
|
52
|
-
this._deleteService(tenant)
|
|
53
|
-
this._mps.invalidateCache(tenant)
|
|
54
|
-
} else {
|
|
55
|
-
this._intervalMap.set(tenant, Date.now())
|
|
56
|
-
}
|
|
57
|
-
} catch (_) {
|
|
58
|
-
this._deleteService(tenant)
|
|
59
|
-
}
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
async _getService4Tenant(req) {
|
|
63
|
-
// here, req is express' req -> req.tenant not available
|
|
64
|
-
const tenant = req.user && req.user.tenant
|
|
65
|
-
const isExtended = await cds.mtx.isExtended(tenant) // REVISIT: avoid await
|
|
66
|
-
if (!isExtended) return false
|
|
67
|
-
|
|
68
|
-
let model = cds.mtx._nodejs_models[tenant]
|
|
69
|
-
if (!model) {
|
|
70
|
-
const raw = await cds.mtx.getCsn(tenant)
|
|
71
|
-
model = cds.mtx._nodejs_models[tenant] = cds.compile.for.nodejs(raw)
|
|
72
|
-
}
|
|
73
|
-
|
|
74
|
-
const service = await createNewService(this._serviceName, model, this._options)
|
|
75
|
-
service._cdsService._isExtended = true
|
|
76
|
-
return service
|
|
77
|
-
}
|
|
78
|
-
|
|
79
|
-
async _getService4(tenant, features) {
|
|
80
|
-
const model = await this._mps.getCsn(tenant, features || [], 'nodejs')
|
|
81
|
-
return createNewService(this._serviceName, model, this._options)
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
async _getService(tenant, features, hash, req) {
|
|
85
|
-
if (cds.mtx) {
|
|
86
|
-
const service = await this._getService4Tenant(req)
|
|
87
|
-
|
|
88
|
-
if (service) return service
|
|
89
|
-
|
|
90
|
-
return this._extMap.get(getModelHash())
|
|
91
|
-
}
|
|
92
|
-
|
|
93
|
-
if (!this._mps) this._mps = await cds.connect.to('cds.xt.ModelProviderService')
|
|
94
|
-
const hashBase = getModelHash(undefined, features)
|
|
95
|
-
if (tenant && tenant !== undefined && hash !== hashBase) {
|
|
96
|
-
const isExtended = cds.requires.extensibility && (await this._mps.isExtended(tenant))
|
|
97
|
-
if (isExtended) {
|
|
98
|
-
return this._getService4(tenant, features)
|
|
99
|
-
} else {
|
|
100
|
-
if (!this._extMap.has(hashBase)) {
|
|
101
|
-
this._extMap.set(hashBase, this._getService4(undefined, features))
|
|
102
|
-
this._intervalMap.set(tenant, Date.now())
|
|
103
|
-
}
|
|
104
|
-
return this._extMap.get(hashBase)
|
|
105
|
-
}
|
|
106
|
-
} else {
|
|
107
|
-
return this._getService4(undefined, features)
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async _handleError(err, hash, req, res) {
|
|
112
|
-
if (LOG._error) {
|
|
113
|
-
err.message = 'Unable to get service from service map due to error: ' + err.message
|
|
114
|
-
LOG.error(err)
|
|
115
|
-
}
|
|
116
|
-
// clear map entry
|
|
117
|
-
this._extMap.delete(hash)
|
|
118
|
-
// return 503 to client
|
|
119
|
-
err = getError(Object.assign(err, { statusCode: 503 }))
|
|
120
|
-
const { error } = normalizeError(err, req)
|
|
121
|
-
|
|
122
|
-
return res.status(503).send({ error })
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
_hasModelProvider() {
|
|
126
|
-
return 'cds.xt.ModelProviderService' in cds.services || 'cds.xt.ModelProviderService' in cds.requires
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* Dispatch request in case of extensibility to other odata adapters.
|
|
131
|
-
*
|
|
132
|
-
* @param req
|
|
133
|
-
* @param res
|
|
134
|
-
* @private
|
|
135
|
-
* @returns {Promise}
|
|
136
|
-
*/
|
|
137
|
-
async dispatch(req, res) {
|
|
138
|
-
// here, req is express' req -> req.tenant not available
|
|
139
|
-
// REVISIT: rethink authentication
|
|
140
|
-
const tenant = req.user && req.user.tenant
|
|
141
|
-
|
|
142
|
-
// single tenant w/o features
|
|
143
|
-
if (!cds.mtx && !this._hasModelProvider()) {
|
|
144
|
-
const service = this._extMap.get(getModelHash())
|
|
145
|
-
return service.process(req, res)
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
// old mtx does not support feature toggles
|
|
149
|
-
const features = cds.mtx ? [] : _features4(req.features || req.user.features)
|
|
150
|
-
const hash = getModelHash(tenant, features)
|
|
151
|
-
|
|
152
|
-
// check for extensions
|
|
153
|
-
if (cds.requires.extensibility && !this._inCheckTenant) {
|
|
154
|
-
this._inCheckTenant = true
|
|
155
|
-
if (this._intervalMap.has(tenant)) {
|
|
156
|
-
const last = this._intervalMap.get(tenant)
|
|
157
|
-
if (Date.now() - last > this._tenantCheckInterval)
|
|
158
|
-
await this._checkTenantExt(tenant, new Date(last).toISOString())
|
|
159
|
-
}
|
|
160
|
-
this._inCheckTenant = false
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
// set promise into the map to avoid conflicts
|
|
164
|
-
if (!this._extMap.has(hash)) {
|
|
165
|
-
this._extMap.set(hash, this._getService(tenant, features, hash, req))
|
|
166
|
-
this._intervalMap.set(tenant, Date.now())
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
let service
|
|
170
|
-
try {
|
|
171
|
-
service = await this._extMap.get(hash)
|
|
172
|
-
} catch (err) {
|
|
173
|
-
return this._handleError(err, hash, req, res)
|
|
174
|
-
}
|
|
175
|
-
|
|
176
|
-
service.process(req, res)
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Return service middleware, which can be used by node server, express, connect, ...
|
|
181
|
-
*
|
|
182
|
-
* @returns {Function}
|
|
183
|
-
*/
|
|
184
|
-
getService() {
|
|
185
|
-
return (req, res) => {
|
|
186
|
-
this.dispatch(req, res)
|
|
187
|
-
}
|
|
188
|
-
}
|
|
189
|
-
}
|
|
190
|
-
|
|
191
|
-
// -----------------------------------------------------
|
|
192
|
-
// Private utils...
|
|
193
|
-
|
|
194
|
-
const OData = require('./OData')
|
|
195
|
-
const DEBUG = cds.debug('extensibility')
|
|
196
|
-
|
|
197
|
-
const { alias2ref } = require('../../../common/utils/csn')
|
|
198
|
-
|
|
199
|
-
function createOdataService(service) {
|
|
200
|
-
const name = (service.definition && service.definition.name) || service.name
|
|
201
|
-
const edm = cds.compile.to.edm(service.model, { service: name })
|
|
202
|
-
alias2ref(service, edm)
|
|
203
|
-
|
|
204
|
-
const odataService = new OData(edm, service.model, service.options)
|
|
205
|
-
odataService.addCDSServiceToChannel(service)
|
|
206
|
-
|
|
207
|
-
return odataService
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
async function createNewService(name, model, options) {
|
|
211
|
-
const { constructor: Service, path } = cds.services[name]
|
|
212
|
-
const service = new Service(name, model, { ...options }) // cloning options to be safe
|
|
213
|
-
if (service.init) await service.prepend(service.init)
|
|
214
|
-
if (options.impl) await service.prepend(options.impl)
|
|
215
|
-
if (path) service.path = path
|
|
216
|
-
DEBUG &&
|
|
217
|
-
DEBUG('Created tenant-specific service:', service.name, '= new', Service.name, {
|
|
218
|
-
_handlers: {
|
|
219
|
-
on: service._handlers.on.map(h => ({ on: h.on, handler: () => {} }))
|
|
220
|
-
}
|
|
221
|
-
})
|
|
222
|
-
return createOdataService(service)
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
const getModelHash = (tenant, features) => {
|
|
226
|
-
// ignore tenant in single tenant mode - use default (undefined) hash
|
|
227
|
-
const hash = cds.requires.multitenancy ? `${tenant}:` : 'undefined:'
|
|
228
|
-
return !features ? hash : hash + features.join(';')
|
|
229
|
-
}
|
|
230
|
-
|
|
231
|
-
const _features4 = features => {
|
|
232
|
-
// ensure features is an array
|
|
233
|
-
if (!features) return []
|
|
234
|
-
if (Array.isArray(features)) return features
|
|
235
|
-
if (typeof features === 'string') return features.split(',')
|
|
236
|
-
if (typeof features === 'object')
|
|
237
|
-
return Object.keys(features)
|
|
238
|
-
.filter(k => features[k])
|
|
239
|
-
.sort()
|
|
240
|
-
}
|