@sap/cds 6.6.2 → 6.7.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 (130) hide show
  1. package/CHANGELOG.md +59 -2
  2. package/README.md +1 -1
  3. package/apis/connect.d.ts +11 -4
  4. package/apis/core.d.ts +1 -1
  5. package/apis/csn.d.ts +1 -0
  6. package/apis/internal/inference.d.ts +15 -2
  7. package/apis/log.d.ts +10 -0
  8. package/apis/serve.d.ts +4 -9
  9. package/apis/services.d.ts +86 -19
  10. package/bin/build/buildTaskEngine.js +16 -42
  11. package/bin/build/constants.js +4 -2
  12. package/bin/build/provider/buildTaskProviderInternal.js +117 -85
  13. package/bin/build/provider/hana/index.js +6 -1
  14. package/bin/build/provider/mtx-extension/index.js +74 -34
  15. package/bin/build/provider/mtx-sidecar/index.js +3 -3
  16. package/bin/build/provider/nodejs/index.js +2 -2
  17. package/bin/build/util.js +63 -14
  18. package/bin/cds-serve.js +6 -0
  19. package/bin/cds.js +20 -4
  20. package/bin/deploy/to-hana/cfUtil.js +15 -1
  21. package/bin/mtx/in-cds.js +2 -9
  22. package/bin/plugins.js +31 -0
  23. package/bin/serve.js +12 -12
  24. package/lib/compile/etc/_localized.js +1 -1
  25. package/lib/compile/for/lean_drafts.js +22 -6
  26. package/lib/compile/for/nodejs.js +4 -1
  27. package/lib/compile/load.js +4 -2
  28. package/lib/core/index.js +35 -15
  29. package/lib/dbs/cds-deploy.js +129 -133
  30. package/lib/env/cds-env.js +25 -17
  31. package/lib/env/cds-requires.js +10 -40
  32. package/lib/env/compat.js +12 -0
  33. package/lib/env/defaults.js +17 -9
  34. package/lib/env/plugins.js +29 -0
  35. package/lib/env/schemas/cds-rc.json +14 -0
  36. package/lib/index.js +3 -0
  37. package/lib/log/cds-log.js +7 -4
  38. package/lib/ql/CREATE.js +1 -1
  39. package/lib/ql/DELETE.js +1 -1
  40. package/lib/ql/DROP.js +3 -3
  41. package/lib/ql/INSERT.js +1 -1
  42. package/lib/ql/Query.js +14 -6
  43. package/lib/ql/SELECT.js +8 -2
  44. package/lib/ql/UPDATE.js +1 -1
  45. package/lib/ql/Whereable.js +1 -1
  46. package/lib/ql/cds-ql.js +1 -9
  47. package/lib/req/cds-context.js +1 -4
  48. package/lib/req/request.js +63 -2
  49. package/lib/req/response.js +3 -2
  50. package/lib/srv/bindings.js +69 -71
  51. package/lib/srv/cds-connect.js +4 -1
  52. package/lib/srv/cds-serve.js +4 -0
  53. package/lib/srv/middlewares/index.js +37 -6
  54. package/lib/srv/protocols/_legacy.js +1 -1
  55. package/lib/srv/protocols/index.js +1 -1
  56. package/lib/srv/srv-api.js +4 -6
  57. package/lib/srv/srv-dispatch.js +4 -3
  58. package/lib/srv/srv-handlers.js +1 -1
  59. package/lib/srv/srv-methods.js +8 -2
  60. package/lib/utils/cds-test.js +4 -1
  61. package/libx/_runtime/audit/Service.js +8 -9
  62. package/libx/_runtime/audit/generic/personal/index.js +1 -1
  63. package/libx/_runtime/audit/generic/personal/utils.js +1 -1
  64. package/libx/_runtime/audit/utils/v2.js +17 -20
  65. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -0
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +11 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +0 -1
  68. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +3 -3
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  70. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +4 -4
  71. package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +2 -2
  72. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +1 -1
  73. package/libx/_runtime/cds-services/services/Service.js +1 -1
  74. package/libx/_runtime/cds-services/util/assert.js +41 -65
  75. package/libx/_runtime/common/code-ext/WorkerPool.js +90 -0
  76. package/libx/_runtime/common/code-ext/WorkerReq.js +0 -4
  77. package/libx/_runtime/common/code-ext/execute.js +28 -18
  78. package/libx/_runtime/common/code-ext/handlers.js +5 -4
  79. package/libx/_runtime/common/code-ext/worker.js +45 -3
  80. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +8 -7
  81. package/libx/_runtime/common/composition/delete.js +1 -1
  82. package/libx/_runtime/common/composition/update.js +3 -5
  83. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  84. package/libx/_runtime/common/generic/auth/readOnly.js +5 -4
  85. package/libx/_runtime/common/generic/auth/restrict.js +7 -2
  86. package/libx/_runtime/common/generic/crud.js +12 -1
  87. package/libx/_runtime/common/generic/etag.js +11 -3
  88. package/libx/_runtime/common/generic/input.js +8 -6
  89. package/libx/_runtime/common/generic/paging.js +25 -8
  90. package/libx/_runtime/common/generic/put.js +1 -1
  91. package/libx/_runtime/common/generic/sorting.js +0 -1
  92. package/libx/_runtime/common/i18n/messages.properties +1 -0
  93. package/libx/_runtime/common/utils/cqn.js +5 -1
  94. package/libx/_runtime/common/utils/cqn2cqn4sql.js +2 -2
  95. package/libx/_runtime/common/utils/resolveView.js +14 -10
  96. package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -3
  97. package/libx/_runtime/common/utils/templateProcessor.js +15 -17
  98. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +18 -6
  99. package/libx/_runtime/db/Service.js +1 -0
  100. package/libx/_runtime/db/data-conversion/post-processing.js +0 -18
  101. package/libx/_runtime/db/expand/expand-v2.js +2 -2
  102. package/libx/_runtime/db/expand/rawToExpanded.js +6 -6
  103. package/libx/_runtime/db/generic/integrity.js +1 -1
  104. package/libx/_runtime/db/utils/columns.js +5 -5
  105. package/libx/_runtime/fiori/generic/activate.js +3 -3
  106. package/libx/_runtime/fiori/generic/edit.js +1 -1
  107. package/libx/_runtime/fiori/generic/new.js +4 -0
  108. package/libx/_runtime/fiori/lean-draft.js +138 -46
  109. package/libx/_runtime/hana/execute.js +3 -1
  110. package/libx/_runtime/hana/pool.js +10 -2
  111. package/libx/_runtime/messaging/common-utils/AMQPClient.js +6 -1
  112. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  113. package/libx/_runtime/remote/Service.js +16 -13
  114. package/libx/_runtime/remote/utils/client.js +6 -1
  115. package/libx/_runtime/sqlite/Service.js +5 -59
  116. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
  117. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -2
  118. package/libx/_runtime/sqlite/execute.js +3 -1
  119. package/libx/_runtime/types/api.js +12 -3
  120. package/libx/odata/afterburner.js +36 -0
  121. package/libx/odata/cqn2odata.js +1 -1
  122. package/libx/odata/grammar.pegjs +5 -3
  123. package/libx/odata/parser.js +1 -1
  124. package/libx/odata/utils.js +1 -1
  125. package/libx/rest/RestAdapter.js +1 -1
  126. package/libx/rest/RestRequest.js +1 -0
  127. package/package.json +5 -2
  128. package/libx/_runtime/common/code-ext/workerQuery.js +0 -45
  129. package/libx/_runtime/common/constants/limit.js +0 -12
  130. package/libx/_runtime/common/utils/page.js +0 -39
