@sap/cds 6.6.2 → 6.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (130) hide show
  1. package/CHANGELOG.md +59 -2
  2. package/README.md +1 -1
  3. package/apis/connect.d.ts +11 -4
  4. package/apis/core.d.ts +1 -1
  5. package/apis/csn.d.ts +1 -0
  6. package/apis/internal/inference.d.ts +15 -2
  7. package/apis/log.d.ts +10 -0
  8. package/apis/serve.d.ts +4 -9
  9. package/apis/services.d.ts +86 -19
  10. package/bin/build/buildTaskEngine.js +16 -42
  11. package/bin/build/constants.js +4 -2
  12. package/bin/build/provider/buildTaskProviderInternal.js +117 -85
  13. package/bin/build/provider/hana/index.js +6 -1
  14. package/bin/build/provider/mtx-extension/index.js +74 -34
  15. package/bin/build/provider/mtx-sidecar/index.js +3 -3
  16. package/bin/build/provider/nodejs/index.js +2 -2
  17. package/bin/build/util.js +63 -14
  18. package/bin/cds-serve.js +6 -0
  19. package/bin/cds.js +20 -4
  20. package/bin/deploy/to-hana/cfUtil.js +15 -1
  21. package/bin/mtx/in-cds.js +2 -9
  22. package/bin/plugins.js +31 -0
  23. package/bin/serve.js +12 -12
  24. package/lib/compile/etc/_localized.js +1 -1
  25. package/lib/compile/for/lean_drafts.js +22 -6
  26. package/lib/compile/for/nodejs.js +4 -1
  27. package/lib/compile/load.js +4 -2
  28. package/lib/core/index.js +35 -15
  29. package/lib/dbs/cds-deploy.js +129 -133
  30. package/lib/env/cds-env.js +25 -17
  31. package/lib/env/cds-requires.js +10 -40
  32. package/lib/env/compat.js +12 -0
  33. package/lib/env/defaults.js +17 -9
  34. package/lib/env/plugins.js +29 -0
  35. package/lib/env/schemas/cds-rc.json +14 -0
  36. package/lib/index.js +3 -0
  37. package/lib/log/cds-log.js +7 -4
  38. package/lib/ql/CREATE.js +1 -1
  39. package/lib/ql/DELETE.js +1 -1
  40. package/lib/ql/DROP.js +3 -3
  41. package/lib/ql/INSERT.js +1 -1
  42. package/lib/ql/Query.js +14 -6
  43. package/lib/ql/SELECT.js +8 -2
  44. package/lib/ql/UPDATE.js +1 -1
  45. package/lib/ql/Whereable.js +1 -1
  46. package/lib/ql/cds-ql.js +1 -9
  47. package/lib/req/cds-context.js +1 -4
  48. package/lib/req/request.js +63 -2
  49. package/lib/req/response.js +3 -2
  50. package/lib/srv/bindings.js +69 -71
  51. package/lib/srv/cds-connect.js +4 -1
  52. package/lib/srv/cds-serve.js +4 -0
  53. package/lib/srv/middlewares/index.js +37 -6
  54. package/lib/srv/protocols/_legacy.js +1 -1
  55. package/lib/srv/protocols/index.js +1 -1
  56. package/lib/srv/srv-api.js +4 -6
  57. package/lib/srv/srv-dispatch.js +4 -3
  58. package/lib/srv/srv-handlers.js +1 -1
  59. package/lib/srv/srv-methods.js +8 -2
  60. package/lib/utils/cds-test.js +4 -1
  61. package/libx/_runtime/audit/Service.js +8 -9
  62. package/libx/_runtime/audit/generic/personal/index.js +1 -1
  63. package/libx/_runtime/audit/generic/personal/utils.js +1 -1
  64. package/libx/_runtime/audit/utils/v2.js +17 -20
  65. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -0
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +11 -4
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +0 -1
  68. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +3 -3
  69. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  70. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +4 -4
  71. package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +2 -2
  72. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +1 -1
  73. package/libx/_runtime/cds-services/services/Service.js +1 -1
  74. package/libx/_runtime/cds-services/util/assert.js +41 -65
  75. package/libx/_runtime/common/code-ext/WorkerPool.js +90 -0
  76. package/libx/_runtime/common/code-ext/WorkerReq.js +0 -4
  77. package/libx/_runtime/common/code-ext/execute.js +28 -18
  78. package/libx/_runtime/common/code-ext/handlers.js +5 -4
  79. package/libx/_runtime/common/code-ext/worker.js +45 -3
  80. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +8 -7
  81. package/libx/_runtime/common/composition/delete.js +1 -1
  82. package/libx/_runtime/common/composition/update.js +3 -5
  83. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  84. package/libx/_runtime/common/generic/auth/readOnly.js +5 -4
  85. package/libx/_runtime/common/generic/auth/restrict.js +7 -2
  86. package/libx/_runtime/common/generic/crud.js +12 -1
  87. package/libx/_runtime/common/generic/etag.js +11 -3
  88. package/libx/_runtime/common/generic/input.js +8 -6
  89. package/libx/_runtime/common/generic/paging.js +25 -8
  90. package/libx/_runtime/common/generic/put.js +1 -1
  91. package/libx/_runtime/common/generic/sorting.js +0 -1
  92. package/libx/_runtime/common/i18n/messages.properties +1 -0
  93. package/libx/_runtime/common/utils/cqn.js +5 -1
  94. package/libx/_runtime/common/utils/cqn2cqn4sql.js +2 -2
  95. package/libx/_runtime/common/utils/resolveView.js +14 -10
  96. package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -3
  97. package/libx/_runtime/common/utils/templateProcessor.js +15 -17
  98. package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +18 -6
  99. package/libx/_runtime/db/Service.js +1 -0
  100. package/libx/_runtime/db/data-conversion/post-processing.js +0 -18
  101. package/libx/_runtime/db/expand/expand-v2.js +2 -2
  102. package/libx/_runtime/db/expand/rawToExpanded.js +6 -6
  103. package/libx/_runtime/db/generic/integrity.js +1 -1
  104. package/libx/_runtime/db/utils/columns.js +5 -5
  105. package/libx/_runtime/fiori/generic/activate.js +3 -3
  106. package/libx/_runtime/fiori/generic/edit.js +1 -1
  107. package/libx/_runtime/fiori/generic/new.js +4 -0
  108. package/libx/_runtime/fiori/lean-draft.js +138 -46
  109. package/libx/_runtime/hana/execute.js +3 -1
  110. package/libx/_runtime/hana/pool.js +10 -2
  111. package/libx/_runtime/messaging/common-utils/AMQPClient.js +6 -1
  112. package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
  113. package/libx/_runtime/remote/Service.js +16 -13
  114. package/libx/_runtime/remote/utils/client.js +6 -1
  115. package/libx/_runtime/sqlite/Service.js +5 -59
  116. package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
  117. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -2
  118. package/libx/_runtime/sqlite/execute.js +3 -1
  119. package/libx/_runtime/types/api.js +12 -3
  120. package/libx/odata/afterburner.js +36 -0
  121. package/libx/odata/cqn2odata.js +1 -1
  122. package/libx/odata/grammar.pegjs +5 -3
  123. package/libx/odata/parser.js +1 -1
  124. package/libx/odata/utils.js +1 -1
  125. package/libx/rest/RestAdapter.js +1 -1
  126. package/libx/rest/RestRequest.js +1 -0
  127. package/package.json +5 -2
  128. package/libx/_runtime/common/code-ext/workerQuery.js +0 -45
  129. package/libx/_runtime/common/constants/limit.js +0 -12
  130. package/libx/_runtime/common/utils/page.js +0 -39
