@sap/cds 6.4.1 → 6.6.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 (139) hide show
  1. package/CHANGELOG.md +79 -6
  2. package/README.md +5 -0
  3. package/apis/cqn.d.ts +14 -3
  4. package/apis/ql.d.ts +8 -8
  5. package/apis/services.d.ts +37 -65
  6. package/apis/test.d.ts +7 -0
  7. package/bin/build/buildTaskEngine.js +9 -14
  8. package/bin/build/buildTaskFactory.js +1 -1
  9. package/bin/build/buildTaskHandler.js +3 -14
  10. package/bin/build/index.js +8 -2
  11. package/bin/build/provider/buildTaskProviderInternal.js +18 -13
  12. package/bin/build/provider/fiori/index.js +5 -10
  13. package/bin/build/provider/hana/2migration.js +11 -2
  14. package/bin/build/provider/hana/index.js +17 -14
  15. package/bin/build/provider/hana/template/.hdiconfig-hanacloud +137 -0
  16. package/bin/build/provider/hana/template/package.json +3 -0
  17. package/bin/build/provider/mtx/resourcesTarBuilder.js +12 -3
  18. package/bin/build/provider/mtx-extension/index.js +57 -37
  19. package/bin/build/provider/mtx-sidecar/index.js +1 -1
  20. package/bin/build/util.js +18 -1
  21. package/bin/cds.js +1 -5
  22. package/bin/deploy/to-hana/hana.js +10 -3
  23. package/bin/serve.js +36 -20
  24. package/common.cds +7 -0
  25. package/lib/auth/jwt-auth.js +8 -7
  26. package/lib/compile/for/lean_drafts.js +55 -6
  27. package/lib/compile/minify.js +3 -3
  28. package/lib/dbs/cds-deploy.js +18 -17
  29. package/lib/env/cds-requires.js +1 -1
  30. package/lib/env/defaults.js +5 -1
  31. package/lib/env/schemas/cds-rc.json +74 -3
  32. package/lib/index.js +4 -2
  33. package/lib/lazy.js +6 -8
  34. package/lib/log/cds-error.js +2 -2
  35. package/lib/ql/Whereable.js +22 -11
  36. package/lib/ql/cds-ql.js +1 -1
  37. package/lib/req/cds-context.js +3 -3
  38. package/lib/req/response.js +8 -3
  39. package/lib/req/user.js +12 -2
  40. package/lib/srv/bindings.js +1 -2
  41. package/lib/srv/cds-serve.js +2 -1
  42. package/lib/srv/middlewares/trace.js +31 -15
  43. package/lib/srv/protocols/odata-v2-proxy.js +8 -8
  44. package/lib/srv/srv-handlers.js +26 -7
  45. package/lib/srv/srv-methods.js +2 -2
  46. package/lib/srv/srv-models.js +8 -3
  47. package/lib/utils/cds-test.js +7 -5
  48. package/lib/utils/cds-utils.js +3 -1
  49. package/lib/utils/tar.js +6 -3
  50. package/libx/_runtime/auth/strategies/JWT.js +1 -0
  51. package/libx/_runtime/auth/strategies/ias-auth.js +3 -2
  52. package/libx/_runtime/auth/strategies/mock.js +12 -1
  53. package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
  54. package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
  55. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +6 -2
  56. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -2
  57. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -0
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +11 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +8 -8
  62. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +1 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +14 -14
  64. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +1 -0
  65. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ResourceJsonSerializer.js +3 -0
  66. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +2 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +3 -2
  68. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +7 -0
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +0 -3
  70. package/libx/_runtime/cds-services/services/Service.js +11 -19
  71. package/libx/_runtime/cds-services/services/utils/columns.js +42 -40
  72. package/libx/_runtime/cds-services/util/assert.js +7 -1
  73. package/libx/_runtime/common/code-ext/WorkerReq.js +81 -0
  74. package/libx/_runtime/common/code-ext/config.js +13 -0
  75. package/libx/_runtime/common/code-ext/execute.js +113 -0
  76. package/libx/_runtime/common/code-ext/handlers.js +49 -0
  77. package/libx/_runtime/common/code-ext/worker.js +40 -0
  78. package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
  79. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +36 -0
  80. package/libx/_runtime/common/composition/data.js +5 -2
  81. package/libx/_runtime/common/composition/tree.js +2 -0
  82. package/libx/_runtime/common/generic/auth/restrict.js +1 -1
  83. package/libx/_runtime/common/generic/crud.js +4 -0
  84. package/libx/_runtime/common/generic/etag.js +3 -1
  85. package/libx/_runtime/common/generic/input.js +12 -14
  86. package/libx/_runtime/common/i18n/index.js +1 -1
  87. package/libx/_runtime/common/utils/cqn2cqn4sql.js +47 -22
  88. package/libx/_runtime/common/utils/path.js +5 -26
  89. package/libx/_runtime/common/utils/search2cqn4sql.js +16 -9
  90. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +19 -13
  91. package/libx/_runtime/db/data-conversion/post-processing.js +1 -1
  92. package/libx/_runtime/db/expand/expandCQNToJoin.js +7 -4
  93. package/libx/_runtime/db/expand/rawToExpanded.js +3 -2
  94. package/libx/_runtime/db/generic/input.js +2 -2
  95. package/libx/_runtime/db/generic/integrity.js +1 -0
  96. package/libx/_runtime/db/generic/virtual.js +1 -0
  97. package/libx/_runtime/db/query/read.js +3 -2
  98. package/libx/_runtime/db/utils/localized.js +1 -1
  99. package/libx/_runtime/fiori/generic/activate.js +7 -1
  100. package/libx/_runtime/fiori/generic/before.js +9 -1
  101. package/libx/_runtime/fiori/generic/edit.js +8 -1
  102. package/libx/_runtime/fiori/generic/new.js +2 -0
  103. package/libx/_runtime/fiori/generic/patch.js +2 -0
  104. package/libx/_runtime/fiori/generic/prepare.js +2 -0
  105. package/libx/_runtime/fiori/generic/read.js +16 -5
  106. package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
  107. package/libx/_runtime/fiori/lean-draft.js +505 -241
  108. package/libx/_runtime/fiori/utils/delete.js +2 -0
  109. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
  110. package/libx/_runtime/hana/pool.js +1 -1
  111. package/libx/_runtime/hana/search2cqn4sql.js +51 -51
  112. package/libx/_runtime/messaging/Outbox.js +1 -1
  113. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -0
  114. package/libx/_runtime/messaging/enterprise-messaging.js +2 -6
  115. package/libx/_runtime/messaging/file-based.js +1 -2
  116. package/libx/_runtime/messaging/outbox/OutboxRunner.js +1 -1
  117. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  118. package/libx/_runtime/messaging/service.js +0 -1
  119. package/libx/_runtime/remote/Service.js +1 -0
  120. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +19 -3
  121. package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +0 -18
  122. package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +0 -18
  123. package/libx/_runtime/sqlite/customBuilder/CustomSelectBuilder.js +0 -24
  124. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -1
  125. package/libx/_runtime/sqlite/customBuilder/index.js +47 -32
  126. package/libx/odata/afterburner.js +23 -8
  127. package/libx/odata/cqn2odata.js +1 -1
  128. package/libx/odata/grammar.pegjs +3 -4
  129. package/libx/odata/index.js +5 -1
  130. package/libx/odata/parseToCqn.js +3 -3
  131. package/libx/odata/parser.js +1 -1
  132. package/libx/odata/utils.js +58 -1
  133. package/libx/rest/middleware/parse.js +26 -4
  134. package/package.json +1 -1
  135. package/server.js +1 -1
  136. package/libx/_runtime/sqlite/customBuilder/CustomDeleteBuilder.js +0 -17
  137. package/libx/_runtime/sqlite/customBuilder/CustomReferenceBuilder.js +0 -11
  138. package/libx/_runtime/sqlite/customBuilder/CustomUpdateBuilder.js +0 -17
  139. /package/bin/build/provider/hana/template/{.hdiconfig → .hdiconfig-haas} +0 -0