@@ -142,13 +142,18 @@ const _getRestrictionForTarget = (resolvedApplicables, target) => {
142
142
  }
143
143
 
144
144
  const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
145
- if (!cds.env.features.lean_draft && req.target._isDraftEnabled) {
145
+ if (!cds.env.fiori.lean_draft && req.target._isDraftEnabled) {
146
146
  req.query._draftRestrictions = resolvedApplicables
147
147
  return
148
148
  }
149
149
 
150
150
  if (typeof req.query.SELECT.from === 'object')
151
- req.query.SELECT.from.ref = _addWheresToRef(req.query.SELECT.from.ref, model, resolvedApplicables)
151
+ // in case of $apply take a ref from sub SELECT//
152
+ req.query.SELECT.from.ref = _addWheresToRef(
153
+ req.query.SELECT.from.ref || req.query.SELECT.from.SELECT?.from?.ref,
154
+ model,
155
+ resolvedApplicables
156
+ )
152
157
 
153
158
  const restrictionForTarget = _getRestrictionForTarget(resolvedApplicables, req.target)
154
159
  if (!restrictionForTarget) return
@@ -54,10 +54,21 @@ exports.impl = cds.service.impl(function () {
54
54
 
55
55
  if (req.event in { DELETE: 1, UPDATE: 1 } && req.target && req.target._isSingleton) {
56
56
  if (req.event === 'DELETE' && !req.target['@odata.singleton.nullable']) req.reject(400, 'SINGLETON_NOT_NULLABLE')
57
+ const selectSingleton = SELECT.one(req.target)
57
58
  const keyColumns = getColumns(req.target, { onlyNames: true, keysOnly: true })
58
- const selectSingleton = SELECT.one(req.target).columns(keyColumns)
59
+
60
+ // if no keys available, select all columns so we can delete the singleton with same content
61
+ if (keyColumns.length) selectSingleton.columns(keyColumns)
62
+
59
63
  const singleton = await cds.tx(req).run(selectSingleton)
60
64
  if (!singleton) req.reject(404)
65
+
66
+ // REVISIT: Workaround for singleton, to get keys into singleton
67
+ for (const keyName in singleton) {
68
+ if (!keyColumns.includes(keyName)) continue
69
+ req.data[keyName] = singleton[keyName]
70
+ }
71
+
61
72
  req.query.where(singleton)
62
73
  }
63
74
 
@@ -6,8 +6,7 @@ const { isActiveEntityRequested } = require('../../fiori/utils/where')
6
6
  const { ensureDraftsSuffix } = require('../../fiori/utils/handler')
7
7
  const { cqn2cqn4sql } = require('../../common/utils/cqn2cqn4sql')
8
8
  const { isAsteriskColumn } = require('../../common/utils/rewriteAsterisks')
9
- const ODataRequest = require('../../cds-services/adapter/odata-v4/ODataRequest')
10
- const { resolveView, getTransition } = require('../utils/resolveView')
9
+ const { resolveView } = require('../utils/resolveView')
11
10
 
12
11
  const C_U_ = {
13
12
  CREATE: 1,
@@ -61,8 +60,17 @@ const _addEtagColumns = (columns, entity) => {
61
60
  const _isConcurrentODataReq = req => {
62
61
  const isReadAfterDraftAction =
63
62
  req.event === 'READ' && req.target._isDraftEnabled && req.context.event in { draftActivate: 1, EDIT: 1 }
63
+ // It's allowed to also delete drafts when actives are deleted
64
+ if (
65
+ cds.env.fiori?.lean_draft &&
66
+ req.event === 'READ' &&
67
+ req.context.event === 'DELETE' &&
68
+ req.target?.name.endsWith('.drafts') &&
69
+ !req.context?.target?.name.endsWith('.drafts')
70
+ )
71
+ return
64
72
  const _req = isReadAfterDraftAction ? req.context : req
65
- return _req instanceof ODataRequest && _req.isConcurrentResource
73
+ return _req._isOData && _req.isConcurrentResource
66
74
  }
67
75
 
68
76
  /**
@@ -86,7 +86,7 @@ const _preProcessAssertTarget = (assocInfo, assertMap) => {
86
86
  if (parentKeys.length === 0) return
87
87
 
88
88
  foreignKeys.forEach(keyMap => {
89
- const clonedAssocInfo = Object.assign({}, assocInfo, { pathSegments: assocInfo.pathSegments.slice(0) })
89
+ const clonedAssocInfo = Object.assign({}, assocInfo, { pathSegmentsInfo: assocInfo.pathSegmentsInfo.slice(0) })
90
90
  const target = {
91
91
  key: mapKey,
92
92
  entity: assocTarget,
@@ -126,6 +126,8 @@ const _processCategory = (req, category, value, elementInfo, assertMap) => {
126
126
  // > preserve computed values if triggered by draftActivate and not managed
127
127
  return
128
128
  }
129
+ // Always take over the values from active entities
130
+ if (cds.env.fiori?.lean_draft && req.context?.event === 'EDIT') return
129
131
 
130
132
  delete row[key]
131
133
  value.val = undefined
@@ -152,7 +154,7 @@ const _getProcessorFn = (req, errors, assertMap) => {
152
154
  const event = req.event
153
155
 
154
156
  return elementInfo => {
155
- const { row, key, element, plain, pathSegments } = elementInfo
157
+ const { row, key, element, plain, pathSegmentsInfo } = elementInfo
156
158
  // ugly pointer passing for sonar
157
159
  const value = { mandatory: false, val: row && row[key] }
158
160
 
@@ -163,7 +165,7 @@ const _getProcessorFn = (req, errors, assertMap) => {
163
165
  if (_shouldSuppressErrorPropagation(event, value)) return
164
166
 
165
167
  // REVISIT: Convert checkInputConstraints to template mechanism
166
- checkInputConstraints({ element, value: value.val, errors, pathSegments, event })
168
+ checkInputConstraints({ element, value: value.val, errors, pathSegmentsInfo, event })
167
169
  }
168
170
  }
169
171
 
@@ -239,7 +241,7 @@ async function commonGenericInput(req) {
239
241
  const pathOptions = {
240
242
  rowUUIDGenerator: getRowUUIDGeneratorFn(req.event),
241
243
  includeKeyValues: true,
242
- pathSegments: []
244
+ pathSegmentsInfo: []
243
245
  }
244
246
 
245
247
  const boundAction = _getBoundAction(req)
@@ -247,7 +249,7 @@ async function commonGenericInput(req) {
247
249
  if (boundAction) {
248
250
  const pathSegment = _getBoundActionBindingParameter(boundAction)
249
251
  const keys = req._ && req._.params && req._.params[0]
250
- if (pathSegment) pathOptions.pathSegments.push(pathSegment)
252
+ if (pathSegment) pathOptions.pathSegmentsInfo.push(pathSegment)
251
253
 
252
254
  if (keys && 'IsActiveEntity' in keys) {
253
255
  pathOptions.draftKeys = { IsActiveEntity: keys.IsActiveEntity }
@@ -364,7 +366,7 @@ commonGenericInput._initial = true
364
366
  _actionFunctionHandler._initial = true
365
367
 
366
368
  module.exports = cds.service.impl(function () {
367
- if (cds.env.features.lean_draft) {
369
+ if (cds.env.fiori.lean_draft) {
368
370
  this.before(['CREATE', 'UPDATE'], '*', commonGenericInput)
369
371
  } else {
370
372
  this.before(['CREATE', 'UPDATE', 'NEW', 'PATCH'], '*', commonGenericInput)
@@ -1,5 +1,27 @@
1
1
  const cds = require('../../cds')
2
- const { getPageSize } = require('../utils/page')
2
+
3
+ module.exports = exports = cds.service.impl(function () {
4
+ commonGenericPaging._initial = true
5
+ this.before('READ', '*', commonGenericPaging)
6
+ })
7
+
8
+ const DEFAULT = cds.env.query?.limit?.default || 1000
9
+ const MAX = cds.env.query?.limit?.max || 1000
10
+ const _cached = Symbol('@cds.query.limit')
11
+
12
+ const getPageSize = def => {
13
+ if (_cached in def) return def[_cached]
14
+ let max = def['@cds.query.limit.max'] ?? def._service?.['@cds.query.limit.max'] ?? MAX
15
+ let _default =
16
+ def['@cds.query.limit.default'] ??
17
+ def['@cds.query.limit'] ??
18
+ def._service?.['@cds.query.limit.default'] ??
19
+ def._service?.['@cds.query.limit'] ??
20
+ DEFAULT
21
+ if (!max) max = Number.MAX_SAFE_INTEGER
22
+ if (!_default || _default > max) _default = max
23
+ return (def[_cached] = { default: _default, max })
24
+ }
3
25
 
4
26
  const commonGenericPaging = function (req) {
5
27
  // only if http request
@@ -21,10 +43,5 @@ const _addPaging = function ({ SELECT }, target) {
21
43
  if (SELECT.from.SELECT?.limit) _addPaging(SELECT.from, target)
22
44
  }
23
45
 
24
- /**
25
- * handler registration
26
- */
27
- module.exports = cds.service.impl(function () {
28
- commonGenericPaging._initial = true
29
- this.before('READ', '*', commonGenericPaging)
30
- })
46
+ exports.getPageSize = getPageSize
47
+ exports.commonGenericPaging = commonGenericPaging
@@ -24,7 +24,7 @@ const _fillStructure = (row, parts, element, category, args) => {
24
24
  }
25
25
 
26
26
  const _getProcessorFn = req => {
27
- const REST = req.constructor.name === 'RestRequest'
27
+ const REST = req._isRest
28
28
 
29
29
  return ({ row, key, element, plain }) => {
30
30
  if (!row || row[key] !== undefined) return
@@ -81,5 +81,4 @@ module.exports = cds.service.impl(function () {
81
81
  this.before('READ', '*', commonGenericSorting)
82
82
  })
83
83
 
84
- // REVISIT: remove (currently needed for test)
85
84
  module.exports.handler = commonGenericSorting
@@ -86,6 +86,7 @@ CRUD_VIA_NAVIGATION_NOT_SUPPORTED=CRUD via navigations is not yet supported
86
86
  # draft
87
87
  DRAFT_ALREADY_EXISTS=A draft for this entity already exists
88
88
  DRAFT_LOCKED_BY_ANOTHER_USER=The entity is locked by another user
89
+ DRAFT_MODIFICATION_ONLY_VIA_ROOT=A draft can only be modified via its root entity
89
90
 
90
91
  # singleton
91
92
  SINGLETON_NOT_NULLABLE=The singleton entity is not nullable
@@ -17,7 +17,11 @@ const getEntityNameFromDeleteCQN = cqn => {
17
17
  }
18
18
 
19
19
  const getEntityNameFromUpdateCQN = cqn => {
20
- return (cqn.UPDATE.entity.ref && cqn.UPDATE.entity.ref[0]) || cqn.UPDATE.entity.name || cqn.UPDATE.entity
20
+ return (
21
+ (cqn.UPDATE.entity.ref && cqn.UPDATE.entity.ref[0] && (cqn.UPDATE.entity.ref[0].id || cqn.UPDATE.entity.ref[0])) ||
22
+ cqn.UPDATE.entity.name ||
23
+ cqn.UPDATE.entity
24
+ )
21
25
  }
22
26
 
23
27
  // scope: simple wheres à la "[{ ref: ['foo'] }, '=', { val: 'bar' }, 'and', ... ]"
@@ -713,7 +713,7 @@ const _convertToOneEqNullInFilter = (query, target) => {
713
713
  }
714
714
  // eslint-disable-next-line complexity
715
715
  const _convertSelect = (query, model, _options) => {
716
- const _4db = _options.service instanceof cds.DatabaseService
716
+ const _4db = _options.service?.isDatabaseService
717
717
  const options = Object.assign({ _4db, isStreaming: query._streaming }, _options)
718
718
 
719
719
  // ensure query is ql enabled
@@ -796,7 +796,7 @@ const _convertSelect = (query, model, _options) => {
796
796
  if (options._4db && !query.SELECT.columns) {
797
797
  let target = query._target
798
798
  if (target && target._unresolved && typeof target.name === 'string') {
799
- target = model.definitions[ensureNoDraftsSuffix(target.name)] || target
799
+ target = model.definitions[cds.env.fiori.lean_draft ? target.name : ensureNoDraftsSuffix(target.name)] || target
800
800
  }
801
801
 
802
802
  if (target && !Object.prototype.hasOwnProperty.call(target, '_unresolved')) {
@@ -5,7 +5,7 @@ const PERSISTENCE_TABLE = '@cds.persistence.table'
5
5
  const { rewriteAsterisks } = require('../../common/utils/rewriteAsterisks')
6
6
 
7
7
  const getError = require('../error')
8
- const { getEntityNameFromDeleteCQN, getEntityNameFromUpdateCQN } = require('../utils/cqn')
8
+ const { getEntityNameFromDeleteCQN } = require('../utils/cqn')
9
9
 
10
10
  const _setInverseTransition = (mapping, ref, mapped) => {
11
11
  const existing = mapping.get(ref)
@@ -332,7 +332,7 @@ const _newUpdate = (query, transitions, service) => {
332
332
  newUpdate.where = _newWhere(
333
333
  newUpdate.where,
334
334
  targetTransition,
335
- getEntityNameFromUpdateCQN(query),
335
+ cds.infer(query, service.model.definitions).name,
336
336
  query.UPDATE.entity.as
337
337
  )
338
338
  }
@@ -353,7 +353,7 @@ const _newSelect = (query, transitions, service) => {
353
353
  if (!newSelect.columns && targetTransition.mapping.size) newSelect.columns = _initialColumns(targetTransition)
354
354
  if (newSelect.columns) {
355
355
  rewriteAsterisks({ SELECT: query.SELECT }, service.model, {
356
- _4db: service instanceof cds.DatabaseService || service.kind === 'better-sqlite',
356
+ _4db: service.isDatabaseService,
357
357
  target: targetTransition.queryTarget
358
358
  })
359
359
  newSelect.columns = _newColumns(newSelect.columns, targetTransition, service, service.kind !== 'app-service')
@@ -380,12 +380,16 @@ const _newInsert = (query, transitions, service) => {
380
380
  const targetTransition = transitions[transitions.length - 1]
381
381
  const targetName = targetTransition.target.name
382
382
  const newInsert = Object.create(query.INSERT)
383
- newInsert.into = newInsert.into.ref
384
- ? {
385
- ...newInsert.into,
386
- ref: _rewriteQueryPath(query.INSERT.into, transitions)
387
- }
388
- : targetName
383
+ if (newInsert.into) {
384
+ const refObject = newInsert.into.ref ? newInsert.into : { ref: [query.INSERT.into] }
385
+ newInsert.into = {
386
+ ...refObject,
387
+ ref: _rewriteQueryPath(refObject, transitions)
388
+ }
389
+ if (!query.INSERT.into.ref) newInsert.into = newInsert.into.ref[0] // leave as string
390
+ } else {
391
+ newInsert.into = targetName
392
+ }
389
393
  if (newInsert.columns) newInsert.columns = _newInsertColumns(newInsert.columns, targetTransition)
390
394
  if (newInsert.entries) newInsert.entries = _newEntries(newInsert.entries, targetTransition, service)
391
395
  Object.defineProperty(newInsert, '_transitions', {
@@ -536,7 +540,7 @@ const _getTransitionData = (target, columns, service, skipForbiddenViewCheck) =>
536
540
  if (!skipForbiddenViewCheck) _checkForForbiddenViews(target)
537
541
  const targetStartsWithSrvName = service.namespace && target.name.startsWith(`${service.namespace}.`)
538
542
  const persistenceTable = _isPersistenceTable(target)
539
- const isDatabaseService = service instanceof cds.DatabaseService || service.kind === 'better-sqlite'
543
+ const isDatabaseService = service.isDatabaseService
540
544
  columns = _queryColumns(target, columns, persistenceTable, !isDatabaseService && !targetStartsWithSrvName)
541
545
  // REVISIT: Change once we expose database service
542
546
  if (persistenceTable && isDatabaseService) {
@@ -29,7 +29,7 @@ const _expandColumn = (column, target, _4db) => {
29
29
  const rewriteExpandAsterisk = (columns, target) => {
30
30
  const expandAllColIdx = columns.findIndex(col => {
31
31
  if (col.ref || !col.expand) return
32
- return !Array.isArray(col.expand) ? col.expand === '*' : col.expand.indexOf('*') > -1
32
+ return col.expand.includes('*')
33
33
  })
34
34
  if (expandAllColIdx > -1) {
35
35
  const { expand } = columns.splice(expandAllColIdx, 1)[0]
@@ -56,7 +56,6 @@ const _rewriteAsterisk = (columns, target, _4db, isRoot) => {
56
56
  }
57
57
 
58
58
  const _rewriteAsterisks = (cqn, target, _4db, isRoot) => {
59
- if (cqn.expand === '*') cqn.expand = ['*']
60
59
  const columns = cqn.expand || cqn.columns
61
60
  _rewriteAsterisk(columns, target, _4db, isRoot)
62
61
  rewriteExpandAsterisk(columns, target)
@@ -119,7 +118,7 @@ const rewriteAsterisks = (query, model, options) => {
119
118
  if (!target) return
120
119
 
121
120
  query.SELECT.columns = getColumns(target, { _4db }).map(col => ({ ref: [col.name] }))
122
- if (_4db && target._isDraftEnabled && !cds.env.features.lean_draft)
121
+ if (_4db && target._isDraftEnabled && !cds.env.fiori.lean_draft)
123
122
  query.SELECT.columns.push(..._cqlDraftColumns(target))
124
123
  }
125
124
  }
@@ -1,19 +1,17 @@
1
1
  const DELIMITER = require('./templateDelimiter')
2
- const pathSerializer = require('./templateProcessorPathSerializer')
3
2
 
4
- const _processElement = (processFn, row, key, target, picked = {}, isRoot, pathSegments) => {
3
+ const _processElement = (processFn, row, key, target, picked = {}, isRoot, pathSegmentsInfo) => {
5
4
  const element = (target.elements || target.params)[key]
6
5
  const { plain } = picked
7
6
 
8
7
  if (!plain) return
9
8
 
10
- /**
11
- * @type import('../../types/api').templateElementInfo
12
- */
13
- const elementInfo = { row, key, element, target, plain, isRoot, pathSegments }
14
- if (!element && target._flat2struct && target._flat2struct[key] && elementInfo.pathSegments) {
15
- elementInfo.pathSegments = pathSegments.slice(0)
16
- elementInfo.pathSegments.push(...target._flat2struct[key])
9
+ /** @type import('../../types/api').templateElementInfo */
10
+ const elementInfo = { row, key, element, target, plain, isRoot, pathSegmentsInfo }
11
+
12
+ if (!element && target._flat2struct?.[key] && elementInfo.pathSegmentsInfo) {
13
+ elementInfo.pathSegmentsInfo = pathSegmentsInfo.slice(0)
14
+ elementInfo.pathSegmentsInfo.push(...target._flat2struct[key])
17
15
  }
18
16
 
19
17
  processFn(elementInfo)
@@ -23,7 +21,7 @@ const _processRow = (processFn, row, template, tKey, tValue, isRoot, pathOptions
23
21
  const { template: subTemplate, picked } = tValue
24
22
  const key = tKey.split(DELIMITER).pop()
25
23
 
26
- _processElement(processFn, row, key, template.target, picked, isRoot, pathOptions.pathSegments)
24
+ _processElement(processFn, row, key, template.target, picked, isRoot, pathOptions.pathSegmentsInfo)
27
25
 
28
26
  // process deep
29
27
  if (subTemplate && typeof row === 'object' && row) {
@@ -48,20 +46,20 @@ const _processComplex = (processFn, row, template, key, pathOptions) => {
48
46
  if (rows.length === 0) return
49
47
  const keyNames = pathOptions.includeKeyValues && _getTargetKeyNames(template.target)
50
48
 
51
- for (let idx = 0; idx < rows.length; idx++) {
52
- const row = rows[idx]
49
+ for (const row of rows) {
53
50
  if (row == null) continue
54
51
  const args = { processFn, row, template, isRoot: false, pathOptions }
55
52
 
56
- let pathSegment
53
+ let pathSegmentInfo
57
54
  if (pathOptions.includeKeyValues) {
58
- if (pathOptions.rowUUIDGenerator) pathOptions.rowUUIDGenerator(keyNames, row, template)
59
- pathSegment = pathSerializer(key, keyNames, row, template.target.elements, pathOptions.draftKeys)
55
+ pathOptions.rowUUIDGenerator?.(keyNames, row, template)
56
+ /** @type import('../../types/api').pathSegmentInfo */
57
+ pathSegmentInfo = { key, keyNames, row, elements: template.target.elements, draftKeys: pathOptions.draftKeys }
60
58
  }
61
59
 
62
- if (pathOptions.pathSegments) pathOptions.pathSegments.push(pathSegment || key)
60
+ if (pathOptions.pathSegmentsInfo) pathOptions.pathSegmentsInfo.push(pathSegmentInfo || key)
63
61
  templateProcessor(args)
64
- if (pathOptions.pathSegments) pathOptions.pathSegments.pop()
62
+ if (pathOptions.pathSegmentsInfo) pathOptions.pathSegmentsInfo.pop()
65
63
  }
66
64
  }
67
65
 
@@ -1,10 +1,10 @@
1
1
  const cds = require('../../cds')
2
- const templatePathSerializer = (tKey, keyNames, row, elements, draftKeys) => {
2
+
3
+ const segmentSerializer = pathSegmentInfo => {
4
+ const { key: tKey, row, elements, draftKeys } = pathSegmentInfo
5
+ let keyNames = pathSegmentInfo.keyNames
6
+
3
7
  const keyValuePairs = keyNames
4
- .filter(key => {
5
- if (cds.env.features.lean_draft && key === 'IsActiveEntity') return false
6
- return true
7
- })
8
8
  .map(key => {
9
9
  let quote
10
10
 
@@ -19,11 +19,23 @@ const templatePathSerializer = (tKey, keyNames, row, elements, draftKeys) => {
19
19
  }
20
20
 
21
21
  const keyValue = row[key] ?? draftKeys?.[key]
22
+ if (keyValue == null) return
22
23
  return `${key}=${quote}${keyValue}${quote}`
23
24
  })
25
+ .filter(c => c)
24
26
 
25
27
  const keyValuePairsSerialized = keyValuePairs.join(',')
26
- return `${tKey}(${keyValuePairsSerialized})`
28
+ const pathSegment = `${tKey}(${keyValuePairsSerialized})`
29
+ return pathSegment
30
+ }
31
+
32
+ const templatePathSerializer = (elementName, pathSegmentsInfo) => {
33
+ const pathSegments = pathSegmentsInfo.map(pathSegmentInfo => {
34
+ if (typeof pathSegmentInfo === 'string') return pathSegmentInfo
35
+ return segmentSerializer(pathSegmentInfo)
36
+ })
37
+ const path = `${pathSegments.join('/')}${pathSegments.length ? '/' : ''}${elementName}`
38
+ return path
27
39
  }
28
40
 
29
41
  module.exports = templatePathSerializer
@@ -138,4 +138,5 @@ class DatabaseService extends cds.Service {
138
138
  }
139
139
  }
140
140
 
141
+ DatabaseService.prototype.isDatabaseService = true
141
142
  module.exports = DatabaseService
@@ -1,7 +1,5 @@
1
1
  const { getEntityNameFromCQN, traverseFroms } = require('../../common/utils/entityFromCqn')
2
2
  const { ensureNoDraftsSuffix } = require('../../common/utils/draft')
3
- const { proxifyIfFlattened } = require('../../../common/utils/ucsn')
4
- const cds = require('../../../../lib')
5
3
 
6
4
  const _getCastFunction = ({ type }) => {
7
5
  switch (type) {
@@ -34,13 +32,6 @@ const _getNestedElement = (entity, key) => {
34
32
  return _structElement(entity, structPath)
35
33
  }
36
34
 
37
- const _structure = csnEntity => (value, key, row, unaliasedKey) => {
38
- proxifyIfFlattened(csnEntity, row)
39
- const effectiveKey = unaliasedKey || key
40
- delete row[effectiveKey]
41
- row[effectiveKey] = value
42
- }
43
-
44
35
  const _addConverter = (mapper, name, converter) => {
45
36
  if (mapper.has(name)) {
46
37
  const oldConverter = mapper.get(name)
@@ -126,15 +117,6 @@ const _getMapperForListedElements = (conversionMap, csn, cqn) => {
126
117
  row[effectiveKey] = JSON.parse(val)
127
118
  }) // > arrayed elements
128
119
  }
129
-
130
- if (
131
- cds.env.features.ucsn_struct_conversion &&
132
- element.parent &&
133
- element.parent._isStructured &&
134
- !element._isStructured
135
- ) {
136
- _addConverter(mapper, col.as ? col.as : name, _structure(entity))
137
- }
138
120
  }
139
121
  }
140
122
 
@@ -119,7 +119,7 @@ const _addForeignKeys = (columns, entity, options) => {
119
119
  * @returns object
120
120
  */
121
121
  const expandV2 = async (model, dbc, query, user, locale, txTimestamp, executeSelectCQN) => {
122
- const expandColumn = query.SELECT.columns.find(c => c.expand && typeof c.expand === 'string')
122
+ const expandColumn = query.SELECT.columns.find(c => c.expand && typeof c.expand[0] === 'string')
123
123
  const options = Object.assign({ onlyKeys: false, onlyCompositions: false }, expandColumn._options)
124
124
  // remove expand columns from query without modifying
125
125
  const topLevelSelect = query.clone().columns(query.SELECT.columns.filter(c => !c.expand))
@@ -137,7 +137,7 @@ const expandV2 = async (model, dbc, query, user, locale, txTimestamp, executeSel
137
137
 
138
138
  // _associations contains compositions and associations
139
139
  if (entity._associations) {
140
- const depth = expandColumn.expand === '**' ? -1 : Number(expandColumn.expand.replace('*', ''))
140
+ const depth = expandColumn.expand[0] === '**' ? -1 : Number(expandColumn.expand[0].replace('*', ''))
141
141
  await _autoExpandNavsAndAttachToResult(entity, Array.isArray(result) ? result : [result], depth, options)
142
142
  }
143
143
 
@@ -111,7 +111,7 @@ class RawToExpanded {
111
111
  let expandedItems = this._getResultCache(toManyTree.concat(key))[mapping[GET_KEY_VALUE](false, entry)] || []
112
112
 
113
113
  // the expanded items may include the actives of the deleted drafts -> filter out
114
- if (!cds.env.features.lean_draft && rootIsActiveEntity !== null) {
114
+ if (!cds.env.fiori.lean_draft && rootIsActiveEntity !== null) {
115
115
  if (mapping[TO_ACTIVE]) expandedItems = expandedItems.filter(ele => ele.IsActiveEntity !== false)
116
116
  else expandedItems = expandedItems.filter(ele => !!ele.IsActiveEntity === rootIsActiveEntity)
117
117
  }
@@ -144,7 +144,7 @@ class RawToExpanded {
144
144
  else if (rootIsActiveEntity) row[key] = parsed && parsed.IsActiveEntity !== false ? parsed : null
145
145
  else row[key] = parsed && parsed.IsActiveEntity === rootIsActiveEntity ? parsed : null
146
146
  }
147
- if (mapping[CLEANUP_KEYS]) {
147
+ if (parsed && mapping[CLEANUP_KEYS]) {
148
148
  for (const key in mapping[CLEANUP_KEYS]) delete parsed[key]
149
149
  }
150
150
  } else {
@@ -153,7 +153,7 @@ class RawToExpanded {
153
153
  // Assume a DB will not return undefined, but always null
154
154
  this._convertValue(rawValue, conversionMapper.get(mapping), mapping, row, key)
155
155
 
156
- isEntityNull = this._isNull(isEntityNull, rawValue)
156
+ isEntityNull = this._isNull(isEntityNull, rawValue, key)
157
157
  }
158
158
  }
159
159
 
@@ -173,12 +173,12 @@ class RawToExpanded {
173
173
  * @returns {boolean}
174
174
  * @private
175
175
  */
176
- _isNull(isEntityNull, value) {
176
+ _isNull(isEntityNull, value, key) {
177
177
  if (isEntityNull === undefined) {
178
- return value === null || value === undefined
178
+ return value === null || value === undefined || key === 'IsActiveEntity'
179
179
  }
180
180
 
181
- return isEntityNull === true && (value === null || value === undefined)
181
+ return isEntityNull === true && (value === null || value === undefined || key === 'IsActiveEntity')
182
182
  }
183
183
 
184
184
  /**
@@ -333,7 +333,7 @@ const _checkReferenceIntegrity = (entity, data, req, csn, run) => {
333
333
  }
334
334
 
335
335
  const _checkIntegrityWrapper = (req, csn, run) => async (data, entity) => {
336
- if (cds.env.features.lean_draft && entity.name?.endsWith('.drafts')) return
336
+ if (cds.env.fiori.lean_draft && entity.name?.endsWith('.drafts')) return
337
337
  const errors = await _checkReferenceIntegrity(entity, data, req, csn, run)
338
338
  if (errors && errors.length !== 0) for (const err of errors) req.error(err)
339
339
  }
@@ -16,15 +16,15 @@ const getColumns = (entity, { _4db, onlyKeys } = { _4db: true, onlyKeys: false }
16
16
  if (!(entity && entity.elements)) return []
17
17
  const columnNames = []
18
18
  // REVISIT!!!
19
- const elements = cds.env.features.lean_draft
20
- ? entity.elements
21
- : Object.getPrototypeOf(entity.elements) || entity.elements
19
+ const { structs = cds.env.features.ucsn_struct_conversion } = cds.env.effective.odata
20
+ const { lean_draft } = cds.env.fiori
21
+ const elements = lean_draft ? entity.elements : Object.getPrototypeOf(entity.elements) || entity.elements
22
22
  for (const elementName in elements) {
23
23
  const element = elements[elementName]
24
24
  if (onlyKeys && !element.key) continue
25
25
  if (element.isAssociation) continue
26
- if (!cds.env.features.lean_draft && _4db && entity._isDraftEnabled && elementName in DRAFT_COLUMNS_MAP) continue
27
- if ((cds.env.effective.odata.structs || cds.env.features.ucsn_struct_conversion) && element.elements) {
26
+ if (!lean_draft && _4db && entity._isDraftEnabled && elementName in DRAFT_COLUMNS_MAP) continue
27
+ if (structs && element.elements) {
28
28
  columnNames.push(...resolveStructured({ element, structProperties: [] }, false))
29
29
  continue
30
30
  }
@@ -146,8 +146,8 @@ const fioriGenericActivate = async function (req) {
146
146
 
147
147
  // REVISIT: should not be necessary
148
148
  r._ = Object.assign(r._, req._)
149
- r.getUriInfo = () => req.getUriInfo()
150
- r.getUrlObject = () => req.getUrlObject()
149
+ if (req.getUriInfo) r.getUriInfo = () => req.getUriInfo()
150
+ if (req.getUrlObject) r.getUrlObject = () => req.getUrlObject()
151
151
  r._.params = req.params
152
152
  r._.query = req.query
153
153
 
@@ -178,7 +178,7 @@ const fioriGenericActivate = async function (req) {
178
178
 
179
179
  // REVISIT: we need to use okra API here because it must be set in the batched request
180
180
  // status code must be set in handler to allow overriding for FE V2
181
- req?._?.odataRes.setStatusCode(201)
181
+ req?._?.odataRes?.setStatusCode(201)
182
182
 
183
183
  return result
184
184
  }
@@ -164,7 +164,7 @@ const fioriGenericEdit = async function (req) {
164
164
 
165
165
  // REVISIT: we need to use okra API here because it must be set in the batched request
166
166
  // status code must be set in handler to allow overriding for FE V2
167
- req?._?.odataRes.setStatusCode(201)
167
+ req?._?.odataRes?.setStatusCode(201)
168
168
 
169
169
  return results[0][0]
170
170
  }
@@ -56,6 +56,10 @@ const fioriGenericNew = async function (req, next) {
56
56
 
57
57
  if (!cds.db) req.reject('NO_DATABASE_CONNECTION')
58
58
 
59
+ const isRoot = typeof req.query.INSERT.into === 'string' || req.query.INSERT.into.ref?.length === 1
60
+ // Only allowed for pseudo draft roots (entities with this action)
61
+ if (isRoot && !req.target['@Common.DraftRoot.ActivationAction']) req.reject(403, 'DRAFT_MODIFICATION_ONLY_VIA_ROOT')
62
+
59
63
  const navigationToMany = isNavigationToMany(req)
60
64
 
61
65
  const adminDataCQN = navigationToMany