@sap/cds 6.6.2 → 6.7.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +72 -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 +22 -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 +23 -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/auth/strategies/mock.js +1 -1
  66. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -0
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +12 -5
  71. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -2
  72. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +5 -5
  73. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  74. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +4 -4
  75. package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +2 -2
  76. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +1 -1
  77. package/libx/_runtime/cds-services/services/Service.js +28 -1
  78. package/libx/_runtime/cds-services/util/assert.js +41 -65
  79. package/libx/_runtime/common/code-ext/WorkerPool.js +90 -0
  80. package/libx/_runtime/common/code-ext/WorkerReq.js +0 -4
  81. package/libx/_runtime/common/code-ext/execute.js +28 -18
  82. package/libx/_runtime/common/code-ext/handlers.js +5 -4
  83. package/libx/_runtime/common/code-ext/worker.js +45 -3
  84. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +8 -7
  85. package/libx/_runtime/common/composition/delete.js +1 -1
  86. package/libx/_runtime/common/composition/update.js +3 -5
  87. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  88. package/libx/_runtime/common/generic/auth/readOnly.js +5 -4
  89. package/libx/_runtime/common/generic/auth/restrict.js +7 -2
  90. package/libx/_runtime/common/generic/auth/utils.js +5 -2
  91. package/libx/_runtime/common/generic/crud.js +12 -1
  92. package/libx/_runtime/common/generic/etag.js +11 -3
  93. package/libx/_runtime/common/generic/input.js +8 -6
  94. package/libx/_runtime/common/generic/paging.js +25 -8
  95. package/libx/_runtime/common/generic/put.js +1 -1
  96. package/libx/_runtime/common/generic/sorting.js +0 -1
  97. package/libx/_runtime/common/i18n/messages.properties +1 -0
  98. package/libx/_runtime/common/utils/cqn.js +5 -1
  99. package/libx/_runtime/common/utils/cqn2cqn4sql.js +3 -3
  100. package/libx/_runtime/common/utils/resolveView.js +14 -10
  101. package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -3
  102. package/libx/_runtime/common/utils/templateProcessor.js +15 -17
  103. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +18 -6
  104. package/libx/_runtime/db/Service.js +1 -0
  105. package/libx/_runtime/db/data-conversion/post-processing.js +0 -18
  106. package/libx/_runtime/db/expand/expand-v2.js +2 -2
  107. package/libx/_runtime/db/expand/rawToExpanded.js +6 -6
  108. package/libx/_runtime/db/generic/integrity.js +1 -1
  109. package/libx/_runtime/db/utils/columns.js +5 -5
  110. package/libx/_runtime/fiori/generic/activate.js +3 -3
  111. package/libx/_runtime/fiori/generic/edit.js +1 -1
  112. package/libx/_runtime/fiori/generic/new.js +4 -0
  113. package/libx/_runtime/fiori/lean-draft.js +178 -68
  114. package/libx/_runtime/hana/execute.js +3 -1
  115. package/libx/_runtime/hana/pool.js +10 -2
  116. package/libx/_runtime/hana/search2cqn4sql.js +1 -1
  117. package/libx/_runtime/messaging/common-utils/AMQPClient.js +6 -1
  118. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  119. package/libx/_runtime/remote/Service.js +16 -13
  120. package/libx/_runtime/remote/utils/client.js +6 -1
  121. package/libx/_runtime/sqlite/Service.js +5 -59
  122. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
  123. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -2
  124. package/libx/_runtime/sqlite/execute.js +3 -1
  125. package/libx/_runtime/types/api.js +12 -3
  126. package/libx/odata/afterburner.js +38 -2
  127. package/libx/odata/cqn2odata.js +3 -2
  128. package/libx/odata/grammar.pegjs +5 -3
  129. package/libx/odata/parser.js +1 -1
  130. package/libx/odata/utils.js +1 -1
  131. package/libx/rest/RestAdapter.js +1 -1
  132. package/libx/rest/RestRequest.js +1 -0
  133. package/package.json +5 -2
  134. package/libx/_runtime/common/code-ext/workerQuery.js +0 -45
  135. package/libx/_runtime/common/constants/limit.js +0 -12
  136. package/libx/_runtime/common/utils/page.js +0 -39
@@ -1,30 +1,31 @@
1
1
  const cds = require('../../cds')
2
2
  const LOG = cds.log()
3
3
  const { parentPort } = require('worker_threads')
4
- const executorCallbackMap = new Map()
4
+ const executionContextMap = new Map()
5
5
 
6
6
  parentPort.on('message', function onWorkerMessageReceived(message) {
7
7
  const { id, kind, result } = message
8
+ if (!executionContextMap.has(id)) return
8
9
  if (LOG._debug) LOG.debug(`Post message received on worker thread (workerQueryExecutor.js) from main thread`, message)
9
- if (!executorCallbackMap.has(id)) return
10
10
 
11
11
  switch (kind) {
12
12
  case 'responseData':
13
- executorCallbackMap.get(id)(result)
14
- executorCallbackMap.delete(id)
13
+ executionContextMap.get(id)(result)
14
+ executionContextMap.delete(id)
15
15
  return
16
16
 
17
17
  case 'cleanup':
18
- executorCallbackMap.delete(id)
18
+ executionContextMap.delete(id)
19
19
  return
20
20
  }
21
21
  })
22
22
 
