@sap/cds 7.3.1 → 7.4.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 +69 -3
- package/_i18n/i18n_es_MX.properties +110 -0
- package/apis/cds.d.ts +13 -12
- package/apis/core.d.ts +27 -108
- package/apis/cqn.d.ts +15 -18
- package/apis/csn.d.ts +95 -60
- package/apis/env.d.ts +25 -0
- package/apis/events.d.ts +125 -0
- package/apis/{reflect.d.ts → linked.d.ts} +29 -38
- package/apis/models.d.ts +60 -45
- package/apis/ql.d.ts +19 -5
- package/apis/{serve.d.ts → server.d.ts} +59 -33
- package/apis/services.d.ts +76 -147
- package/apis/test.d.ts +1 -1
- package/bin/serve.js +3 -0
- package/lib/compile/cds-compile.js +2 -2
- package/lib/compile/etc/csv.js +2 -1
- package/lib/compile/to/edm.js +8 -3
- package/lib/compile/to/gql.js +4 -0
- package/lib/dbs/cds-deploy.js +52 -4
- package/lib/env/cds-requires.js +27 -15
- package/lib/env/defaults.js +1 -0
- package/lib/env/schemas/index.js +10 -0
- package/lib/index.js +7 -4
- package/lib/linked/models.js +8 -5
- package/lib/ql/CREATE.js +2 -0
- package/lib/ql/DELETE.js +1 -0
- package/lib/ql/DROP.js +2 -0
- package/lib/ql/INSERT.js +2 -22
- package/lib/ql/Query.js +59 -22
- package/lib/ql/SELECT.js +5 -0
- package/lib/ql/STREAM.js +2 -0
- package/lib/ql/UPDATE.js +2 -0
- package/lib/ql/UPSERT.js +3 -1
- package/lib/ql/cds-ql.js +21 -5
- package/lib/ql/infer.js +129 -0
- package/lib/req/cds-context.js +8 -5
- package/lib/srv/cds-connect.js +3 -1
- package/lib/utils/axios.js +4 -2
- package/lib/utils/data.js +3 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +12 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +27 -9
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +8 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +11 -8
- package/libx/_runtime/common/code-ext/worker.js +5 -16
- package/libx/_runtime/common/generic/auth/capabilities.js +11 -2
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/common/utils/postProcessing.js +1 -1
- package/libx/_runtime/common/utils/resolveView.js +28 -9
- package/libx/{common → _runtime/common}/utils/ucsn.js +19 -11
- package/libx/_runtime/db/expand/expandCQNToJoin.js +6 -6
- package/libx/_runtime/db/expand/rawToExpanded.js +4 -4
- package/libx/_runtime/db/sql-builder/InsertBuilder.js +6 -1
- package/libx/_runtime/db/sql-builder/UpdateBuilder.js +6 -1
- package/libx/_runtime/db/sql-builder/dollar.js +7 -7
- package/libx/_runtime/fiori/generic/activate.js +2 -2
- package/libx/_runtime/fiori/generic/edit.js +25 -45
- package/libx/_runtime/fiori/generic/read.js +3 -5
- package/libx/_runtime/fiori/lean-draft.js +171 -84
- package/libx/_runtime/fiori/utils/delete.js +7 -1
- package/libx/_runtime/fiori/utils/handler.js +4 -6
- package/libx/_runtime/fiori/utils/lockInfo.js +27 -0
- package/libx/_runtime/fiori/utils/where.js +20 -1
- package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -2
- package/libx/_runtime/messaging/Outbox.js +12 -47
- package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -3
- package/libx/_runtime/messaging/common-utils/authorizedRequest.js +3 -0
- package/libx/_runtime/messaging/common-utils/connections.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging.js +12 -13
- package/libx/_runtime/messaging/file-based.js +7 -5
- package/libx/_runtime/messaging/redis-messaging.js +10 -11
- package/libx/_runtime/messaging/service.js +12 -26
- package/libx/_runtime/remote/Service.js +52 -36
- package/libx/_runtime/remote/utils/client.js +24 -125
- package/libx/odata/afterburner.js +16 -6
- package/libx/odata/grammar.peggy +26 -7
- package/libx/odata/metadata.js +18 -1
- package/libx/odata/parser.js +1 -1
- package/libx/odata/service-document.js +0 -1
- package/libx/odata/utils.js +19 -3
- package/libx/{_runtime/messaging/outbox/utils.js → outbox/index.js} +94 -24
- package/libx/rest/middleware/parse.js +1 -1
- package/package.json +2 -2
- package/apis/connect.d.ts +0 -39
- package/bin/utils/modules.js +0 -7
- package/bin/utils/term.js +0 -56
- package/lib/env/schema.js +0 -9
- package/lib/linked/queries.js +0 -41
- package/lib/srv/protocols/odata-v2-proxy.js +0 -3699
- package/libx/common/asserts.js +0 -0
- package/libx/common/crud.js +0 -0
- package/libx/common/etag.js +0 -0
- package/libx/common/localized.js +0 -0
- package/libx/common/managed.js +0 -0
- package/libx/common/paging.js +0 -0
- package/libx/common/readme.md +0 -4
- package/libx/common/sorting.js +0 -0
- package/libx/common/temporal.js +0 -0
- package/libx/connect/auth.js +0 -0
- package/libx/connect/perf.js +0 -0
- package/libx/connect/readme.md +0 -3
- package/libx/fiori/draft/readme.md +0 -1
- package/libx/fiori/readme.md +0 -1
- package/libx/hana/readme.md +0 -1
- package/libx/msg/readme.md +0 -3
- package/libx/readme.md +0 -1
- package/libx/sqlite/readme.md +0 -1
- /package/libx/_runtime/{messaging/common-utils → common/utils}/waitingTime.js +0 -0
- /package/libx/{_runtime/messaging/outbox → outbox}/OutboxRunner.js +0 -0
|
@@ -3,15 +3,13 @@ const {
|
|
|
3
3
|
} = require('../okra/odata-server')
|
|
4
4
|
|
|
5
5
|
const { findCsnTargetFor } = require('../../../../common/utils/csn')
|
|
6
|
-
const { convertStructured } = require('
|
|
6
|
+
const { convertStructured } = require('../../../../common/utils/ucsn')
|
|
7
7
|
const { deepCopy } = require('../../../../common/utils/copy')
|
|
8
8
|
const { getSegmentKeyValue } = require('../odata-to-cqn/utils')
|
|
9
|
+
const { MULTIPLE_ERRORS } = require('../../../../common/error/constants')
|
|
9
10
|
|
|
10
|
-
const _isFunctionInvocation = odataReq =>
|
|
11
|
-
|
|
12
|
-
odataReq.getUriInfo().getLastSegment().getFunction() || odataReq.getUriInfo().getLastSegment().getFunctionImport()
|
|
13
|
-
)
|
|
14
|
-
}
|
|
11
|
+
const _isFunctionInvocation = odataReq =>
|
|
12
|
+
odataReq.getUriInfo().getLastSegment().getFunction() || odataReq.getUriInfo().getLastSegment().getFunctionImport()
|
|
15
13
|
|
|
16
14
|
const _addStructuredProperties = ([structName, property, ...nestedProperties], paramData, value) => {
|
|
17
15
|
paramData[structName] = paramData[structName] || {}
|
|
@@ -168,16 +166,21 @@ const _getFunctionParameters = (lastSegment, keyValues, service, target) => {
|
|
|
168
166
|
for (const key in keyValues) {
|
|
169
167
|
paramValues[key] = keyValues[key]
|
|
170
168
|
}
|
|
169
|
+
const errors = []
|
|
171
170
|
if (lastSegment.getKind() === 'BOUND.FUNCTION') {
|
|
172
171
|
const targetFunction = target && target.actions && target.actions[lastSegment.getFunction().getName()]
|
|
173
172
|
if (!targetFunction.params) return {}
|
|
174
|
-
convertStructured(service, targetFunction, paramValues)
|
|
173
|
+
convertStructured(service, targetFunction, paramValues, { errors })
|
|
175
174
|
} else if (lastSegment.getKind() === 'FUNCTION.IMPORT') {
|
|
176
175
|
const { namespace, name } = lastSegment.getFunctionImport().getFullQualifiedName()
|
|
177
176
|
const targetFunction = service.model && service.model.definitions[`${namespace}.${name}`]
|
|
178
177
|
if (!targetFunction.params) return {}
|
|
179
|
-
convertStructured(service, targetFunction, paramValues)
|
|
178
|
+
convertStructured(service, targetFunction, paramValues, { errors })
|
|
180
179
|
}
|
|
180
|
+
|
|
181
|
+
if (errors.length > 1) throw Object.assign(new Error(MULTIPLE_ERRORS), { details: errors })
|
|
182
|
+
if (errors.length === 1) throw errors[0]
|
|
183
|
+
|
|
181
184
|
return paramValues
|
|
182
185
|
}
|
|
183
186
|
|
|
@@ -1,11 +1,6 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
const LOG = cds.log()
|
|
3
3
|
const { parentPort, workerData } = require('worker_threads')
|
|
4
|
-
const SELECT = require('../../../../lib/ql/SELECT')
|
|
5
|
-
const INSERT = require('../../../../lib/ql/INSERT')
|
|
6
|
-
const UPSERT = require('../../../../lib/ql/UPSERT')
|
|
7
|
-
const UPDATE = require('../../../../lib/ql/UPDATE')
|
|
8
|
-
const DELETE = require('../../../../lib/ql/DELETE')
|
|
9
4
|
const queryExecutor = require('./workerQueryExecutor')
|
|
10
5
|
const WorkerReq = require('./WorkerReq')
|
|
11
6
|
const { timeout } = require('./config')
|
|
@@ -19,42 +14,36 @@ parentPort.on('message', function onWorkerMessageReceived(message) {
|
|
|
19
14
|
const { VM } = require('vm2')
|
|
20
15
|
const workerReq = new WorkerReq(contextId, reqData)
|
|
21
16
|
|
|
22
|
-
class WorkerSELECT extends SELECT {
|
|
17
|
+
class WorkerSELECT extends SELECT.class {
|
|
23
18
|
then(r, e) {
|
|
24
19
|
return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
|
|
25
20
|
}
|
|
26
21
|
}
|
|
27
22
|
|
|
28
|
-
class WorkerINSERT extends INSERT {
|
|
23
|
+
class WorkerINSERT extends INSERT.class {
|
|
29
24
|
then(r, e) {
|
|
30
25
|
return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
|
|
31
26
|
}
|
|
32
27
|
}
|
|
33
28
|
|
|
34
|
-
class WorkerUPSERT extends UPSERT {
|
|
29
|
+
class WorkerUPSERT extends UPSERT.class {
|
|
35
30
|
then(r, e) {
|
|
36
31
|
return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
|
|
37
32
|
}
|
|
38
33
|
}
|
|
39
34
|
|
|
40
|
-
class WorkerUPDATE extends UPDATE {
|
|
35
|
+
class WorkerUPDATE extends UPDATE.class {
|
|
41
36
|
then(r, e) {
|
|
42
37
|
return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
|
|
43
38
|
}
|
|
44
39
|
}
|
|
45
40
|
|
|
46
|
-
class WorkerDELETE extends DELETE {
|
|
41
|
+
class WorkerDELETE extends DELETE.class {
|
|
47
42
|
then(r, e) {
|
|
48
43
|
return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
|
|
49
44
|
}
|
|
50
45
|
}
|
|
51
46
|
|
|
52
|
-
Object.defineProperty(WorkerSELECT.prototype, 'cmd', { value: 'SELECT' })
|
|
53
|
-
Object.defineProperty(WorkerINSERT.prototype, 'cmd', { value: 'INSERT' })
|
|
54
|
-
Object.defineProperty(WorkerUPSERT.prototype, 'cmd', { value: 'UPSERT' })
|
|
55
|
-
Object.defineProperty(WorkerUPDATE.prototype, 'cmd', { value: 'UPDATE' })
|
|
56
|
-
Object.defineProperty(WorkerDELETE.prototype, 'cmd', { value: 'DELETE' })
|
|
57
|
-
|
|
58
47
|
const vm = new VM({
|
|
59
48
|
console: 'inherit',
|
|
60
49
|
timeout, // specifies the number of milliseconds to execute code before terminating execution
|
|
@@ -15,15 +15,24 @@ const _getNavigationRestriction = (target, path, annotation, req) => {
|
|
|
15
15
|
const [restriction, operation] = annotation.split('.')
|
|
16
16
|
for (const r of target['@Capabilities.NavigationRestrictions.RestrictedProperties']) {
|
|
17
17
|
// prefix check to support both notations: { InsertRestrictions: { Insertable: false } } and { InsertRestrictions.Insertable: false }
|
|
18
|
+
// however, { InsertRestrictions.Insertable: false } is actually not supported bc compiler does not expand shorthands inside an annotation
|
|
18
19
|
if (r.NavigationProperty['='] === path && Object.keys(r).some(k => k.startsWith(restriction))) {
|
|
19
20
|
const capability = r[annotation] ?? r[restriction]?.[operation]
|
|
20
|
-
|
|
21
|
+
const capabilityReadByKey =
|
|
22
|
+
r.ReadRestrictions?.['ReadByKeyRestrictions.Readable'] ?? r.ReadRestrictions?.ReadByKeyRestrictions?.Readable
|
|
23
|
+
return _getRestriction(req, capability, capabilityReadByKey)
|
|
21
24
|
}
|
|
22
25
|
}
|
|
23
26
|
}
|
|
24
27
|
|
|
25
28
|
const _localName = entity => entity.name.replace(entity._service.name + '.', '')
|
|
26
29
|
|
|
30
|
+
const _getNav = from => {
|
|
31
|
+
if (from?.SELECT) return _getNav(from.SELECT.from)
|
|
32
|
+
if (from?.ref) return from.ref.map(el => el.id || el)
|
|
33
|
+
return []
|
|
34
|
+
}
|
|
35
|
+
|
|
27
36
|
function handler(req) {
|
|
28
37
|
// TODO: Determine auth-relevant entity
|
|
29
38
|
const annotation = RESTRICTIONS[req.event]
|
|
@@ -32,7 +41,7 @@ function handler(req) {
|
|
|
32
41
|
|
|
33
42
|
const action = annotation.split('.').pop().toUpperCase()
|
|
34
43
|
const from = cqnFrom(req)
|
|
35
|
-
const nav = (from
|
|
44
|
+
const nav = _getNav(from)
|
|
36
45
|
|
|
37
46
|
let navRestriction
|
|
38
47
|
if (nav.length > 1) {
|
|
@@ -86,6 +86,7 @@ DRAFT_ALREADY_EXISTS=A draft for this entity already exists
|
|
|
86
86
|
DRAFT_NOT_EXISTING=No draft for this entity exists
|
|
87
87
|
DRAFT_LOCKED_BY_ANOTHER_USER=The entity is locked by user "{0}"
|
|
88
88
|
DRAFT_MODIFICATION_ONLY_VIA_ROOT=A draft can only be modified via its root entity
|
|
89
|
+
DRAFT_ACTIVE_DELETE_FORBIDDEN_DRAFT_EXISTS=Entity cannot be deleted because a draft exists
|
|
89
90
|
|
|
90
91
|
# singleton
|
|
91
92
|
SINGLETON_NOT_NULLABLE=The singleton entity is not nullable
|
|
@@ -77,7 +77,7 @@ const postProcess = (query, result, service, onlySelectAliases = false) => {
|
|
|
77
77
|
if (!onlySelectAliases) {
|
|
78
78
|
const transition =
|
|
79
79
|
query.SELECT && query.SELECT._transitions && query.SELECT._transitions[query.SELECT._transitions.length - 1]
|
|
80
|
-
if (transition) return revertData(result, transition, service)
|
|
80
|
+
if (transition && result) return revertData(result, transition, service)
|
|
81
81
|
}
|
|
82
82
|
|
|
83
83
|
return result
|
|
@@ -239,7 +239,22 @@ const _newWhereRef = (newWhereElement, transition, alias, tableName, isSubSelect
|
|
|
239
239
|
|
|
240
240
|
if (newRef[0] === alias) {
|
|
241
241
|
const mapped = transition.mapping.get(newRef[1])
|
|
242
|
-
if (mapped)
|
|
242
|
+
if (mapped) {
|
|
243
|
+
const tableAlias =
|
|
244
|
+
transition.queryTarget.query?.SELECT?.from.as ??
|
|
245
|
+
transition.queryTarget.query?.SELECT?.from.ref.at(-1).split('.').pop()
|
|
246
|
+
const newMapped = []
|
|
247
|
+
|
|
248
|
+
if (tableAlias && mapped.ref[0] === tableAlias) {
|
|
249
|
+
// remove table alias from mapped array
|
|
250
|
+
newMapped.push(...mapped.ref.slice(1))
|
|
251
|
+
} else {
|
|
252
|
+
newMapped.push(...mapped.ref)
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// we assume it's a foreign key or single element
|
|
256
|
+
newRef[1] = newMapped.join('_')
|
|
257
|
+
}
|
|
243
258
|
} else if (newRef[0] === tableName) {
|
|
244
259
|
newRef[0] = transition.target.name
|
|
245
260
|
const mapped = transition.mapping.get(newRef[1])
|
|
@@ -267,6 +282,10 @@ const _newWhere = (where = [], transition, tableName, alias, isSubselect = false
|
|
|
267
282
|
return { xpr: _newWhere(whereElement.xpr, transition, tableName, alias, isSubselect) }
|
|
268
283
|
}
|
|
269
284
|
|
|
285
|
+
if (whereElement.list) {
|
|
286
|
+
return { list: _newWhere(whereElement.list, transition, tableName, alias, isSubselect) }
|
|
287
|
+
}
|
|
288
|
+
|
|
270
289
|
const newWhereElement = { ...whereElement }
|
|
271
290
|
if (!whereElement.ref && !whereElement.SELECT && !whereElement.func) return whereElement
|
|
272
291
|
|
|
@@ -763,14 +782,14 @@ const findQueryTarget = q => {
|
|
|
763
782
|
return q.SELECT && q.SELECT._transitions
|
|
764
783
|
? q.SELECT._transitions[q.SELECT._transitions.length - 1].target
|
|
765
784
|
: q.INSERT
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
785
|
+
? q.INSERT._transitions[q.INSERT._transitions.length - 1].target
|
|
786
|
+
: q.UPDATE
|
|
787
|
+
? q.UPDATE._transitions[q.UPDATE._transitions.length - 1].target
|
|
788
|
+
: q.UPSERT
|
|
789
|
+
? q.UPSERT._transitions[q.UPSERT._transitions.length - 1].target
|
|
790
|
+
: q.DELETE
|
|
791
|
+
? q.DELETE._transitions[q.DELETE._transitions.length - 1].target
|
|
792
|
+
: undefined
|
|
774
793
|
}
|
|
775
794
|
|
|
776
795
|
module.exports = {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
const cds = require('../../
|
|
2
|
-
const
|
|
3
|
-
const
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
const getError = require('../error')
|
|
3
|
+
const getTemplate = require('./template')
|
|
4
|
+
const templateProcessor = require('./templateProcessor')
|
|
4
5
|
const IS_PROXY = Symbol('flat2structProxy')
|
|
5
6
|
|
|
6
7
|
const proxifyIfFlattened = (definition, payload) => {
|
|
@@ -61,7 +62,9 @@ const _processor = ({ row, key, plain: { category }, element }) => {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
|
|
64
|
-
|
|
65
|
+
// REVISIT: check function complexity
|
|
66
|
+
// eslint-disable-next-line complexity
|
|
67
|
+
const _cleanup = (row, definition, cleanupNull, cleanupStruct, errors, prefix = []) => {
|
|
65
68
|
if (!row || !definition) return
|
|
66
69
|
const elements = definition.elements || definition.params
|
|
67
70
|
for (const key of Object.keys(row)) {
|
|
@@ -69,8 +72,11 @@ const _cleanup = (row, definition, cleanupNull, cleanupStruct, prefix = []) => {
|
|
|
69
72
|
const element = elements[key] || (cleanupStruct && elements[`${prefix.join('_')}_${key}`])
|
|
70
73
|
if (!element) {
|
|
71
74
|
if (cleanupStruct && typeof row[key] === 'object' && !Array.isArray(row[key])) {
|
|
72
|
-
_cleanup(
|
|
75
|
+
_cleanup(row[key], definition, cleanupNull, cleanupStruct, errors, [...prefix, key])
|
|
73
76
|
} else {
|
|
77
|
+
if (errors) {
|
|
78
|
+
errors.push(getError(400, 'Property ' + key + ' does not exist in ' + definition.name))
|
|
79
|
+
}
|
|
74
80
|
delete row[key]
|
|
75
81
|
}
|
|
76
82
|
continue
|
|
@@ -79,20 +85,22 @@ const _cleanup = (row, definition, cleanupNull, cleanupStruct, prefix = []) => {
|
|
|
79
85
|
if (element.isAssociation) {
|
|
80
86
|
if (element.is2many) {
|
|
81
87
|
for (const r of row[key]) {
|
|
82
|
-
_cleanup(r, element._target, cleanupNull, cleanupStruct, [])
|
|
88
|
+
_cleanup(r, element._target, cleanupNull, cleanupStruct, errors, [])
|
|
83
89
|
}
|
|
84
90
|
} else {
|
|
85
|
-
_cleanup(row[key], element._target, cleanupNull, cleanupStruct, [])
|
|
91
|
+
_cleanup(row[key], element._target, cleanupNull, cleanupStruct, errors, [])
|
|
86
92
|
}
|
|
87
93
|
} else if (element.elements) {
|
|
88
|
-
_cleanup(row[key], element, cleanupNull, cleanupStruct, prefix)
|
|
89
|
-
if (!Object.keys(row).length)
|
|
94
|
+
_cleanup(row[key], element, cleanupNull, cleanupStruct, errors, prefix)
|
|
95
|
+
if (!Object.keys(row).length) {
|
|
96
|
+
delete row[key]
|
|
97
|
+
}
|
|
90
98
|
if (cleanupNull && Object.values(row[key]).every(v => v == null)) row[key] = null
|
|
91
99
|
}
|
|
92
100
|
}
|
|
93
101
|
}
|
|
94
102
|
|
|
95
|
-
function convertStructured(service, definition, data, { cleanupNull = false, cleanupStruct = false } = {}) {
|
|
103
|
+
function convertStructured(service, definition, data, { cleanupNull = false, cleanupStruct = false, errors } = {}) {
|
|
96
104
|
if (!definition) return
|
|
97
105
|
// REVISIT check `structs` mode only for now as uCSN is not yet available
|
|
98
106
|
const flatAccess = cds.env.features.compat_flat_access
|
|
@@ -105,7 +113,7 @@ function convertStructured(service, definition, data, { cleanupNull = false, cle
|
|
|
105
113
|
}
|
|
106
114
|
}
|
|
107
115
|
for (const row of arrayData) {
|
|
108
|
-
_cleanup(row, definition, cleanupNull, cleanupStruct)
|
|
116
|
+
_cleanup(row, definition, cleanupNull, cleanupStruct, errors)
|
|
109
117
|
}
|
|
110
118
|
}
|
|
111
119
|
|
|
@@ -10,7 +10,7 @@ const { getCQNUnionFrom } = require('../../common/utils/union')
|
|
|
10
10
|
|
|
11
11
|
const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
12
12
|
|
|
13
|
-
const {
|
|
13
|
+
const { entity_keys } = require('../../fiori/utils/handler')
|
|
14
14
|
|
|
15
15
|
const getError = require('../../common/error')
|
|
16
16
|
|
|
@@ -1282,10 +1282,10 @@ class JoinCQNFromExpanded {
|
|
|
1282
1282
|
element.ref[0] === alias
|
|
1283
1283
|
? [...element.ref]
|
|
1284
1284
|
: element.ref.length === 1
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1285
|
+
? [alias, element.ref[0]]
|
|
1286
|
+
: this._isPathExpressionToOne(element.ref, expandedEntity)
|
|
1287
|
+
? [alias, ...element.ref]
|
|
1288
|
+
: [alias, element.ref[1]]
|
|
1289
1289
|
|
|
1290
1290
|
return (sort && { ref, sort }) || { ref }
|
|
1291
1291
|
})
|
|
@@ -1293,7 +1293,7 @@ class JoinCQNFromExpanded {
|
|
|
1293
1293
|
|
|
1294
1294
|
_getHasDraftEntityXpr(expandedEntity, tableAlias) {
|
|
1295
1295
|
const draftTable = ensureDraftsSuffix(expandedEntity.name)
|
|
1296
|
-
const where =
|
|
1296
|
+
const where = entity_keys(expandedEntity).reduce((res, keyName) => {
|
|
1297
1297
|
if (res.length !== 0) res.push('and')
|
|
1298
1298
|
res.push({ ref: [draftTable, keyName] }, '=', { ref: [tableAlias, keyName] })
|
|
1299
1299
|
return res
|
|
@@ -97,10 +97,10 @@ class RawToExpanded {
|
|
|
97
97
|
? null
|
|
98
98
|
: !!entry[mappings.IsActiveEntity]
|
|
99
99
|
: 'IsActiveEntity' in entry
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
100
|
+
? entry.IsActiveEntity === null
|
|
101
|
+
? null
|
|
102
|
+
: !!entry.IsActiveEntity
|
|
103
|
+
: null
|
|
104
104
|
: null
|
|
105
105
|
|
|
106
106
|
// A raw row contains more elements than the config. Iterating over config is faster.
|
|
@@ -62,7 +62,12 @@ class InsertBuilder extends BaseBuilder {
|
|
|
62
62
|
// replace $ values
|
|
63
63
|
// REVISIT: better
|
|
64
64
|
if (this._obj.INSERT.entries) {
|
|
65
|
-
dollar.entries(
|
|
65
|
+
dollar.entries(
|
|
66
|
+
this._obj.INSERT.entries,
|
|
67
|
+
this._options.user,
|
|
68
|
+
this._options.now,
|
|
69
|
+
this._csn?.definitions?.[this._obj.INSERT.into.ref?.[0] || this._obj.INSERT.into]?.elements
|
|
70
|
+
)
|
|
66
71
|
} else if (this._obj.INSERT.values) {
|
|
67
72
|
dollar.values(this._obj.INSERT.values, this._options.user, this._options.now)
|
|
68
73
|
} else if (this._obj.INSERT.rows) {
|
|
@@ -64,7 +64,12 @@ class UpdateBuilder extends BaseBuilder {
|
|
|
64
64
|
// replace $ values
|
|
65
65
|
// REVISIT: better
|
|
66
66
|
if (this._obj.UPDATE.data) {
|
|
67
|
-
dollar.data(
|
|
67
|
+
dollar.data(
|
|
68
|
+
this._obj.UPDATE.data,
|
|
69
|
+
this._options.user,
|
|
70
|
+
this._options.now,
|
|
71
|
+
this._csn?.definitions?.[this._obj.UPDATE.entity?.ref?.[0] || this._obj.UPDATE.entity]?.elements
|
|
72
|
+
)
|
|
68
73
|
}
|
|
69
74
|
|
|
70
75
|
const entityName = this._entity()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
const replaceManagedData = require('../../common/utils/dollar')
|
|
2
2
|
|
|
3
|
-
const _object = (row, user, now) => {
|
|
3
|
+
const _object = (row, user, now, elements) => {
|
|
4
4
|
Object.keys(row).forEach(k => {
|
|
5
|
-
replaceManagedData(row, k, user, now)
|
|
5
|
+
if (elements?.[k]?.['@cds.on.insert'] || elements?.[k]?.['@cds.on.update']) replaceManagedData(row, k, user, now)
|
|
6
6
|
})
|
|
7
7
|
}
|
|
8
8
|
|
|
@@ -12,9 +12,9 @@ const _array = (row, user, now) => {
|
|
|
12
12
|
}
|
|
13
13
|
}
|
|
14
14
|
|
|
15
|
-
const entries = (_entries, user, now) => {
|
|
16
|
-
if (!Array.isArray(_entries)) _object(_entries, user, now)
|
|
17
|
-
else for (const row of _entries) _object(row, user, now)
|
|
15
|
+
const entries = (_entries, user, now, elements) => {
|
|
16
|
+
if (!Array.isArray(_entries)) _object(_entries, user, now, elements)
|
|
17
|
+
else for (const row of _entries) _object(row, user, now, elements)
|
|
18
18
|
}
|
|
19
19
|
|
|
20
20
|
const values = (_values, user, now) => {
|
|
@@ -25,8 +25,8 @@ const rows = (_rows, user, now) => {
|
|
|
25
25
|
for (const row of _rows) _array(row, user, now)
|
|
26
26
|
}
|
|
27
27
|
|
|
28
|
-
const data = (_data, user, now) => {
|
|
29
|
-
_object(_data, user, now)
|
|
28
|
+
const data = (_data, user, now, elements) => {
|
|
29
|
+
_object(_data, user, now, elements)
|
|
30
30
|
}
|
|
31
31
|
|
|
32
32
|
module.exports = {
|
|
@@ -4,7 +4,7 @@ const { INSERT, SELECT, UPDATE, DELETE } = cds.ql
|
|
|
4
4
|
const {
|
|
5
5
|
ensureNoDraftsSuffix,
|
|
6
6
|
ensureDraftsSuffix,
|
|
7
|
-
|
|
7
|
+
entity_keys,
|
|
8
8
|
getDeleteDraftAdminCqn,
|
|
9
9
|
getCompositionTargets
|
|
10
10
|
} = require('../utils/handler')
|
|
@@ -14,7 +14,7 @@ const { getColumns } = require('../../cds-services/services/utils/columns')
|
|
|
14
14
|
const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
15
15
|
|
|
16
16
|
const _getRootCQN = (context, requestActiveData) => {
|
|
17
|
-
const keys =
|
|
17
|
+
const keys = entity_keys(context.target)
|
|
18
18
|
const keyData = getKeyData(keys, context.query.SELECT.from.ref[0].where)
|
|
19
19
|
const columns = getColumns(context.target, { onlyNames: true, filterVirtual: true })
|
|
20
20
|
return SELECT.from(
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
+
const getLockInfo = require('../utils/lockInfo')
|
|
2
3
|
const { INSERT, SELECT, DELETE } = cds.ql
|
|
3
4
|
|
|
4
5
|
const { getCompositionTree } = require('../../common/composition')
|
|
5
6
|
const { getColumns } = require('../../cds-services/services/utils/columns')
|
|
6
|
-
const {
|
|
7
|
-
const {
|
|
8
|
-
const { isActiveEntityRequested, getKeyData } = require('../utils/where')
|
|
7
|
+
const { draftIsLocked, ensureDraftsSuffix, ensureNoDraftsSuffix, getSubCQNs } = require('../utils/handler')
|
|
8
|
+
const { isActiveEntityRequested } = require('../utils/where')
|
|
9
9
|
|
|
10
10
|
const _getDraftColumns = draftUUID => ({
|
|
11
11
|
IsActiveEntity: false,
|
|
@@ -32,33 +32,20 @@ const _getInsertAdminDataCQN = ({ user }, draftUUID, time) => {
|
|
|
32
32
|
return INSERT.into('DRAFT.DraftAdministrativeData').entries(_getAdminData({ user }, draftUUID, time))
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
-
|
|
36
|
-
if (columnsMap.size === 0) return where
|
|
37
|
-
const whereKeys = Object.keys(where)
|
|
38
|
-
const lockWhere = {}
|
|
39
|
-
|
|
40
|
-
whereKeys.forEach(key => {
|
|
41
|
-
const mappedKey = columnsMap.get(key)
|
|
42
|
-
const lockKey = mappedKey ? mappedKey.ref[0] : key // REVISIT: Why the mapped key is empty?
|
|
43
|
-
lockWhere[lockKey] = where[key]
|
|
44
|
-
})
|
|
45
|
-
|
|
46
|
-
return lockWhere
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
const _select = async (lockRecordCQN, draftExistsCQN, selectCQNs, req, dbtx) => {
|
|
35
|
+
async function _lockAndSelectActive(req, lockRecordCQN, selectCQNs, draftExistsCQN) {
|
|
50
36
|
try {
|
|
51
|
-
await
|
|
37
|
+
await this.run(lockRecordCQN)
|
|
52
38
|
} catch (e) {
|
|
53
|
-
const drafts = await
|
|
39
|
+
const drafts = await this.run(draftExistsCQN)
|
|
54
40
|
if (drafts.length) req.reject(409, 'DRAFT_ALREADY_EXISTS')
|
|
55
41
|
req.reject(409, 'ENTITY_LOCKED')
|
|
56
42
|
}
|
|
57
43
|
|
|
58
|
-
const
|
|
44
|
+
const cqns = [this.run(draftExistsCQN), ...selectCQNs.map(cqn => this.run(cqn))]
|
|
45
|
+
const promisesResults = await Promise.allSettled(cqns)
|
|
59
46
|
const firstRejected = promisesResults.find(r => r.status === 'rejected')
|
|
60
47
|
if (firstRejected) req.reject(firstRejected.reason)
|
|
61
|
-
return promisesResults.map(
|
|
48
|
+
return promisesResults.map(result => result.value)
|
|
62
49
|
}
|
|
63
50
|
|
|
64
51
|
/**
|
|
@@ -76,29 +63,16 @@ const fioriGenericEdit = async function (req, next) {
|
|
|
76
63
|
if (!cds.db) req.reject('NO_DATABASE_CONNECTION')
|
|
77
64
|
|
|
78
65
|
const { definitions } = this.model
|
|
66
|
+
const lockInfo = getLockInfo(req)
|
|
67
|
+
const rootWhere = lockInfo.rootWhere
|
|
79
68
|
|
|
80
|
-
//
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
// cds.db and not "this" as we want to resolve as db here
|
|
89
|
-
const transition = getTransition(req.target, cds.db)
|
|
90
|
-
const lockWhere = _getLockWhere(rootWhere, transition.mapping)
|
|
91
|
-
|
|
92
|
-
// gets the underlying target entity, as record locking can't be
|
|
93
|
-
// applied to localized views
|
|
94
|
-
const lockTargetEntity = transition.target
|
|
95
|
-
|
|
96
|
-
// Lock the root record of the active entity to prevent simultaneous access to it,
|
|
97
|
-
// thus preventing duplicate draft entities from being created or overwritten.
|
|
98
|
-
// Only allows one active entity to be processed at a time, locking out other
|
|
99
|
-
// users who need to edit the same record simultaneously.
|
|
100
|
-
// .forUpdate(): lock the record, a wait of 0 is equivalent to no wait
|
|
101
|
-
const lockRecordCQN = SELECT.from(lockTargetEntity, [1]).where(lockWhere).forUpdate({ wait: 0 })
|
|
69
|
+
// Ensure exclusive access to the root record of the active entity by applying a lock,
|
|
70
|
+
// which effectively prevents the creation or overwriting of duplicate draft entities.
|
|
71
|
+
// This lock mechanism enforces a strict processing order for active entities,
|
|
72
|
+
// allowing only one entity to be worked on at any given time.
|
|
73
|
+
// By using .forUpdate() with a wait value of 0, we immediately lock the record,
|
|
74
|
+
// ensuring there is no waiting time for other users attempting to edit the same record concurrently.
|
|
75
|
+
const activeLockCQN = SELECT.from(lockInfo.target, [1]).where(lockInfo.where).forUpdate({ wait: 0 })
|
|
102
76
|
|
|
103
77
|
const columnNames = getColumns(req.target, { onlyNames: true, filterVirtual: true })
|
|
104
78
|
const rootCQN = SELECT.from(req.target, columnNames).where(rootWhere)
|
|
@@ -121,7 +95,13 @@ const fioriGenericEdit = async function (req, next) {
|
|
|
121
95
|
|
|
122
96
|
const dbtx = cds.tx(req)
|
|
123
97
|
// REVISIT: Use service.read with expand **
|
|
124
|
-
const [draftExists, ...results] = await
|
|
98
|
+
const [draftExists, ...results] = await _lockAndSelectActive.call(
|
|
99
|
+
dbtx,
|
|
100
|
+
req,
|
|
101
|
+
activeLockCQN,
|
|
102
|
+
[...selectCQNs],
|
|
103
|
+
draftExistsCQN
|
|
104
|
+
)
|
|
125
105
|
|
|
126
106
|
if (!results[0].length) req.reject(404)
|
|
127
107
|
|
|
@@ -14,7 +14,7 @@ const {
|
|
|
14
14
|
getEnrichedCQN,
|
|
15
15
|
removeDraftUUIDIfNecessary,
|
|
16
16
|
replaceRefWithDraft,
|
|
17
|
-
|
|
17
|
+
entity_keys
|
|
18
18
|
} = require('../utils/handler')
|
|
19
19
|
const { deleteCondition, readAndDeleteKeywords, removeIsActiveEntityRecursively } = require('../utils/where')
|
|
20
20
|
const { adaptStreamCQN } = require('../utils/stream')
|
|
@@ -104,9 +104,7 @@ const _getTableName = (
|
|
|
104
104
|
}
|
|
105
105
|
}
|
|
106
106
|
|
|
107
|
-
const _getTargetKeys = ({ target }) =>
|
|
108
|
-
return filterKeys(target.keys)
|
|
109
|
-
}
|
|
107
|
+
const _getTargetKeys = ({ target }) => entity_keys(target)
|
|
110
108
|
|
|
111
109
|
const DRAFT_COLUMNS_CASTED = [
|
|
112
110
|
{
|
|
@@ -382,7 +380,7 @@ const _allActive = (req, columns) => {
|
|
|
382
380
|
_getDefaultDraftProperties({ hasDraft: null })
|
|
383
381
|
)
|
|
384
382
|
|
|
385
|
-
const ids =
|
|
383
|
+
const ids = entity_keys(req.target)
|
|
386
384
|
const isCount = columns.some(element => element.func === 'count')
|
|
387
385
|
const xpr = {
|
|
388
386
|
xpr: [
|