@sap/cds 8.2.3 → 8.3.1
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 +44 -3
- package/bin/test.js +1 -1
- package/lib/compile/etc/_localized.js +1 -0
- package/lib/compile/etc/csv.js +1 -1
- package/lib/compile/for/lean_drafts.js +5 -0
- package/lib/dbs/cds-deploy.js +8 -5
- package/lib/env/cds-requires.js +0 -13
- package/lib/linked/validate.js +11 -9
- package/lib/log/cds-error.js +10 -7
- package/lib/plugins.js +8 -3
- package/lib/srv/middlewares/cds-context.js +1 -1
- package/lib/srv/middlewares/errors.js +5 -3
- package/lib/srv/protocols/index.js +4 -4
- package/lib/srv/srv-methods.js +1 -0
- package/lib/utils/cds-test.js +2 -1
- package/lib/utils/cds-utils.js +14 -1
- package/lib/utils/colors.js +45 -44
- package/libx/_runtime/common/composition/data.js +4 -2
- package/libx/_runtime/common/composition/index.js +1 -2
- package/libx/_runtime/common/composition/tree.js +1 -24
- package/libx/_runtime/common/error/frontend.js +18 -4
- package/libx/_runtime/common/generic/auth/restrict.js +29 -4
- package/libx/_runtime/common/generic/auth/restrictions.js +29 -36
- package/libx/_runtime/common/i18n/messages.properties +1 -1
- package/libx/_runtime/common/utils/cqn.js +0 -26
- package/libx/_runtime/common/utils/csn.js +0 -14
- package/libx/_runtime/common/utils/differ.js +1 -0
- package/libx/_runtime/common/utils/resolveView.js +28 -9
- package/libx/_runtime/common/utils/templateProcessor.js +3 -0
- package/libx/_runtime/fiori/lean-draft.js +30 -12
- package/libx/_runtime/types/api.js +1 -1
- package/libx/_runtime/ucl/Service.js +2 -2
- package/libx/common/utils/path.js +1 -4
- package/libx/odata/ODataAdapter.js +6 -0
- package/libx/odata/middleware/batch.js +7 -9
- package/libx/odata/middleware/create.js +4 -2
- package/libx/odata/middleware/delete.js +3 -1
- package/libx/odata/middleware/operation.js +7 -5
- package/libx/odata/middleware/read.js +14 -10
- package/libx/odata/middleware/service-document.js +1 -1
- package/libx/odata/middleware/stream.js +1 -0
- package/libx/odata/middleware/update.js +5 -3
- package/libx/odata/parse/afterburner.js +37 -49
- package/libx/odata/utils/index.js +3 -2
- package/libx/odata/utils/postProcess.js +3 -8
- package/package.json +1 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +0 -2
- package/libx/_runtime/messaging/event-broker.js +0 -317
|
@@ -12,19 +12,19 @@ const { getKeysAndParamsFromPath } = require('../../common/utils')
|
|
|
12
12
|
const { getPageSize } = require('../../_runtime/common/generic/paging')
|
|
13
13
|
const { handleStreamProperties } = require('../../_runtime/common/utils/streamProp')
|
|
14
14
|
|
|
15
|
-
const _getCount = result =>
|
|
16
|
-
Array.isArray(result)
|
|
17
|
-
? result.reduce((acc, val) => {
|
|
18
|
-
return acc + ((val && (val.$count || val._counted_)) || (val[0] && (val[0].$count || val[0]._counted_))) || 0
|
|
19
|
-
}, 0)
|
|
20
|
-
: result.$count || result._counted_ || 0
|
|
15
|
+
const _getCount = result => (Array.isArray(result) && result.length ? result[0].$count || 0 : result.$count || 0)
|
|
21
16
|
|
|
22
17
|
const _setNextLink = (req, result) => {
|
|
23
18
|
const $skiptoken = result.$nextLink ?? _calculateSkiptoken(req, result)
|
|
24
19
|
if (!$skiptoken) return
|
|
25
20
|
|
|
26
21
|
const queryParamsWithSkipToken = { ...req.req.query, $skiptoken }
|
|
27
|
-
|
|
22
|
+
const encodedQueryParams = querystring.stringify(queryParamsWithSkipToken)
|
|
23
|
+
|
|
24
|
+
// percent-encode all path segments with key values inside parentheses, but keep Navigation Properties untouched
|
|
25
|
+
const encodedPath = req.req.path.slice(1).replace(/\('([^']*)'\)/g, (match, key) => `('${encodeURIComponent(key)}')`)
|
|
26
|
+
|
|
27
|
+
result.$nextLink = `${encodedPath}?${encodedQueryParams}`
|
|
28
28
|
}
|
|
29
29
|
|
|
30
30
|
const _calculateSkiptoken = (req, result) => {
|
|
@@ -184,6 +184,8 @@ module.exports = adapter => {
|
|
|
184
184
|
// $apply with concat -> multiple queries with special handling
|
|
185
185
|
if (Array.isArray(req._query)) return _handleArrayOfQueries(req, res, next)
|
|
186
186
|
|
|
187
|
+
const model = cds.context.model ?? service.model
|
|
188
|
+
|
|
187
189
|
// REVISIT: better solution for _propertyAccess
|
|
188
190
|
let {
|
|
189
191
|
SELECT: { from, one },
|
|
@@ -193,7 +195,7 @@ module.exports = adapter => {
|
|
|
193
195
|
const { _query: query } = req
|
|
194
196
|
|
|
195
197
|
// payload & params
|
|
196
|
-
const { keys, params } = getKeysAndParamsFromPath(from,
|
|
198
|
+
const { keys, params } = getKeysAndParamsFromPath(from, { model })
|
|
197
199
|
const data = keys //> for read and delete, we provide keys in req.data
|
|
198
200
|
|
|
199
201
|
// cdsReq.headers should contain merged headers of envelope and subreq
|
|
@@ -212,7 +214,7 @@ module.exports = adapter => {
|
|
|
212
214
|
|
|
213
215
|
if (!query.SELECT.columns) query.SELECT.columns = ['*']
|
|
214
216
|
|
|
215
|
-
handleStreamProperties(target, query.SELECT.columns,
|
|
217
|
+
handleStreamProperties(target, query.SELECT.columns, model)
|
|
216
218
|
|
|
217
219
|
// REVISIT: what is this for? some tests fail without it... we should find a better solution!
|
|
218
220
|
Object.defineProperty(query.SELECT, '_4odata', { value: true })
|
|
@@ -246,10 +248,12 @@ module.exports = adapter => {
|
|
|
246
248
|
if (result == null) {
|
|
247
249
|
result = []
|
|
248
250
|
if (req.query.$count) result.$count = 0
|
|
251
|
+
} else if (query.SELECT.count && !result.$count) {
|
|
252
|
+
result.$count = 0
|
|
249
253
|
}
|
|
250
254
|
|
|
251
255
|
if (!one) _setNextLink(cdsReq, result)
|
|
252
|
-
postProcess(cdsReq.target,
|
|
256
|
+
postProcess(cdsReq.target, model, result)
|
|
253
257
|
if (result?.$etag) res.set('ETag', result.$etag) //> must be done after post processing
|
|
254
258
|
|
|
255
259
|
const lastSeg = req.path.split('/').slice(-1)[0]
|
|
@@ -22,7 +22,7 @@ module.exports = adapter => {
|
|
|
22
22
|
throw Object.assign(new Error(msg), { statusCode: 405 })
|
|
23
23
|
}
|
|
24
24
|
|
|
25
|
-
const model = cds.context.model
|
|
25
|
+
const model = cds.context.model ?? service.model
|
|
26
26
|
const csnService = model.definitions[service.definition.name]
|
|
27
27
|
|
|
28
28
|
if (req.headers['if-match']) {
|
|
@@ -170,6 +170,7 @@ module.exports = adapter => {
|
|
|
170
170
|
|
|
171
171
|
if (isRedirect(query)) {
|
|
172
172
|
const cdsReq = adapter.request4({ query, req, res })
|
|
173
|
+
|
|
173
174
|
service.dispatch(cdsReq).then(result => {
|
|
174
175
|
if (result[query._propertyAccess]) res.set('Location', result[query._propertyAccess])
|
|
175
176
|
return res.sendStatus(307)
|
|
@@ -69,9 +69,11 @@ module.exports = adapter => {
|
|
|
69
69
|
throw Object.assign(new Error(`Method ${req.method} is not allowed for properties`), { statusCode: 405 })
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
+
const model = cds.context.model ?? service.model
|
|
73
|
+
|
|
72
74
|
// payload & params
|
|
73
75
|
const data = _propertyAccess ? { [_propertyAccess]: req.body.value } : req.body
|
|
74
|
-
const { keys, params } = getKeysAndParamsFromPath(from,
|
|
76
|
+
const { keys, params } = getKeysAndParamsFromPath(from, { model })
|
|
75
77
|
// add keys from url into payload (overwriting if already present)
|
|
76
78
|
if (!_propertyAccess) Object.assign(data, keys)
|
|
77
79
|
|
|
@@ -111,7 +113,7 @@ module.exports = adapter => {
|
|
|
111
113
|
if (result == null) return res.sendStatus(204)
|
|
112
114
|
|
|
113
115
|
const isMinimal = getPreferReturnHeader(req) === 'minimal'
|
|
114
|
-
postProcess(cdsReq.target,
|
|
116
|
+
postProcess(cdsReq.target, model, result, isMinimal)
|
|
115
117
|
|
|
116
118
|
if (isMinimal && !target._isSingleton) {
|
|
117
119
|
// determine calculation based on result with req.data as fallback
|
|
@@ -139,7 +141,7 @@ module.exports = adapter => {
|
|
|
139
141
|
// PUT / PATCH with if-match header means "only if already exists" -> no insert if it does not
|
|
140
142
|
if (req.headers['if-match']) return next(Object.assign(new Error('412'), { statusCode: 412 }))
|
|
141
143
|
|
|
142
|
-
if (!upsertSupported(from,
|
|
144
|
+
if (!upsertSupported(from, model)) return next(Object.assign(new Error('422'), { statusCode: 422 }))
|
|
143
145
|
|
|
144
146
|
// -> forward to POST
|
|
145
147
|
req.method = 'POST'
|
|
@@ -383,6 +383,14 @@ function _processSegments(from, model, namespace, cqn, protocol) {
|
|
|
383
383
|
ref[i] = { operation: current.name }
|
|
384
384
|
if (params) ref[i].args = _getDataFromParams(params, current)
|
|
385
385
|
if (current.returns && current.returns._type) one = true
|
|
386
|
+
|
|
387
|
+
if (current.returns) {
|
|
388
|
+
if (current.returns._type) {
|
|
389
|
+
one = true
|
|
390
|
+
}
|
|
391
|
+
|
|
392
|
+
target = current.returns.items ?? current.returns
|
|
393
|
+
}
|
|
386
394
|
} else if (current.isAssociation) {
|
|
387
395
|
if (!current._target._service) {
|
|
388
396
|
// not exposed target
|
|
@@ -589,23 +597,26 @@ const _checkAllKeysProvided = (params, entity) => {
|
|
|
589
597
|
}
|
|
590
598
|
}
|
|
591
599
|
|
|
592
|
-
const _doesNotExistError = (isExpand, refName, targetName) => {
|
|
600
|
+
const _doesNotExistError = (isExpand, refName, targetName, targetKind) => {
|
|
593
601
|
const msg = isExpand
|
|
594
602
|
? `Navigation property "${refName}" is not defined in "${targetName}"`
|
|
595
|
-
: `Property "${refName}" does not exist in "${targetName}"`
|
|
603
|
+
: `Property "${refName}" does not exist in ${targetKind === 'type' ? 'type ' : ''}"${targetName}"`
|
|
596
604
|
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
597
605
|
}
|
|
598
606
|
|
|
599
|
-
function _validateXpr(xpr,
|
|
607
|
+
function _validateXpr(xpr, target, isOne, model, aliases = []) {
|
|
600
608
|
if (!xpr) return []
|
|
601
609
|
|
|
610
|
+
const ignoredColumns = Object.values(target.elements ?? {})
|
|
611
|
+
.filter(element => element['@cds.api.ignore'] && !element.isAssociation)
|
|
612
|
+
.map(element => element.name)
|
|
602
613
|
const _aliases = []
|
|
603
614
|
|
|
604
615
|
for (const x of xpr) {
|
|
605
616
|
if (x.as) _aliases.push(x.as)
|
|
606
617
|
|
|
607
618
|
if (x.xpr) {
|
|
608
|
-
_validateXpr(x.xpr,
|
|
619
|
+
_validateXpr(x.xpr, target, isOne, model)
|
|
609
620
|
continue
|
|
610
621
|
}
|
|
611
622
|
|
|
@@ -614,101 +625,82 @@ function _validateXpr(xpr, ignoredColumns, target, isOne, model, aliases = []) {
|
|
|
614
625
|
|
|
615
626
|
if (x.ref[0].where) {
|
|
616
627
|
const element = target.elements[refName]
|
|
617
|
-
|
|
618
628
|
if (!element) {
|
|
619
629
|
_doesNotExistError(true, refName, target.name)
|
|
620
630
|
}
|
|
621
|
-
_validateXpr(x.ref[0].where,
|
|
631
|
+
_validateXpr(x.ref[0].where, element._target ?? element.items, isOne, model)
|
|
622
632
|
}
|
|
623
633
|
|
|
624
634
|
if (!target?.elements) {
|
|
625
|
-
_doesNotExistError(false, refName, target.name)
|
|
635
|
+
_doesNotExistError(false, refName, target.name, target.kind)
|
|
626
636
|
}
|
|
627
637
|
|
|
628
638
|
if (ignoredColumns.includes(refName) || (!target.elements[refName] && !aliases.includes(refName))) {
|
|
629
639
|
_doesNotExistError(x.expand, refName, target.name)
|
|
630
640
|
} else if (x.ref.length > 1) {
|
|
631
641
|
const element = target.elements[refName]
|
|
632
|
-
|
|
633
642
|
if (element.isAssociation) {
|
|
634
643
|
// navigation
|
|
635
|
-
|
|
636
|
-
const _ignoredColumns = Object.values(_target.elements ?? {})
|
|
637
|
-
.filter(element => element['@cds.api.ignore'])
|
|
638
|
-
.map(element => element.name)
|
|
639
|
-
if (element.is2one) {
|
|
640
|
-
_validateXpr([{ ref: x.ref.slice(1) }], _ignoredColumns, _target, false, model)
|
|
641
|
-
} else {
|
|
642
|
-
_validateXpr([{ ref: x.ref.slice(1) }], _ignoredColumns, _target, false, model)
|
|
643
|
-
}
|
|
644
|
+
_validateXpr([{ ref: x.ref.slice(1) }], element._target, false, model)
|
|
644
645
|
} else if (element.kind === 'element') {
|
|
645
646
|
// structured
|
|
646
|
-
_validateXpr([{ ref: x.ref.slice(1) }],
|
|
647
|
+
_validateXpr([{ ref: x.ref.slice(1) }], element, isOne, model)
|
|
647
648
|
} else {
|
|
648
649
|
throw new Error('not yet validated')
|
|
649
650
|
}
|
|
650
651
|
}
|
|
652
|
+
|
|
651
653
|
if (x.expand) {
|
|
652
654
|
let element = target.elements[refName]
|
|
653
655
|
if (element.kind === 'element' && element.elements) {
|
|
654
656
|
// structured
|
|
655
|
-
_validateXpr([{ ref: x.ref.slice(1) }],
|
|
657
|
+
_validateXpr([{ ref: x.ref.slice(1) }], element, isOne, model)
|
|
656
658
|
element = _structProperty(x.ref.slice(1), element)
|
|
657
659
|
}
|
|
658
|
-
|
|
659
660
|
if (!element._target) {
|
|
660
661
|
_doesNotExistError(true, refName, target.name)
|
|
661
662
|
}
|
|
662
|
-
|
|
663
|
-
const _ignoredColumns = Object.values(element._target.elements ?? {})
|
|
664
|
-
.filter(element => element['@cds.api.ignore'])
|
|
665
|
-
.map(element => element.name)
|
|
666
|
-
_validateXpr(x.expand, _ignoredColumns, element._target, false, model)
|
|
667
|
-
|
|
663
|
+
_validateXpr(x.expand, element._target, false, model)
|
|
668
664
|
if (x.where) {
|
|
669
|
-
_validateXpr(x.where,
|
|
665
|
+
_validateXpr(x.where, element._target, false, model)
|
|
670
666
|
}
|
|
671
|
-
|
|
672
667
|
if (x.orderBy) {
|
|
673
|
-
_validateXpr(x.orderBy,
|
|
668
|
+
_validateXpr(x.orderBy, element._target, false, model)
|
|
674
669
|
}
|
|
675
670
|
}
|
|
676
671
|
}
|
|
677
672
|
|
|
678
673
|
if (x.func) {
|
|
679
|
-
_validateXpr(x.args,
|
|
674
|
+
_validateXpr(x.args, target, isOne, model)
|
|
680
675
|
continue
|
|
681
676
|
}
|
|
682
677
|
|
|
683
678
|
if (x.SELECT) {
|
|
684
679
|
const { target } = targetFromPath(x.SELECT.from, model)
|
|
685
|
-
|
|
686
|
-
.filter(element => element['@cds.api.ignore'])
|
|
687
|
-
.map(element => element.name)
|
|
688
|
-
_validateQuery(x.SELECT, _ignoredColumns, target, x.SELECT.one, model)
|
|
680
|
+
_validateQuery(x.SELECT, target, x.SELECT.one, model)
|
|
689
681
|
}
|
|
690
682
|
}
|
|
691
683
|
|
|
692
684
|
return _aliases
|
|
693
685
|
}
|
|
694
686
|
|
|
695
|
-
function _validateQuery(SELECT,
|
|
687
|
+
function _validateQuery(SELECT, target, isOne, model) {
|
|
696
688
|
const aliases = []
|
|
689
|
+
|
|
697
690
|
if (SELECT.from.SELECT) {
|
|
698
691
|
const { target } = targetFromPath(SELECT.from.SELECT.from, model)
|
|
699
|
-
const
|
|
700
|
-
.filter(element => element['@cds.api.ignore'])
|
|
701
|
-
.map(element => element.name)
|
|
702
|
-
const subselectAliases = _validateQuery(SELECT.from.SELECT, _ignoredColumns, target, SELECT.from.SELECT.one, model)
|
|
692
|
+
const subselectAliases = _validateQuery(SELECT.from.SELECT, target, SELECT.from.SELECT.one, model)
|
|
703
693
|
aliases.push(...subselectAliases)
|
|
704
694
|
}
|
|
705
695
|
|
|
706
|
-
const columnAliases = _validateXpr(SELECT.columns,
|
|
696
|
+
const columnAliases = _validateXpr(SELECT.columns, target, isOne, model)
|
|
707
697
|
aliases.push(...columnAliases)
|
|
708
|
-
|
|
709
|
-
_validateXpr(SELECT.
|
|
710
|
-
_validateXpr(SELECT.
|
|
711
|
-
_validateXpr(SELECT.
|
|
698
|
+
|
|
699
|
+
_validateXpr(SELECT.orderBy, target, isOne, model, aliases)
|
|
700
|
+
_validateXpr(SELECT.where, target, isOne, model, aliases)
|
|
701
|
+
_validateXpr(SELECT.groupBy, target, isOne, model, aliases)
|
|
702
|
+
_validateXpr(SELECT.having, target, isOne, model, aliases)
|
|
703
|
+
|
|
712
704
|
return aliases
|
|
713
705
|
}
|
|
714
706
|
|
|
@@ -773,12 +765,8 @@ module.exports = (cqn, model, namespace, protocol) => {
|
|
|
773
765
|
_processColumns(cqn, current, protocol)
|
|
774
766
|
|
|
775
767
|
if (target) {
|
|
776
|
-
const ignoredColumns = Object.values(target.elements ?? {})
|
|
777
|
-
.filter(element => element['@cds.api.ignore'])
|
|
778
|
-
.map(element => element.name)
|
|
779
|
-
|
|
780
768
|
// validate whether only known properties are used in query options
|
|
781
|
-
_validateQuery(cqn.SELECT,
|
|
769
|
+
_validateQuery(cqn.SELECT, target, one, model)
|
|
782
770
|
}
|
|
783
771
|
|
|
784
772
|
return cqn
|
|
@@ -278,9 +278,11 @@ const skipToken = (token, cqn) => {
|
|
|
278
278
|
const calculateLocationHeader = (target, srv, result) => {
|
|
279
279
|
const targetName = target.name.replace(`${srv.definition.name}.`, '')
|
|
280
280
|
const filteredKeys = [...target.keys].filter(k => !k.isAssociation).map(k => k.name)
|
|
281
|
+
if (!filteredKeys.every(k => k in result)) return
|
|
282
|
+
|
|
281
283
|
const keyValuePairs = filteredKeys.reduce((acc, key) => {
|
|
282
284
|
const value = result[key]
|
|
283
|
-
if (value === undefined) return
|
|
285
|
+
if (value === undefined) return acc
|
|
284
286
|
if (Buffer.isBuffer(value)) {
|
|
285
287
|
acc[key] = value.toString('base64')
|
|
286
288
|
} else {
|
|
@@ -290,7 +292,6 @@ const calculateLocationHeader = (target, srv, result) => {
|
|
|
290
292
|
}
|
|
291
293
|
return acc
|
|
292
294
|
}, {})
|
|
293
|
-
if (!keyValuePairs) return
|
|
294
295
|
let keys
|
|
295
296
|
const entries = Object.entries(keyValuePairs)
|
|
296
297
|
if (entries.length === 1) {
|
|
@@ -1,5 +1,3 @@
|
|
|
1
|
-
const cds = require('../../_runtime/cds')
|
|
2
|
-
|
|
3
1
|
const getTemplate = require('../../_runtime/common/utils/template')
|
|
4
2
|
|
|
5
3
|
const _addEtags = (row, key) => {
|
|
@@ -38,18 +36,15 @@ const _processorFn = elementInfo => {
|
|
|
38
36
|
const _pick = element => {
|
|
39
37
|
const categories = []
|
|
40
38
|
if (element['@odata.etag']) categories.push('@odata.etag')
|
|
41
|
-
if (element['@cds.api.ignore']) categories.push('@cds.api.ignore')
|
|
39
|
+
if (element['@cds.api.ignore'] && !element.isAssociation) categories.push('@cds.api.ignore')
|
|
42
40
|
if (element._type === 'cds.Binary') categories.push('binary')
|
|
43
41
|
if (element.items) categories.push('array')
|
|
44
42
|
if (categories.length) return { categories }
|
|
45
43
|
}
|
|
46
44
|
|
|
47
|
-
module.exports = function postProcess(target,
|
|
45
|
+
module.exports = function postProcess(target, model, result, isMinimal) {
|
|
48
46
|
if (!result) return
|
|
49
47
|
|
|
50
|
-
let { model } = service
|
|
51
|
-
if (service.isExtensible) model = cds.context?.model || model
|
|
52
|
-
|
|
53
48
|
if (!model.definitions[target.name]) {
|
|
54
49
|
if (model.definitions[target.items?.type]) target = target.items
|
|
55
50
|
else return
|
|
@@ -57,7 +52,7 @@ module.exports = function postProcess(target, service, result, isMinimal) {
|
|
|
57
52
|
|
|
58
53
|
const cacheKey = isMinimal ? 'postProcessMinimal' : 'postProcess'
|
|
59
54
|
const options = { pick: _pick, ignore: isMinimal ? el => el.isAssociation : undefined }
|
|
60
|
-
const template = getTemplate(cacheKey,
|
|
55
|
+
const template = getTemplate(cacheKey, { model }, target, options)
|
|
61
56
|
|
|
62
57
|
if (template.elements.size === 0) return
|
|
63
58
|
|
package/package.json
CHANGED