@sap/cds 6.4.1 → 6.5.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 +43 -6
- package/apis/cqn.d.ts +14 -3
- package/apis/ql.d.ts +8 -8
- package/apis/services.d.ts +34 -67
- package/apis/test.d.ts +7 -0
- package/bin/build/buildTaskEngine.js +9 -12
- package/bin/build/buildTaskHandler.js +3 -14
- package/bin/build/index.js +8 -2
- package/bin/build/provider/buildTaskProviderInternal.js +8 -7
- package/bin/build/provider/hana/template/package.json +3 -0
- package/bin/build/provider/mtx/resourcesTarBuilder.js +12 -3
- package/bin/build/provider/mtx-extension/index.js +41 -38
- package/bin/build/util.js +17 -0
- package/bin/serve.js +6 -2
- package/common.cds +7 -0
- package/lib/auth/jwt-auth.js +4 -3
- package/lib/compile/for/lean_drafts.js +1 -1
- package/lib/compile/minify.js +3 -3
- package/lib/dbs/cds-deploy.js +13 -10
- package/lib/env/cds-requires.js +1 -1
- package/lib/env/defaults.js +5 -1
- package/lib/env/schemas/cds-rc.json +74 -3
- package/lib/lazy.js +6 -8
- package/lib/log/cds-error.js +2 -2
- package/lib/ql/Whereable.js +22 -11
- package/lib/ql/cds-ql.js +1 -1
- package/lib/req/response.js +8 -3
- package/lib/req/user.js +12 -2
- package/lib/srv/srv-models.js +6 -1
- package/lib/utils/cds-utils.js +3 -1
- package/lib/utils/tar.js +6 -3
- package/libx/_runtime/auth/strategies/JWT.js +1 -0
- package/libx/_runtime/auth/strategies/ias-auth.js +2 -1
- package/libx/_runtime/auth/strategies/mock.js +12 -1
- package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
- package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -2
- package/libx/_runtime/cds-services/services/Service.js +3 -0
- package/libx/_runtime/cds-services/services/utils/columns.js +35 -36
- package/libx/_runtime/common/code-ext/WorkerReq.js +79 -0
- package/libx/_runtime/common/code-ext/config.js +13 -0
- package/libx/_runtime/common/code-ext/execute.js +106 -0
- package/libx/_runtime/common/code-ext/handlers.js +49 -0
- package/libx/_runtime/common/code-ext/worker.js +36 -0
- package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
- package/libx/_runtime/common/code-ext/workerQueryExecutor.js +33 -0
- package/libx/_runtime/common/generic/crud.js +4 -0
- package/libx/_runtime/common/i18n/index.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +16 -11
- package/libx/_runtime/common/utils/path.js +5 -25
- package/libx/_runtime/common/utils/search2cqn4sql.js +13 -9
- package/libx/_runtime/db/expand/expandCQNToJoin.js +2 -1
- package/libx/_runtime/db/utils/localized.js +1 -1
- package/libx/_runtime/fiori/generic/activate.js +4 -0
- package/libx/_runtime/fiori/generic/before.js +8 -1
- package/libx/_runtime/fiori/generic/edit.js +5 -0
- package/libx/_runtime/fiori/generic/read.js +8 -3
- package/libx/_runtime/fiori/lean-draft.js +12 -1
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
- package/libx/_runtime/hana/pool.js +1 -1
- package/libx/_runtime/hana/search2cqn4sql.js +51 -51
- package/libx/odata/afterburner.js +6 -3
- package/libx/odata/cqn2odata.js +1 -1
- package/libx/rest/middleware/parse.js +26 -4
- package/package.json +1 -1
|
@@ -42,9 +42,10 @@ module.exports = function ias_auth(config) {
|
|
|
42
42
|
if (req.tokenInfo.getClientId() === req.tokenInfo.getSubject()) {
|
|
43
43
|
req.user = new cds.User({
|
|
44
44
|
id: 'system',
|
|
45
|
-
roles: ['
|
|
45
|
+
roles: ['authenticated-user'],
|
|
46
46
|
attr: {}
|
|
47
47
|
})
|
|
48
|
+
req.user._is_system = true
|
|
48
49
|
} else {
|
|
49
50
|
// add all unknown attributes to req.user.attr in order to keep public API small
|
|
50
51
|
const payload = req.tokenInfo.getPayload()
|
|
@@ -21,6 +21,17 @@ class MockStrategy {
|
|
|
21
21
|
if (user.password && user.password !== password) return this.fail(CHALLENGE)
|
|
22
22
|
|
|
23
23
|
const { features } = req.headers
|
|
24
|
+
// Only in the mock strategy the pseudo roles are kept in the role list.
|
|
25
|
+
// In all other cases pseudo roles are filtered out.
|
|
26
|
+
if (user.roles) {
|
|
27
|
+
if (Array.isArray(user.roles)) {
|
|
28
|
+
if (user.roles.includes('system-user')) user._is_system = true
|
|
29
|
+
if (user.roles.includes('internal-user')) user._is_internal = true
|
|
30
|
+
} else {
|
|
31
|
+
if ('system-user' in user.roles) user._is_system = true
|
|
32
|
+
if ('internal-user' in user.roles) user._is_internal = true
|
|
33
|
+
}
|
|
34
|
+
}
|
|
24
35
|
this.success(new cds.User(features ? { ...user, features } : user))
|
|
25
36
|
}
|
|
26
37
|
}
|
|
@@ -44,7 +55,7 @@ const _init_users = (users, tenants = {}) => {
|
|
|
44
55
|
Array.isArray(user.roles) ? user.roles.push(...scopes) : (user.roles = scopes)
|
|
45
56
|
}
|
|
46
57
|
if (user.jwt.grant_type === 'client_credentials' || user.jwt.grant_type === 'client_x509') {
|
|
47
|
-
user.
|
|
58
|
+
user._is_system = true
|
|
48
59
|
}
|
|
49
60
|
if (!user.tenant && user.jwt.zid) user.tenant = user.jwt.zid
|
|
50
61
|
}
|
|
@@ -5,21 +5,19 @@ const getUserId = (user, info) => {
|
|
|
5
5
|
return user.id || (info && info.getClientId && info.getClientId())
|
|
6
6
|
}
|
|
7
7
|
|
|
8
|
-
const
|
|
8
|
+
const addRolesFromGrantType = (user, info, credentials) => {
|
|
9
9
|
const grantType = info && (info.grantType || (info.getGrantType && info.getGrantType()))
|
|
10
10
|
if (grantType) {
|
|
11
11
|
// > not "weak"
|
|
12
|
-
roles
|
|
12
|
+
user.roles['authenticated-user'] = true
|
|
13
13
|
if (grantType in CLIENT) {
|
|
14
|
-
|
|
15
|
-
if (info.getClientId() === credentials.clientid)
|
|
14
|
+
user._is_system = true
|
|
15
|
+
if (info.getClientId() === credentials.clientid) user._is_internal = true
|
|
16
16
|
}
|
|
17
17
|
}
|
|
18
18
|
}
|
|
19
19
|
|
|
20
|
-
const getRoles = (roles, info
|
|
21
|
-
_addRolesFromGrantType(roles, info, credentials)
|
|
22
|
-
|
|
20
|
+
const getRoles = (roles, info) => {
|
|
23
21
|
// convert to object
|
|
24
22
|
roles = Object.assign(...roles.map(ele => ({ [ele]: true })))
|
|
25
23
|
|
|
@@ -90,5 +88,6 @@ module.exports = {
|
|
|
90
88
|
getRoles,
|
|
91
89
|
getAttrForJWT,
|
|
92
90
|
getAttrForXSSEC,
|
|
93
|
-
getTenant
|
|
91
|
+
getTenant,
|
|
92
|
+
addRolesFromGrantType
|
|
94
93
|
}
|
|
@@ -25,6 +25,7 @@ class XSUAAStrategy extends JS {
|
|
|
25
25
|
roles: xssecUtils.getRoles(['any', 'identified-user'], info, credentials),
|
|
26
26
|
attr: xssecUtils.getAttrForXSSEC(info)
|
|
27
27
|
})
|
|
28
|
+
xssecUtils.addRolesFromGrantType(user, info, credentials)
|
|
28
29
|
const tenant = xssecUtils.getTenant(info)
|
|
29
30
|
if (tenant) user.tenant = tenant
|
|
30
31
|
// call "super.success"
|
|
@@ -60,9 +60,8 @@ const action = service => {
|
|
|
60
60
|
await tx.commit(result)
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
-
if (isReturnMinimal(req) || result === null) odataRes.setStatusCode(204)
|
|
63
|
+
if (isReturnMinimal(req) || result === null) odataRes.setStatusCode(204, { overwrite: true })
|
|
64
64
|
else if (req.event === 'draftActivate' || req.event === 'EDIT') {
|
|
65
|
-
odataRes.setStatusCode(201)
|
|
66
65
|
const keys = Object.keys(req.target.keys).filter(k => {
|
|
67
66
|
return k !== 'IsActiveEntity' && !req.target.keys[k]._isAssociationStrict
|
|
68
67
|
})
|
|
@@ -22,6 +22,9 @@ class ApplicationService extends cds.Service {
|
|
|
22
22
|
require('../../common/generic/temporal').call(this, this)
|
|
23
23
|
require('../../common/generic/paging').call(this, this) // > paging must be executed before sorting
|
|
24
24
|
require('../../common/generic/sorting').call(this, this)
|
|
25
|
+
|
|
26
|
+
if (cds.env.requires.extensibility?.code) require('../../common/code-ext/handlers').call(this, this)
|
|
27
|
+
|
|
25
28
|
this.registerFioriHandlers(this)
|
|
26
29
|
this.registerPersonalDataHandlers(this)
|
|
27
30
|
this.registerCrudHandlers(this) // default .on handlers, have to go last
|
|
@@ -122,44 +122,43 @@ const _getSearchableColumns = entity => {
|
|
|
122
122
|
* @returns {import('../../../types/api').ColumnRefs}
|
|
123
123
|
*/
|
|
124
124
|
const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, alias) => {
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
if (cqn.
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
cqn.SELECT.columns.length === 1 &&
|
|
143
|
-
column.func === 'count' &&
|
|
144
|
-
(column.as === '_counted_' || column.as === '$count')
|
|
145
|
-
)
|
|
125
|
+
let toBeSearched = []
|
|
126
|
+
|
|
127
|
+
// aggregations case
|
|
128
|
+
// in the new parser groupBy is moved to sub select.
|
|
129
|
+
if (cqn._aggregated || /* new parser */ cqn.SELECT.groupBy || cqn.SELECT?.from?.SELECT?.groupBy) {
|
|
130
|
+
cqn.SELECT.columns &&
|
|
131
|
+
cqn.SELECT.columns.forEach(column => {
|
|
132
|
+
if (column.func) {
|
|
133
|
+
// exclude $count by SELECT of number of Items in a Collection
|
|
134
|
+
if (
|
|
135
|
+
cqn.SELECT.columns.length === 1 &&
|
|
136
|
+
column.func === 'count' &&
|
|
137
|
+
(column.as === '_counted_' || column.as === '$count')
|
|
138
|
+
)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
toBeSearched.push(column)
|
|
146
142
|
return
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
const columnRef = column.ref
|
|
146
|
+
if (columnRef) {
|
|
147
|
+
column = { ref: [...column.ref] }
|
|
148
|
+
if (alias) column.ref.unshift(alias)
|
|
149
|
+
toBeSearched.push(column)
|
|
150
|
+
}
|
|
151
|
+
})
|
|
152
|
+
} else {
|
|
153
|
+
toBeSearched = entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
|
|
154
|
+
|
|
155
|
+
if (cqn.SELECT.groupBy) toBeSearched = toBeSearched.filter(tbs => cqn.SELECT.groupBy.some(gb => gb.ref[0] === tbs))
|
|
156
|
+
toBeSearched = toBeSearched.map(c => {
|
|
157
|
+
const col = { ref: [c] }
|
|
158
|
+
if (alias) col.ref.unshift(alias)
|
|
159
|
+
return col
|
|
161
160
|
})
|
|
162
|
-
|
|
161
|
+
}
|
|
163
162
|
return toBeSearched
|
|
164
163
|
}
|
|
165
164
|
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const { parentPort } = require('worker_threads')
|
|
2
|
+
const { Responses, Errors } = require('../../../../lib/req/response')
|
|
3
|
+
|
|
4
|
+
class WorkerReq {
|
|
5
|
+
constructor(reqData) {
|
|
6
|
+
Object.assign(this, reqData)
|
|
7
|
+
this.postMessages = []
|
|
8
|
+
this.messages = this.messages ?? []
|
|
9
|
+
this.errors = this.errors ?? new Errors()
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
#push(args) {
|
|
13
|
+
this.postMessages.push({
|
|
14
|
+
kind: 'run',
|
|
15
|
+
target: 'req',
|
|
16
|
+
...args
|
|
17
|
+
})
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
notify(...args) {
|
|
21
|
+
this.#push({
|
|
22
|
+
prop: 'notify',
|
|
23
|
+
args
|
|
24
|
+
})
|
|
25
|
+
|
|
26
|
+
const notify = Responses.get(1, ...args)
|
|
27
|
+
this.messages.push(notify)
|
|
28
|
+
return notify
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
info(...args) {
|
|
32
|
+
this.#push({
|
|
33
|
+
prop: 'info',
|
|
34
|
+
args
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const info = Responses.get(2, ...args)
|
|
38
|
+
this.messages.push(info)
|
|
39
|
+
return info
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
warn(...args) {
|
|
43
|
+
this.#push({
|
|
44
|
+
prop: 'warn',
|
|
45
|
+
args
|
|
46
|
+
})
|
|
47
|
+
|
|
48
|
+
const warn = Responses.get(3, ...args)
|
|
49
|
+
this.messages.push(warn)
|
|
50
|
+
return warn
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
error(...args) {
|
|
54
|
+
this.#push({
|
|
55
|
+
prop: 'error',
|
|
56
|
+
args
|
|
57
|
+
})
|
|
58
|
+
|
|
59
|
+
let error = Responses.get(4, ...args)
|
|
60
|
+
if (!error.stack) Error.captureStackTrace((error = Object.assign(new Error(), error)), this.error)
|
|
61
|
+
this.errors.push(error)
|
|
62
|
+
return error
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
reject(...args) {
|
|
66
|
+
parentPort.postMessage({
|
|
67
|
+
kind: 'run',
|
|
68
|
+
target: 'req',
|
|
69
|
+
prop: 'reject',
|
|
70
|
+
args
|
|
71
|
+
})
|
|
72
|
+
|
|
73
|
+
let error = Responses.get(4, ...args)
|
|
74
|
+
if (!error.stack) Error.captureStackTrace((error = Object.assign(new Error(), error)), this.reject)
|
|
75
|
+
throw error
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = WorkerReq
|
|
@@ -0,0 +1,13 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
const { Worker } = require('worker_threads')
|
|
3
|
+
const path = require('node:path')
|
|
4
|
+
const { timeout, resourceLimits } = require('./config')
|
|
5
|
+
const workerPath = path.resolve(__dirname, 'worker.js')
|
|
6
|
+
const { Errors } = require('../../../../lib/req/response')
|
|
7
|
+
|
|
8
|
+
const _getReqData = req => {
|
|
9
|
+
return {
|
|
10
|
+
data: req.data,
|
|
11
|
+
params: req.params,
|
|
12
|
+
results: req.results,
|
|
13
|
+
messages: req.messages,
|
|
14
|
+
errors: req.errors ?? new Errors()
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
module.exports = async function executeCode(code, req) {
|
|
19
|
+
const reqData = _getReqData(req)
|
|
20
|
+
const _getTarget = target => {
|
|
21
|
+
switch (target) {
|
|
22
|
+
case 'srv':
|
|
23
|
+
return this
|
|
24
|
+
|
|
25
|
+
case 'req':
|
|
26
|
+
return req
|
|
27
|
+
|
|
28
|
+
// no default
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const workerId = cds.utils.uuid()
|
|
32
|
+
const worker = new Worker(workerPath, {
|
|
33
|
+
workerData: { id: workerId },
|
|
34
|
+
resourceLimits
|
|
35
|
+
})
|
|
36
|
+
|
|
37
|
+
const executePromise = new Promise(function executeCodePromiseExecutor(resolve, reject) {
|
|
38
|
+
worker.on('online', onStarted)
|
|
39
|
+
worker.on('message', onMessageReceived)
|
|
40
|
+
worker.on('error', onError)
|
|
41
|
+
worker.on('exit', onExit)
|
|
42
|
+
|
|
43
|
+
let onStartTimeoutID
|
|
44
|
+
|
|
45
|
+
function onStarted() {
|
|
46
|
+
onStartTimeoutID = setTimeout(() => {
|
|
47
|
+
worker.terminate()
|
|
48
|
+
reject(new Error(`Script execution timed out after ${timeout}ms`))
|
|
49
|
+
}, timeout)
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
function onMessageReceived(message) {
|
|
53
|
+
switch (message.kind) {
|
|
54
|
+
case 'run':
|
|
55
|
+
run(message)
|
|
56
|
+
return
|
|
57
|
+
|
|
58
|
+
case 'success':
|
|
59
|
+
onSuccess(message)
|
|
60
|
+
cleanup()
|
|
61
|
+
return
|
|
62
|
+
|
|
63
|
+
case 'error':
|
|
64
|
+
onError(message.error)
|
|
65
|
+
return
|
|
66
|
+
|
|
67
|
+
// no default
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function onSuccess(message) {
|
|
72
|
+
for (const m of message.postMessages) await run(m)
|
|
73
|
+
req.data && Object.assign(req.data, message.req.data) // REVISIT: Why Object.assign(...) is a required?
|
|
74
|
+
req.results = message.req.results
|
|
75
|
+
resolve(req.results ?? message.result)
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function onError(error) {
|
|
79
|
+
reject(error)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function onExit(exitCode) {
|
|
83
|
+
if (exitCode !== 0) reject(new Error(`Worker thread stopped with exit code ${exitCode}`))
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function run(message) {
|
|
87
|
+
try {
|
|
88
|
+
let result = _getTarget(message.target)[message.prop](...message.args)
|
|
89
|
+
if (typeof result?.then === 'function') result = await result
|
|
90
|
+
if (message.responseData) worker.postMessage({ id: message.id, kind: 'responseData', result })
|
|
91
|
+
} catch (error) {
|
|
92
|
+
worker.postMessage({ id: message.id, kind: 'cleanup' })
|
|
93
|
+
reject(error)
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function cleanup() {
|
|
98
|
+
clearTimeout(onStartTimeoutID)
|
|
99
|
+
worker.terminate()
|
|
100
|
+
}
|
|
101
|
+
})
|
|
102
|
+
|
|
103
|
+
// triggers execution of the code in the worker thread
|
|
104
|
+
worker.postMessage({ id: workerId, code, reqData })
|
|
105
|
+
return executePromise
|
|
106
|
+
}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
const executeCode = require('./execute')
|
|
3
|
+
|
|
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)
|
|
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)
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
this.on('*', async function (req, next) {
|
|
33
|
+
if (this.name.startsWith('cds.xt')) return next()
|
|
34
|
+
// REVISIT: req.target -> wait until implementation task finished
|
|
35
|
+
let fqn = req.target?.actions?.[`${req.event}`] // check for bound action/function
|
|
36
|
+
if (!fqn) {
|
|
37
|
+
if (req.target) return next()
|
|
38
|
+
fqn = this.model.definitions[`${this.name}.${req.event}`] // check for unbound action/function or event
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// REVISIT: DO NOT OVERWRITE EXISTING Action Implementations!
|
|
42
|
+
// REVISIT: check whether action/function or event is part of an extension
|
|
43
|
+
if (fqn.kind === 'action' || fqn.kind === 'function' || req.constructor.name === 'EventMessage') {
|
|
44
|
+
const code = await getCodeFromAnnotation(req?.target?.name ?? fqn.name, req.event, 'on')
|
|
45
|
+
if (!code) return
|
|
46
|
+
return await executeCode.call(this, code, req)
|
|
47
|
+
}
|
|
48
|
+
})
|
|
49
|
+
})
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
const { parentPort, workerData } = require('worker_threads')
|
|
2
|
+
const { WorkerSELECT, WorkerINSERT, WorkerUPSERT, WorkerUPDATE, WorkerDELETE } = require('./workerQuery')
|
|
3
|
+
const WorkerReq = require('./WorkerReq')
|
|
4
|
+
const { timeout } = require('./config')
|
|
5
|
+
|
|
6
|
+
parentPort.once('message', function onMessageReceived({ id, code, reqData }) {
|
|
7
|
+
if (id !== workerData.id) return
|
|
8
|
+
|
|
9
|
+
// eslint-disable-next-line cds/no-missing-dependencies
|
|
10
|
+
const { VM } = require('vm2')
|
|
11
|
+
const workerReq = new WorkerReq(reqData)
|
|
12
|
+
const vm = new VM({
|
|
13
|
+
console: 'inherit',
|
|
14
|
+
timeout, // specifies the number of milliseconds to execute code before terminating execution
|
|
15
|
+
allowAsync: true,
|
|
16
|
+
|
|
17
|
+
// the sandbox represents the global object inside the vm instance
|
|
18
|
+
sandbox: {
|
|
19
|
+
req: workerReq,
|
|
20
|
+
SELECT: WorkerSELECT._api(),
|
|
21
|
+
INSERT: WorkerINSERT._api(),
|
|
22
|
+
UPSERT: WorkerUPSERT._api(),
|
|
23
|
+
UPDATE: WorkerUPDATE._api(),
|
|
24
|
+
DELETE: WorkerDELETE._api()
|
|
25
|
+
}
|
|
26
|
+
})
|
|
27
|
+
|
|
28
|
+
try {
|
|
29
|
+
;(async function () {
|
|
30
|
+
const result = await vm.run(code)
|
|
31
|
+
parentPort.postMessage({ kind: 'success', req: reqData, postMessages: workerReq.postMessages, result })
|
|
32
|
+
})()
|
|
33
|
+
} catch (error) {
|
|
34
|
+
parentPort.postMessage({ kind: 'error', error })
|
|
35
|
+
}
|
|
36
|
+
})
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const SELECT = require('../../../../lib/ql/SELECT')
|
|
2
|
+
const INSERT = require('../../../../lib/ql/INSERT')
|
|
3
|
+
const UPSERT = require('../../../../lib/ql/UPSERT')
|
|
4
|
+
const UPDATE = require('../../../../lib/ql/UPDATE')
|
|
5
|
+
const DELETE = require('../../../../lib/ql/DELETE')
|
|
6
|
+
const queryExecutor = require('./workerQueryExecutor')
|
|
7
|
+
|
|
8
|
+
class WorkerSELECT extends SELECT {
|
|
9
|
+
// intercept await SELECT.from(...) calls
|
|
10
|
+
then(r, e) {
|
|
11
|
+
return new Promise(queryExecutor.bind(this)).then(r, e)
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
class WorkerINSERT extends INSERT {
|
|
16
|
+
then(r, e) {
|
|
17
|
+
return new Promise(queryExecutor.bind(this)).then(r, e)
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
class WorkerUPSERT extends UPSERT {
|
|
22
|
+
then(r, e) {
|
|
23
|
+
return new Promise(queryExecutor.bind(this)).then(r, e)
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class WorkerUPDATE extends UPDATE {
|
|
28
|
+
then(r, e) {
|
|
29
|
+
return new Promise(queryExecutor.bind(this)).then(r, e)
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
class WorkerDELETE extends DELETE {
|
|
34
|
+
then(r, e) {
|
|
35
|
+
return new Promise(queryExecutor.bind(this)).then(r, e)
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
Object.defineProperty(WorkerSELECT.prototype, 'cmd', { value: 'SELECT' })
|
|
40
|
+
Object.defineProperty(WorkerINSERT.prototype, 'cmd', { value: 'INSERT' })
|
|
41
|
+
Object.defineProperty(WorkerUPSERT.prototype, 'cmd', { value: 'UPSERT' })
|
|
42
|
+
Object.defineProperty(WorkerUPDATE.prototype, 'cmd', { value: 'UPDATE' })
|
|
43
|
+
Object.defineProperty(WorkerDELETE.prototype, 'cmd', { value: 'DELETE' })
|
|
44
|
+
|
|
45
|
+
module.exports = { WorkerSELECT, WorkerINSERT, WorkerUPSERT, WorkerUPDATE, WorkerDELETE }
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
const { parentPort } = require('worker_threads')
|
|
3
|
+
const executorCallbackMap = new Map()
|
|
4
|
+
|
|
5
|
+
parentPort.on('message', function onMessageReceived({ id, kind, result }) {
|
|
6
|
+
if (!executorCallbackMap.has(id)) return
|
|
7
|
+
|
|
8
|
+
switch (kind) {
|
|
9
|
+
case 'responseData':
|
|
10
|
+
executorCallbackMap.get(id)(result)
|
|
11
|
+
executorCallbackMap.delete(id)
|
|
12
|
+
return
|
|
13
|
+
|
|
14
|
+
case 'cleanup':
|
|
15
|
+
executorCallbackMap.delete(id)
|
|
16
|
+
return
|
|
17
|
+
}
|
|
18
|
+
})
|
|
19
|
+
|
|
20
|
+
function queryExecutor(resolve, reject) {
|
|
21
|
+
const id = cds.utils.uuid()
|
|
22
|
+
executorCallbackMap.set(id, result => resolve(result))
|
|
23
|
+
parentPort.postMessage({
|
|
24
|
+
id,
|
|
25
|
+
kind: 'run',
|
|
26
|
+
target: 'srv',
|
|
27
|
+
prop: 'run',
|
|
28
|
+
responseData: true,
|
|
29
|
+
args: [this]
|
|
30
|
+
})
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
module.exports = queryExecutor
|
|
@@ -61,6 +61,10 @@ exports.impl = cds.service.impl(function () {
|
|
|
61
61
|
req.query.where(singleton)
|
|
62
62
|
}
|
|
63
63
|
|
|
64
|
+
if (req.event === 'READ' && req.query?.SELECT) {
|
|
65
|
+
req.query.SELECT.localized = true
|
|
66
|
+
}
|
|
67
|
+
|
|
64
68
|
if (!result) {
|
|
65
69
|
result = await cds.tx(req).run(req.query, req.data)
|
|
66
70
|
}
|
|
@@ -8,7 +8,7 @@ const dirs = (cds.env.i18n && cds.env.i18n.folders) || []
|
|
|
8
8
|
const i18ns = {}
|
|
9
9
|
|
|
10
10
|
function exists(args, locale) {
|
|
11
|
-
const file = path.join(
|
|
11
|
+
const file = path.join(cds.root, ...args, locale ? `messages_${locale}.properties` : 'messages.properties')
|
|
12
12
|
return fs.existsSync(file) ? file : undefined
|
|
13
13
|
}
|
|
14
14
|
|
|
@@ -11,7 +11,7 @@ const search2cqn4sql = require('./search2cqn4sql')
|
|
|
11
11
|
const { getEntityNameFromCQN } = require('./entityFromCqn')
|
|
12
12
|
const getError = require('../../common/error')
|
|
13
13
|
const { rewriteAsterisks } = require('./rewriteAsterisks')
|
|
14
|
-
const {
|
|
14
|
+
const { getEntityFromPath } = require('../../common/utils/path')
|
|
15
15
|
const { removeIsActiveEntityRecursively } = require('../../fiori/utils/where')
|
|
16
16
|
const { addRefToWhereIfNecessary } = require('../../../odata/afterburner')
|
|
17
17
|
const { addAliasToExpression, PARENT_ALIAS, FOREIGN_ALIAS } = require('../../db/utils/generateAliases')
|
|
@@ -445,7 +445,7 @@ const convertWhereExists = (query, model, options, currentTarget) => {
|
|
|
445
445
|
if (currentTarget) {
|
|
446
446
|
queryTarget = getEntityFromPath({ ref }, currentTarget)
|
|
447
447
|
} else {
|
|
448
|
-
queryTarget = getEntityFromPath(
|
|
448
|
+
queryTarget = getEntityFromPath({ ref }, model)
|
|
449
449
|
outerAlias = as || PARENT_ALIAS + lambdaIteration
|
|
450
450
|
innerAlias = FOREIGN_ALIAS + lambdaIteration
|
|
451
451
|
}
|
|
@@ -604,14 +604,19 @@ const _convertExpand = expand => {
|
|
|
604
604
|
})
|
|
605
605
|
}
|
|
606
606
|
|
|
607
|
-
const
|
|
608
|
-
if (
|
|
607
|
+
const _simplifyWhere = col => {
|
|
608
|
+
if (col.ref?.[0].where) {
|
|
609
|
+
col.where = col.ref[0].where
|
|
610
|
+
col.ref[0] = col.ref[0].id
|
|
611
|
+
}
|
|
612
|
+
}
|
|
609
613
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
614
|
+
const _simplifyWhereInColumns = columns => {
|
|
615
|
+
if (!columns) return
|
|
616
|
+
for (const col of columns) {
|
|
617
|
+
_simplifyWhere(col)
|
|
618
|
+
if (col.expand) _simplifyWhereInColumns(col.expand)
|
|
619
|
+
}
|
|
615
620
|
}
|
|
616
621
|
|
|
617
622
|
const _convertPathExpression = (query, model, options = {}) => {
|
|
@@ -742,8 +747,8 @@ const _convertSelect = (query, model, _options) => {
|
|
|
742
747
|
_convertToOneEqNullInFilter(query.SELECT, target)
|
|
743
748
|
}
|
|
744
749
|
|
|
745
|
-
// extract where clause if it is in
|
|
746
|
-
|
|
750
|
+
// extract where clause if it is in an expand column
|
|
751
|
+
_simplifyWhereInColumns(query.SELECT.columns)
|
|
747
752
|
|
|
748
753
|
// REVISIT: The following operations only work for _one_ entity.
|
|
749
754
|
// We must also enable them for joins etc.
|