@sap/cds 8.6.1 → 8.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 (53) hide show
  1. package/CHANGELOG.md +36 -0
  2. package/_i18n/i18n_en_US_saptrc.properties +4 -7
  3. package/bin/serve.js +3 -1
  4. package/lib/compile/for/lean_drafts.js +1 -1
  5. package/lib/compile/for/nodejs.js +1 -0
  6. package/lib/compile/to/sql.js +12 -8
  7. package/lib/core/classes.js +3 -4
  8. package/lib/core/types.js +1 -0
  9. package/lib/env/cds-requires.js +2 -2
  10. package/lib/ql/cds-ql.js +8 -1
  11. package/lib/ql/cds.ql-Query.js +9 -2
  12. package/lib/req/validate.js +3 -2
  13. package/lib/srv/cds-connect.js +1 -1
  14. package/lib/srv/cds-serve.js +2 -9
  15. package/lib/srv/cds.Service.js +0 -1
  16. package/lib/srv/factory.js +56 -71
  17. package/lib/srv/middlewares/auth/ias-auth.js +44 -14
  18. package/lib/srv/middlewares/auth/jwt-auth.js +45 -16
  19. package/lib/srv/middlewares/auth/xssec.js +1 -1
  20. package/lib/srv/middlewares/errors.js +8 -10
  21. package/lib/utils/cds-utils.js +5 -1
  22. package/lib/utils/tar-lib.js +58 -0
  23. package/libx/_runtime/common/Service.js +0 -4
  24. package/libx/_runtime/common/generic/auth/utils.js +1 -1
  25. package/libx/_runtime/common/generic/input.js +3 -1
  26. package/libx/_runtime/common/utils/csn.js +5 -1
  27. package/libx/_runtime/common/utils/resolveView.js +1 -1
  28. package/libx/_runtime/fiori/lean-draft.js +1 -1
  29. package/libx/_runtime/messaging/enterprise-messaging-shared.js +7 -3
  30. package/libx/odata/middleware/batch.js +22 -23
  31. package/libx/odata/middleware/create.js +4 -0
  32. package/libx/odata/middleware/delete.js +2 -0
  33. package/libx/odata/middleware/operation.js +2 -0
  34. package/libx/odata/middleware/read.js +4 -0
  35. package/libx/odata/middleware/stream.js +4 -0
  36. package/libx/odata/middleware/update.js +4 -0
  37. package/libx/odata/parse/afterburner.js +9 -4
  38. package/libx/odata/parse/grammar.peggy +7 -8
  39. package/libx/odata/parse/multipartToJson.js +0 -1
  40. package/libx/odata/parse/parser.js +1 -1
  41. package/libx/odata/utils/normalizeTimeData.js +43 -0
  42. package/libx/odata/utils/readAfterWrite.js +1 -1
  43. package/libx/outbox/index.js +1 -1
  44. package/libx/rest/RestAdapter.js +20 -4
  45. package/package.json +6 -2
  46. package/lib/srv/protocols/odata-v2.js +0 -26
  47. package/libx/_runtime/common/code-ext/WorkerPool.js +0 -90
  48. package/libx/_runtime/common/code-ext/WorkerReq.js +0 -77
  49. package/libx/_runtime/common/code-ext/config.js +0 -13
  50. package/libx/_runtime/common/code-ext/execute.js +0 -123
  51. package/libx/_runtime/common/code-ext/handlers.js +0 -50
  52. package/libx/_runtime/common/code-ext/worker.js +0 -70
  53. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +0 -37
