@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.
Files changed (134) hide show
  1. package/CHANGELOG.md +153 -19
  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/build/util.js +6 -4
  19. package/bin/deploy/to-hana/cfUtil.js +7 -2
  20. package/bin/deploy/to-hana/hana.js +6 -3
  21. package/bin/serve.js +8 -13
  22. package/lib/compile/{index.js → cds-compile.js} +0 -0
  23. package/lib/compile/extend.js +15 -5
  24. package/lib/compile/minify.js +1 -15
  25. package/lib/compile/parse.js +1 -1
  26. package/lib/compile/resolve.js +2 -2
  27. package/lib/compile/to/srvinfo.js +6 -4
  28. package/lib/{deploy.js → dbs/cds-deploy.js} +8 -8
  29. package/lib/env/{index.js → cds-env.js} +1 -17
  30. package/lib/env/{requires.js → cds-requires.js} +24 -3
  31. package/lib/env/defaults.js +7 -1
  32. package/lib/env/schemas/cds-package.json +11 -0
  33. package/lib/env/schemas/cds-rc.json +605 -0
  34. package/lib/index.js +20 -17
  35. package/lib/log/{errors.js → cds-error.js} +1 -1
  36. package/lib/log/{index.js → cds-log.js} +0 -0
  37. package/lib/ql/SELECT.js +1 -1
  38. package/lib/ql/{index.js → cds-ql.js} +0 -0
  39. package/lib/req/cds-context.js +1 -1
  40. package/lib/req/context.js +35 -7
  41. package/lib/req/locale.js +5 -1
  42. package/lib/{serve → srv}/adapters.js +23 -19
  43. package/lib/{connect → srv}/bindings.js +0 -0
  44. package/lib/{connect/index.js → srv/cds-connect.js} +1 -1
  45. package/lib/{serve/index.js → srv/cds-serve.js} +1 -1
  46. package/lib/{serve → srv}/factory.js +2 -3
  47. package/lib/{serve/Service-api.js → srv/srv-api.js} +14 -6
  48. package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +3 -2
  49. package/lib/{serve/Service-handlers.js → srv/srv-handlers.js} +10 -0
  50. package/lib/{serve/Service-methods.js → srv/srv-methods.js} +10 -8
  51. package/lib/srv/srv-models.js +206 -0
  52. package/lib/{serve/Transaction.js → srv/srv-tx.js} +6 -1
  53. package/lib/utils/{tests.js → cds-test.js} +2 -2
  54. package/lib/utils/cds-utils.js +146 -0
  55. package/lib/utils/index.js +2 -136
  56. package/lib/utils/jest.js +43 -0
  57. package/lib/utils/resources/index.js +14 -24
  58. package/lib/utils/resources/tar.js +18 -41
  59. package/libx/_runtime/auth/index.js +13 -10
  60. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +9 -20
  61. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -4
  62. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +19 -7
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +8 -11
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -4
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -2
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +6 -19
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -4
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +2 -2
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -10
  70. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
  71. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -6
  72. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +8 -5
  73. package/libx/_runtime/cds-services/services/utils/differ.js +4 -0
  74. package/libx/_runtime/cds-services/util/errors.js +1 -29
  75. package/libx/_runtime/common/constants/events.js +1 -3
  76. package/libx/_runtime/common/i18n/messages.properties +2 -1
  77. package/libx/_runtime/common/perf/index.js +10 -15
  78. package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
  79. package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
  80. package/libx/_runtime/common/utils/template.js +1 -1
  81. package/libx/_runtime/db/Service.js +2 -14
  82. package/libx/_runtime/db/expand/expandCQNToJoin.js +28 -25
  83. package/libx/_runtime/db/generic/input.js +4 -0
  84. package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
  85. package/libx/_runtime/extensibility/activate.js +47 -47
  86. package/libx/_runtime/extensibility/add.js +19 -13
  87. package/libx/_runtime/extensibility/addExtension.js +17 -13
  88. package/libx/_runtime/extensibility/defaults.js +25 -30
  89. package/libx/_runtime/extensibility/linter/allowlist_checker.js +373 -0
  90. package/libx/_runtime/extensibility/linter/annotations_checker.js +113 -0
  91. package/libx/_runtime/extensibility/linter/checker_base.js +20 -0
  92. package/libx/_runtime/extensibility/linter/namespace_checker.js +180 -0
  93. package/libx/_runtime/extensibility/linter.js +32 -0
  94. package/libx/_runtime/extensibility/push.js +78 -21
  95. package/libx/_runtime/extensibility/service.js +29 -12
  96. package/libx/_runtime/extensibility/token.js +56 -0
  97. package/libx/_runtime/extensibility/validation.js +6 -9
  98. package/libx/_runtime/fiori/generic/activate.js +0 -4
  99. package/libx/_runtime/fiori/generic/edit.js +1 -9
  100. package/libx/_runtime/fiori/generic/new.js +3 -28
  101. package/libx/_runtime/fiori/generic/patch.js +6 -7
  102. package/libx/_runtime/fiori/generic/prepare.js +11 -18
  103. package/libx/_runtime/fiori/generic/read.js +11 -1
  104. package/libx/_runtime/fiori/utils/handler.js +0 -17
  105. package/libx/_runtime/hana/Service.js +0 -1
  106. package/libx/_runtime/hana/conversion.js +12 -1
  107. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +4 -3
  108. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -0
  109. package/libx/_runtime/hana/pool.js +6 -10
  110. package/libx/_runtime/hana/search2Contains.js +0 -5
  111. package/libx/_runtime/hana/search2cqn4sql.js +1 -0
  112. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +18 -19
  113. package/libx/_runtime/messaging/file-based.js +1 -0
  114. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  115. package/libx/_runtime/messaging/service.js +11 -6
  116. package/libx/_runtime/remote/utils/client.js +6 -2
  117. package/libx/_runtime/remote/utils/data.js +5 -0
  118. package/libx/_runtime/sqlite/Service.js +0 -1
  119. package/libx/odata/afterburner.js +79 -2
  120. package/libx/odata/cqn2odata.js +9 -7
  121. package/libx/odata/grammar.pegjs +161 -77
  122. package/libx/odata/index.js +9 -3
  123. package/libx/odata/parser.js +1 -1
  124. package/libx/odata/utils.js +39 -5
  125. package/libx/rest/RestAdapter.js +1 -2
  126. package/libx/rest/middleware/delete.js +4 -5
  127. package/libx/rest/middleware/parse.js +3 -2
  128. package/package.json +3 -3
  129. package/server.js +1 -1
  130. package/srv/extensibility-service.cds +6 -3
  131. package/srv/model-provider.cds +3 -1
  132. package/srv/model-provider.js +84 -104
  133. package/srv/mtx.js +7 -1
  134. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +0 -240
