@sap/cds 6.0.4 → 6.1.2

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 (139) hide show
  1. package/CHANGELOG.md +180 -18
  2. package/apis/cds.d.ts +11 -7
  3. package/apis/log.d.ts +124 -0
  4. package/apis/ql.d.ts +72 -15
  5. package/apis/services.d.ts +13 -2
  6. package/bin/build/buildTaskHandler.js +5 -2
  7. package/bin/build/constants.js +4 -1
  8. package/bin/build/provider/buildTaskHandlerEdmx.js +11 -39
  9. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +14 -34
  10. package/bin/build/provider/buildTaskHandlerInternal.js +56 -4
  11. package/bin/build/provider/buildTaskProviderInternal.js +22 -14
  12. package/bin/build/provider/hana/index.js +12 -9
  13. package/bin/build/provider/java/index.js +18 -8
  14. package/bin/build/provider/mtx/index.js +7 -4
  15. package/bin/build/provider/mtx/resourcesTarBuilder.js +60 -35
  16. package/bin/build/provider/mtx-extension/index.js +57 -0
  17. package/bin/build/provider/mtx-sidecar/index.js +46 -18
  18. package/bin/build/provider/nodejs/index.js +34 -13
  19. package/bin/deploy/to-hana/cfUtil.js +7 -2
  20. package/bin/deploy/to-hana/hana.js +20 -25
  21. package/bin/deploy/to-hana/hdiDeployUtil.js +13 -2
  22. package/bin/serve.js +7 -4
  23. package/lib/compile/{index.js → cds-compile.js} +0 -0
  24. package/lib/compile/extend.js +15 -5
  25. package/lib/compile/minify.js +1 -15
  26. package/lib/compile/parse.js +1 -1
  27. package/lib/compile/resolve.js +2 -2
  28. package/lib/compile/to/srvinfo.js +6 -4
  29. package/lib/{deploy.js → dbs/cds-deploy.js} +9 -8
  30. package/lib/env/{index.js → cds-env.js} +1 -17
  31. package/lib/env/{requires.js → cds-requires.js} +24 -3
  32. package/lib/env/defaults.js +7 -1
  33. package/lib/env/schemas/cds-package.json +11 -0
  34. package/lib/env/schemas/cds-rc.json +614 -0
  35. package/lib/index.js +19 -16
  36. package/lib/log/{errors.js → cds-error.js} +1 -1
  37. package/lib/log/{index.js → cds-log.js} +0 -0
  38. package/lib/log/format/kibana.js +19 -1
  39. package/lib/ql/Query.js +9 -3
  40. package/lib/ql/SELECT.js +2 -2
  41. package/lib/ql/UPDATE.js +2 -2
  42. package/lib/ql/{index.js → cds-ql.js} +4 -10
  43. package/lib/req/context.js +49 -17
  44. package/lib/req/locale.js +5 -1
  45. package/lib/{serve → srv}/adapters.js +23 -19
  46. package/lib/{connect → srv}/bindings.js +0 -0
  47. package/lib/{connect/index.js → srv/cds-connect.js} +1 -1
  48. package/lib/{serve/index.js → srv/cds-serve.js} +0 -0
  49. package/lib/{serve → srv}/factory.js +1 -1
  50. package/lib/{serve/Service-api.js → srv/srv-api.js} +22 -6
  51. package/lib/{serve/Service-dispatch.js → srv/srv-dispatch.js} +13 -8
  52. package/lib/{serve/Service-handlers.js → srv/srv-handlers.js} +10 -0
  53. package/lib/{serve/Service-methods.js → srv/srv-methods.js} +10 -8
  54. package/lib/srv/srv-models.js +207 -0
  55. package/lib/{serve/Transaction.js → srv/srv-tx.js} +57 -40
  56. package/lib/utils/{tests.js → cds-test.js} +2 -2
  57. package/lib/utils/cds-utils.js +146 -0
  58. package/lib/utils/index.js +2 -145
  59. package/lib/utils/jest.js +43 -0
  60. package/lib/utils/resources/index.js +15 -25
  61. package/lib/utils/resources/tar.js +18 -41
  62. package/libx/_runtime/auth/index.js +14 -11
  63. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +7 -19
  64. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -4
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -4
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -4
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +2 -2
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +6 -19
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +3 -5
  71. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +1 -1
  72. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -4
  73. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +0 -2
  74. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +3 -1
  75. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +38 -4
  76. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +1 -3
  77. package/libx/_runtime/cds-services/services/utils/differ.js +4 -0
  78. package/libx/_runtime/cds-services/util/errors.js +1 -29
  79. package/libx/_runtime/common/i18n/messages.properties +2 -1
  80. package/libx/_runtime/common/perf/index.js +10 -15
  81. package/libx/_runtime/common/utils/binary.js +3 -4
  82. package/libx/_runtime/common/utils/cqn2cqn4sql.js +0 -1
  83. package/libx/_runtime/common/utils/entityFromCqn.js +8 -5
  84. package/libx/_runtime/common/utils/keys.js +14 -6
  85. package/libx/_runtime/common/utils/resolveView.js +1 -1
  86. package/libx/_runtime/common/utils/template.js +1 -1
  87. package/libx/_runtime/db/Service.js +2 -14
  88. package/libx/_runtime/db/expand/expandCQNToJoin.js +28 -25
  89. package/libx/_runtime/db/expand/rawToExpanded.js +7 -6
  90. package/libx/_runtime/db/generic/input.js +8 -1
  91. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
  92. package/libx/_runtime/db/sql-builder/SelectBuilder.js +37 -18
  93. package/libx/_runtime/extensibility/activate.js +47 -47
  94. package/libx/_runtime/extensibility/add.js +22 -13
  95. package/libx/_runtime/extensibility/addExtension.js +17 -13
  96. package/libx/_runtime/extensibility/defaults.js +25 -30
  97. package/libx/_runtime/extensibility/handler/transformREAD.js +20 -18
  98. package/libx/_runtime/extensibility/linter/allowlist_checker.js +373 -0
  99. package/libx/_runtime/extensibility/linter/annotations_checker.js +113 -0
  100. package/libx/_runtime/extensibility/linter/checker_base.js +20 -0
  101. package/libx/_runtime/extensibility/linter/namespace_checker.js +180 -0
  102. package/libx/_runtime/extensibility/linter.js +32 -0
  103. package/libx/_runtime/extensibility/push.js +77 -20
  104. package/libx/_runtime/extensibility/service.js +29 -12
  105. package/libx/_runtime/extensibility/token.js +57 -0
  106. package/libx/_runtime/extensibility/utils.js +8 -6
  107. package/libx/_runtime/extensibility/validation.js +6 -9
  108. package/libx/_runtime/fiori/generic/new.js +0 -11
  109. package/libx/_runtime/fiori/utils/where.js +1 -1
  110. package/libx/_runtime/hana/Service.js +0 -1
  111. package/libx/_runtime/hana/conversion.js +12 -1
  112. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +4 -3
  113. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -0
  114. package/libx/_runtime/hana/pool.js +6 -10
  115. package/libx/_runtime/hana/search2Contains.js +0 -5
  116. package/libx/_runtime/hana/search2cqn4sql.js +1 -0
  117. package/libx/_runtime/messaging/common-utils/authorizedRequest.js +1 -1
  118. package/libx/_runtime/messaging/enterprise-messaging-utils/EMManagement.js +1 -2
  119. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  120. package/libx/_runtime/messaging/service.js +11 -6
  121. package/libx/_runtime/remote/utils/data.js +5 -0
  122. package/libx/_runtime/sqlite/Service.js +7 -6
  123. package/libx/_runtime/sqlite/execute.js +41 -28
  124. package/libx/odata/afterburner.js +79 -2
  125. package/libx/odata/cqn2odata.js +15 -9
  126. package/libx/odata/grammar.pegjs +157 -76
  127. package/libx/odata/index.js +9 -3
  128. package/libx/odata/parser.js +1 -1
  129. package/libx/odata/utils.js +39 -5
  130. package/libx/rest/RestAdapter.js +3 -7
  131. package/libx/rest/middleware/delete.js +4 -5
  132. package/libx/rest/middleware/parse.js +3 -2
  133. package/package.json +3 -3
  134. package/server.js +1 -1
  135. package/srv/extensibility-service.cds +6 -3
  136. package/srv/model-provider.cds +3 -1
  137. package/srv/model-provider.js +86 -106
  138. package/srv/mtx.js +7 -1
  139. package/libx/_runtime/cds-services/adapter/odata-v4/Dispatcher.js +0 -240
