@sap/cds 6.3.1 → 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 (114) hide show
  1. package/CHANGELOG.md +87 -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 +4 -1
  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/parse.js +2 -1
  45. package/lib/compile/to/gql.js +1 -1
  46. package/lib/compile/to/sql.js +11 -1
  47. package/lib/core/entities.js +1 -1
  48. package/lib/core/index.js +8 -9
  49. package/lib/core/infer.js +1 -0
  50. package/lib/dbs/cds-deploy.js +97 -41
  51. package/lib/env/cds-env.js +9 -10
  52. package/lib/env/cds-requires.js +8 -2
  53. package/lib/env/defaults.js +0 -4
  54. package/lib/env/schemas/cds-rc.json +38 -0
  55. package/lib/ql/SELECT.js +10 -4
  56. package/lib/srv/bindings.js +1 -1
  57. package/lib/srv/factory.js +1 -1
  58. package/lib/srv/protocols/index.js +3 -1
  59. package/lib/srv/srv-methods.js +1 -1
  60. package/lib/utils/cds-utils.js +11 -0
  61. package/lib/utils/inflect.js +13 -12
  62. package/lib/utils/tar.js +53 -10
  63. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -2
  64. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  65. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -15
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +1 -1
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  70. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/errors/UriSyntaxError.js +1 -1
  71. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +6 -1
  72. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +1 -1
  73. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/ConditionalRequestValidator.js +0 -12
  74. package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +1 -7
  75. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +4 -0
  76. package/libx/_runtime/cds-services/services/Service.js +23 -1
  77. package/libx/_runtime/cds-services/util/assert.js +0 -41
  78. package/libx/_runtime/common/composition/data.js +5 -1
  79. package/libx/_runtime/common/generic/auth/utils.js +3 -3
  80. package/libx/_runtime/common/generic/input.js +4 -24
  81. package/libx/_runtime/common/generic/paging.js +3 -3
  82. package/libx/_runtime/common/utils/csn.js +21 -15
  83. package/libx/_runtime/common/utils/draft.js +2 -1
  84. package/libx/_runtime/common/utils/resolveView.js +25 -4
  85. package/libx/_runtime/common/utils/rewriteAsterisks.js +3 -1
  86. package/libx/_runtime/common/utils/rowUUIDGenerator.js +21 -0
  87. package/libx/_runtime/common/utils/templateProcessor.js +12 -15
  88. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +23 -0
  89. package/libx/_runtime/db/expand/expandCQNToJoin.js +29 -12
  90. package/libx/_runtime/db/generic/input.js +7 -13
  91. package/libx/_runtime/db/sql-builder/UpsertBuilder.js +47 -0
  92. package/libx/_runtime/db/sql-builder/index.js +2 -0
  93. package/libx/_runtime/db/sql-builder/sqlFactory.js +9 -0
  94. package/libx/_runtime/db/utils/columns.js +4 -2
  95. package/libx/_runtime/fiori/generic/read.js +1 -12
  96. package/libx/_runtime/fiori/lean-draft.js +657 -0
  97. package/libx/_runtime/fiori/utils/handler.js +1 -1
  98. package/libx/_runtime/hana/pool.js +16 -1
  99. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +2 -1
  100. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -1
  101. package/libx/_runtime/messaging/enterprise-messaging.js +2 -3
  102. package/libx/_runtime/messaging/outbox/utils.js +109 -70
  103. package/libx/_runtime/messaging/service.js +16 -7
  104. package/libx/_runtime/remote/Service.js +15 -2
  105. package/libx/_runtime/remote/utils/client.js +41 -11
  106. package/libx/_runtime/sqlite/Service.js +3 -0
  107. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +56 -0
  108. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +59 -0
  109. package/libx/_runtime/sqlite/customBuilder/index.js +5 -0
  110. package/libx/_runtime/sqlite/execute.js +1 -1
  111. package/libx/_runtime/types/api.js +2 -2
  112. package/libx/rest/RestAdapter.js +15 -13
  113. package/package.json +1 -1
  114. package/server.js +1 -0
@@ -179,7 +179,7 @@ const update = service => {
179
179
  await tx.rollback(e).catch(() => {})
180
180
  }
181
181
  } finally {
182
- req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req._.req))
182
+ req.messages && odataRes.setHeader('sap-messages', getSapMessages(req.messages, req.http.req))
183
183
 
184
184
  if (err) next(err)
185
185
  else if (primitive && result) {
@@ -37,7 +37,7 @@ UriSyntaxError.Message = {
37
37
  KEY_VALUE_NOT_FOUND: "No '%s' value found for key '%s'",
38
38
  PREVIOUS_TYPE_HAS_NO_MEDIA: "Previous segment type '%s' does not have a media resource",
39
39
  MUST_BE_COUNT_OR_BOUND_OPERATION: "Expected current segment '%s' to be '$count' or a bound operation",
40
- MUST_BE_COUNT_OR_REF_OR_BOUND_OPERATION: "Expected current segment '%s' to be '$count', '$ref', or a bound operation",
40
+ MUST_BE_COUNT_OR_REF_OR_BOUND_OPERATION: "Expected current segment '%s' to be '$count', '$ref', a bound operation or a key value with a proper type",
41
41
 
42
42
  ALIAS_NOT_FOUND: "Parameter alias '%s' not found",
43
43
  WRONG_ALIAS_VALUE: "Wrong value for parameter alias '%s'",
@@ -143,7 +143,12 @@ class UriParser {
143
143
  * @returns {UriInfo} the result of parsing
144
144
  */
145
145
  parseRelativeUri (uri, queryOptions) {
146
- let uriPathSegments = uri.split('/').map(decodeURIComponent)
146
+ let uriPathSegments
147
+ try {
148
+ uriPathSegments = uri.split('/').map(decodeURIComponent)
149
+ } catch (error) {
150
+ throw new UriSyntaxError('wrong percent encoding in uri: ' + uri)
151
+ }
147
152
 
148
153
  let uriInfo = new UriInfo()
149
154
 
@@ -1,6 +1,6 @@
1
1
  'use strict'
2
2
 
3
- const { AsyncResource } = require('node:async_hooks')
3
+ const { AsyncResource } = require('async_hooks')
4
4
  const Transform = require('stream').Transform
5
5
 
6
6
  /**
@@ -24,18 +24,6 @@ class ConditionalRequestValidator {
24
24
  if (method !== HttpMethods.GET && !ifMatch && !ifNoneMatch) throw new PreconditionRequiredError()
25
25
  return
26
26
  }
27
-
28
- if (ifMatch || ifNoneMatch) {
29
- // Careless clients send this also for DELETE and POST, other careless clients send the star in double-quotes.
30
- if ([HttpMethods.POST].includes(method)) {
31
- if (
32
- (ifMatch && ifMatch.trim() !== '*' && ifMatch.trim() !== '"*"') ||
33
- (ifNoneMatch && ifNoneMatch.trim() !== '*' && ifNoneMatch.trim() !== '"*"')
34
- ) {
35
- throw new PreconditionFailedError()
36
- }
37
- }
38
- }
39
27
  }
40
28
 
41
29
  /**
@@ -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