@sap/cds 8.6.1 → 8.7.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 +36 -0
- package/_i18n/i18n_en_US_saptrc.properties +4 -7
- package/bin/serve.js +3 -1
- package/lib/compile/for/lean_drafts.js +1 -1
- package/lib/compile/for/nodejs.js +1 -0
- package/lib/compile/to/sql.js +12 -8
- package/lib/core/classes.js +3 -4
- package/lib/core/types.js +1 -0
- package/lib/env/cds-requires.js +2 -2
- package/lib/ql/cds-ql.js +8 -1
- package/lib/ql/cds.ql-Query.js +9 -2
- package/lib/req/validate.js +3 -2
- package/lib/srv/cds-connect.js +1 -1
- package/lib/srv/cds-serve.js +2 -9
- package/lib/srv/cds.Service.js +0 -1
- package/lib/srv/factory.js +56 -71
- package/lib/srv/middlewares/auth/ias-auth.js +44 -14
- package/lib/srv/middlewares/auth/jwt-auth.js +45 -16
- package/lib/srv/middlewares/auth/xssec.js +1 -1
- package/lib/srv/middlewares/errors.js +8 -10
- package/lib/utils/cds-utils.js +5 -1
- package/lib/utils/tar-lib.js +58 -0
- package/libx/_runtime/common/Service.js +0 -4
- package/libx/_runtime/common/generic/auth/utils.js +1 -1
- package/libx/_runtime/common/generic/input.js +3 -1
- package/libx/_runtime/common/utils/csn.js +5 -1
- package/libx/_runtime/common/utils/resolveView.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-shared.js +7 -3
- package/libx/odata/middleware/batch.js +22 -23
- package/libx/odata/middleware/create.js +4 -0
- package/libx/odata/middleware/delete.js +2 -0
- package/libx/odata/middleware/operation.js +2 -0
- package/libx/odata/middleware/read.js +4 -0
- package/libx/odata/middleware/stream.js +4 -0
- package/libx/odata/middleware/update.js +4 -0
- package/libx/odata/parse/afterburner.js +9 -4
- package/libx/odata/parse/grammar.peggy +7 -8
- package/libx/odata/parse/multipartToJson.js +0 -1
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/normalizeTimeData.js +43 -0
- package/libx/odata/utils/readAfterWrite.js +1 -1
- package/libx/outbox/index.js +1 -1
- package/libx/rest/RestAdapter.js +20 -4
- package/package.json +6 -2
- package/lib/srv/protocols/odata-v2.js +0 -26
- package/libx/_runtime/common/code-ext/WorkerPool.js +0 -90
- package/libx/_runtime/common/code-ext/WorkerReq.js +0 -77
- package/libx/_runtime/common/code-ext/config.js +0 -13
- package/libx/_runtime/common/code-ext/execute.js +0 -123
- package/libx/_runtime/common/code-ext/handlers.js +0 -50
- package/libx/_runtime/common/code-ext/worker.js +0 -70
- package/libx/_runtime/common/code-ext/workerQueryExecutor.js +0 -37
|
@@ -1,17 +1,17 @@
|
|
|
1
1
|
const { isStandardError } = require('../../../libx/_runtime/common/error/standardError')
|
|
2
2
|
|
|
3
3
|
const production = process.env.NODE_ENV === 'production'
|
|
4
|
-
const cds = require
|
|
4
|
+
const cds = require('../..')
|
|
5
5
|
const LOG = cds.log('error')
|
|
6
6
|
const { inspect } = cds.utils
|
|
7
7
|
|
|
8
|
-
|
|
9
8
|
module.exports = () => {
|
|
9
|
+
// eslint-disable-next-line no-unused-vars
|
|
10
10
|
return async function http_error(error, req, res, next) {
|
|
11
11
|
if (isStandardError(error) && cds.env.server.shutdown_on_uncaught_errors) {
|
|
12
12
|
cds.log().error('❗️Uncaught', error)
|
|
13
13
|
await cds.shutdown(error)
|
|
14
|
-
return
|
|
14
|
+
return
|
|
15
15
|
}
|
|
16
16
|
|
|
17
17
|
// In case of 401 require login if available by auth strategy
|
|
@@ -26,11 +26,13 @@ module.exports = () => {
|
|
|
26
26
|
error.stack = error.stack.replace(/\n {4}at .*(?:node_modules\/express|node:internal).*/g, '')
|
|
27
27
|
|
|
28
28
|
if (400 <= status && status < 500) {
|
|
29
|
-
LOG.warn
|
|
29
|
+
LOG.warn(status, '>', inspect(error))
|
|
30
30
|
} else {
|
|
31
|
-
LOG.error
|
|
31
|
+
LOG.error(status, '>', inspect(error))
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
+
if (res.headersSent) return
|
|
35
|
+
|
|
34
36
|
// Expose as little information as possible in production, and as much as possible in development
|
|
35
37
|
if (production) {
|
|
36
38
|
Object.defineProperties(error, {
|
|
@@ -44,14 +46,10 @@ module.exports = () => {
|
|
|
44
46
|
})
|
|
45
47
|
}
|
|
46
48
|
|
|
47
|
-
if (res.headersSent) {
|
|
48
|
-
return next(error)
|
|
49
|
-
}
|
|
50
|
-
|
|
51
49
|
// Send the error response
|
|
52
50
|
return res.status(status).json({ error })
|
|
53
51
|
|
|
54
52
|
// Note: express returns errors as XML, we prefer JSON
|
|
55
|
-
//
|
|
53
|
+
// next(error)
|
|
56
54
|
}
|
|
57
55
|
}
|
package/lib/utils/cds-utils.js
CHANGED
|
@@ -1,6 +1,10 @@
|
|
|
1
1
|
const cwd = process.env._original_cwd || process.cwd()
|
|
2
2
|
const cds = require('../index')
|
|
3
3
|
|
|
4
|
+
/* eslint no-empty: ["error", { "allowEmptyCatch": true }] */
|
|
5
|
+
// eslint-disable-next-line no-unused-vars
|
|
6
|
+
const _tarLib = () => { try { return require('tar') } catch(_) {} }
|
|
7
|
+
|
|
4
8
|
module.exports = exports = new class {
|
|
5
9
|
get colors() { return super.colors = require('./colors') }
|
|
6
10
|
get inflect() { return super.inflect = require('./inflect') }
|
|
@@ -16,7 +20,7 @@ module.exports = exports = new class {
|
|
|
16
20
|
get uuid() { return super.uuid = require('crypto').randomUUID }
|
|
17
21
|
get yaml() { return super.yaml = require('@sap/cds-foss').yaml }
|
|
18
22
|
get pool() { return super.pool = require('@sap/cds-foss').pool }
|
|
19
|
-
get tar()
|
|
23
|
+
get tar() { return super.tar = process.platform === 'win32' && _tarLib() ? require('./tar-lib') : require('./tar') }
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
/** @type {import('node:path')} */
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
const cds = require('../index'), { path, mkdirp } = cds.utils
|
|
2
|
+
const tar = require('tar')
|
|
3
|
+
const { Readable } = require('stream')
|
|
4
|
+
const cons = require('stream/consumers')
|
|
5
|
+
|
|
6
|
+
const _resolve = (...x) => path.resolve (cds.root,...x)
|
|
7
|
+
|
|
8
|
+
exports.create = async (root, ...args) => {
|
|
9
|
+
if (typeof root === 'string') root = _resolve(root)
|
|
10
|
+
if (Array.isArray(root)) [ root, ...args ] = [ cds.root, root, ...args ]
|
|
11
|
+
|
|
12
|
+
const options = {}
|
|
13
|
+
if (args.includes('-z')) options.gzip = true
|
|
14
|
+
const index = args.findIndex(el => el === '-f')
|
|
15
|
+
if (index>=0) options.file = _resolve(args[index+1])
|
|
16
|
+
options.cwd = root
|
|
17
|
+
|
|
18
|
+
let dirs = []
|
|
19
|
+
for (let i=0; i<args.length; i++) {
|
|
20
|
+
if (args[i] === '-z' || args[i] === '-f') break
|
|
21
|
+
if (Array.isArray(args[i])) args[i].forEach(a => dirs.push(_resolve(a)))
|
|
22
|
+
else if (typeof args[i] === 'string') dirs.push(_resolve(args[i]))
|
|
23
|
+
}
|
|
24
|
+
if (!dirs.length) dirs.push(root)
|
|
25
|
+
dirs = dirs.map(d => path.relative(root, d))
|
|
26
|
+
|
|
27
|
+
const stream = await tar.c(options, dirs)
|
|
28
|
+
|
|
29
|
+
return stream && await cons.buffer(stream)
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
exports.extract = (archive, ...args) => ({
|
|
33
|
+
async to (dest) {
|
|
34
|
+
if (typeof dest === 'string') dest = _resolve(dest)
|
|
35
|
+
const stream = Readable.from(archive)
|
|
36
|
+
|
|
37
|
+
const options = { C: dest }
|
|
38
|
+
if (args.includes('-z')) options.gzip = true
|
|
39
|
+
|
|
40
|
+
return new Promise((resolve, reject) => {
|
|
41
|
+
const tr = tar.x(options)
|
|
42
|
+
stream.pipe(tr)
|
|
43
|
+
tr.on('close', () => resolve())
|
|
44
|
+
tr.on('error', e => reject(e))
|
|
45
|
+
})
|
|
46
|
+
}
|
|
47
|
+
})
|
|
48
|
+
|
|
49
|
+
const tar_ = exports
|
|
50
|
+
exports.c = tar_.create
|
|
51
|
+
exports.cz = (d,...args) => tar_.c (d, ...args, '-z')
|
|
52
|
+
exports.cf = (t,d,...args) => tar_.c (d, ...args, '-f',t)
|
|
53
|
+
exports.czf = (t,d,...args) => tar_.c (d, ...args, '-z', '-f',t)
|
|
54
|
+
exports.czfd = (t,...args) => mkdirp(path.dirname(t)).then (()=> tar_.czf (t,...args))
|
|
55
|
+
exports.x = tar_.xf = tar_.extract
|
|
56
|
+
exports.xz = tar_.xzf = a => tar_.x (a, '-z')
|
|
57
|
+
exports.xv = tar_.xvf = a => tar_.x (a)
|
|
58
|
+
exports.xvz = tar_.xvzf = a => tar_.x (a, '-z')
|
|
@@ -62,10 +62,6 @@ class ApplicationService extends cds.Service {
|
|
|
62
62
|
require('./generic/sorting').call(this)
|
|
63
63
|
}
|
|
64
64
|
|
|
65
|
-
static handle_code_ext() {
|
|
66
|
-
if (cds.env.requires.extensibility?.code) require('./code-ext/handlers').call(this)
|
|
67
|
-
}
|
|
68
|
-
|
|
69
65
|
static handle_fiori() {
|
|
70
66
|
require('../fiori/lean-draft').impl.call(this)
|
|
71
67
|
}
|
|
@@ -37,7 +37,7 @@ const getRejectReason = (req, annotation, definition, restrictedCount, unrestric
|
|
|
37
37
|
}
|
|
38
38
|
|
|
39
39
|
const _isNull = element => element.val === null || element.list?.length === 0
|
|
40
|
-
const _isNotNull = element => element.val !== null &&
|
|
40
|
+
const _isNotNull = element => element.val !== null && element.list?.length > 0
|
|
41
41
|
|
|
42
42
|
const _processNullAttr = where => {
|
|
43
43
|
if (!where) return
|
|
@@ -212,7 +212,7 @@ const _pick = element => {
|
|
|
212
212
|
// should be a db feature, as we cannot handle completely on service level (cf. deep update)
|
|
213
213
|
// -> add to attic env behavior once new dbs handle this
|
|
214
214
|
// also happens in validate but because of draft activate we have to do it twice (where cleansing is suppressed)
|
|
215
|
-
if (element['@Core.Immutable']) {
|
|
215
|
+
if (element['@Core.Immutable'] && !element.key) {
|
|
216
216
|
categories.push('immutable')
|
|
217
217
|
}
|
|
218
218
|
|
|
@@ -260,6 +260,8 @@ async function commonGenericInput(req) {
|
|
|
260
260
|
const bound = req.target.actions?.[req.event] || req.target.actions?.[req._.event]
|
|
261
261
|
if (bound) assertOptions.path = [bound['@cds.odata.bindingparameter.name'] || 'in']
|
|
262
262
|
|
|
263
|
+
if (req.protocol) assertOptions.rejectIgnore = true
|
|
264
|
+
|
|
263
265
|
const errs = cds.validate(req.data, req.target, assertOptions)
|
|
264
266
|
if (errs) {
|
|
265
267
|
if (errs.length === 1) throw errs[0]
|
|
@@ -115,19 +115,23 @@ const prefixForStruct = element => {
|
|
|
115
115
|
function getDraftTreeRoot(entity, model) {
|
|
116
116
|
if (entity.own('__draftTreeRoot')) return entity.__draftTreeRoot
|
|
117
117
|
|
|
118
|
+
const previous = new Set() // track visited entities to identify hierarchies
|
|
118
119
|
let parent
|
|
119
120
|
let current = entity
|
|
120
121
|
while (current && !current['@Common.DraftRoot.ActivationAction']) {
|
|
122
|
+
previous.add(current.name)
|
|
121
123
|
const parents = []
|
|
122
124
|
for (const k in model.definitions) {
|
|
125
|
+
if (previous.has(k)) continue
|
|
123
126
|
const e = model.definitions[k]
|
|
124
127
|
if (e.kind !== 'entity' || !e.compositions) continue
|
|
125
128
|
for (const c in e.compositions)
|
|
126
129
|
if (
|
|
127
130
|
e.compositions[c].target === current.name ||
|
|
128
131
|
e.compositions[c].target === current.name.replace(/\.drafts/, '')
|
|
129
|
-
)
|
|
132
|
+
) {
|
|
130
133
|
parents.push(e)
|
|
134
|
+
}
|
|
131
135
|
}
|
|
132
136
|
if (parents.length > 1 && parents.some(p => p !== parents[0])) {
|
|
133
137
|
// > unable to determine single parent
|
|
@@ -34,7 +34,7 @@ const _inverseTransition = transition => {
|
|
|
34
34
|
const ref0 = value.ref[0]
|
|
35
35
|
if (value.ref.length > 1) {
|
|
36
36
|
// ignore flattened columns like author.name
|
|
37
|
-
if (transition.target.elements[ref0]
|
|
37
|
+
if (transition.target.elements[ref0]?.isAssociation) continue
|
|
38
38
|
|
|
39
39
|
const nested = inverseTransition.mapping.get(ref0) || {}
|
|
40
40
|
if (!nested.transition) nested.transition = { mapping: new Map() }
|
|
@@ -1509,7 +1509,7 @@ function expandStarStar(target, draftActivate, recursion = new Map()) {
|
|
|
1509
1509
|
}
|
|
1510
1510
|
|
|
1511
1511
|
async function onNewCleanse(req) {
|
|
1512
|
-
cds.validate(req.data, req.target, {})
|
|
1512
|
+
cds.validate(req.data, req.target, { insert: true })
|
|
1513
1513
|
}
|
|
1514
1514
|
onNewCleanse._initial = true
|
|
1515
1515
|
async function onNew(req) {
|
|
@@ -14,13 +14,17 @@ class EnterpriseMessagingShared extends AMQPWebhookMessaging {
|
|
|
14
14
|
|
|
15
15
|
getClient() {
|
|
16
16
|
if (this.client) return this.client
|
|
17
|
+
this.client = new AMQPClient(this.getClientOptions())
|
|
18
|
+
return this.client
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
getClientOptions() {
|
|
17
22
|
const optionsAMQP = optionsMessagingAMQP(this.options)
|
|
18
|
-
|
|
23
|
+
return {
|
|
19
24
|
optionsAMQP,
|
|
20
25
|
prefix: { topic: 'topic:', queue: 'queue:' },
|
|
21
26
|
service: this
|
|
22
|
-
}
|
|
23
|
-
return this.client
|
|
27
|
+
}
|
|
24
28
|
}
|
|
25
29
|
|
|
26
30
|
getManagement() {
|
|
@@ -167,30 +167,23 @@ const _createExpressReqResLookalike = (request, _req, _res) => {
|
|
|
167
167
|
}
|
|
168
168
|
|
|
169
169
|
const _writeResponseMultipart = (responses, res, rejected, group, boundary) => {
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
res.write(`content-type: multipart/mixed;boundary=${group}${CRLF}${CRLF}`)
|
|
173
|
-
}
|
|
174
|
-
const header = group || boundary
|
|
170
|
+
res.write(`--${boundary}${CRLF}`)
|
|
171
|
+
|
|
175
172
|
if (rejected) {
|
|
176
173
|
const resp = responses.find(r => r.status === 'fail')
|
|
177
|
-
if (resp.separator && res._writeSeparator) res.write(resp.separator)
|
|
178
174
|
resp.txt.forEach(txt => {
|
|
179
|
-
res.write(
|
|
180
|
-
res.write(`${txt}`)
|
|
175
|
+
res.write(`${txt}${CRLF}`)
|
|
181
176
|
})
|
|
182
177
|
} else {
|
|
178
|
+
if (group) res.write(`content-type: multipart/mixed;boundary=${group}${CRLF}${CRLF}`)
|
|
183
179
|
for (const resp of responses) {
|
|
184
|
-
if (resp.separator) res.write(resp.separator)
|
|
185
180
|
resp.txt.forEach(txt => {
|
|
186
|
-
res.write(`--${
|
|
187
|
-
res.write(`${txt}`)
|
|
181
|
+
if (group) res.write(`--${group}${CRLF}`)
|
|
182
|
+
res.write(`${txt}${CRLF}`)
|
|
188
183
|
})
|
|
189
184
|
}
|
|
185
|
+
if (group) res.write(`--${group}--${CRLF}`)
|
|
190
186
|
}
|
|
191
|
-
if (group) res.write(`${CRLF}--${group}--${CRLF}`)
|
|
192
|
-
// indicates that we need to write a potential separator before the next error response
|
|
193
|
-
res._writeSeparator = true
|
|
194
187
|
}
|
|
195
188
|
|
|
196
189
|
const _writeResponseJson = (responses, res) => {
|
|
@@ -281,12 +274,18 @@ const _tx_done = async (tx, responses, isJson) => {
|
|
|
281
274
|
delete txt.headers['content-length']
|
|
282
275
|
res.txt = [JSON.stringify(txt)]
|
|
283
276
|
} else {
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
277
|
+
const commitError = [
|
|
278
|
+
'content-type: application/http',
|
|
279
|
+
'content-transfer-encoding: binary',
|
|
280
|
+
'',
|
|
281
|
+
`HTTP/1.1 ${statusCode} ${STATUS_CODES[statusCode]}`,
|
|
282
|
+
'odata-version: 4.0',
|
|
283
|
+
'content-type: application/json;odata.metadata=minimal;IEEE754Compatible=true',
|
|
284
|
+
'',
|
|
285
|
+
JSON.stringify({ error })
|
|
286
|
+
].join(CRLF)
|
|
287
|
+
res.txt = [commitError]
|
|
288
|
+
break
|
|
290
289
|
}
|
|
291
290
|
}
|
|
292
291
|
}
|
|
@@ -421,14 +420,14 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
421
420
|
.then(req => {
|
|
422
421
|
const resp = { status: 'ok' }
|
|
423
422
|
if (separator) resp.separator = separator
|
|
424
|
-
else separator =
|
|
423
|
+
else separator = Buffer.from(',')
|
|
425
424
|
resp.txt = _formatResponse(req, atomicityGroup)
|
|
426
425
|
responses.push(resp)
|
|
427
426
|
})
|
|
428
427
|
.catch(failedReq => {
|
|
429
428
|
const resp = { status: 'fail' }
|
|
430
429
|
if (separator) resp.separator = separator
|
|
431
|
-
else separator =
|
|
430
|
+
else separator = Buffer.from(',')
|
|
432
431
|
resp.txt = _formatResponse(failedReq, atomicityGroup)
|
|
433
432
|
tx.failed = failedReq
|
|
434
433
|
responses.push(resp)
|
|
@@ -446,7 +445,7 @@ const _processBatch = async (srv, router, req, res, next, body, ct, boundary) =>
|
|
|
446
445
|
? _writeResponseJson(responses, res)
|
|
447
446
|
: _writeResponseMultipart(responses, res, rejected, previousAtomicityGroup, boundary)
|
|
448
447
|
} else sendPreludeOnce()
|
|
449
|
-
res.write(isJson ? ']}' :
|
|
448
|
+
res.write(isJson ? ']}' : `--${boundary}--${CRLF}`)
|
|
450
449
|
res.end()
|
|
451
450
|
|
|
452
451
|
return
|
|
@@ -6,6 +6,7 @@ const getODataMetadata = require('../utils/metadata')
|
|
|
6
6
|
const postProcess = require('../utils/postProcess')
|
|
7
7
|
const readAfterWrite4 = require('../utils/readAfterWrite')
|
|
8
8
|
const getODataResult = require('../utils/result')
|
|
9
|
+
const normalizeTimeData = require('../utils/normalizeTimeData')
|
|
9
10
|
|
|
10
11
|
const { getKeysAndParamsFromPath } = require('../../common/utils')
|
|
11
12
|
|
|
@@ -32,6 +33,7 @@ module.exports = (adapter, isUpsert) => {
|
|
|
32
33
|
|
|
33
34
|
// payload & params
|
|
34
35
|
const data = req.body
|
|
36
|
+
normalizeTimeData(data, model, target)
|
|
35
37
|
const { keys, params } = getKeysAndParamsFromPath(from, { model })
|
|
36
38
|
// add keys from url into payload (overwriting if already present)
|
|
37
39
|
Object.assign(data, keys)
|
|
@@ -66,6 +68,8 @@ module.exports = (adapter, isUpsert) => {
|
|
|
66
68
|
})
|
|
67
69
|
})
|
|
68
70
|
.then(result => {
|
|
71
|
+
if (res.headersSent) return
|
|
72
|
+
|
|
69
73
|
handleSapMessages(cdsReq, req, res)
|
|
70
74
|
|
|
71
75
|
// case: read after write returns no results, e.g., due to auth (academic but possible)
|
|
@@ -101,6 +101,8 @@ module.exports = adapter => {
|
|
|
101
101
|
return service
|
|
102
102
|
.run(() => service.dispatch(cdsReq))
|
|
103
103
|
.then(result => {
|
|
104
|
+
if (res.headersSent) return
|
|
105
|
+
|
|
104
106
|
handleSapMessages(cdsReq, req, res)
|
|
105
107
|
|
|
106
108
|
if (operation.returns?.items && result == null) result = []
|
|
@@ -128,6 +128,8 @@ const _handleArrayOfQueriesFactory = adapter => {
|
|
|
128
128
|
})
|
|
129
129
|
})
|
|
130
130
|
.then(result => {
|
|
131
|
+
if (res.headersSent) return
|
|
132
|
+
|
|
131
133
|
handleSapMessages(cdsReq, req, res)
|
|
132
134
|
|
|
133
135
|
if (req.url.match(/\/\$count/)) return res.set('Content-Type', 'text/plain').send(_count(result).toString())
|
|
@@ -238,6 +240,8 @@ module.exports = adapter => {
|
|
|
238
240
|
})
|
|
239
241
|
})
|
|
240
242
|
.then(result => {
|
|
243
|
+
if (res.headersSent) return
|
|
244
|
+
|
|
241
245
|
handleSapMessages(cdsReq, req, res)
|
|
242
246
|
|
|
243
247
|
// 204
|
|
@@ -218,6 +218,8 @@ module.exports = adapter => {
|
|
|
218
218
|
return service
|
|
219
219
|
.run(() => {
|
|
220
220
|
return service.dispatch(cdsReq).then(async result => {
|
|
221
|
+
if (res.headersSent) return
|
|
222
|
+
|
|
221
223
|
_validateStream(req, result)
|
|
222
224
|
|
|
223
225
|
if (validateIfNoneMatch(cdsReq.target, req.headers?.['if-none-match'], result)) return res.sendStatus(304)
|
|
@@ -241,6 +243,8 @@ module.exports = adapter => {
|
|
|
241
243
|
})
|
|
242
244
|
})
|
|
243
245
|
.then(() => {
|
|
246
|
+
if (res.headersSent) return
|
|
247
|
+
|
|
244
248
|
handleSapMessages(cdsReq, req, res)
|
|
245
249
|
|
|
246
250
|
res.end()
|
|
@@ -6,6 +6,7 @@ const getODataMetadata = require('../utils/metadata')
|
|
|
6
6
|
const postProcess = require('../utils/postProcess')
|
|
7
7
|
const readAfterWrite4 = require('../utils/readAfterWrite')
|
|
8
8
|
const getODataResult = require('../utils/result')
|
|
9
|
+
const normalizeTimeData = require('../utils/normalizeTimeData')
|
|
9
10
|
|
|
10
11
|
const { getKeysAndParamsFromPath } = require('../../common/utils')
|
|
11
12
|
|
|
@@ -69,6 +70,7 @@ module.exports = adapter => {
|
|
|
69
70
|
|
|
70
71
|
// payload & params
|
|
71
72
|
const data = _propertyAccess ? { [_propertyAccess]: req.body.value } : req.body
|
|
73
|
+
normalizeTimeData(data, model, target)
|
|
72
74
|
const { keys, params } = getKeysAndParamsFromPath(from, { model })
|
|
73
75
|
// add keys from url into payload (overwriting if already present)
|
|
74
76
|
if (!_propertyAccess) Object.assign(data, keys)
|
|
@@ -112,6 +114,8 @@ module.exports = adapter => {
|
|
|
112
114
|
})
|
|
113
115
|
})
|
|
114
116
|
.then(result => {
|
|
117
|
+
if (res.headersSent) return
|
|
118
|
+
|
|
115
119
|
handleSapMessages(cdsReq, req, res)
|
|
116
120
|
|
|
117
121
|
// case: read after write returns no results, e.g., due to auth (academic but possible)
|
|
@@ -447,7 +447,7 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
447
447
|
_resolveAliasesInXpr(ref[i].where, current)
|
|
448
448
|
_processWhere(ref[i].where, current)
|
|
449
449
|
}
|
|
450
|
-
} else if (current.kind === 'element' && current.elements && i < ref.length - 1) {
|
|
450
|
+
} else if (current.kind === 'element' && current.type !== 'cds.Map' && current.elements && i < ref.length - 1) {
|
|
451
451
|
// > structured
|
|
452
452
|
continue
|
|
453
453
|
} else {
|
|
@@ -474,11 +474,16 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
474
474
|
Object.defineProperty(cqn, '_propertyAccess', { value: current.name, enumerable: false })
|
|
475
475
|
|
|
476
476
|
// if we end up with structured, keep path as is, if we end up with property in structured, cut off property
|
|
477
|
-
if (!current.elements) from.ref.splice(-1)
|
|
477
|
+
if (!current.elements || current.type === 'cds.Map') from.ref.splice(-1)
|
|
478
478
|
break
|
|
479
479
|
} else if (Object.keys(target.elements).includes(current.name)) {
|
|
480
480
|
if (!cqn.SELECT.columns) cqn.SELECT.columns = []
|
|
481
|
-
|
|
481
|
+
const propRef = ref.slice(i)
|
|
482
|
+
if (propRef[0].where?.length === 0) {
|
|
483
|
+
const msg = 'Parentheses are not allowed when addressing properties.'
|
|
484
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
485
|
+
}
|
|
486
|
+
cqn.SELECT.columns.push({ ref: propRef })
|
|
482
487
|
|
|
483
488
|
// we need the keys to generate the correct @odata.context
|
|
484
489
|
for (const key in target.keys || {}) {
|
|
@@ -650,7 +655,7 @@ const _doesNotExistError = (isExpand, refName, targetName, targetKind) => {
|
|
|
650
655
|
function _validateXpr(xpr, target, isOne, model, aliases = []) {
|
|
651
656
|
if (!xpr) return []
|
|
652
657
|
|
|
653
|
-
const ignoredColumns = Object.values(target
|
|
658
|
+
const ignoredColumns = Object.values(target?.elements ?? {})
|
|
654
659
|
.filter(element => element['@cds.api.ignore'] && !element.isAssociation)
|
|
655
660
|
.map(element => element.name)
|
|
656
661
|
const _aliases = []
|
|
@@ -74,7 +74,7 @@
|
|
|
74
74
|
(col.ref && exp.ref && col.ref.join('') === exp.ref.join(''))
|
|
75
75
|
const _remapFunc = columns => c => {
|
|
76
76
|
if (Array.isArray(c)) return c.map(_remapFunc(columns))
|
|
77
|
-
const fnObj = c.ref && columns.find(col => col
|
|
77
|
+
const fnObj = c.ref && columns.find(col => 'func' in col && col.as && col.as === c.ref[0])
|
|
78
78
|
if (fnObj) return fnObj
|
|
79
79
|
return c
|
|
80
80
|
}
|
|
@@ -175,12 +175,11 @@
|
|
|
175
175
|
return columns
|
|
176
176
|
}, [])
|
|
177
177
|
: aggregatedColumns
|
|
178
|
-
if (cqn.where) {
|
|
179
|
-
cqn.
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
}
|
|
178
|
+
if (cqn.where && cqn.groupBy) {
|
|
179
|
+
cqn.having = cqn.where
|
|
180
|
+
.map(_remapFunc(cqn.columns))
|
|
181
|
+
.map(_replaceNullRef(cqn.groupBy))
|
|
182
|
+
delete cqn.where
|
|
184
183
|
}
|
|
185
184
|
// expand navigation refs in aggregated columns
|
|
186
185
|
cqn.columns = cqn.columns.reduce((columns, col) => {
|
|
@@ -823,7 +822,7 @@
|
|
|
823
822
|
// / mathCalc - needs CAP support
|
|
824
823
|
)
|
|
825
824
|
func:aggregateWith? aggregateFrom? as:asAlias?
|
|
826
|
-
{ return { func, args: [ path ], as } }
|
|
825
|
+
{ return { func, args: [ path ], as: as ?? path.ref[0] } }
|
|
827
826
|
/ identifier OPEN aggregateExpr CLOSE // needs CAP support
|
|
828
827
|
// / customAggregate // needs CAP support
|
|
829
828
|
aggregateWith
|
|
@@ -138,7 +138,6 @@ const _parseStream = async function* (body, boundary) {
|
|
|
138
138
|
|
|
139
139
|
if (typeof ret !== 'number') {
|
|
140
140
|
if (ret.message === 'Parse Error') {
|
|
141
|
-
// console.trace(ret, ret.bytesParsed ? `\n\nin:\n${changed.substr(0, ret.bytesParsed + 1)}\n\n` : '')
|
|
142
141
|
ret.statusCode = 400
|
|
143
142
|
ret.message = `Error while parsing batch body at position ${ret.bytesParsed}: ${ret.reason}`
|
|
144
143
|
}
|