@sap/cds 6.4.1 → 6.6.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 +79 -6
- package/README.md +5 -0
- package/apis/cqn.d.ts +14 -3
- package/apis/ql.d.ts +8 -8
- package/apis/services.d.ts +37 -65
- package/apis/test.d.ts +7 -0
- package/bin/build/buildTaskEngine.js +9 -14
- package/bin/build/buildTaskFactory.js +1 -1
- package/bin/build/buildTaskHandler.js +3 -14
- package/bin/build/index.js +8 -2
- package/bin/build/provider/buildTaskProviderInternal.js +18 -13
- package/bin/build/provider/fiori/index.js +5 -10
- package/bin/build/provider/hana/2migration.js +11 -2
- package/bin/build/provider/hana/index.js +17 -14
- package/bin/build/provider/hana/template/.hdiconfig-hanacloud +137 -0
- 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 +57 -37
- package/bin/build/provider/mtx-sidecar/index.js +1 -1
- package/bin/build/util.js +18 -1
- package/bin/cds.js +1 -5
- package/bin/deploy/to-hana/hana.js +10 -3
- package/bin/serve.js +36 -20
- package/common.cds +7 -0
- package/lib/auth/jwt-auth.js +8 -7
- package/lib/compile/for/lean_drafts.js +55 -6
- package/lib/compile/minify.js +3 -3
- package/lib/dbs/cds-deploy.js +18 -17
- 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/index.js +4 -2
- 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/cds-context.js +3 -3
- package/lib/req/response.js +8 -3
- package/lib/req/user.js +12 -2
- package/lib/srv/bindings.js +1 -2
- package/lib/srv/cds-serve.js +2 -1
- package/lib/srv/middlewares/trace.js +31 -15
- package/lib/srv/protocols/odata-v2-proxy.js +8 -8
- package/lib/srv/srv-handlers.js +26 -7
- package/lib/srv/srv-methods.js +2 -2
- package/lib/srv/srv-models.js +8 -3
- package/lib/utils/cds-test.js +7 -5
- 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 +3 -2
- 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/OData.js +6 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +26 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +8 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +11 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/PrimitiveValueDecoder.js +8 -8
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/utils/ValueConverter.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/validator/ValueValidator.js +14 -14
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/DeserializerFactory.js +1 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ResourceJsonSerializer.js +3 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/UriHelper.js +2 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +3 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +7 -0
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/stream.js +0 -3
- package/libx/_runtime/cds-services/services/Service.js +11 -19
- package/libx/_runtime/cds-services/services/utils/columns.js +42 -40
- package/libx/_runtime/cds-services/util/assert.js +7 -1
- package/libx/_runtime/common/code-ext/WorkerReq.js +81 -0
- package/libx/_runtime/common/code-ext/config.js +13 -0
- package/libx/_runtime/common/code-ext/execute.js +113 -0
- package/libx/_runtime/common/code-ext/handlers.js +49 -0
- package/libx/_runtime/common/code-ext/worker.js +40 -0
- package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
- package/libx/_runtime/common/code-ext/workerQueryExecutor.js +36 -0
- package/libx/_runtime/common/composition/data.js +5 -2
- package/libx/_runtime/common/composition/tree.js +2 -0
- package/libx/_runtime/common/generic/auth/restrict.js +1 -1
- package/libx/_runtime/common/generic/crud.js +4 -0
- package/libx/_runtime/common/generic/etag.js +3 -1
- package/libx/_runtime/common/generic/input.js +12 -14
- package/libx/_runtime/common/i18n/index.js +1 -1
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +47 -22
- package/libx/_runtime/common/utils/path.js +5 -26
- package/libx/_runtime/common/utils/search2cqn4sql.js +16 -9
- package/libx/_runtime/common/utils/templateProcessorPathSerializer.js +19 -13
- package/libx/_runtime/db/data-conversion/post-processing.js +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +7 -4
- package/libx/_runtime/db/expand/rawToExpanded.js +3 -2
- package/libx/_runtime/db/generic/input.js +2 -2
- package/libx/_runtime/db/generic/integrity.js +1 -0
- package/libx/_runtime/db/generic/virtual.js +1 -0
- package/libx/_runtime/db/query/read.js +3 -2
- package/libx/_runtime/db/utils/localized.js +1 -1
- package/libx/_runtime/fiori/generic/activate.js +7 -1
- package/libx/_runtime/fiori/generic/before.js +9 -1
- package/libx/_runtime/fiori/generic/edit.js +8 -1
- package/libx/_runtime/fiori/generic/new.js +2 -0
- package/libx/_runtime/fiori/generic/patch.js +2 -0
- package/libx/_runtime/fiori/generic/prepare.js +2 -0
- package/libx/_runtime/fiori/generic/read.js +16 -5
- package/libx/_runtime/fiori/generic/readOverDraft.js +2 -0
- package/libx/_runtime/fiori/lean-draft.js +505 -241
- package/libx/_runtime/fiori/utils/delete.js +2 -0
- 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/_runtime/messaging/Outbox.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -0
- package/libx/_runtime/messaging/enterprise-messaging.js +2 -6
- package/libx/_runtime/messaging/file-based.js +1 -2
- package/libx/_runtime/messaging/outbox/OutboxRunner.js +1 -1
- package/libx/_runtime/messaging/outbox/utils.js +1 -1
- package/libx/_runtime/messaging/service.js +0 -1
- package/libx/_runtime/remote/Service.js +1 -0
- package/libx/_runtime/sqlite/convertDraftAdminPathExpression.js +19 -3
- package/libx/_runtime/sqlite/customBuilder/CustomExpressionBuilder.js +0 -18
- package/libx/_runtime/sqlite/customBuilder/CustomFunctionBuilder.js +0 -18
- package/libx/_runtime/sqlite/customBuilder/CustomSelectBuilder.js +0 -24
- package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +2 -1
- package/libx/_runtime/sqlite/customBuilder/index.js +47 -32
- package/libx/odata/afterburner.js +23 -8
- package/libx/odata/cqn2odata.js +1 -1
- package/libx/odata/grammar.pegjs +3 -4
- package/libx/odata/index.js +5 -1
- package/libx/odata/parseToCqn.js +3 -3
- package/libx/odata/parser.js +1 -1
- package/libx/odata/utils.js +58 -1
- package/libx/rest/middleware/parse.js +26 -4
- package/package.json +1 -1
- package/server.js +1 -1
- package/libx/_runtime/sqlite/customBuilder/CustomDeleteBuilder.js +0 -17
- package/libx/_runtime/sqlite/customBuilder/CustomReferenceBuilder.js +0 -11
- package/libx/_runtime/sqlite/customBuilder/CustomUpdateBuilder.js +0 -17
- /package/bin/build/provider/hana/template/{.hdiconfig → .hdiconfig-haas} +0 -0
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
const LOG = cds.log()
|
|
3
|
+
const { parentPort, workerData } = require('worker_threads')
|
|
4
|
+
const { WorkerSELECT, WorkerINSERT, WorkerUPSERT, WorkerUPDATE, WorkerDELETE } = require('./workerQuery')
|
|
5
|
+
const WorkerReq = require('./WorkerReq')
|
|
6
|
+
const { timeout } = require('./config')
|
|
7
|
+
|
|
8
|
+
parentPort.once('message', function onWorkerMessageReceived(message) {
|
|
9
|
+
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
|
+
if (kind !== 'start' || workerId !== workerData.id) return
|
|
12
|
+
|
|
13
|
+
// eslint-disable-next-line cds/no-missing-dependencies
|
|
14
|
+
const { VM } = require('vm2')
|
|
15
|
+
const workerReq = new WorkerReq(contextId, reqData)
|
|
16
|
+
const vm = new VM({
|
|
17
|
+
console: 'inherit',
|
|
18
|
+
timeout, // specifies the number of milliseconds to execute code before terminating execution
|
|
19
|
+
allowAsync: true,
|
|
20
|
+
|
|
21
|
+
// the sandbox represents the global object inside the vm instance
|
|
22
|
+
sandbox: {
|
|
23
|
+
req: workerReq,
|
|
24
|
+
SELECT: WorkerSELECT._api(),
|
|
25
|
+
INSERT: WorkerINSERT._api(),
|
|
26
|
+
UPSERT: WorkerUPSERT._api(),
|
|
27
|
+
UPDATE: WorkerUPDATE._api(),
|
|
28
|
+
DELETE: WorkerDELETE._api()
|
|
29
|
+
}
|
|
30
|
+
})
|
|
31
|
+
|
|
32
|
+
try {
|
|
33
|
+
;(async function () {
|
|
34
|
+
const result = await vm.run(code)
|
|
35
|
+
parentPort.postMessage({ contextId, kind: 'success', req: reqData, postMessages: workerReq.postMessages, result })
|
|
36
|
+
})()
|
|
37
|
+
} catch (error) {
|
|
38
|
+
parentPort.postMessage({ contextId, kind: 'error', error })
|
|
39
|
+
}
|
|
40
|
+
})
|
|
@@ -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,36 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
2
|
+
const LOG = cds.log()
|
|
3
|
+
const { parentPort } = require('worker_threads')
|
|
4
|
+
const executorCallbackMap = new Map()
|
|
5
|
+
|
|
6
|
+
parentPort.on('message', function onWorkerMessageReceived(message) {
|
|
7
|
+
const { id, kind, result } = message
|
|
8
|
+
if (LOG._debug) LOG.debug(`Post message received on worker thread (workerQueryExecutor.js) from main thread`, message)
|
|
9
|
+
if (!executorCallbackMap.has(id)) return
|
|
10
|
+
|
|
11
|
+
switch (kind) {
|
|
12
|
+
case 'responseData':
|
|
13
|
+
executorCallbackMap.get(id)(result)
|
|
14
|
+
executorCallbackMap.delete(id)
|
|
15
|
+
return
|
|
16
|
+
|
|
17
|
+
case 'cleanup':
|
|
18
|
+
executorCallbackMap.delete(id)
|
|
19
|
+
return
|
|
20
|
+
}
|
|
21
|
+
})
|
|
22
|
+
|
|
23
|
+
function queryExecutor(resolve) {
|
|
24
|
+
const id = cds.utils.uuid()
|
|
25
|
+
executorCallbackMap.set(id, result => resolve(result))
|
|
26
|
+
parentPort.postMessage({
|
|
27
|
+
id,
|
|
28
|
+
kind: 'run',
|
|
29
|
+
target: 'srv',
|
|
30
|
+
prop: 'run',
|
|
31
|
+
responseData: true,
|
|
32
|
+
args: [this]
|
|
33
|
+
})
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
module.exports = queryExecutor
|
|
@@ -78,7 +78,9 @@ const _whereKeys = keySet => {
|
|
|
78
78
|
}
|
|
79
79
|
|
|
80
80
|
const _parentKey = (element, key) => {
|
|
81
|
-
|
|
81
|
+
let links = [...element.customBackLinks, ...element.backLinks]
|
|
82
|
+
if (element.is2one && links.some(l => l.for2one)) links = links.filter(l => l.for2one)
|
|
83
|
+
return links.reduce((parentKey, customBackLink) => {
|
|
82
84
|
// TODO: why Object.prototype.hasOwnProperty?
|
|
83
85
|
parentKey[customBackLink.entityKey] = Object.prototype.hasOwnProperty.call(key, customBackLink.targetKey)
|
|
84
86
|
? key[customBackLink.targetKey]
|
|
@@ -139,7 +141,8 @@ const _getWhereObj = (row, links) => {
|
|
|
139
141
|
|
|
140
142
|
const _subWhere = (result, element) => {
|
|
141
143
|
let where
|
|
142
|
-
|
|
144
|
+
let links = [...element.backLinks, ...element.customBackLinks]
|
|
145
|
+
if (element.is2one && links.some(l => l.for2one)) links = links.filter(l => l.for2one)
|
|
143
146
|
if (result.length && links && links.length > 0) {
|
|
144
147
|
where = {}
|
|
145
148
|
const keys0 = Object.keys(_getWhereObj(result[0], links))
|
|
@@ -132,6 +132,8 @@ const _getCompositionTreeRec = ({
|
|
|
132
132
|
if (element.is2many) {
|
|
133
133
|
subObject.customBackLinks.push(...backLinks)
|
|
134
134
|
} else {
|
|
135
|
+
subObject.is2one = true
|
|
136
|
+
backLinks.forEach(b => (b.for2one = true))
|
|
135
137
|
subObject.backLinks.push(...backLinks)
|
|
136
138
|
}
|
|
137
139
|
}
|
|
@@ -142,7 +142,7 @@ const _getRestrictionForTarget = (resolvedApplicables, target) => {
|
|
|
142
142
|
}
|
|
143
143
|
|
|
144
144
|
const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
|
|
145
|
-
if (req.target._isDraftEnabled) {
|
|
145
|
+
if (!cds.env.features.lean_draft && req.target._isDraftEnabled) {
|
|
146
146
|
req.query._draftRestrictions = resolvedApplicables
|
|
147
147
|
return
|
|
148
148
|
}
|
|
@@ -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
|
}
|
|
@@ -81,7 +81,9 @@ const commonGenericEtag = async function (req) {
|
|
|
81
81
|
|
|
82
82
|
// validate
|
|
83
83
|
if (req.isConditional && !req.query.INSERT) {
|
|
84
|
-
|
|
84
|
+
let cqn = getSelectCQN(req.query, req.target, this.model)
|
|
85
|
+
if (req.query.UPDATE || req.query.DELETE) cqn = cqn.forUpdate()
|
|
86
|
+
const result = await cds.tx(req).run(cqn)
|
|
85
87
|
|
|
86
88
|
if (result.length === 1) {
|
|
87
89
|
const etag = Object.values(result[0])[0]
|
|
@@ -36,10 +36,10 @@ const _getSimpleCategory = category => {
|
|
|
36
36
|
}
|
|
37
37
|
|
|
38
38
|
const _isDraftCoreComputed = (req, element, event) =>
|
|
39
|
+
element['@Core.Computed'] &&
|
|
39
40
|
cds.env.features.preserve_computed !== false &&
|
|
40
41
|
req._ &&
|
|
41
42
|
req._.event === 'draftActivate' &&
|
|
42
|
-
element['@Core.Computed'] &&
|
|
43
43
|
!((event === 'CREATE' && element['@cds.on.insert']) || element['@cds.on.update'])
|
|
44
44
|
|
|
45
45
|
const _isStreamingProperty = (elements, row, property) =>
|
|
@@ -217,16 +217,8 @@ const _callError = (req, errors) => {
|
|
|
217
217
|
for (const error of errors) req.error(error)
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
-
const
|
|
221
|
-
|
|
222
|
-
const _getBoundActionBindingParameter = req => {
|
|
223
|
-
// REVISIT: req._ gets set in onDraftActivate to original req
|
|
224
|
-
const action = (req._ && req._.event) || req.event
|
|
225
|
-
const actions = req.target.actions
|
|
226
|
-
|
|
227
|
-
// 'in' is the default binding parameter name for bound actions/functions
|
|
228
|
-
return (actions && actions[action] && actions[action]['@cds.odata.bindingparameter.name']) || 'in'
|
|
229
|
-
}
|
|
220
|
+
const _getBoundAction = req => req.target.actions?.[req.context?.event]
|
|
221
|
+
const _getBoundActionBindingParameter = action => action['@cds.odata.bindingparameter.name'] || 'in'
|
|
230
222
|
|
|
231
223
|
async function commonGenericInput(req) {
|
|
232
224
|
if (!req.query) return // FIXME: the code below expects req.query to be defined
|
|
@@ -250,8 +242,10 @@ async function commonGenericInput(req) {
|
|
|
250
242
|
pathSegments: []
|
|
251
243
|
}
|
|
252
244
|
|
|
253
|
-
|
|
254
|
-
|
|
245
|
+
const boundAction = _getBoundAction(req)
|
|
246
|
+
|
|
247
|
+
if (boundAction) {
|
|
248
|
+
const pathSegment = _getBoundActionBindingParameter(boundAction)
|
|
255
249
|
const keys = req._ && req._.params && req._.params[0]
|
|
256
250
|
if (pathSegment) pathOptions.pathSegments.push(pathSegment)
|
|
257
251
|
|
|
@@ -370,7 +364,11 @@ commonGenericInput._initial = true
|
|
|
370
364
|
_actionFunctionHandler._initial = true
|
|
371
365
|
|
|
372
366
|
module.exports = cds.service.impl(function () {
|
|
373
|
-
|
|
367
|
+
if (cds.env.features.lean_draft) {
|
|
368
|
+
this.before(['CREATE', 'UPDATE'], '*', commonGenericInput)
|
|
369
|
+
} else {
|
|
370
|
+
this.before(['CREATE', 'UPDATE', 'NEW', 'PATCH'], '*', commonGenericInput)
|
|
371
|
+
}
|
|
374
372
|
const operationNames = []
|
|
375
373
|
|
|
376
374
|
for (const operation of this.operations) {
|
|
@@ -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')
|
|
@@ -142,14 +142,16 @@ const _getWindowWhere = (where, bottomTop) => {
|
|
|
142
142
|
|
|
143
143
|
const _getOrderByForWindowFn = bottomTop => {
|
|
144
144
|
const orderBy = _getBottomTopRefOrVal(bottomTop[0], 'ref')[0]
|
|
145
|
-
|
|
146
|
-
return orderBy
|
|
145
|
+
const sort = bottomTop[0].func === 'topcount' ? 'desc' : 'asc'
|
|
146
|
+
return [orderBy, sort]
|
|
147
147
|
}
|
|
148
148
|
|
|
149
149
|
const _getWindowXpr = (groupBy, bottomTop) => {
|
|
150
|
-
const
|
|
150
|
+
const overXpr = { xpr: [] }
|
|
151
|
+
const xpr = [{ func: 'ROW_NUMBER', args: [] }, 'OVER', overXpr]
|
|
152
|
+
|
|
151
153
|
if (groupBy)
|
|
152
|
-
xpr.push(
|
|
154
|
+
overXpr.xpr.push(
|
|
153
155
|
'PARTITION BY',
|
|
154
156
|
...groupBy.reduce((acc, el, i) => {
|
|
155
157
|
if (i < groupBy.length - 1) {
|
|
@@ -163,9 +165,8 @@ const _getWindowXpr = (groupBy, bottomTop) => {
|
|
|
163
165
|
}, [])
|
|
164
166
|
)
|
|
165
167
|
|
|
166
|
-
xpr.push('ORDER BY', _getOrderByForWindowFn(bottomTop))
|
|
167
|
-
xpr
|
|
168
|
-
return { xpr: xpr, as: 'rowNumber' }
|
|
168
|
+
overXpr.xpr.push('ORDER BY', ..._getOrderByForWindowFn(bottomTop))
|
|
169
|
+
return { xpr, as: 'rowNumber' }
|
|
169
170
|
}
|
|
170
171
|
|
|
171
172
|
const _isNavCountFunc = el => el.func && el.func === 'count' && el.args[0] !== '*'
|
|
@@ -445,7 +446,7 @@ const convertWhereExists = (query, model, options, currentTarget) => {
|
|
|
445
446
|
if (currentTarget) {
|
|
446
447
|
queryTarget = getEntityFromPath({ ref }, currentTarget)
|
|
447
448
|
} else {
|
|
448
|
-
queryTarget = getEntityFromPath(
|
|
449
|
+
queryTarget = getEntityFromPath({ ref }, model)
|
|
449
450
|
outerAlias = as || PARENT_ALIAS + lambdaIteration
|
|
450
451
|
innerAlias = FOREIGN_ALIAS + lambdaIteration
|
|
451
452
|
}
|
|
@@ -586,6 +587,7 @@ const _convertOrderByOrWhereIfSkip = (cqn, target, model) => {
|
|
|
586
587
|
}
|
|
587
588
|
}
|
|
588
589
|
|
|
590
|
+
/* REVISIT: Unused
|
|
589
591
|
const _convertExpand = expand => {
|
|
590
592
|
expand.forEach(expandElement => {
|
|
591
593
|
if (expandElement.ref && expandElement.ref[0]) {
|
|
@@ -603,15 +605,21 @@ const _convertExpand = expand => {
|
|
|
603
605
|
}
|
|
604
606
|
})
|
|
605
607
|
}
|
|
608
|
+
*/
|
|
606
609
|
|
|
607
|
-
const
|
|
608
|
-
if (
|
|
610
|
+
const _simplifyWhere = col => {
|
|
611
|
+
if (col.ref?.[0].where) {
|
|
612
|
+
col.where = col.ref[0].where
|
|
613
|
+
col.ref[0] = col.ref[0].id
|
|
614
|
+
}
|
|
615
|
+
}
|
|
609
616
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
617
|
+
const _simplifyWhereInColumns = columns => {
|
|
618
|
+
if (!columns) return
|
|
619
|
+
for (const col of columns) {
|
|
620
|
+
_simplifyWhere(col)
|
|
621
|
+
if (col.expand) _simplifyWhereInColumns(col.expand)
|
|
622
|
+
}
|
|
615
623
|
}
|
|
616
624
|
|
|
617
625
|
const _convertPathExpression = (query, model, options = {}) => {
|
|
@@ -742,8 +750,8 @@ const _convertSelect = (query, model, _options) => {
|
|
|
742
750
|
_convertToOneEqNullInFilter(query.SELECT, target)
|
|
743
751
|
}
|
|
744
752
|
|
|
745
|
-
// extract where clause if it is in
|
|
746
|
-
|
|
753
|
+
// extract where clause if it is in an expand column
|
|
754
|
+
_simplifyWhereInColumns(query.SELECT.columns)
|
|
747
755
|
|
|
748
756
|
// REVISIT: The following operations only work for _one_ entity.
|
|
749
757
|
// We must also enable them for joins etc.
|
|
@@ -822,7 +830,11 @@ const _convertUpsert = (query, model) => {
|
|
|
822
830
|
|
|
823
831
|
// We add all previous properties ot the newly created query.
|
|
824
832
|
// Reason is to not lose the query API functionality
|
|
825
|
-
|
|
833
|
+
|
|
834
|
+
for (const key in query.UPSERT) {
|
|
835
|
+
upsert.UPSERT[key] = query.UPSERT[key]
|
|
836
|
+
}
|
|
837
|
+
Object.assign(upsert.UPSERT, { into: { ref: [resolvedIntoClause], as: query.UPSERT.into.as } })
|
|
826
838
|
|
|
827
839
|
const resolved = resolveView(upsert, model, cds.db)
|
|
828
840
|
// required for deplyoing of extensions, not used anywhere else except UpsertBuilder
|
|
@@ -843,7 +855,11 @@ const _convertInsert = (query, model) => {
|
|
|
843
855
|
|
|
844
856
|
// We add all previous properties ot the newly created query.
|
|
845
857
|
// Reason is to not lose the query API functionality
|
|
846
|
-
|
|
858
|
+
insert.INSERT = {}
|
|
859
|
+
for (const prop in query.INSERT) {
|
|
860
|
+
insert.INSERT[prop] = query.INSERT[prop]
|
|
861
|
+
}
|
|
862
|
+
Object.assign(insert.INSERT, { into: { ref: [resolvedIntoClause], as: query.INSERT.into.as } })
|
|
847
863
|
|
|
848
864
|
const target = model.definitions[resolvedIntoClause]
|
|
849
865
|
if (!target) return insert
|
|
@@ -895,7 +911,12 @@ const _convertDelete = (query, model, options) => {
|
|
|
895
911
|
|
|
896
912
|
const { target, alias, where } = convertPathExpressionToWhere(query.DELETE.from, model, options)
|
|
897
913
|
const deleet = DELETE('x')
|
|
898
|
-
|
|
914
|
+
|
|
915
|
+
for (const key in query.DELETE) {
|
|
916
|
+
deleet.DELETE[key] = query.DELETE[key]
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
Object.assign(deleet.DELETE, { from: target, where: undefined })
|
|
899
920
|
|
|
900
921
|
if (alias) deleet.DELETE.from = { ref: [target], as: alias }
|
|
901
922
|
if (where) deleet.where(where)
|
|
@@ -940,7 +961,11 @@ const _convertUpdate = (query, model, options) => {
|
|
|
940
961
|
// link .with and .data and set query target and remove current where clause
|
|
941
962
|
// REVISIT: update statement does not accept cqn partial as input
|
|
942
963
|
const update = UPDATE('x')
|
|
943
|
-
Object.assign(update.UPDATE, query.UPDATE, { entity: target, where: undefined })
|
|
964
|
+
// Object.assign(update.UPDATE, query.UPDATE, { entity: target, where: undefined })
|
|
965
|
+
for (const key in query.UPDATE) {
|
|
966
|
+
update.UPDATE[key] = query.UPDATE[key]
|
|
967
|
+
}
|
|
968
|
+
Object.assign(update.UPDATE, { entity: target, where: undefined })
|
|
944
969
|
|
|
945
970
|
if (alias) update.UPDATE.entity = { ref: [target], as: alias }
|
|
946
971
|
if (where) update.where(where)
|
|
@@ -1,41 +1,20 @@
|
|
|
1
|
-
const cds = require('../../cds')
|
|
2
1
|
const { ensureNoDraftsSuffix } = require('./draft')
|
|
3
2
|
|
|
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
3
|
/*
|
|
19
4
|
* returns the target entity for the given path
|
|
20
5
|
*/
|
|
21
6
|
const getEntityFromPath = (path, def) => {
|
|
22
7
|
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]
|
|
8
|
+
|
|
9
|
+
let id
|
|
10
|
+
for (const segment of path.ref) {
|
|
11
|
+
id = ensureNoDraftsSuffix(segment.id || segment)
|
|
12
|
+
current = current.elements[id]
|
|
33
13
|
if (current && current.target) current = current._target
|
|
34
14
|
}
|
|
35
15
|
return current
|
|
36
16
|
}
|
|
37
17
|
|
|
38
18
|
module.exports = {
|
|
39
|
-
getPathFromRef,
|
|
40
19
|
getEntityFromPath
|
|
41
20
|
}
|
|
@@ -15,20 +15,27 @@ 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 columnsToBeSearched = computeColumnsToBeSearched(query, entity, alias)
|
|
32
|
+
const expression = columnsToBeSearched?.length
|
|
33
|
+
? searchToLike(cqnSearchPhrase, columnsToBeSearched)
|
|
34
|
+
: [{ val: 0 }, '=', { val: 1 }]
|
|
27
35
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
|
|
36
|
+
// REVISIT: find out here if where or having must be used
|
|
37
|
+
query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
|
|
38
|
+
}
|
|
32
39
|
}
|
|
33
40
|
|
|
34
41
|
module.exports = search2cqn4sql
|
|
@@ -1,20 +1,26 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
1
2
|
const templatePathSerializer = (tKey, keyNames, row, elements, draftKeys) => {
|
|
2
|
-
const keyValuePairs = keyNames
|
|
3
|
-
|
|
3
|
+
const keyValuePairs = keyNames
|
|
4
|
+
.filter(key => {
|
|
5
|
+
if (cds.env.features.lean_draft && key === 'IsActiveEntity') return false
|
|
6
|
+
return true
|
|
7
|
+
})
|
|
8
|
+
.map(key => {
|
|
9
|
+
let quote
|
|
4
10
|
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
11
|
+
switch (elements[key].type) {
|
|
12
|
+
case 'cds.String':
|
|
13
|
+
quote = "'"
|
|
14
|
+
break
|
|
9
15
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
16
|
+
default:
|
|
17
|
+
quote = ''
|
|
18
|
+
break
|
|
19
|
+
}
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
21
|
+
const keyValue = row[key] ?? draftKeys?.[key]
|
|
22
|
+
return `${key}=${quote}${keyValue}${quote}`
|
|
23
|
+
})
|
|
18
24
|
|
|
19
25
|
const keyValuePairsSerialized = keyValuePairs.join(',')
|
|
20
26
|
return `${tKey}(${keyValuePairsSerialized})`
|
|
@@ -91,7 +91,7 @@ const _getMapperForListedElements = (conversionMap, csn, cqn) => {
|
|
|
91
91
|
if (col.cast) {
|
|
92
92
|
const name = col.as ? col.as : col.ref[col.ref.length - 1]
|
|
93
93
|
_addConverter(mapper, name, (val, key, row, unaliasedKey) => {
|
|
94
|
-
row[unaliasedKey || key] = _getCastFunction(col.cast)(val)
|
|
94
|
+
row[unaliasedKey || key] = val === null ? null : _getCastFunction(col.cast)(val)
|
|
95
95
|
})
|
|
96
96
|
continue
|
|
97
97
|
}
|
|
@@ -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)) {
|
|
@@ -866,7 +867,7 @@ class JoinCQNFromExpanded {
|
|
|
866
867
|
const subWhere = []
|
|
867
868
|
|
|
868
869
|
for (const key in entity.keys) {
|
|
869
|
-
if (key === 'IsActiveEntity') continue
|
|
870
|
+
if (key === 'IsActiveEntity' || entity.keys[key]._isAssociationStrict) continue
|
|
870
871
|
if (subWhere.length) {
|
|
871
872
|
subWhere.push('and')
|
|
872
873
|
}
|
|
@@ -981,7 +982,7 @@ class JoinCQNFromExpanded {
|
|
|
981
982
|
...column,
|
|
982
983
|
xpr: column.xpr.map(x => {
|
|
983
984
|
if (x.ref) {
|
|
984
|
-
const res = this._buildNewAliasColumn(x, entity, tableAlias, mappings)
|
|
985
|
+
const res = this._buildNewAliasColumn(x, entity, tableAlias, mappings, true)
|
|
985
986
|
delete res.as
|
|
986
987
|
return res
|
|
987
988
|
} else return x
|
|
@@ -1012,7 +1013,7 @@ class JoinCQNFromExpanded {
|
|
|
1012
1013
|
return column.ref && typeof column.ref[column.ref.length - 1] !== 'string'
|
|
1013
1014
|
}
|
|
1014
1015
|
|
|
1015
|
-
_buildNewAliasColumn(column, entity, tableAlias, mappings) {
|
|
1016
|
+
_buildNewAliasColumn(column, entity, tableAlias, mappings, skipMapping = false) {
|
|
1016
1017
|
// Casted name, vs column name
|
|
1017
1018
|
const identifier = this._getIdentifier(column, tableAlias)
|
|
1018
1019
|
const as = column[SKIP_MAPPING] ? column.as : `${tableAlias}_${identifier}`
|
|
@@ -1026,6 +1027,8 @@ class JoinCQNFromExpanded {
|
|
|
1026
1027
|
aliasedElement.ref = alias ? [alias, column.ref[0]] : [column.ref[0]]
|
|
1027
1028
|
}
|
|
1028
1029
|
|
|
1030
|
+
if (skipMapping) return aliasedElement
|
|
1031
|
+
|
|
1029
1032
|
if (!column[SKIP_MAPPING]) {
|
|
1030
1033
|
mappings[column[IDENTIFIER] || identifier] = as
|
|
1031
1034
|
if (column[CLEANUP_KEYS]) {
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { DRAFT_COLUMNS_MAP } = require('../../common/constants/draft')
|
|
2
2
|
const DRAFT_COLUMNS_ARRAY = Object.keys(DRAFT_COLUMNS_MAP)
|
|
3
|
+
const cds = require('../../cds')
|
|
3
4
|
|
|
4
5
|
const EXPAND = Symbol.for('sap.cds.expand')
|
|
5
6
|
const GET_KEY_VALUE = Symbol.for('sap.cds.getKeyValue')
|
|
@@ -110,9 +111,9 @@ class RawToExpanded {
|
|
|
110
111
|
let expandedItems = this._getResultCache(toManyTree.concat(key))[mapping[GET_KEY_VALUE](false, entry)] || []
|
|
111
112
|
|
|
112
113
|
// the expanded items may include the actives of the deleted drafts -> filter out
|
|
113
|
-
if (rootIsActiveEntity !== null) {
|
|
114
|
+
if (!cds.env.features.lean_draft && rootIsActiveEntity !== null) {
|
|
114
115
|
if (mapping[TO_ACTIVE]) expandedItems = expandedItems.filter(ele => ele.IsActiveEntity !== false)
|
|
115
|
-
else expandedItems = expandedItems.filter(ele => ele.IsActiveEntity === rootIsActiveEntity)
|
|
116
|
+
else expandedItems = expandedItems.filter(ele => !!ele.IsActiveEntity === rootIsActiveEntity)
|
|
116
117
|
}
|
|
117
118
|
|
|
118
119
|
row[key] = !mapping[CLEANUP_KEYS]
|
|
@@ -197,11 +197,11 @@ function dbGenericInput(req) {
|
|
|
197
197
|
// call with this for this.model
|
|
198
198
|
normalizeTimeData.call(this, req)
|
|
199
199
|
|
|
200
|
-
const draft = req.target.name && req.target.name.match(/_drafts$/)
|
|
200
|
+
const draft = req.target.name && (req.target.name.match(/_drafts$/) || req.target.name.match(/\.drafts$/))
|
|
201
201
|
|
|
202
202
|
const target =
|
|
203
203
|
req.target._unresolved && req.target.name
|
|
204
|
-
? this.model.definitions[req.target.name.replace(/_drafts$/, '')]
|
|
204
|
+
? this.model.definitions[req.target.name] || this.model.definitions[req.target.name.replace(/_drafts$/, '')]
|
|
205
205
|
: req.target
|
|
206
206
|
if (!target || target._unresolved) return
|
|
207
207
|
|
|
@@ -333,6 +333,7 @@ const _checkReferenceIntegrity = (entity, data, req, csn, run) => {
|
|
|
333
333
|
}
|
|
334
334
|
|
|
335
335
|
const _checkIntegrityWrapper = (req, csn, run) => async (data, entity) => {
|
|
336
|
+
if (cds.env.features.lean_draft && entity.name?.endsWith('.drafts')) return
|
|
336
337
|
const errors = await _checkReferenceIntegrity(entity, data, req, csn, run)
|
|
337
338
|
if (errors && errors.length !== 0) for (const err of errors) req.error(err)
|
|
338
339
|
}
|
|
@@ -18,6 +18,7 @@ const _convert = (columns, target, model, alias) => {
|
|
|
18
18
|
if (element) {
|
|
19
19
|
if (element.virtual) {
|
|
20
20
|
col.as = col.as || col.ref[col.ref.length - 1]
|
|
21
|
+
col.cast = { type: element._type }
|
|
21
22
|
delete col.ref
|
|
22
23
|
col.val = (element.default && element.default.val) || null
|
|
23
24
|
}
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
const { timestampToISO } = require('../data-conversion/timestamp')
|
|
2
2
|
const { deepCopyObject } = require('../../common/utils/copy')
|
|
3
|
+
const getError = require('../../common/error')
|
|
3
4
|
|
|
4
5
|
function _arrayWithCount(a, count) {
|
|
5
6
|
const _map = a.map
|
|
@@ -41,8 +42,8 @@ const read = (executeSelectCQN, executeStreamCQN) => (model, dbc, query, req) =>
|
|
|
41
42
|
const isoTs = timestampToISO(timestamp)
|
|
42
43
|
|
|
43
44
|
if (query._streaming) {
|
|
44
|
-
if (!query.SELECT || (query.SELECT &&
|
|
45
|
-
|
|
45
|
+
if (!query.SELECT || (query.SELECT && !query.SELECT.columns)) {
|
|
46
|
+
throw getError(500, 'Invalid SELECT statement for streaming')
|
|
46
47
|
}
|
|
47
48
|
return executeStreamCQN(model, dbc, query, user, locale, isoTs)
|
|
48
49
|
}
|