@sap/cds 6.6.2 → 6.7.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 +72 -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 +22 -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 +23 -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/auth/strategies/mock.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +12 -5
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +5 -5
- 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 +28 -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/auth/utils.js +5 -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 +3 -3
- 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 +178 -68
- package/libx/_runtime/hana/execute.js +3 -1
- package/libx/_runtime/hana/pool.js +10 -2
- package/libx/_runtime/hana/search2cqn4sql.js +1 -1
- 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 +38 -2
- package/libx/odata/cqn2odata.js +3 -2
- 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
|
@@ -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
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const cds = require('../cds'),
|
|
2
2
|
{ Object_keys } = cds.utils
|
|
3
3
|
const LOG = cds.log('fiori|drafts')
|
|
4
|
+
const original = Symbol('original')
|
|
4
5
|
|
|
5
6
|
const DRAFT_ELEMENTS = new Set([
|
|
6
7
|
'IsActiveEntity',
|
|
@@ -41,6 +42,14 @@ const _inProcessByUserXpr = lockShiftedNow => ({
|
|
|
41
42
|
cast: { type: 'cds.String' }
|
|
42
43
|
})
|
|
43
44
|
|
|
45
|
+
/// It's important to wait for the completion of all promises, otherwise a rollback might happen too soon
|
|
46
|
+
const _promiseAll = async array => {
|
|
47
|
+
const results = await Promise.allSettled(array)
|
|
48
|
+
const e = results.find(r => r.status === 'rejected')
|
|
49
|
+
if (e) throw e.reason
|
|
50
|
+
return results.map(r => r.value)
|
|
51
|
+
}
|
|
52
|
+
|
|
44
53
|
const _lock = {
|
|
45
54
|
get shiftedNow() {
|
|
46
55
|
return new Date(Math.max(0, Date.now() - DRAFT_CANCEL_TIMEOUT_IN_MIN() * 60 * 1000)).toISOString()
|
|
@@ -69,12 +78,14 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
69
78
|
|
|
70
79
|
if (
|
|
71
80
|
!req.query ||
|
|
81
|
+
req.query.UPSERT || // skip UPSERTs (might have an additional INSERT)
|
|
72
82
|
(!req.query.SELECT && !req.query.INSERT && !req.query.UPDATE && !req.query.DELETE) ||
|
|
73
83
|
req.query._draftParams
|
|
74
84
|
)
|
|
75
85
|
return handle(req)
|
|
76
86
|
const query = _cleansed(req.query, this.model)
|
|
77
|
-
_cleanseParams(req.params)
|
|
87
|
+
_cleanseParams(req.params, req.target)
|
|
88
|
+
if (req.data) _cleanseParams(req.data, req.target)
|
|
78
89
|
const draftParams = query._draftParams
|
|
79
90
|
|
|
80
91
|
const _newReq = (req, query, draftParams, event) => {
|
|
@@ -82,8 +93,11 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
82
93
|
query._target = undefined
|
|
83
94
|
query._draftParams = draftParams
|
|
84
95
|
cds.infer(query, this.model.definitions)
|
|
96
|
+
|
|
97
|
+
// REVISIT: This is extremely bad. We should be able to just create a copy without such hacks.
|
|
85
98
|
const _req = cds.Request.for(req._) // REVISIT: this causes req._.data of WRITE reqs copied to READ reqs
|
|
86
|
-
|
|
99
|
+
// If we create a `READ` event based on a modifying request, we delete data
|
|
100
|
+
if (event === 'READ' && req.event !== 'READ') delete _req.data // which we fix here -> but this is an ugly workaround
|
|
87
101
|
_req.query = query
|
|
88
102
|
_req.event =
|
|
89
103
|
event ||
|
|
@@ -93,19 +107,24 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
93
107
|
(query.DELETE && 'DELETE') ||
|
|
94
108
|
req.event
|
|
95
109
|
_req.target = query._target
|
|
110
|
+
_req._ = Object.assign({}, req._ || {}) // don't share the same `_` object
|
|
96
111
|
_req._.params = req.params
|
|
97
112
|
_req.params = req.params
|
|
98
113
|
_req._.query = query
|
|
99
114
|
_req._ = req._
|
|
100
|
-
_req.
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
Object.defineProperty(_req, '_messages', {
|
|
108
|
-
|
|
115
|
+
_req._isRest = req._isRest
|
|
116
|
+
_req._isOData = req._isOData
|
|
117
|
+
_req.isConcurrentResource = req.isConcurrentResource
|
|
118
|
+
_req.isConditional = req.isConditional
|
|
119
|
+
_req.validateEtag = req.validateEtag
|
|
120
|
+
const cqnData = _req.query.UPDATE?.data || _req.query.INSERT?.entries?.[0]
|
|
121
|
+
if (cqnData) _req.data = cqnData // must point to the same object
|
|
122
|
+
Object.defineProperty(_req, '_messages', {
|
|
123
|
+
get: function () {
|
|
124
|
+
return req._messages
|
|
125
|
+
}
|
|
126
|
+
})
|
|
127
|
+
if (req.tx) _req.tx = req.tx
|
|
109
128
|
return _req
|
|
110
129
|
}
|
|
111
130
|
|
|
@@ -155,7 +174,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
155
174
|
deletes.push(
|
|
156
175
|
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
|
|
157
176
|
)
|
|
158
|
-
await
|
|
177
|
+
await _promiseAll(deletes)
|
|
159
178
|
return req.data
|
|
160
179
|
}
|
|
161
180
|
|
|
@@ -188,7 +207,8 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
188
207
|
delete res.DraftAdministrativeData
|
|
189
208
|
const HasActiveEntity = res.HasActiveEntity
|
|
190
209
|
delete res.HasActiveEntity
|
|
191
|
-
|
|
210
|
+
delete res.IsActiveEntity
|
|
211
|
+
await _promiseAll([
|
|
192
212
|
run(DELETE.from(targetDraft).where(targetWhere)),
|
|
193
213
|
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: DraftAdministrativeData_DraftUUID })
|
|
194
214
|
])
|
|
@@ -196,7 +216,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
196
216
|
const result = await run(
|
|
197
217
|
HasActiveEntity ? UPDATE(req.target).data(res).where(targetWhere) : INSERT.into(req.target).entries(res)
|
|
198
218
|
)
|
|
199
|
-
req?._?.odataRes
|
|
219
|
+
req?._?.odataRes?.setStatusCode(201)
|
|
200
220
|
|
|
201
221
|
return Object.assign(result, { IsActiveEntity: true })
|
|
202
222
|
|
|
@@ -252,11 +272,20 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
252
272
|
const updateData = { ...req.data }
|
|
253
273
|
delete updateData.IsActiveEntity
|
|
254
274
|
await run(UPDATE({ ref: draftsRef }).data(updateData))
|
|
255
|
-
|
|
275
|
+
req.data.IsActiveEntity = false
|
|
276
|
+
return req.data
|
|
256
277
|
}
|
|
257
278
|
}
|
|
258
279
|
|
|
259
280
|
if (req.event === 'READ') {
|
|
281
|
+
if (
|
|
282
|
+
!Object.keys(draftParams).length &&
|
|
283
|
+
!req.query._target.name?.endsWith('DraftAdministrativeData') &&
|
|
284
|
+
!req.query._target.drafts
|
|
285
|
+
) {
|
|
286
|
+
req.query = query
|
|
287
|
+
return handle(req)
|
|
288
|
+
}
|
|
260
289
|
const read = req.query._target.name.endsWith('.drafts')
|
|
261
290
|
? Read.ownDrafts
|
|
262
291
|
: draftParams.IsActiveEntity === false && draftParams.SiblingEntity_IsActiveEntity === null
|
|
@@ -281,8 +310,34 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
281
310
|
return result
|
|
282
311
|
}
|
|
283
312
|
|
|
284
|
-
|
|
285
|
-
const result = await handle(
|
|
313
|
+
req.query = query
|
|
314
|
+
const result = await handle(req)
|
|
315
|
+
return result
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// REVISIT: It's not optimal to first calculate the whole result array and only later
|
|
319
|
+
// delete unrequested properties. However, as a first step, we do it that way,
|
|
320
|
+
// especially since the current db driver always adds those fields.
|
|
321
|
+
// Once we switch to the new driver, we'll adapt it.
|
|
322
|
+
const _requested = (result, query) => {
|
|
323
|
+
const originalQuery = query[original]
|
|
324
|
+
if (!result || !originalQuery) return result
|
|
325
|
+
const all = ['HasActiveEntity', 'HasDraftEntity']
|
|
326
|
+
|
|
327
|
+
const ignoredCols = new Set(all.concat('DraftAdministrativeData'))
|
|
328
|
+
const _isODataV2 = cds.context?.http?.req?.headers?.['x-cds-odata-version'] === 'v2'
|
|
329
|
+
if (!_isODataV2) ignoredCols.add('DraftAdministrativeData_DraftUUID')
|
|
330
|
+
for (const col of originalQuery.SELECT.columns || ['*']) {
|
|
331
|
+
const name = col.as || col.ref?.[0] || col
|
|
332
|
+
if (all.includes(name) || name === 'DraftAdministrativeData' || name === 'DraftAdministrativeData_DraftUUID')
|
|
333
|
+
ignoredCols.delete(name)
|
|
334
|
+
if (name === '*') all.forEach(c => ignoredCols.delete(c))
|
|
335
|
+
}
|
|
336
|
+
if (!ignoredCols.size) return result
|
|
337
|
+
const resArray = Array.isArray(result) ? result : [result]
|
|
338
|
+
for (const row of resArray) {
|
|
339
|
+
for (const ignoredCol of ignoredCols) delete row[ignoredCol]
|
|
340
|
+
}
|
|
286
341
|
return result
|
|
287
342
|
}
|
|
288
343
|
|
|
@@ -291,6 +346,13 @@ const Read = {
|
|
|
291
346
|
LOG.debug('List Editing Status: Only Active')
|
|
292
347
|
// DraftAdministrativeData is only accessible via drafts
|
|
293
348
|
if (query._target.name.endsWith('.DraftAdministrativeData')) return run(query._drafts)
|
|
349
|
+
if (!query._target._isDraftEnabled) return run(query)
|
|
350
|
+
if (!query.SELECT.groupBy && query.SELECT.columns && !query.SELECT.columns.some(c => c === '*')) {
|
|
351
|
+
const keys = Object_keys(query._target.keys).filter(k => k !== 'IsActiveEntity')
|
|
352
|
+
for (const key of keys) {
|
|
353
|
+
if (!query.SELECT.columns.some(c => c.ref?.[0] === key)) query.SELECT.columns.push({ ref: [key] })
|
|
354
|
+
}
|
|
355
|
+
}
|
|
294
356
|
const actives = await run(query)
|
|
295
357
|
if (!actives || (Array.isArray(actives) && !actives.length) || !query._target.drafts) return actives
|
|
296
358
|
let drafts
|
|
@@ -313,7 +375,7 @@ const Read = {
|
|
|
313
375
|
DraftAdministrativeData_DraftUUID: null
|
|
314
376
|
})
|
|
315
377
|
)
|
|
316
|
-
return actives
|
|
378
|
+
return _requested(actives, query)
|
|
317
379
|
},
|
|
318
380
|
unchanged: async function (run, query) {
|
|
319
381
|
LOG.debug('List Editing Status: Unchanged')
|
|
@@ -325,10 +387,10 @@ const Read = {
|
|
|
325
387
|
draftsQuery.SELECT.columns = keys.map(k => ({ ref: [k] }))
|
|
326
388
|
|
|
327
389
|
const drafts = await run(draftsQuery)
|
|
328
|
-
const res = Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
|
|
390
|
+
const res = await Read.onlyActives(run, query.where(Read.whereNotIn(query._target, drafts)), {
|
|
329
391
|
ignoreDrafts: true
|
|
330
392
|
})
|
|
331
|
-
return res
|
|
393
|
+
return _requested(res, query)
|
|
332
394
|
},
|
|
333
395
|
ownDrafts: async function (run, query) {
|
|
334
396
|
LOG.debug('List Editing Status: Own Draft')
|
|
@@ -356,11 +418,11 @@ const Read = {
|
|
|
356
418
|
const drafts = await run(draftsQuery)
|
|
357
419
|
Read.merge(query._target, drafts, [], row =>
|
|
358
420
|
Object.assign(row, {
|
|
359
|
-
|
|
360
|
-
|
|
421
|
+
HasDraftEntity: false,
|
|
422
|
+
IsActiveEntity: false
|
|
361
423
|
})
|
|
362
424
|
)
|
|
363
|
-
return drafts
|
|
425
|
+
return _requested(drafts, query)
|
|
364
426
|
},
|
|
365
427
|
all: async function (run, query) {
|
|
366
428
|
LOG.debug('List Editing Status: All')
|
|
@@ -391,6 +453,13 @@ const Read = {
|
|
|
391
453
|
else ownNewDrafts.push(draft)
|
|
392
454
|
}
|
|
393
455
|
|
|
456
|
+
// We can't properly calculate `count`:
|
|
457
|
+
// - Not all actives are retrieved (e.g. top = 0), hence there could be more deletes if more actives are requested,
|
|
458
|
+
// hence we cannot count deletions based on data.
|
|
459
|
+
// - We can't rely on the fact that `HasActiveEntity` always has an active counterpart because the filter
|
|
460
|
+
// is applied on draft and active data respectively (you could fetch a draft but not an active instance).
|
|
461
|
+
// However, there's not much we can do, so we use use this as a best guess.
|
|
462
|
+
|
|
394
463
|
const count = isFirstPage ? ownNewDrafts.length + (isCount ? actives[0]?.$count : actives.$count) : actives.$count
|
|
395
464
|
if (isCount) return { $count: count }
|
|
396
465
|
|
|
@@ -423,7 +492,7 @@ const Read = {
|
|
|
423
492
|
})
|
|
424
493
|
const res = isFirstPage ? [...ownNewDrafts, ...ownEditDrafts, ...actives] : actives
|
|
425
494
|
if (query.SELECT.count) res.$count = count
|
|
426
|
-
return res
|
|
495
|
+
return _requested(res, query)
|
|
427
496
|
},
|
|
428
497
|
activesFromDrafts: async function (run, query, { isLocked = true }) {
|
|
429
498
|
const draftsQuery = query._drafts
|
|
@@ -450,7 +519,7 @@ const Read = {
|
|
|
450
519
|
? Object.assign(row, other, { IsActiveEntity: true, HasDraftEntity: true, HasActiveEntity: false })
|
|
451
520
|
: Object.assign({ IsActiveEntity: true, HasDraftEntity: false, HasActiveEntity: false })
|
|
452
521
|
)
|
|
453
|
-
return actives
|
|
522
|
+
return _requested(actives, query)
|
|
454
523
|
},
|
|
455
524
|
unsavedChangesByAnotherUser: async function (run, query) {
|
|
456
525
|
LOG.debug('List Editing Status: Unsaved Changes by Another User')
|
|
@@ -464,6 +533,7 @@ const Read = {
|
|
|
464
533
|
whereIn: (target, data, not = false) => {
|
|
465
534
|
const keys = Object_keys(target.keys).filter(k => k !== 'IsActiveEntity')
|
|
466
535
|
const dataArray = data ? (Array.isArray(data) ? data : [data]) : []
|
|
536
|
+
if (not && !dataArray.length) return []
|
|
467
537
|
return [
|
|
468
538
|
{ list: keys.map(k => ({ ref: [k] })) },
|
|
469
539
|
not ? 'not in' : 'in',
|
|
@@ -529,29 +599,33 @@ const Read = {
|
|
|
529
599
|
}
|
|
530
600
|
}
|
|
531
601
|
|
|
532
|
-
function _cleanseParams(params) {
|
|
602
|
+
function _cleanseParams(params, target) {
|
|
603
|
+
if (!target?.drafts) return
|
|
533
604
|
if (Array.isArray(params)) {
|
|
534
|
-
for (const param of params) _cleanseParams(param)
|
|
605
|
+
for (const param of params) _cleanseParams(param, target)
|
|
535
606
|
return
|
|
536
607
|
}
|
|
537
608
|
if (typeof params === 'object') {
|
|
538
609
|
for (const key in params) {
|
|
539
|
-
if (key === 'IsActiveEntity')
|
|
610
|
+
if (key === 'IsActiveEntity') {
|
|
611
|
+
const value = params[key]
|
|
612
|
+
delete params[key]
|
|
613
|
+
if (cds.env.fiori?.draft_compat) Object.defineProperty(params, key, { value, enumerable: false })
|
|
614
|
+
}
|
|
540
615
|
}
|
|
541
616
|
}
|
|
542
617
|
}
|
|
543
618
|
|
|
544
|
-
function _cleanseCols(columns, elements) {
|
|
545
|
-
|
|
546
|
-
return
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
)
|
|
619
|
+
function _cleanseCols(columns, elements, target) {
|
|
620
|
+
// TODO: sometimes target is undefined
|
|
621
|
+
if (!target || typeof columns?.filter !== 'function') return columns
|
|
622
|
+
const filtered = target?.drafts ? columns.filter(c => !elements.has(c.ref?.[0])) : columns
|
|
623
|
+
return filtered.map(c => {
|
|
624
|
+
if (c.expand && c.ref) {
|
|
625
|
+
return { ...c, expand: _cleanseCols(c.expand, elements, target.elements[c.ref[0]]?._target) }
|
|
626
|
+
}
|
|
627
|
+
return c
|
|
628
|
+
})
|
|
555
629
|
}
|
|
556
630
|
|
|
557
631
|
/**
|
|
@@ -559,10 +633,10 @@ function _cleanseCols(columns, elements) {
|
|
|
559
633
|
*/
|
|
560
634
|
function _cleansed(query, model) {
|
|
561
635
|
const draftParams = {} //> used to collect draft filter criteria
|
|
562
|
-
const q = _cleanseQuery(query, draftParams)
|
|
636
|
+
const q = _cleanseQuery(query, draftParams, model)
|
|
563
637
|
if (query.SELECT) {
|
|
564
638
|
const getDrafts = () => {
|
|
565
|
-
const draftsQuery = _cleanseQuery(query, {}) // could just clone `q` but the latter is ruined by database layer
|
|
639
|
+
const draftsQuery = _cleanseQuery(query, {}, model) // could just clone `q` but the latter is ruined by database layer
|
|
566
640
|
draftsQuery._target = undefined
|
|
567
641
|
const [root, ...tail] = draftsQuery.SELECT.from.ref
|
|
568
642
|
const draft = model.definitions[root.id || root].drafts
|
|
@@ -572,7 +646,7 @@ function _cleansed(query, model) {
|
|
|
572
646
|
cds.infer(draftsQuery, model.definitions)
|
|
573
647
|
// draftsQuery._target = draftsQuery._target?.drafts || draftsQuery._target
|
|
574
648
|
if (query.SELECT.columns && query._target.drafts)
|
|
575
|
-
draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS)
|
|
649
|
+
draftsQuery.SELECT.columns = _cleanseCols(query.SELECT.columns, REDUCED_DRAFT_ELEMENTS, draft)
|
|
576
650
|
|
|
577
651
|
if (draftsQuery._target.name.endsWith('.DraftAdministrativeData')) {
|
|
578
652
|
draftsQuery.SELECT.columns = _tweakAdminCols(draftsQuery.SELECT.columns)
|
|
@@ -592,21 +666,30 @@ function _cleansed(query, model) {
|
|
|
592
666
|
}
|
|
593
667
|
|
|
594
668
|
Object.defineProperty(q, '_draftParams', { value: draftParams, enumerable: false })
|
|
669
|
+
q[original] = query
|
|
595
670
|
return q
|
|
596
671
|
|
|
597
|
-
function _cleanseQuery(query, draftParams) {
|
|
672
|
+
function _cleanseQuery(query, draftParams, model) {
|
|
673
|
+
const target = query._target
|
|
598
674
|
const q = cds.ql.clone(query)
|
|
599
675
|
|
|
600
676
|
const ref = q.SELECT?.from.ref || q.UPDATE?.entity.ref || q.INSERT?.into.ref || q.DELETE?.from.ref
|
|
601
677
|
const cqn = q.SELECT || q.UPDATE || q.INSERT || q.DELETE
|
|
602
678
|
|
|
603
679
|
if (ref) {
|
|
604
|
-
|
|
680
|
+
let entity
|
|
681
|
+
const cleansedRef = ref.map(r => {
|
|
682
|
+
entity = (entity && entity.elements[r.id || r]._target) || model.definitions[r.id || r]
|
|
683
|
+
if (!entity?.drafts) return r
|
|
684
|
+
return r.where ? { ...r, where: _cleanseWhere(r.where, draftParams) } : r
|
|
685
|
+
})
|
|
605
686
|
if (q.SELECT) q.SELECT.from = { ...q.SELECT.from, ref: cleansedRef }
|
|
606
687
|
else if (q.DELETE) q.DELETE.from = { ...q.DELETE.from, ref: cleansedRef }
|
|
607
688
|
else if (q.UPDATE) q.UPDATE.entity = { ...q.UPDATE.entity, ref: cleansedRef }
|
|
608
689
|
else if (q.INSERT) q.INSERT.into = { ...q.INSERT.into, ref: cleansedRef }
|
|
609
690
|
|
|
691
|
+
// This only works for simple cases of `SiblingEntity`, e.g. `root(ID=1,IsActiveEntity=false)/SiblingEntity`
|
|
692
|
+
// , check if there are more complicated use cases
|
|
610
693
|
const siblingIdx = cleansedRef.findIndex(r => r === 'SiblingEntity')
|
|
611
694
|
if (siblingIdx !== -1) {
|
|
612
695
|
cleansedRef.splice(siblingIdx, 1)
|
|
@@ -614,10 +697,9 @@ function _cleansed(query, model) {
|
|
|
614
697
|
}
|
|
615
698
|
}
|
|
616
699
|
|
|
617
|
-
if (cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams)
|
|
618
|
-
if (cqn.
|
|
619
|
-
if (cqn.
|
|
620
|
-
|
|
700
|
+
if (target.drafts && cqn.where) cqn.where = _cleanseWhere(cqn.where, draftParams)
|
|
701
|
+
if (target.drafts && cqn.orderBy) cqn.orderBy = _cleanseWhere(cqn.orderBy, {})
|
|
702
|
+
if (cqn.columns) cqn.columns = _cleanseCols(cqn.columns, DRAFT_ELEMENTS, target)
|
|
621
703
|
return q
|
|
622
704
|
}
|
|
623
705
|
|
|
@@ -648,9 +730,9 @@ function _cleansed(query, model) {
|
|
|
648
730
|
'=',
|
|
649
731
|
{ val: cds.context.user.id },
|
|
650
732
|
'then',
|
|
651
|
-
|
|
733
|
+
'true',
|
|
652
734
|
'else',
|
|
653
|
-
|
|
735
|
+
'false',
|
|
654
736
|
'end'
|
|
655
737
|
],
|
|
656
738
|
as: 'DraftIsCreatedByMe',
|
|
@@ -671,9 +753,9 @@ function _cleansed(query, model) {
|
|
|
671
753
|
'>',
|
|
672
754
|
{ val: _lock.shiftedNow },
|
|
673
755
|
'then',
|
|
674
|
-
|
|
756
|
+
'true',
|
|
675
757
|
'else',
|
|
676
|
-
|
|
758
|
+
'false',
|
|
677
759
|
'end'
|
|
678
760
|
],
|
|
679
761
|
as: 'DraftIsProcessedByMe',
|
|
@@ -740,7 +822,10 @@ function expandStarStar(target, recursion = new Map()) {
|
|
|
740
822
|
|
|
741
823
|
async function onNew(req) {
|
|
742
824
|
LOG.debug('new draft')
|
|
743
|
-
const isRoot = typeof req.query.INSERT.into === 'string'
|
|
825
|
+
const isRoot = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
|
|
826
|
+
// Only allowed for pseudo draft roots (entities with this action)
|
|
827
|
+
if (isRoot && !req.target.actives['@Common.DraftRoot.ActivationAction'])
|
|
828
|
+
req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
|
|
744
829
|
let DraftUUID
|
|
745
830
|
if (isRoot) DraftUUID = cds.utils.uuid()
|
|
746
831
|
else {
|
|
@@ -777,15 +862,29 @@ async function onNew(req) {
|
|
|
777
862
|
})
|
|
778
863
|
.where({ DraftUUID })
|
|
779
864
|
|
|
780
|
-
const
|
|
781
|
-
{ DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false },
|
|
782
|
-
|
|
783
|
-
|
|
865
|
+
const _assignDraftData = (obj, target) => {
|
|
866
|
+
const newObj = Object.assign({ DraftAdministrativeData_DraftUUID: DraftUUID, HasActiveEntity: false }, obj)
|
|
867
|
+
if (!target) return newObj
|
|
868
|
+
|
|
869
|
+
// Also support deep insertions
|
|
870
|
+
for (const key in newObj) {
|
|
871
|
+
if (!target.elements[key]?.isComposition) continue
|
|
872
|
+
if (Array.isArray(newObj[key]))
|
|
873
|
+
newObj[key] = newObj[key].map(v => _assignDraftData(v, target.elements[key]._target))
|
|
874
|
+
else if (typeof newObj[key] === 'object') {
|
|
875
|
+
newObj[key] = _assignDraftData(newObj[key], target.elements[key]._target)
|
|
876
|
+
}
|
|
877
|
+
}
|
|
878
|
+
|
|
879
|
+
return newObj
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const draftData = _assignDraftData(req.query.INSERT.entries[0], req.target)
|
|
784
883
|
|
|
785
884
|
delete draftData.IsActiveEntity
|
|
786
885
|
const draftCQN = INSERT.into(req.target).entries(draftData)
|
|
787
886
|
|
|
788
|
-
await
|
|
887
|
+
await _promiseAll([cds.run(adminDataCQN), this.run(draftCQN)])
|
|
789
888
|
req._.readAfterWrite = true
|
|
790
889
|
return { ...draftData, IsActiveEntity: false }
|
|
791
890
|
}
|
|
@@ -794,7 +893,7 @@ async function onEdit(req) {
|
|
|
794
893
|
LOG.debug('edit active')
|
|
795
894
|
const draftParams = req.query._draftParams
|
|
796
895
|
if (req.query.SELECT.from.ref.length > 1 || draftParams.IsActiveEntity !== true) {
|
|
797
|
-
req.reject(400, 'Action "draftEdit" can only be called on the root entity')
|
|
896
|
+
req.reject(400, 'Action "draftEdit" can only be called on the root active entity')
|
|
798
897
|
}
|
|
799
898
|
const targetWhere = req.query.SELECT.from.ref[0].where
|
|
800
899
|
|
|
@@ -816,18 +915,29 @@ async function onEdit(req) {
|
|
|
816
915
|
}
|
|
817
916
|
}
|
|
818
917
|
_addDraftColumns(req.target, cols)
|
|
819
|
-
|
|
918
|
+
|
|
919
|
+
const existingDraft = SELECT.one(req.target.drafts)
|
|
820
920
|
.columns({ ref: ['DraftAdministrativeData'], expand: [_inProcessByUserXpr(_lock.shiftedNow)] })
|
|
821
921
|
.where(targetWhere)
|
|
822
|
-
.forUpdate({ wait: 0 })
|
|
823
922
|
// prevent service to check for own user
|
|
824
|
-
Object.defineProperty(
|
|
923
|
+
Object.defineProperty(existingDraft, '_draftParams', { value: draftParams, enumerable: false })
|
|
924
|
+
|
|
925
|
+
const activeCQN = SELECT.one.from(req.target).columns(cols).where(targetWhere)
|
|
926
|
+
activeCQN._suppressLocalization = true // in the future we should be able to just set activeCQN.SELECT.localized = false
|
|
825
927
|
|
|
826
|
-
const
|
|
827
|
-
|
|
928
|
+
const activeCheck = SELECT.one(req.target).columns([1]).where(targetWhere).forUpdate()
|
|
929
|
+
Object.defineProperty(activeCheck, '_draftParams', { value: draftParams, enumerable: false })
|
|
930
|
+
// It's not possible to use `FOR UPDATE` in HANA if the view contains joins/unions. Unfortunately, we can't resolve the table entity
|
|
931
|
+
// because we must trigger the app-service request on the target entity (which could be delegated to a remote service).
|
|
932
|
+
// The best we can do is to catch a potential error
|
|
933
|
+
await this.run(activeCheck).catch(_ => {})
|
|
934
|
+
|
|
935
|
+
const [res, draft] = await _promiseAll([
|
|
936
|
+
this.run(activeCQN),
|
|
828
937
|
// no user check must be done here...
|
|
829
|
-
this.run(
|
|
938
|
+
this.run(existingDraft)
|
|
830
939
|
])
|
|
940
|
+
|
|
831
941
|
if (!res) req.reject(404)
|
|
832
942
|
const preserveChanges = req.context?.data?.PreserveChanges
|
|
833
943
|
const inProcessByUser = draft?.DraftAdministrativeData?.InProcessByUser
|
|
@@ -835,7 +945,7 @@ async function onEdit(req) {
|
|
|
835
945
|
if (inProcessByUser || preserveChanges) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
836
946
|
const keys = {}
|
|
837
947
|
for (const key in req.target.drafts.keys) keys[key] = res[key]
|
|
838
|
-
await
|
|
948
|
+
await _promiseAll([
|
|
839
949
|
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID }),
|
|
840
950
|
this.run(DELETE.from(req.target.drafts).where(keys))
|
|
841
951
|
])
|
|
@@ -862,7 +972,7 @@ async function onEdit(req) {
|
|
|
862
972
|
|
|
863
973
|
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
864
974
|
// status code must be set in handler to allow overriding for FE V2
|
|
865
|
-
req?._?.odataRes
|
|
975
|
+
req?._?.odataRes?.setStatusCode(201)
|
|
866
976
|
|
|
867
977
|
return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
|
|
868
978
|
}
|
|
@@ -891,7 +1001,7 @@ async function onCancel(req) {
|
|
|
891
1001
|
DELETE.from('DRAFT.DraftAdministrativeData').where({ DraftUUID: draft.DraftAdministrativeData_DraftUUID })
|
|
892
1002
|
)
|
|
893
1003
|
if (draftParams.IsActiveEntity) deletes.push(this.run(DELETE.from({ ref: activeRef })))
|
|
894
|
-
await
|
|
1004
|
+
await _promiseAll(deletes)
|
|
895
1005
|
return req.data
|
|
896
1006
|
}
|
|
897
1007
|
|
|
@@ -225,7 +225,9 @@ function _processExpand(model, dbc, cqn, user, locale, txTimestamp) {
|
|
|
225
225
|
function executeSelectCQN(model, dbc, query, user, locale, txTimestamp) {
|
|
226
226
|
if (hasExpand(query)) {
|
|
227
227
|
// expand: '**' or '*3' is handled by new impl
|
|
228
|
-
if (
|
|
228
|
+
if (
|
|
229
|
+
query.SELECT.columns.some(c => c.expand && typeof c.expand[0] === 'string' && /^\*{1}[\d|*]+/.test(c.expand[0]))
|
|
230
|
+
) {
|
|
229
231
|
return expandV2(model, dbc, query, user, locale, txTimestamp, executeSelectCQN)
|
|
230
232
|
}
|
|
231
233
|
return _processExpand(model, dbc, query, user, locale, txTimestamp)
|