@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.
Files changed (110) hide show
  1. package/CHANGELOG.md +69 -3
  2. package/_i18n/i18n_es_MX.properties +110 -0
  3. package/apis/cds.d.ts +13 -12
  4. package/apis/core.d.ts +27 -108
  5. package/apis/cqn.d.ts +15 -18
  6. package/apis/csn.d.ts +95 -60
  7. package/apis/env.d.ts +25 -0
  8. package/apis/events.d.ts +125 -0
  9. package/apis/{reflect.d.ts → linked.d.ts} +29 -38
  10. package/apis/models.d.ts +60 -45
  11. package/apis/ql.d.ts +19 -5
  12. package/apis/{serve.d.ts → server.d.ts} +59 -33
  13. package/apis/services.d.ts +76 -147
  14. package/apis/test.d.ts +1 -1
  15. package/bin/serve.js +3 -0
  16. package/lib/compile/cds-compile.js +2 -2
  17. package/lib/compile/etc/csv.js +2 -1
  18. package/lib/compile/to/edm.js +8 -3
  19. package/lib/compile/to/gql.js +4 -0
  20. package/lib/dbs/cds-deploy.js +52 -4
  21. package/lib/env/cds-requires.js +27 -15
  22. package/lib/env/defaults.js +1 -0
  23. package/lib/env/schemas/index.js +10 -0
  24. package/lib/index.js +7 -4
  25. package/lib/linked/models.js +8 -5
  26. package/lib/ql/CREATE.js +2 -0
  27. package/lib/ql/DELETE.js +1 -0
  28. package/lib/ql/DROP.js +2 -0
  29. package/lib/ql/INSERT.js +2 -22
  30. package/lib/ql/Query.js +59 -22
  31. package/lib/ql/SELECT.js +5 -0
  32. package/lib/ql/STREAM.js +2 -0
  33. package/lib/ql/UPDATE.js +2 -0
  34. package/lib/ql/UPSERT.js +3 -1
  35. package/lib/ql/cds-ql.js +21 -5
  36. package/lib/ql/infer.js +129 -0
  37. package/lib/req/cds-context.js +8 -5
  38. package/lib/srv/cds-connect.js +3 -1
  39. package/lib/utils/axios.js +4 -2
  40. package/lib/utils/data.js +3 -0
  41. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +12 -0
  42. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +27 -9
  43. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +1 -1
  44. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/TrustedResourceJsonSerializer.js +8 -1
  45. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +11 -8
  46. package/libx/_runtime/common/code-ext/worker.js +5 -16
  47. package/libx/_runtime/common/generic/auth/capabilities.js +11 -2
  48. package/libx/_runtime/common/i18n/messages.properties +1 -0
  49. package/libx/_runtime/common/utils/postProcessing.js +1 -1
  50. package/libx/_runtime/common/utils/resolveView.js +28 -9
  51. package/libx/{common → _runtime/common}/utils/ucsn.js +19 -11
  52. package/libx/_runtime/db/expand/expandCQNToJoin.js +6 -6
  53. package/libx/_runtime/db/expand/rawToExpanded.js +4 -4
  54. package/libx/_runtime/db/sql-builder/InsertBuilder.js +6 -1
  55. package/libx/_runtime/db/sql-builder/UpdateBuilder.js +6 -1
  56. package/libx/_runtime/db/sql-builder/dollar.js +7 -7
  57. package/libx/_runtime/fiori/generic/activate.js +2 -2
  58. package/libx/_runtime/fiori/generic/edit.js +25 -45
  59. package/libx/_runtime/fiori/generic/read.js +3 -5
  60. package/libx/_runtime/fiori/lean-draft.js +171 -84
  61. package/libx/_runtime/fiori/utils/delete.js +7 -1
  62. package/libx/_runtime/fiori/utils/handler.js +4 -6
  63. package/libx/_runtime/fiori/utils/lockInfo.js +27 -0
  64. package/libx/_runtime/fiori/utils/where.js +20 -1
  65. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +3 -2
  66. package/libx/_runtime/messaging/Outbox.js +12 -47
  67. package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -3
  68. package/libx/_runtime/messaging/common-utils/authorizedRequest.js +3 -0
  69. package/libx/_runtime/messaging/common-utils/connections.js +1 -1
  70. package/libx/_runtime/messaging/enterprise-messaging.js +12 -13
  71. package/libx/_runtime/messaging/file-based.js +7 -5
  72. package/libx/_runtime/messaging/redis-messaging.js +10 -11
  73. package/libx/_runtime/messaging/service.js +12 -26
  74. package/libx/_runtime/remote/Service.js +52 -36
  75. package/libx/_runtime/remote/utils/client.js +24 -125
  76. package/libx/odata/afterburner.js +16 -6
  77. package/libx/odata/grammar.peggy +26 -7
  78. package/libx/odata/metadata.js +18 -1
  79. package/libx/odata/parser.js +1 -1
  80. package/libx/odata/service-document.js +0 -1
  81. package/libx/odata/utils.js +19 -3
  82. package/libx/{_runtime/messaging/outbox/utils.js → outbox/index.js} +94 -24
  83. package/libx/rest/middleware/parse.js +1 -1
  84. package/package.json +2 -2
  85. package/apis/connect.d.ts +0 -39
  86. package/bin/utils/modules.js +0 -7
  87. package/bin/utils/term.js +0 -56
  88. package/lib/env/schema.js +0 -9
  89. package/lib/linked/queries.js +0 -41
  90. package/lib/srv/protocols/odata-v2-proxy.js +0 -3699
  91. package/libx/common/asserts.js +0 -0
  92. package/libx/common/crud.js +0 -0
  93. package/libx/common/etag.js +0 -0
  94. package/libx/common/localized.js +0 -0
  95. package/libx/common/managed.js +0 -0
  96. package/libx/common/paging.js +0 -0
  97. package/libx/common/readme.md +0 -4
  98. package/libx/common/sorting.js +0 -0
  99. package/libx/common/temporal.js +0 -0
  100. package/libx/connect/auth.js +0 -0
  101. package/libx/connect/perf.js +0 -0
  102. package/libx/connect/readme.md +0 -3
  103. package/libx/fiori/draft/readme.md +0 -1
  104. package/libx/fiori/readme.md +0 -1
  105. package/libx/hana/readme.md +0 -1
  106. package/libx/msg/readme.md +0 -3
  107. package/libx/readme.md +0 -1
  108. package/libx/sqlite/readme.md +0 -1
  109. /package/libx/_runtime/{messaging/common-utils → common/utils}/waitingTime.js +0 -0
  110. /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('../../../../../common/utils/ucsn')
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
- return (
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
- return _getRestriction(req, capability, r.ReadRestrictions?.['ReadByKeyRestrictions.Readable'])
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?.ref && from.ref.map(el => el.id || el)) || []
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) newRef[1] = mapped.ref[0]
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
- ? q.INSERT._transitions[q.INSERT._transitions.length - 1].target
767
- : q.UPDATE
768
- ? q.UPDATE._transitions[q.UPDATE._transitions.length - 1].target
769
- : q.UPSERT
770
- ? q.UPSERT._transitions[q.UPSERT._transitions.length - 1].target
771
- : q.DELETE
772
- ? q.DELETE._transitions[q.DELETE._transitions.length - 1].target
773
- : undefined
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('../../_runtime/cds')
2
- const getTemplate = require('../../_runtime/common/utils/template')
3
- const templateProcessor = require('../../_runtime/common/utils/templateProcessor')
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
- const _cleanup = (row, definition, cleanupNull, cleanupStruct, prefix = []) => {
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( row[key], definition, cleanupNull, cleanupStruct, [...prefix, key])
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) delete row[key]
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 { filterKeys } = require('../../fiori/utils/handler')
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
- ? [alias, element.ref[0]]
1286
- : this._isPathExpressionToOne(element.ref, expandedEntity)
1287
- ? [alias, ...element.ref]
1288
- : [alias, element.ref[1]]
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 = filterKeys(expandedEntity.keys).reduce((res, keyName) => {
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
- ? entry.IsActiveEntity === null
101
- ? null
102
- : !!entry.IsActiveEntity
103
- : null
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(this._obj.INSERT.entries, this._options.user, this._options.now)
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(this._obj.UPDATE.data, this._options.user, this._options.now)
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
- filterKeys,
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 = filterKeys(context.target.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 { getTransition } = require('../../common/utils/resolveView')
7
- const { draftIsLocked, ensureDraftsSuffix, ensureNoDraftsSuffix, getSubCQNs, filterKeys } = require('../utils/handler')
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
- const _getLockWhere = (where, columnsMap) => {
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 dbtx.run(lockRecordCQN)
37
+ await this.run(lockRecordCQN)
52
38
  } catch (e) {
53
- const drafts = await dbtx.run(draftExistsCQN)
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 promisesResults = await Promise.allSettled([dbtx.run(draftExistsCQN), ...selectCQNs.map(cqn => dbtx.run(cqn))])
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(r => r.value)
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
- // TODO replace with generic where filter
81
- const keys = filterKeys(req.target.keys)
82
- const data = getKeyData(keys, req.query.SELECT.from.ref[0].where)
83
- const rootWhere = keys.reduce((res, key) => {
84
- res[key] = data[key]
85
- return res
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 _select(lockRecordCQN, draftExistsCQN, [...selectCQNs], req, dbtx)
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
- filterKeys
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 = filterKeys(req.target.keys)
383
+ const ids = entity_keys(req.target)
386
384
  const isCount = columns.some(element => element.func === 'count')
387
385
  const xpr = {
388
386
  xpr: [