@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.
- package/CHANGELOG.md +67 -3
- 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/deploy/to-hana/hana.js +1 -1
- package/bin/deploy/to-hana/hdiDeployUtil.js +1 -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
|
@@ -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
|
|
@@ -142,13 +142,18 @@ const _getRestrictionForTarget = (resolvedApplicables, target) => {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
|
|
145
|
-
if (!cds.env.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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, {
|
|
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,
|
|
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,
|
|
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
|
-
|
|
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.
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
27
|
+
const REST = req._isRest
|
|
28
28
|
|
|
29
29
|
return ({ row, key, element, plain }) => {
|
|
30
30
|
if (!row || row[key] !== undefined) return
|
|
@@ -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 (
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
384
|
-
? {
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
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
|
|
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
|
|
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.
|
|
121
|
+
if (_4db && target._isDraftEnabled && !cds.env.fiori.lean_draft)
|
|
123
122
|
query.SELECT.columns.push(..._cqlDraftColumns(target))
|
|
124
123
|
}
|
|
125
124
|
}
|