@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
|
@@ -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
|
}
|
|
@@ -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)) {
|
|
@@ -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
|
|
|
@@ -155,6 +155,13 @@ const _validateDraftBoundAction = async function (req) {
|
|
|
155
155
|
if (result && result.length > 0) _validateDraft(req, result, isBoundAction)
|
|
156
156
|
}
|
|
157
157
|
|
|
158
|
+
const _allowEntityCollectionOnAction = action => {
|
|
159
|
+
return (
|
|
160
|
+
action['@cds.odata.bindingparameter.collection'] ||
|
|
161
|
+
(action.params && Object.values(action.params).some(e => e?.items?.type === '$self'))
|
|
162
|
+
)
|
|
163
|
+
}
|
|
164
|
+
|
|
158
165
|
const _registerBoundActionHandlers = function (entityName, actions) {
|
|
159
166
|
if (!actions) return
|
|
160
167
|
|
|
@@ -164,7 +171,7 @@ const _registerBoundActionHandlers = function (entityName, actions) {
|
|
|
164
171
|
action.name !== 'draftPrepare' &&
|
|
165
172
|
action.name !== 'draftEdit' &&
|
|
166
173
|
action.name !== 'draftActivate' &&
|
|
167
|
-
!action
|
|
174
|
+
!_allowEntityCollectionOnAction(action)
|
|
168
175
|
)
|
|
169
176
|
|
|
170
177
|
for (const action of boundActions) {
|
|
@@ -156,6 +156,11 @@ const fioriGenericEdit = async function (req) {
|
|
|
156
156
|
}
|
|
157
157
|
|
|
158
158
|
await Promise.all(insertCQNs.map(CQN => dbtx.run(CQN)))
|
|
159
|
+
|
|
160
|
+
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
161
|
+
// status code must be set in handler to allow overriding for FE V2
|
|
162
|
+
req?._?.odataRes.setStatusCode(201)
|
|
163
|
+
|
|
159
164
|
return results[0][0]
|
|
160
165
|
}
|
|
161
166
|
|
|
@@ -37,15 +37,16 @@ const _findRootSubSelectFor = query => {
|
|
|
37
37
|
|
|
38
38
|
// append where with clauses from @restrict
|
|
39
39
|
const _getWhereWithAppendedDraftRestrictions = (where = [], req) => {
|
|
40
|
+
const restrictions = []
|
|
40
41
|
if (req.query._draftRestrictions) {
|
|
41
42
|
for (const each of req.query._draftRestrictions) {
|
|
42
43
|
const xpr = each._xpr
|
|
43
44
|
if (each.target.name === ensureUnlocalized(req.target.name)) {
|
|
44
|
-
if (where.length) where.push('and')
|
|
45
45
|
// restriction might contain or clause -> use xpr for grouping
|
|
46
|
-
|
|
46
|
+
if (restrictions.length) restrictions.push('or')
|
|
47
|
+
xpr.includes('or') ? restrictions.push({ xpr }) : restrictions.push(...xpr)
|
|
47
48
|
} else {
|
|
48
|
-
//
|
|
49
|
+
// restriction inherited from parent via autoexposure
|
|
49
50
|
// find inner most sub select if available and append restriction to where clause
|
|
50
51
|
const rootSubSelect = _findRootSubSelectFor({ SELECT: { where } })
|
|
51
52
|
if (rootSubSelect && rootSubSelect.SELECT.from) {
|
|
@@ -62,6 +63,10 @@ const _getWhereWithAppendedDraftRestrictions = (where = [], req) => {
|
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
65
|
|
|
66
|
+
if (restrictions.length) {
|
|
67
|
+
if (where.length) where.push('and', { xpr: restrictions })
|
|
68
|
+
else where.push(...restrictions)
|
|
69
|
+
}
|
|
65
70
|
return where
|
|
66
71
|
}
|
|
67
72
|
|
|
@@ -33,7 +33,8 @@ cds.ApplicationService.prototype.handle = function (req) {
|
|
|
33
33
|
// REVISIT: This is a bit hacky -> better way?
|
|
34
34
|
query._target = undefined
|
|
35
35
|
cds.infer(query, this.model.definitions)
|
|
36
|
-
const _req = cds.Request.for(req._)
|
|
36
|
+
const _req = cds.Request.for(req._) // REVISIT: this causes req._.data of WRITE reqs copied to READ reqs
|
|
37
|
+
if (query.SELECT) delete _req.data // which we fix here -> but this is an ugly workaround
|
|
37
38
|
_req.query = query
|
|
38
39
|
_req.event = req.event
|
|
39
40
|
_req.target = query._target
|
|
@@ -575,6 +576,11 @@ async function onDraftEdit(req) {
|
|
|
575
576
|
|
|
576
577
|
const targetDraft = req.target.drafts
|
|
577
578
|
await INSERT.into(targetDraft).entries(res)
|
|
579
|
+
|
|
580
|
+
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
581
|
+
// status code must be set in handler to allow overriding for FE V2
|
|
582
|
+
req?._?.odataRes.setStatusCode(201)
|
|
583
|
+
|
|
578
584
|
return { ...res, IsActiveEntity: false } // REVISIT: Flatten?
|
|
579
585
|
}
|
|
580
586
|
exports.onDraftEdit = onDraftEdit
|
|
@@ -612,6 +618,11 @@ async function onDraftActivate(req) {
|
|
|
612
618
|
}
|
|
613
619
|
const r = new cds.Request({ event, query, data: res })
|
|
614
620
|
const result = await this.dispatch(r)
|
|
621
|
+
|
|
622
|
+
// REVISIT: we need to use okra API here because it must be set in the batched request
|
|
623
|
+
// status code must be set in handler to allow overriding for FE V2
|
|
624
|
+
req?._?.odataRes.setStatusCode(201)
|
|
625
|
+
|
|
615
626
|
return Object.assign(result, { IsActiveEntity: true })
|
|
616
627
|
}
|
|
617
628
|
exports.onDraftActivate = onDraftActivate
|
|
@@ -4,6 +4,11 @@ const LOG = cds.log('hana|db|sql')
|
|
|
4
4
|
const SelectBuilder = require('../../db/sql-builder').SelectBuilder
|
|
5
5
|
|
|
6
6
|
class CustomSelectBuilder extends SelectBuilder {
|
|
7
|
+
constructor(obj, options, csn) {
|
|
8
|
+
super(obj, options, csn)
|
|
9
|
+
// $searchUsingContains property is set in the sub SELECT of the query, optimized search case
|
|
10
|
+
if (this._obj?._$searchUsingContains) this._options.$searchUsingContains = this._obj._$searchUsingContains
|
|
11
|
+
}
|
|
7
12
|
get FunctionBuilder() {
|
|
8
13
|
const FunctionBuilder = require('./CustomFunctionBuilder')
|
|
9
14
|
Object.defineProperty(this, 'FunctionBuilder', { value: FunctionBuilder })
|
|
@@ -28,11 +33,6 @@ class CustomSelectBuilder extends SelectBuilder {
|
|
|
28
33
|
return SelectBuilder
|
|
29
34
|
}
|
|
30
35
|
|
|
31
|
-
getDefaultOptions() {
|
|
32
|
-
const options = { $searchUsingContains: !!this._obj.SELECT._$searchUsingContains }
|
|
33
|
-
return { ...super.getDefaultOptions(), ...options }
|
|
34
|
-
}
|
|
35
|
-
|
|
36
36
|
_val(obj) {
|
|
37
37
|
if (typeof obj.val === 'boolean') return { sql: obj.val ? 'true' : 'false', values: [] }
|
|
38
38
|
return super._val(obj)
|
|
@@ -79,7 +79,7 @@ async function credentials4(tenant, db) {
|
|
|
79
79
|
}
|
|
80
80
|
|
|
81
81
|
if (cds.xt?.serviceManager && !cds.env.requires['cds.xt.DeploymentService']?.['old-instance-manager']) {
|
|
82
|
-
return (await db._instance_manager.get(tenant)).credentials
|
|
82
|
+
return (await db._instance_manager.get(tenant, { disableCache: true })).credentials
|
|
83
83
|
}
|
|
84
84
|
|
|
85
85
|
return new Promise((resolve, reject) => {
|
|
@@ -1,8 +1,25 @@
|
|
|
1
|
+
const cds = require('../cds')
|
|
1
2
|
const { computeColumnsToBeSearched } = require('../cds-services/services/utils/columns')
|
|
2
3
|
const searchToLike = require('../common/utils/searchToLike')
|
|
3
4
|
const { isContainsPredicateSupported, search2Contains } = require('./search2Contains')
|
|
4
5
|
const { addAliasToExpression } = require('../db/utils/generateAliases')
|
|
5
|
-
|
|
6
|
+
const targetAlias = 'Target'
|
|
7
|
+
const textsAlias = 'Texts'
|
|
8
|
+
const _generateKeysWhereCondition = (entity, alias1, alias2) => {
|
|
9
|
+
const keys = Object.keys(entity.keys).filter(k => !entity.keys[k].isAssociation)
|
|
10
|
+
const where = []
|
|
11
|
+
keys.forEach(key => {
|
|
12
|
+
if (where.length > 0) where.push('and')
|
|
13
|
+
where.push({ ref: [alias1, key] }, '=', { ref: [alias2, key] })
|
|
14
|
+
})
|
|
15
|
+
return where
|
|
16
|
+
}
|
|
17
|
+
const _generateContainsColumns = (columns, entity) => {
|
|
18
|
+
const columnsTarget = addAliasToExpression(columns, targetAlias)
|
|
19
|
+
const columns2SearchText = columns.filter(col => col.ref && entity.elements[col.ref[col.ref.length - 1]].localized)
|
|
20
|
+
const columnsText = addAliasToExpression(columns2SearchText, textsAlias)
|
|
21
|
+
return [...columnsTarget, ...columnsText]
|
|
22
|
+
}
|
|
6
23
|
/**
|
|
7
24
|
* Computes a CQN expression for a search query.
|
|
8
25
|
*
|
|
@@ -24,67 +41,50 @@ const search2cqn4sql = (query, entity, options) => {
|
|
|
24
41
|
const cqnSearchPhrase = query.SELECT.search
|
|
25
42
|
if (!cqnSearchPhrase) return query
|
|
26
43
|
|
|
27
|
-
let { columns: columns2Search = computeColumnsToBeSearched(query, entity), locale } = options
|
|
28
44
|
const localizedAssociation = entity.associations?.localized
|
|
29
|
-
|
|
30
|
-
// Resolve localized data at runtime if the localized association is defined for the target entity.
|
|
31
|
-
// Notice that if the localized association is defined, there should be at least one localized element.
|
|
32
45
|
if (localizedAssociation) {
|
|
33
|
-
|
|
46
|
+
let { columns: columns2Search = computeColumnsToBeSearched(query, entity), locale } = options
|
|
47
|
+
const viewAlias = query.SELECT.from.as ? query.SELECT.from.as : 'LocalizedView'
|
|
48
|
+
if (!query.SELECT.from.as) {
|
|
49
|
+
_addAliasToQuery(query, viewAlias)
|
|
50
|
+
}
|
|
34
51
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
onCondition
|
|
52
|
+
const subQuery = cds.ql.SELECT.from(entity.name).columns(1)
|
|
53
|
+
subQuery.SELECT.from.as = targetAlias
|
|
54
|
+
const onCondition = _generateKeysWhereCondition(entity, targetAlias, textsAlias)
|
|
55
|
+
onCondition.push('and', { ref: [textsAlias, 'locale'] }, '=', { val: locale || "SESSION_CONTEXT('LOCALE')" })
|
|
38
56
|
|
|
39
|
-
//
|
|
40
|
-
|
|
41
|
-
|
|
57
|
+
// left outer join the target table with the _texts table (the _texts table contains the translated texts)
|
|
58
|
+
subQuery.leftJoin(localizedAssociation.target, textsAlias).on(onCondition)
|
|
59
|
+
// add condition for equal keys of target table and localized view
|
|
60
|
+
subQuery.where(_generateKeysWhereCondition(entity, targetAlias, viewAlias))
|
|
61
|
+
const containsColumns = _generateContainsColumns(columns2Search, entity)
|
|
62
|
+
let expression
|
|
63
|
+
if (isContainsPredicateSupported(query, entity, columns2Search)) {
|
|
64
|
+
// generate CQN expression with `CONTAINS` predicate for the columns from the target and text table
|
|
65
|
+
expression = search2Contains(cqnSearchPhrase, containsColumns)
|
|
66
|
+
Object.defineProperty(subQuery, '_$searchUsingContains', { value: true, enumerable: true })
|
|
67
|
+
} else {
|
|
68
|
+
expression = searchToLike(cqnSearchPhrase, containsColumns)
|
|
69
|
+
}
|
|
42
70
|
|
|
43
|
-
|
|
44
|
-
columns2Search = _addAliasToQuery(query, entity, columns2Search)
|
|
71
|
+
subQuery.where(expression)
|
|
45
72
|
|
|
46
|
-
// suppress the localize handler from redirecting the
|
|
47
|
-
Object.defineProperty(
|
|
48
|
-
|
|
73
|
+
// suppress the localize handler from redirecting the subQuery's target to the localized view
|
|
74
|
+
Object.defineProperty(subQuery, '_suppressLocalization', { value: true })
|
|
75
|
+
query.where('exists', subQuery)
|
|
49
76
|
|
|
50
|
-
|
|
51
|
-
let expression
|
|
52
|
-
|
|
53
|
-
if (useContains) {
|
|
54
|
-
expression = search2Contains(cqnSearchPhrase, columns2Search)
|
|
55
|
-
Object.defineProperty(query.SELECT, '_$searchUsingContains', { value: true, enumerable: true })
|
|
56
|
-
} else {
|
|
57
|
-
// No CONTAINS optimization possible. The search implementation for localized
|
|
58
|
-
// texts falls back to the LIKE predicate.
|
|
59
|
-
expression = searchToLike(cqnSearchPhrase, columns2Search)
|
|
77
|
+
return query
|
|
60
78
|
}
|
|
61
|
-
|
|
62
|
-
// REVISIT: find out here if where or having must be used
|
|
63
|
-
query._aggregated || /* if new parser */ query.SELECT.groupBy ? query.having(expression) : query.where(expression)
|
|
64
|
-
return query
|
|
65
79
|
}
|
|
66
80
|
|
|
67
|
-
|
|
68
|
-
// therefore add the table/entity name (as a preceding element) to the columns ref
|
|
69
|
-
// to prevent a SQL ambiguity error.
|
|
70
|
-
const _addAliasToQuery = (query, entity, columnsToBeSearched) => {
|
|
81
|
+
const _addAliasToQuery = (query, alias) => {
|
|
71
82
|
const SELECT = query.SELECT
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const localizedElement = !!elements[columnName].localized
|
|
78
|
-
const targetEntityName = localizedElement ? localizedEntityName : entityName
|
|
79
|
-
return targetEntityName
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
SELECT.columns = addAliasToExpression(SELECT.columns, getEntityName)
|
|
83
|
-
columnsToBeSearched = addAliasToExpression(columnsToBeSearched, getEntityName)
|
|
84
|
-
SELECT.groupBy = addAliasToExpression(SELECT.groupBy, getEntityName)
|
|
85
|
-
SELECT.orderBy = addAliasToExpression(SELECT.orderBy, getEntityName)
|
|
86
|
-
SELECT.where = addAliasToExpression(SELECT.where, getEntityName)
|
|
87
|
-
return columnsToBeSearched
|
|
83
|
+
SELECT.from.as = alias
|
|
84
|
+
SELECT.columns = addAliasToExpression(SELECT.columns, alias)
|
|
85
|
+
SELECT.groupBy = addAliasToExpression(SELECT.groupBy, alias)
|
|
86
|
+
SELECT.orderBy = addAliasToExpression(SELECT.orderBy, alias)
|
|
87
|
+
SELECT.where = addAliasToExpression(SELECT.where, alias)
|
|
88
88
|
}
|
|
89
89
|
|
|
90
90
|
module.exports = search2cqn4sql
|
|
@@ -284,11 +284,14 @@ function _processSegments(from, model, namespace) {
|
|
|
284
284
|
incompleteKeys = ref[i].where ? false : i === ref.length - 1 || one ? false : true
|
|
285
285
|
|
|
286
286
|
if (incompleteKeys && action) {
|
|
287
|
-
if (
|
|
287
|
+
if (
|
|
288
|
+
action['@cds.odata.bindingparameter.collection'] ||
|
|
289
|
+
(action.params && Object.values(action.params).some(e => e?.items?.type === '$self'))
|
|
290
|
+
) {
|
|
288
291
|
incompleteKeys = false
|
|
289
292
|
} else {
|
|
290
|
-
const msg = `
|
|
291
|
-
throw Object.assign(new Error(msg), { statusCode:
|
|
293
|
+
const msg = `"${action.name}" must be called on a single instance of "${current.name}".`
|
|
294
|
+
throw Object.assign(new Error(msg), { statusCode: 400 })
|
|
292
295
|
}
|
|
293
296
|
}
|
|
294
297
|
|
package/libx/odata/cqn2odata.js
CHANGED
|
@@ -40,8 +40,8 @@ module.exports = (req, res, next) => {
|
|
|
40
40
|
case 'HEAD':
|
|
41
41
|
case 'GET':
|
|
42
42
|
if (operation) {
|
|
43
|
-
|
|
44
|
-
|
|
43
|
+
req._operation = operation = definition
|
|
44
|
+
if (operation.kind === 'action') cds.error('Action must be called by POST', { code: 400 })
|
|
45
45
|
if (!unbound) query = one ? SELECT.one(_target) : SELECT.from(_target)
|
|
46
46
|
else query = undefined
|
|
47
47
|
} else {
|
|
@@ -50,8 +50,8 @@ module.exports = (req, res, next) => {
|
|
|
50
50
|
break
|
|
51
51
|
case 'POST':
|
|
52
52
|
if (operation) {
|
|
53
|
-
|
|
54
|
-
|
|
53
|
+
req._operation = operation = definition
|
|
54
|
+
if (operation.kind === 'function') cds.error('Function must be called by GET', { code: 400 })
|
|
55
55
|
if (!unbound) query = one ? SELECT.one(_target) : SELECT.from(_target)
|
|
56
56
|
else query = undefined
|
|
57
57
|
} else {
|
|
@@ -62,10 +62,32 @@ module.exports = (req, res, next) => {
|
|
|
62
62
|
break
|
|
63
63
|
case 'PUT':
|
|
64
64
|
case 'PATCH':
|
|
65
|
+
if (operation) {
|
|
66
|
+
let errorMsg
|
|
67
|
+
if (definition) {
|
|
68
|
+
errorMsg = `${definition.kind.charAt(0).toUpperCase() + definition.kind.slice(1)} must be called by ${
|
|
69
|
+
definition.kind === 'action' ? 'POST' : 'GET'
|
|
70
|
+
}`
|
|
71
|
+
} else {
|
|
72
|
+
errorMsg = `That action/function must be called by POST/GET`
|
|
73
|
+
}
|
|
74
|
+
cds.error(errorMsg, { code: 400 })
|
|
75
|
+
}
|
|
65
76
|
if (!one) throw { statusCode: 400, code: '400', message: `INVALID_${req.method}` }
|
|
66
77
|
query = UPDATE(_target)
|
|
67
78
|
break
|
|
68
79
|
case 'DELETE':
|
|
80
|
+
if (operation) {
|
|
81
|
+
let errorMsg
|
|
82
|
+
if (definition) {
|
|
83
|
+
errorMsg = `${definition.kind.charAt(0).toUpperCase() + definition.kind.slice(1)} must be called by ${
|
|
84
|
+
definition.kind === 'action' ? 'POST' : 'GET'
|
|
85
|
+
}`
|
|
86
|
+
} else {
|
|
87
|
+
errorMsg = `That action/function must be called by POST/GET`
|
|
88
|
+
}
|
|
89
|
+
cds.error(errorMsg, { code: 400 })
|
|
90
|
+
}
|
|
69
91
|
if (!one) cds.error('DELETE not allowed on collection', { code: 400 })
|
|
70
92
|
query = DELETE.from(_target)
|
|
71
93
|
break
|