@sap/cds 6.3.2 → 6.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (112) hide show
  1. package/CHANGELOG.md +76 -0
  2. package/apis/cds.d.ts +1 -1
  3. package/apis/core.d.ts +118 -90
  4. package/apis/cqn.d.ts +11 -2
  5. package/apis/internal/inference.d.ts +7 -2
  6. package/apis/ql.d.ts +45 -11
  7. package/apis/serve.d.ts +8 -1
  8. package/apis/services.d.ts +303 -305
  9. package/bin/build/buildTaskEngine.js +28 -36
  10. package/bin/build/buildTaskFactory.js +32 -81
  11. package/bin/build/buildTaskHandler.js +3 -2
  12. package/bin/build/buildTaskProvider.js +2 -2
  13. package/bin/build/buildTaskProviderFactory.js +5 -14
  14. package/bin/build/constants.js +0 -1
  15. package/bin/build/provider/buildTaskHandlerEdmx.js +7 -6
  16. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +6 -5
  17. package/bin/build/provider/buildTaskHandlerInternal.js +9 -30
  18. package/bin/build/provider/buildTaskProviderInternal.js +70 -58
  19. package/bin/build/provider/fiori/index.js +6 -5
  20. package/bin/build/provider/hana/2migration.js +20 -3
  21. package/bin/build/provider/hana/2tabledata.js +1 -0
  22. package/bin/build/provider/hana/index.js +40 -17
  23. package/bin/build/provider/java/index.js +10 -10
  24. package/bin/build/provider/mtx/index.js +25 -16
  25. package/bin/build/provider/mtx/resourcesTarBuilder.js +22 -27
  26. package/bin/build/provider/mtx-extension/index.js +3 -2
  27. package/bin/build/provider/mtx-sidecar/index.js +16 -15
  28. package/bin/build/provider/nodejs/index.js +14 -56
  29. package/bin/build/util.js +56 -16
  30. package/bin/deploy/to-hana/cfUtil.js +2 -0
  31. package/bin/deploy/to-hana/gitUtil.js +1 -1
  32. package/bin/deploy/to-hana/hana.js +45 -38
  33. package/bin/deploy/to-hana/hdiDeployUtil.js +8 -9
  34. package/bin/deploy/to-hana/mtaUtil.js +13 -14
  35. package/bin/mtx/in-cds.js +3 -1
  36. package/bin/serve.js +1 -1
  37. package/bin/version.js +2 -1
  38. package/lib/compile/cds-compile.js +1 -0
  39. package/lib/compile/cdsc.js +1 -0
  40. package/lib/compile/etc/_localized.js +2 -2
  41. package/lib/compile/for/lean_drafts.js +83 -0
  42. package/lib/compile/for/nodejs.js +1 -0
  43. package/lib/compile/minify.js +2 -1
  44. package/lib/compile/to/gql.js +1 -1
  45. package/lib/compile/to/sql.js +11 -1
  46. package/lib/core/entities.js +1 -1
  47. package/lib/core/index.js +8 -9
  48. package/lib/core/infer.js +1 -0
  49. package/lib/dbs/cds-deploy.js +97 -41
  50. package/lib/env/cds-env.js +9 -10
  51. package/lib/env/cds-requires.js +8 -2
  52. package/lib/env/defaults.js +0 -4
  53. package/lib/env/schemas/cds-rc.json +38 -0
  54. package/lib/ql/SELECT.js +10 -4
  55. package/lib/srv/bindings.js +1 -1
  56. package/lib/srv/factory.js +1 -1
  57. package/lib/srv/srv-methods.js +1 -1
  58. package/lib/utils/cds-utils.js +11 -0
  59. package/lib/utils/inflect.js +13 -12
  60. package/lib/utils/tar.js +12 -4
  61. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -2
  62. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -15
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  68. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/errors/UriSyntaxError.js +1 -1
  69. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +6 -1
  70. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -1
  71. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -12
  72. package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +1 -7
  73. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +4 -0
  74. package/libx/_runtime/cds-services/services/Service.js +23 -1
  75. package/libx/_runtime/cds-services/util/assert.js +0 -41
  76. package/libx/_runtime/common/composition/data.js +5 -1
  77. package/libx/_runtime/common/generic/auth/utils.js +3 -3
  78. package/libx/_runtime/common/generic/input.js +4 -24
  79. package/libx/_runtime/common/generic/paging.js +3 -3
  80. package/libx/_runtime/common/utils/csn.js +21 -15
  81. package/libx/_runtime/common/utils/draft.js +2 -1
  82. package/libx/_runtime/common/utils/resolveView.js +25 -4
  83. package/libx/_runtime/common/utils/rewriteAsterisks.js +3 -1
  84. package/libx/_runtime/common/utils/rowUUIDGenerator.js +21 -0
  85. package/libx/_runtime/common/utils/templateProcessor.js +12 -15
  86. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +23 -0
  87. package/libx/_runtime/db/expand/expandCQNToJoin.js +29 -12
  88. package/libx/_runtime/db/generic/input.js +7 -13
  89. package/libx/_runtime/db/sql-builder/UpsertBuilder.js +47 -0
  90. package/libx/_runtime/db/sql-builder/index.js +2 -0
  91. package/libx/_runtime/db/sql-builder/sqlFactory.js +9 -0
  92. package/libx/_runtime/db/utils/columns.js +4 -2
  93. package/libx/_runtime/fiori/generic/read.js +1 -12
  94. package/libx/_runtime/fiori/lean-draft.js +657 -0
  95. package/libx/_runtime/fiori/utils/handler.js +1 -1
  96. package/libx/_runtime/hana/pool.js +16 -1
  97. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +2 -1
  98. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
  99. package/libx/_runtime/messaging/enterprise-messaging.js +2 -3
  100. package/libx/_runtime/messaging/outbox/utils.js +109 -70
  101. package/libx/_runtime/messaging/service.js +16 -7
  102. package/libx/_runtime/remote/Service.js +15 -2
  103. package/libx/_runtime/remote/utils/client.js +41 -11
  104. package/libx/_runtime/sqlite/Service.js +3 -0
  105. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +56 -0
  106. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +59 -0
  107. package/libx/_runtime/sqlite/customBuilder/index.js +5 -0
  108. package/libx/_runtime/sqlite/execute.js +1 -1
  109. package/libx/_runtime/types/api.js +2 -2
  110. package/libx/rest/RestAdapter.js +15 -13
  111. package/package.json +1 -1
  112. package/server.js +1 -0