@@ -0,0 +1,43 @@
1
+ let vm = require('vm')
2
+ if (!vm.__caching__) {
3
+ vm.__caching__ = true
4
+ const scriptCache = {}
5
+ vm._Script = vm._Script || vm.Script
6
+ class CachedScript extends vm._Script {
7
+ constructor(src, options) {
8
+ const cached = scriptCache[options.filename]
9
+ if (cached) {
10
+ return cached
11
+ }
12
+ super(src, options)
13
+ // Assume that .test.js files are only loaded once
14
+ if (!options.filename.endsWith('.test.js')) {
15
+ scriptCache[options.filename] = this
16
+ }
17
+ }
18
+ }
19
+
20
+ vm.Script = CachedScript
21
+ }
22
+ vm = undefined
23
+
24
+ let asyncHooks = require('async_hooks')
25
+ if (!asyncHooks._AsyncLocalStorage) {
26
+ asyncHooks._AsyncLocalStorage = asyncHooks.AsyncLocalStorage
27
+ class TmpAsyncLocalStorage extends asyncHooks._AsyncLocalStorage {
28
+ disable() {
29
+ if (this._timer) clearTimeout(this._timer)
30
+ return super.disable()
31
+ }
32
+
33
+ _enable() {
34
+ if (this._timer) clearTimeout(this._timer)
35
+ this._timer = setTimeout(() => this.disable(), 60 * 1000)
36
+ this._timer.unref()
37
+ return super._enable()
38
+ }
39
+ }
40
+
41
+ asyncHooks.AsyncLocalStorage = TmpAsyncLocalStorage
42
+ }
43
+ asyncHooks = null
@@ -1,54 +1,44 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
3
 