@@ -1,19 +1,40 @@
1
- /* eslint-disable no-console */
2
- const fs = require('fs')
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('../libx/_runtime/cds')
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
- const TEMP_DIR = fs.realpathSync(require('os').tmpdir())
24
+ module.exports = class ModelProviderService extends cds.ApplicationService {
13
25
 
14
- module.exports = class ModelProviderService extends cds.Service {
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.on('getCsn', req => _getCsn(req, false, this._nodejs_models))
28
- this.on('getExtCsn', req => _getExtCsn(req, this._nodejs_models))
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
- const csn = await _getCsn(req, false, this._nodejs_models)
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 require('fs').promises.readFile(require('path').resolve(cds.root, conf?.root || '', 'resources.tgz')) }
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 (!cds.requires.extensibility) return req.reject(400, 'Missing extensibility parameter')
52
- const rs = await run4 (req.data.tenant, SELECT('sources').from('cds.xt.Extensions'))
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 temp = await fs.promises.mkdtemp(`${TEMP_DIR}${path.sep}tar-`)
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(JSON.parse(row.sources)), temp, false)))
59
- return await packTarArchive(collectFiles(temp, ['.csv', '.properties']), temp)
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 (fs.promises.rm || fs.promises.rmdir)(temp, { recursive: true, force: true }).catch(()=>{})
90
+ await fs.rm (dir, { recursive: true, force: true }).catch(()=>{})
62
91
  }
63
92
  })
64
93
 
65
- this.on('isExtended', async req => _isExtended(req.data.tenant))
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 (!cds.requires.extensibility) return req.reject(400, 'Missing extensibility parameter')
69
- const exts = await run4 (req.data.tenant, SELECT('csn').from('cds.xt.Extensions'))
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
- this.invalidateCache = tenant => {
83
- for (const key in this._nodejs_models) {
84
- if (key.startsWith( `${tenant}:`)) {
85
- delete this._nodejs_models[key]
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 isExtended = await _isExtended(req.data.tenant)
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).then(cds.minify)
117
- if (isExtended) csn = await _addExtensions(csn, req.data.tenant)
118
- if (javaornode) csn = cds.compile.for[javaornode](csn)
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
- async function _getExtCsn (req, cache) {
129
- if (!cds.requires.extensibility) return req.reject(400, 'Missing extensibility parameter')
130
- return _getCsn (req, true, cache)
131
- }
132
- }
133
- }
134
-
135
-
136
- const run4 = (t,q) => t ? cds.tx({tenant:t}, tx => tx.run(q)) : q
137
-
138
- /** Add extensions if applicable and exist */
139
- const _addExtensions = async (csn, tenant) => {
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
- async function _isExtended (tenant) {
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) return cds.mtx?.in (cds.app) // old 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
- }