@sap/cds 6.4.0 → 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 +59 -3
- package/apis/cds.d.ts +2 -0
- package/apis/cqn.d.ts +14 -3
- package/apis/ql.d.ts +12 -8
- package/apis/services.d.ts +39 -64
- 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 +13 -4
- package/bin/build/provider/mtx-extension/index.js +41 -38
- package/bin/build/util.js +17 -0
- package/bin/deploy/to-hana/hdiDeployUtil.js +11 -5
- package/bin/serve.js +6 -2
- package/common.cds +7 -0
- package/lib/auth/index.js +17 -15
- 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/core/index.js +1 -0
- 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/middlewares/cds-context.js +0 -2
- package/lib/srv/middlewares/ctx-auth.js +11 -0
- package/lib/srv/middlewares/ctx-model.js +22 -20
- package/lib/srv/middlewares/index.js +7 -9
- package/lib/srv/protocols/_legacy.js +4 -0
- package/lib/srv/protocols/graphql.js +2 -2
- package/lib/srv/protocols/index.js +7 -3
- package/lib/srv/srv-api.js +1 -0
- package/lib/srv/srv-models.js +6 -1
- package/lib/utils/cds-utils.js +3 -1
- package/lib/utils/data.js +2 -2
- package/lib/utils/tar.js +37 -12
- 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 +5 -1
- package/libx/_runtime/common/generic/paging.js +8 -7
- package/libx/_runtime/common/i18n/index.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +47 -11
- package/libx/_runtime/common/utils/path.js +5 -25
- package/libx/_runtime/common/utils/resolveView.js +2 -0
- package/libx/_runtime/common/utils/search2cqn4sql.js +13 -9
- package/libx/_runtime/db/expand/expandCQNToJoin.js +2 -1
- package/libx/_runtime/db/sql-builder/InsertBuilder.js +5 -1
- package/libx/_runtime/db/sql-builder/UpsertBuilder.js +9 -32
- package/libx/_runtime/db/sql-builder/annotations.js +6 -3
- 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/Service.js +1 -1
- package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
- package/libx/_runtime/hana/execute.js +5 -5
- package/libx/_runtime/hana/pool.js +1 -1
- package/libx/_runtime/hana/search2cqn4sql.js +51 -51
- package/libx/_runtime/sqlite/Service.js +1 -1
- package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +20 -38
- 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
- package/server.js +2 -20
|
@@ -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
|
|
@@ -26,7 +26,7 @@ const _targetEntityDoesNotExist = async req => {
|
|
|
26
26
|
|
|
27
27
|
exports.impl = cds.service.impl(function () {
|
|
28
28
|
// eslint-disable-next-line complexity
|
|
29
|
-
this.on(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', async function (req) {
|
|
29
|
+
this.on(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', async function (req) {
|
|
30
30
|
if (typeof req.query !== 'string' && req.target && req.target._hasPersistenceSkip) {
|
|
31
31
|
throw getError({
|
|
32
32
|
code: 501,
|
|
@@ -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
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
|
-
const {
|
|
2
|
+
const { getPageSize } = require('../utils/page')
|
|
3
3
|
|
|
4
4
|
const commonGenericPaging = function (req) {
|
|
5
5
|
// only if http request
|
|
@@ -11,13 +11,14 @@ const commonGenericPaging = function (req) {
|
|
|
11
11
|
_addPaging(req.query, req.target)
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
-
const _addPaging = function (
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
14
|
+
const _addPaging = function ({ SELECT }, target) {
|
|
15
|
+
const { rows } = SELECT.limit || (SELECT.limit = {})
|
|
16
|
+
const conf = getPageSize(target)
|
|
17
|
+
SELECT.limit.rows = {
|
|
18
|
+
val: !rows ? conf.default : Math.min(rows.val ?? rows, conf.max)
|
|
19
|
+
}
|
|
19
20
|
//Handle nested limits
|
|
20
|
-
if (
|
|
21
|
+
if (SELECT.from.SELECT?.limit) _addPaging(SELECT.from, target)
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
/**
|
|
@@ -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.
|
|
@@ -804,6 +809,33 @@ const _convertSelect = (query, model, _options) => {
|
|
|
804
809
|
return query
|
|
805
810
|
}
|
|
806
811
|
|
|
812
|
+
const _convertUpsert = (query, model) => {
|
|
813
|
+
// resolve path expression
|
|
814
|
+
const resolvedIntoClause = _convertPathExpressionForInsert(query.UPSERT.into, model)
|
|
815
|
+
|
|
816
|
+
const target = model.definitions[resolvedIntoClause]
|
|
817
|
+
if (!target) {
|
|
818
|
+
// if there is no target, just return original query, as a copy is not deep anyways and all the sub items of query.UPSERT are referenced only anyways
|
|
819
|
+
return query
|
|
820
|
+
}
|
|
821
|
+
|
|
822
|
+
// overwrite only .into, foreign keys are already set
|
|
823
|
+
// 'a' added as placeholder since its overwritten by Object.assign below
|
|
824
|
+
const upsert = UPSERT.into('a')
|
|
825
|
+
|
|
826
|
+
// REVISIT flatten structured types, currently its done in SQL builder
|
|
827
|
+
|
|
828
|
+
// We add all previous properties ot the newly created query.
|
|
829
|
+
// Reason is to not lose the query API functionality
|
|
830
|
+
Object.assign(upsert.UPSERT, query.UPSERT, { into: { ref: [resolvedIntoClause], as: query.UPSERT.into.as } })
|
|
831
|
+
|
|
832
|
+
const resolved = resolveView(upsert, model, cds.db)
|
|
833
|
+
// required for deplyoing of extensions, not used anywhere else except UpsertBuilder
|
|
834
|
+
resolved._target = resolved.UPSERT?._transitions?.[0].target || query._target
|
|
835
|
+
// resolved._target = query._target
|
|
836
|
+
return resolved
|
|
837
|
+
}
|
|
838
|
+
|
|
807
839
|
const _convertInsert = (query, model) => {
|
|
808
840
|
// resolve path expression
|
|
809
841
|
const resolvedIntoClause = _convertPathExpressionForInsert(query.INSERT.into, model)
|
|
@@ -950,6 +982,10 @@ const cqn2cqn4sql = (query, model, options = { suppressSearch: false }) => {
|
|
|
950
982
|
return _convertInsert(query, model)
|
|
951
983
|
}
|
|
952
984
|
|
|
985
|
+
if (query.UPSERT) {
|
|
986
|
+
return _convertUpsert(query, model)
|
|
987
|
+
}
|
|
988
|
+
|
|
953
989
|
if (query.DELETE) {
|
|
954
990
|
return _convertDelete(query, model, options)
|
|
955
991
|
}
|
|
@@ -1,41 +1,21 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
const { ensureNoDraftsSuffix } = require('./draft')
|
|
3
3
|
|
|
4
|
-
/*
|
|
5
|
-
* returns path like <service>.<entity>:<prop1>.<prop2> for ref = [{ id: '<service>.<entity>' }, '<prop1>', '<prop2>']
|
|
6
|
-
*/
|
|
7
|
-
const getPathFromRef = ref => {
|
|
8
|
-
const x = ref.reduce((acc, cur) => {
|
|
9
|
-
acc += (acc ? ':' : '') + (cur.id ? cur.id : cur)
|
|
10
|
-
return acc
|
|
11
|
-
}, '')
|
|
12
|
-
const y = x.split(':')
|
|
13
|
-
let z = y.shift()
|
|
14
|
-
if (y.length) z += ':' + y.join('.')
|
|
15
|
-
return z
|
|
16
|
-
}
|
|
17
|
-
|
|
18
4
|
/*
|
|
19
5
|
* returns the target entity for the given path
|
|
20
6
|
*/
|
|
21
7
|
const getEntityFromPath = (path, def) => {
|
|
22
8
|
let current = def.definitions ? { elements: def.definitions } : def
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
segment.id = ensureNoDraftsSuffix(segment.id)
|
|
29
|
-
} else if (typeof segment === 'string') {
|
|
30
|
-
segment = ensureNoDraftsSuffix(segment)
|
|
31
|
-
}
|
|
32
|
-
current = current.elements[segment.id || segment]
|
|
9
|
+
|
|
10
|
+
let id
|
|
11
|
+
for (const segment of path.ref) {
|
|
12
|
+
id = ensureNoDraftsSuffix(segment.id || segment)
|
|
13
|
+
current = current.elements[id]
|
|
33
14
|
if (current && current.target) current = current._target
|
|
34
15
|
}
|
|
35
16
|
return current
|
|
36
17
|
}
|
|
37
18
|
|
|
38
19
|
module.exports = {
|
|
39
|
-
getPathFromRef,
|
|
40
20
|
getEntityFromPath
|
|
41
21
|
}
|
|
@@ -679,6 +679,8 @@ const findQueryTarget = q => {
|
|
|
679
679
|
? q.INSERT._transitions[q.INSERT._transitions.length - 1].target
|
|
680
680
|
: q.UPDATE
|
|
681
681
|
? q.UPDATE._transitions[q.UPDATE._transitions.length - 1].target
|
|
682
|
+
: q.UPSERT
|
|
683
|
+
? q.UPSERT._transitions[q.UPSERT._transitions.length - 1].target
|
|
682
684
|
: q.DELETE
|
|
683
685
|
? q.DELETE._transitions[q.DELETE._transitions.length - 1].target
|
|
684
686
|
: undefined
|
|
@@ -15,20 +15,24 @@ const search2cqn4sql = (query, model, options = {}) => {
|
|
|
15
15
|
const { search2cqn4sql } = options
|
|
16
16
|
const { entityName, alias } = _targetFrom(query.SELECT.from, options)
|
|
17
17
|
const entity = model.definitions[entityName]
|
|
18
|
-
const
|
|
19
|
-
|
|
18
|
+
const localizedAssociation = entity.associations?.localized
|
|
20
19
|
// Call custom (optimized search to cqn for sql implementation) that tries
|
|
21
20
|
// to optimize the search behavior for a specific database service.
|
|
22
21
|
// REVISIT: $search query option combined with $count is not currently optimized
|
|
23
|
-
if (
|
|
24
|
-
|
|
22
|
+
if (
|
|
23
|
+
typeof search2cqn4sql === 'function' &&
|
|
24
|
+
!query.SELECT.count &&
|
|
25
|
+
localizedAssociation &&
|
|
26
|
+
!(query._aggregated || /* new parser */ query.SELECT.groupBy)
|
|
27
|
+
) {
|
|
28
|
+
const search2cqnOptions = { columns: computeColumnsToBeSearched(query, entity), locale: options.locale }
|
|
25
29
|
return search2cqn4sql(query, entity, search2cqnOptions)
|
|
26
|
-
}
|
|
30
|
+
} else {
|
|
31
|
+
const expression = searchToLike(cqnSearchPhrase, computeColumnsToBeSearched(query, entity, alias))
|
|
27
32
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
|
|
33
|
+
// REVISIT: find out here if where or having must be used
|
|
34
|
+
query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
|
|
35
|
+
}
|
|
32
36
|
}
|
|
33
37
|
|
|
34
38
|
module.exports = search2cqn4sql
|
|
@@ -40,7 +40,8 @@ function getCqnCopy(readToOneCQN) {
|
|
|
40
40
|
|
|
41
41
|
class JoinCQNFromExpanded {
|
|
42
42
|
constructor(cqn, csn, locale) {
|
|
43
|
-
this._SELECT =
|
|
43
|
+
this._SELECT = {}
|
|
44
|
+
for (const prop in cqn.SELECT) this._SELECT[prop] = cqn.SELECT[prop]
|
|
44
45
|
this._csn = csn
|
|
45
46
|
// REVISIT: locale is only passed in case of sqlite -> bad coding
|
|
46
47
|
if (cds.env.i18n.for_sqlite.includes(locale)) {
|
|
@@ -36,6 +36,10 @@ class InsertBuilder extends BaseBuilder {
|
|
|
36
36
|
this._csn = csn
|
|
37
37
|
}
|
|
38
38
|
|
|
39
|
+
annotatedColumns(entityName, csn) {
|
|
40
|
+
return getAnnotatedColumns(entityName, csn)
|
|
41
|
+
}
|
|
42
|
+
|
|
39
43
|
/**
|
|
40
44
|
* Builds an Object based on the properties of the CQN object.
|
|
41
45
|
*
|
|
@@ -77,7 +81,7 @@ class InsertBuilder extends BaseBuilder {
|
|
|
77
81
|
this._findUuidKeys(entityName)
|
|
78
82
|
|
|
79
83
|
this._columnIndexesToDelete = []
|
|
80
|
-
const annotatedColumns =
|
|
84
|
+
const annotatedColumns = this.annotatedColumns(entityName, this._csn)
|
|
81
85
|
|
|
82
86
|
if (this._obj.INSERT.columns) {
|
|
83
87
|
this._removeAlreadyExistingInsertAnnotatedColumnsFromMap(annotatedColumns)
|
|
@@ -6,40 +6,17 @@ class UpsertBuilder extends InsertBuilder {
|
|
|
6
6
|
super(obj, options, csn)
|
|
7
7
|
}
|
|
8
8
|
|
|
9
|
+
annotatedColumns(entityName, csn) {
|
|
10
|
+
const { updateAnnotatedColumns } = getAnnotatedColumns(entityName, csn)
|
|
11
|
+
return { insertAnnotatedColumns: updateAnnotatedColumns }
|
|
12
|
+
}
|
|
13
|
+
|
|
9
14
|
// REVISIT: We need to copy over the implementation for annotation handling
|
|
10
15
|
build() {
|
|
11
|
-
this.
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
this._obj = { INSERT: this._obj.UPSERT, _target: this._obj._target }
|
|
16
|
-
|
|
17
|
-
const entityName = this._into()
|
|
18
|
-
|
|
19
|
-
this._columnIndexesToDelete = []
|
|
20
|
-
const annotatedColumns = getAnnotatedColumns(entityName, this._csn)
|
|
21
|
-
// hack: treat update annotations as insert because of sql builder impl
|
|
22
|
-
if (annotatedColumns) {
|
|
23
|
-
annotatedColumns.insertAnnotatedColumns = annotatedColumns.updateAnnotatedColumns
|
|
24
|
-
}
|
|
25
|
-
|
|
26
|
-
if (this._obj.INSERT.columns) {
|
|
27
|
-
this._removeAlreadyExistingInsertAnnotatedColumnsFromMap(annotatedColumns)
|
|
28
|
-
this._columns(annotatedColumns)
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
if (this._obj.INSERT.values || this._obj.INSERT.rows) {
|
|
32
|
-
if (annotatedColumns && !this._obj.INSERT.columns) {
|
|
33
|
-
// if columns not provided get indexes from csn
|
|
34
|
-
this._getAnnotatedColumnIndexes(annotatedColumns)
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
this._values(annotatedColumns)
|
|
38
|
-
} else if (this._obj.INSERT.entries && this._obj.INSERT.entries.length !== 0) {
|
|
39
|
-
this._entries(annotatedColumns)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
this._outputObj.sql = this._outputObj.sql.join(' ') + ' WITH PRIMARY KEY'
|
|
16
|
+
this._obj = { INSERT: this._obj.UPSERT }
|
|
17
|
+
super.build()
|
|
18
|
+
this._outputObj.sql = this._outputObj.sql.replace('INSERT INTO', 'UPSERT')
|
|
19
|
+
this._outputObj.sql += ' WITH PRIMARY KEY'
|
|
43
20
|
return this._outputObj
|
|
44
21
|
}
|
|
45
22
|
}
|
|
@@ -16,7 +16,10 @@ const _getAnnotationNames = column => {
|
|
|
16
16
|
const getAnnotatedColumns = (entityName, csn) => {
|
|
17
17
|
const entityNameWithoutSuffix = ensureNoDraftsSuffix(entityName)
|
|
18
18
|
if (!csn || !csn.definitions[entityNameWithoutSuffix]) {
|
|
19
|
-
return
|
|
19
|
+
return {
|
|
20
|
+
insertAnnotatedColumns: new Map(),
|
|
21
|
+
updateAnnotatedColumns: new Map()
|
|
22
|
+
}
|
|
20
23
|
}
|
|
21
24
|
const columns = getColumns(csn.definitions[entityNameWithoutSuffix])
|
|
22
25
|
const insertAnnotatedColumns = new Map()
|
|
@@ -39,8 +42,8 @@ const getAnnotatedColumns = (entityName, csn) => {
|
|
|
39
42
|
}
|
|
40
43
|
|
|
41
44
|
return {
|
|
42
|
-
insertAnnotatedColumns
|
|
43
|
-
updateAnnotatedColumns
|
|
45
|
+
insertAnnotatedColumns,
|
|
46
|
+
updateAnnotatedColumns
|
|
44
47
|
}
|
|
45
48
|
}
|
|
46
49
|
|
|
@@ -174,6 +174,10 @@ const fioriGenericActivate = async function (req) {
|
|
|
174
174
|
})
|
|
175
175
|
])
|
|
176
176
|
|
|
177
|
+
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
178
|
+
// status code must be set in handler to allow overriding for FE V2
|
|
179
|
+
req?._?.odataRes.setStatusCode(201)
|
|
180
|
+
|
|
177
181
|
return result
|
|
178
182
|
}
|
|
179
183
|
|