@sap/cds 6.6.1 → 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 (132) hide show
  1. package/CHANGELOG.md +67 -3
  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/deploy/to-hana/hana.js +1 -1
  22. package/bin/deploy/to-hana/hdiDeployUtil.js +1 -1
  23. package/bin/mtx/in-cds.js +2 -9
  24. package/bin/plugins.js +31 -0
  25. package/bin/serve.js +12 -12
  26. package/lib/compile/etc/_localized.js +1 -1
  27. package/lib/compile/for/lean_drafts.js +22 -6
  28. package/lib/compile/for/nodejs.js +4 -1
  29. package/lib/compile/load.js +4 -2
  30. package/lib/core/index.js +35 -15
  31. package/lib/dbs/cds-deploy.js +129 -133
  32. package/lib/env/cds-env.js +25 -17
  33. package/lib/env/cds-requires.js +10 -40
  34. package/lib/env/compat.js +12 -0
  35. package/lib/env/defaults.js +17 -9
  36. package/lib/env/plugins.js +29 -0
  37. package/lib/env/schemas/cds-rc.json +14 -0
  38. package/lib/index.js +3 -0
  39. package/lib/log/cds-log.js +7 -4
  40. package/lib/ql/CREATE.js +1 -1
  41. package/lib/ql/DELETE.js +1 -1
  42. package/lib/ql/DROP.js +3 -3
  43. package/lib/ql/INSERT.js +1 -1
  44. package/lib/ql/Query.js +14 -6
  45. package/lib/ql/SELECT.js +8 -2
  46. package/lib/ql/UPDATE.js +1 -1
  47. package/lib/ql/Whereable.js +1 -1
  48. package/lib/ql/cds-ql.js +1 -9
  49. package/lib/req/cds-context.js +1 -4
  50. package/lib/req/request.js +63 -2
  51. package/lib/req/response.js +3 -2
  52. package/lib/srv/bindings.js +69 -71
  53. package/lib/srv/cds-connect.js +4 -1
  54. package/lib/srv/cds-serve.js +4 -0
  55. package/lib/srv/middlewares/index.js +37 -6
  56. package/lib/srv/protocols/_legacy.js +1 -1
  57. package/lib/srv/protocols/index.js +1 -1
  58. package/lib/srv/srv-api.js +4 -6
  59. package/lib/srv/srv-dispatch.js +4 -3
  60. package/lib/srv/srv-handlers.js +1 -1
  61. package/lib/srv/srv-methods.js +8 -2
  62. package/lib/utils/cds-test.js +4 -1
  63. package/libx/_runtime/audit/Service.js +8 -9
  64. package/libx/_runtime/audit/generic/personal/index.js +1 -1
  65. package/libx/_runtime/audit/generic/personal/utils.js +1 -1
  66. package/libx/_runtime/audit/utils/v2.js +17 -20
  67. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -0
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +11 -4
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +0 -1
  70. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +3 -3
  71. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  72. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +4 -4
  73. package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +2 -2
  74. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +1 -1
  75. package/libx/_runtime/cds-services/services/Service.js +1 -1
  76. package/libx/_runtime/cds-services/util/assert.js +41 -65
  77. package/libx/_runtime/common/code-ext/WorkerPool.js +90 -0
  78. package/libx/_runtime/common/code-ext/WorkerReq.js +0 -4
  79. package/libx/_runtime/common/code-ext/execute.js +28 -18
  80. package/libx/_runtime/common/code-ext/handlers.js +5 -4
  81. package/libx/_runtime/common/code-ext/worker.js +45 -3
  82. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +8 -7
  83. package/libx/_runtime/common/composition/delete.js +1 -1
  84. package/libx/_runtime/common/composition/update.js +3 -5
  85. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  86. package/libx/_runtime/common/generic/auth/readOnly.js +5 -4
  87. package/libx/_runtime/common/generic/auth/restrict.js +7 -2
  88. package/libx/_runtime/common/generic/crud.js +12 -1
  89. package/libx/_runtime/common/generic/etag.js +11 -3
  90. package/libx/_runtime/common/generic/input.js +8 -6
  91. package/libx/_runtime/common/generic/paging.js +25 -8
  92. package/libx/_runtime/common/generic/put.js +1 -1
  93. package/libx/_runtime/common/generic/sorting.js +0 -1
  94. package/libx/_runtime/common/i18n/messages.properties +1 -0
  95. package/libx/_runtime/common/utils/cqn.js +5 -1
  96. package/libx/_runtime/common/utils/cqn2cqn4sql.js +2 -2
  97. package/libx/_runtime/common/utils/resolveView.js +14 -10
  98. package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -3
  99. package/libx/_runtime/common/utils/templateProcessor.js +15 -17
  100. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +18 -6
  101. package/libx/_runtime/db/Service.js +1 -0
  102. package/libx/_runtime/db/data-conversion/post-processing.js +0 -18
  103. package/libx/_runtime/db/expand/expand-v2.js +2 -2
  104. package/libx/_runtime/db/expand/rawToExpanded.js +6 -6
  105. package/libx/_runtime/db/generic/integrity.js +1 -1
  106. package/libx/_runtime/db/utils/columns.js +5 -5
  107. package/libx/_runtime/fiori/generic/activate.js +3 -3
  108. package/libx/_runtime/fiori/generic/edit.js +1 -1
  109. package/libx/_runtime/fiori/generic/new.js +4 -0
  110. package/libx/_runtime/fiori/lean-draft.js +138 -46
  111. package/libx/_runtime/hana/execute.js +3 -1
  112. package/libx/_runtime/hana/pool.js +10 -2
  113. package/libx/_runtime/messaging/common-utils/AMQPClient.js +6 -1
  114. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  115. package/libx/_runtime/remote/Service.js +16 -13
  116. package/libx/_runtime/remote/utils/client.js +6 -1
  117. package/libx/_runtime/sqlite/Service.js +5 -59
  118. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
  119. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -2
  120. package/libx/_runtime/sqlite/execute.js +3 -1
  121. package/libx/_runtime/types/api.js +12 -3
  122. package/libx/odata/afterburner.js +36 -0
  123. package/libx/odata/cqn2odata.js +1 -1
  124. package/libx/odata/grammar.pegjs +5 -3
  125. package/libx/odata/parser.js +1 -1
  126. package/libx/odata/utils.js +1 -1
  127. package/libx/rest/RestAdapter.js +1 -1
  128. package/libx/rest/RestRequest.js +1 -0
  129. package/package.json +5 -2
  130. package/libx/_runtime/common/code-ext/workerQuery.js +0 -45
  131. package/libx/_runtime/common/constants/limit.js +0 -12
  132. package/libx/_runtime/common/utils/page.js +0 -39