4
- const cds = require('../../../libx/_runtime/cds')
5
- const { packArchive, packArchiveCLI, unpackArchive, unpackArchiveCLI } = require('./tar')
4
+ const { packArchiveCLI, unpackArchiveCLI } = require('./tar')
6
5
  const { exists } = require('./utils')
7
6
 
8
- // use tar command line interface
9
7
  const TEMP_DIR = fs.realpathSync(require('os').tmpdir())
10
8
 
11
- const packTarArchive = async (files, root, flat = false, cli = true) => {
12
- let tgzBuffer, temp
9
+ const packTarArchive = async (files, root, flat = false) => {
10
+ if (typeof files === 'string') return await packArchiveCLI(files)
13
11
 
12
+ let tgzBuffer, temp
14
13
  try {
15
14
  temp = await fs.promises.mkdtemp(`${TEMP_DIR}${path.sep}tar-`)
16
- if (flat) {
17
- const files_ = []
18
- for (const file of files) {
19
- const destination = path.join(temp, path.basename(file))
20
- await fs.promises.copyFile(file, destination)
21
- files_.push(destination)
22
- }
23
- files = files_
24
- root = temp
15
+ for (const file of files) {
16
+ const fname = flat ? path.basename(file) : path.relative(root, file)
17
+ const destination = path.join(temp, fname)
18
+ const dirname = path.dirname(destination)
19
+ if (!await exists(dirname)) await fs.promises.mkdir(dirname, { recursive: true })
20
+ await fs.promises.copyFile(file, destination)
25
21
  }
26
22
 
27
- const relativeFiles = files.map(file => path.relative(root, file))
28
- if (cli) {
29
- const output = path.relative(root, path.join(temp, `${cds.utils.uuid()}.tgz`))
30
- tgzBuffer = await packArchiveCLI(relativeFiles, root, output)
31
- } else {
32
- tgzBuffer = await packArchive(relativeFiles, root)
33
- }
23
+ tgzBuffer = await packArchiveCLI(temp)
34
24
  } finally {
35
25
  if (await exists(temp)) {
36
- await (fs.promises.rm || fs.promises.rmdir)(temp, { recursive: true, force: true })
26
+ await fs.promises.rm(temp, { recursive: true, force: true })
37
27
  }
38
28
  }
39
29
 
40
30
  return tgzBuffer
41
31
  }
42
32
 
43
- const unpackTarArchive = async (buffer, folder, cli = true) => {
33
+ const unpackTarArchive = async (buffer, folder) => {
44
34
  const temp = await fs.promises.mkdtemp(`${TEMP_DIR}${path.sep}tar-`)
45
35
  const tgz = path.join(temp, 'resources.tgz')
46
36
  await fs.promises.writeFile(tgz, Buffer.from(buffer), 'binary')
47
37
 
48
38
  try {
49
- cli ? await unpackArchiveCLI(tgz, folder) : await unpackArchive(tgz, folder)
39
+ await unpackArchiveCLI(tgz, folder)
50
40
  } finally {
51
- if (await exists(temp)) await (fs.promises.rm || fs.promises.rmdir)(temp, { recursive: true, force: true })
41
+ if (await exists(temp)) await fs.promises.rm(temp, { recursive: true, force: true })
52
42
  }
53
43
  }
54
44
 
@@ -1,21 +1,13 @@
1
1
  const fs = require('fs')
2
2
  const path = require('path')
3
3
  const exec = require('child_process').exec
4
+ const cds = require('../../../libx/_runtime/cds')
5
+ const { exists } = require('./utils')
4
6
 
5
- const _createArchive = (files, root) => {
6
- const tar = require('tar')
7
- return tar.c(
8
- {
9
- gzip: true,
10
- preservePaths: false,
11
- cwd: root
12
- },
13
- files
14
- )
15
- }
7
+ const TEMP_DIR = fs.realpathSync(require('os').tmpdir())
16
8
 
17
- const _createArchiveCLI = (files, root, output) => {
18
- const cmd = 'cd ' + root + ' && tar -czf ' + output + ' ' + files.join(' ')
9
+ const _createArchiveCLI = (root, output) => {
10
+ const cmd = 'tar -zcvf ' + output + ' -C ' + root + ' .'
19
11
 
20
12
  return new Promise((resolve, reject) => {
21
13
  exec(cmd, err => {
@@ -25,32 +17,19 @@ const _createArchiveCLI = (files, root, output) => {
25
17
  })
26
18
  }
27
19
 
28
- const packArchive = (files, root) => {
29
- _createArchive(files, root)
30
- const stream = _createArchive(files, root)
31
- return new Promise((resolve, reject) => {
32
- const chunks = []
33
- stream.on('data', data => chunks.push(data))
34
- stream.on('error', error => reject(error))
35
- stream.on('end', () => {
36
- resolve(Buffer.concat(chunks))
37
- })
38
- })
39
- }
40
-
41
- const packArchiveCLI = async (files, root, output) => {
42
- await _createArchiveCLI(files, root, output)
43
-
44
- return fs.promises.readFile(path.join(root, output))
45
- }
46
-
47
- const unpackArchive = async (tgz, folder) => {
48
- const tar = require('tar')
49
- await tar.x({
50
- file: tgz,
51
- gzip: true,
52
- C: folder
53
- })
20
+ const packArchiveCLI = async (root) => {
21
+ const temp = await fs.promises.mkdtemp(`${TEMP_DIR}${path.sep}tar-`)
22
+ const output = path.join(temp, `${cds.utils.uuid()}.tgz`)
23
+ try {
24
+ await _createArchiveCLI(root, output)
25
+
26
+ return fs.promises.readFile(output)
27
+ }
28
+ finally {
29
+ if (await exists(temp)) {
30
+ await fs.promises.rm(temp, { recursive: true, force: true })
31
+ }
32
+ }
54
33
  }
55
34
 
56
35
  const unpackArchiveCLI = async (tgz, folder) => {
@@ -65,8 +44,6 @@ const unpackArchiveCLI = async (tgz, folder) => {
65
44
  }
66
45
 
67
46
  module.exports = {
68
- packArchive,
69
47
  packArchiveCLI,
70
- unpackArchive,
71
48
  unpackArchiveCLI
72
49
  }
@@ -57,7 +57,7 @@ const _log = (req, challenges) => {
57
57
  LOG.debug(`User "${req.user.id}" request URL`, req.url, '\n', ...challengesLog)
58
58
  }
59
59
 
60
- const _authCallback = (req, res, next, internalError, user, challenges) => {
60
+ const cap_auth_callback = (req, res, next, internalError, user, challenges) => {
61
61
  // An internal error occurs during the authentication process
62
62
  if (internalError) {
63
63
  // REVISIT: What to do? Security log?
@@ -83,7 +83,7 @@ const _authCallback = (req, res, next, internalError, user, challenges) => {
83
83
 
84
84
  const _mountCustomAuth = (srv, app, config) => {
85
85
  const impl = cds.resolve(config.impl)
86
- app.use(srv.path, _require(impl))
86
+ app.use(_require(impl))
87
87
  }
88
88
 
89
89
  const _mountMockAuth = (srv, app, strategy, config) => {
@@ -92,12 +92,12 @@ const _mountMockAuth = (srv, app, strategy, config) => {
92
92
  ? new (require('./strategies/dummy'))()
93
93
  : new (require('./strategies/mock'))(config, `mock_${srv.name}`)
94
94
 
95
- app.use(srv.path, (req, res, next) => {
95
+ app.use(function cap_auth(req, res, next) {
96
96
  let user, challenge
97
97
  impl.success = arg => (user = arg)
98
98
  impl.fail = arg => (challenge = arg)
99
99
  impl.authenticate(req)
100
- _authCallback(req, res, next, undefined, user, [challenge])
100
+ cap_auth_callback(req, res, next, undefined, user, [challenge])
101
101
  })
102
102
  }
103
103
 
@@ -119,10 +119,10 @@ const _mountPassportAuth = (srv, app, strategy, config) => {
119
119
  }
120
120
 
121
121
  // authenticate
122
- app.use(srv.path, passport.initialize())
123
- app.use(srv.path, (req, res, next) => {
122
+ app.use(passport.initialize())
123
+ app.use((req, res, next) => {
124
124
  const options = { session: false, failWithError: true }
125
- const callback = _authCallback.bind(undefined, req, res, next)
125
+ const callback = cap_auth_callback.bind(undefined, req, res, next)
126
126
  passport.authenticate(strategy === 'jwt' ? 'JWT' : strategy, options, callback)(req, res, next)
127
127
  })
128
128
  }
@@ -131,7 +131,9 @@ const _mountPassportAuth = (srv, app, strategy, config) => {
131
131
  * export authentication middleware
132
132
  */
133
133
  // eslint-disable-next-line complexity
134
- module.exports = (srv, app, options) => {
134
+ module.exports = (srv, options = srv.options) => {
135
+ const handlers = [],
136
+ app = { use: h => handlers.push(h) }
135
137
  // NOTE: options.auth is not an official API
136
138
  let config = 'auth' in options ? options.auth : cds.env.requires.auth
137
139
  if (!config) {
@@ -147,7 +149,7 @@ module.exports = (srv, app, options) => {
147
149
  LOG._warn && LOG.warn(`No authentication configured. This is not recommended in production.`)
148
150
  }
149
151
  // no auth wanted > return
150
- return
152
+ return handlers
151
153
  }
152
154
 
153
155
  // cds.env.requires.auth = { kind: 'xsuaa-auth' } was briefly documented on capire -> also support
@@ -181,11 +183,12 @@ module.exports = (srv, app, options) => {
181
183
  (process.env.NODE_ENV === 'production' && config.credentials && config.restrict_all_services)
182
184
  ) {
183
185
  if (!logged) LOG._debug && LOG.debug(`Enforcing authenticated users for all services`)
184
- app.use(srv.path, _enforce_authenticated_user)
186
+ app.use(cap_enforce_login)
185
187
  }
186
188
 
187
189
  // so we don't log the same stuff multiple times
188
190
  logged = true
191
+ return handlers
189
192
  }
190
193
 
191
194
  const _strategy4 = config => {
@@ -196,7 +199,7 @@ const _strategy4 = config => {
196
199
  throw new Error(`Authentication kind "${config.kind}" is not supported`)
197
200
  }
198
201
 
199
- const _enforce_authenticated_user = (req, res, next) => {
202
+ const cap_enforce_login = (req, res, next) => {
200
203
  if (req.user && req.user.is('authenticated-user')) return next() // pass if user is authenticated
201
204
  if (!req.user || req.user._is_anonymous) {
202
205
  if (req.user && req.user._challenges) res.set('WWW-Authenticate', req.user._challenges.join(';'))
@@ -107,8 +107,7 @@ class OData {
107
107
  constructor(edm, csn, options = {}) {
108
108
  this._validateEdm(edm)
109
109
  this._options = options
110
- this._csn = csn
111
- this._createOdataService(edm)
110
+ this._createOdataService(edm, csn)
112
111
  }
113
112
 
114
113
  _validateEdm(edm) {
@@ -118,7 +117,7 @@ class OData {
118
117
  }
119
118
  }
120
119
 
121
- _createOdataService(edm) {
120
+ _createOdataService(edm, csn) {
122
121
  const ServiceFactory = require('./okra/odata-server').ServiceFactory
123
122
 
124
123
  // skip okra's validation in production or implicitly for w4 and x4
@@ -133,7 +132,7 @@ class OData {
133
132
  effective.odata.proxies ||
134
133
  effective.odata.xrefs
135
134
 
136
- this._odataService = ServiceFactory.createService(edm, _config(edm, this._csn, this._options)).trust(isTrusted)
135
+ this._odataService = ServiceFactory.createService(edm, _config(edm, csn, this._options)).trust(isTrusted)
137
136
 
138
137
  // will be added to express app like app.use('/base/path/', service) and odata-v4 wants app.use('/', service) if basePath is set
139
138
  this._odataService.setBasePath('/')
@@ -165,8 +164,7 @@ class OData {
165
164
  req,
166
165
  res
167
166
  } = data
168
- // REVISIT: _model should not be necessary
169
- const tx = (txs[odataContext.id] = cdsService.tx({ user, req, res, _model: cdsService.model }))
167
+ const tx = (txs[odataContext.id] = cdsService.tx({ user, req, res }))
170
168
  cds.context = tx.context
171
169
  // for collecting results and errors
172
170
  data.results = data.results || {}
@@ -183,19 +181,8 @@ class OData {
183
181
  if (errors) {
184
182
  // rollback without errors to not trigger srv.on('error') with array
185
183
  await tx.rollback()
186
- // invoke srv.on('error') for each error and build failedRequests that reflects error modifications
187
- errors = odataContext.applicationData.errors[odataContext.id]
188
- const failedRequests = {}
189
-
190
- for (const e of errors) {
191
- const { error: err, req } = e
192
- for (const each of cdsService._handlers._error) each.handler.call(cdsService, err, req)
193
- const requestId = req._.odataReq.getOdataRequestId()
194
- const { error, statusCode } = normalizeError(err, req)
195
- failedRequests[requestId] = Object.assign(error, { statusCode })
196
- }
197
-
198
- done(new Error(`Atomicity group "${odataContext.id}" failed`), { failedRequests })
184
+
185
+ done()
199
186
  return
200
187
  }
201
188
 
@@ -226,6 +213,7 @@ class OData {
226
213
  this._odataService.use(DATA_DELETE_HANDLER, _delete(cdsService))
227
214
 
228
215
  this._odataService.use(ACTION_EXECUTE_HANDLER, _action(cdsService))
216
+ return this
229
217
  }
230
218
 
231
219
  /**
@@ -163,12 +163,9 @@ class ODataRequest extends cds.Request {
163
163
  */
164
164
  const { user } = req
165
165
 
166
- // REVISIT: _model should not be necessary
167
- const _model = service.model
168
-
169
166
  // REVISIT: public API for query options (express style req.query already in use)?
170
167
  const _queryOptions = odataReq.getQueryOptions()
171
- super({ event, target, data, query, user, method, headers, req, res, _model, _queryOptions })
168
+ super({ event, target, data, query, user, method, headers, req, res, _queryOptions })
172
169
  }
173
170
 
174
171
  /*
@@ -77,10 +77,7 @@ const action = service => {
77
77
  } catch (e) {
78
78
  err = e
79
79
 
80
- if (changeset) {
81
- // for passing into rollback
82
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
83
- } else {
80
+ if (!changeset) {
84
81
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
85
82
  await tx.rollback(e).catch(() => {})
86
83
  }
@@ -63,10 +63,7 @@ const create = service => {
63
63
  } catch (e) {
64
64
  err = e
65
65
 
66
- if (changeset) {
67
- // for passing into rollback
68
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
69
- } else {
66
+ if (!changeset) {
70
67
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
71
68
  await tx.rollback(e).catch(() => {})
72
69
  }
@@ -44,10 +44,7 @@ const del = service => {
44
44
  } catch (e) {
45
45
  err = e
46
46
 
47
- if (changeset) {
48
- // for passing into rollback
49
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
50
- } else {
47
+ if (!changeset) {
51
48
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
52
49
  await tx.rollback(e).catch(() => {})
53
50
  }
@@ -114,13 +114,13 @@ const getErrorHandler = (crashOnError = true, srv) => {
114
114
 
115
115
  // invoke srv.on('error', function (err, req) { ... }) here in special situations
116
116
  // REVISIT: if for compat reasons, remove once cds^5.1
117
- if (srv._handlers._error) {
117
+ if (srv._handlers._error.length) {
118
118
  let ctx = cds.context
119
119
  if (!ctx) {
120
120
  // > error before req was dispatched
121
121
  ctx = new cds.Request({ req, res: req.res, user: req.user || new cds.User.Anonymous() })
122
122
  for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
123
- } else if (err.getRootCause) {
123
+ } else {
124
124
  // > error after req was dispatched, e.g., serialization error in okra
125
125
  for (const each of srv._handlers._error) each.handler.call(srv, err, ctx)
126
126
  }
@@ -5,10 +5,6 @@ const { toODataResult } = require('../utils/result')
5
5
  const { normalizeError } = require('../../../../common/error/frontend')
6
6
  const getError = require('../../../../common/error')
7
7
 
8
- const _getMetadata4Tenant = async (tenant, locale, service) => {
9
- return await cds.mtx.getEdmx(tenant, service.name, locale)
10
- }
11
-
12
8
  /**
13
9
  * Provide localized metadata handler.
14
10
  *
@@ -23,21 +19,12 @@ const metadata = service => {
23
19
  const locale = odataRes.getContract().getLocale()
24
20
 
25
21
  try {
26
- let edmx
27
-
28
- if (cds.mtx && service._isExtended) {
29
- edmx = await _getMetadata4Tenant(tenant, locale, service)
30
- }
31
-
32
- if (!edmx) {
33
- edmx = cds.localize(
34
- service.model,
35
- locale,
36
- // REVISIT: we could cache this in a weak map
37
- cds.compile.to.edmx(service.model, { service: service.definition.name })
38
- )
39
- }
40
-
22
+ let edmx = cds.localize(
23
+ service.model,
24
+ locale,
25
+ // REVISIT: we could cache this in model._cached
26
+ cds.compile.to.edmx(service.model, { service: service.definition.name })
27
+ )
41
28
  return next(null, toODataResult(edmx))
42
29
  } catch (e) {
43
30
  if (LOG._error) {
@@ -81,7 +81,8 @@ const _getCount = async (tx, readReq) => {
81
81
  // REVISIT: this process appears to be rather clumsy
82
82
  // Copy CQN including from, where and search + changing columns
83
83
  const select = SELECT.from(readReq.query.SELECT.from)
84
- select.SELECT.columns = [{ func: 'count', args: [{ val: '1' }], as: '$count' }]
84
+ // { val: 1 } is used on purpose, as "numbers" are not used as param in prepared stmt
85
+ select.SELECT.columns = [{ func: 'count', args: [{ val: 1 }], as: '$count' }]
85
86
 
86
87
  if (readReq.query.SELECT.where) select.SELECT.where = readReq.query.SELECT.where
87
88
  if (readReq.query.SELECT.search) select.SELECT.search = readReq.query.SELECT.search
@@ -484,10 +485,7 @@ const read = service => {
484
485
  } catch (e) {
485
486
  err = e
486
487
 
487
- if (changeset) {
488
- // for passing into rollback
489
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
490
- } else {
488
+ if (!changeset) {
491
489
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
492
490
  await tx.rollback(e).catch(() => {})
493
491
  }
@@ -6,7 +6,7 @@ module.exports = srv => {
6
6
  const requires = getRequiresAsArray(srv.definition)
7
7
  const restricted = isRestricted(srv)
8
8
 
9
- return (odataReq, odataRes, next) => {
9
+ return function ODataRequestHandler(odataReq, odataRes, next) {
10
10
  const req = odataReq.getBatchApplicationData()
11
11
  ? odataReq.getBatchApplicationData().req
12
12
  : odataReq.getIncomingRequest()
@@ -174,10 +174,7 @@ const update = service => {
174
174
  } catch (e) {
175
175
  err = e
176
176
 
177
- if (changeset) {
178
- // for passing into rollback
179
- odataReq.getBatchApplicationData().errors[changeset].push({ error: e, req })
180
- } else {
177
+ if (!changeset) {
181
178
  // REVISIT: rollback needed if an error occurred before commit attempted -> how to distinguish?
182
179
  await tx.rollback(e).catch(() => {})
183
180
  }
@@ -68,8 +68,6 @@ const MULTI_LINE_STRING_VALIDATION = new RegExp('^' + SRID + 'MultiLineString\\(
68
68
  const MULTI_POLYGON_VALIDATION = new RegExp('^' + SRID + 'MultiPolygon\\((' + MULTI_POLYGON + ')\\)$', 'i')
69
69
  const COLLECTION_VALIDATION = new RegExp('^' + SRID + 'Collection\\((' + MULTI_GEO_LITERAL + ')\\)$', 'i')
70
70
 
71
- const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
72
-
73
71
  function _getBase64(val) {
74
72
  if (isInvalidBase64string(val)) return
75
73
  // convert url-safe to standard base64
@@ -40,8 +40,10 @@ class DeserializerFactory {
40
40
  let additionalInformation = { hasDelta: false }
41
41
  const deserializer = new ResourceJsonDeserializer(edm, jsonContentTypeInfo)
42
42
  return (edmObject, value) => {
43
+ const body = deserializer[name](edmObject, value, expand, additionalInformation)
44
+
43
45
  return {
44
- body: deserializer[name](edmObject, value, expand, additionalInformation),
46
+ body,
45
47
  expand,
46
48
  additionalInformation
47
49
  }
@@ -1,7 +1,41 @@
1
- const Dispatcher = require('./Dispatcher')
1
+ const { alias2ref } = require('../../../common/utils/csn') // REVISIT: eliminate that
2
+ const cds = require('../../../cds')
3
+ const OData = require('./OData')
2
4
 
3
- const to = service => {
4
- return new Dispatcher(service).getService()
5
+ /**
6
+ * This is the express handler for a specific OData endpoint.
7
+ * Note: the same service can be served at different endpoints.
8
+ */
9
+ module.exports = srv => {
10
+ const okra = new OkraAdapter(srv)
11
+ return okra.process.bind(okra)
5
12
  }
6
13
 
7
- module.exports = to
14
+ function OkraAdapter(srv, model = srv.model) {
15
+ const edm = cds.compile.to.edm(model, { service: srv.definition?.name || srv.name })
16
+ alias2ref(srv, edm) // REVISIT: eliminate that -> done again and again -> search for _alias2ref
17
+ return new OData(edm, model, srv.options).addCDSServiceToChannel(srv)
18
+ }
19
+
20
+ //////////////////////////////////////////////////////////////////////////////
21
+ //
22
+ // REVISIT: Move to ExtensibilityService
23
+ //
24
+ if (cds.mtx || cds.requires.extensibility || cds.requires.toggles)
25
+ module.exports = srv => {
26
+ const id = `${++unique} - ${srv.path}` // REVISIT: this is to allow running multiple express apps serving same endpoints, as done by some questionable tests
27
+ return function ODataAdapter(req, res) {
28
+ const model = cds.context?.model || srv.model
29
+ if (!model._cached) Object.defineProperty(model, '_cached', { value: {} })
30
+
31
+ // Note: cache is attached to model cache so they get disposed when models are evicted from cache
32
+ let adapters = model._cached._odata_adapters || (model._cached._odata_adapters = {})
33
+ let okra = adapters[id]
34
+ if (!okra) {
35
+ const _srv = { __proto__: srv, _real_srv: srv, model } // REVISIT: we need to do that better in new adapters
36
+ okra = adapters[id] = new OkraAdapter(_srv, model)
37
+ }
38
+ return okra.process(req, res)
39
+ }
40
+ }
41
+ let unique = 0
@@ -22,10 +22,8 @@ const _isNoAccessError = e => Number(e.code) === 403 || Number(e.code) === 401
22
22
  const _isNotFoundError = e => Number(e.code) === 404
23
23
  const _isEntityNotReadableError = e => Number(e.code) === 405
24
24
 
25
- const _handleReadError = (err, req) => {
25
+ const _handleReadError = err => {
26
26
  if (!(_isNoAccessError(err) || _isEntityNotReadableError(err) || _isNotFoundError(err))) throw err
27
- const log = Object.assign(err, { level: 'ERROR', message: normalizeError(err, req).error.message })
28
- process.env.NODE_ENV !== 'production' && LOG._warn && LOG.warn(log)
29
27
  }
30
28
 
31
29
  const _getOperationQueryColumns = urlQueryOptions => {
@@ -18,6 +18,10 @@ module.exports = class Differ {
18
18
  _createSelectColumnsForDelete(entity) {
19
19
  const columns = []
20
20
  for (const element of Object.values(entity.elements)) {
21
+ // Don't take into account virtual or computed properties to make the diff result
22
+ // consistent with the ones for UPDATE/CREATE (where we don't have access to that
23
+ // information).
24
+ if (!element.key && (element.virtual || element['@Core.Computed'])) continue
21
25
  if (element.isComposition) {
22
26
  if (element._target._hasPersistenceSkip) continue
23
27
  columns.push({
@@ -9,35 +9,7 @@ const getFeatureNotSupportedError = message => {
9
9
  return getError(501, `Feature is not supported: ${message}`)
10
10
  }
11
11
 
12
- const getAuditLogNotWrittenError = (rootCauseError, phase, event) => {
13
- const errorMessage =
14
- !phase || event === 'READ' ? 'Audit log could not be written' : `Audit log could not be written ${phase}`
15
- const error = new Error(errorMessage)
16
- error.rootCause = rootCauseError
17
- return error
18
- }
19
-
20
- const hasBeenCalledError = (method, query) => {
21
- return new Error(`Method ${method} has been called before. Invalid CQN: ${JSON.stringify(query)}`)
22
- }
23
-
24
- const unexpectedFunctionCallError = (functionName, expectedFunction) => {
25
- return new Error(`Cannot build CQN object. Invalid call of "${functionName}" before "${expectedFunction}"`)
26
- }
27
-
28
- const invalidFunctionArgumentError = (statement, arg) => {
29
- const details = JSON.stringify(arg, (key, value) => (value === undefined ? '__undefined__' : value)).replace(
30
- /"__undefined__"/g,
31
- 'undefined'
32
- )
33
- return new Error(`Cannot build ${statement} statement. Invalid data provided: ${details}`)
34
- }
35
-
36
12
  module.exports = {
37
13
  getModelNotDefinedError,
38
- getFeatureNotSupportedError,
39
- getAuditLogNotWrittenError,
40
- hasBeenCalledError,
41
- unexpectedFunctionCallError,
42
- invalidFunctionArgumentError
14
+ getFeatureNotSupportedError
43
15
  }
@@ -74,8 +74,9 @@ ENTITY_IS_NOT_CRUD=Entity "{0}" is not {1}
74
74
  ENTITY_IS_NOT_CRUD_VIA_NAVIGATION=Entity "{0}" is not {1} via navigation "{2}"
75
75
  ENTITY_IS_AUTOEXPOSED=Entity "{0}" is not explicitly exposed as part of the service
76
76
  EXPAND_IS_RESTRICTED=Navigation property "{0}" is not allowed for expand operation
77
- EXPAND_COUNT_UNSUPPORTED="$count" is not supported for expand operation
77
+ EXPAND_COUNT_UNSUPPORTED="/$count" is not supported for expand operation
78
78
  ORDERBY_LAMBDA_UNSUPPORTED="$orderby" does not support lambda
79
+ EXPAND_APPLY_UNSUPPORTED="$apply" is not supported for expand operation
79
80
 
80
81
  # rest protocol adapter
81
82
  INVALID_RESOURCE="{0}" is not a valid resource