@@ -0,0 +1,40 @@
1
+ const cds = require('../../cds')
2
+ const LOG = cds.log()
3
+ const { parentPort, workerData } = require('worker_threads')
4
+ const { WorkerSELECT, WorkerINSERT, WorkerUPSERT, WorkerUPDATE, WorkerDELETE } = require('./workerQuery')
5
+ const WorkerReq = require('./WorkerReq')
6
+ const { timeout } = require('./config')
7
+
8
+ parentPort.once('message', function onWorkerMessageReceived(message) {
9
+ const { contextId, workerId, kind, code, reqData } = message
10
+ if (LOG._debug) LOG.debug(`Post message received on worker thread (worker.js) from main thread`, message)
11
+ if (kind !== 'start' || workerId !== workerData.id) return
12
+
13
+ // eslint-disable-next-line cds/no-missing-dependencies
14
+ const { VM } = require('vm2')
15
+ const workerReq = new WorkerReq(contextId, reqData)
16
+ const vm = new VM({
17
+ console: 'inherit',
18
+ timeout, // specifies the number of milliseconds to execute code before terminating execution
19
+ allowAsync: true,
20
+
21
+ // the sandbox represents the global object inside the vm instance
22
+ sandbox: {
23
+ req: workerReq,
24
+ SELECT: WorkerSELECT._api(),
25
+ INSERT: WorkerINSERT._api(),
26
+ UPSERT: WorkerUPSERT._api(),
27
+ UPDATE: WorkerUPDATE._api(),
28
+ DELETE: WorkerDELETE._api()
29
+ }
30
+ })
31
+
32
+ try {
33
+ ;(async function () {
34
+ const result = await vm.run(code)
35
+ parentPort.postMessage({ contextId, kind: 'success', req: reqData, postMessages: workerReq.postMessages, result })
36
+ })()
37
+ } catch (error) {
38
+ parentPort.postMessage({ contextId, kind: 'error', error })
39
+ }
40
+ })
@@ -0,0 +1,45 @@
1
+ const SELECT = require('../../../../lib/ql/SELECT')
2
+ const INSERT = require('../../../../lib/ql/INSERT')
3
+ const UPSERT = require('../../../../lib/ql/UPSERT')
4
+ const UPDATE = require('../../../../lib/ql/UPDATE')
5
+ const DELETE = require('../../../../lib/ql/DELETE')
6
+ const queryExecutor = require('./workerQueryExecutor')
7
+
8
+ class WorkerSELECT extends SELECT {
9
+ // intercept await SELECT.from(...) calls
10
+ then(r, e) {
11
+ return new Promise(queryExecutor.bind(this)).then(r, e)
12
+ }
13
+ }
14
+
15
+ class WorkerINSERT extends INSERT {
16
+ then(r, e) {
17
+ return new Promise(queryExecutor.bind(this)).then(r, e)
18
+ }
19
+ }
20
+
21
+ class WorkerUPSERT extends UPSERT {
22
+ then(r, e) {
23
+ return new Promise(queryExecutor.bind(this)).then(r, e)
24
+ }
25
+ }
26
+
27
+ class WorkerUPDATE extends UPDATE {
28
+ then(r, e) {
29
+ return new Promise(queryExecutor.bind(this)).then(r, e)
30
+ }
31
+ }
32
+
33
+ class WorkerDELETE extends DELETE {
34
+ then(r, e) {
35
+ return new Promise(queryExecutor.bind(this)).then(r, e)
36
+ }
37
+ }
38
+
39
+ Object.defineProperty(WorkerSELECT.prototype, 'cmd', { value: 'SELECT' })
40
+ Object.defineProperty(WorkerINSERT.prototype, 'cmd', { value: 'INSERT' })
41
+ Object.defineProperty(WorkerUPSERT.prototype, 'cmd', { value: 'UPSERT' })
42
+ Object.defineProperty(WorkerUPDATE.prototype, 'cmd', { value: 'UPDATE' })
43
+ Object.defineProperty(WorkerDELETE.prototype, 'cmd', { value: 'DELETE' })
44
+
45
+ module.exports = { WorkerSELECT, WorkerINSERT, WorkerUPSERT, WorkerUPDATE, WorkerDELETE }
@@ -0,0 +1,36 @@
1
+ const cds = require('../../cds')
2
+ const LOG = cds.log()
3
+ const { parentPort } = require('worker_threads')
4
+ const executorCallbackMap = new Map()
5
+
6
+ parentPort.on('message', function onWorkerMessageReceived(message) {
7
+ const { id, kind, result } = message
8
+ if (LOG._debug) LOG.debug(`Post message received on worker thread (workerQueryExecutor.js) from main thread`, message)
9
+ if (!executorCallbackMap.has(id)) return
10
+
11
+ switch (kind) {
12
+ case 'responseData':
13
+ executorCallbackMap.get(id)(result)
14
+ executorCallbackMap.delete(id)
15
+ return
16
+
17
+ case 'cleanup':
18
+ executorCallbackMap.delete(id)
19
+ return
20
+ }
21
+ })
22
+
23
+ function queryExecutor(resolve) {
24
+ const id = cds.utils.uuid()
25
+ executorCallbackMap.set(id, result => resolve(result))
26
+ parentPort.postMessage({
27
+ id,
28
+ kind: 'run',
29
+ target: 'srv',
30
+ prop: 'run',
31
+ responseData: true,
32
+ args: [this]
33
+ })
34
+ }
35
+
36
+ module.exports = queryExecutor
@@ -78,7 +78,9 @@ const _whereKeys = keySet => {
78
78
  }
