@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.
Files changed (65) hide show
  1. package/CHANGELOG.md +43 -6
  2. package/apis/cqn.d.ts +14 -3
  3. package/apis/ql.d.ts +8 -8
  4. package/apis/services.d.ts +34 -67
  5. package/apis/test.d.ts +7 -0
  6. package/bin/build/buildTaskEngine.js +9 -12
  7. package/bin/build/buildTaskHandler.js +3 -14
  8. package/bin/build/index.js +8 -2
  9. package/bin/build/provider/buildTaskProviderInternal.js +8 -7
  10. package/bin/build/provider/hana/template/package.json +3 -0
  11. package/bin/build/provider/mtx/resourcesTarBuilder.js +12 -3
  12. package/bin/build/provider/mtx-extension/index.js +41 -38
  13. package/bin/build/util.js +17 -0
  14. package/bin/serve.js +6 -2
  15. package/common.cds +7 -0
  16. package/lib/auth/jwt-auth.js +4 -3
  17. package/lib/compile/for/lean_drafts.js +1 -1
  18. package/lib/compile/minify.js +3 -3
  19. package/lib/dbs/cds-deploy.js +13 -10
  20. package/lib/env/cds-requires.js +1 -1
  21. package/lib/env/defaults.js +5 -1
  22. package/lib/env/schemas/cds-rc.json +74 -3
  23. package/lib/lazy.js +6 -8
  24. package/lib/log/cds-error.js +2 -2
  25. package/lib/ql/Whereable.js +22 -11
  26. package/lib/ql/cds-ql.js +1 -1
  27. package/lib/req/response.js +8 -3
  28. package/lib/req/user.js +12 -2
  29. package/lib/srv/srv-models.js +6 -1
  30. package/lib/utils/cds-utils.js +3 -1
  31. package/lib/utils/tar.js +6 -3
  32. package/libx/_runtime/auth/strategies/JWT.js +1 -0
  33. package/libx/_runtime/auth/strategies/ias-auth.js +2 -1
  34. package/libx/_runtime/auth/strategies/mock.js +12 -1
  35. package/libx/_runtime/auth/strategies/xssecUtils.js +7 -8
  36. package/libx/_runtime/auth/strategies/xsuaa.js +1 -0
  37. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -2
  38. package/libx/_runtime/cds-services/services/Service.js +3 -0
  39. package/libx/_runtime/cds-services/services/utils/columns.js +35 -36
  40. package/libx/_runtime/common/code-ext/WorkerReq.js +79 -0
  41. package/libx/_runtime/common/code-ext/config.js +13 -0
  42. package/libx/_runtime/common/code-ext/execute.js +106 -0
  43. package/libx/_runtime/common/code-ext/handlers.js +49 -0
  44. package/libx/_runtime/common/code-ext/worker.js +36 -0
  45. package/libx/_runtime/common/code-ext/workerQuery.js +45 -0
  46. package/libx/_runtime/common/code-ext/workerQueryExecutor.js +33 -0
  47. package/libx/_runtime/common/generic/crud.js +4 -0
  48. package/libx/_runtime/common/i18n/index.js +1 -1
  49. package/libx/_runtime/common/utils/cqn2cqn4sql.js +16 -11
  50. package/libx/_runtime/common/utils/path.js +5 -25
  51. package/libx/_runtime/common/utils/search2cqn4sql.js +13 -9
  52. package/libx/_runtime/db/expand/expandCQNToJoin.js +2 -1
  53. package/libx/_runtime/db/utils/localized.js +1 -1
  54. package/libx/_runtime/fiori/generic/activate.js +4 -0
  55. package/libx/_runtime/fiori/generic/before.js +8 -1
  56. package/libx/_runtime/fiori/generic/edit.js +5 -0
  57. package/libx/_runtime/fiori/generic/read.js +8 -3
  58. package/libx/_runtime/fiori/lean-draft.js +12 -1
  59. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +5 -5
  60. package/libx/_runtime/hana/pool.js +1 -1
  61. package/libx/_runtime/hana/search2cqn4sql.js +51 -51
  62. package/libx/odata/afterburner.js +6 -3
  63. package/libx/odata/cqn2odata.js +1 -1
  64. package/libx/rest/middleware/parse.js +26 -4
  65. 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
- path = typeof path === 'string' ? cds.parse.path(path) : path
24
- const segments = [...path.ref]
25
- while (segments.length) {
26
- let segment = segments.shift()
27
- if (segment.id && typeof segment.id === 'string') {
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 columns = computeColumnsToBeSearched(query, entity, alias)
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 (typeof search2cqn4sql === 'function' && !query.SELECT.count) {
24
- const search2cqnOptions = { columns, locale: options.locale }
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
- const expression = searchToLike(cqnSearchPhrase, columns)
29
-
30
- // REVISIT: find out here if where or having must be used
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 = Object.assign({}, cqn.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)) {
@@ -11,7 +11,7 @@ const _redirectXpr = (xpr, localize) => {
11
11
  }
12
12
 
13
13
  if (ele.SELECT) {
14
- redirect(ele.SELECT, localize)
14
+ if (!ele._suppressLocalization) redirect(ele.SELECT, localize)
15
15
  }
16
16
  })
17
17
  }
@@ -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['@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
@@ -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
- 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
@@ -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.1",
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": [