@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.
Files changed (88) hide show
  1. package/CHANGELOG.md +59 -3
  2. package/apis/cds.d.ts +2 -0
  3. package/apis/cqn.d.ts +14 -3
  4. package/apis/ql.d.ts +12 -8
  5. package/apis/services.d.ts +39 -64
  6. package/apis/test.d.ts +7 -0
  7. package/bin/build/buildTaskEngine.js +9 -12
  8. package/bin/build/buildTaskHandler.js +3 -14
  9. package/bin/build/index.js +8 -2
  10. package/bin/build/provider/buildTaskProviderInternal.js +8 -7
  11. package/bin/build/provider/hana/template/package.json +3 -0
  12. package/bin/build/provider/mtx/resourcesTarBuilder.js +13 -4
  13. package/bin/build/provider/mtx-extension/index.js +41 -38
  14. package/bin/build/util.js +17 -0
  15. package/bin/deploy/to-hana/hdiDeployUtil.js +11 -5
  16. package/bin/serve.js +6 -2
  17. package/common.cds +7 -0
  18. package/lib/auth/index.js +17 -15
  19. package/lib/auth/jwt-auth.js +4 -3
  20. package/lib/compile/for/lean_drafts.js +1 -1
  21. package/lib/compile/minify.js +3 -3
  22. package/lib/core/index.js +1 -0
  23. package/lib/dbs/cds-deploy.js +13 -10
  24. package/lib/env/cds-requires.js +1 -1
  25. package/lib/env/defaults.js +5 -1
  26. package/lib/env/schemas/cds-rc.json +74 -3
  27. package/lib/lazy.js +6 -8
  28. package/lib/log/cds-error.js +2 -2
  29. package/lib/ql/Whereable.js +22 -11
  30. package/lib/ql/cds-ql.js +1 -1
  31. package/lib/req/response.js +8 -3
  32. package/lib/req/user.js +12 -2
  33. package/lib/srv/middlewares/cds-context.js +0 -2
  34. package/lib/srv/middlewares/ctx-auth.js +11 -0
  35. package/lib/srv/middlewares/ctx-model.js +22 -20
  36. package/lib/srv/middlewares/index.js +7 -9
  37. package/lib/srv/protocols/_legacy.js +4 -0
  38. package/lib/srv/protocols/graphql.js +2 -2
  39. package/lib/srv/protocols/index.js +7 -3
  40. package/lib/srv/srv-api.js +1 -0
  41. package/lib/srv/srv-models.js +6 -1
  42. package/lib/utils/cds-utils.js +3 -1
  43. package/lib/utils/data.js +2 -2
  44. package/lib/utils/tar.js +37 -12
  45. package/libx/_runtime/auth/strategies/JWT.js +1 -0
  46. package/libx/_runtime/auth/strategies/ias-auth.js +2 -1
  47. package/libx/_runtime/auth/strategies/mock.js +12 -1
  48. package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
  49. package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
  50. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -2
  51. package/libx/_runtime/cds-services/services/Service.js +3 -0
  52. package/libx/_runtime/cds-services/services/utils/columns.js +35 -36
  53. package/libx/_runtime/common/code-ext/WorkerReq.js +79 -0
  54. package/libx/_runtime/common/code-ext/config.js +13 -0
  55. package/libx/_runtime/common/code-ext/execute.js +106 -0
  56. package/libx/_runtime/common/code-ext/handlers.js +49 -0
  57. package/libx/_runtime/common/code-ext/worker.js +36 -0
  58. package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
  59. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +33 -0
  60. package/libx/_runtime/common/generic/crud.js +5 -1
  61. package/libx/_runtime/common/generic/paging.js +8 -7
  62. package/libx/_runtime/common/i18n/index.js +1 -1
  63. package/libx/_runtime/common/utils/cqn2cqn4sql.js +47 -11
  64. package/libx/_runtime/common/utils/path.js +5 -25
  65. package/libx/_runtime/common/utils/resolveView.js +2 -0
  66. package/libx/_runtime/common/utils/search2cqn4sql.js +13 -9
  67. package/libx/_runtime/db/expand/expandCQNToJoin.js +2 -1
  68. package/libx/_runtime/db/sql-builder/InsertBuilder.js +5 -1
  69. package/libx/_runtime/db/sql-builder/UpsertBuilder.js +9 -32
  70. package/libx/_runtime/db/sql-builder/annotations.js +6 -3
  71. package/libx/_runtime/db/utils/localized.js +1 -1
  72. package/libx/_runtime/fiori/generic/activate.js +4 -0
  73. package/libx/_runtime/fiori/generic/before.js +8 -1
  74. package/libx/_runtime/fiori/generic/edit.js +5 -0
  75. package/libx/_runtime/fiori/generic/read.js +8 -3
  76. package/libx/_runtime/fiori/lean-draft.js +12 -1
  77. package/libx/_runtime/hana/Service.js +1 -1
  78. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
  79. package/libx/_runtime/hana/execute.js +5 -5
  80. package/libx/_runtime/hana/pool.js +1 -1
  81. package/libx/_runtime/hana/search2cqn4sql.js +51 -51
  82. package/libx/_runtime/sqlite/Service.js +1 -1
  83. package/libx/_runtime/sqlite/customBuilder/CustomUpsertBuilder.js +20 -38
  84. package/libx/odata/afterburner.js +6 -3
  85. package/libx/odata/cqn2odata.js +1 -1
  86. package/libx/rest/middleware/parse.js +26 -4
  87. package/package.json +1 -1
  88. 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['@cds.odata.bindingparameter.collection']
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
- xpr.includes('or') ? where.push({ xpr }) : where.push(...xpr)
46
+ if (restrictions.length) restrictions.push('or')
47
+ xpr.includes('or') ? restrictions.push({ xpr }) : restrictions.push(...xpr)
47
48
  } else {
48
- // > restriction inherited from parent via autoexposure
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 < args.length; 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
- do {
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
- } while (resultSet.nextResult())
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
- const onCondition = entity._relations[localizedAssociation.name].join(localizedAssociation.target, entity.name)
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
- // REVISIT this is dirty but works for now
36
- // replace $user_locale placeholder with the user locale or the HANA session context
37
- onCondition[0].xpr[onCondition[0].xpr.length - 1] = { val: locale || "SESSION_CONTEXT('LOCALE')" }
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
- // inner join the target table with the _texts table (the _texts table contains the translated texts)
40
- const localizedEntityName = localizedAssociation.target
41
- query.join(localizedEntityName).on(onCondition)
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
- // prevent SQL ambiguity error for columns with the same name
44
- columns2Search = _addAliasToQuery(query, entity, columns2Search)
71
+ subQuery.where(expression)
45
72
 
46
- // suppress the localize handler from redirecting the query's target to the localized view
47
- Object.defineProperty(query, '_suppressLocalization', { value: true })
48
- } // else --> resolve localized texts via localized view (default)
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
- const useContains = !!localizedAssociation && isContainsPredicateSupported(query, entity, columns2Search)
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
- // The inner join modifies the original SELECT ... FROM query and adds ambiguity,
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
- const localizedEntityName = entity.associations?.localized.target
73
- const elements = entity.elements
74
- const entityName = entity.name
75
- const getEntityName = columnRef => {
76
- const columnName = columnRef[0]
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
- // REVISIT: We need to copy over the implementation for annotation handling
6
- build() {
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
- const entityName = this._into()
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
- if (this._obj.INSERT.values || this._obj.INSERT.rows) {
29
- if (annotatedColumns && !this._obj.INSERT.columns) {
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
- const csnKeys = this._obj._target ? this._obj._target.keys : this._csn.definitions[this._obj.INSERT.into].keys
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
- if (updates.length) {
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 (action['@cds.odata.bindingparameter.collection']) {
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 = `Bound operations are not supported on entity collections.`
291
- throw Object.assign(new Error(msg), { statusCode: 501 })
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
 
@@ -318,7 +318,7 @@ const _parseColumnsV2 = (columns, prefix = []) => {
318
318
  }
319
319
 
320
320
  if (column === '*') {
321
- select.push(encodeURIComponent(`${prefix.join('/')}/*`))
321
+ select.push(encodeURIComponent(prefix.length ? `${prefix.join('/')}/*` : '*'))
322
322
  }
323
323
  }
324
324
 
@@ -40,8 +40,8 @@ module.exports = (req, res, next) => {
40
40
  case 'HEAD':
41
41
  case 'GET':
42
42
  if (operation) {
43
- // function
44
- req._operation = operation = definition.kind === 'function' ? definition : definition.actions[operation]
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
- // action
54
- req._operation = operation = definition.kind === 'action' ? definition : definition.actions[operation]
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "6.4.0",
3
+ "version": "6.5.0",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [
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, correlate,
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
  }