@sap/cds 9.7.1 → 9.8.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 +48 -0
- package/_i18n/i18n_en_US_saptrc.properties +1 -56
- package/_i18n/messages_en_US_saptrc.properties +1 -92
- package/eslint.config.mjs +4 -1
- package/lib/compile/cds-compile.js +1 -0
- package/lib/compile/for/direct_crud.js +23 -0
- package/lib/compile/for/lean_drafts.js +12 -0
- package/lib/compile/for/odata.js +1 -18
- package/lib/compile/to/edm.js +1 -0
- package/lib/compile/to/json.js +4 -2
- package/lib/env/defaults.js +1 -0
- package/lib/env/serviceBindings.js +15 -5
- package/lib/index.js +1 -1
- package/lib/log/cds-error.js +33 -20
- package/lib/req/spawn.js +2 -2
- package/lib/srv/bindings.js +6 -13
- package/lib/srv/cds.Service.js +8 -36
- package/lib/srv/protocols/hcql.js +19 -2
- package/lib/utils/cds-utils.js +25 -16
- package/lib/utils/tar-win.js +106 -0
- package/lib/utils/tar.js +23 -158
- package/libx/_runtime/common/generic/crud.js +8 -7
- package/libx/_runtime/common/generic/sorting.js +7 -3
- package/libx/_runtime/common/utils/resolveView.js +47 -40
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -0
- package/libx/_runtime/fiori/lean-draft.js +11 -2
- package/libx/_runtime/messaging/kafka.js +6 -5
- package/libx/_runtime/messaging/service.js +3 -1
- package/libx/_runtime/remote/Service.js +3 -0
- package/libx/_runtime/remote/utils/client.js +2 -4
- package/libx/_runtime/remote/utils/query.js +4 -4
- package/libx/odata/middleware/batch.js +323 -339
- package/libx/odata/middleware/create.js +0 -5
- package/libx/odata/middleware/delete.js +0 -5
- package/libx/odata/middleware/operation.js +10 -8
- package/libx/odata/middleware/read.js +0 -10
- package/libx/odata/middleware/stream.js +1 -0
- package/libx/odata/middleware/update.js +0 -6
- package/libx/odata/parse/afterburner.js +47 -22
- package/libx/odata/parse/cqn2odata.js +6 -1
- package/libx/odata/parse/grammar.peggy +14 -2
- package/libx/odata/parse/multipartToJson.js +2 -1
- package/libx/odata/parse/parser.js +1 -1
- package/package.json +2 -2
|
@@ -53,15 +53,15 @@ const _inverseTransition = transition => {
|
|
|
53
53
|
return inverseTransition
|
|
54
54
|
}
|
|
55
55
|
|
|
56
|
-
const revertData = (data, transition, service
|
|
56
|
+
const revertData = (data, transition, service) => {
|
|
57
57
|
if (!transition || !transition.mapping.size) return data
|
|
58
58
|
const inverseTransition = _inverseTransition(transition)
|
|
59
59
|
return Array.isArray(data)
|
|
60
|
-
? data.map(entry => _newData(entry, inverseTransition, true, service
|
|
61
|
-
: _newData(data, inverseTransition, true, service
|
|
60
|
+
? data.map(entry => _newData(entry, inverseTransition, true, service))
|
|
61
|
+
: _newData(data, inverseTransition, true, service)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
-
const _newSubData = (val, key, transition, el, inverse, service, options) => {
|
|
64
|
+
const _newSubData = (val, key, transition, el, inverse, service, options = {}) => {
|
|
65
65
|
if ((!Array.isArray(val) && typeof val === 'object') || (Array.isArray(val) && val.length !== 0)) {
|
|
66
66
|
let mapped = transition.mapping.get(key)
|
|
67
67
|
if (!mapped) {
|
|
@@ -70,7 +70,7 @@ const _newSubData = (val, key, transition, el, inverse, service, options) => {
|
|
|
70
70
|
}
|
|
71
71
|
|
|
72
72
|
if (!mapped.transition) {
|
|
73
|
-
const subTransition = getTransition(el._target, service,
|
|
73
|
+
const subTransition = getTransition(el._target, service, null, options.event)
|
|
74
74
|
mapped.transition = inverse ? _inverseTransition(subTransition) : subTransition
|
|
75
75
|
}
|
|
76
76
|
|
|
@@ -196,7 +196,7 @@ const _newColumns = (columns = [], transition, service, withAlias = false, optio
|
|
|
196
196
|
|
|
197
197
|
// reuse _newColumns with new transition
|
|
198
198
|
const expandTarget = def._target
|
|
199
|
-
const subtransition = getTransition(expandTarget, service,
|
|
199
|
+
const subtransition = getTransition(expandTarget, service, null, options.event)
|
|
200
200
|
mapped.transition = subtransition
|
|
201
201
|
newColumn.expand = _newColumns(column.expand, subtransition, service, withAlias, options)
|
|
202
202
|
}
|
|
@@ -523,19 +523,26 @@ const _queryColumns = (target, columns = [], isAborted) => {
|
|
|
523
523
|
return queryColumns
|
|
524
524
|
}
|
|
525
525
|
|
|
526
|
-
const _mappedValue = (col, alias) => {
|
|
526
|
+
const _mappedValue = (col, alias, target) => {
|
|
527
527
|
const key = col.as || col.ref[0]
|
|
528
528
|
|
|
529
529
|
if (col.ref) {
|
|
530
|
-
|
|
530
|
+
let columnRef = col.ref.filter(columnName => columnName !== alias)
|
|
531
|
+
|
|
532
|
+
if (columnRef.length > 1 && target) {
|
|
533
|
+
const firstElement = target.elements?.[columnRef[0]]
|
|
534
|
+
if (firstElement?.foreignKeys) {
|
|
535
|
+
// It's a managed association - use foreignKeys property to get the FK column name
|
|
536
|
+
if (columnRef[1] in firstElement.foreignKeys) columnRef = [`${firstElement.name}_${columnRef[1]}`]
|
|
537
|
+
}
|
|
538
|
+
}
|
|
539
|
+
|
|
531
540
|
return [key, { ref: columnRef }]
|
|
532
541
|
}
|
|
533
542
|
|
|
534
543
|
return [key, { val: col.val }]
|
|
535
544
|
}
|
|
536
545
|
|
|
537
|
-
const getDBTable = target => cds.db.resolve.table(target)
|
|
538
|
-
|
|
539
546
|
const _appendForeignKeys = (newColumns, target, columns, { as, ref = [] }) => {
|
|
540
547
|
const el = target.elements[as] || target.query._target?.elements[ref.at(-1)]
|
|
541
548
|
|
|
@@ -578,26 +585,27 @@ const _checkForForbiddenViews = (queryTarget, event) => {
|
|
|
578
585
|
}
|
|
579
586
|
}
|
|
580
587
|
|
|
581
|
-
const _getTransitionData = (target, columns, service,
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const
|
|
588
|
+
const _getTransitionData = (target, columns, service, event) => {
|
|
589
|
+
_checkForForbiddenViews(target, event)
|
|
590
|
+
|
|
591
|
+
const _dbAbort = target =>
|
|
592
|
+
!!(Object.prototype.hasOwnProperty.call(target, '@cds.persistence.table') || !target.query?._target)
|
|
593
|
+
const _defaultAbort = target => target._service?.name === service.definition?.name
|
|
594
|
+
const _abort = service.isDatabaseService ? _dbAbort : _defaultAbort
|
|
595
|
+
|
|
596
|
+
const isAborted = _abort(target)
|
|
588
597
|
columns = _queryColumns(target, columns, isAborted)
|
|
589
598
|
|
|
590
599
|
if (isAborted) return { target, transitionColumns: columns }
|
|
591
600
|
|
|
592
601
|
if (!target.query?._target) {
|
|
593
602
|
// for cross service in x4 and DRAFT.DraftAdministrativeData we cannot abort properly
|
|
594
|
-
// therefore return last resolved target
|
|
595
603
|
if (cds.env.features.restrict_service_scope === false) return { target, transitionColumns: columns }
|
|
596
604
|
return undefined
|
|
597
605
|
} else {
|
|
598
606
|
const newTarget = target.query._target
|
|
599
607
|
// continue projection resolving for projections
|
|
600
|
-
return _getTransitionData(newTarget, columns, service,
|
|
608
|
+
return _getTransitionData(newTarget, columns, service, event)
|
|
601
609
|
}
|
|
602
610
|
}
|
|
603
611
|
|
|
@@ -606,24 +614,20 @@ const _getTransitionData = (target, columns, service, options) => {
|
|
|
606
614
|
*
|
|
607
615
|
* @param queryTarget
|
|
608
616
|
* @param service
|
|
609
|
-
* @param
|
|
617
|
+
* @param _dummy - unused positional argument (for db-service < 2.9)
|
|
618
|
+
* @param event
|
|
610
619
|
*/
|
|
611
|
-
const getTransition = (queryTarget, service,
|
|
620
|
+
const getTransition = (queryTarget, service, _dummy, event) => {
|
|
612
621
|
// Never resolve unknown targets (e.g. for drafts)
|
|
613
|
-
if (!queryTarget) {
|
|
614
|
-
return { target: queryTarget, queryTarget, mapping: new Map() }
|
|
615
|
-
}
|
|
622
|
+
if (!queryTarget) return { target: queryTarget, queryTarget, mapping: new Map() }
|
|
616
623
|
|
|
617
|
-
const transitionData = _getTransitionData(queryTarget, [], service,
|
|
618
|
-
skipForbiddenViewCheck,
|
|
619
|
-
event,
|
|
620
|
-
abort: options?.abort
|
|
621
|
-
})
|
|
624
|
+
const transitionData = _getTransitionData(queryTarget, [], service, event)
|
|
622
625
|
if (!transitionData) return undefined
|
|
623
626
|
const { target: _target, transitionColumns } = transitionData
|
|
624
627
|
const query = queryTarget.query
|
|
625
628
|
const alias = query && query.SELECT && query.SELECT.from && query.SELECT.from.as
|
|
626
|
-
const
|
|
629
|
+
const mappingTarget = query?._target
|
|
630
|
+
const mappedColumns = transitionColumns.map(column => _mappedValue(column, alias, mappingTarget))
|
|
627
631
|
const mapping = new Map(mappedColumns)
|
|
628
632
|
return { target: _target, queryTarget, mapping }
|
|
629
633
|
}
|
|
@@ -632,11 +636,7 @@ const _entityTransitionsForTarget = (from, model, service, options) => {
|
|
|
632
636
|
let previousEntity = options.previousEntity
|
|
633
637
|
|
|
634
638
|
if (typeof from === 'string') {
|
|
635
|
-
return (
|
|
636
|
-
model.definitions[from] && [
|
|
637
|
-
getTransition(model.definitions[from], service, undefined, options.event, { abort: options.abort })
|
|
638
|
-
]
|
|
639
|
-
)
|
|
639
|
+
return model.definitions[from] && [getTransition(model.definitions[from], service, null, options.event)]
|
|
640
640
|
}
|
|
641
641
|
|
|
642
642
|
if (typeof from === 'object' && from.SELECT) {
|
|
@@ -651,7 +651,7 @@ const _entityTransitionsForTarget = (from, model, service, options) => {
|
|
|
651
651
|
const entity = model.definitions[element]
|
|
652
652
|
if (entity) {
|
|
653
653
|
previousEntity = entity
|
|
654
|
-
return getTransition(entity, service,
|
|
654
|
+
return getTransition(entity, service, null, options.event)
|
|
655
655
|
}
|
|
656
656
|
}
|
|
657
657
|
|
|
@@ -660,7 +660,7 @@ const _entityTransitionsForTarget = (from, model, service, options) => {
|
|
|
660
660
|
if (entity) {
|
|
661
661
|
// > assoc
|
|
662
662
|
previousEntity = entity
|
|
663
|
-
return getTransition(entity, service,
|
|
663
|
+
return getTransition(entity, service, null, options.event)
|
|
664
664
|
}
|
|
665
665
|
|
|
666
666
|
// > struct
|
|
@@ -674,7 +674,7 @@ const _entityTransitionsForTarget = (from, model, service, options) => {
|
|
|
674
674
|
})
|
|
675
675
|
}
|
|
676
676
|
|
|
677
|
-
const resolveView = (query, model, service
|
|
677
|
+
const resolveView = (query, model, service) => {
|
|
678
678
|
// swap logger
|
|
679
679
|
const _LOG = LOG
|
|
680
680
|
LOG = cds.log(service.kind) // REVISIT: Avoid obtaining loggers per request!
|
|
@@ -699,7 +699,7 @@ const resolveView = (query, model, service, abort) => {
|
|
|
699
699
|
DELETE: ['from', _newDelete]
|
|
700
700
|
}[kind]
|
|
701
701
|
|
|
702
|
-
const options = {
|
|
702
|
+
const options = { event: kind, service, model }
|
|
703
703
|
const transitions = _entityTransitionsForTarget(query[kind][_prop], model, service, options)
|
|
704
704
|
if (!service.isDatabaseService && cds.env.features.restrict_service_scope !== false && transitions.some(t => !t))
|
|
705
705
|
return
|
|
@@ -719,8 +719,15 @@ const resolveView = (query, model, service, abort) => {
|
|
|
719
719
|
return newQuery
|
|
720
720
|
}
|
|
721
721
|
|
|
722
|
+
const _resolve_table = target => {
|
|
723
|
+
if (target.query?._target && !Object.prototype.hasOwnProperty.call(target, '@cds.persistence.table'))
|
|
724
|
+
return _resolve_table(target.query._target)
|
|
725
|
+
return target
|
|
726
|
+
}
|
|
727
|
+
|
|
722
728
|
module.exports = {
|
|
723
|
-
getDBTable
|
|
729
|
+
// REVISIT: remove getDBTable with cds^10
|
|
730
|
+
getDBTable: _resolve_table,
|
|
724
731
|
resolveView,
|
|
725
732
|
getTransition,
|
|
726
733
|
revertData
|
|
@@ -95,6 +95,7 @@ const rewriteExpandAsterisk = (columns, target) => {
|
|
|
95
95
|
|
|
96
96
|
for (const elName in target.elements) {
|
|
97
97
|
if (!target.elements[elName]._target) continue
|
|
98
|
+
if (target.elements[elName]._target?._service !== target._service) continue
|
|
98
99
|
if (restrictions.includes(elName)) continue
|
|
99
100
|
if (elName === 'SiblingEntity') continue
|
|
100
101
|
if (columns.find(col => col.expand && col.ref && col.ref[0] === elName)) continue
|
|
@@ -6,6 +6,7 @@ const { Object_keys } = cds.utils
|
|
|
6
6
|
const { Readable, PassThrough } = require('stream')
|
|
7
7
|
|
|
8
8
|
const { getPageSize, commonGenericPaging } = require('../common/generic/paging')
|
|
9
|
+
const { getPreferReturnHeader } = require('../../odata/utils')
|
|
9
10
|
const { handler: commonGenericSorting } = require('../common/generic/sorting')
|
|
10
11
|
const { addEtagColumns } = require('../common/utils/etag')
|
|
11
12
|
const { handleStreamProperties } = require('../common/utils/streamProp')
|
|
@@ -710,7 +711,7 @@ const draftHandle = async function (req) {
|
|
|
710
711
|
let newDraftAction = rootEntity['@Common.DraftRoot.NewAction']
|
|
711
712
|
if (typeof newDraftAction != 'string' || !newDraftAction.length) newDraftAction = false
|
|
712
713
|
else newDraftAction = newDraftAction.split('.').pop()
|
|
713
|
-
const shouldHandleNewDraftAction = isNewDraftViaActionEnabled && req.target === rootEntity
|
|
714
|
+
const shouldHandleNewDraftAction = isNewDraftViaActionEnabled && req.target.name === rootEntity.name
|
|
714
715
|
|
|
715
716
|
// Create active instance of draft-enabled entity
|
|
716
717
|
// REVISIT: New OData adapter only sets `NEW` for drafts... how to distinguish programmatic modifications?
|
|
@@ -781,7 +782,13 @@ const draftHandle = async function (req) {
|
|
|
781
782
|
req.data = createNewResult
|
|
782
783
|
req.res.status(201)
|
|
783
784
|
|
|
784
|
-
|
|
785
|
+
const result = await _readAfterDraftAction.call(this, { req, payload: createNewResult, action: 'draftNew' })
|
|
786
|
+
if (result === null) req.res.status(204)
|
|
787
|
+
req.res.set(
|
|
788
|
+
'location',
|
|
789
|
+
'../' + location4(req.target.actives, this, result || { ...createNewResult, IsActiveEntity: false })
|
|
790
|
+
)
|
|
791
|
+
return result
|
|
785
792
|
}
|
|
786
793
|
|
|
787
794
|
// Handle draft-only events, that can only ever target entities in draft state
|
|
@@ -2442,6 +2449,8 @@ async function onPrepare(req, next) {
|
|
|
2442
2449
|
}
|
|
2443
2450
|
|
|
2444
2451
|
const _readAfterDraftAction = async function ({ req, payload, action }) {
|
|
2452
|
+
if (getPreferReturnHeader(req) === 'minimal') return null
|
|
2453
|
+
|
|
2445
2454
|
const entity = action === 'draftActivate' ? req.target : req.target.drafts
|
|
2446
2455
|
|
|
2447
2456
|
// read after write with query options
|
|
@@ -123,13 +123,14 @@ class KafkaService extends cds.MessagingService {
|
|
|
123
123
|
eachMessage: async raw => {
|
|
124
124
|
try {
|
|
125
125
|
const msg = _normalizeIncomingMessage(raw.message.value.toString())
|
|
126
|
+
|
|
126
127
|
msg.headers = {}
|
|
127
128
|
for (const header in raw.message.headers || {}) {
|
|
128
129
|
msg.headers[header] = raw.message.headers[header]?.toString()
|
|
129
130
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
msg.
|
|
131
|
+
|
|
132
|
+
msg.tenant = msg.headers['x-sap-cap-tenant-id']
|
|
133
|
+
msg.event = msg.headers['x-sap-cap-effective-topic'] ?? msg.headers.type
|
|
133
134
|
if (!msg.event) return
|
|
134
135
|
|
|
135
136
|
await this.processInboundMsg({ tenant: msg.tenant }, msg)
|
|
@@ -210,8 +211,8 @@ async function _getConfig(srv) {
|
|
|
210
211
|
const caCerts = await _getCaCerts(srv)
|
|
211
212
|
|
|
212
213
|
const allBrokers =
|
|
213
|
-
srv.options.credentials.
|
|
214
|
-
srv.options.credentials
|
|
214
|
+
srv.options.credentials['cluster.public']?.['brokers.client_ssl'] ||
|
|
215
|
+
srv.options.credentials.cluster?.['brokers.client_ssl']
|
|
215
216
|
const brokers = allBrokers.split(',')
|
|
216
217
|
|
|
217
218
|
return {
|
|
@@ -77,9 +77,11 @@ module.exports = class MessagingService extends cds.Service {
|
|
|
77
77
|
if (!cds.context) cds.context = {}
|
|
78
78
|
if (ctx.tenant) cds.context.tenant = ctx.tenant
|
|
79
79
|
if (!ctx.user) ctx.user = cds.User.privileged
|
|
80
|
+
// REVISIT: is cds.context.model really needed?
|
|
80
81
|
// this.tx expects cds.context.model
|
|
81
|
-
if (cds.model && (cds.env.requires.extensibility || cds.env.requires.toggles))
|
|
82
|
+
if (ctx.tenant && cds.model && (cds.env.requires.extensibility || cds.env.requires.toggles))
|
|
82
83
|
cds.context.model = await ExtendedModels.model4(ctx.tenant, ctx.features || {})
|
|
84
|
+
else if (cds.model) cds.context.model = cds.model
|
|
83
85
|
const me = this.options.inboxed || this.options.inbox ? cds.queued(this) : this
|
|
84
86
|
return await me.tx(ctx, tx => tx.emit(msg))
|
|
85
87
|
}
|
|
@@ -249,6 +249,9 @@ class RemoteService extends cds.Service {
|
|
|
249
249
|
const requestConfig = extractRequestConfig(req, query, this)
|
|
250
250
|
requestConfig.headers = _getHeaders(requestConfig.headers, req)
|
|
251
251
|
|
|
252
|
+
if (this.kind === 'hcql' && req.iterator && !req.objectMode)
|
|
253
|
+
requestConfig.headers.accept = 'application/octet-stream,application/json'
|
|
254
|
+
|
|
252
255
|
// REVISIT: we should not have to set the content-type at all for that
|
|
253
256
|
if (requestConfig.headers.accept?.match(/stream|image|tar/)) requestConfig.responseType = 'stream'
|
|
254
257
|
|
|
@@ -43,9 +43,6 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
|
|
|
43
43
|
else if (LOG._warn) LOG.warn('Missing JWT token for forwardAuthToken!')
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
-
// Cloud SDK throws error if useCache is activated and jwt is undefined
|
|
47
|
-
if (destination.jwt === undefined) destination.useCache = false
|
|
48
|
-
|
|
49
46
|
if (LOG._debug) _logRequest(requestConfig, destination)
|
|
50
47
|
|
|
51
48
|
// cloud sdk requires a new mechanism to differentiate the priority of headers
|
|
@@ -250,7 +247,8 @@ module.exports.run = async (requestConfig, options) => {
|
|
|
250
247
|
}
|
|
251
248
|
|
|
252
249
|
const { kind, resolvedTarget, returnType } = options
|
|
253
|
-
if (kind === 'hcql')
|
|
250
|
+
if (kind === 'hcql')
|
|
251
|
+
return response.headers?.['content-type']?.includes('application/octet-stream') ? response.data : response.data.data
|
|
254
252
|
if (kind === 'odata-v4') return _purgeODataV4(response.data)
|
|
255
253
|
if (kind === 'odata-v2') return _purgeODataV2(response.data, resolvedTarget, returnType)
|
|
256
254
|
if (kind === 'odata') {
|
|
@@ -26,11 +26,11 @@ const _cqnWithPublicEntries = query => {
|
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
const _cqnToHcqlRequestConfig = cqn => {
|
|
29
|
-
if (cqn.SELECT && cqn.SELECT.from) return { method: '
|
|
29
|
+
if (cqn.SELECT && cqn.SELECT.from) return { method: 'POST', data: { SELECT: _cqnWithPublicEntries(cqn.SELECT) } }
|
|
30
30
|
if (cqn.INSERT && cqn.INSERT.into) return { method: 'POST', data: { INSERT: _cqnWithPublicEntries(cqn.INSERT) } }
|
|
31
|
-
if (cqn.UPDATE && cqn.UPDATE.entity) return { method: '
|
|
32
|
-
if (cqn.UPSERT && cqn.UPSERT.into) return { method: '
|
|
33
|
-
if (cqn.DELETE && cqn.DELETE.from) return { method: '
|
|
31
|
+
if (cqn.UPDATE && cqn.UPDATE.entity) return { method: 'POST', data: { UPDATE: _cqnWithPublicEntries(cqn.UPDATE) } }
|
|
32
|
+
if (cqn.UPSERT && cqn.UPSERT.into) return { method: 'POST', data: { UPSERT: _cqnWithPublicEntries(cqn.UPSERT) } }
|
|
33
|
+
if (cqn.DELETE && cqn.DELETE.from) return { method: 'POST', data: { DELETE: _cqnWithPublicEntries(cqn.DELETE) } }
|
|
34
34
|
|
|
35
35
|
cds.error(400, 'Invalid CQN object can not be processed.', JSON.stringify(cqn), _cqnToHcqlRequestConfig)
|
|
36
36
|
}
|