@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.
- package/CHANGELOG.md +59 -2
- package/README.md +1 -1
- package/apis/connect.d.ts +11 -4
- package/apis/core.d.ts +1 -1
- package/apis/csn.d.ts +1 -0
- package/apis/internal/inference.d.ts +15 -2
- package/apis/log.d.ts +10 -0
- package/apis/serve.d.ts +4 -9
- package/apis/services.d.ts +86 -19
- package/bin/build/buildTaskEngine.js +16 -42
- package/bin/build/constants.js +4 -2
- package/bin/build/provider/buildTaskProviderInternal.js +117 -85
- package/bin/build/provider/hana/index.js +6 -1
- package/bin/build/provider/mtx-extension/index.js +74 -34
- package/bin/build/provider/mtx-sidecar/index.js +3 -3
- package/bin/build/provider/nodejs/index.js +2 -2
- package/bin/build/util.js +63 -14
- package/bin/cds-serve.js +6 -0
- package/bin/cds.js +20 -4
- package/bin/deploy/to-hana/cfUtil.js +15 -1
- package/bin/mtx/in-cds.js +2 -9
- package/bin/plugins.js +31 -0
- package/bin/serve.js +12 -12
- package/lib/compile/etc/_localized.js +1 -1
- package/lib/compile/for/lean_drafts.js +22 -6
- package/lib/compile/for/nodejs.js +4 -1
- package/lib/compile/load.js +4 -2
- package/lib/core/index.js +35 -15
- package/lib/dbs/cds-deploy.js +129 -133
- package/lib/env/cds-env.js +25 -17
- package/lib/env/cds-requires.js +10 -40
- package/lib/env/compat.js +12 -0
- package/lib/env/defaults.js +17 -9
- package/lib/env/plugins.js +29 -0
- package/lib/env/schemas/cds-rc.json +14 -0
- package/lib/index.js +3 -0
- package/lib/log/cds-log.js +7 -4
- package/lib/ql/CREATE.js +1 -1
- package/lib/ql/DELETE.js +1 -1
- package/lib/ql/DROP.js +3 -3
- package/lib/ql/INSERT.js +1 -1
- package/lib/ql/Query.js +14 -6
- package/lib/ql/SELECT.js +8 -2
- package/lib/ql/UPDATE.js +1 -1
- package/lib/ql/Whereable.js +1 -1
- package/lib/ql/cds-ql.js +1 -9
- package/lib/req/cds-context.js +1 -4
- package/lib/req/request.js +63 -2
- package/lib/req/response.js +3 -2
- package/lib/srv/bindings.js +69 -71
- package/lib/srv/cds-connect.js +4 -1
- package/lib/srv/cds-serve.js +4 -0
- package/lib/srv/middlewares/index.js +37 -6
- package/lib/srv/protocols/_legacy.js +1 -1
- package/lib/srv/protocols/index.js +1 -1
- package/lib/srv/srv-api.js +4 -6
- package/lib/srv/srv-dispatch.js +4 -3
- package/lib/srv/srv-handlers.js +1 -1
- package/lib/srv/srv-methods.js +8 -2
- package/lib/utils/cds-test.js +4 -1
- package/libx/_runtime/audit/Service.js +8 -9
- package/libx/_runtime/audit/generic/personal/index.js +1 -1
- package/libx/_runtime/audit/generic/personal/utils.js +1 -1
- package/libx/_runtime/audit/utils/v2.js +17 -20
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +2 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +11 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +0 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +3 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +4 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/oDataConfiguration.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +1 -1
- package/libx/_runtime/cds-services/services/Service.js +1 -1
- package/libx/_runtime/cds-services/util/assert.js +41 -65
- package/libx/_runtime/common/code-ext/WorkerPool.js +90 -0
- package/libx/_runtime/common/code-ext/WorkerReq.js +0 -4
- package/libx/_runtime/common/code-ext/execute.js +28 -18
- package/libx/_runtime/common/code-ext/handlers.js +5 -4
- package/libx/_runtime/common/code-ext/worker.js +45 -3
- package/libx/_runtime/common/code-ext/workerQueryExecutor.js +8 -7
- package/libx/_runtime/common/composition/delete.js +1 -1
- package/libx/_runtime/common/composition/update.js +3 -5
- package/libx/_runtime/common/generic/auth/expand.js +1 -1
- package/libx/_runtime/common/generic/auth/readOnly.js +5 -4
- package/libx/_runtime/common/generic/auth/restrict.js +7 -2
- package/libx/_runtime/common/generic/crud.js +12 -1
- package/libx/_runtime/common/generic/etag.js +11 -3
- package/libx/_runtime/common/generic/input.js +8 -6
- package/libx/_runtime/common/generic/paging.js +25 -8
- package/libx/_runtime/common/generic/put.js +1 -1
- package/libx/_runtime/common/generic/sorting.js +0 -1
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/common/utils/cqn.js +5 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +2 -2
- package/libx/_runtime/common/utils/resolveView.js +14 -10
- package/libx/_runtime/common/utils/rewriteAsterisks.js +2 -3
- package/libx/_runtime/common/utils/templateProcessor.js +15 -17
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +18 -6
- package/libx/_runtime/db/Service.js +1 -0
- package/libx/_runtime/db/data-conversion/post-processing.js +0 -18
- package/libx/_runtime/db/expand/expand-v2.js +2 -2
- package/libx/_runtime/db/expand/rawToExpanded.js +6 -6
- package/libx/_runtime/db/generic/integrity.js +1 -1
- package/libx/_runtime/db/utils/columns.js +5 -5
- package/libx/_runtime/fiori/generic/activate.js +3 -3
- package/libx/_runtime/fiori/generic/edit.js +1 -1
- package/libx/_runtime/fiori/generic/new.js +4 -0
- package/libx/_runtime/fiori/lean-draft.js +138 -46
- package/libx/_runtime/hana/execute.js +3 -1
- package/libx/_runtime/hana/pool.js +10 -2
- package/libx/_runtime/messaging/common-utils/AMQPClient.js +6 -1
- package/libx/_runtime/messaging/enterprise-messaging.js +1 -0
- package/libx/_runtime/remote/Service.js +16 -13
- package/libx/_runtime/remote/utils/client.js +6 -1
- package/libx/_runtime/sqlite/Service.js +5 -59
- package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +1 -0
- package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -2
- package/libx/_runtime/sqlite/execute.js +3 -1
- package/libx/_runtime/types/api.js +12 -3
- package/libx/odata/afterburner.js +36 -0
- package/libx/odata/cqn2odata.js +1 -1
- package/libx/odata/grammar.pegjs +5 -3
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +1 -1
- package/libx/rest/RestAdapter.js +1 -1
- package/libx/rest/RestRequest.js +1 -0
- package/package.json +5 -2
- package/libx/_runtime/common/code-ext/workerQuery.js +0 -45
- package/libx/_runtime/common/constants/limit.js +0 -12
- 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?.
|
|
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.
|
|
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,
|
|
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
|
|
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
|
-
|
|
110
|
-
|
|
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
|
-
|
|
141
|
-
|
|
142
|
-
|
|
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
|
-
|
|
220
|
-
}
|
|
198
|
+
const _isNotFilled = value =>
|
|
199
|
+
value === null || value === undefined || (typeof value === 'string' && value.trim() === '')
|
|
221
200
|
|
|
222
|
-
const _checkMandatoryElement = (element, value, errors, key,
|
|
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,
|
|
204
|
+
errors.push(assertError(ASSERT_NOT_NULL, element, value, key, pathSegmentsInfo))
|
|
226
205
|
}
|
|
227
206
|
}
|
|
228
207
|
|
|
229
|
-
const _isNavigationColumn = (column, searched) =>
|
|
230
|
-
|
|
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
|
-
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
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,
|
|
246
|
+
const checkInputConstraints = ({ element, value, errors, key, pathSegmentsInfo }) => {
|
|
272
247
|
if (!element) return errors
|
|
248
|
+
let path
|
|
273
249
|
|
|
274
|
-
|
|
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,
|
|
279
|
-
|
|
280
|
-
|
|
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://
|
|
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,
|
|
335
|
+
const { row, pathSegmentsInfo } = target.assocInfo
|
|
362
336
|
const key = target.foreignKey.name
|
|
363
|
-
|
|
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
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
-
const
|
|
3
|
-
const
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
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)
|
|
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.
|
|
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
|
|
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.
|
|
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
|
|
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
|
-
|
|
14
|
-
|
|
13
|
+
executionContextMap.get(id)(result)
|
|
14
|
+
executionContextMap.delete(id)
|
|
15
15
|
return
|
|
16
16
|
|
|
17
17
|
case 'cleanup':
|
|
18
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|