@sap/cds 7.2.0 → 7.3.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 (63) hide show
  1. package/CHANGELOG.md +174 -126
  2. package/README.md +1 -1
  3. package/apis/connect.d.ts +1 -1
  4. package/apis/core.d.ts +6 -4
  5. package/apis/serve.d.ts +1 -1
  6. package/apis/services.d.ts +51 -31
  7. package/apis/test.d.ts +24 -10
  8. package/bin/serve.js +4 -3
  9. package/common.cds +4 -4
  10. package/lib/auth/ias-auth.js +7 -8
  11. package/lib/compile/cdsc.js +5 -7
  12. package/lib/compile/etc/csv.js +22 -11
  13. package/lib/dbs/cds-deploy.js +1 -2
  14. package/lib/env/cds-env.js +26 -20
  15. package/lib/env/defaults.js +4 -3
  16. package/lib/env/schema.js +9 -0
  17. package/lib/i18n/localize.js +83 -77
  18. package/lib/index.js +6 -2
  19. package/lib/linked/classes.js +13 -13
  20. package/lib/plugins.js +41 -45
  21. package/lib/req/user.js +2 -2
  22. package/lib/srv/protocols/_legacy.js +0 -1
  23. package/lib/srv/protocols/odata-v4.js +4 -0
  24. package/lib/utils/axios.js +7 -1
  25. package/lib/utils/cds-test.js +140 -133
  26. package/lib/utils/cds-utils.js +1 -1
  27. package/lib/utils/check-version.js +6 -0
  28. package/lib/utils/data.js +19 -6
  29. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +20 -19
  30. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +10 -1
  31. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +1 -1
  32. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +2 -3
  33. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +0 -14
  34. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/core/OdataRequest.js +1 -0
  35. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +5 -2
  36. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/MetadataHandler.js +1 -1
  37. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/ServiceHandler.js +1 -1
  38. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/DispatcherCommand.js +2 -2
  39. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -3
  40. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -1
  41. package/libx/_runtime/common/composition/update.js +18 -2
  42. package/libx/_runtime/common/error/frontend.js +46 -34
  43. package/libx/_runtime/common/generic/auth/capabilities.js +33 -14
  44. package/libx/_runtime/common/generic/input.js +1 -1
  45. package/libx/_runtime/common/generic/paging.js +1 -0
  46. package/libx/_runtime/common/i18n/messages.properties +1 -0
  47. package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -3
  48. package/libx/_runtime/db/query/update.js +48 -30
  49. package/libx/_runtime/fiori/lean-draft.js +23 -24
  50. package/libx/_runtime/hana/conversion.js +3 -2
  51. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -1
  52. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  53. package/libx/_runtime/remote/Service.js +11 -26
  54. package/libx/_runtime/remote/utils/client.js +3 -2
  55. package/libx/_runtime/remote/utils/data.js +5 -7
  56. package/libx/odata/{grammar.pegjs → grammar.peggy} +1 -1
  57. package/libx/odata/metadata.js +121 -0
  58. package/libx/odata/parser.js +1 -1
  59. package/libx/odata/service-document.js +61 -0
  60. package/libx/odata/utils.js +102 -48
  61. package/libx/rest/RestAdapter.js +2 -2
  62. package/libx/rest/middleware/error.js +1 -1
  63. package/package.json +1 -1
@@ -1,47 +1,71 @@
1
- const { is_mocha, is_jest } = support_jest_and_mocha()
1
+ // Provide same global functions for jest and mocha
2
+ ;(function _support_jest_and_mocha() {
3
+ const is_jest = !!global.beforeAll
4
+ const is_mocha = !!global.before
5
+ if (is_mocha) {
6
+ global.beforeAll = global.before
7
+ global.afterAll = global.after
8
+ global.test = global.it
9
+ // Adding test.each() and describe.each() to mocha
10
+ const { format } = require('util')
11
+ for (let td of [ 'test', 'describe' ]) global[td].each = function(table) {
12
+ return (msg,fn) => Promise.all (table.map (each => {
13
+ if (!Array.isArray(each)) each = [each]
14
+ return this (format(msg,...each), ()=> fn(...each))
15
+ }))
16
+ }
17
+ } else if (is_jest) { // it's jest
18
+ global.before = (msg,fn) => global.beforeAll(fn||msg)
19
+ global.after = (msg,fn) => global.afterAll(fn||msg)
20
+ } else { // it's none of both
21
+ global.beforeAll = global.before = (msg,fn) => (fn||msg)()
22
+ global.afterAll = global.after = (msg,fn) => global.cds?.repl?.on('exit',fn||msg)
23
+ global.beforeEach = global.afterEach = ()=>{}
24
+ }
25
+ })()
2
26
 