@@ -1,11 +1,12 @@
1
1
  const cds = require('../../cds')
2
- const { Worker } = require('worker_threads')
3
- const path = require('node:path')
2
+ const path = require('path')
3
+ const { AsyncResource } = require('async_hooks')
4
4
  const { timeout, resourceLimits } = require('./config')
5
+ const WorkerPool = require('./WorkerPool')
5
6
  const workerPath = path.resolve(__dirname, 'worker.js')
7
+ const workerPool = new WorkerPool(workerPath, { resourceLimits })
6
8
  const { Errors } = require('../../../../lib/req/response')
7
9
  const LOG = cds.log()
8
-
9
10
  const _getReqData = req => {
10
11
  return {
11
12
  data: req.data,
@@ -30,21 +31,20 @@ module.exports = async function executeCode(code, req) {
30
31
  // no default
31
32
  }
32
33
  }
33
- const workerId = cds.utils.uuid()
34
- const contextId = cds.utils.uuid()
35
- const worker = new Worker(workerPath, {
36
- workerData: { id: workerId },
37
- resourceLimits
38
- })
39
34
 
35
+ const worker = workerPool.adquire()
36
+ const workerId = worker.id
37
+ const contextId = cds.utils.uuid()
40
38
  const executePromise = new Promise(function executeCodePromiseExecutor(resolve, reject) {
41
- worker.on('online', onStarted)
42
- worker.on('message', onMessageReceived)
43
- worker.on('error', onError)
44
- worker.on('exit', onExit)
39
+ queueMicrotask(AsyncResource.bind(onStarted))
40
+ const onMessageReceivedProxy = AsyncResource.bind(onMessageReceived)
41
+ const onErrorProxy = AsyncResource.bind(onError)
42
+ const onExitProxy = AsyncResource.bind(onExit)
43
+ worker.on('message', onMessageReceivedProxy)
44
+ worker.on('error', onErrorProxy)
45
+ worker.on('exit', onExitProxy)
45
46
 
46
47
  let onStartTimeoutID
47
-
48
48
  function onStarted() {
49
49
  onStartTimeoutID = setTimeout(() => {
50
50
  worker.terminate()
@@ -53,8 +53,11 @@ module.exports = async function executeCode(code, req) {
53
53
  }
54
54
 
55
55
  function onMessageReceived(message) {
56
+ if (message.contextId !== contextId) return
57
+
56
58
  if (LOG._debug)
57
59
  LOG.debug(`Post message received on main thread (code-ext/execute.js) from worker thread`, message)
60
+
58
61
  switch (message.kind) {
59
62
  case 'run':
60
63
  run(message)
@@ -62,7 +65,6 @@ module.exports = async function executeCode(code, req) {
62
65
 
63
66
  case 'success':
64
67
  onSuccess(message)
65
- cleanup()
66
68
  return
67
69
 
68
70
  case 'error':
@@ -77,15 +79,19 @@ module.exports = async function executeCode(code, req) {
77
79
  for (const m of message.postMessages) await run(m)
78
80
  req.data && Object.assign(req.data, message.req.data) // REVISIT: Why Object.assign(...) is a required?
79
81
  req.results = message.req.results
82
+ cleanup()
80
83
  resolve(req.results ?? message.result)
81
84
  }
82
85
 
83
86
  function onError(error) {
87
+ cleanup()
84
88
  reject(error)
85
89
  }
86
90
 
87
91
  function onExit(exitCode) {
88
- if (exitCode !== 0) reject(new Error(`Worker thread stopped with exit code ${exitCode}`))
92
+ if (exitCode !== 0) {
93
+ reject(new Error(`Worker thread stopped with exit code ${exitCode}`))
94
+ }
89
95
  }
90
96
 
91
97
  async function run(message) {
@@ -95,14 +101,18 @@ module.exports = async function executeCode(code, req) {
95
101
  if (message.responseData) worker.postMessage({ id: message.id, kind: 'responseData', result })
96
102
  } catch (error) {
97
103
  if (LOG._debug) LOG.debug(`Calling ${message.target}.${message.prop}(...) throws an error.`, error)
98
- worker.postMessage({ id: message.id, kind: 'cleanup' })
104
+ if (message.id) worker.postMessage({ id: message.id, kind: 'cleanup' })
105
+ cleanup()
99
106
  reject(error)
100
107
  }
101
108
  }
102
109
 
103
110
  function cleanup() {
104
111
  clearTimeout(onStartTimeoutID)
105
- worker.terminate()
112
+ worker.removeListener('message', onMessageReceivedProxy)
113
+ worker.removeListener('error', onErrorProxy)
114
+ worker.removeListener('exit', onExitProxy)
115
+ workerPool.release(worker)
106
116
  }
107
117
  })
108
118
 
@@ -1,6 +1,6 @@
1
1
  const cds = require('../../cds')
2
+ const LOG = cds.log()
2
3
  const executeCode = require('./execute')
3
-
4
4
  const CODE_ANNOTATION = '@extension.code'
5
5
 
6
6
  module.exports = cds.service.impl(function () {
@@ -20,17 +20,18 @@ module.exports = cds.service.impl(function () {
20
20
  if (result == null) return // whether result is null or undefined
21
21
  const code = await getCodeFromAnnotation(req.target.name, req.event, 'after')
22
22
  if (!code) return
23
- await executeCode.call(this, code, req)
23
+ await executeCode.call(this, code, req).catch(error => LOG._debug && LOG.debug(error))
24
24
  })
25
25
 
26
26
  this.before(['CREATE', 'UPDATE', 'DELETE'], async function (req) {
27
27
  const code = await getCodeFromAnnotation(req.target.name, req.event, 'before')
28
28
  if (!code) return
29
- await executeCode.call(this, code, req)
29
+ await executeCode.call(this, code, req).catch(error => LOG._debug && LOG.debug(error))
30
30
  })
31
31
 
32
32
  this.on('*', async function (req, next) {
33
33
  if (this.name.startsWith('cds.xt')) return next()
34
+
34
35
  // REVISIT: req.target -> wait until implementation task finished
35
36
  let fqn = req.target?.actions?.[`${req.event}`] // check for bound action/function
36
37
  if (!fqn) {
@@ -43,7 +44,7 @@ module.exports = cds.service.impl(function () {
43
44
  if (fqn.kind === 'action' || fqn.kind === 'function' || req.constructor.name === 'EventMessage') {
44
45
  const code = await getCodeFromAnnotation(req?.target?.name ?? fqn.name, req.event, 'on')
45
46
  if (!code) return next()
46
- return await executeCode.call(this, code, req)
47
+ return await executeCode.call(this, code, req).catch(error => LOG._debug && LOG.debug(error))
47
48
  }
48
49
  })
49
50
  })
@@ -1,18 +1,60 @@
1
1
  const cds = require('../../cds')
2
2
  const LOG = cds.log()
3
3
  const { parentPort, workerData } = require('worker_threads')
4
- const { WorkerSELECT, WorkerINSERT, WorkerUPSERT, WorkerUPDATE, WorkerDELETE } = require('./workerQuery')
4
+ const SELECT = require('../../../../lib/ql/SELECT')
5
+ const INSERT = require('../../../../lib/ql/INSERT')
6
+ const UPSERT = require('../../../../lib/ql/UPSERT')
7
+ const UPDATE = require('../../../../lib/ql/UPDATE')
8
+ const DELETE = require('../../../../lib/ql/DELETE')
9
+ const queryExecutor = require('./workerQueryExecutor')
5
10
  const WorkerReq = require('./WorkerReq')
6
11
  const { timeout } = require('./config')
7
12
 
8
- parentPort.once('message', function onWorkerMessageReceived(message) {
13
+ parentPort.on('message', function onWorkerMessageReceived(message) {
9
14
  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
15
  if (kind !== 'start' || workerId !== workerData.id) return
16
+ if (LOG._debug) LOG.debug(`Post message received on worker thread (worker.js) from main thread`, message)
12
17
 
13
18
  // eslint-disable-next-line cds/no-missing-dependencies
14
19
  const { VM } = require('vm2')
15
20
  const workerReq = new WorkerReq(contextId, reqData)
21
+
22
+ class WorkerSELECT extends SELECT {
23
+ then(r, e) {
24
+ return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
25
+ }
26
+ }
27
+
28
+ class WorkerINSERT extends INSERT {
29
+ then(r, e) {
30
+ return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
31
+ }
32
+ }
33
+
34
+ class WorkerUPSERT extends UPSERT {
35
+ then(r, e) {
36
+ return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
37
+ }
38
+ }
39
+
40
+ class WorkerUPDATE extends UPDATE {
41
+ then(r, e) {
42
+ return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
43
+ }
44
+ }
45
+
46
+ class WorkerDELETE extends DELETE {
47
+ then(r, e) {
48
+ return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
49
+ }
50
+ }
51
+
52
+ Object.defineProperty(WorkerSELECT.prototype, 'cmd', { value: 'SELECT' })
53
+ Object.defineProperty(WorkerINSERT.prototype, 'cmd', { value: 'INSERT' })
54
+ Object.defineProperty(WorkerUPSERT.prototype, 'cmd', { value: 'UPSERT' })
55
+ Object.defineProperty(WorkerUPDATE.prototype, 'cmd', { value: 'UPDATE' })
56
+ Object.defineProperty(WorkerDELETE.prototype, 'cmd', { value: 'DELETE' })
57
+
16
58
  const vm = new VM({
17
59
  console: 'inherit',
18
60
  timeout, // specifies the number of milliseconds to execute code before terminating execution
@@ -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
@@ -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
  }