@sap/cds 6.6.2 → 6.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 +59 -2
- package/README.md +1 -1
- package/apis/connect.d.ts +11 -4
- package/apis/core.d.ts +1 -1
- package/apis/csn.d.ts +1 -0
- package/apis/internal/inference.d.ts +15 -2
- package/apis/log.d.ts +10 -0
- package/apis/serve.d.ts +4 -9
- package/apis/services.d.ts +86 -19
- package/bin/build/buildTaskEngine.js +16 -42
- package/bin/build/constants.js +4 -2
- package/bin/build/provider/buildTaskProviderInternal.js +117 -85
- package/bin/build/provider/hana/index.js +6 -1
- package/bin/build/provider/mtx-extension/index.js +74 -34
- package/bin/build/provider/mtx-sidecar/index.js +3 -3
- package/bin/build/provider/nodejs/index.js +2 -2
- package/bin/build/util.js +63 -14
- package/bin/cds-serve.js +6 -0
- package/bin/cds.js +20 -4
- package/bin/deploy/to-hana/cfUtil.js +15 -1
- package/bin/mtx/in-cds.js +2 -9
- package/bin/plugins.js +31 -0
- package/bin/serve.js +12 -12
- package/lib/compile/etc/_localized.js +1 -1
- package/lib/compile/for/lean_drafts.js +22 -6
- package/lib/compile/for/nodejs.js +4 -1
- package/lib/compile/load.js +4 -2
- package/lib/core/index.js +35 -15
- package/lib/dbs/cds-deploy.js +129 -133
- package/lib/env/cds-env.js +25 -17
- package/lib/env/cds-requires.js +10 -40
- package/lib/env/compat.js +12 -0
- package/lib/env/defaults.js +17 -9
- package/lib/env/plugins.js +29 -0
- package/lib/env/schemas/cds-rc.json +14 -0
- package/lib/index.js +3 -0
- package/lib/log/cds-log.js +7 -4
- package/lib/ql/CREATE.js +1 -1
- package/lib/ql/DELETE.js +1 -1
- package/lib/ql/DROP.js +3 -3
- package/lib/ql/INSERT.js +1 -1
- package/lib/ql/Query.js +14 -6
- package/lib/ql/SELECT.js +8 -2
- package/lib/ql/UPDATE.js +1 -1
- package/lib/ql/Whereable.js +1 -1
- package/lib/ql/cds-ql.js +1 -9
- package/lib/req/cds-context.js +1 -4
- package/lib/req/request.js +63 -2
- package/lib/req/response.js +3 -2
- package/lib/srv/bindings.js +69 -71
- package/lib/srv/cds-connect.js +4 -1
- package/lib/srv/cds-serve.js +4 -0
- package/lib/srv/middlewares/index.js +37 -6
- package/lib/srv/protocols/_legacy.js +1 -1
- package/lib/srv/protocols/index.js +1 -1
- package/lib/srv/srv-api.js +4 -6
- package/lib/srv/srv-dispatch.js +4 -3
- package/lib/srv/srv-handlers.js +1 -1
- package/lib/srv/srv-methods.js +8 -2
- package/lib/utils/cds-test.js +4 -1
- package/libx/_runtime/audit/Service.js +8 -9
- package/libx/_runtime/audit/generic/personal/index.js +1 -1
- package/libx/_runtime/audit/generic/personal/utils.js +1 -1
- package/libx/_runtime/audit/utils/v2.js +17 -20
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +11 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +0 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +3 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +4 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +1 -1
- package/libx/_runtime/cds-services/services/Service.js +1 -1
- package/libx/_runtime/cds-services/util/assert.js +41 -65
- package/libx/_runtime/common/code-ext/WorkerPool.js +90 -0
- package/libx/_runtime/common/code-ext/WorkerReq.js +0 -4
- package/libx/_runtime/common/code-ext/execute.js +28 -18
- package/libx/_runtime/common/code-ext/handlers.js +5 -4
- package/libx/_runtime/common/code-ext/worker.js +45 -3
- package/libx/_runtime/common/code-ext/workerQueryExecutor.js +8 -7
- package/libx/_runtime/common/composition/delete.js +1 -1
- package/libx/_runtime/common/composition/update.js +3 -5
- package/libx/_runtime/common/generic/auth/expand.js +1 -1
- package/libx/_runtime/common/generic/auth/readOnly.js +5 -4
- package/libx/_runtime/common/generic/auth/restrict.js +7 -2
- package/libx/_runtime/common/generic/crud.js +12 -1
- package/libx/_runtime/common/generic/etag.js +11 -3
- package/libx/_runtime/common/generic/input.js +8 -6
- package/libx/_runtime/common/generic/paging.js +25 -8
- package/libx/_runtime/common/generic/put.js +1 -1
- package/libx/_runtime/common/generic/sorting.js +0 -1
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/common/utils/cqn.js +5 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +2 -2
- package/libx/_runtime/common/utils/resolveView.js +14 -10
- package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -3
- package/libx/_runtime/common/utils/templateProcessor.js +15 -17
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +18 -6
- package/libx/_runtime/db/Service.js +1 -0
- package/libx/_runtime/db/data-conversion/post-processing.js +0 -18
- package/libx/_runtime/db/expand/expand-v2.js +2 -2
- package/libx/_runtime/db/expand/rawToExpanded.js +6 -6
- package/libx/_runtime/db/generic/integrity.js +1 -1
- package/libx/_runtime/db/utils/columns.js +5 -5
- package/libx/_runtime/fiori/generic/activate.js +3 -3
- package/libx/_runtime/fiori/generic/edit.js +1 -1
- package/libx/_runtime/fiori/generic/new.js +4 -0
- package/libx/_runtime/fiori/lean-draft.js +138 -46
- package/libx/_runtime/hana/execute.js +3 -1
- package/libx/_runtime/hana/pool.js +10 -2
- package/libx/_runtime/messaging/common-utils/AMQPClient.js +6 -1
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
- package/libx/_runtime/remote/Service.js +16 -13
- package/libx/_runtime/remote/utils/client.js +6 -1
- package/libx/_runtime/sqlite/Service.js +5 -59
- package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
- package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -2
- package/libx/_runtime/sqlite/execute.js +3 -1
- package/libx/_runtime/types/api.js +12 -3
- package/libx/odata/afterburner.js +36 -0
- package/libx/odata/cqn2odata.js +1 -1
- package/libx/odata/grammar.pegjs +5 -3
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +1 -1
- package/libx/rest/RestAdapter.js +1 -1
- package/libx/rest/RestRequest.js +1 -0
- package/package.json +5 -2
- package/libx/_runtime/common/code-ext/workerQuery.js +0 -45
- package/libx/_runtime/common/constants/limit.js +0 -12
- package/libx/_runtime/common/utils/page.js +0 -39
|
@@ -142,13 +142,18 @@ const _getRestrictionForTarget = (resolvedApplicables, target) => {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
|
|
145
|
-
if (!cds.env.
|
|
145
|
+
if (!cds.env.fiori.lean_draft && req.target._isDraftEnabled) {
|
|
146
146
|
req.query._draftRestrictions = resolvedApplicables
|
|
147
147
|
return
|
|
148
148
|
}
|
|
149
149
|
|
|
150
150
|
if (typeof req.query.SELECT.from === 'object')
|
|
151
|
-
|
|
151
|
+
// in case of $apply take a ref from sub SELECT//
|
|
152
|
+
req.query.SELECT.from.ref = _addWheresToRef(
|
|
153
|
+
req.query.SELECT.from.ref || req.query.SELECT.from.SELECT?.from?.ref,
|
|
154
|
+
model,
|
|
155
|
+
resolvedApplicables
|
|
156
|
+
)
|
|
152
157
|
|
|
153
158
|
const restrictionForTarget = _getRestrictionForTarget(resolvedApplicables, req.target)
|
|
154
159
|
if (!restrictionForTarget) return
|
|
@@ -54,10 +54,21 @@ exports.impl = cds.service.impl(function () {
|
|
|
54
54
|
|
|
55
55
|
if (req.event in { DELETE: 1, UPDATE: 1 } && req.target && req.target._isSingleton) {
|
|
56
56
|
if (req.event === 'DELETE' && !req.target['@odata.singleton.nullable']) req.reject(400, 'SINGLETON_NOT_NULLABLE')
|
|
57
|
+
const selectSingleton = SELECT.one(req.target)
|
|
57
58
|
const keyColumns = getColumns(req.target, { onlyNames: true, keysOnly: true })
|
|
58
|
-
|
|
59
|
+
|
|
60
|
+
// if no keys available, select all columns so we can delete the singleton with same content
|
|
61
|
+
if (keyColumns.length) selectSingleton.columns(keyColumns)
|
|
62
|
+
|
|
59
63
|
const singleton = await cds.tx(req).run(selectSingleton)
|
|
60
64
|
if (!singleton) req.reject(404)
|
|
65
|
+
|
|
66
|
+
// REVISIT: Workaround for singleton, to get keys into singleton
|
|
67
|
+
for (const keyName in singleton) {
|
|
68
|
+
if (!keyColumns.includes(keyName)) continue
|
|
69
|
+
req.data[keyName] = singleton[keyName]
|
|
70
|
+
}
|
|
71
|
+
|
|
61
72
|
req.query.where(singleton)
|
|
62
73
|
}
|
|
63
74
|
|
|
@@ -6,8 +6,7 @@ const { isActiveEntityRequested } = require('../../fiori/utils/where')
|
|
|
6
6
|
const { ensureDraftsSuffix } = require('../../fiori/utils/handler')
|
|
7
7
|
const { cqn2cqn4sql } = require('../../common/utils/cqn2cqn4sql')
|
|
8
8
|
const { isAsteriskColumn } = require('../../common/utils/rewriteAsterisks')
|
|
9
|
-
const
|
|
10
|
-
const { resolveView, getTransition } = require('../utils/resolveView')
|
|
9
|
+
const { resolveView } = require('../utils/resolveView')
|
|
11
10
|
|
|
12
11
|
const C_U_ = {
|
|
13
12
|
CREATE: 1,
|
|
@@ -61,8 +60,17 @@ const _addEtagColumns = (columns, entity) => {
|
|
|
61
60
|
const _isConcurrentODataReq = req => {
|
|
62
61
|
const isReadAfterDraftAction =
|
|
63
62
|
req.event === 'READ' && req.target._isDraftEnabled && req.context.event in { draftActivate: 1, EDIT: 1 }
|
|
63
|
+
// It's allowed to also delete drafts when actives are deleted
|
|
64
|
+
if (
|
|
65
|
+
cds.env.fiori?.lean_draft &&
|
|
66
|
+
req.event === 'READ' &&
|
|
67
|
+
req.context.event === 'DELETE' &&
|
|
68
|
+
req.target?.name.endsWith('.drafts') &&
|
|
69
|
+
!req.context?.target?.name.endsWith('.drafts')
|
|
70
|
+
)
|
|
71
|
+
return
|
|
64
72
|
const _req = isReadAfterDraftAction ? req.context : req
|
|
65
|
-
return _req
|
|
73
|
+
return _req._isOData && _req.isConcurrentResource
|
|
66
74
|
}
|
|
67
75
|
|
|
68
76
|
/**
|
|
@@ -86,7 +86,7 @@ const _preProcessAssertTarget = (assocInfo, assertMap) => {
|
|
|
86
86
|
if (parentKeys.length === 0) return
|
|
87
87
|
|
|
88
88
|
foreignKeys.forEach(keyMap => {
|
|
89
|
-
const clonedAssocInfo = Object.assign({}, assocInfo, {
|
|
89
|
+
const clonedAssocInfo = Object.assign({}, assocInfo, { pathSegmentsInfo: assocInfo.pathSegmentsInfo.slice(0) })
|
|
90
90
|
const target = {
|
|
91
91
|
key: mapKey,
|
|
92
92
|
entity: assocTarget,
|
|
@@ -126,6 +126,8 @@ const _processCategory = (req, category, value, elementInfo, assertMap) => {
|
|
|
126
126
|
// > preserve computed values if triggered by draftActivate and not managed
|
|
127
127
|
return
|
|
128
128
|
}
|
|
129
|
+
// Always take over the values from active entities
|
|
130
|
+
if (cds.env.fiori?.lean_draft && req.context?.event === 'EDIT') return
|
|
129
131
|
|
|
130
132
|
delete row[key]
|
|
131
133
|
value.val = undefined
|
|
@@ -152,7 +154,7 @@ const _getProcessorFn = (req, errors, assertMap) => {
|
|
|
152
154
|
const event = req.event
|
|
153
155
|
|
|
154
156
|
return elementInfo => {
|
|
155
|
-
const { row, key, element, plain,
|
|
157
|
+
const { row, key, element, plain, pathSegmentsInfo } = elementInfo
|
|
156
158
|
// ugly pointer passing for sonar
|
|
157
159
|
const value = { mandatory: false, val: row && row[key] }
|
|
158
160
|
|
|
@@ -163,7 +165,7 @@ const _getProcessorFn = (req, errors, assertMap) => {
|
|
|
163
165
|
if (_shouldSuppressErrorPropagation(event, value)) return
|
|
164
166
|
|
|
165
167
|
// REVISIT: Convert checkInputConstraints to template mechanism
|
|
166
|
-
checkInputConstraints({ element, value: value.val, errors,
|
|
168
|
+
checkInputConstraints({ element, value: value.val, errors, pathSegmentsInfo, event })
|
|
167
169
|
}
|
|
168
170
|
}
|
|
169
171
|
|
|
@@ -239,7 +241,7 @@ async function commonGenericInput(req) {
|
|
|
239
241
|
const pathOptions = {
|
|
240
242
|
rowUUIDGenerator: getRowUUIDGeneratorFn(req.event),
|
|
241
243
|
includeKeyValues: true,
|
|
242
|
-
|
|
244
|
+
pathSegmentsInfo: []
|
|
243
245
|
}
|
|
244
246
|
|
|
245
247
|
const boundAction = _getBoundAction(req)
|
|
@@ -247,7 +249,7 @@ async function commonGenericInput(req) {
|
|
|
247
249
|
if (boundAction) {
|
|
248
250
|
const pathSegment = _getBoundActionBindingParameter(boundAction)
|
|
249
251
|
const keys = req._ && req._.params && req._.params[0]
|
|
250
|
-
if (pathSegment) pathOptions.
|
|
252
|
+
if (pathSegment) pathOptions.pathSegmentsInfo.push(pathSegment)
|
|
251
253
|
|
|
252
254
|
if (keys && 'IsActiveEntity' in keys) {
|
|
253
255
|
pathOptions.draftKeys = { IsActiveEntity: keys.IsActiveEntity }
|
|
@@ -364,7 +366,7 @@ commonGenericInput._initial = true
|
|
|
364
366
|
_actionFunctionHandler._initial = true
|
|
365
367
|
|
|
366
368
|
module.exports = cds.service.impl(function () {
|
|
367
|
-
if (cds.env.
|
|
369
|
+
if (cds.env.fiori.lean_draft) {
|
|
368
370
|
this.before(['CREATE', 'UPDATE'], '*', commonGenericInput)
|
|
369
371
|
} else {
|
|
370
372
|
this.before(['CREATE', 'UPDATE', 'NEW', 'PATCH'], '*', commonGenericInput)
|
|
@@ -1,5 +1,27 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
module.exports = exports = cds.service.impl(function () {
|
|
4
|
+
commonGenericPaging._initial = true
|
|
5
|
+
this.before('READ', '*', commonGenericPaging)
|
|
6
|
+
})
|
|
7
|
+
|
|
8
|
+
const DEFAULT = cds.env.query?.limit?.default || 1000
|
|
9
|
+
const MAX = cds.env.query?.limit?.max || 1000
|
|
10
|
+
const _cached = Symbol('@cds.query.limit')
|
|
11
|
+
|
|
12
|
+
const getPageSize = def => {
|
|
13
|
+
if (_cached in def) return def[_cached]
|
|
14
|
+
let max = def['@cds.query.limit.max'] ?? def._service?.['@cds.query.limit.max'] ?? MAX
|
|
15
|
+
let _default =
|
|
16
|
+
def['@cds.query.limit.default'] ??
|
|
17
|
+
def['@cds.query.limit'] ??
|
|
18
|
+
def._service?.['@cds.query.limit.default'] ??
|
|
19
|
+
def._service?.['@cds.query.limit'] ??
|
|
20
|
+
DEFAULT
|
|
21
|
+
if (!max) max = Number.MAX_SAFE_INTEGER
|
|
22
|
+
if (!_default || _default > max) _default = max
|
|
23
|
+
return (def[_cached] = { default: _default, max })
|
|
24
|
+
}
|
|
3
25
|
|
|
4
26
|
const commonGenericPaging = function (req) {
|
|
5
27
|
// only if http request
|
|
@@ -21,10 +43,5 @@ const _addPaging = function ({ SELECT }, target) {
|
|
|
21
43
|
if (SELECT.from.SELECT?.limit) _addPaging(SELECT.from, target)
|
|
22
44
|
}
|
|
23
45
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
*/
|
|
27
|
-
module.exports = cds.service.impl(function () {
|
|
28
|
-
commonGenericPaging._initial = true
|
|
29
|
-
this.before('READ', '*', commonGenericPaging)
|
|
30
|
-
})
|
|
46
|
+
exports.getPageSize = getPageSize
|
|
47
|
+
exports.commonGenericPaging = commonGenericPaging
|
|
@@ -24,7 +24,7 @@ const _fillStructure = (row, parts, element, category, args) => {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const _getProcessorFn = req => {
|
|
27
|
-
const REST = req.
|
|
27
|
+
const REST = req._isRest
|
|
28
28
|
|
|
29
29
|
return ({ row, key, element, plain }) => {
|
|
30
30
|
if (!row || row[key] !== undefined) return
|
|
@@ -86,6 +86,7 @@ CRUD_VIA_NAVIGATION_NOT_SUPPORTED=CRUD via navigations is not yet supported
|
|
|
86
86
|
# draft
|
|
87
87
|
DRAFT_ALREADY_EXISTS=A draft for this entity already exists
|
|
88
88
|
DRAFT_LOCKED_BY_ANOTHER_USER=The entity is locked by another user
|
|
89
|
+
DRAFT_MODIFICATION_ONLY_VIA_ROOT=A draft can only be modified via its root entity
|
|
89
90
|
|
|
90
91
|
# singleton
|
|
91
92
|
SINGLETON_NOT_NULLABLE=The singleton entity is not nullable
|
|
@@ -17,7 +17,11 @@ const getEntityNameFromDeleteCQN = cqn => {
|
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
const getEntityNameFromUpdateCQN = cqn => {
|
|
20
|
-
return (
|
|
20
|
+
return (
|
|
21
|
+
(cqn.UPDATE.entity.ref && cqn.UPDATE.entity.ref[0] && (cqn.UPDATE.entity.ref[0].id || cqn.UPDATE.entity.ref[0])) ||
|
|
22
|
+
cqn.UPDATE.entity.name ||
|
|
23
|
+
cqn.UPDATE.entity
|
|
24
|
+
)
|
|
21
25
|
}
|
|
22
26
|
|
|
23
27
|
// scope: simple wheres à la "[{ ref: ['foo'] }, '=', { val: 'bar' }, 'and', ... ]"
|
|
@@ -713,7 +713,7 @@ const _convertToOneEqNullInFilter = (query, target) => {
|
|
|
713
713
|
}
|
|
714
714
|
// eslint-disable-next-line complexity
|
|
715
715
|
const _convertSelect = (query, model, _options) => {
|
|
716
|
-
const _4db = _options.service
|
|
716
|
+
const _4db = _options.service?.isDatabaseService
|
|
717
717
|
const options = Object.assign({ _4db, isStreaming: query._streaming }, _options)
|
|
718
718
|
|
|
719
719
|
// ensure query is ql enabled
|
|
@@ -796,7 +796,7 @@ const _convertSelect = (query, model, _options) => {
|
|
|
796
796
|
if (options._4db && !query.SELECT.columns) {
|
|
797
797
|
let target = query._target
|
|
798
798
|
if (target && target._unresolved && typeof target.name === 'string') {
|
|
799
|
-
target = model.definitions[ensureNoDraftsSuffix(target.name)] || target
|
|
799
|
+
target = model.definitions[cds.env.fiori.lean_draft ? target.name : ensureNoDraftsSuffix(target.name)] || target
|
|
800
800
|
}
|
|
801
801
|
|
|
802
802
|
if (target && !Object.prototype.hasOwnProperty.call(target, '_unresolved')) {
|
|
@@ -5,7 +5,7 @@ const PERSISTENCE_TABLE = '@cds.persistence.table'
|
|
|
5
5
|
const { rewriteAsterisks } = require('../../common/utils/rewriteAsterisks')
|
|
6
6
|
|
|
7
7
|
const getError = require('../error')
|
|
8
|
-
const { getEntityNameFromDeleteCQN
|
|
8
|
+
const { getEntityNameFromDeleteCQN } = require('../utils/cqn')
|
|
9
9
|
|
|
10
10
|
const _setInverseTransition = (mapping, ref, mapped) => {
|
|
11
11
|
const existing = mapping.get(ref)
|
|
@@ -332,7 +332,7 @@ const _newUpdate = (query, transitions, service) => {
|
|
|
332
332
|
newUpdate.where = _newWhere(
|
|
333
333
|
newUpdate.where,
|
|
334
334
|
targetTransition,
|
|
335
|
-
|
|
335
|
+
cds.infer(query, service.model.definitions).name,
|
|
336
336
|
query.UPDATE.entity.as
|
|
337
337
|
)
|
|
338
338
|
}
|
|
@@ -353,7 +353,7 @@ const _newSelect = (query, transitions, service) => {
|
|
|
353
353
|
if (!newSelect.columns && targetTransition.mapping.size) newSelect.columns = _initialColumns(targetTransition)
|
|
354
354
|
if (newSelect.columns) {
|
|
355
355
|
rewriteAsterisks({ SELECT: query.SELECT }, service.model, {
|
|
356
|
-
_4db: service
|
|
356
|
+
_4db: service.isDatabaseService,
|
|
357
357
|
target: targetTransition.queryTarget
|
|
358
358
|
})
|
|
359
359
|
newSelect.columns = _newColumns(newSelect.columns, targetTransition, service, service.kind !== 'app-service')
|
|
@@ -380,12 +380,16 @@ const _newInsert = (query, transitions, service) => {
|
|
|
380
380
|
const targetTransition = transitions[transitions.length - 1]
|
|
381
381
|
const targetName = targetTransition.target.name
|
|
382
382
|
const newInsert = Object.create(query.INSERT)
|
|
383
|
-
newInsert.into
|
|
384
|
-
? {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
383
|
+
if (newInsert.into) {
|
|
384
|
+
const refObject = newInsert.into.ref ? newInsert.into : { ref: [query.INSERT.into] }
|
|
385
|
+
newInsert.into = {
|
|
386
|
+
...refObject,
|
|
387
|
+
ref: _rewriteQueryPath(refObject, transitions)
|
|
388
|
+
}
|
|
389
|
+
if (!query.INSERT.into.ref) newInsert.into = newInsert.into.ref[0] // leave as string
|
|
390
|
+
} else {
|
|
391
|
+
newInsert.into = targetName
|
|
392
|
+
}
|
|
389
393
|
if (newInsert.columns) newInsert.columns = _newInsertColumns(newInsert.columns, targetTransition)
|
|
390
394
|
if (newInsert.entries) newInsert.entries = _newEntries(newInsert.entries, targetTransition, service)
|
|
391
395
|
Object.defineProperty(newInsert, '_transitions', {
|
|
@@ -536,7 +540,7 @@ const _getTransitionData = (target, columns, service, skipForbiddenViewCheck) =>
|
|
|
536
540
|
if (!skipForbiddenViewCheck) _checkForForbiddenViews(target)
|
|
537
541
|
const targetStartsWithSrvName = service.namespace && target.name.startsWith(`${service.namespace}.`)
|
|
538
542
|
const persistenceTable = _isPersistenceTable(target)
|
|
539
|
-
const isDatabaseService = service
|
|
543
|
+
const isDatabaseService = service.isDatabaseService
|
|
540
544
|
columns = _queryColumns(target, columns, persistenceTable, !isDatabaseService && !targetStartsWithSrvName)
|
|
541
545
|
// REVISIT: Change once we expose database service
|
|
542
546
|
if (persistenceTable && isDatabaseService) {
|
|
@@ -29,7 +29,7 @@ const _expandColumn = (column, target, _4db) => {
|
|
|
29
29
|
const rewriteExpandAsterisk = (columns, target) => {
|
|
30
30
|
const expandAllColIdx = columns.findIndex(col => {
|
|
31
31
|
if (col.ref || !col.expand) return
|
|
32
|
-
return
|
|
32
|
+
return col.expand.includes('*')
|
|
33
33
|
})
|
|
34
34
|
if (expandAllColIdx > -1) {
|
|
35
35
|
const { expand } = columns.splice(expandAllColIdx, 1)[0]
|
|
@@ -56,7 +56,6 @@ const _rewriteAsterisk = (columns, target, _4db, isRoot) => {
|
|
|
56
56
|
}
|
|
57
57
|
|
|
58
58
|
const _rewriteAsterisks = (cqn, target, _4db, isRoot) => {
|
|
59
|
-
if (cqn.expand === '*') cqn.expand = ['*']
|
|
60
59
|
const columns = cqn.expand || cqn.columns
|
|
61
60
|
_rewriteAsterisk(columns, target, _4db, isRoot)
|
|
62
61
|
rewriteExpandAsterisk(columns, target)
|
|
@@ -119,7 +118,7 @@ const rewriteAsterisks = (query, model, options) => {
|
|
|
119
118
|
if (!target) return
|
|
120
119
|
|
|
121
120
|
query.SELECT.columns = getColumns(target, { _4db }).map(col => ({ ref: [col.name] }))
|
|
122
|
-
if (_4db && target._isDraftEnabled && !cds.env.
|
|
121
|
+
if (_4db && target._isDraftEnabled && !cds.env.fiori.lean_draft)
|
|
123
122
|
query.SELECT.columns.push(..._cqlDraftColumns(target))
|
|
124
123
|
}
|
|
125
124
|
}
|
|
@@ -1,19 +1,17 @@
|
|
|
1
1
|
const DELIMITER = require('./templateDelimiter')
|
|
2
|
-
const pathSerializer = require('./templateProcessorPathSerializer')
|
|
3
2
|
|
|
4
|
-
const _processElement = (processFn, row, key, target, picked = {}, isRoot,
|
|
3
|
+
const _processElement = (processFn, row, key, target, picked = {}, isRoot, pathSegmentsInfo) => {
|
|
5
4
|
const element = (target.elements || target.params)[key]
|
|
6
5
|
const { plain } = picked
|
|
7
6
|
|
|
8
7
|
if (!plain) return
|
|
9
8
|
|
|
10
|
-
/**
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
elementInfo.
|
|
16
|
-
elementInfo.pathSegments.push(...target._flat2struct[key])
|
|
9
|
+
/** @type import('../../types/api').templateElementInfo */
|
|
10
|
+
const elementInfo = { row, key, element, target, plain, isRoot, pathSegmentsInfo }
|
|
11
|
+
|
|
12
|
+
if (!element && target._flat2struct?.[key] && elementInfo.pathSegmentsInfo) {
|
|
13
|
+
elementInfo.pathSegmentsInfo = pathSegmentsInfo.slice(0)
|
|
14
|
+
elementInfo.pathSegmentsInfo.push(...target._flat2struct[key])
|
|
17
15
|
}
|
|
18
16
|
|
|
19
17
|
processFn(elementInfo)
|
|
@@ -23,7 +21,7 @@ const _processRow = (processFn, row, template, tKey, tValue, isRoot, pathOptions
|
|
|
23
21
|
const { template: subTemplate, picked } = tValue
|
|
24
22
|
const key = tKey.split(DELIMITER).pop()
|
|
25
23
|
|
|
26
|
-
_processElement(processFn, row, key, template.target, picked, isRoot, pathOptions.
|
|
24
|
+
_processElement(processFn, row, key, template.target, picked, isRoot, pathOptions.pathSegmentsInfo)
|
|
27
25
|
|
|
28
26
|
// process deep
|
|
29
27
|
if (subTemplate && typeof row === 'object' && row) {
|
|
@@ -48,20 +46,20 @@ const _processComplex = (processFn, row, template, key, pathOptions) => {
|
|
|
48
46
|
if (rows.length === 0) return
|
|
49
47
|
const keyNames = pathOptions.includeKeyValues && _getTargetKeyNames(template.target)
|
|
50
48
|
|
|
51
|
-
for (
|
|
52
|
-
const row = rows[idx]
|
|
49
|
+
for (const row of rows) {
|
|
53
50
|
if (row == null) continue
|
|
54
51
|
const args = { processFn, row, template, isRoot: false, pathOptions }
|
|
55
52
|
|
|
56
|
-
let
|
|
53
|
+
let pathSegmentInfo
|
|
57
54
|
if (pathOptions.includeKeyValues) {
|
|
58
|
-
|
|
59
|
-
|
|
55
|
+
pathOptions.rowUUIDGenerator?.(keyNames, row, template)
|
|
56
|
+
/** @type import('../../types/api').pathSegmentInfo */
|
|
57
|
+
pathSegmentInfo = { key, keyNames, row, elements: template.target.elements, draftKeys: pathOptions.draftKeys }
|
|
60
58
|
}
|
|
61
59
|
|
|
62
|
-
if (pathOptions.
|
|
60
|
+
if (pathOptions.pathSegmentsInfo) pathOptions.pathSegmentsInfo.push(pathSegmentInfo || key)
|
|
63
61
|
templateProcessor(args)
|
|
64
|
-
if (pathOptions.
|
|
62
|
+
if (pathOptions.pathSegmentsInfo) pathOptions.pathSegmentsInfo.pop()
|
|
65
63
|
}
|
|
66
64
|
}
|
|
67
65
|
|
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
const segmentSerializer = pathSegmentInfo => {
|
|
4
|
+
const { key: tKey, row, elements, draftKeys } = pathSegmentInfo
|
|
5
|
+
let keyNames = pathSegmentInfo.keyNames
|
|
6
|
+
|
|
3
7
|
const keyValuePairs = keyNames
|
|
4
|
-
.filter(key => {
|
|
5
|
-
if (cds.env.features.lean_draft && key === 'IsActiveEntity') return false
|
|
6
|
-
return true
|
|
7
|
-
})
|
|
8
8
|
.map(key => {
|
|
9
9
|
let quote
|
|
10
10
|
|
|
@@ -19,11 +19,23 @@ const templatePathSerializer = (tKey, keyNames, row, elements, draftKeys) => {
|
|
|
19
19
|
}
|
|
20
20
|
|
|
21
21
|
const keyValue = row[key] ?? draftKeys?.[key]
|
|
22
|
+
if (keyValue == null) return
|
|
22
23
|
return `${key}=${quote}${keyValue}${quote}`
|
|
23
24
|
})
|
|
25
|
+
.filter(c => c)
|
|
24
26
|
|
|
25
27
|
const keyValuePairsSerialized = keyValuePairs.join(',')
|
|
26
|
-
|
|
28
|
+
const pathSegment = `${tKey}(${keyValuePairsSerialized})`
|
|
29
|
+
return pathSegment
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const templatePathSerializer = (elementName, pathSegmentsInfo) => {
|
|
33
|
+
const pathSegments = pathSegmentsInfo.map(pathSegmentInfo => {
|
|
34
|
+
if (typeof pathSegmentInfo === 'string') return pathSegmentInfo
|
|
35
|
+
return segmentSerializer(pathSegmentInfo)
|
|
36
|
+
})
|
|
37
|
+
const path = `${pathSegments.join('/')}${pathSegments.length ? '/' : ''}${elementName}`
|
|
38
|
+
return path
|
|
27
39
|
}
|
|
28
40
|
|
|
29
41
|
module.exports = templatePathSerializer
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
const { getEntityNameFromCQN, traverseFroms } = require('../../common/utils/entityFromCqn')
|
|
2
2
|
const { ensureNoDraftsSuffix } = require('../../common/utils/draft')
|
|
3
|
-
const { proxifyIfFlattened } = require('../../../common/utils/ucsn')
|
|
4
|
-
const cds = require('../../../../lib')
|
|
5
3
|
|
|
6
4
|
const _getCastFunction = ({ type }) => {
|
|
7
5
|
switch (type) {
|
|
@@ -34,13 +32,6 @@ const _getNestedElement = (entity, key) => {
|
|
|
34
32
|
return _structElement(entity, structPath)
|
|
35
33
|
}
|
|
36
34
|
|
|
37
|
-
const _structure = csnEntity => (value, key, row, unaliasedKey) => {
|
|
38
|
-
proxifyIfFlattened(csnEntity, row)
|
|
39
|
-
const effectiveKey = unaliasedKey || key
|
|
40
|
-
delete row[effectiveKey]
|
|
41
|
-
row[effectiveKey] = value
|
|
42
|
-
}
|
|
43
|
-
|
|
44
35
|
const _addConverter = (mapper, name, converter) => {
|
|
45
36
|
if (mapper.has(name)) {
|
|
46
37
|
const oldConverter = mapper.get(name)
|
|
@@ -126,15 +117,6 @@ const _getMapperForListedElements = (conversionMap, csn, cqn) => {
|
|
|
126
117
|
row[effectiveKey] = JSON.parse(val)
|
|
127
118
|
}) // > arrayed elements
|
|
128
119
|
}
|
|
129
|
-
|
|
130
|
-
if (
|
|
131
|
-
cds.env.features.ucsn_struct_conversion &&
|
|
132
|
-
element.parent &&
|
|
133
|
-
element.parent._isStructured &&
|
|
134
|
-
!element._isStructured
|
|
135
|
-
) {
|
|
136
|
-
_addConverter(mapper, col.as ? col.as : name, _structure(entity))
|
|
137
|
-
}
|
|
138
120
|
}
|
|
139
121
|
}
|
|
140
122
|
|
|
@@ -119,7 +119,7 @@ const _addForeignKeys = (columns, entity, options) => {
|
|
|
119
119
|
* @returns object
|
|
120
120
|
*/
|
|
121
121
|
const expandV2 = async (model, dbc, query, user, locale, txTimestamp, executeSelectCQN) => {
|
|
122
|
-
const expandColumn = query.SELECT.columns.find(c => c.expand && typeof c.expand === 'string')
|
|
122
|
+
const expandColumn = query.SELECT.columns.find(c => c.expand && typeof c.expand[0] === 'string')
|
|
123
123
|
const options = Object.assign({ onlyKeys: false, onlyCompositions: false }, expandColumn._options)
|
|
124
124
|
// remove expand columns from query without modifying
|
|
125
125
|
const topLevelSelect = query.clone().columns(query.SELECT.columns.filter(c => !c.expand))
|
|
@@ -137,7 +137,7 @@ const expandV2 = async (model, dbc, query, user, locale, txTimestamp, executeSel
|
|
|
137
137
|
|
|
138
138
|
// _associations contains compositions and associations
|
|
139
139
|
if (entity._associations) {
|
|
140
|
-
const depth = expandColumn.expand === '**' ? -1 : Number(expandColumn.expand.replace('*', ''))
|
|
140
|
+
const depth = expandColumn.expand[0] === '**' ? -1 : Number(expandColumn.expand[0].replace('*', ''))
|
|
141
141
|
await _autoExpandNavsAndAttachToResult(entity, Array.isArray(result) ? result : [result], depth, options)
|
|
142
142
|
}
|
|
143
143
|
|
|
@@ -111,7 +111,7 @@ class RawToExpanded {
|
|
|
111
111
|
let expandedItems = this._getResultCache(toManyTree.concat(key))[mapping[GET_KEY_VALUE](false, entry)] || []
|
|
112
112
|
|
|
113
113
|
// the expanded items may include the actives of the deleted drafts -> filter out
|
|
114
|
-
if (!cds.env.
|
|
114
|
+
if (!cds.env.fiori.lean_draft && rootIsActiveEntity !== null) {
|
|
115
115
|
if (mapping[TO_ACTIVE]) expandedItems = expandedItems.filter(ele => ele.IsActiveEntity !== false)
|
|
116
116
|
else expandedItems = expandedItems.filter(ele => !!ele.IsActiveEntity === rootIsActiveEntity)
|
|
117
117
|
}
|
|
@@ -144,7 +144,7 @@ class RawToExpanded {
|
|
|
144
144
|
else if (rootIsActiveEntity) row[key] = parsed && parsed.IsActiveEntity !== false ? parsed : null
|
|
145
145
|
else row[key] = parsed && parsed.IsActiveEntity === rootIsActiveEntity ? parsed : null
|
|
146
146
|
}
|
|
147
|
-
if (mapping[CLEANUP_KEYS]) {
|
|
147
|
+
if (parsed && mapping[CLEANUP_KEYS]) {
|
|
148
148
|
for (const key in mapping[CLEANUP_KEYS]) delete parsed[key]
|
|
149
149
|
}
|
|
150
150
|
} else {
|
|
@@ -153,7 +153,7 @@ class RawToExpanded {
|
|
|
153
153
|
// Assume a DB will not return undefined, but always null
|
|
154
154
|
this._convertValue(rawValue, conversionMapper.get(mapping), mapping, row, key)
|
|
155
155
|
|
|
156
|
-
isEntityNull = this._isNull(isEntityNull, rawValue)
|
|
156
|
+
isEntityNull = this._isNull(isEntityNull, rawValue, key)
|
|
157
157
|
}
|
|
158
158
|
}
|
|
159
159
|
|
|
@@ -173,12 +173,12 @@ class RawToExpanded {
|
|
|
173
173
|
* @returns {boolean}
|
|
174
174
|
* @private
|
|
175
175
|
*/
|
|
176
|
-
_isNull(isEntityNull, value) {
|
|
176
|
+
_isNull(isEntityNull, value, key) {
|
|
177
177
|
if (isEntityNull === undefined) {
|
|
178
|
-
return value === null || value === undefined
|
|
178
|
+
return value === null || value === undefined || key === 'IsActiveEntity'
|
|
179
179
|
}
|
|
180
180
|
|
|
181
|
-
return isEntityNull === true && (value === null || value === undefined)
|
|
181
|
+
return isEntityNull === true && (value === null || value === undefined || key === 'IsActiveEntity')
|
|
182
182
|
}
|
|
183
183
|
|
|
184
184
|
/**
|
|
@@ -333,7 +333,7 @@ const _checkReferenceIntegrity = (entity, data, req, csn, run) => {
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
const _checkIntegrityWrapper = (req, csn, run) => async (data, entity) => {
|
|
336
|
-
if (cds.env.
|
|
336
|
+
if (cds.env.fiori.lean_draft && entity.name?.endsWith('.drafts')) return
|
|
337
337
|
const errors = await _checkReferenceIntegrity(entity, data, req, csn, run)
|
|
338
338
|
if (errors && errors.length !== 0) for (const err of errors) req.error(err)
|
|
339
339
|
}
|
|
@@ -16,15 +16,15 @@ const getColumns = (entity, { _4db, onlyKeys } = { _4db: true, onlyKeys: false }
|
|
|
16
16
|
if (!(entity && entity.elements)) return []
|
|
17
17
|
const columnNames = []
|
|
18
18
|
// REVISIT!!!
|
|
19
|
-
const
|
|
20
|
-
|
|
21
|
-
|
|
19
|
+
const { structs = cds.env.features.ucsn_struct_conversion } = cds.env.effective.odata
|
|
20
|
+
const { lean_draft } = cds.env.fiori
|
|
21
|
+
const elements = lean_draft ? entity.elements : Object.getPrototypeOf(entity.elements) || entity.elements
|
|
22
22
|
for (const elementName in elements) {
|
|
23
23
|
const element = elements[elementName]
|
|
24
24
|
if (onlyKeys && !element.key) continue
|
|
25
25
|
if (element.isAssociation) continue
|
|
26
|
-
if (!
|
|
27
|
-
if (
|
|
26
|
+
if (!lean_draft && _4db && entity._isDraftEnabled && elementName in DRAFT_COLUMNS_MAP) continue
|
|
27
|
+
if (structs && element.elements) {
|
|
28
28
|
columnNames.push(...resolveStructured({ element, structProperties: [] }, false))
|
|
29
29
|
continue
|
|
30
30
|
}
|
|
@@ -146,8 +146,8 @@ const fioriGenericActivate = async function (req) {
|
|
|
146
146
|
|
|
147
147
|
// REVISIT: should not be necessary
|
|
148
148
|
r._ = Object.assign(r._, req._)
|
|
149
|
-
r.getUriInfo = () => req.getUriInfo()
|
|
150
|
-
r.getUrlObject = () => req.getUrlObject()
|
|
149
|
+
if (req.getUriInfo) r.getUriInfo = () => req.getUriInfo()
|
|
150
|
+
if (req.getUrlObject) r.getUrlObject = () => req.getUrlObject()
|
|
151
151
|
r._.params = req.params
|
|
152
152
|
r._.query = req.query
|
|
153
153
|
|
|
@@ -178,7 +178,7 @@ const fioriGenericActivate = async function (req) {
|
|
|
178
178
|
|
|
179
179
|
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
180
180
|
// status code must be set in handler to allow overriding for FE V2
|
|
181
|
-
req?._?.odataRes
|
|
181
|
+
req?._?.odataRes?.setStatusCode(201)
|
|
182
182
|
|
|
183
183
|
return result
|
|
184
184
|
}
|
|
@@ -164,7 +164,7 @@ const fioriGenericEdit = async function (req) {
|
|
|
164
164
|
|
|
165
165
|
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
166
166
|
// status code must be set in handler to allow overriding for FE V2
|
|
167
|
-
req?._?.odataRes
|
|
167
|
+
req?._?.odataRes?.setStatusCode(201)
|
|
168
168
|
|
|
169
169
|
return results[0][0]
|
|
170
170
|
}
|
|
@@ -56,6 +56,10 @@ const fioriGenericNew = async function (req, next) {
|
|
|
56
56
|
|
|
57
57
|
if (!cds.db) req.reject('NO_DATABASE_CONNECTION')
|
|
58
58
|
|
|
59
|
+
const isRoot = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
|
|
60
|
+
// Only allowed for pseudo draft roots (entities with this action)
|
|
61
|
+
if (isRoot && !req.target['@Common.DraftRoot.ActivationAction']) req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
|
|
62
|
+
|
|
59
63
|
const navigationToMany = isNavigationToMany(req)
|
|
60
64
|
|
|
61
65
|
const adminDataCQN = navigationToMany
|