@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +43 -6
  2. package/apis/cqn.d.ts +14 -3
  3. package/apis/ql.d.ts +8 -8
  4. package/apis/services.d.ts +34 -67
  5. package/apis/test.d.ts +7 -0
  6. package/bin/build/buildTaskEngine.js +9 -12
  7. package/bin/build/buildTaskHandler.js +3 -14
  8. package/bin/build/index.js +8 -2
  9. package/bin/build/provider/buildTaskProviderInternal.js +8 -7
  10. package/bin/build/provider/hana/template/package.json +3 -0
  11. package/bin/build/provider/mtx/resourcesTarBuilder.js +12 -3
  12. package/bin/build/provider/mtx-extension/index.js +41 -38
  13. package/bin/build/util.js +17 -0
  14. package/bin/serve.js +6 -2
  15. package/common.cds +7 -0
  16. package/lib/auth/jwt-auth.js +4 -3
  17. package/lib/compile/for/lean_drafts.js +1 -1
  18. package/lib/compile/minify.js +3 -3
  19. package/lib/dbs/cds-deploy.js +13 -10
  20. package/lib/env/cds-requires.js +1 -1
  21. package/lib/env/defaults.js +5 -1
  22. package/lib/env/schemas/cds-rc.json +74 -3
  23. package/lib/lazy.js +6 -8
  24. package/lib/log/cds-error.js +2 -2
  25. package/lib/ql/Whereable.js +22 -11
  26. package/lib/ql/cds-ql.js +1 -1
  27. package/lib/req/response.js +8 -3
  28. package/lib/req/user.js +12 -2
  29. package/lib/srv/srv-models.js +6 -1
  30. package/lib/utils/cds-utils.js +3 -1
  31. package/lib/utils/tar.js +6 -3
  32. package/libx/_runtime/auth/strategies/JWT.js +1 -0
  33. package/libx/_runtime/auth/strategies/ias-auth.js +2 -1
  34. package/libx/_runtime/auth/strategies/mock.js +12 -1
  35. package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
  36. package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
  37. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -2
  38. package/libx/_runtime/cds-services/services/Service.js +3 -0
  39. package/libx/_runtime/cds-services/services/utils/columns.js +35 -36
  40. package/libx/_runtime/common/code-ext/WorkerReq.js +79 -0
  41. package/libx/_runtime/common/code-ext/config.js +13 -0
  42. package/libx/_runtime/common/code-ext/execute.js +106 -0
  43. package/libx/_runtime/common/code-ext/handlers.js +49 -0
  44. package/libx/_runtime/common/code-ext/worker.js +36 -0
  45. package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
  46. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +33 -0
  47. package/libx/_runtime/common/generic/crud.js +4 -0
  48. package/libx/_runtime/common/i18n/index.js +1 -1
  49. package/libx/_runtime/common/utils/cqn2cqn4sql.js +16 -11
  50. package/libx/_runtime/common/utils/path.js +5 -25
  51. package/libx/_runtime/common/utils/search2cqn4sql.js +13 -9
  52. package/libx/_runtime/db/expand/expandCQNToJoin.js +2 -1
  53. package/libx/_runtime/db/utils/localized.js +1 -1
  54. package/libx/_runtime/fiori/generic/activate.js +4 -0
  55. package/libx/_runtime/fiori/generic/before.js +8 -1
  56. package/libx/_runtime/fiori/generic/edit.js +5 -0
  57. package/libx/_runtime/fiori/generic/read.js +8 -3
  58. package/libx/_runtime/fiori/lean-draft.js +12 -1
  59. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
  60. package/libx/_runtime/hana/pool.js +1 -1
  61. package/libx/_runtime/hana/search2cqn4sql.js +51 -51
  62. package/libx/odata/afterburner.js +6 -3
  63. package/libx/odata/cqn2odata.js +1 -1
  64. package/libx/rest/middleware/parse.js +26 -4
  65. 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: ['system-user', 'authenticated-user'],
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.roles.push('system-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 _addRolesFromGrantType = (roles, info, credentials) => {
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.push('authenticated-user')
12
+ user.roles['authenticated-user'] = true
13
13
  if (grantType in CLIENT) {
14
- roles.push('system-user')
15
- if (info.getClientId() === credentials.clientid) roles.push('internal-user')
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, credentials) => {
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
- // if there is a group by clause, only columns in it may be searched
126
- let toBeSearched =
127
- entity.own('__searchableColumns') || entity.set('__searchableColumns', _getSearchableColumns(entity))
128
-
129
- if (cqn.SELECT.groupBy) toBeSearched = toBeSearched.filter(tbs => cqn.SELECT.groupBy.some(gb => gb.ref[0] === tbs))
130
- toBeSearched = toBeSearched.map(c => {
131
- const col = { ref: [c] }
132
- if (alias) col.ref.unshift(alias)
133
- return col
134
- })
135
-
136
- // add aggregations
137
- cqn.SELECT.columns &&
138
- cqn.SELECT.columns.forEach(column => {
139
- if (column.func) {
140
- // exclude $count by SELECT of number of Items in a Collection
141
- if (
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
- toBeSearched.push(column)
149
- return
150
- }
151
-
152
- const columnRef = column.ref
153
- if (columnRef) {
154
- const columnName = columnRef[columnRef.length - 1]
155
- const csnColumn = entity.elements[columnName]
156
- if (csnColumn) return
157
- const col = { ref: [columnName] }
158
- if (alias) col.ref.unshift(alias)
159
- toBeSearched.push(col)
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(process.cwd(), ...args, locale ? `messages_${locale}.properties` : 'messages.properties')
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 { getPathFromRef, getEntityFromPath } = require('../../common/utils/path')
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(getPathFromRef(ref), model)
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 _convertRefWhereInExpand = columns => {
608
- if (!columns) return
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
- columns.forEach(col => {
611
- if (col.expand && typeof col.expand !== 'string') {
612
- _convertExpand(col.expand)
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 column expand ref
746
- _convertRefWhereInExpand(query.SELECT.columns)
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.