@@ -18,12 +18,6 @@ const _getEntitySets = (edm, namespace) => {
18
18
  return entities
19
19
  }
20
20
 
21
- const _getConcurrent = (namespace, element, csn) => {
22
- // autoexposed entities now used . in csn and _ in edm
23
- const e = findCsnTargetFor(element, csn, namespace)
24
- return !!e._etag
25
- }
26
-
27
21
  const oDataConfiguration = (edm, csn) => {
28
22
  let namespace
29
23
  for (const prop in edm) {
@@ -44,7 +38,7 @@ const oDataConfiguration = (edm, csn) => {
44
38
 
45
39
  configuration[entitySet] = {
46
40
  maxPageSize: getMaxPageSize(e),
47
- isConcurrent: _getConcurrent(namespace, entitySet, csn)
41
+ isConcurrent: !!e._etag
48
42
  }
49
43
 
50
44
  // custom aggregates
@@ -208,6 +208,9 @@ const _processCategory = (req, category, elementInfo, options, previousResult) =
208
208
  localizeAfterDraftActivate(row, key, req.locale)
209
209
  break
210
210
 
211
+ case '@cds.Boolean':
212
+ if (row[key] != null) row[key] = !!row[key]
213
+
211
214
  // no default
212
215
  }
213
216
  }
@@ -270,6 +273,7 @@ const _pick = options => (element, target) => {
270
273
 
271
274
  if (element['@odata.etag']) categories.push('@odata.etag')
272
275
  if (element._type === 'cds.Decimal') categories.push('@cds.Decimal')
276
+ if (cds.db?.kind === 'better-sqlite' && element._type === 'cds.Boolean') categories.push('@cds.Boolean')
273
277
 
274
278
  categories.push(..._assocs(element, target))
275
279
 
@@ -50,7 +50,29 @@ class ApplicationService extends cds.Service {
50
50
  }
51
51
 
52
52
  registerFioriHandlers() {
53
- return require('../../fiori/generic').impl.call(this)
53
+ if (cds.env.features.lean_draft) {
54
+ const {
55
+ onNewDraft,
56
+ onDraftPrepare,
57
+ onDraftActivate,
58
+ onPatch,
59
+ onDraftEdit,
60
+ onDelete
61
+ } = require('../../fiori/lean-draft')
62
+ const LOG = cds.log('fiori|drafts')
63
+
64
+ for (let each of this.entities)
65
+ if (each.drafts) {
66
+ LOG.debug('serving drafts for', { entity: each.name })
67
+ this.on('NEW', each, onNewDraft)
68
+ this.on('PATCH', each, onPatch)
69
+ this.on('EDIT', each, onDraftEdit)
70
+ this.on('draftPrepare', each, onDraftPrepare)
71
+ this.on('draftActivate', each, onDraftActivate)
72
+ this.on('draftActivate', each, onDraftActivate)
73
+ this.on(['CANCEL', 'DELETE'], each, onDelete)
74
+ }
75
+ } else return require('../../fiori/generic').impl.call(this)
54
76
  }
55
77
 
56
78
  registerCrudHandlers() {
@@ -259,46 +259,6 @@ const _checkFormatElement = (element, value, errors, key, pathSegments) => {
259
259
  }
260
260
  }
261
261
 
262
- // check for forbidden deep operations for association
263
- const checkIfAssocDeep = (element, value, req) => {
264
- if (!value) return
265
-
266
- if (element.on) {
267
- req.error(
268
- assertError(
269
- element.is2one
270
- ? { code: ASSERT_DEEP_ASSOCIATION, args: ['unmanaged to-one', element.name] }
271
- : { code: ASSERT_DEEP_ASSOCIATION, args: ['to-many', element.name] },
272
- element,
273
- value
274
- )
275
- )
276
-
277
- return
278
- }
279
-
280
- if (element.is2one) {
281
- // managed to one
282
- Object.keys(value).forEach(prop => {
283
- if (typeof value[prop] !== 'object') {
284
- const foreignKey = element._foreignKeys.find(fk => fk.childElement.name === prop)
285
- if (foreignKey) return
286
-
287
- const key = element.keys.find(element => element.ref[0] === prop)
288
- if (key) return
289
-
290
- const err = assertError(
291
- { code: ASSERT_DEEP_ASSOCIATION, args: ['managed to-one', element.name] },
292
- element,
293
- value
294
- )
295
- err.target += `.${prop}`
296
- req.error(err)
297
- }
298
- })
299
- }
300
- }
301
-
302
262
  /**
303
263
  * @param {import('../../types/api').InputConstraints} constraints
304
264
  */
@@ -407,7 +367,6 @@ module.exports = {
407
367
  checkInputConstraints,
408
368
  checkKeys,
409
369
  assertError,
410
- checkIfAssocDeep,
411
370
  checkStaticElementByKey,
412
371
  assertNotNullError,
413
372
  assertTargets
@@ -283,7 +283,11 @@ const _selectDeepUpdateData = async args => {
283
283
 
284
284
  // if a view has an orderBy with renamed field, we need to resolve it
285
285
  const _resolveOrderBy = (orderBy, transitions) => {
286
- if (orderBy && transitions.length > 0) orderBy.map(el => (el.ref[0] = transitions[0].mapping.get(el.ref[0]).ref[0]))
286
+ // no resolved entity found
287
+ if (!transitions?.length) return
288
+ // if there are no renamed fields, no need to resolve
289
+ if (!transitions[0].mapping.size) return
290
+ if (orderBy) orderBy.map(el => (el.ref[0] = transitions[0].mapping.get(el.ref[0]).ref[0]))
287
291
  }
288
292
 
289
293
  /*
@@ -9,9 +9,9 @@ const reject = (req, reason = null) => {
9
9
  // unauthorized or forbidden?
10
10
  if (req.user._is_anonymous) {
11
11
  // REVISIT: challenges handling should be done in protocol adapter (i.e., express error middleware)
12
- // REVISIT: improve `req._.req` check if this is an HTTP request
13
- if (req._.req && req.user._challenges && req.user._challenges.length > 0) {
14
- req._.res.set('WWW-Authenticate', req.user._challenges.join(';'))
12
+ // REVISIT: improve `req.http.req` check if this is an HTTP request
13
+ if (req.http?.req && req.user._challenges && req.user._challenges.length > 0) {
14
+ req.http.res.set('WWW-Authenticate', req.user._challenges.join(';'))
15
15
  }
16
16
 
17
17
  // REVISIT: security log in else case?
@@ -16,6 +16,7 @@ const { checkInputConstraints, assertTargets } = require('../../cds-services/uti
16
16
  const getTemplate = require('../utils/template')
17
17
  const templateProcessor = require('../utils/templateProcessor')
18
18
  const { getDataFromCQN, setDataFromCQN } = require('../utils/data')
19
+ const getRowUUIDGeneratorFn = require('../utils/rowUUIDGenerator')
19
20
 
20
21
  const _shouldSuppressErrorPropagation = (event, value) => {
21
22
  return (
@@ -34,24 +35,6 @@ const _getSimpleCategory = category => {
34
35
  return category
35
36
  }
36
37
 
37
- const _rowKeysGenerator = eventName => {
38
- if (eventName === 'UPDATE') return
39
- return (keyNames, row, template) => {
40
- for (const keyName of keyNames) {
41
- if (Object.prototype.hasOwnProperty.call(row, keyName)) {
42
- continue
43
- }
44
-
45
- const elementInfo = template.elements.get(keyName)
46
- const plain = elementInfo && elementInfo.picked && elementInfo.picked.plain
47
- if (!plain || !plain.categories) continue
48
- if (plain.categories.includes('uuid')) {
49
- row[keyName] = cds.utils.uuid()
50
- }
51
- }
52
- }
53
- }
54
-
55
38
  const _isDraftCoreComputed = (req, element, event) =>
56
39
  cds.env.features.preserve_computed !== false &&
57
40
  req._ &&
@@ -65,10 +48,7 @@ const _isStreamingProperty = (elements, row, property) =>
65
48
  )
66
49
 
67
50
  const _getMediaTypeValue = req =>
68
- req._.req &&
69
- req._.req.headers['content-type'] &&
70
- !req._.req.headers['content-type'].match(/json|multipart/i) &&
71
- req._.req.headers['content-type']
51
+ !req.http?.req?.headers?.['content-type'].match(/json|multipart/i) && req.http?.req?.headers?.['content-type']
72
52
 
73
53
  const _preProcessAssertTarget = (assocInfo, assertMap) => {
74
54
  const { element: assoc, row } = assocInfo
@@ -265,7 +245,7 @@ async function commonGenericInput(req) {
265
245
  }
266
246
 
267
247
  const pathOptions = {
268
- rowKeysGenerator: _rowKeysGenerator(req.event),
248
+ rowUUIDGenerator: getRowUUIDGeneratorFn(req.event),
269
249
  includeKeyValues: true,
270
250
  pathSegments: []
271
251
  }
@@ -276,7 +256,7 @@ async function commonGenericInput(req) {
276
256
  if (pathSegment) pathOptions.pathSegments.push(pathSegment)
277
257
 
278
258
  if (keys && 'IsActiveEntity' in keys) {
279
- pathOptions.extraKeys = { IsActiveEntity: keys.IsActiveEntity }
259
+ pathOptions.draftKeys = { IsActiveEntity: keys.IsActiveEntity }
280
260
  }
281
261
  }
282
262
 
@@ -3,10 +3,10 @@ const { getDefaultPageSize, getMaxPageSize } = require('../utils/page')
3
3
 
4
4
  const commonGenericPaging = function (req) {
5
5
  // only if http request
6
- if (!(req.http?.req || req._.req)) return
6
+ if (!req.http?.req) return
7
7
 
8
8
  // target === null if view with parameters
9
- if (!req.target || !req.query.SELECT || req.query.SELECT.one) return
9
+ if (!req.target || !req.query?.SELECT || req.query.SELECT.one) return
10
10
 
11
11
  _addPaging(req.query, req.target)
12
12
  }
@@ -17,7 +17,7 @@ const _addPaging = function (query, target) {
17
17
  offset = offset && 'val' in offset ? offset.val : 0
18
18
  query.limit(...[Math.min(rows, getMaxPageSize(target)), offset])
19
19
  //Handle nested limits
20
- if (query.SELECT.from.SELECT) _addPaging(query.SELECT.from, target)
20
+ if (query.SELECT.from.SELECT?.limit) _addPaging(query.SELECT.from, target)
21
21
  }
22
22
 
23
23
  /**
@@ -129,21 +129,22 @@ const _findCsnTarget = (edmName, model, namespace) => {
129
129
  return target
130
130
  }
131
131
 
132
+ const _initializeCache = (model, namespace) => {
133
+ const cache = {}
134
+ for (const name in model.definitions) {
135
+ // do no cache entities within different namespace
136
+ if (!name.startsWith(`${namespace}.`)) continue
137
+ // cut off namespace and underscoreify entity name (OData does not allow dots)
138
+ cache[name.replace(new RegExp(`^${namespace}\\.`), '').replace(/\./g, '_')] = model.definitions[name]
139
+ }
140
+ return cache
141
+ }
142
+
132
143
  const findCsnTargetFor = (edmName, model, namespace) => {
133
- const cache =
134
- model._edmToCSNNameMap || Object.defineProperty(model, '_edmToCSNNameMap', { value: {} })._edmToCSNNameMap
135
- const edm2csnMap =
136
- cache[namespace] ||
137
- Object.defineProperty(cache, namespace, {
138
- get() {
139
- const _ = {}
140
- for (const name in model.definitions) {
141
- if (!name.startsWith(`${namespace}.`)) continue
142
- _[name.replace(new RegExp(`^${namespace}\\.`), '').replace(/\./g, '_')] = model.definitions[name]
143
- }
144
- return _
145
- }
146
- })[namespace]
144
+ const cache = model._edmToCSNNameMap || (model._edmToCSNNameMap = {})
145
+ const edm2csnMap = cache[namespace] || (cache[namespace] = _initializeCache(model, namespace))
146
+
147
+ if (edm2csnMap[edmName]) return edm2csnMap[edmName]
147
148
 
148
149
  const target = _findCsnTarget(edmName, model, namespace)
149
150
 
@@ -226,7 +227,12 @@ function getDraftTreeRoot(entity, model) {
226
227
  for (const k in model.definitions) {
227
228
  const e = model.definitions[k]
228
229
  if (e.kind !== 'entity' || !e.compositions) continue
229
- for (const c in e.compositions) if (e.compositions[c].target === current.name) parents.push(e)
230
+ for (const c in e.compositions)
231
+ if (
232
+ e.compositions[c].target === current.name ||
233
+ e.compositions[c].target === current.name.replace(/\.drafts/, '')
234
+ )
235
+ parents.push(e)
230
236
  }
231
237
  if (parents.length > 1 && parents.some(p => p !== parents[0])) {
232
238
  // > unable to determine single parent
@@ -19,7 +19,8 @@ const ensureUnlocalized = table => {
19
19
  return _table
20
20
  }
21
21
 
22
- const ensureDraftsSuffix = name => (name.endsWith('_drafts') ? name : `${ensureUnlocalized(name)}_drafts`)
22
+ const ensureDraftsSuffix = name =>
23
+ name.endsWith('_drafts') || name.endsWith('.drafts') ? name : `${ensureUnlocalized(name)}_drafts`
23
24
 
24
25
  const ensureNoDraftsSuffix = name => name.replace(/_drafts$/g, '')
25
26
 
@@ -319,7 +319,7 @@ const _rewriteQueryPath = (path, transitions) => {
319
319
  const _newUpdate = (query, transitions, service) => {
320
320
  const targetTransition = transitions[transitions.length - 1]
321
321
  const targetName = targetTransition.target.name
322
- const newUpdate = { ...query.UPDATE }
322
+ const newUpdate = Object.create(query.UPDATE)
323
323
  newUpdate.entity = newUpdate.entity.ref
324
324
  ? {
325
325
  ...newUpdate.entity,
@@ -345,7 +345,7 @@ const _newUpdate = (query, transitions, service) => {
345
345
 
346
346
  const _newSelect = (query, transitions, service) => {
347
347
  const targetTransition = transitions[transitions.length - 1]
348
- const newSelect = { ...query.SELECT }
348
+ const newSelect = Object.create(query.SELECT)
349
349
  newSelect.from = {
350
350
  ...newSelect.from,
351
351
  ref: _rewriteQueryPath(query.SELECT.from, transitions)
@@ -379,7 +379,7 @@ const _newSelect = (query, transitions, service) => {
379
379
  const _newInsert = (query, transitions, service) => {
380
380
  const targetTransition = transitions[transitions.length - 1]
381
381
  const targetName = targetTransition.target.name
382
- const newInsert = { ...query.INSERT }
382
+ const newInsert = Object.create(query.INSERT)
383
383
  newInsert.into = newInsert.into.ref
384
384
  ? {
385
385
  ...newInsert.into,
@@ -395,10 +395,29 @@ const _newInsert = (query, transitions, service) => {
395
395
  return newInsert
396
396
  }
397
397
 
398
+ const _newUpsert = (query, transitions, service) => {
399
+ const targetTransition = transitions[transitions.length - 1]
400
+ const targetName = targetTransition.target.name
401
+ const newUpsert = Object.create(query.UPSERT)
402
+ newUpsert.into = newUpsert.into.ref
403
+ ? {
404
+ ...newUpsert.into,
405
+ ref: _rewriteQueryPath(query.UPSERT.into, transitions)
406
+ }
407
+ : targetName
408
+ if (newUpsert.columns) newUpsert.columns = _newInsertColumns(newUpsert.columns, targetTransition)
409
+ if (newUpsert.entries) newUpsert.entries = _newEntries(newUpsert.entries, targetTransition, service)
410
+ Object.defineProperty(newUpsert, '_transitions', {
411
+ enumerable: false,
412
+ value: transitions
413
+ })
414
+ return newUpsert
415
+ }
416
+
398
417
  const _newDelete = (query, transitions) => {
399
418
  const targetTransition = transitions[transitions.length - 1]
400
419
  const targetName = targetTransition.target.name
401
- const newDelete = { ...query.DELETE }
420
+ const newDelete = Object.create(query.DELETE)
402
421
  newDelete.from = newDelete.from.ref
403
422
  ? {
404
423
  ...newDelete.from,
@@ -600,6 +619,7 @@ const _newQuery = (query, event, model, service) => {
600
619
  const [_prop, _func] = {
601
620
  SELECT: ['from', _newSelect],
602
621
  INSERT: ['into', _newInsert],
622
+ UPSERT: ['into', _newUpsert],
603
623
  UPDATE: ['entity', _newUpdate],
604
624
  DELETE: ['from', _newDelete]
605
625
  }[event]
@@ -619,6 +639,7 @@ const resolveView = (query, model, service) => {
619
639
  if (query.cmd) _event = query.cmd
620
640
  else if (query.SELECT) _event = 'SELECT'
621
641
  else if (query.INSERT) _event = 'INSERT'
642
+ else if (query.UPSERT) _event = 'UPSERT'
622
643
  else if (query.UPDATE) _event = 'UPDATE'
623
644
  else if (query.DELETE) _event = 'DELETE'
624
645
 
@@ -2,6 +2,7 @@ const { getNavigationIfStruct } = require('./structured')
2
2
  const getColumns = require('../../db/utils/columns')
3
3
  const { ensureNoDraftsSuffix, getDraftColumnsCQNForActive } = require('./draft')
4
4
  const { getEntityNameFromCQN } = require('./entityFromCqn')
5
+ const cds = require('../../cds')
5
6
 
6
7
  const isAsteriskColumn = col => col === '*' || (col.ref && col.ref[0] === '*' && !col.expand)
7
8
 
@@ -118,7 +119,8 @@ const rewriteAsterisks = (query, model, options) => {
118
119
  if (!target) return
119
120
 
120
121
  query.SELECT.columns = getColumns(target, { _4db }).map(col => ({ ref: [col.name] }))
121
- if (_4db && target._isDraftEnabled) query.SELECT.columns.push(..._cqlDraftColumns(target))
122
+ if (_4db && target._isDraftEnabled && !cds.env.features.lean_draft)
123
+ query.SELECT.columns.push(..._cqlDraftColumns(target))
122
124
  }
123
125
  }
124
126
 
@@ -0,0 +1,21 @@
1
+ const cds = require('../../cds')
2
+
3
+ const getRowUUIDGeneratorFn = eventName => {
4
+ if (eventName === 'UPDATE') return
5
+ return (keyNames, row, template) => {
6
+ for (const keyName of keyNames) {
7
+ if (Object.prototype.hasOwnProperty.call(row, keyName)) {
8
+ continue
9
+ }
10
+
11
+ const elementInfo = template.elements.get(keyName)
12
+ const plain = elementInfo && elementInfo.picked && elementInfo.picked.plain
13
+ if (!plain || !plain.categories) continue
14
+ if (plain.categories.includes('uuid')) {
15
+ row[keyName] = cds.utils.uuid()
16
+ }
17
+ }
18
+ }
19
+ }
20
+
21
+ module.exports = getRowUUIDGeneratorFn
@@ -1,16 +1,12 @@
1
1
  const DELIMITER = require('./templateDelimiter')
2
-
3
- const _formatRowContext = (tKey, keyNames, row) => {
4
- const keyValuePairs = keyNames.map(key => `${key}=${row[key]}`)
5
- const keyValuePairsSerialized = keyValuePairs.join(',')
6
- return `${tKey}(${keyValuePairsSerialized})`
7
- }
2
+ const pathSerializer = require('./templateProcessorPathSerializer')
8
3
 
9
4
  const _processElement = (processFn, row, key, target, picked = {}, isRoot, pathSegments) => {
10
5
  const element = (target.elements || target.params)[key]
11
6
  const { plain } = picked
12
7
 
13
8
  if (!plain) return
9
+
14
10
  /**
15
11
  * @type import('../../types/api').templateElementInfo
16
12
  */
@@ -19,6 +15,7 @@ const _processElement = (processFn, row, key, target, picked = {}, isRoot, pathS
19
15
  elementInfo.pathSegments = pathSegments.slice(0)
20
16
  elementInfo.pathSegments.push(...target._flat2struct[key])
21
17
  }
18
+
22
19
  processFn(elementInfo)
23
20
  }
24
21
 
@@ -36,34 +33,34 @@ const _processRow = (processFn, row, template, tKey, tValue, isRoot, pathOptions
36
33
 
37
34
  const _getTargetKeyNames = target => {
38
35
  const keyNames = []
36
+
39
37
  for (const keyName in target.keys) {
40
38
  if (target.keys[keyName].__isAssociationStrict) continue
41
39
  keyNames.push(keyName)
42
40
  }
41
+
43
42
  return keyNames
44
43
  }
45
44
 
46
45
  const _processComplex = (processFn, row, template, key, pathOptions) => {
47
- const value = row && row[key]
48
- const rows = Array.isArray(value) ? value : [value]
46
+ const subRow = row?.[key]
47
+ const rows = Array.isArray(subRow) ? subRow : [subRow]
49
48
  if (rows.length === 0) return
50
- const keyNames = _getTargetKeyNames(template.target)
49
+ const keyNames = pathOptions.includeKeyValues && _getTargetKeyNames(template.target)
51
50
 
52
51
  for (let idx = 0; idx < rows.length; idx++) {
53
52
  const row = rows[idx]
54
53
  if (row == null) continue
55
54
  const args = { processFn, row, template, isRoot: false, pathOptions }
56
55
 
57
- let rowContext
56
+ let pathSegment
58
57
  if (pathOptions.includeKeyValues) {
59
- if (pathOptions.rowKeysGenerator) pathOptions.rowKeysGenerator(keyNames, row, template)
60
- rowContext = _formatRowContext(key, keyNames, Object.assign({}, row, pathOptions.extraKeys))
58
+ if (pathOptions.rowUUIDGenerator) pathOptions.rowUUIDGenerator(keyNames, row, template)
59
+ pathSegment = pathSerializer(key, keyNames, row, template.target.elements, pathOptions.draftKeys)
61
60
  }
62
61
 
63
- if (pathOptions.pathSegments) pathOptions.pathSegments.push(rowContext || key)
64
-
62
+ if (pathOptions.pathSegments) pathOptions.pathSegments.push(pathSegment || key)
65
63
  templateProcessor(args)
66
-
67
64
  if (pathOptions.pathSegments) pathOptions.pathSegments.pop()
68
65
  }
69
66
  }
@@ -0,0 +1,23 @@
1
+ const templatePathSerializer = (tKey, keyNames, row, elements, draftKeys) => {
2
+ const keyValuePairs = keyNames.map(key => {
3
+ let quote
4
+
5
+ switch (elements[key].type) {
6
+ case 'cds.String':
7
+ quote = "'"
8
+ break
9
+
10
+ default:
11
+ quote = ''
12
+ break
13
+ }
14
+
15
+ const keyValue = row[key] ?? draftKeys?.[key]
16
+ return `${key}=${quote}${keyValue}${quote}`
17
+ })
18
+
19
+ const keyValuePairsSerialized = keyValuePairs.join(',')
20
+ return `${tKey}(${keyValuePairsSerialized})`
21
+ }
22
+
23
+ module.exports = templatePathSerializer
@@ -138,6 +138,7 @@ class JoinCQNFromExpanded {
138
138
  this._addImplicitOrderBy(readToOneCQN, entity, tableAlias)
139
139
  const givenColumns = readToOneCQN.columns
140
140
  readToOneCQN.columns = []
141
+ if (entity['@cds.localized'] === false) defaultLanguage = true
141
142
  this._expandedToFlat({ entity, givenColumns, readToOneCQN, tableAlias, toManyTree, defaultLanguage })
142
143
  } else {
143
144
  const table = unionTable || this._getRef(SELECT).table
@@ -150,6 +151,7 @@ class JoinCQNFromExpanded {
150
151
 
151
152
  const givenColumns = readToOneCQN.columns
152
153
  readToOneCQN.columns = []
154
+ if (entity['@cds.localized'] === false) defaultLanguage = true
153
155
  this._expandedToFlat({ entity, givenColumns, readToOneCQN, tableAlias, toManyTree, defaultLanguage })
154
156
  }
155
157
 
@@ -491,6 +493,7 @@ class JoinCQNFromExpanded {
491
493
  * @param {boolean} arg.defaultLanguage - Use default language for localized fields
492
494
  * @private
493
495
  */
496
+ // eslint-disable-next-line complexity
494
497
  _expandedToFlat({ entity, givenColumns, readToOneCQN, tableAlias, toManyTree, defaultLanguage }) {
495
498
  const toManyColumns = []
496
499
  const mappings = this._getMappingObject(toManyTree)
@@ -518,7 +521,8 @@ class JoinCQNFromExpanded {
518
521
  entity._isDraftEnabled &&
519
522
  navTarget._isAssociationStrict &&
520
523
  !navTarget['@odata.draft.enclosed'] &&
521
- navTarget.name !== 'DraftAdministrativeData'
524
+ navTarget.name !== 'DraftAdministrativeData' &&
525
+ !entity.elements[navProp]._isCompositionBacklink
522
526
  ) {
523
527
  mappings[navProp] = { [TO_ACTIVE]: true }
524
528
  }
@@ -660,7 +664,10 @@ class JoinCQNFromExpanded {
660
664
  const activeTableRequired =
661
665
  readToOneCQN[IS_UNION_DRAFT] || // > REVISIT: blocks expanding comp2one
662
666
  readToOneCQN[IS_ACTIVE] ||
663
- (element && element._isAssociationStrict && !element['@odata.draft.enclosed']) ||
667
+ (element &&
668
+ element._isAssociationStrict &&
669
+ !element['@odata.draft.enclosed'] &&
670
+ !element._isCompositionBacklink) ||
664
671
  !this._csn.definitions[target]._isDraftEnabled
665
672
 
666
673
  const colTarget = target && ensureUnlocalized(target)
@@ -688,7 +695,7 @@ class JoinCQNFromExpanded {
688
695
 
689
696
  const expandedEntity = this._getEntityForTable(target)
690
697
  if (readToOneCQN[IS_UNION_DRAFT] && expandedEntity.drafts) {
691
- const cols = column.expand.filter(c => !c.expand && !(c.ref[0] in DRAFT_COLUMNS_MAP)).map(c => c.ref[0])
698
+ const cols = column.expand.filter(c => c.ref && !c.expand && !(c.ref[0] in DRAFT_COLUMNS_MAP)).map(c => c.ref[0])
692
699
  const ks = Object.keys(expandedEntity.keys).filter(
693
700
  c => !expandedEntity.keys[c].isAssociation && !(c in DRAFT_COLUMNS_MAP)
694
701
  )
@@ -968,6 +975,23 @@ class JoinCQNFromExpanded {
968
975
  return column
969
976
  }
970
977
 
978
+ if (Array.isArray(column.xpr)) {
979
+ return this._buildNewAliasColumn(
980
+ {
981
+ ...column,
982
+ xpr: column.xpr.map(x => {
983
+ if (x.ref) {
984
+ const res = this._buildNewAliasColumn(x, entity, tableAlias, mappings)
985
+ delete res.as
986
+ return res
987
+ } else return x
988
+ })
989
+ },
990
+ entity,
991
+ tableAlias,
992
+ mappings
993
+ )
994
+ }
971
995
  return this._buildNewAliasColumn(column, entity, tableAlias, mappings)
972
996
  }
973
997
 
@@ -1146,7 +1170,8 @@ class JoinCQNFromExpanded {
1146
1170
  readToOneCQN[IS_ACTIVE] ||
1147
1171
  (element._isAssociationStrict && !element['@odata.draft.enclosed']) ||
1148
1172
  !this._csn.definitions[colTarget]._isDraftEnabled
1149
- const ref = this._getJoinRef(entity.elements, colRef[0], expandActive, defaultLanguageThis)
1173
+
1174
+ const ref = this._refFromRefByExpand(column.ref[0], colTarget, defaultLanguageThis, expandActive)
1150
1175
  const tableAlias = this._createAlias(toManyTree.concat(colRef).join(':'))
1151
1176
  const on = entity._relations[colRef[0]].join(tableAlias, 'filterExpand')
1152
1177
  const filterExpand = this._getFilterExpandCQN(readToOneCQN, on, parentAlias, entity.keys)
@@ -1239,14 +1264,6 @@ class JoinCQNFromExpanded {
1239
1264
  return cqn
1240
1265
  }
1241
1266
 
1242
- _getJoinRef(elements, column, isActive, defaultLanguage) {
1243
- const assoc = elements[column]
1244
- if (typeof isActive !== 'boolean' || isActive || !assoc.isComposition) {
1245
- return defaultLanguage ? ensureUnlocalized(assoc.target) : assoc.target
1246
- }
1247
- return assoc.target + '_drafts'
1248
- }
1249
-
1250
1267
  _isPathExpressionToOne(ref, entity) {
1251
1268
  const ref0 = ref[0]
1252
1269
  const el = entity.elements[ref0]
@@ -18,8 +18,6 @@ const propagateForeignKeys = require('../../common/utils/propagateForeignKeys')
18
18
  const getTemplate = require('../../common/utils/template')
19
19
  const templateProcessor = require('../../common/utils/templateProcessor')
20
20
 
21
- const { checkIfAssocDeep } = require('../../cds-services/util/assert')
22
-
23
21
  const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
24
22
 
25
23
  const _isManaged = (category, event) =>
@@ -85,7 +83,12 @@ const _processCategory = (req, category, { row, key, element }) => {
85
83
  if (k.isAssociation || k.name === 'IsActiveEntity') continue
86
84
 
87
85
  if (!k.isUUID && !(k.name in val)) {
88
- req.error(400, 'ASSERT_NOT_NULL', key + '[' + k + ']', [key + '[' + k + ']'])
86
+ req.error({
87
+ code: 'MUST_NOT_BE_NULL',
88
+ message: 'Value is required',
89
+ target: key + '[' + k + ']',
90
+ args: [key + '[' + k + ']']
91
+ })
89
92
  return
90
93
  }
91
94
  }
@@ -93,7 +96,7 @@ const _processCategory = (req, category, { row, key, element }) => {
93
96
 
94
97
  // not null without default (for better error message)
95
98
  if (category === '!default' && val == null && req.event === 'CREATE') {
96
- req.error(400, 'ASSERT_NOT_NULL', key, [key])
99
+ req.error({ code: 'MUST_NOT_BE_NULL', message: 'Value is required', target: key, args: [key] })
97
100
  return
98
101
  }
99
102
 
@@ -101,11 +104,6 @@ const _processCategory = (req, category, { row, key, element }) => {
101
104
  if (category === 'uuid' && !val && req.event === 'CREATE') {
102
105
  row[key] = cds.utils.uuid()
103
106
  }
104
-
105
- // check for forbidden deep operations for association
106
- if (category === 'associationEffective' && (req.event === 'CREATE' || req.event === 'UPDATE')) {
107
- checkIfAssocDeep(element, val, req)
108
- }
109
107
  }
110
108
 
111
109
  const _processorFn = req => elementInfo => {
@@ -152,10 +150,6 @@ const _pickCRUD = element => {
152
150
  categories.push({ category: '@cds.on.update', args: element['@cds.on.update'] })
153
151
  }
154
152
 
155
- if (element._isAssociationStrict && !element._target._hasPersistenceSkip) {
156
- categories.push('associationEffective')
157
- }
158
-
159
153
  // REVISIT: element._foreignKeys.length seems to be a very broad check
160
154
  if (element.isAssociation && element._foreignKeys.length) {
161
155
  categories.push({ category: 'propagateForeignKeys' })