@@ -1,90 +0,0 @@
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,77 +0,0 @@
1
- const { parentPort } = require('worker_threads')
2
- const { Responses, Errors } = require('../../../../lib/req/response')
3
-
4
- class WorkerReq {
5
- constructor(contextId, reqData) {
6
- this.contextId = contextId
7
- Object.assign(this, reqData)
8
- this.postMessages = []
9
- this.messages = this.messages ?? []
10
- this.errors = this.errors ?? new Errors()
11
- }
12
-
13
- #push(args) {
14
- this.postMessages.push({
15
- kind: 'run',
16
- target: 'req',
17
- ...args
18
- })
19
- }
20
-
21
- notify(...args) {
22
- this.#push({
23
- prop: 'notify',
24
- args
25
- })
26
-
27
- const notify = Responses.get(1, ...args)
28
- this.messages.push(notify)
29
- return notify
30
- }
31
-
32
- info(...args) {
33
- this.#push({
34
- prop: 'info',
35
- args
36
- })
37
-
38
- const info = Responses.get(2, ...args)
39
- this.messages.push(info)
40
- return info
41
- }
42
-
43
- warn(...args) {
44
- this.#push({
45
- prop: 'warn',
46
- args
47
- })
48
-
49
- const warn = Responses.get(3, ...args)
50
- this.messages.push(warn)
51
- return warn
52
- }
53
-
54
- error(...args) {
55
- this.#push({
56
- prop: 'error',
57
- args
58
- })
59
-
60
- let error = Responses.get(4, ...args)
61
- if (!error.stack) Error.captureStackTrace((error = Object.assign(new Error(), error)), this.error)
62
- this.errors.push(error)
63
- return error
64
- }
65
-
66
- reject(...args) {
67
- parentPort.postMessage({
68
- contextId: this.contextId,
69
- kind: 'run',
70
- target: 'req',
71
- prop: 'reject',
72
- args
73
- })
74
- }
75
- }
76
-
77
- module.exports = WorkerReq
@@ -1,13 +0,0 @@
1
- const os = require('os')
2
- const totalMemory = os.totalmem() // total amount of system memory in bytes
3
- const maxOldGenerationSizeMb = Math.floor(totalMemory / 1024 ** 2 / 8) // max size of the main heap in MB
4
- const maxYoungGenerationSizeMb = Math.floor(totalMemory / 1024 ** 2 / 8) // max size of a heap space for recently created objects
5
-
6
- module.exports = {
7
- timeout: 10000,
8
- resourceLimits: {
9
- maxOldGenerationSizeMb,
10
- maxYoungGenerationSizeMb,
11
- stackSizeMb: 4 // default
12
- }
13
- }
@@ -1,123 +0,0 @@
1
- const cds = require('../../cds')
2
- const path = require('path')
3
- const { AsyncResource } = require('async_hooks')
4
- const { timeout, resourceLimits } = require('./config')
5
- const WorkerPool = require('./WorkerPool')
6
- const workerPath = path.resolve(__dirname, 'worker.js')
7
- const workerPool = new WorkerPool(workerPath, { resourceLimits })
8
- const { Errors } = require('../../../../lib/req/response')
9
- const LOG = cds.log()
10
- const _getReqData = req => {
11
- return {
12
- data: req.data,
13
- params: req.params,
14
- results: req.results,
15
- messages: req.messages,
16
- errors: req.errors ?? new Errors()
17
- }
18
- }
19
-
20
- module.exports = async function executeCode(code, req) {
21
- const reqData = _getReqData(req)
22
- const srv = this
23
- const _getTarget = target => {
24
- switch (target) {
25
- case 'srv':
26
- return srv
27
-
28
- case 'req':
29
- return req
30
-
31
- // no default
32
- }
33
- }
34
-
35
- const worker = workerPool.adquire()
36
- const workerId = worker.id
37
- const contextId = cds.utils.uuid()
38
- const executePromise = new Promise(function executeCodePromiseExecutor(resolve, reject) {
39
- queueMicrotask(AsyncResource.bind(onStarted)) // eslint-disable-line no-undef
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)
46
-
47
- let onStartTimeoutID
48
- function onStarted() {
49
- onStartTimeoutID = setTimeout(() => {
50
- worker.terminate()
51
- reject(new Error(`Script execution timed out after ${timeout}ms`))
52
- }, timeout)
53
- }
54
-
55
- function onMessageReceived(message) {
56
- if (message.contextId !== contextId) return
57
-
58
- if (LOG._debug)
59
- LOG.debug(`Post message received on main thread (code-ext/execute.js) from worker thread`, message)
60
-
61
- switch (message.kind) {
62
- case 'run':
63
- run(message)
64
- return
65
-
66
- case 'success':
67
- onSuccess(message)
68
- return
69
-
70
- case 'error':
71
- onError(message.error)
72
- return
73
-
74
- // no default
75
- }
76
- }
77
-
78
- async function onSuccess(message) {
79
- for (const m of message.postMessages) await run(m)
80
- req.data && Object.assign(req.data, message.req.data) // REVISIT: Why Object.assign(...) is a required?
81
- req.results = message.req.results
82
- cleanup()
83
- resolve(req.results ?? message.result)
84
- }
85
-
86
- function onError(error) {
87
- cleanup()
88
- reject(error)
89
- }
90
-
91
- function onExit(exitCode) {
92
- if (exitCode !== 0) {
93
- reject(new Error(`Worker thread stopped with exit code ${exitCode}`))
94
- }
95
- }
96
-
97
- async function run(message) {
98
- try {
99
- let result = _getTarget(message.target)[message.prop](...message.args)
100
- if (typeof result?.then === 'function') result = await result
101
- if (message.responseData) worker.postMessage({ id: message.id, kind: 'responseData', result })
102
- } catch (error) {
103
- if (LOG._debug) LOG.debug(`Calling ${message.target}.${message.prop}(...) throws an error.`, error)
104
- if (message.id) worker.postMessage({ id: message.id, kind: 'cleanup' })
105
- cleanup()
106
- reject(error)
107
- }
108
- }
109
-
110
- function cleanup() {
111
- clearTimeout(onStartTimeoutID)
112
- worker.removeListener('message', onMessageReceivedProxy)
113
- worker.removeListener('error', onErrorProxy)
114
- worker.removeListener('exit', onExitProxy)
115
- workerPool.release(worker)
116
- }
117
- })
118
-
119
- // triggers execution of the code in the worker thread
120
- const message = { contextId, workerId, kind: 'start', code, reqData }
121
- worker.postMessage(message)
122
- return executePromise
123
- }
@@ -1,50 +0,0 @@
1
- const cds = require('../../cds')
2
- const LOG = cds.log()
3
- const executeCode = require('./execute')
4
- const CODE_ANNOTATION = '@extension.code'
5
-
6
- module.exports = cds.service.impl(function () {
7
- const getCodeFromAnnotation = async (defName, operation, registration) => {
8
- // REVISIT: tenant info in not in this.model and cds.context.model is undefined for single tenancy
9
- const model = cds.context.model || this.model
10
- const el = model.definitions[defName]
11
- const boundEl = el.actions?.[operation]
12
- const extensionCode = boundEl?.[CODE_ANNOTATION] ?? el[CODE_ANNOTATION]
13
- if (extensionCode) {
14
- const annotation = extensionCode.filter(element => element[registration] === operation)
15
- return annotation.length && annotation[0].code
16
- }
17
- }
18
-
19
- this.after('READ', async function (result, req) {
20
- if (result == null) return // whether result is null or undefined
21
- const code = await getCodeFromAnnotation(req.target.name, req.event, 'after')
22
- if (!code) return
23
- await executeCode.call(this, code, req).catch(error => LOG._debug && LOG.debug(error))
24
- })
25
-
26
- this.before(['CREATE', 'UPDATE', 'DELETE'], async function (req) {
27
- const code = await getCodeFromAnnotation(req.target.name, req.event, 'before')
28
- if (!code) return
29
- await executeCode.call(this, code, req).catch(error => LOG._debug && LOG.debug(error))
30
- })
31
-
32
- this.on('*', async function (req, next) {
33
- if (this.name.startsWith('cds.xt')) return next()
34
-
35
- // REVISIT: req.target -> wait until implementation task finished
36
- let fqn = req.target?.actions?.[`${req.event}`] // check for bound action/function
37
- if (!fqn) {
38
- if (req.target) return next()
39
- fqn = this.model.definitions[`${this.name}.${req.event}`] // check for bound action/function or event
40
- }
41
-
42
- // REVISIT: DO NOT OVERWRITE EXISTING Action Implementations!
43
- // REVISIT: check whether action/function or event is part of an extension
44
- if (fqn.kind === 'action' || fqn.kind === 'function' || req.constructor.name === 'EventMessage') {
45
- const code = await getCodeFromAnnotation(req?.target?.name ?? fqn.name, req.event, 'on')
46
- if (!code) return next()
47
- return await executeCode.call(this, code, req).catch(error => LOG._debug && LOG.debug(error))
48
- }
49
- })
50
- })
@@ -1,70 +0,0 @@
1
- const cds = require('../../cds')
2
- const LOG = cds.log()
3
- const { parentPort, workerData } = require('worker_threads')
4
- const queryExecutor = require('./workerQueryExecutor')
5
- const WorkerReq = require('./WorkerReq')
6
- const { timeout } = require('./config')
7
-
8
- parentPort.on('message', function onWorkerMessageReceived(message) {
9
- const { contextId, workerId, kind, code, reqData } = message
10
- if (kind !== 'start' || workerId !== workerData.id) return
11
- if (LOG._debug) LOG.debug(`Post message received on worker thread (worker.js) from main thread`, message)
12
-
13
- const { VM } = require('vm2')
14
- const workerReq = new WorkerReq(contextId, reqData)
15
-
16
- class WorkerSELECT extends SELECT.class {
17
- then(r, e) {
18
- return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
19
- }
20
- }
21
-
22
- class WorkerINSERT extends INSERT.class {
23
- then(r, e) {
24
- return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
25
- }
26
- }
27
-
28
- class WorkerUPSERT extends UPSERT.class {
29
- then(r, e) {
30
- return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
31
- }
32
- }
33
-
34
- class WorkerUPDATE extends UPDATE.class {
35
- then(r, e) {
36
- return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
37
- }
38
- }
39
-
40
- class WorkerDELETE extends DELETE.class {
41
- then(r, e) {
42
- return new Promise(queryExecutor.bind(this, contextId)).then(r, e)
43
- }
44
- }
45
-
46
- const vm = new VM({
47
- console: 'inherit',
48
- timeout, // specifies the number of milliseconds to execute code before terminating execution
49
- allowAsync: true,
50
-
51
- // the sandbox represents the global object inside the vm instance
52
- sandbox: {
53
- req: workerReq,
54
- SELECT: WorkerSELECT._api(),
55
- INSERT: WorkerINSERT._api(),
56
- UPSERT: WorkerUPSERT._api(),
57
- UPDATE: WorkerUPDATE._api(),
58
- DELETE: WorkerDELETE._api()
59
- }
60
- })
61
-
62
- try {
63
- ;(async function () {
64
- const result = await vm.run(code)
65
- parentPort.postMessage({ contextId, kind: 'success', req: reqData, postMessages: workerReq.postMessages, result })
66
- })()
67
- } catch (error) {
68
- parentPort.postMessage({ contextId, kind: 'error', error })
69
- }
70
- })
@@ -1,37 +0,0 @@
1
- const cds = require('../../cds')
2
- const LOG = cds.log()
3
- const { parentPort } = require('worker_threads')
4
- const executionContextMap = new Map()
5
-
6
- parentPort.on('message', function onWorkerMessageReceived(message) {
7
- const { id, kind, result } = message
8
- if (!executionContextMap.has(id)) return
9
- if (LOG._debug) LOG.debug(`Post message received on worker thread (workerQueryExecutor.js) from main thread`, message)
10
-
11
- switch (kind) {
12
- case 'responseData':
13
- executionContextMap.get(id)(result)
14
- executionContextMap.delete(id)
15
- return
16
-
17
- case 'cleanup':
18
- executionContextMap.delete(id)
19
- return
20
- }
21
- })
22
-
23
- function queryExecutor(contextId, resolve) {
24
- const id = cds.utils.uuid()
25
- executionContextMap.set(id, result => resolve(result))
26
- parentPort.postMessage({
27
- id,
28
- contextId,
29
- kind: 'run',
30
- target: 'srv',
31
- prop: 'run',
32
- responseData: true,
33
- args: [this]
34
- })
35
- }
36
-
37
- module.exports = queryExecutor