@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.
- package/CHANGELOG.md +174 -126
- package/README.md +1 -1
- package/apis/connect.d.ts +1 -1
- package/apis/core.d.ts +6 -4
- package/apis/serve.d.ts +1 -1
- package/apis/services.d.ts +51 -31
- package/apis/test.d.ts +24 -10
- package/bin/serve.js +4 -3
- package/common.cds +4 -4
- package/lib/auth/ias-auth.js +7 -8
- package/lib/compile/cdsc.js +5 -7
- package/lib/compile/etc/csv.js +22 -11
- package/lib/dbs/cds-deploy.js +1 -2
- package/lib/env/cds-env.js +26 -20
- package/lib/env/defaults.js +4 -3
- package/lib/env/schema.js +9 -0
- package/lib/i18n/localize.js +83 -77
- package/lib/index.js +6 -2
- package/lib/linked/classes.js +13 -13
- package/lib/plugins.js +41 -45
- package/lib/req/user.js +2 -2
- package/lib/srv/protocols/_legacy.js +0 -1
- package/lib/srv/protocols/odata-v4.js +4 -0
- package/lib/utils/axios.js +7 -1
- package/lib/utils/cds-test.js +140 -133
- package/lib/utils/cds-utils.js +1 -1
- package/lib/utils/check-version.js +6 -0
- package/lib/utils/data.js +19 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +20 -19
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +10 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +2 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +0 -14
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/core/OdataRequest.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/BatchRequestListBuilder.js +5 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/MetadataHandler.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/ServiceHandler.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/invocation/DispatcherCommand.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -1
- package/libx/_runtime/common/composition/update.js +18 -2
- package/libx/_runtime/common/error/frontend.js +46 -34
- package/libx/_runtime/common/generic/auth/capabilities.js +33 -14
- package/libx/_runtime/common/generic/input.js +1 -1
- package/libx/_runtime/common/generic/paging.js +1 -0
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -3
- package/libx/_runtime/db/query/update.js +48 -30
- package/libx/_runtime/fiori/lean-draft.js +23 -24
- package/libx/_runtime/hana/conversion.js +3 -2
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -1
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/remote/Service.js +11 -26
- package/libx/_runtime/remote/utils/client.js +3 -2
- package/libx/_runtime/remote/utils/data.js +5 -7
- package/libx/odata/{grammar.pegjs → grammar.peggy} +1 -1
- package/libx/odata/metadata.js +121 -0
- package/libx/odata/parser.js +1 -1
- package/libx/odata/service-document.js +61 -0
- package/libx/odata/utils.js +102 -48
- package/libx/rest/RestAdapter.js +2 -2
- package/libx/rest/middleware/error.js +1 -1
- package/package.json +1 -1
package/lib/utils/cds-test.js
CHANGED
|
@@ -1,47 +1,71 @@
|
|
|
1
|
-
|
|
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 (
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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 (
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
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 (()
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
//
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
*
|
|
109
|
+
* Method to spy on a function in an object, similar to jest.spyOn().
|
|
72
110
|
*/
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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
|
-
/**
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
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
|
-
|
|
127
|
+
resolve({ server: this.server, url: this.url })
|
|
91
128
|
} else {
|
|
92
|
-
cds.once('listening',
|
|
129
|
+
this.cds.once('listening', resolve)
|
|
93
130
|
}
|
|
94
131
|
}
|
|
95
|
-
}
|
|
96
132
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
global
|
|
102
|
-
|
|
103
|
-
global.
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
116
|
-
|
|
117
|
-
|
|
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
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
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
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
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
|
-
|
|
169
|
-
|
|
170
|
-
return
|
|
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
|
-
|
|
177
|
-
|
|
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()
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -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('
|
|
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(
|
|
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
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
if (acceptHeader
|
|
233
|
-
if (acceptHeader.includes('
|
|
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
|
-
|
|
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
|
|
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
|
|
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.
|
|
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
|
-
//
|
|
17
|
-
|
|
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
|
|
package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js
CHANGED
|
@@ -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
|
|
package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/MetadataHandler.js
CHANGED
|
@@ -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
|
}
|
package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/handler/ServiceHandler.js
CHANGED
|
@@ -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
|
}
|
package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js
CHANGED
|
@@ -24,9 +24,7 @@ class BufferedWriter extends Transform {
|
|
|
24
24
|
}
|
|
25
25
|
})
|
|
26
26
|
|
|
27
|
-
//
|
|
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.
|
|
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({
|
|
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)
|
|
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 }) {
|