@sap/cds 7.6.3 → 7.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 +38 -1
- package/_i18n/i18n.properties +3 -0
- package/app/index.js +18 -12
- package/bin/serve.js +51 -19
- package/common.cds +16 -0
- package/lib/auth/ias-auth.js +2 -2
- package/lib/auth/index.js +1 -1
- package/lib/auth/jwt-auth.js +1 -1
- package/lib/compile/cdsc.js +23 -11
- package/lib/compile/for/nodejs.js +2 -2
- package/lib/compile/for/odata.js +4 -0
- package/lib/compile/load.js +7 -2
- package/lib/compile/to/sql.js +3 -0
- package/lib/dbs/cds-deploy.js +197 -220
- package/lib/env/defaults.js +2 -1
- package/lib/index.js +8 -2
- package/lib/linked/types.js +1 -0
- package/lib/log/format/json.js +1 -1
- package/lib/plugins.js +2 -2
- package/lib/ql/Query.js +1 -1
- package/lib/ql/SELECT.js +8 -8
- package/lib/req/context.js +22 -13
- package/lib/req/request.js +10 -4
- package/lib/srv/cds-connect.js +9 -3
- package/lib/srv/cds-serve.js +5 -3
- package/lib/srv/middlewares/ctx-model.js +1 -1
- package/lib/srv/protocols/odata-v4.js +38 -9
- package/lib/srv/srv-api.js +98 -140
- package/lib/srv/srv-models.js +2 -2
- package/lib/srv/srv-tx.js +1 -0
- package/lib/utils/cds-utils.js +32 -23
- package/lib/utils/data.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +18 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +7 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/http/HttpHeaderReader.js +4 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/index.js +5 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +71 -25
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +10 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +6 -1
- package/libx/_runtime/cds-services/util/assert.js +50 -240
- package/libx/_runtime/cds.js +5 -0
- package/libx/_runtime/common/aspects/any.js +53 -45
- package/libx/_runtime/common/generic/input.js +14 -10
- package/libx/_runtime/common/generic/paging.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/keys.js +1 -1
- package/libx/_runtime/common/utils/quotingStyles.js +1 -1
- package/libx/_runtime/common/utils/resolveStructured.js +4 -1
- package/libx/_runtime/common/utils/rewriteAsterisks.js +5 -12
- package/libx/_runtime/common/utils/stream.js +2 -16
- package/libx/_runtime/common/utils/streamProp.js +16 -6
- package/libx/_runtime/common/utils/ucsn.js +1 -0
- package/libx/_runtime/db/utils/columns.js +6 -1
- package/libx/_runtime/fiori/generic/activate.js +11 -3
- package/libx/_runtime/fiori/generic/edit.js +8 -2
- package/libx/_runtime/fiori/lean-draft.js +99 -30
- package/libx/_runtime/hana/execute.js +2 -5
- package/libx/_runtime/messaging/service.js +6 -2
- package/libx/common/assert/index.js +232 -0
- package/libx/common/assert/type.js +109 -0
- package/libx/common/assert/utils.js +125 -0
- package/libx/common/assert/validation.js +109 -0
- package/libx/odata/index.js +5 -5
- package/libx/odata/middleware/create.js +83 -0
- package/libx/odata/middleware/delete.js +38 -0
- package/libx/odata/middleware/error.js +8 -0
- package/libx/odata/{metadata.js → middleware/metadata.js} +8 -6
- package/libx/odata/middleware/operation.js +78 -0
- package/libx/odata/middleware/parse.js +11 -0
- package/libx/odata/{read.js → middleware/read.js} +42 -20
- package/libx/odata/{service-document.js → middleware/service-document.js} +2 -1
- package/libx/odata/middleware/stream.js +237 -0
- package/libx/odata/middleware/update.js +165 -0
- package/libx/odata/{afterburner.js → parse/afterburner.js} +79 -29
- package/libx/odata/{cqn2odata.js → parse/cqn2odata.js} +5 -3
- package/libx/odata/{parseToCqn.js → parse/parseToCqn.js} +3 -6
- package/libx/odata/{utils.js → utils/index.js} +91 -9
- package/libx/outbox/index.js +5 -4
- package/libx/rest/RestAdapter.js +0 -1
- package/libx/rest/middleware/operation.js +6 -4
- package/libx/rest/middleware/parse.js +20 -2
- package/package.json +1 -1
- package/server.js +43 -71
- package/libx/odata/create.js +0 -44
- package/libx/odata/delete.js +0 -25
- package/libx/odata/error.js +0 -12
- package/libx/odata/update.js +0 -110
- /package/libx/odata/{grammar.peggy → parse/grammar.peggy} +0 -0
- /package/libx/odata/{parser.js → parse/parser.js} +0 -0
- /package/libx/odata/{result.js → utils/result.js} +0 -0
|
@@ -2,7 +2,8 @@ const cds = require('../../cds')
|
|
|
2
2
|
const { ensureNoDraftsSuffix, ensureUnlocalized } = require('../../fiori/utils/handler')
|
|
3
3
|
const { isDuplicate } = require('./rewriteAsterisks')
|
|
4
4
|
|
|
5
|
-
const _addColumn = (name, type, columns) => {
|
|
5
|
+
const _addColumn = (name, type, columns, url) => {
|
|
6
|
+
if (cds.env.features.odata_new_adapter) return
|
|
6
7
|
if (typeof type === 'object') {
|
|
7
8
|
let mType = type['='].replaceAll(/\./g, '_')
|
|
8
9
|
const ref = {
|
|
@@ -14,13 +15,22 @@ const _addColumn = (name, type, columns) => {
|
|
|
14
15
|
const val = { val: type, as: `${name}@odata.mediaContentType` }
|
|
15
16
|
if (!columns.find(isDuplicate(val))) columns.push(val)
|
|
16
17
|
}
|
|
18
|
+
|
|
19
|
+
if (url) {
|
|
20
|
+
const ref = {
|
|
21
|
+
ref: [name],
|
|
22
|
+
as: `${name}@odata.mediaReadLink`
|
|
23
|
+
}
|
|
24
|
+
if (!columns.find(isDuplicate(ref))) columns.push(ref)
|
|
25
|
+
}
|
|
17
26
|
}
|
|
18
27
|
|
|
19
28
|
const _addColumns = (target, columns) => {
|
|
29
|
+
if (cds.env.features.odata_new_adapter) return
|
|
20
30
|
for (const k in target.elements) {
|
|
21
31
|
const el = target.elements[k]
|
|
22
32
|
if (el['@Core.MediaType']) {
|
|
23
|
-
_addColumn(el.name, el['@Core.MediaType'], columns)
|
|
33
|
+
_addColumn(el.name, el['@Core.MediaType'], columns, el['@Core.IsURL'])
|
|
24
34
|
}
|
|
25
35
|
}
|
|
26
36
|
}
|
|
@@ -38,11 +48,11 @@ const handleStreamProperties = (target, columns, model) => {
|
|
|
38
48
|
|
|
39
49
|
if (col === '*') {
|
|
40
50
|
_addColumns(target, columns)
|
|
41
|
-
} else if (col.ref && type === 'cds.LargeBinary') {
|
|
51
|
+
} else if (col.ref && (type === 'cds.LargeBinary' || mediaType)) {
|
|
42
52
|
if (mediaType) {
|
|
43
|
-
_addColumn(name, mediaType, columns)
|
|
44
|
-
|
|
45
|
-
if (
|
|
53
|
+
_addColumn(name, mediaType, columns, element['@Core.IsURL'])
|
|
54
|
+
columns.splice(index, 1)
|
|
55
|
+
} else if (!cds.env.features.stream_compat) {
|
|
46
56
|
columns.splice(index, 1)
|
|
47
57
|
}
|
|
48
58
|
} else if (col.expand && col.ref) {
|
|
@@ -100,6 +100,7 @@ const _cleanup = (row, definition, cleanupNull, cleanupStruct, errors, prefix =
|
|
|
100
100
|
}
|
|
101
101
|
}
|
|
102
102
|
|
|
103
|
+
// REVISIT: when needed?
|
|
103
104
|
function convertStructured(service, definition, data, { cleanupNull = false, cleanupStruct = false, errors } = {}) {
|
|
104
105
|
if (!definition) return
|
|
105
106
|
// REVISIT check `structs` mode only for now as uCSN is not yet available
|
|
@@ -3,6 +3,10 @@ const resolveStructured = require('../../common/utils/resolveStructured')
|
|
|
3
3
|
|
|
4
4
|
const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
5
5
|
|
|
6
|
+
const _isStreamProperty = element => {
|
|
7
|
+
return element.type === 'cds.LargeBinary' || element['@Core.IsURL']
|
|
8
|
+
}
|
|
9
|
+
|
|
6
10
|
/**
|
|
7
11
|
* This method gets all columns for an entity.
|
|
8
12
|
* It includes the generated foreign keys from managed associations, structured elements and complex and custom types.
|
|
@@ -11,7 +15,7 @@ const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
|
11
15
|
* @param entity - the csn entity
|
|
12
16
|
* @returns {Array} - array of columns
|
|
13
17
|
*/
|
|
14
|
-
const getColumns = (entity, { _4db, onlyKeys } = { _4db: true, onlyKeys: false }) => {
|
|
18
|
+
const getColumns = (entity, { _4db, onlyKeys, omitStream } = { _4db: true, onlyKeys: false, omitStream: false }) => {
|
|
15
19
|
// REVISIT is this correct or just a problem that occurs because of new structure we do not deal with yet?
|
|
16
20
|
if (!(entity && entity.elements)) return []
|
|
17
21
|
const columnNames = []
|
|
@@ -23,6 +27,7 @@ const getColumns = (entity, { _4db, onlyKeys } = { _4db: true, onlyKeys: false }
|
|
|
23
27
|
const element = elements[elementName]
|
|
24
28
|
if (element['@cds.api.ignore']) continue
|
|
25
29
|
if (onlyKeys && !element.key) continue
|
|
30
|
+
if (omitStream && _isStreamProperty(element)) continue
|
|
26
31
|
if (element.isAssociation) continue
|
|
27
32
|
if (!lean_draft && _4db && entity._isDraftEnabled && elementName in DRAFT_COLUMNS_MAP) continue
|
|
28
33
|
if (structs && element.elements) {
|
|
@@ -171,9 +171,17 @@ const fioriGenericActivate = async function (req, next) {
|
|
|
171
171
|
})
|
|
172
172
|
])
|
|
173
173
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
174
|
+
if (event === 'CREATE') {
|
|
175
|
+
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
176
|
+
// status code must be set in handler to allow overriding for FE V2
|
|
177
|
+
// REVISIT: needs reworking for new adapter, especially re $batch
|
|
178
|
+
if (req._?.odataRes) {
|
|
179
|
+
req._?.odataRes?.setStatusCode(201, { overwrite: true })
|
|
180
|
+
} else if (req.http?.res) {
|
|
181
|
+
req.http.res.status(201)
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
|
|
177
185
|
return result
|
|
178
186
|
}
|
|
179
187
|
|
|
@@ -141,8 +141,14 @@ const fioriGenericEdit = async function (req, next) {
|
|
|
141
141
|
await Promise.all(insertCQNs.map(CQN => dbtx.run(CQN)))
|
|
142
142
|
|
|
143
143
|
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
144
|
-
//
|
|
145
|
-
|
|
144
|
+
// status code must be set in handler to allow overriding for FE V2
|
|
145
|
+
// REVISIT: needs reworking for new adapter, especially re $batch
|
|
146
|
+
if (req._?.odataRes) {
|
|
147
|
+
req._?.odataRes?.setStatusCode(201, { overwrite: true })
|
|
148
|
+
} else if (req.http?.res) {
|
|
149
|
+
req.http.res.status(201)
|
|
150
|
+
}
|
|
151
|
+
|
|
146
152
|
return results[0][0]
|
|
147
153
|
}
|
|
148
154
|
|
|
@@ -8,31 +8,71 @@ const original = Symbol('original')
|
|
|
8
8
|
const DRAFT_PARAMS = Symbol('draftParams')
|
|
9
9
|
const AGGREGATION_FUNCTIONS = ['sum', 'min', 'max', 'avg', 'count']
|
|
10
10
|
|
|
11
|
+
const calcTimeMs = timeout => {
|
|
12
|
+
const match = timeout.match(/^([0-9]+)(w|d|h|hrs|min)$/)
|
|
13
|
+
if (!match) return
|
|
14
|
+
const [, val, t] = match
|
|
15
|
+
switch (t) {
|
|
16
|
+
case 'w':
|
|
17
|
+
return val * 1000 * 3600 * 24 * 7
|
|
18
|
+
case 'd':
|
|
19
|
+
return val * 1000 * 3600 * 24
|
|
20
|
+
case 'h':
|
|
21
|
+
case 'hrs':
|
|
22
|
+
return val * 1000 * 3600
|
|
23
|
+
case 'min':
|
|
24
|
+
return val * 1000 * 60
|
|
25
|
+
default:
|
|
26
|
+
return val
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const _config_to_ms = (config, _default) => {
|
|
31
|
+
const timeout = cds.env.fiori?.[config]
|
|
32
|
+
let timeout_ms
|
|
33
|
+
if (timeout === true) {
|
|
34
|
+
timeout_ms = calcTimeMs(_default)
|
|
35
|
+
} else if (typeof timeout === 'string') {
|
|
36
|
+
timeout_ms = calcTimeMs(timeout)
|
|
37
|
+
if (!timeout_ms)
|
|
38
|
+
throw new Error(`
|
|
39
|
+
${timeout} is an invalid value for \`cds.fiori.${config}\`.
|
|
40
|
+
Please provide a value in format /^([0-9]+)(w|d|h|hrs|min)$/.
|
|
41
|
+
`)
|
|
42
|
+
} else {
|
|
43
|
+
timeout_ms = timeout
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return timeout_ms
|
|
47
|
+
}
|
|
48
|
+
|
|
11
49
|
const DEL_TIMEOUT = {
|
|
12
50
|
get value() {
|
|
13
|
-
const
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
51
|
+
const timeout_ms = _config_to_ms('draft_deletion_timeout', '30d')
|
|
52
|
+
Object.defineProperty(DEL_TIMEOUT, 'value', { value: timeout_ms })
|
|
53
|
+
return timeout_ms
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const LOCK_TIMEOUT = {
|
|
58
|
+
get value() {
|
|
59
|
+
let timeout_ms = _config_to_ms('draft_lock_timeout', '15min')
|
|
60
|
+
|
|
61
|
+
const deprecated = cds.env.drafts?.cancellationTimeout // in min
|
|
62
|
+
if (deprecated) {
|
|
63
|
+
// in order to still support legacy use cases for tests, e. g. 0.000001
|
|
64
|
+
timeout_ms = deprecated * 1000 * 60
|
|
19
65
|
}
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
? Number(parts[1]) * 1000 * 3600 * 24
|
|
24
|
-
: Number(parts[1]) * 1000 * 3600
|
|
25
|
-
: Number(timeout) || 0
|
|
26
|
-
|
|
27
|
-
Object.defineProperty(DEL_TIMEOUT, 'value', { value: result })
|
|
28
|
-
return result
|
|
66
|
+
|
|
67
|
+
Object.defineProperty(LOCK_TIMEOUT, 'value', { value: timeout_ms })
|
|
68
|
+
return timeout_ms
|
|
29
69
|
}
|
|
30
70
|
}
|
|
31
71
|
|
|
32
72
|
const reject_bypassed_draft = req => {
|
|
33
73
|
const msg =
|
|
34
74
|
!cds.profiles?.includes('production') &&
|
|
35
|
-
'`cds.env.fiori.bypass_draft` must be enabled to support the directly modification of active instances.'
|
|
75
|
+
'`cds.env.fiori.bypass_draft` must be enabled or the entity must be annotated with `@odata.draft.bypass` to support the directly modification of active instances.'
|
|
36
76
|
return req.reject(501, msg)
|
|
37
77
|
}
|
|
38
78
|
|
|
@@ -116,13 +156,10 @@ const _inProcessByUserXpr = lockShiftedNow => ({
|
|
|
116
156
|
|
|
117
157
|
const _lock = {
|
|
118
158
|
get shiftedNow() {
|
|
119
|
-
return new Date(Math.max(0, Date.now() -
|
|
159
|
+
return new Date(Math.max(0, Date.now() - LOCK_TIMEOUT.value)).toISOString()
|
|
120
160
|
}
|
|
121
161
|
}
|
|
122
162
|
|
|
123
|
-
const DRAFT_CANCEL_TIMEOUT_IN_MIN = () =>
|
|
124
|
-
(cds.env.drafts?.cancellationTimeout && Number(cds.env.drafts?.cancellationTimeout)) || 15
|
|
125
|
-
|
|
126
163
|
const _redirectRefToDrafts = (ref, model) => {
|
|
127
164
|
const [root, ...tail] = ref
|
|
128
165
|
const draft = model.definitions[root.id || root].drafts
|
|
@@ -281,7 +318,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
281
318
|
if (req.event === 'NEW' || req.event === 'CANCEL' || req.event === 'draftPrepare') {
|
|
282
319
|
if (req.event === 'draftPrepare' && draftParams.IsActiveEntity) req.reject(400)
|
|
283
320
|
if (req.event === 'NEW' && req.data?.IsActiveEntity === true) {
|
|
284
|
-
if (!cds.env.fiori.bypass_draft) return reject_bypassed_draft(req)
|
|
321
|
+
if (!cds.env.fiori.bypass_draft && !req.target['@odata.draft.bypass']) return reject_bypassed_draft(req)
|
|
285
322
|
const containsDraftRoot =
|
|
286
323
|
this.model.definitions[query.INSERT.into?.ref?.[0]?.id || query.INSERT.into?.ref?.[0] || query.INSERT.into][
|
|
287
324
|
'@Common.DraftRoot.ActivationAction'
|
|
@@ -390,6 +427,19 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
390
427
|
const HasActiveEntity = res.HasActiveEntity
|
|
391
428
|
delete res.HasActiveEntity
|
|
392
429
|
|
|
430
|
+
if (cds.env.features.cds_assert) {
|
|
431
|
+
const assertOptions = { path: [req.target.actions[req.event]['@cds.odata.bindingparameter.name'] || 'in'] }
|
|
432
|
+
const errs = cds.assert(res, req.target, assertOptions)
|
|
433
|
+
if (errs) {
|
|
434
|
+
if (errs.length === 1) throw Object.assign(errs[0], { '@Common.numericSeverity': 4 })
|
|
435
|
+
throw Object.assign(new Error('MULTIPLE_ERRORS'), {
|
|
436
|
+
statusCode: 400,
|
|
437
|
+
details: errs,
|
|
438
|
+
'@Common.numericSeverity': 4 //> TODO: should not be needed here
|
|
439
|
+
})
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
|
|
393
443
|
// First run the handlers as they might need access to DraftAdministrativeData or the draft entities
|
|
394
444
|
const result = await run(
|
|
395
445
|
HasActiveEntity
|
|
@@ -402,12 +452,18 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
402
452
|
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
|
|
403
453
|
])
|
|
404
454
|
|
|
405
|
-
if (!HasActiveEntity)
|
|
455
|
+
if (!HasActiveEntity) {
|
|
456
|
+
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
457
|
+
// status code must be set in handler to allow overriding for FE V2
|
|
458
|
+
// REVISIT: needs reworking for new adapter, especially re $batch
|
|
459
|
+
if (req._?.odataRes) {
|
|
460
|
+
req._?.odataRes?.setStatusCode(201, { overwrite: true })
|
|
461
|
+
} else if (req.http?.res) {
|
|
462
|
+
req.http.res.status(201)
|
|
463
|
+
}
|
|
464
|
+
}
|
|
406
465
|
|
|
407
466
|
return Object.assign(result, { IsActiveEntity: true })
|
|
408
|
-
|
|
409
|
-
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
410
|
-
// status code must be set in handler to allow overriding for FE V2
|
|
411
467
|
}
|
|
412
468
|
|
|
413
469
|
if (req.target.actions?.[req.event] && draftParams.IsActiveEntity === false) {
|
|
@@ -457,7 +513,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
457
513
|
|
|
458
514
|
LOG.debug('patch active')
|
|
459
515
|
|
|
460
|
-
if (!cds.env.fiori.bypass_draft) return reject_bypassed_draft(req)
|
|
516
|
+
if (!cds.env.fiori.bypass_draft && !req.target['@odata.draft.bypass']) return reject_bypassed_draft(req)
|
|
461
517
|
|
|
462
518
|
const entityRef = query.UPDATE.entity.ref
|
|
463
519
|
|
|
@@ -549,6 +605,7 @@ const Read = {
|
|
|
549
605
|
unchanged: async function (run, query) {
|
|
550
606
|
LOG.debug('List Editing Status: Unchanged')
|
|
551
607
|
const draftsQuery = query._drafts
|
|
608
|
+
if (!draftsQuery) throw new Error('Invalid draft request')
|
|
552
609
|
draftsQuery.SELECT.count = undefined
|
|
553
610
|
draftsQuery.SELECT.orderBy = undefined
|
|
554
611
|
draftsQuery.SELECT.limit = null
|
|
@@ -564,6 +621,8 @@ const Read = {
|
|
|
564
621
|
ownDrafts: async function (run, query) {
|
|
565
622
|
LOG.debug('List Editing Status: Own Draft')
|
|
566
623
|
|
|
624
|
+
if (!query._drafts) throw new Error('Invalid draft request')
|
|
625
|
+
|
|
567
626
|
// read active from draft
|
|
568
627
|
if (!query._drafts._target?.name.endsWith('.drafts')) {
|
|
569
628
|
const result = await run(query._drafts)
|
|
@@ -758,6 +817,8 @@ const Read = {
|
|
|
758
817
|
|
|
759
818
|
activesFromDrafts: async function (run, query, { isLocked = true }) {
|
|
760
819
|
const draftsQuery = query._drafts
|
|
820
|
+
if (!draftsQuery) throw new Error('Invalid draft request')
|
|
821
|
+
|
|
761
822
|
const additionalCols = draftsQuery.SELECT.columns
|
|
762
823
|
? draftsQuery.SELECT.columns.filter(
|
|
763
824
|
c => c.ref && ['DraftAdministrativeData', 'DraftAdministrativeData_DraftUUID'].includes(c.ref[0])
|
|
@@ -1051,7 +1112,10 @@ function _cleansed(query, model) {
|
|
|
1051
1112
|
}
|
|
1052
1113
|
if (ignoredElements.has(e) && xpr[i + 2]) {
|
|
1053
1114
|
let { val } = xpr[i + 2]
|
|
1054
|
-
|
|
1115
|
+
const param = x.ref.join('_')
|
|
1116
|
+
// outer-most parameters win
|
|
1117
|
+
if (draftParams[param] === undefined)
|
|
1118
|
+
draftParams[param] = xpr[i + 1] === '!=' ? (typeof val === 'boolean' ? !val : 'not ' + val) : val
|
|
1055
1119
|
i += 2
|
|
1056
1120
|
const last = cleansed[cleansed.length - 1]
|
|
1057
1121
|
if (last === 'and' || last === 'or') cleansed.pop()
|
|
@@ -1293,7 +1357,7 @@ async function onEdit(req) {
|
|
|
1293
1357
|
}
|
|
1294
1358
|
|
|
1295
1359
|
if (!res) req.reject(404)
|
|
1296
|
-
const preserveChanges = req.
|
|
1360
|
+
const preserveChanges = req.data?.PreserveChanges
|
|
1297
1361
|
const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
|
|
1298
1362
|
|
|
1299
1363
|
if (draft) {
|
|
@@ -1327,8 +1391,13 @@ async function onEdit(req) {
|
|
|
1327
1391
|
await INSERT.into(targetDraft).entries(res)
|
|
1328
1392
|
|
|
1329
1393
|
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
1330
|
-
//
|
|
1331
|
-
|
|
1394
|
+
// status code must be set in handler to allow overriding for FE V2
|
|
1395
|
+
// REVISIT: needs reworking for new adapter, especially re $batch
|
|
1396
|
+
if (req._?.odataRes) {
|
|
1397
|
+
req._?.odataRes?.setStatusCode(201, { overwrite: true })
|
|
1398
|
+
} else if (req.http?.res) {
|
|
1399
|
+
req.http.res.status(201)
|
|
1400
|
+
}
|
|
1332
1401
|
|
|
1333
1402
|
return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
|
|
1334
1403
|
}
|
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
readStreamWithHdb
|
|
16
16
|
} = require('./streaming')
|
|
17
17
|
const { convertStream } = require('../db/utils/stream')
|
|
18
|
+
const { isBase64String } = require('../../common/assert/utils')
|
|
18
19
|
|
|
19
20
|
function _cqnToSQL(model, query, user, locale, txTimestamp) {
|
|
20
21
|
return sqlFactory(
|
|
@@ -48,8 +49,6 @@ function _getBinaries(stmt) {
|
|
|
48
49
|
}, [])
|
|
49
50
|
}
|
|
50
51
|
|
|
51
|
-
const BASE64 = /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{4}|[A-Za-z0-9+/]{3}=|[A-Za-z0-9+/]{2}={2})$/
|
|
52
|
-
|
|
53
52
|
function _getProcedureNameAndSchema(sql) {
|
|
54
53
|
// name delimited with "" allows any character
|
|
55
54
|
const match = sql
|
|
@@ -142,9 +141,7 @@ function _executeAsPreparedStatement(dbc, sql, values, reject, resolve) {
|
|
|
142
141
|
const vals = Array.isArray(values[0]) ? values : [values]
|
|
143
142
|
for (const i of binaries) {
|
|
144
143
|
for (const row of vals) {
|
|
145
|
-
if (row[i] &&
|
|
146
|
-
row[i] = Buffer.from(row[i], 'base64')
|
|
147
|
-
}
|
|
144
|
+
if (row[i] && isBase64String(row[i])) row[i] = Buffer.from(row[i], 'base64')
|
|
148
145
|
}
|
|
149
146
|
}
|
|
150
147
|
}
|
|
@@ -126,8 +126,12 @@ class MessagingService extends cds.Service {
|
|
|
126
126
|
const subscribedEvent =
|
|
127
127
|
this.subscribedTopics.get(_msg.event) ||
|
|
128
128
|
(this.wildcarded && this.subscribedTopics.get(this.wildcarded(_msg.event)))
|
|
129
|
-
if (!subscribedEvent && !this._listenToAll.value)
|
|
130
|
-
|
|
129
|
+
if (!subscribedEvent && !this._listenToAll.value) {
|
|
130
|
+
const err = new Error(`No handler for incoming message with topic '${_msg.event}' found.`)
|
|
131
|
+
err.code = 'NO_HANDLER_FOUND' // consumers might want to react to that
|
|
132
|
+
throw err
|
|
133
|
+
}
|
|
134
|
+
|
|
131
135
|
_msg.event = subscribedEvent || _msg.event
|
|
132
136
|
}
|
|
133
137
|
return _msg
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
const { cds } = global
|
|
2
|
+
|
|
3
|
+
const typeCheckers = require('./type')
|
|
4
|
+
const { checkMandatory, checkEnum, checkRange, checkFormat } = require('./validation')
|
|
5
|
+
const { getNested, getTarget, resolveCDSType, resolveSegment } = require('./utils')
|
|
6
|
+
|
|
7
|
+
const NUMBER_TYPES = new Set(['cds.UInt8', 'cds.Int16', 'cds.Int32', 'cds.Integer', 'cds.Double'])
|
|
8
|
+
|
|
9
|
+
const _no_op = () => {}
|
|
10
|
+
|
|
11
|
+
const _reject_unknown = (_, k, def, errs) =>
|
|
12
|
+
errs.push(new cds.error(`Property ${k} does not exist in ${def.name}`, { statusCode: 400, code: '400' }))
|
|
13
|
+
|
|
14
|
+
const _filter_unknown = (obj, k) => delete obj[k]
|
|
15
|
+
|
|
16
|
+
const _handle_mandatories = (obj, def, errs, path) => {
|
|
17
|
+
for (const [k, ele] of def._mandatories) {
|
|
18
|
+
const v = obj[k] === undefined ? getNested(k, obj) : obj[k]
|
|
19
|
+
checkMandatory(v, ele, errs, path, k)
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const _handle_mandatories_if_insert = (obj, def, errs, path) => {
|
|
24
|
+
if (!def.keys) return _handle_mandatories(obj, def, errs, path)
|
|
25
|
+
const allKeysProvided = Object.keys(def.keys).every(k => k in obj)
|
|
26
|
+
for (const [k, ele] of def._mandatories) {
|
|
27
|
+
const v = obj[k] === undefined ? getNested(k, obj) : obj[k]
|
|
28
|
+
if (!allKeysProvided || v !== undefined) checkMandatory(v, ele, errs, path, k)
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function _recurse(obj, prefix, def, errs, opts) {
|
|
33
|
+
for (const [k, v] of Object.entries(obj)) {
|
|
34
|
+
if (v != null && typeof v === 'object' && !Array.isArray(v)) {
|
|
35
|
+
_recurse(v, prefix + k + '_', def, errs, opts)
|
|
36
|
+
continue
|
|
37
|
+
}
|
|
38
|
+
const flat = { [prefix + k]: v }
|
|
39
|
+
_process(flat, def, errs, opts)
|
|
40
|
+
if (!Object.keys(flat).length) delete obj[k] //> filtered out inside _process -> propagate to original object
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function _process(obj, def, errs, opts) {
|
|
45
|
+
if (obj == null) return
|
|
46
|
+
|
|
47
|
+
if (Array.isArray(obj)) {
|
|
48
|
+
for (const row of obj) _process(row, def, errs, opts)
|
|
49
|
+
return
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
// TODO: path should be cqn
|
|
53
|
+
const prev = opts.path.length && opts.path[opts.path.length - 1]
|
|
54
|
+
if (prev?.keys || prev?.index) opts.path[opts.path.length - 1] = resolveSegment(prev, obj, def)
|
|
55
|
+
|
|
56
|
+
if (def._mandatories?.length) opts._handle_mandatories(obj, def, errs, opts.path)
|
|
57
|
+
|
|
58
|
+
for (let [k, v] of Object.entries(obj)) {
|
|
59
|
+
let ele = def.elements?.[k] || def.params?.[k] || def.items
|
|
60
|
+
|
|
61
|
+
/*
|
|
62
|
+
* TODO: should we support this? with or without transformation?
|
|
63
|
+
* structured vs flat
|
|
64
|
+
* the combination of the two cases below SHOULD cover mixed cases like
|
|
65
|
+
* foo: { bar: { baz: { ... } } } and foo_bar: { baz: { ... } }
|
|
66
|
+
* TODO: add tests!!!
|
|
67
|
+
*/
|
|
68
|
+
// case 1: structured data but flat model
|
|
69
|
+
if (
|
|
70
|
+
!ele &&
|
|
71
|
+
typeof obj[k] === 'object' &&
|
|
72
|
+
!Array.isArray(obj[k]) &&
|
|
73
|
+
(def.elements || def.params) &&
|
|
74
|
+
Object.keys(def.elements || def.params).find(key => key.startsWith(`${k}_`))
|
|
75
|
+
) {
|
|
76
|
+
_recurse(obj[k], k + '_', def, errs, opts)
|
|
77
|
+
continue
|
|
78
|
+
}
|
|
79
|
+
// case 2: flat data but structured model
|
|
80
|
+
if (!ele && k.split('_').length > 1) {
|
|
81
|
+
// TODO: handle stuff like foo__bar, i.e., foo_: { bar: ... }
|
|
82
|
+
const parts = k.split('_')
|
|
83
|
+
let cur = def.elements || def.params
|
|
84
|
+
while (cur && parts.length) cur = (cur.elements || cur.params)?.[parts.shift()]
|
|
85
|
+
if (cur) ele = cur
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (!ele) {
|
|
89
|
+
if (!def['@open']) opts._handle_unknown(obj, k, def, errs)
|
|
90
|
+
continue
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (ele.isAssociation) {
|
|
94
|
+
const keys = ele.keys?.map(k => k.ref[0]) || Object.keys(ele._target.keys)
|
|
95
|
+
opts.path.push(ele.is2many || Object.keys(keys).length ? { assoc: k, keys } : k)
|
|
96
|
+
// NOTE: the assumption is that children with all keys provided are not inserted, but updated
|
|
97
|
+
// -> incomplete but best we can do without roundtrip
|
|
98
|
+
_process(v, ele._target, errs, {
|
|
99
|
+
...opts,
|
|
100
|
+
_handle_mandatories:
|
|
101
|
+
opts.mandatories === false || ele._isAssociationStrict
|
|
102
|
+
? _no_op
|
|
103
|
+
: opts.mandatories === true
|
|
104
|
+
? _handle_mandatories
|
|
105
|
+
: _handle_mandatories_if_insert
|
|
106
|
+
})
|
|
107
|
+
opts.path.pop()
|
|
108
|
+
continue
|
|
109
|
+
}
|
|
110
|
+
if (ele._isStructured) {
|
|
111
|
+
opts.path.push(k)
|
|
112
|
+
_process(v, ele, errs, opts)
|
|
113
|
+
opts.path.pop()
|
|
114
|
+
continue
|
|
115
|
+
}
|
|
116
|
+
if (ele instanceof cds.builtin.classes.array) {
|
|
117
|
+
for (let i = 0; i < v.length; i++) {
|
|
118
|
+
opts.path.push({ prop: k, index: i })
|
|
119
|
+
const _def = ele.items?.__proto__.elements ? ele.items.__proto__ : ele.__proto__
|
|
120
|
+
const _obj = _def.elements ? v[i] : { [k]: v[i] }
|
|
121
|
+
_process(_obj, _def, errs, opts)
|
|
122
|
+
opts.path.pop()
|
|
123
|
+
}
|
|
124
|
+
continue
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (ele.notNull && v === null) {
|
|
128
|
+
const target = getTarget(opts.path, k)
|
|
129
|
+
errs.push(new cds.error('ASSERT_NOT_NULL', { target, statusCode: 400, code: '400' }))
|
|
130
|
+
continue
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const type = resolveCDSType(ele)
|
|
134
|
+
if (type?.match(/^cds\.hana\./)) continue
|
|
135
|
+
|
|
136
|
+
let typeChecker = typeCheckers[type]
|
|
137
|
+
if (typeChecker) {
|
|
138
|
+
if (v == null) continue
|
|
139
|
+
|
|
140
|
+
// if used in protocol adapter, adjust val/ checker if necessary
|
|
141
|
+
if (opts.http) {
|
|
142
|
+
if (typeof v !== 'boolean') {
|
|
143
|
+
if (NUMBER_TYPES.has(type)) v = Number(v)
|
|
144
|
+
else if (type === 'cds.Double') v = parseFloat(v)
|
|
145
|
+
|
|
146
|
+
// REVISIT: consider ieee754 and exp dec headers?
|
|
147
|
+
// const ieee = opts.http.req?.headers['content-type'].match(/IEEE754Compatible=(\w+)/i)
|
|
148
|
+
// const exp = opts.http.req?.headers['content-type'].match(/ExponentialDecimals=(\w+)/i)
|
|
149
|
+
// if (type === 'cds.Decimal') {
|
|
150
|
+
// TODO
|
|
151
|
+
// }
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
// use relaxed uuid check if not in strict mode
|
|
156
|
+
if (type === 'cds.UUID' && !opts.strict) typeChecker = typeCheckers['relaxed.UUID']
|
|
157
|
+
|
|
158
|
+
// type check
|
|
159
|
+
// REVISIT: all checkers should add errors themselves!
|
|
160
|
+
if (type === 'cds.Decimal')
|
|
161
|
+
typeChecker(v, ele, errs, opts.path, k) //> _checkDecimal adds error itself
|
|
162
|
+
else if (!typeChecker(v, ele) || (opts.strict && typeChecker.name === '_checkBuffer' && typeof v === 'string')) {
|
|
163
|
+
errs.push(
|
|
164
|
+
new cds.error('ASSERT_DATA_TYPE', {
|
|
165
|
+
args: [typeof obj[k] === 'string' ? `"${obj[k]}"` : obj[k], ele._type],
|
|
166
|
+
target: getTarget(opts.path, k),
|
|
167
|
+
statusCode: 400,
|
|
168
|
+
code: '400'
|
|
169
|
+
})
|
|
170
|
+
)
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// propagate correction if necessary
|
|
174
|
+
if (obj[k] !== v) obj[k] = v
|
|
175
|
+
|
|
176
|
+
// @assert
|
|
177
|
+
if (ele['@assert.enum'] || (ele['@assert.range'] && ele.enum)) checkEnum(v, ele, errs, opts.path, k)
|
|
178
|
+
if (ele['@assert.range']) checkRange(v, ele, errs, opts.path, k)
|
|
179
|
+
if (ele['@assert.format']) checkFormat(v, ele, errs, opts.path, k)
|
|
180
|
+
// REVISIT: @assert.target? -> no because async, but maybe return the necessary query to execute?
|
|
181
|
+
|
|
182
|
+
continue
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
throw new Error(`Missing type check for "${ele.type}" (property "${k}" of "${def.name}")`)
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Asserts the given data against the given CSN definition and returns an array of errors or undefined.
|
|
191
|
+
*
|
|
192
|
+
* @param {object} data - the data to be checked
|
|
193
|
+
* @param {LinkedCSN} definition - the CSN definition to which the data should be checked against
|
|
194
|
+
* @param {object} [options] - options
|
|
195
|
+
* @param {boolean} [options.strict] - if true, an error is thrown if a property is not defined in the CSN
|
|
196
|
+
* @param {boolean} [options.filter] - if true, properties not defined in the CSN are filtered out
|
|
197
|
+
* @param {boolean} [options.mandatories] - if false, mandatory properties are never checked.
|
|
198
|
+
* if true, mandatory properties are always checked.
|
|
199
|
+
* if undefined, mandatory properties are checked for presumed insert rows only (determined by a heuristic to avoid roundtrip).
|
|
200
|
+
* @param {object} [options.http] - the HTTP request object providing access to headers, etc.
|
|
201
|
+
* @param {*[]} [options.path] - collector for the current path, should not be set manually
|
|
202
|
+
* @return {Array} - an array of errors or undefined if no errors
|
|
203
|
+
*/
|
|
204
|
+
module.exports = (data, definition, options = {}) => {
|
|
205
|
+
if (!data) throw new Error('Argument "data" was not provided')
|
|
206
|
+
if (typeof data !== 'object') throw new Error('Argument "data" must be an object (or an array)')
|
|
207
|
+
|
|
208
|
+
if (!definition) throw new Error('Argument "entity" was not provided')
|
|
209
|
+
// FIXME: definition instanceof cds.builtin.classes.any doesn't always work for some reason
|
|
210
|
+
if (!(definition instanceof cds.builtin.classes.any) && !(definition.kind in { entity: 1, action: 1, function: 1 })) {
|
|
211
|
+
throw new Error('Argument "definition" is not a valid CSN element')
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// TODO: feature flags instead of process env vars
|
|
215
|
+
options.strict ??= process.env.CDS_ASSERT_STRICT === 'true'
|
|
216
|
+
options.filter ??= process.env.CDS_ASSERT_FILTER === 'true'
|
|
217
|
+
options.path ??= []
|
|
218
|
+
|
|
219
|
+
// materialize what is done ...
|
|
220
|
+
// ... in case of unknown elements
|
|
221
|
+
if (options.strict) options._handle_unknown = _reject_unknown
|
|
222
|
+
else if (options.filter) options._handle_unknown = _filter_unknown
|
|
223
|
+
else options._handle_unknown = _no_op
|
|
224
|
+
// ... regarding mandatory elements
|
|
225
|
+
if (options.mandatories === false) options._handle_mandatories = _no_op
|
|
226
|
+
else if (options.mandatories === true) options._handle_mandatories = _handle_mandatories
|
|
227
|
+
else options._handle_mandatories = _handle_mandatories_if_insert
|
|
228
|
+
|
|
229
|
+
const errs = []
|
|
230
|
+
_process(data, definition, errs, options)
|
|
231
|
+
return errs.length ? errs : undefined
|
|
232
|
+
}
|