@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
|
@@ -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
|
|
@@ -93,7 +93,7 @@ class HanaDatabase extends DatabaseService {
|
|
|
93
93
|
_registerBeforeHandlers() {
|
|
94
94
|
this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
|
|
95
95
|
this.before('READ', '*', search) // > has to run before rewrite
|
|
96
|
-
this.before(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', this._rewrite)
|
|
96
|
+
this.before(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', this._rewrite)
|
|
97
97
|
|
|
98
98
|
this.before('READ', '*', localized) // > has to run after rewrite
|
|
99
99
|
this.before('READ', '*', this._virtual)
|
|
@@ -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)
|
|
@@ -61,7 +61,7 @@ function _hdbGetResultForProcedure(rows, args, outParameters) {
|
|
|
61
61
|
// merge table output params into scalar params
|
|
62
62
|
if (args && args.length && outParameters) {
|
|
63
63
|
const params = outParameters.filter(md => !(md.PARAMETER_NAME in rows))
|
|
64
|
-
for (let i = 0; i <
|
|
64
|
+
for (let i = 0; i < params.length; i++) {
|
|
65
65
|
result[params[i].PARAMETER_NAME] = args[i]
|
|
66
66
|
}
|
|
67
67
|
}
|
|
@@ -82,14 +82,14 @@ function _hcGetResultForProcedure(stmt, resultSet, outParameters) {
|
|
|
82
82
|
// merge table output params into scalar params
|
|
83
83
|
const params = Array.isArray(outParameters) && outParameters.filter(md => !(md.PARAMETER_NAME in result))
|
|
84
84
|
if (params && params.length) {
|
|
85
|
-
let i = 0
|
|
86
|
-
|
|
87
|
-
const parameterName = params[i++].PARAMETER_NAME
|
|
85
|
+
for (let i = 0; i < params.length; i++) {
|
|
86
|
+
const parameterName = params[i].PARAMETER_NAME
|
|
88
87
|
result[parameterName] = []
|
|
89
88
|
while (resultSet.next()) {
|
|
90
89
|
result[parameterName].push(resultSet.getValues())
|
|
91
90
|
}
|
|
92
|
-
|
|
91
|
+
resultSet.nextResult()
|
|
92
|
+
}
|
|
93
93
|
}
|
|
94
94
|
return result
|
|
95
95
|
}
|
|
@@ -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
|
|
@@ -73,7 +73,7 @@ module.exports = class SQLiteDatabase extends DatabaseService {
|
|
|
73
73
|
|
|
74
74
|
_registerBeforeHandlers() {
|
|
75
75
|
this.before(['CREATE', 'UPDATE'], '*', this._input) // > has to run before rewrite
|
|
76
|
-
this.before(['CREATE', 'READ', 'UPDATE', 'DELETE'], '*', this._rewrite)
|
|
76
|
+
this.before(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', this._rewrite)
|
|
77
77
|
|
|
78
78
|
if (cds.env.features.lean_draft && cds.db?.kind !== 'better-sqlite')
|
|
79
79
|
this.before('READ', '*', convertDraftAdminPathExpression)
|
|
@@ -2,56 +2,38 @@ const InsertBuilder = require('../../db/sql-builder').InsertBuilder
|
|
|
2
2
|
const getAnnotatedColumns = require('../../db/sql-builder/annotations')
|
|
3
3
|
|
|
4
4
|
class CustomUpsertBuilder extends InsertBuilder {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
this._outputObj = {
|
|
8
|
-
sql: ['INSERT', 'INTO'],
|
|
9
|
-
values: []
|
|
10
|
-
}
|
|
11
|
-
|
|
12
|
-
this._obj = { INSERT: this._obj.UPSERT, _target: this._obj._target }
|
|
5
|
+
annotatedColumns(entityName, csn) {
|
|
6
|
+
const { updateAnnotatedColumns } = getAnnotatedColumns(entityName, csn)
|
|
13
7
|
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
this._columnIndexesToDelete = []
|
|
17
|
-
const annotatedColumns = getAnnotatedColumns(entityName, this._csn)
|
|
18
|
-
// hack: treat update annotations as insert because of sql builder impl
|
|
19
|
-
if (annotatedColumns) {
|
|
20
|
-
annotatedColumns.insertAnnotatedColumns = annotatedColumns.updateAnnotatedColumns
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
if (this._obj.INSERT.columns) {
|
|
24
|
-
this._removeAlreadyExistingInsertAnnotatedColumnsFromMap(annotatedColumns)
|
|
25
|
-
this._columns(annotatedColumns)
|
|
8
|
+
if (updateAnnotatedColumns?.size) {
|
|
9
|
+
this.managedCols = Array.from(updateAnnotatedColumns.keys())
|
|
26
10
|
}
|
|
27
11
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
// if columns not provided get indexes from csn
|
|
31
|
-
this._getAnnotatedColumnIndexes(annotatedColumns)
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
this._values(annotatedColumns)
|
|
35
|
-
} else if (this._obj.INSERT.entries && this._obj.INSERT.entries.length !== 0) {
|
|
36
|
-
this._entries(annotatedColumns)
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const insertSql = this._outputObj.sql.join(' ')
|
|
12
|
+
return { insertAnnotatedColumns: updateAnnotatedColumns }
|
|
13
|
+
}
|
|
40
14
|
|
|
41
|
-
|
|
15
|
+
// REVISIT: We need to copy over the implementation for annotation handling
|
|
16
|
+
build() {
|
|
17
|
+
this._obj = { INSERT: this._obj.UPSERT, _target: this._obj._target }
|
|
18
|
+
super.build()
|
|
19
|
+
const csnKeys =
|
|
20
|
+
(this._obj._target ? this._obj._target.keys : this._csn.definitions[this._obj.INSERT.into].keys) || {}
|
|
42
21
|
const keys = Object.keys(csnKeys).filter(k => !csnKeys[k].isAssociation)
|
|
43
|
-
const conflict = ` ON CONFLICT(${keys}) DO UPDATE SET `
|
|
44
22
|
const updates = []
|
|
45
23
|
const columns = this._obj.INSERT.columns || Object.keys(this._obj.INSERT.entries[0])
|
|
24
|
+
if (this.managedCols) {
|
|
25
|
+
columns.push(...this.managedCols)
|
|
26
|
+
}
|
|
27
|
+
|
|
46
28
|
columns.forEach(col => {
|
|
47
29
|
const col_ = col.replace(/\./g, '_')
|
|
48
30
|
if (!keys.includes(col_)) updates.push(`${col_}=excluded.${col_}`)
|
|
49
31
|
})
|
|
32
|
+
const conflict = updates.length
|
|
33
|
+
? ` ON CONFLICT(${keys}) DO UPDATE SET ` + updates.join(', ')
|
|
34
|
+
: ` ON CONFLICT(${keys}) DO NOTHING`
|
|
50
35
|
|
|
51
|
-
|
|
52
|
-
this._outputObj.sql = insertSql + conflict + updates.join(', ')
|
|
53
|
-
}
|
|
54
|
-
|
|
36
|
+
this._outputObj.sql = this._outputObj.sql + conflict
|
|
55
37
|
return this._outputObj
|
|
56
38
|
}
|
|
57
39
|
}
|
|
@@ -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
|
package/package.json
CHANGED
package/server.js
CHANGED
|
@@ -47,7 +47,6 @@ module.exports = async function cds_server (options) {
|
|
|
47
47
|
if (cds.requires.messaging) await cds.connect.to ('messaging')
|
|
48
48
|
|
|
49
49
|
// serve all services declared in models
|
|
50
|
-
if (cds.requires.middlewares) cds.middlewares.bootstrap(); else if (o.correlate) app.use (o.correlate)
|
|
51
50
|
await cds.serve (o.service,o) .in (app)
|
|
52
51
|
await cds.emit ('served', cds.services) //> hook for listeners
|
|
53
52
|
|
|
@@ -71,7 +70,7 @@ module.exports = async function cds_server (options) {
|
|
|
71
70
|
//
|
|
72
71
|
const defaults = {
|
|
73
72
|
|
|
74
|
-
cors,
|
|
73
|
+
cors,
|
|
75
74
|
|
|
76
75
|
get static() { return cds.env.folders.app }, //> defaults to ./app
|
|
77
76
|
|
|
@@ -100,7 +99,7 @@ const _app_serve = function (endpoint) { return {
|
|
|
100
99
|
}}
|
|
101
100
|
|
|
102
101
|
|
|
103
|
-
function cors (req, res, next) {
|
|
102
|
+
function cors (req, res, next) { // REVISIT: should that move into middlewares?
|
|
104
103
|
const { origin } = req.headers
|
|
105
104
|
if (origin) res.set('access-control-allow-origin', origin)
|
|
106
105
|
if (origin && req.method === 'OPTIONS')
|
|
@@ -108,23 +107,6 @@ function cors (req, res, next) {
|
|
|
108
107
|
next()
|
|
109
108
|
}
|
|
110
109
|
|
|
111
|
-
function correlate (req, res, next) {
|
|
112
|
-
// derive correlation id from req
|
|
113
|
-
const id = req.headers['x-correlation-id'] || req.headers['x-correlationid']
|
|
114
|
-
|| req.headers['x-request-id'] || req.headers['x-vcap-request-id']
|
|
115
|
-
|| cds.utils.uuid()
|
|
116
|
-
// new intermediate cds.context, if necessary
|
|
117
|
-
if (!cds.context) cds.context = { id }
|
|
118
|
-
// guarantee x-correlation-id going forward and set on res
|
|
119
|
-
req.headers['x-correlation-id'] = id
|
|
120
|
-
res.set('X-Correlation-ID', id)
|
|
121
|
-
// guaranteed access to cds.context._.req -> REVISIT
|
|
122
|
-
if (!cds.context._) cds.context._ = {}
|
|
123
|
-
if (!cds.context._.req) cds.context._.req = req
|
|
124
|
-
if (!cds.context._.res) cds.context._.res = res
|
|
125
|
-
next()
|
|
126
|
-
}
|
|
127
|
-
|
|
128
110
|
function express_static (dir) {
|
|
129
111
|
return express.static (path.resolve (cds.root,dir))
|
|
130
112
|
}
|