23
- function queryExecutor(resolve) {
23
+ function queryExecutor(contextId, resolve) {
24
24
  const id = cds.utils.uuid()
25
- executorCallbackMap.set(id, result => resolve(result))
25
+ executionContextMap.set(id, result => resolve(result))
26
26
  parentPort.postMessage({
27
27
  id,
28
+ contextId,
28
29
  kind: 'run',
29
30
  target: 'srv',
30
31
  prop: 'run',
@@ -230,7 +230,7 @@ const getSetNullParentForeignKeyCQNs = async (model, req, dbQuery) => {
230
230
  const cqns = []
231
231
  const query = dbQuery || req.query
232
232
  // REVISIT: req._tx should not be used like that!
233
- const origQuery = (req.tx instanceof cds.DatabaseService && req._ && req._.query) || req.query
233
+ const origQuery = (req.tx.isDatabaseService && req._ && req._.query) || req.query
234
234
  if (!dbQuery && origQuery && origQuery.DELETE && origQuery.DELETE.from.ref && origQuery.DELETE.from.ref.length > 1) {
235
235
  // delete via 2one navigation => parent is known => no need to SELECT
236
236
  const ref = origQuery.DELETE.from.ref
@@ -9,6 +9,7 @@ const { ensureNoDraftsSuffix } = require('../utils/draft')
9
9
  const { deepCopyObject } = require('../utils/copy')
10
10
 
11
11
  const getError = require('../../common/error')
12
+ const { getEntityNameFromUpdateCQN } = require('../utils/cqn')
12
13
 
13
14
  const CHUNK_SIZE = cds.env.features.chunk_deep || Number.MAX_VALUE
14
15
 
@@ -264,9 +265,7 @@ const _addSubDeepUpdateCQN = async ({ model, compositionTree, data, selectData,
264
265
 
265
266
  const hasDeepUpdate = (model, cqn) => {
266
267
  if (cqn && cqn.UPDATE && cqn.UPDATE.entity && (cqn.UPDATE.data || cqn.UPDATE.with)) {
267
- const updateEntity = cqn.UPDATE.entity
268
- const entityName =
269
- (updateEntity.ref && (updateEntity.ref[0].id || updateEntity.ref[0])) || updateEntity.name || updateEntity
268
+ const entityName = getEntityNameFromUpdateCQN(cqn)
270
269
  const entity = model.definitions[ensureNoDraftsSuffix(entityName)]
271
270
 
272
271
  if (entity) {
@@ -287,8 +286,7 @@ const getDeepUpdateCQNs = async (model, req, selectData) => {
287
286
  if (selectData.length > 1) throw getError('Deep update can only be performed on a single instance')
288
287
 
289
288
  const cqns = []
290
- const from =
291
- (query.UPDATE.entity.ref && query.UPDATE.entity.ref[0]) || query.UPDATE.entity.name || query.UPDATE.entity
289
+ const from = getEntityNameFromUpdateCQN(query)
292
290
  const entityName = ensureNoDraftsSuffix(from)
293
291
  const draft = entityName !== from
294
292
  const data = query.UPDATE.data ? deepCopyObject(query.UPDATE.data) : {}
@@ -30,7 +30,7 @@ const _getRestrictedExpand = (columns, target, definitions) => {
30
30
  if (ref_) return ref_
31
31
  }
32
32
  // expand: '**' or '*3' is only possible within custom handler, no check needed
33
- if (typeof col.expand === 'string' && /^\*{1}[\d|*]+/.test(col.expand)) {
33
+ if (typeof col.expand[0] === 'string' && /^\*{1}[\d|*]+/.test(col.expand[0])) {
34
34
  continue
35
35
  } else {
36
36
  const restricted = _getRestrictedExpand(col.expand, _getTarget(col.ref, target, definitions), definitions)
@@ -1,3 +1,4 @@
1
+ const cds = require('../../../cds')
1
2
  const { getAuthRelevantEntity } = require('./utils')
2
3
  const { WRITE_EVENTS } = require('./constants')
3
4
 
@@ -14,11 +15,11 @@ function handler(req) {
14
15
  }
15
16
 
16
17
  // @read-only
17
- const entity = getAuthRelevantEntity(req, this.model, ['@readonly'])
18
+ let entity = getAuthRelevantEntity(req, this.model, ['@readonly'])
19
+ if (cds.env.fiori.lean_draft && (req.event === 'NEW' || req.event === 'UPDATE')) entity = entity?.actives
20
+
18
21
  if (!entity || !entity['@readonly']) return
19
- if (entity['@readonly'] && req.event in WRITE_EVENTS) {
20
- req.reject(405, 'ENTITY_IS_READ_ONLY', [entity.name])
21
- }
22
+ if (entity['@readonly'] && req.event in WRITE_EVENTS) req.reject(405, 'ENTITY_IS_READ_ONLY', [entity.name])
22
23
  }
23
24
 
24
25
  handler._initial = true
@@ -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
@@ -141,8 +141,11 @@ const resolveUserAttrs = (restrict, req) => {
141
141
  attr = parts.shift()
142
142
  }
143
143
 
144
- if (!skip)
145
- restrict.where = restrict.where.replace(next[0], val === undefined ? null : val).replace('in null', 'is null')
144
+ if (!skip) {
145
+ const v = val === undefined ? null : typeof val === 'string' && val.match(/^\d*$/) ? `'${val}'` : val
146
+ restrict.where = restrict.where.replace(next[0], v).replace('in null', 'is null')
147
+ }
148
+
146
149
  next = _getNext(restrict.where)
147
150
  }
148
151
 
@@ -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', ... ]"
@@ -280,7 +280,7 @@ const _createWindowCQN = (SELECT, model) => {
280
280
  }
281
281
  }
282
282
 
283
- delete SELECT.groupBy
283
+ SELECT.groupBy = undefined
284
284
  }
285
285
 
286
286
  const _unshiftRefsWithNavigation = nav => el => {
@@ -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