@@ -273,7 +273,7 @@ const _pick = options => (element, target) => {
273
273
 
274
274
  if (element['@odata.etag']) categories.push('@odata.etag')
275
275
  if (element._type === 'cds.Decimal') categories.push('@cds.Decimal')
276
- if (cds.db?.kind === 'better-sqlite' && element._type === 'cds.Boolean') categories.push('@cds.Boolean')
276
+ if (cds.db?.cqn2sql && element._type === 'cds.Boolean') categories.push('@cds.Boolean') // REVISIT: violates modularization -> do we still need that?
277
277
 
278
278
  categories.push(..._assocs(element, target))
279
279
 
@@ -53,7 +53,7 @@ class ApplicationService extends cds.Service {
53
53
  }
54
54
 
55
55
  registerFioriHandlers() {
56
- if (cds.env.features.lean_draft) {
56
+ if (cds.env.fiori.lean_draft) {
57
57
  const { onNew, onPrepare, onEdit, onCancel } = require('../../fiori/lean-draft')
58
58
 
59
59
  for (const each of this.entities)
@@ -1,5 +1,6 @@
1
1
  const cds = require('../../cds')
2
2
  const LOG = cds.log('app')
3
+ const templatePathSerializer = require('../../common/utils/templateProcessorPathSerializer')
3
4
 
4
5
  // REVISIT: replace with cds.Request
5
6
  const getEntry = require('../../common/error/entry')
@@ -42,7 +43,7 @@ const _enumValues = element => {
42
43
  }
43
44
 
44
45
  // REVISIT: this needs a cleanup!
45
- const assertError = (code, element, value, key, pathSegments = []) => {
46
+ const assertError = (code, element, value, key, path) => {
46
47
  let args
47
48
 
48
49
  if (typeof code === 'object') {
@@ -51,14 +52,12 @@ const assertError = (code, element, value, key, pathSegments = []) => {
51
52
  }
52
53
 
53
54
  const { name, type, precision, scale } = element
54
- const path = `${pathSegments.join('/')}${pathSegments.length ? '/' : ''}${name || key}`
55
-
56
55
  const error = new Error()
57
56
  const errorEntry = {
58
57
  code,
59
58
  message: code,
60
- target: path,
61
- args: args || [name || key]
59
+ target: path ?? element.name ?? key,
60
+ args: args ?? [name ?? key]
62
61
  }
63
62
 
64
63
  const assertError = Object.assign(error, getEntry(errorEntry))
@@ -81,13 +80,9 @@ const assertError = (code, element, value, key, pathSegments = []) => {
81
80
  return assertError
82
81
  }
83
82
 
84
- const _checkString = value => {
85
- return typeof value === 'string'
86
- }
83
+ const _checkString = value => typeof value === 'string'
87
84
 
88
- const _checkNumber = value => {
89
- return typeof value === 'number'
90
- }
85
+ const _checkNumber = value => typeof value === 'number'
91
86
 
92
87
  const _checkDecimal = (value, element) => {
93
88
  const [left, right] = String(value).split('.')
@@ -98,38 +93,24 @@ const _checkDecimal = (value, element) => {
98
93
  )
99
94
  }
100
95
 
101
- const _checkInteger = value => {
102
- return _checkNumber(value) && parseInt(value, 10) === value
103
- }
96
+ const _checkInteger = value => _checkNumber(value) && parseInt(value, 10) === value
104
97
 
105
- const _checkBoolean = value => {
106
- return typeof value === 'boolean'
107
- }
98
+ const _checkBoolean = value => typeof value === 'boolean'
108
99
 
109
- const _checkBuffer = value => {
110
- // REVISIT: Extension parameter in push is an object with buffer data
111
- return Buffer.isBuffer(value) || value.type === 'Buffer'
112
- }
100
+ // REVISIT: Extension parameter in push is an object with buffer data
101
+ const _checkBuffer = value => Buffer.isBuffer(value) || value.type === 'Buffer'
113
102
 
114
103
  const _checkUUID = value => {
115
104
  return _checkString(value) && UUID_REGEX.test(value)
116
105
  }
117
106
 
118
- const _checkISODate = value => {
119
- return (_checkString(value) && ISO_DATE_REGEX.test(value)) || value instanceof Date
120
- }
107
+ const _checkISODate = value => (_checkString(value) && ISO_DATE_REGEX.test(value)) || value instanceof Date
121
108
 
122
- const _checkISOTime = value => {
123
- return _checkString(value) && ISO_TIME_REGEX.test(value)
124
- }
109
+ const _checkISOTime = value => _checkString(value) && ISO_TIME_REGEX.test(value)
125
110
 
126
- const _checkISODateTime = value => {
127
- return (_checkString(value) && ISO_DATE_TIME_REGEX.test(value)) || value instanceof Date
128
- }
111
+ const _checkISODateTime = value => (_checkString(value) && ISO_DATE_TIME_REGEX.test(value)) || value instanceof Date
129
112
 
130
- const _checkISOTimestamp = value => {
131
- return (_checkString(value) && ISO_TIMESTAMP_REGEX.test(value)) || value instanceof Date
132
- }
113
+ const _checkISOTimestamp = value => (_checkString(value) && ISO_TIMESTAMP_REGEX.test(value)) || value instanceof Date
133
114
 
134
115
  const _checkInRange = (val, range) => {
135
116
  return _checkISODate(val)
@@ -137,10 +118,9 @@ const _checkInRange = (val, range) => {
137
118
  : (val - range[0]) * (val - range[1]) <= 0
138
119
  }
139
120
 
140
- const _checkRegExpFormat = (val, format) => {
141
- // process.env.CDS_ASSERT_FORMAT_FLAGS not official!
142
- return _checkString(val) && val.match(new RegExp(format, process.env.CDS_ASSERT_FORMAT_FLAGS || 'u'))
143
- }
121
+ // process.env.CDS_ASSERT_FORMAT_FLAGS not official!
122
+ const _checkRegExpFormat = (val, format) =>
123
+ _checkString(val) && val.match(new RegExp(format, process.env.CDS_ASSERT_FORMAT_FLAGS || 'u'))
144
124
 
145
125
  const CDS_TYPE_CHECKS = {
146
126
  'cds.UUID': _checkUUID,
@@ -215,28 +195,23 @@ const checkStaticElementByKey = (definition, key, value, result = [], ignoreNonM
215
195
  return result
216
196
  }
217
197
 
218
- const _isNotFilled = value => {
219
- return value === null || value === undefined || (typeof value === 'string' && value.trim() === '')
220
- }
198
+ const _isNotFilled = value =>
199
+ value === null || value === undefined || (typeof value === 'string' && value.trim() === '')
221
200
 
222
- const _checkMandatoryElement = (element, value, errors, key, pathSegments) => {
201
+ const _checkMandatoryElement = (element, value, errors, key, pathSegmentsInfo) => {
223
202
  if (element.parent?.query?.SELECT?.columns?.find(col => _isNavigationColumn(col, element.name))) return
224
203
  if (element._isMandatory && !element.default && _isNotFilled(value)) {
225
- errors.push(assertError(ASSERT_NOT_NULL, element, value, key, pathSegments))
204
+ errors.push(assertError(ASSERT_NOT_NULL, element, value, key, pathSegmentsInfo))
226
205
  }
227
206
  }
228
207
 
229
- const _isNavigationColumn = (column, searched) => {
230
- return (
231
- column.ref && column.ref.length > 1 && (column.as === searched || column.ref[column.ref.length - 1] === searched)
232
- )
233
- }
208
+ const _isNavigationColumn = (column, searched) =>
209
+ column.ref?.length > 1 && (column.as === searched || column.ref[column.ref.length - 1] === searched)
234
210
 
235
- const _getEnumElement = element => {
236
- return (element['@assert.range'] && element.enum) || element['@assert.enum'] ? element.enum : undefined
237
- }
211
+ const _getEnumElement = element =>
212
+ (element['@assert.range'] && element.enum) || element['@assert.enum'] ? element.enum : undefined
238
213
 
239
- const _checkEnumElement = (element, value, errors, key, pathSegments) => {
214
+ const _checkEnumElement = (element, value, errors, key, pathSegmentsInfo) => {
240
215
  const enumElements = _getEnumElement(element)
241
216
  const enumValues = enumElements && _enumValues(enumElements)
242
217
 
@@ -246,15 +221,15 @@ const _checkEnumElement = (element, value, errors, key, pathSegments) => {
246
221
  ? ['"' + value + '"', enumValues.map(ele => '"' + ele + '"').join(', ')]
247
222
  : [value, enumValues.join(', ')]
248
223
 
249
- errors.push(assertError({ code: ASSERT_ENUM, args }, element, value, key, pathSegments))
224
+ errors.push(assertError({ code: ASSERT_ENUM, args }, element, value, key, pathSegmentsInfo))
250
225
  }
251
226
  }
252
227
 
253
- const _checkRangeElement = (element, value, errors, key, pathSegments) => {
228
+ const _checkRangeElement = (element, value, errors, key, pathSegmentsInfo) => {
254
229
  const rangeElements = element['@assert.range'] && !_getEnumElement(element) ? element['@assert.range'] : undefined
255
230
  if (rangeElements && !_checkInRange(value, rangeElements)) {
256
231
  const args = [value, ...element['@assert.range']]
257
- errors.push(assertError({ code: ASSERT_RANGE, args }, element, value, key, pathSegments))
232
+ errors.push(assertError({ code: ASSERT_RANGE, args }, element, value, key, pathSegmentsInfo))
258
233
  }
259
234
  }
260
235
 
@@ -268,19 +243,18 @@ const _checkFormatElement = (element, value, errors, key, pathSegments) => {
268
243
  /**
269
244
  * @param {import('../../types/api').InputConstraints} constraints
270
245
  */
271
- const checkInputConstraints = ({ element, value, errors, key, pathSegments }) => {
246
+ const checkInputConstraints = ({ element, value, errors, key, pathSegmentsInfo }) => {
272
247
  if (!element) return errors
248
+ let path
273
249
 
274
- _checkMandatoryElement(element, value, errors, key, pathSegments)
250
+ if (pathSegmentsInfo?.length) path = templatePathSerializer(element.name || key, pathSegmentsInfo)
251
+ _checkMandatoryElement(element, value, errors, key, path)
275
252
 
276
253
  if (value == null) return errors
277
254
 
278
- _checkEnumElement(element, value, errors, key, pathSegments)
279
-
280
- _checkRangeElement(element, value, errors, key, pathSegments)
281
-
282
- _checkFormatElement(element, value, errors, key, pathSegments)
283
-
255
+ _checkEnumElement(element, value, errors, key, path)
256
+ _checkRangeElement(element, value, errors, key, path)
257
+ _checkFormatElement(element, value, errors, key, path)
284
258
  return errors
285
259
  }
286
260
 
@@ -323,7 +297,7 @@ const assertNotNullError = element => assertError(ASSERT_NOT_NULL, element)
323
297
  *
324
298
  * @param {import('../../types/api').assertTargetMap} assertMap
325
299
  * @param {array} errors An array to appends the possible errors.
326
- * @see {@link https://pages.github.tools.sap/cap/docs/guides/providing-services#assert-target @assert.target} for
300
+ * @see {@link https://cap.cloud.sap/docs/guides/providing-services#assert-target @assert.target} for
327
301
  * further information.
328
302
  */
329
303
  const assertTargets = async (assertMap, errors) => {
@@ -358,9 +332,11 @@ const assertTargets = async (assertMap, errors) => {
358
332
  allTargets
359
333
  .filter(t => t.key === target.key)
360
334
  .forEach(target => {
361
- const { row, pathSegments } = target.assocInfo
335
+ const { row, pathSegmentsInfo } = target.assocInfo
362
336
  const key = target.foreignKey.name
363
- const error = assertError('ASSERT_TARGET', target.foreignKey, row[key], key, pathSegments)
337
+ let path
338
+ if (pathSegmentsInfo?.length) path = templatePathSerializer(key, pathSegmentsInfo)
339
+ const error = assertError('ASSERT_TARGET', target.foreignKey, row[key], key, path)
364
340
  errors.push(error)
365
341
  })
366
342
  })
@@ -0,0 +1,90 @@
1
+ const cds = require('../../cds')
2
+ const os = require('os')
3
+ const { Worker } = require('worker_threads')
4
+
5
+ class ExtensionWorker extends Worker {
6
+ constructor(id, workerPath, options) {
7
+ super(workerPath, options)
8
+ this.id = id
9
+ this.tasksAssigned = 0
10
+ }
11
+ }
12
+
13
+ class WorkerPool {
14
+ static instances = []
15
+ constructor(workerPath, options) {
16
+ this.workerPath = workerPath
17
+ this.options = options
18
+ this.size = options.size ?? Math.max(os.cpus().length, 1)
19
+ this.idleWorkers = new Set()
20
+ this.workers = []
21
+ WorkerPool.instances.push(this)
22
+ }
23
+
24
+ #createWorker() {
25
+ const id = cds.utils.uuid()
26
+ const worker = new ExtensionWorker(id, this.workerPath, {
27
+ workerData: { id },
28
+ resourceLimits: this.options.resourceLimits
29
+ })
30
+ worker.on('exit', this.#onWorkerExit.bind(this, worker))
31
+ return worker
32
+ }
33
+
34
+ #onWorkerExit(worker) {
35
+ this.idleWorkers.delete(worker)
36
+ this.workers.splice(this.workers.indexOf(worker), 1)
37
+ worker.tasksAssigned = 0
38
+ }
39
+
40
+ adquire() {
41
+ if (this.idleWorkers.size === 0 && this.workers.length < this.size) {
42
+ const worker = this.#createWorker()
43
+ this.idleWorkers.add(worker)
44
+ this.workers.push(worker)
45
+ }
46
+
47
+ const worker = this.idleWorkers.values().next().value
48
+
49
+ if (worker) {
50
+ this.idleWorkers.delete(worker)
51
+ worker.tasksAssigned++
52
+ return worker
53
+ }
54
+
55
+ const randomWorkerIndex = Math.floor(Math.random() * this.workers.length)
56
+ const busyWorker = this.workers[randomWorkerIndex]
57
+ busyWorker.tasksAssigned++
58
+ return busyWorker
59
+ }
60
+
61
+ release(worker) {
62
+ if (worker.tasksAssigned === 0) return
63
+
64
+ worker.tasksAssigned--
65
+ if (worker.tasksAssigned === 0) {
66
+ this.idleWorkers.add(worker)
67
+ }
68
+ }
69
+
70
+ async destroy() {
71
+ if (this.workers.length === 0) return
72
+
73
+ const workers = Array.from(this.workers)
74
+ const iterable = workers.map(worker => {
75
+ worker.removeAllListeners()
76
+ return worker.terminate()
77
+ })
78
+
79
+ await Promise.all(iterable)
80
+ this.idleWorkers = new Set()
81
+ this.workers = []
82
+ WorkerPool.instances = []
83
+ }
84
+
85
+ static async destroyAll() {
86
+ for (const workerPool of WorkerPool.instances) await workerPool.destroy()
87
+ }
88
+ }
89
+
90
+ module.exports = WorkerPool
@@ -71,10 +71,6 @@ class WorkerReq {
71
71
  prop: 'reject',
72
72
  args
73
73
  })
74
-
75
- let error = Responses.get(4, ...args)
76
- if (!error.stack) Error.captureStackTrace((error = Object.assign(new Error(), error)), this.reject)
77
- throw error
78
74
  }
79
75
  }
80
76
 
@@ -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