27
+ /**
28
+ * Instances of this class are constructed and returned by cds.test().
29
+ */
3
30
  class Test extends require('./axios') {
4
31
 
32
+ /**
33
+ * Allows: const { GET, expect, test } = cds.test()
34
+ */
35
+ test = this
36
+
37
+ get cds() { return require('../index') }
38
+ get sleep() { return super.sleep = require('util').promisify(setTimeout) }
39
+ get data() { return super.data = new (require('./data'))}
40
+
5
41
  /**
6
42
  * Launches a cds server with arbitrary port and returns a subclass which
7
43
  * also acts as an axios lookalike, providing methods to send requests.
8
44
  */
9
- run (cmd='.', ...args) {
10
-
11
- const {cds} = this; this.cmd = cmd, this.args = args
12
- if (!/^(serve|run)$/.test(cmd)) {
13
- try {
14
- const project = cds.utils.isdir(cmd) || require.resolve (cmd+'/package.json').slice(0,-13)
15
- this.cmd = cmd = 'serve'; args.push ('--in-memory?')
16
- this.in (project)
17
- } catch(e) {
18
- throw cds.error (`No such folder or package '${process.cwd()}' -> '${cmd}'`)
19
- }
20
- } else if ('run' === cmd && args.length > 0) { // `run <project>` -> `serve --project <project>`
21
- if (!args.includes('--project')) args.unshift('--project')
45
+ run (folder_or_cmd, ...args) {
46
+
47
+ switch (folder_or_cmd) {
48
+ case 'serve': break // nothing to do as all arguments are given
49
+ case 'run': if (args.length > 0) args.unshift('--project'); break
50
+ default: this.in(folder_or_cmd); args.push('--in-memory?')
22
51
  }
52
+ const {cds} = this
23
53
 
24
54
  // launch cds server...
25
- before (`launching ${cmd} ${args.join(' ')}...`, async () => {
26
- // cds.plugins is filling cds.env before cds serve -> profile must be parsed here
27
- const profile = args.indexOf('--profile')
28
- if (profile !== -1) process.env.CDS_ENV = [...process.env.CDS_ENV?.split(',') ?? [], ...args[profile+1].split(',')]
29
- await cds.plugins
30
- cds.once ('listening', ({server,url}) => {
31
- const axp = Reflect.getOwnPropertyDescriptor(this,'axios')
32
- if (axp) axp.value.defaults.baseURL = url
33
- this.server = server
34
- this.url = url
35
- })
36
- try { return cds.exec (...args, ...(args.includes('--port') ? [] : ['--port', '0'])) }
37
- catch (e) { if (is_mocha) console.error(e) } // eslint-disable-line no-console
55
+ before (async ()=>{
56
+ if (!args.includes('--port')) args.push('--port', '0')
57
+ let { server, url } = await cds.exec(...args)
58
+ this.server = server
59
+ this.url = url
38
60
  })
39
61
 
40
62
  // gracefully shutdown cds server...
41
- after (() => this.server && cds.shutdown())
42
-
43
- beforeEach (async () => {
44
- if (this.data._autoReset) await this.data.reset()
63
+ after (()=>{
64
+ this.server && cds.shutdown()
65
+ // cds.service.providers = []
66
+ // delete cds.services
67
+ // delete cds.plugins
68
+ // delete cds.env
45
69
  })
46
70
 
47
71
  return this
@@ -52,127 +76,110 @@ class Test extends require('./axios') {
52
76
  * of path components which are concatenated with path.resolve().
53
77
  * Checks conflicts with cds.env loaded in other folder before.
54
78
  */
55
- in (...paths) {
56
- const {cds} = this; cds.root = require('path').resolve (cds.root, ...paths)
57
- // const env = Reflect.getOwnPropertyDescriptor(cds,'env')
58
- // if (env && env.value && env.value._home !== cds.root) {
59
- // throw new Error (`[cds.test] - 'cds.env' was invoked before 'cds.test.in' from a different home:
60
-
61
- // cds.env._home: ${cds.env._home}
62
- // cds.test.in: ${cds.root}
63
-
64
- // > throwing this as tests would likely behave erratically.
65
- // `)
66
- // }
79
+ in (folder, ...paths) {
80
+ if (!folder) return this
81
+ const {cds} = this, { isdir, local } = cds.utils
82
+ // try to resolve folder relative to cds.root, or as a node module
83
+ try {
84
+ const path = require('path')
85
+ folder = isdir (path.resolve (cds.root, folder, ...paths))
86
+ || path.join (require.resolve (folder+'/package.json').slice(0,-13), ...paths)
87
+ } catch(e) {
88
+ throw cds.error (`No such folder or package '${process.cwd()}' -> '${folder}'`)
89
+ }
90
+ // Check if cds.env was loaded before running cds.test in different folder
91
+ if (process.env.CDS_TEST_ENV_CHECK) {
92
+ const env = Reflect.getOwnPropertyDescriptor(cds,'env')?.value
93
+ if (env && env._home !== folder && env.stack) {
94
+ let filter = line => !line.match(/node_modules\/jest-|node:internal/)
95
+ let err = new Error; err.message =
96
+ `Detected cds.env loaded before running cds.test in different folder: \n` +
97
+ `1. cds.env loaded from: ${local(cds.env._home)||'./'} \n` +
98
+ `2. cds.test running in: ${local(folder)} \n\n` +
99
+ err.stack.split('\n').filter(filter).slice(1).join('\n')
100
+ err.stack = env.stack.split('\n').filter(filter).slice(1).join('\n')
101
+ throw err
102
+ }
103
+ }
104
+ cds.root = folder
67
105
  return this
68
106
  }
69
107
 
70
108
  /**
71
- * Switch on/off console log output.
109
+ * Method to spy on a function in an object, similar to jest.spyOn().
72
110
  */
73
- verbose (v) {
74
- initLogging({ is_mocha, is_jest, verbose: v })
75
- return this
111
+ spy (o,f) {
112
+ const origin = o[f]
113
+ const fn = function (...args) {
114
+ ++fn.called
115
+ return origin.apply(this,args)
116
+ }
117
+ fn.called = 0
118
+ fn.restore = ()=> o[f] = origin
119
+ return o[f] = fn
76
120
  }
77
121
 
78
- /** Lazily loads and returns an instance of chai */
79
- get chai() { return super.chai = load_chai() }
80
- get expect() { global.describe.each || support_jest_and_mocha(); return this.chai.expect }
81
- get assert() { global.describe.each || support_jest_and_mocha(); return this.chai.assert }
82
- get sleep() { return super.sleep = require('util').promisify(setTimeout) }
83
- get data() { return super.data = new (require('./data'))}
84
- get cds() { return require('../index') }
85
- get spy() { return spy }
86
-
87
- then(r) {
88
- const {cds} = this
122
+ /**
123
+ * For usage in repl, e.g. var test = await cds.test()
124
+ */
125
+ then (resolve) {
89
126
  if (this.server) {
90
- r({ server: this.server, url: this.url })
127
+ resolve({ server: this.server, url: this.url })
91
128
  } else {
92
- cds.once('listening', r)
129
+ this.cds.once('listening', resolve)
93
130
  }
94
131
  }
95
- }
96
132
 
97
- function support_jest_and_mocha() {
98
- const is_jest = !!global.beforeAll
99
- const is_mocha = !!global.before
100
- if (is_mocha) {
101
- global.beforeAll = global.before
102
- global.afterAll = global.after
103
- global.test = global.it
104
- const { format } = require('util')
105
- for (let td of [ 'test', 'describe' ]) global[td].each = function(table) {
106
- return (title,fn) => Promise.all (table.map (each => {
107
- if (!Array.isArray(each)) each = [each]
108
- return this (format(title,...each), ()=> fn(...each))
109
- }))
110
- }
111
- after(()=>{
112
- delete global.cds
113
- for (let k in require.cache) delete require.cache[k] // REVISIT: Whay are we doing that?
133
+ /**
134
+ * Captures console.log output.
135
+ */
136
+ log (_capture, afterEach = global.afterEach) {
137
+ const {console} = global, {format} = require('util')
138
+ const log = { output: '' }
139
+ beforeAll(()=> global.console = { __proto__: console,
140
+ log: _capture ??= (..._)=> log.output += format(..._)+'\n',
141
+ info: _capture,
142
+ warn: _capture,
143
+ debug: _capture,
144
+ trace: _capture,
145
+ error: _capture,
146
+ timeEnd: _capture, time: ()=>{},
114
147
  })
115
- } else if (is_jest) { // it's jest
116
- global.before = (msg,fn) => global.beforeAll(fn||msg)
117
- global.after = (msg,fn) => global.afterAll(fn||msg)
118
- } else { // it's none of both
119
- global.before = global.beforeAll = (_,fn) => fn()
120
- global.beforeEach = ()=>{}
121
- global.afterEach = ()=>{}
122
- global.after = global.afterAll = (fn) => {
123
- const repl = global.cds?.repl
124
- repl && repl.on('exit',fn)
125
- }
148
+ afterAll (log.release = ()=>{ log.output = ''; global.console = console })
149
+ afterEach (log.clear = ()=>{ log.output = '' })
150
+ return log
126
151
  }
127
- initLogging ({ is_jest, is_mocha })
128
- return { is_jest, is_mocha }
129
- }
130
152
 
131
- function load_chai() {
132
- const require = (mod) => { try { return module.require(mod) } catch(e) {
133
- if (e.code === 'MODULE_NOT_FOUND') throw new Error (`
134
- Failed to load required package '${mod}'. Please add it thru:
135
- npm add -D chai chai-as-promised chai-subset
136
- `)}}
137
- const chai = require('chai') // eslint-disable-line cds/no-missing-dependencies
138
- chai.use (require('chai-subset')) // eslint-disable-line cds/no-missing-dependencies
139
- chai.use (require('chai-as-promised')) // eslint-disable-line cds/no-missing-dependencies
140
- return chai
141
- }
153
+ /**
154
+ * Silences all console log output, e.g.: CDS_TEST_SILENT=y jest/mocha ...
155
+ */
156
+ silent(){ this.log(()=>{},()=>{}); return this }
157
+ /** @deprecated */ verbose(){ return this }
142
158
 
143
- function initLogging ({ verbose }={}) {
144
- if (verbose && global.console.logs) return global.console = global.console.__proto__
145
- if (process.env.CDS_TEST_SILENT) {
146
- const console = global.console, logs = []
147
- const {format} = require('util')
148
- global.console = { __proto__: console, logs,
149
- time: ()=>{}, timeEnd: (...args)=> logs.push(args),
150
- debug: (...args)=> logs.push(args),
151
- info: (...args)=> logs.push(args),
152
- log: (...args)=> logs.push(args),
153
- warn: (...args)=> logs.push(args),
154
- trace: (...args)=> logs.push(args),
155
- error: (...args)=> logs.push(args),
156
- dump(){ for (let each of logs) process.stdout.write (format(...each)+'\n') },
157
- }
158
- afterAll (()=> global.console = console)
159
- }
160
- }
161
159
 
162
- const spy = (o,f) => {
163
- const origin = o[f]
164
- const fn = function (...args) {
165
- ++fn.called
166
- return origin.apply(this,args)
160
+ /**
161
+ * Lazily loads and returns an instance of chai
162
+ */
163
+ get chai() {
164
+ let chai = require('chai') // eslint-disable-line cds/no-missing-dependencies
165
+ chai.use (require('chai-subset')) // eslint-disable-line cds/no-missing-dependencies
166
+ chai.use (require('chai-as-promised')) // eslint-disable-line cds/no-missing-dependencies
167
+ return chai
168
+ function require (mod) { try { return module.require(mod) } catch(e) {
169
+ if (e.code === 'MODULE_NOT_FOUND') throw new Error (`
170
+ Failed to load required package '${mod}'. Please add it thru:
171
+ npm add -D chai chai-as-promised chai-subset
172
+ `)}}
167
173
  }
168
- fn.called = 0
169
- fn.restore = ()=> o[f] = origin
170
- return o[f] = fn
174
+ get assert() { return this.chai.assert }
175
+ get expect() { return this.chai.expect }
176
+ get should() { return this.chai.should() }
171
177
  }
172
178
 
173
179
 
174
180
  /** @type Test & ()=>Test */
175
- module.exports = Object.assign (
176
- Object.setPrototypeOf ((..._) => (new Test).run(..._), Test.prototype),
177
- { Test }
178
- )
181
+ const cds_test = module.exports = Object.assign ((..._) => (new Test).run(..._), { Test })
182
+
183
+ // Set prototype to allow usages like cds.test.in(), cds.test.log(), ...
184
+ Object.setPrototypeOf (cds_test, Test.prototype)
185
+ if (process.env.CDS_TEST_SILENT) cds_test.silent()
@@ -4,7 +4,7 @@ const cds = require('../index')
4
4
  const ux = module.exports = exports = new class {
5
5
  get inflect() { return super.inflect = require('./inflect') }
6
6
  get inspect() { return super.inspect = require('util').inspect }
7
- get uuid() { return super.uuid = require('@sap/cds-foss').uuid }
7
+ get uuid() { return super.uuid = require('crypto').randomUUID }
8
8
  get yaml() { return super.yaml = require('@sap/cds-foss').yaml }
9
9
  get pool() { return super.pool = require('@sap/cds-foss').pool }
10
10
  get tar() { return super.tar = require('./tar') }
@@ -15,3 +15,9 @@ if (given.major < required.major || given.major === required.major && given.mino
15
15
  Current v${given.version} does not satisfy this.
16
16
  \n`
17
17
  ) || 1)
18
+
19
+ if (given.major < 18) {
20
+ process.stderr.write (`WARNING: \n
21
+ Node.js v${given.major} has reached end of life. Please upgrade to v18 or higher.
22
+ `)
23
+ }
package/lib/utils/data.js CHANGED
@@ -2,19 +2,34 @@ const cds = require('../index')
2
2
 
3
3
  class DataUtil {
4
4
 
5
+ constructor() {
6
+ // This is to support simplified usage like that: beforeEach(test.data.reset)
7
+ this.reset = (fn) => {
8
+ if (typeof fn === 'function') this.__proto__.reset().then(fn)
9
+ else return this.__proto__.reset(fn)
10
+ }
11
+ }
12
+
13
+ autoReset() {
14
+ global.beforeEach (() => this.reset())
15
+ }
16
+
17
+ async deploy(db) {
18
+ if (!db) db = await cds.connect.to('db')
19
+ await cds.deploy.init(db)
20
+ }
21
+
5
22
  async delete(db) {
6
23
  if (!db) db = await cds.connect.to('db')
7
24
  if (!this._deletes) {
8
25
  this._deletes = []
9
26
  for (const entity of db.model.each('entity')) {
10
27
  if (!entity.query && entity['@cds.persistence.skip'] !== true) {
11
- this._deletes.push(cds.ql.DELETE.from(entity))
28
+ this._deletes.push(DELETE.from(entity))
12
29
  }
13
30
  }
14
31
  }
15
32
  if (this._deletes.length > 0) {
16
- const LOG = cds.log('deploy')
17
- if (!this._autoReset) LOG.info('Deleting all data for', db.model.each('entity'))
18
33
  await db.run(this._deletes)
19
34
  }
20
35
  }
@@ -23,11 +38,9 @@ class DataUtil {
23
38
  async reset(db) {
24
39
  if (!db) db = await cds.connect.to('db')
25
40
  await this.delete(db)
26
- await cds.deploy.init(db)
41
+ await this.deploy(db)
27
42
  }
28
43
 
29
- autoReset(enabled) { this._autoReset = enabled; return this }
30
-
31
44
  }
32
45
 
33
46
  module.exports = DataUtil
@@ -225,29 +225,30 @@ class OData {
225
225
  */
226
226
  // REVISIT: Remove this when we replaced Okra
227
227
  process(req, res) {
228
- const headers = req.headers
229
- const acceptHeader = headers && headers.accept
230
-
231
- // default to combination [...];IEEE754Compatible=true;ExponentialDecimals=true if one is omitted
232
- if (acceptHeader && acceptHeader.startsWith('application/json')) {
233
- if (acceptHeader.includes('IEEE754Compatible=true') && !acceptHeader.includes('ExponentialDecimals')) {
234
- req.headers.accept += ';ExponentialDecimals=true'
235
- } else if (acceptHeader.includes('ExponentialDecimals=true') && !acceptHeader.includes('IEEE754Compatible')) {
228
+ // NOTE: do none of this header manipulation in okra successor!
229
+ // if ExponentialDecimals=true is set in accept header, ensure IEEE754Compatible=true (use case unclear, but don't introduce regression)
230
+ // if IEEE754Compatible=true is set in accept header, but ExponentialDecimals is not specified, set it based on cds.env.odata.defaultExponentialDecimals
231
+ const acceptHeader = req.headers.accept
232
+ if (acceptHeader?.startsWith('application/json')) {
233
+ if (acceptHeader.includes('ExponentialDecimals=true') && !acceptHeader.includes('IEEE754Compatible')) {
236
234
  req.headers.accept += ';IEEE754Compatible=true'
237
- }
238
-
239
- const contentType = headers['content-type']
240
-
241
- // add IEEE754Compatible=true if !strict_numbers
242
- if (
243
- !cds.env.features.strict_numbers &&
244
- contentType &&
245
- contentType.includes('application/json') &&
246
- !contentType.includes('IEEE754Compatible')
235
+ } else if (
236
+ 'defaultExponentialDecimals' in cds.env.odata &&
237
+ acceptHeader.includes('IEEE754Compatible=true') &&
238
+ !acceptHeader.includes('ExponentialDecimals')
247
239
  ) {
248
- req.headers['content-type'] = contentType.replace('application/json', 'application/json;IEEE754Compatible=true')
240
+ req.headers.accept += `;ExponentialDecimals=${cds.env.odata.defaultExponentialDecimals}`
249
241
  }
250
242
  }
243
+ // add IEEE754Compatible=true if !strict_numbers
244
+ const contentType = req.headers['content-type']
245
+ if (
246
+ !cds.env.features.strict_numbers &&
247
+ contentType?.includes('application/json') &&
248
+ !contentType.includes('IEEE754Compatible')
249
+ ) {
250
+ req.headers['content-type'] = contentType.replace('application/json', 'application/json;IEEE754Compatible=true')
251
+ }
251
252
 
252
253
  // this._startPerfMeasurementOData(req)
253
254
  this._odataService.process(req, res).catch(err => {
@@ -12,6 +12,7 @@ const { validateResourcePath } = require('../utils/request')
12
12
  const { toODataResult, postProcess } = require('../utils/result')
13
13
  const { DRAFT_EVENTS } = require('../../../../common/constants/events')
14
14
  const { readAfterWrite } = require('../utils/readAfterWrite')
15
+ const { toBase64url } = require('../../../../common/utils/binary')
15
16
 
16
17
  const _postProcess = async (req, odataReq, odataRes, tx, result) => {
17
18
  const returnType = getActionOrFunctionReturnType(odataReq.getUriInfo().getPathSegments(), tx.model.definitions)
@@ -65,7 +66,15 @@ const action = service => {
65
66
  const keys = Object.keys(req.target.keys).filter(k => {
66
67
  return k !== 'IsActiveEntity' && !req.target.keys[k]._isAssociationStrict
67
68
  })
68
- const keysString = keys.map(key => `${key}=${result[key]}`).join(',')
69
+ const keysString = keys
70
+ .map(key => {
71
+ let val = result[key]
72
+ if (Buffer.isBuffer(val)) {
73
+ val = toBase64url(result[key])
74
+ }
75
+ return `${key}=${val}`
76
+ })
77
+ .join(',')
69
78
  odataRes.setHeader(
70
79
  'location',
71
80
  `../${req.target.name.replace(`${service.name}.`, '')}(${keysString},IsActiveEntity=${
@@ -123,7 +123,7 @@ const getErrorHandler = (crashOnError = true, srv) => {
123
123
  // lost cds.context -> as we fixed that we don't get into this if branch anymore,
124
124
  // but then the ctx in the else branch below isn't the ODataRequest anymore
125
125
  // > error before req was dispatched
126
- const creq = new cds.Request({ req, res: req.res, user: req.user || new cds.User.Anonymous() })
126
+ const creq = new cds.Request({ req, res: req.res, user: req.user || new cds.User.default() })
127
127
  for (const each of srv._handlers._error) each.handler.call(srv, err, creq)
128
128
  } else if (ctx._tx?._done !== 'rolled back') {
129
129
  // > error after req was dispatched, e.g., serialization error in okra
@@ -13,9 +13,8 @@ module.exports = srv => {
13
13
  ? odataReq.getBatchApplicationData().req
14
14
  : odataReq.getIncomingRequest()
15
15
 
16
- // REVISIT: ensure there always is a user (should be the case with new middlewares -> remove with old middlewares)
17
- // prettier-ignore
18
- if (!req.user) req.user = new cds.User.default
16
+ // ensure there always is a user going forward (not always the case with old or custom auth)
17
+ if (!req.user) req.user = new cds.User.default()
19
18
 
20
19
  const { res, user, path, headers } = req
21
20
 
@@ -346,20 +346,6 @@ class ValueConverter {
346
346
  return this._formatParams.getExponentialDecimalsSetting() ? bigValue.toExponential() : bigValue.toFixed()
347
347
  }
348
348
 
349
- // If scale is not specified or is 0 then the value must be serialized as an integer.
350
- if (scale === null || scale === undefined || scale === 0) {
351
- // The value has to be a safe integer in javascript, to prevent rounding problems.
352
- if (bigValue.lt(Number.MIN_SAFE_INTEGER) || bigValue.gt(Number.MAX_SAFE_INTEGER)) {
353
- throw new IllegalArgumentError(
354
- `The Edm.Decimal value ${value} cannot be correctly serialized as an ` +
355
- 'integer. IEEE754Compatible=true format parameter can be specified to serialize the ' +
356
- 'value as a string'
357
- )
358
- }
359
-
360
- return Number.parseInt(bigValue.toFixed(0), 10)
361
- }
362
-
363
349
  const absBigValue = bigValue.abs()
364
350
  // Because the value must be serialized as a number,
365
351
  // check whether the value can be correctly represented as a number in javascript.
@@ -283,6 +283,7 @@ class OdataRequest {
283
283
  * @throws {PreconditionFailedError} if the validations failed
284
284
  */
285
285
  validateEtag (etag) {
286
+ throw new Error("etag validation not supported via okra")
286
287
  this._logger.debug('Provided etag:', etag)
287
288
 
288
289
  this._validateEtagHasBeenCalled = true
@@ -8,6 +8,8 @@ const OdataRequestInBatch = require('../core/OdataRequestInBatch')
8
8
  const PlainHttpRequest = require('../core/PlainHttpRequest')
9
9
  const DeserializationError = require('../errors/DeserializationError')
10
10
 
11
+ const { AsyncResource } = require('node:async_hooks')
12
+
11
13
  /**
12
14
  * Create a list of OdataRequestInBatch from an incoming batch request.
13
15
  */
@@ -86,13 +88,14 @@ class BatchRequestListBuilder {
86
88
  // create reader and start with ContentReader
87
89
  const reader = new ContentReader(parser, this._emitter)
88
90
 
91
+ // restore lost context via AsyncResource.bind()
89
92
  source
90
93
  .pipe(reader)
91
- .on('finish', () => {
94
+ .on('finish', AsyncResource.bind(() => {
92
95
  //Revisit: if statement needed in node v12 and v14, not in v16.
93
96
  //Without it, finish callback reached in 12/14 after error handler was thrown
94
97
  if (!source.res || !source.res.headersSent) callback(null, this._requestInBatchList)
95
- })
98
+ }))
96
99
  .on('error', callback)
97
100
  }
98
101
 
@@ -12,7 +12,7 @@ class MetadataHandler {
12
12
  */
13
13
  static read (request, response, next) {
14
14
  const metadataETag = request.getService().getMetadataEtag()
15
- if (metadataETag) request.validateEtag(metadataETag)
15
+ // if (metadataETag) request.validateEtag(metadataETag)
16
16
 
17
17
  next(null, { [MetaProperties.ETAG]: metadataETag })
18
18
  }
@@ -16,7 +16,7 @@ class ServiceHandler {
16
16
  */
17
17
  static read (request, response, next) {
18
18
  const metadataETag = request.getService().getMetadataEtag()
19
- if (metadataETag) request.validateEtag(metadataETag)
19
+ // if (metadataETag) request.validateEtag(metadataETag)
20
20
  next(null, { [MetaProperties.ETAG]: metadataETag })
21
21
  // TODO this method may be replaced by a custom service handler, so we need a scenario test that ensures that this method is called...
22
22
  }
@@ -56,7 +56,7 @@ class DispatcherCommand extends Command {
56
56
 
57
57
  if (cachedMetadata) {
58
58
  this._response.setBody({ value: cachedMetadata.metadata, [MetaProperties.ETAG]: cachedMetadata.etag })
59
- this._request.validateEtag(cachedMetadata.etag)
59
+ // this._request.validateEtag(cachedMetadata.etag)
60
60
  next()
61
61
  } else {
62
62
  this._dispatcher
@@ -79,7 +79,7 @@ class DispatcherCommand extends Command {
79
79
  'Metadata size exceeds cache boundary. Use cds option odata.metadataCacheLimit to increase the cache size.'
80
80
  )
81
81
  }
82
- this._request.validateEtag(metadataEtag)
82
+ // this._request.validateEtag(metadataEtag)
83
83
  let data = result.data
84
84
  data[MetaProperties.ETAG] = metadataEtag
85
85
  }
@@ -24,9 +24,7 @@ class BufferedWriter extends Transform {
24
24
  }
25
25
  })
26
26
 
27
- // REVISIT: AsyncResource.bind()
28
- // We AsyncResource.bind() here (also for non middleware case!) to ensure subsequent handlers have access to cds.context -> this test would break if not, and there's an async handler before ours in the route: cds/tests/_runtime/odata/__tests__/integration/crud-with-mtx.test.js
29
- // Yet, if we do so this test breaks because the implementation of srv.on('error') is pretty screwed: cds/tests/runtime/req.test.js
27
+ // restore lost context via AsyncResource.bind()
30
28
  this.on('finish', AsyncResource.bind(() => {
31
29
  /**
32
30
  * Result event to emit the result data.
@@ -58,7 +58,7 @@ const _columnsFromQuery = (columns, target, options) => {
58
58
  // must use query.columns as it includes columns from $apply except of $apply=expand()
59
59
  // must use query options to get nested $selects inside $expand() as they are mixed into query columns
60
60
  // example: GET /Foo?$select=bar&$expand=bar => @odata.context: $metadata#Foo(bar,bar())
61
- // REVISIT tbd if having expand column in $select could be integrated into query in grammar.pegjs
61
+ // REVISIT tbd if having expand column in $select could be integrated into query in grammar.peggy
62
62
  // REVISIT support $apply=expand()
63
63
  if (_ignoreColumns(columns, options)) return ''
64
64
  const context = []
@@ -1,3 +1,5 @@
1
+ const parentDataSymbol = Symbol('parentData')
2
+ const cqnSymbol = Symbol('cqn')
1
3
  const cds = require('../../cds')
2
4
  const { getCompositionTree } = require('./tree')
3
5
  const { getDeepInsertCQNs } = require('./insert')
@@ -138,7 +140,13 @@ function _addSubDeepUpdateCQNForUpdateInsert({ entity, entityName, data, selectD
138
140
  const oldData = ctUtils.cleanDeepData(entity, selectEntry)
139
141
  const diff = _diffData(newData, oldData, entity, entry, selectEntry, model)
140
142
  // empty updates will be removed later
141
- updateCQNs.push({ UPDATE: { entity: entityName, data: diff, where: ctUtils.whereKey(key) } })
143
+ updateCQNs.push({
144
+ UPDATE: { entity: entityName, data: Object.assign({}, key, diff), where: ctUtils.whereKey(key) },
145
+ // We take tree information from data and store
146
+ // it in the `updateCQN` array (which itself is just a flat list)
147
+ parent: entry[parentDataSymbol]?.[cqnSymbol]
148
+ })
149
+ entry[cqnSymbol] = updateCQNs[updateCQNs.length - 1]
142
150
  } else {
143
151
  insertCQN.INSERT.entries.push(entry)
144
152
  // inserts are handled deep so they must not be put into deepUpdateData
@@ -189,7 +197,15 @@ const _addToData = (subData, entity, element, entry) => {
189
197
  const value = ctUtils.val(entry[element.name])
190
198
  const subDataEntries = ctUtils.array(value)
191
199
  const unwrappedSubData = subDataEntries.map(entry => _unwrapIfNotArray(entry))
192
- for (const val of unwrappedSubData) subData.push(val)
200
+ for (const val of unwrappedSubData) {
201
+ if (val != null) {
202
+ // We need to conserve the tree information which gets
203
+ // lost because we're creating flat arrays of layers.
204
+ Object.defineProperty(val, parentDataSymbol, { value: entry })
205
+ }
206
+
207
+ subData.push(val)
208
+ }
193
209
  }
194
210
 
195
211
  async function _addSubDeepUpdateCQNRecursion({ model, compositionTree, entity, data, selectData, cqns, draft, req }) {