79
79
 
80
80
  const _parentKey = (element, key) => {
81
- return [...element.customBackLinks, ...element.backLinks].reduce((parentKey, customBackLink) => {
81
+ let links = [...element.customBackLinks, ...element.backLinks]
82
+ if (element.is2one && links.some(l => l.for2one)) links = links.filter(l => l.for2one)
83
+ return links.reduce((parentKey, customBackLink) => {
82
84
  // TODO: why Object.prototype.hasOwnProperty?
83
85
  parentKey[customBackLink.entityKey] = Object.prototype.hasOwnProperty.call(key, customBackLink.targetKey)
84
86
  ? key[customBackLink.targetKey]
@@ -139,7 +141,8 @@ const _getWhereObj = (row, links) => {
139
141
 
140
142
  const _subWhere = (result, element) => {
141
143
  let where
142
- const links = [...element.backLinks, ...element.customBackLinks]
144
+ let links = [...element.backLinks, ...element.customBackLinks]
145
+ if (element.is2one && links.some(l => l.for2one)) links = links.filter(l => l.for2one)
143
146
  if (result.length && links && links.length > 0) {
144
147
  where = {}
145
148
  const keys0 = Object.keys(_getWhereObj(result[0], links))
@@ -132,6 +132,8 @@ const _getCompositionTreeRec = ({
132
132
  if (element.is2many) {
133
133
  subObject.customBackLinks.push(...backLinks)
134
134
  } else {
135
+ subObject.is2one = true
136
+ backLinks.forEach(b => (b.for2one = true))
135
137
  subObject.backLinks.push(...backLinks)
136
138
  }
137
139
  }
@@ -142,7 +142,7 @@ const _getRestrictionForTarget = (resolvedApplicables, target) => {
142
142
  }
143
143
 
144
144
  const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
145
- if (req.target._isDraftEnabled) {
145
+ if (!cds.env.features.lean_draft && req.target._isDraftEnabled) {
146
146
  req.query._draftRestrictions = resolvedApplicables
147
147
  return
148
148
  }
@@ -61,6 +61,10 @@ exports.impl = cds.service.impl(function () {
61
61
  req.query.where(singleton)
62
62
  }
63
63
 
64
+ if (req.event === 'READ' && req.query?.SELECT) {
65
+ req.query.SELECT.localized = true
66
+ }
67
+
64
68
  if (!result) {
65
69
  result = await cds.tx(req).run(req.query, req.data)
66
70
  }
@@ -81,7 +81,9 @@ const commonGenericEtag = async function (req) {
81
81
 
82
82
  // validate
83
83
  if (req.isConditional && !req.query.INSERT) {
84
- const result = await cds.tx(req).run(getSelectCQN(req.query, req.target, this.model))
84
+ let cqn = getSelectCQN(req.query, req.target, this.model)
85
+ if (req.query.UPDATE || req.query.DELETE) cqn = cqn.forUpdate()
86
+ const result = await cds.tx(req).run(cqn)
85
87
 
86
88
  if (result.length === 1) {
87
89
  const etag = Object.values(result[0])[0]
@@ -36,10 +36,10 @@ const _getSimpleCategory = category => {
36
36
  }
37
37
 
38
38
  const _isDraftCoreComputed = (req, element, event) =>
39
+ element['@Core.Computed'] &&
39
40
  cds.env.features.preserve_computed !== false &&
40
41
  req._ &&
41
42
  req._.event === 'draftActivate' &&
42
- element['@Core.Computed'] &&
43
43
  !((event === 'CREATE' && element['@cds.on.insert']) || element['@cds.on.update'])
44
44
 
45
45
  const _isStreamingProperty = (elements, row, property) =>
@@ -217,16 +217,8 @@ const _callError = (req, errors) => {
217
217
  for (const error of errors) req.error(error)
218
218
  }
219
219
 
220
- const _isBoundAction = req => !!(req.getUriInfo && req.getUriInfo().getLastSegment().getKind() === 'BOUND.ACTION')
221
-
222
- const _getBoundActionBindingParameter = req => {
223
- // REVISIT: req._ gets set in onDraftActivate to original req
224
- const action = (req._ && req._.event) || req.event
225
- const actions = req.target.actions
226
-
227
- // 'in' is the default binding parameter name for bound actions/functions
228
- return (actions && actions[action] && actions[action]['@cds.odata.bindingparameter.name']) || 'in'
229
- }
220
+ const _getBoundAction = req => req.target.actions?.[req.context?.event]
221
+ const _getBoundActionBindingParameter = action => action['@cds.odata.bindingparameter.name'] || 'in'
230
222
 
231
223
  async function commonGenericInput(req) {
232
224
  if (!req.query) return // FIXME: the code below expects req.query to be defined
@@ -250,8 +242,10 @@ async function commonGenericInput(req) {
250
242
  pathSegments: []
251
243
  }
252
244
 
253
- if (_isBoundAction(req)) {
254
- const pathSegment = _getBoundActionBindingParameter(req)
245
+ const boundAction = _getBoundAction(req)
246
+
247
+ if (boundAction) {
248
+ const pathSegment = _getBoundActionBindingParameter(boundAction)
255
249
  const keys = req._ && req._.params && req._.params[0]
256
250
  if (pathSegment) pathOptions.pathSegments.push(pathSegment)
257
251
 
@@ -370,7 +364,11 @@ commonGenericInput._initial = true
370
364
  _actionFunctionHandler._initial = true
371
365
 
372
366
  module.exports = cds.service.impl(function () {
373
- this.before(['CREATE', 'UPDATE', 'NEW', 'PATCH'], '*', commonGenericInput)
367
+ if (cds.env.features.lean_draft) {
368
+ this.before(['CREATE', 'UPDATE'], '*', commonGenericInput)
369
+ } else {
370
+ this.before(['CREATE', 'UPDATE', 'NEW', 'PATCH'], '*', commonGenericInput)
371
+ }
374
372
  const operationNames = []
375
373
 
376
374
  for (const operation of this.operations) {
@@ -8,7 +8,7 @@ const dirs = (cds.env.i18n && cds.env.i18n.folders) || []
8
8
  const i18ns = {}
9
9
 
10
10
  function exists(args, locale) {
11
- const file = path.join(process.cwd(), ...args, locale ? `messages_${locale}.properties` : 'messages.properties')
11
+ const file = path.join(cds.root, ...args, locale ? `messages_${locale}.properties` : 'messages.properties')
12
12
  return fs.existsSync(file) ? file : undefined
13
13
  }
14
14
 
@@ -11,7 +11,7 @@ const search2cqn4sql = require('./search2cqn4sql')
11
11
  const { getEntityNameFromCQN } = require('./entityFromCqn')
12
12
  const getError = require('../../common/error')
13
13
  const { rewriteAsterisks } = require('./rewriteAsterisks')
14
- const { getPathFromRef, getEntityFromPath } = require('../../common/utils/path')
14
+ const { getEntityFromPath } = require('../../common/utils/path')
15
15
  const { removeIsActiveEntityRecursively } = require('../../fiori/utils/where')
16
16
  const { addRefToWhereIfNecessary } = require('../../../odata/afterburner')
17
17
  const { addAliasToExpression, PARENT_ALIAS, FOREIGN_ALIAS } = require('../../db/utils/generateAliases')
@@ -142,14 +142,16 @@ const _getWindowWhere = (where, bottomTop) => {
142
142
 
143
143
  const _getOrderByForWindowFn = bottomTop => {
144
144
  const orderBy = _getBottomTopRefOrVal(bottomTop[0], 'ref')[0]
145
- orderBy.sort = bottomTop[0].func === 'topcount' ? 'desc' : 'asc'
146
- return orderBy
145
+ const sort = bottomTop[0].func === 'topcount' ? 'desc' : 'asc'
146
+ return [orderBy, sort]
147
147
  }
148
148
 
149
149
  const _getWindowXpr = (groupBy, bottomTop) => {
150
- const xpr = [{ func: 'ROW_NUMBER', args: [] }, 'OVER', '(']
150
+ const overXpr = { xpr: [] }
151
+ const xpr = [{ func: 'ROW_NUMBER', args: [] }, 'OVER', overXpr]
152
+
151
153
  if (groupBy)
152
- xpr.push(
154
+ overXpr.xpr.push(
153
155
  'PARTITION BY',
154
156
  ...groupBy.reduce((acc, el, i) => {
155
157
  if (i < groupBy.length - 1) {
@@ -163,9 +165,8 @@ const _getWindowXpr = (groupBy, bottomTop) => {
163
165
  }, [])
164
166
  )
165
167
 
166
- xpr.push('ORDER BY', _getOrderByForWindowFn(bottomTop))
167
- xpr.push(')')
168
- return { xpr: xpr, as: 'rowNumber' }
168
+ overXpr.xpr.push('ORDER BY', ..._getOrderByForWindowFn(bottomTop))
169
+ return { xpr, as: 'rowNumber' }
169
170
  }
170
171
 
171
172
  const _isNavCountFunc = el => el.func && el.func === 'count' && el.args[0] !== '*'
@@ -445,7 +446,7 @@ const convertWhereExists = (query, model, options, currentTarget) => {
445
446
  if (currentTarget) {
446
447
  queryTarget = getEntityFromPath({ ref }, currentTarget)
447
448
  } else {
448
- queryTarget = getEntityFromPath(getPathFromRef(ref), model)
449
+ queryTarget = getEntityFromPath({ ref }, model)
449
450
  outerAlias = as || PARENT_ALIAS + lambdaIteration
450
451
  innerAlias = FOREIGN_ALIAS + lambdaIteration
451
452
  }
@@ -586,6 +587,7 @@ const _convertOrderByOrWhereIfSkip = (cqn, target, model) => {
586
587
  }
587
588
  }
588
589
 
590
+ /* REVISIT: Unused
589
591
  const _convertExpand = expand => {
590
592
  expand.forEach(expandElement => {
591
593
  if (expandElement.ref && expandElement.ref[0]) {
@@ -603,15 +605,21 @@ const _convertExpand = expand => {
603
605
  }
604
606
  })
605
607
  }
608
+ */
606
609
 
607
- const _convertRefWhereInExpand = columns => {
608
- if (!columns) return
610
+ const _simplifyWhere = col => {
611
+ if (col.ref?.[0].where) {
612
+ col.where = col.ref[0].where
613
+ col.ref[0] = col.ref[0].id
614
+ }
615
+ }
609
616
 
610
- columns.forEach(col => {
611
- if (col.expand && typeof col.expand !== 'string') {
612
- _convertExpand(col.expand)
613
- }
614
- })
617
+ const _simplifyWhereInColumns = columns => {
618
+ if (!columns) return
619
+ for (const col of columns) {
620
+ _simplifyWhere(col)
621
+ if (col.expand) _simplifyWhereInColumns(col.expand)
622
+ }
615
623
  }
616
624
 
617
625
  const _convertPathExpression = (query, model, options = {}) => {
@@ -742,8 +750,8 @@ const _convertSelect = (query, model, _options) => {
742
750
  _convertToOneEqNullInFilter(query.SELECT, target)
743
751
  }
744
752
 
745
- // extract where clause if it is in column expand ref
746
- _convertRefWhereInExpand(query.SELECT.columns)
753
+ // extract where clause if it is in an expand column
754
+ _simplifyWhereInColumns(query.SELECT.columns)
747
755
 
748
756
  // REVISIT: The following operations only work for _one_ entity.
749
757
  // We must also enable them for joins etc.
@@ -822,7 +830,11 @@ const _convertUpsert = (query, model) => {
822
830
 
823
831
  // We add all previous properties ot the newly created query.
824
832
  // Reason is to not lose the query API functionality
825
- Object.assign(upsert.UPSERT, query.UPSERT, { into: { ref: [resolvedIntoClause], as: query.UPSERT.into.as } })
833
+
834
+ for (const key in query.UPSERT) {
835
+ upsert.UPSERT[key] = query.UPSERT[key]
836
+ }
837
+ Object.assign(upsert.UPSERT, { into: { ref: [resolvedIntoClause], as: query.UPSERT.into.as } })
826
838
 
827
839
  const resolved = resolveView(upsert, model, cds.db)
828
840
  // required for deplyoing of extensions, not used anywhere else except UpsertBuilder
@@ -843,7 +855,11 @@ const _convertInsert = (query, model) => {
843
855
 
844
856
  // We add all previous properties ot the newly created query.
845
857
  // Reason is to not lose the query API functionality
846
- Object.assign(insert.INSERT, query.INSERT, { into: { ref: [resolvedIntoClause], as: query.INSERT.into.as } })
858
+ insert.INSERT = {}
859
+ for (const prop in query.INSERT) {
860
+ insert.INSERT[prop] = query.INSERT[prop]
861
+ }
862
+ Object.assign(insert.INSERT, { into: { ref: [resolvedIntoClause], as: query.INSERT.into.as } })
847
863
 
848
864
  const target = model.definitions[resolvedIntoClause]
849
865
  if (!target) return insert
@@ -895,7 +911,12 @@ const _convertDelete = (query, model, options) => {
895
911
 
896
912
  const { target, alias, where } = convertPathExpressionToWhere(query.DELETE.from, model, options)
897
913
  const deleet = DELETE('x')
898
- Object.assign(deleet.DELETE, query.DELETE, { from: target, where: undefined })
914
+
915
+ for (const key in query.DELETE) {
916
+ deleet.DELETE[key] = query.DELETE[key]
917
+ }
918
+
919
+ Object.assign(deleet.DELETE, { from: target, where: undefined })
899
920
 
900
921
  if (alias) deleet.DELETE.from = { ref: [target], as: alias }
901
922
  if (where) deleet.where(where)
@@ -940,7 +961,11 @@ const _convertUpdate = (query, model, options) => {
940
961
  // link .with and .data and set query target and remove current where clause
941
962
  // REVISIT: update statement does not accept cqn partial as input
942
963
  const update = UPDATE('x')
943
- Object.assign(update.UPDATE, query.UPDATE, { entity: target, where: undefined })
964
+ // Object.assign(update.UPDATE, query.UPDATE, { entity: target, where: undefined })
965
+ for (const key in query.UPDATE) {
966
+ update.UPDATE[key] = query.UPDATE[key]
967
+ }
968
+ Object.assign(update.UPDATE, { entity: target, where: undefined })
944
969
 
945
970
  if (alias) update.UPDATE.entity = { ref: [target], as: alias }
946
971
  if (where) update.where(where)
@@ -1,41 +1,20 @@
1
- const cds = require('../../cds')
2
1
  const { ensureNoDraftsSuffix } = require('./draft')
3
2
 
4
- /*
5
- * returns path like <service>.<entity>:<prop1>.<prop2> for ref = [{ id: '<service>.<entity>' }, '<prop1>', '<prop2>']
6
- */
7
- const getPathFromRef = ref => {
8
- const x = ref.reduce((acc, cur) => {
9
- acc += (acc ? ':' : '') + (cur.id ? cur.id : cur)
10
- return acc
11
- }, '')
12
- const y = x.split(':')
13
- let z = y.shift()
14
- if (y.length) z += ':' + y.join('.')
15
- return z
16
- }
17
-
18
3
  /*
19
4
  * returns the target entity for the given path
20
5
  */
21
6
  const getEntityFromPath = (path, def) => {
22
7
  let current = def.definitions ? { elements: def.definitions } : def
23
- path = typeof path === 'string' ? cds.parse.path(path) : path
24
- const segments = [...path.ref]
25
- while (segments.length) {
26
- let segment = segments.shift()
27
- if (segment.id && typeof segment.id === 'string') {
28
- segment.id = ensureNoDraftsSuffix(segment.id)
29
- } else if (typeof segment === 'string') {
30
- segment = ensureNoDraftsSuffix(segment)
31
- }
32
- current = current.elements[segment.id || segment]
8
+
9
+ let id
10
+ for (const segment of path.ref) {
11
+ id = ensureNoDraftsSuffix(segment.id || segment)
12
+ current = current.elements[id]
33
13
  if (current && current.target) current = current._target
34
14
  }
35
15
  return current
36
16
  }
37
17
 
38
18
  module.exports = {
39
- getPathFromRef,
40
19
  getEntityFromPath
41
20
  }
@@ -15,20 +15,27 @@ const search2cqn4sql = (query, model, options = {}) => {
15
15
  const { search2cqn4sql } = options
16
16
  const { entityName, alias } = _targetFrom(query.SELECT.from, options)
17
17
  const entity = model.definitions[entityName]
18
- const columns = computeColumnsToBeSearched(query, entity, alias)
19
-
18
+ const localizedAssociation = entity.associations?.localized
20
19
  // Call custom (optimized search to cqn for sql implementation) that tries
21
20
  // to optimize the search behavior for a specific database service.
22
21
  // REVISIT: $search query option combined with $count is not currently optimized
23
- if (typeof search2cqn4sql === 'function' && !query.SELECT.count) {
24
- const search2cqnOptions = { columns, locale: options.locale }
22
+ if (
23
+ typeof search2cqn4sql === 'function' &&
24
+ !query.SELECT.count &&
25
+ localizedAssociation &&
26
+ !(query._aggregated || /* new parser */ query.SELECT.groupBy)
27
+ ) {
28
+ const search2cqnOptions = { columns: computeColumnsToBeSearched(query, entity), locale: options.locale }
25
29
  return search2cqn4sql(query, entity, search2cqnOptions)
26
- }
30
+ } else {
31
+ const columnsToBeSearched = computeColumnsToBeSearched(query, entity, alias)
32
+ const expression = columnsToBeSearched?.length
33
+ ? searchToLike(cqnSearchPhrase, columnsToBeSearched)
34
+ : [{ val: 0 }, '=', { val: 1 }]
27
35
 
28
- const expression = searchToLike(cqnSearchPhrase, columns)
29
-
30
- // REVISIT: find out here if where or having must be used
31
- query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
36
+ // REVISIT: find out here if where or having must be used
37
+ query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
38
+ }
32
39
  }
33
40
 
34
41
  module.exports = search2cqn4sql
@@ -1,20 +1,26 @@
1
+ const cds = require('../../cds')
1
2
  const templatePathSerializer = (tKey, keyNames, row, elements, draftKeys) => {
2
- const keyValuePairs = keyNames.map(key => {
3
- let quote
3
+ const keyValuePairs = keyNames
4
+ .filter(key => {
5
+ if (cds.env.features.lean_draft && key === 'IsActiveEntity') return false
6
+ return true
7
+ })
8
+ .map(key => {
9
+ let quote
4
10
 
5
- switch (elements[key].type) {
6
- case 'cds.String':
7
- quote = "'"
8
- break
11
+ switch (elements[key].type) {
12
+ case 'cds.String':
13
+ quote = "'"
14
+ break
9
15
 
10
- default:
11
- quote = ''
12
- break
13
- }
16
+ default:
17
+ quote = ''
18
+ break
19
+ }
14
20
 
15
- const keyValue = row[key] ?? draftKeys?.[key]
16
- return `${key}=${quote}${keyValue}${quote}`
17
- })
21
+ const keyValue = row[key] ?? draftKeys?.[key]
22
+ return `${key}=${quote}${keyValue}${quote}`
23
+ })
18
24
 
19
25
  const keyValuePairsSerialized = keyValuePairs.join(',')
20
26
  return `${tKey}(${keyValuePairsSerialized})`
@@ -91,7 +91,7 @@ const _getMapperForListedElements = (conversionMap, csn, cqn) => {
91
91
  if (col.cast) {
92
92
  const name = col.as ? col.as : col.ref[col.ref.length - 1]
93
93
  _addConverter(mapper, name, (val, key, row, unaliasedKey) => {
94
- row[unaliasedKey || key] = _getCastFunction(col.cast)(val)
94
+ row[unaliasedKey || key] = val === null ? null : _getCastFunction(col.cast)(val)
95
95
  })
96
96
  continue
97
97
  }
@@ -40,7 +40,8 @@ function getCqnCopy(readToOneCQN) {
40
40
 
41
41
  class JoinCQNFromExpanded {
42
42
  constructor(cqn, csn, locale) {
43
- this._SELECT = Object.assign({}, cqn.SELECT)
43
+ this._SELECT = {}
44
+ for (const prop in cqn.SELECT) this._SELECT[prop] = cqn.SELECT[prop]
44
45
  this._csn = csn
45
46
  // REVISIT: locale is only passed in case of sqlite -> bad coding
46
47
  if (cds.env.i18n.for_sqlite.includes(locale)) {
@@ -866,7 +867,7 @@ class JoinCQNFromExpanded {
866
867
  const subWhere = []
867
868
 
868
869
  for (const key in entity.keys) {
869
- if (key === 'IsActiveEntity') continue
870
+ if (key === 'IsActiveEntity' || entity.keys[key]._isAssociationStrict) continue
870
871
  if (subWhere.length) {
871
872
  subWhere.push('and')
872
873
  }
@@ -981,7 +982,7 @@ class JoinCQNFromExpanded {
981
982
  ...column,
982
983
  xpr: column.xpr.map(x => {
983
984
  if (x.ref) {
984
- const res = this._buildNewAliasColumn(x, entity, tableAlias, mappings)
985
+ const res = this._buildNewAliasColumn(x, entity, tableAlias, mappings, true)
985
986
  delete res.as
986
987
  return res
987
988
  } else return x
@@ -1012,7 +1013,7 @@ class JoinCQNFromExpanded {
1012
1013
  return column.ref && typeof column.ref[column.ref.length - 1] !== 'string'
1013
1014
  }
1014
1015
 
1015
- _buildNewAliasColumn(column, entity, tableAlias, mappings) {
1016
+ _buildNewAliasColumn(column, entity, tableAlias, mappings, skipMapping = false) {
1016
1017
  // Casted name, vs column name
1017
1018
  const identifier = this._getIdentifier(column, tableAlias)
1018
1019
  const as = column[SKIP_MAPPING] ? column.as : `${tableAlias}_${identifier}`
@@ -1026,6 +1027,8 @@ class JoinCQNFromExpanded {
1026
1027
  aliasedElement.ref = alias ? [alias, column.ref[0]] : [column.ref[0]]
1027
1028
  }
1028
1029
 
1030
+ if (skipMapping) return aliasedElement
1031
+
1029
1032
  if (!column[SKIP_MAPPING]) {
1030
1033
  mappings[column[IDENTIFIER] || identifier] = as
1031
1034
  if (column[CLEANUP_KEYS]) {
@@ -1,5 +1,6 @@
1
1
  const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
2
2
  const DRAFT_COLUMNS_ARRAY = Object.keys(DRAFT_COLUMNS_MAP)
3
+ const cds = require('../../cds')
3
4
 
4
5
  const EXPAND = Symbol.for('sap.cds.expand')
5
6
  const GET_KEY_VALUE = Symbol.for('sap.cds.getKeyValue')
@@ -110,9 +111,9 @@ class RawToExpanded {
110
111
  let expandedItems = this._getResultCache(toManyTree.concat(key))[mapping[GET_KEY_VALUE](false, entry)] || []
111
112
 
112
113
  // the expanded items may include the actives of the deleted drafts -> filter out
113
- if (rootIsActiveEntity !== null) {
114
+ if (!cds.env.features.lean_draft && rootIsActiveEntity !== null) {
114
115
  if (mapping[TO_ACTIVE]) expandedItems = expandedItems.filter(ele => ele.IsActiveEntity !== false)
115
- else expandedItems = expandedItems.filter(ele => ele.IsActiveEntity === rootIsActiveEntity)
116
+ else expandedItems = expandedItems.filter(ele => !!ele.IsActiveEntity === rootIsActiveEntity)
116
117
  }
117
118
 
118
119
  row[key] = !mapping[CLEANUP_KEYS]
@@ -197,11 +197,11 @@ function dbGenericInput(req) {
197
197
  // call with this for this.model
198
198
  normalizeTimeData.call(this, req)
199
199
 
200
- const draft = req.target.name && req.target.name.match(/_drafts$/)
200
+ const draft = req.target.name && (req.target.name.match(/_drafts$/) || req.target.name.match(/\.drafts$/))
201
201
 
202
202
  const target =
203
203
  req.target._unresolved && req.target.name
204
- ? this.model.definitions[req.target.name.replace(/_drafts$/, '')]
204
+ ? this.model.definitions[req.target.name] || this.model.definitions[req.target.name.replace(/_drafts$/, '')]
205
205
  : req.target
206
206
  if (!target || target._unresolved) return
207
207
 
@@ -333,6 +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
337
  const errors = await _checkReferenceIntegrity(entity, data, req, csn, run)
337
338
  if (errors && errors.length !== 0) for (const err of errors) req.error(err)
338
339
  }
@@ -18,6 +18,7 @@ const _convert = (columns, target, model, alias) => {
18
18
  if (element) {
19
19
  if (element.virtual) {
20
20
  col.as = col.as || col.ref[col.ref.length - 1]
21
+ col.cast = { type: element._type }
21
22
  delete col.ref
22
23
  col.val = (element.default && element.default.val) || null
23
24
  }
@@ -1,5 +1,6 @@
1
1
  const { timestampToISO } = require('../data-conversion/timestamp')
2
2
  const { deepCopyObject } = require('../../common/utils/copy')
3
+ const getError = require('../../common/error')
3
4
 
4
5
  function _arrayWithCount(a, count) {
5
6
  const _map = a.map
@@ -41,8 +42,8 @@ const read = (executeSelectCQN, executeStreamCQN) => (model, dbc, query, req) =>
41
42
  const isoTs = timestampToISO(timestamp)
42
43
 
43
44
  if (query._streaming) {
44
- if (!query.SELECT || (query.SELECT && (!query.SELECT.columns || query.SELECT.columns.length !== 1))) {
45
- req.reject(400)
45
+ if (!query.SELECT || (query.SELECT && !query.SELECT.columns)) {
46
+ throw getError(500, 'Invalid SELECT statement for streaming')
46
47
  }
47
48
  return executeStreamCQN(model, dbc, query, user, locale, isoTs)
48
49
  }