@sap/cds 6.7.2 → 6.8.2

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 (106) hide show
  1. package/CHANGELOG.md +53 -1
  2. package/README.md +1 -0
  3. package/_i18n/i18n.properties +9 -6
  4. package/_i18n/i18n_ar.properties +6 -6
  5. package/_i18n/i18n_cs.properties +6 -6
  6. package/_i18n/i18n_da.properties +6 -6
  7. package/_i18n/i18n_de.properties +6 -6
  8. package/_i18n/i18n_en.properties +6 -6
  9. package/_i18n/i18n_es.properties +6 -6
  10. package/_i18n/i18n_fi.properties +6 -6
  11. package/_i18n/i18n_fr.properties +6 -6
  12. package/_i18n/i18n_hu.properties +6 -6
  13. package/_i18n/i18n_it.properties +6 -6
  14. package/_i18n/i18n_ja.properties +6 -6
  15. package/_i18n/i18n_ko.properties +6 -6
  16. package/_i18n/i18n_ms.properties +6 -6
  17. package/_i18n/i18n_nl.properties +6 -6
  18. package/_i18n/i18n_no.properties +6 -6
  19. package/_i18n/i18n_pl.properties +6 -6
  20. package/_i18n/i18n_pt.properties +6 -6
  21. package/_i18n/i18n_ro.properties +6 -6
  22. package/_i18n/i18n_ru.properties +6 -6
  23. package/_i18n/i18n_sv.properties +6 -6
  24. package/_i18n/i18n_th.properties +6 -6
  25. package/_i18n/i18n_tr.properties +8 -8
  26. package/_i18n/i18n_zh_CN.properties +3 -3
  27. package/_i18n/i18n_zh_TW.properties +6 -6
  28. package/apis/core.d.ts +30 -31
  29. package/apis/csn.d.ts +1 -1
  30. package/apis/ql.d.ts +69 -39
  31. package/apis/serve.d.ts +4 -3
  32. package/apis/services.d.ts +20 -7
  33. package/bin/build/buildTaskEngine.js +1 -1
  34. package/bin/build/index.js +1 -1
  35. package/bin/build/provider/buildTaskProviderInternal.js +9 -6
  36. package/bin/build/provider/hana/index.js +11 -4
  37. package/bin/build/provider/mtx-extension/index.js +13 -1
  38. package/bin/build/provider/mtx-sidecar/index.js +3 -3
  39. package/bin/build/provider/nodejs/index.js +23 -0
  40. package/bin/plugins.js +2 -1
  41. package/bin/version.js +3 -2
  42. package/common.cds +3 -2
  43. package/lib/auth/index.js +3 -0
  44. package/lib/auth/mocked-users.js +13 -0
  45. package/lib/compile/etc/_localized.js +3 -0
  46. package/lib/compile/for/lean_drafts.js +0 -1
  47. package/lib/core/entities.js +7 -3
  48. package/lib/dbs/cds-deploy.js +36 -12
  49. package/lib/env/cds-env.js +47 -14
  50. package/lib/env/cds-requires.js +16 -7
  51. package/lib/env/defaults.js +2 -2
  52. package/lib/env/schemas/cds-rc.json +1 -8
  53. package/lib/index.js +1 -1
  54. package/lib/ql/STREAM.js +89 -0
  55. package/lib/ql/cds-ql.js +2 -1
  56. package/lib/req/request.js +6 -2
  57. package/lib/req/user.js +1 -1
  58. package/lib/srv/middlewares/index.js +9 -7
  59. package/lib/srv/middlewares/trace.js +6 -5
  60. package/lib/srv/srv-api.js +1 -0
  61. package/lib/utils/cds-utils.js +1 -1
  62. package/lib/utils/tar.js +30 -31
  63. package/libx/_runtime/audit/Service.js +96 -37
  64. package/libx/_runtime/audit/generic/personal/utils.js +26 -13
  65. package/libx/_runtime/audit/utils/v2.js +21 -22
  66. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  67. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  68. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +2 -0
  69. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +2 -3
  70. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +1 -1
  71. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/batch/BatchProcessor.js +2 -0
  72. package/libx/_runtime/cds-services/adapter/odata-v4/utils/handlerUtils.js +2 -1
  73. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +10 -3
  74. package/libx/_runtime/cds-services/services/Service.js +2 -7
  75. package/libx/_runtime/cds-services/services/utils/differ.js +1 -1
  76. package/libx/_runtime/cds-services/util/assert.js +28 -5
  77. package/libx/_runtime/common/aspects/any.js +4 -1
  78. package/libx/_runtime/common/generic/auth/utils.js +30 -41
  79. package/libx/_runtime/common/generic/crud.js +1 -1
  80. package/libx/_runtime/common/i18n/messages.properties +1 -1
  81. package/libx/_runtime/common/utils/generateOnCond.js +18 -22
  82. package/libx/_runtime/db/expand/expandCQNToJoin.js +49 -41
  83. package/libx/_runtime/db/expand/rawToExpanded.js +3 -5
  84. package/libx/_runtime/db/generic/rewrite.js +3 -0
  85. package/libx/_runtime/db/utils/generateAliases.js +1 -1
  86. package/libx/_runtime/fiori/generic/activate.js +1 -1
  87. package/libx/_runtime/fiori/generic/before.js +18 -19
  88. package/libx/_runtime/fiori/generic/prepare.js +1 -1
  89. package/libx/_runtime/fiori/generic/read.js +1 -1
  90. package/libx/_runtime/fiori/lean-draft.js +87 -53
  91. package/libx/_runtime/fiori/utils/handler.js +0 -6
  92. package/libx/_runtime/hana/customBuilder/CustomFunctionBuilder.js +1 -1
  93. package/libx/_runtime/hana/customBuilder/CustomReferenceBuilder.js +2 -1
  94. package/libx/_runtime/hana/customBuilder/CustomSelectBuilder.js +0 -5
  95. package/libx/_runtime/hana/execute.js +18 -11
  96. package/libx/_runtime/hana/pool.js +26 -18
  97. package/libx/_runtime/hana/search2Contains.js +1 -1
  98. package/libx/_runtime/hana/search2cqn4sql.js +26 -18
  99. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +23 -16
  100. package/libx/_runtime/messaging/outbox/utils.js +6 -1
  101. package/libx/_runtime/remote/Service.js +83 -48
  102. package/libx/_runtime/remote/utils/client.js +17 -19
  103. package/libx/_runtime/sqlite/execute.js +2 -0
  104. package/libx/rest/middleware/read.js +2 -1
  105. package/libx/rest/middleware/update.js +1 -1
  106. package/package.json +1 -1
@@ -5,21 +5,7 @@ const { isContainsPredicateSupported, search2Contains } = require('./search2Cont
5
5
  const { addAliasToExpression } = require('../db/utils/generateAliases')
6
6
  const targetAlias = 'Target'
7
7
  const textsAlias = 'Texts'
8
- const _generateKeysWhereCondition = (entity, alias1, alias2) => {
9
- const keys = Object.keys(entity.keys).filter(k => !entity.keys[k].isAssociation && !entity.keys[k].virtual)
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
- }
8
+
23
9
  /**
24
10
  * Computes a CQN expression for a search query.
25
11
  *
@@ -40,11 +26,12 @@ const _generateContainsColumns = (columns, entity) => {
40
26
  const search2cqn4sql = (query, entity, options) => {
41
27
  const cqnSearchPhrase = query.SELECT.search
42
28
  if (!cqnSearchPhrase) return query
43
-
44
29
  const localizedAssociation = entity.associations?.localized
30
+
45
31
  if (localizedAssociation) {
46
32
  let { columns: columns2Search = computeColumnsToBeSearched(query, entity), locale } = options
47
33
  const viewAlias = query.SELECT.from.as ? query.SELECT.from.as : 'LocalizedView'
34
+
48
35
  if (!query.SELECT.from.as) {
49
36
  _addAliasToQuery(query, viewAlias)
50
37
  }
@@ -56,14 +43,17 @@ const search2cqn4sql = (query, entity, options) => {
56
43
 
57
44
  // left outer join the target table with the _texts table (the _texts table contains the translated texts)
58
45
  subQuery.leftJoin(localizedAssociation.target, textsAlias).on(onCondition)
46
+
59
47
  // add condition for equal keys of target table and localized view
60
48
  subQuery.where(_generateKeysWhereCondition(entity, targetAlias, viewAlias))
61
49
  const containsColumns = _generateContainsColumns(columns2Search, entity)
50
+
62
51
  let expression
52
+
63
53
  if (isContainsPredicateSupported(query, entity, columns2Search)) {
64
54
  // generate CQN expression with `CONTAINS` predicate for the columns from the target and text table
65
55
  expression = search2Contains(cqnSearchPhrase, containsColumns)
66
- Object.defineProperty(subQuery, '_$searchUsingContains', { value: true, enumerable: true })
56
+ Object.defineProperty(expression, 'searchUsingContains', { value: true, enumerable: true })
67
57
  } else {
68
58
  expression = searchToLike(cqnSearchPhrase, containsColumns)
69
59
  }
@@ -73,11 +63,29 @@ const search2cqn4sql = (query, entity, options) => {
73
63
  // suppress the localize handler from redirecting the subQuery's target to the localized view
74
64
  Object.defineProperty(subQuery, '_suppressLocalization', { value: true })
75
65
  query.where('exists', subQuery)
76
-
77
66
  return query
78
67
  }
79
68
  }
80
69
 
70
+ const _generateKeysWhereCondition = (entity, alias1, alias2) => {
71
+ const keys = Object.keys(entity.keys).filter(key => !entity.keys[key].isAssociation && !entity.keys[key].virtual)
72
+ const where = []
73
+
74
+ keys.forEach(key => {
75
+ if (where.length > 0) where.push('and')
76
+ where.push({ ref: [alias1, key] }, '=', { ref: [alias2, key] })
77
+ })
78
+
79
+ return where
80
+ }
81
+
82
+ const _generateContainsColumns = (columns, entity) => {
83
+ const columnsTarget = addAliasToExpression(columns, targetAlias)
84
+ const columns2SearchText = columns.filter(col => col.ref && entity.elements[col.ref[col.ref.length - 1]].localized)
85
+ const columnsText = addAliasToExpression(columns2SearchText, textsAlias)
86
+ return [...columnsTarget, ...columnsText]
87
+ }
88
+
81
89
  const _addAliasToQuery = (query, alias) => {
82
90
  const SELECT = query.SELECT
83
91
  SELECT.from.as = alias
@@ -2,8 +2,9 @@ const cds = require('../../cds.js')
2
2
  // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
3
3
  const express = require('express')
4
4
  const getTenantInfo = require('./getTenantInfo.js')
5
- const isSecured = () => cds.requires.auth && cds.requires.auth.credentials
6
- const { getTenant } = require('../../auth/strategies/xssecUtils')
5
+ const isSecured = () => cds.requires.auth && (cds.requires.auth.impl || cds.requires.auth.credentials)
6
+ const _require = require('../../common/utils/require')
7
+ const { UNAUTHORIZED } = require('../../auth/utils')
7
8
 
8
9
  const _isAll = a => a && a.includes('all')
9
10
 
@@ -14,18 +15,25 @@ class EndpointRegistry {
14
15
  this.webhookCallbacks = new Map()
15
16
  this.deployCallbacks = new Map()
16
17
  if (isSecured()) {
17
- const JWTStrategy = require('../../auth/strategies/JWT.js')
18
- // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
19
- const passport = require('passport')
20
- // REVISIT: It's unclear if the credentials from cds.requires.auth need to be used here.
21
- // In principle, user-facing endpoints might differ from messaging ones.
22
- passport.use(new JWTStrategy(cds.requires.auth.credentials))
18
+ if (cds.requires.auth.impl) {
19
+ const impl = _require(cds.resolve(cds.requires.auth.impl))
20
+ paths.forEach(path => cds.app.use(path, impl))
21
+ } else {
22
+ const JWTStrategy = require('../../auth/strategies/JWT.js')
23
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
24
+ const passport = _require('passport')
25
+ // REVISIT: It's unclear if the credentials from cds.requires.auth need to be used here.
26
+ // In principle, user-facing endpoints might differ from messaging ones.
27
+ passport.use(new JWTStrategy(cds.requires.auth.credentials))
28
+ paths.forEach(path => {
29
+ cds.app.use(path, passport.initialize())
30
+ cds.app.use(path, passport.authenticate('JWT', { session: false }))
31
+ })
32
+ }
33
+ // unsuccessful auth doesn't automatically reject!
23
34
  paths.forEach(path => {
24
- cds.app.use(path, passport.initialize())
25
- cds.app.use(path, passport.authenticate('JWT', { session: false }))
26
35
  cds.app.use(path, (req, res, next) => {
27
- // unsuccessful auth doesn't automatically reject!
28
- if (!req.user) return res.status(401).json({ message: 'Unauthorized' })
36
+ if (!req.user) res.status(401).json(UNAUTHORIZED)
29
37
  next()
30
38
  })
31
39
  })
@@ -63,17 +71,16 @@ class EndpointRegistry {
63
71
  try {
64
72
  if (isSecured() && !req.user.is('emcallback')) return res.sendStatus(403)
65
73
  const queueName = req.query.q
66
- const authInfo = req.authInfo
67
74
  const xAddress = req.headers['x-address']
68
75
  const topic = xAddress && xAddress.match(/^topic:(.*)/)[1]
69
76
  const payload = req.body
70
77
  const cb = this.webhookCallbacks.get(queueName)
71
78
  if (!topic || !payload || !queueName || !cb) return res.sendStatus(200)
72
- const tenantId = getTenant(authInfo)
73
- const other = authInfo
79
+ const tenant = req.tenant || req.user.tenant
80
+ const other = tenant
74
81
  ? {
75
82
  _: { req, res }, // For `cds.context.http`
76
- tenant: tenantId
83
+ tenant
77
84
  }
78
85
  : {}
79
86
  if (!cb) return res.sendStatus(200)
@@ -15,6 +15,11 @@ const outboxRunner = new OutboxRunner()
15
15
  const cdsUser = 'cds.internal.user'
16
16
  const messageProcessorRegistered = Symbol('message processor registered')
17
17
 
18
+ const _get100NanosecondTimestampISOString = () => {
19
+ const [now, nanoseconds] = [new Date(), process.hrtime()[1]]
20
+ return now.toISOString().replace('Z', `${nanoseconds}`.padStart(9, '0').substring(3, 7) + 'Z')
21
+ }
22
+
18
23
  const _getMessagesEntity = () => {
19
24
  const messagesDbName = 'cds.outbox.Messages'
20
25
  const messagesEntity = cds.model.definitions[messagesDbName]
@@ -221,7 +226,7 @@ const _createMessage = (name, msg, context) => {
221
226
  const outboxMsg = {
222
227
  ID: cds.utils.uuid(),
223
228
  target: name,
224
- timestamp: new Date().toISOString(), // needs to be different for each emit
229
+ timestamp: _get100NanosecondTimestampISOString(), // needs to be different for each emit
225
230
  msg: JSON.stringify(_msg)
226
231
  }
227
232
  return outboxMsg
@@ -7,6 +7,14 @@ const { getKind, run, getDestination, getAdditionalOptions, getReqOptions } = re
7
7
  const { formatVal } = require('../../odata/utils')
8
8
  const { hasAliasedColumns } = require('./utils/data')
9
9
 
10
+ let _cloudSdkConnectivity
11
+ const cloudSdkConnectivity = () => {
12
+ if (_cloudSdkConnectivity) return _cloudSdkConnectivity
13
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
14
+ _cloudSdkConnectivity = require('@sap-cloud-sdk/connectivity')
15
+ return _cloudSdkConnectivity
16
+ }
17
+
10
18
  const _isSimpleCqnQuery = q => typeof q === 'object' && q !== null && !Array.isArray(q) && Object.keys(q).length > 0
11
19
 
12
20
  const _setHeaders = (defaultHeaders, req) => {
@@ -18,6 +26,7 @@ const _setHeaders = (defaultHeaders, req) => {
18
26
  }, {})
19
27
  )
20
28
  }
29
+
21
30
  const _setCorrectValue = (el, data, params, kind) => {
22
31
  return typeof data[el] === 'object' && kind !== 'odata-v2'
23
32
  ? JSON.stringify(data[el])
@@ -29,6 +38,7 @@ const _setCorrectValue = (el, data, params, kind) => {
29
38
  const _buildPartialUrlFunctions = (url, data, params, kind = 'odata-v4') => {
30
39
  const funcParams = []
31
40
  const queryOptions = []
41
+
32
42
  // REVISIT: take params from params after importer fix (the keys should not be part of params)
33
43
  for (const param in _extractParamsFromData(data, params)) {
34
44
  if (kind === 'odata-v2') {
@@ -38,6 +48,7 @@ const _buildPartialUrlFunctions = (url, data, params, kind = 'odata-v4') => {
38
48
  queryOptions.push(`@${param}=${_setCorrectValue(param, data, params, kind)}`)
39
49
  }
40
50
  }
51
+
41
52
  return kind === 'odata-v2'
42
53
  ? `${url}?${funcParams.join('&')}`
43
54
  : `${url}(${funcParams.join(',')})?${queryOptions.join('&')}`
@@ -52,9 +63,11 @@ const _extractParamsFromData = (data, params = {}) => {
52
63
 
53
64
  const _buildKeys = (req, kind) => {
54
65
  const keys = []
66
+
55
67
  if (req.params && req.params.length > 0) {
56
68
  const p1 = req.params[0]
57
69
  if (typeof p1 !== 'object') return [p1]
70
+
58
71
  for (const key in req.target.keys) {
59
72
  keys.push(`${key}=${formatVal(p1[key], key, req.target, kind)}`)
60
73
  }
@@ -64,6 +77,7 @@ const _buildKeys = (req, kind) => {
64
77
  keys.push(`${key}=${formatVal(req.data[key], key, req.target, kind)}`)
65
78
  }
66
79
  }
80
+
67
81
  return keys
68
82
  }
69
83
 
@@ -89,6 +103,7 @@ const _handleUnboundActionFunction = (srv, def, req, event) => {
89
103
  def &&
90
104
  def.returns &&
91
105
  (def.returns.type === 'cds.LargeBinary' || def.returns.type === 'cds.Binary')
106
+
92
107
  return srv.send({ method: 'POST', path: `/${event}`, data: req.data, _binary: isBinary })
93
108
  }
94
109
 
@@ -112,22 +127,26 @@ const _handleV2ActionFunction = (srv, def, req, event, kind) => {
112
127
  const _handleV2BoundActionFunction = (srv, def, req, event, kind) => {
113
128
  const params = []
114
129
  const data = req.data
130
+
115
131
  // REVISIT: take params from def.params, after importer fix (the keys should not be part of params)
116
132
  for (const param in _extractParamsFromData(req.data, def.params)) {
117
133
  params.push(`${param}=${formatVal(data[param], param, { elements: def.params }, kind)}`)
118
134
  }
135
+
119
136
  const keys = _buildKeys(req, this.kind)
120
137
  if (keys.length === 1 && typeof req.params[0] !== 'object') {
121
138
  params.push(`${Object.keys(req.target.keys)[0]}=${keys[0]}`)
122
139
  } else {
123
140
  params.push(...keys)
124
141
  }
142
+
125
143
  const url = `${`/${event}`}?${params.join('&')}`
126
144
  return _sendV2RequestActionFunction(srv, def, url)
127
145
  }
128
146
 
129
147
  const _addHandlerActionFunction = (srv, def, target) => {
130
148
  const event = def.name.match(/\w*$/)[0]
149
+
131
150
  if (target) {
132
151
  srv.on(event, target, async function (req) {
133
152
  const shortEntityName = req.target.name.replace(`${this.namespace}.`, '')
@@ -135,65 +154,85 @@ const _addHandlerActionFunction = (srv, def, target) => {
135
154
  const url = `/${shortEntityName}(${_buildKeys(req, this.kind).join(',')})/${this.namespace}.${event}`
136
155
  return _handleBoundActionFunction(srv, def, req, url)
137
156
  })
138
- } else {
139
- srv.on(event, async function (req) {
140
- if (this.kind === 'odata-v2') return _handleV2ActionFunction(srv, def, req, event, this.kind)
141
- return _handleUnboundActionFunction(srv, def, req, event)
142
- })
157
+
158
+ return
143
159
  }
144
- }
145
160
 
146
- const _selectOnlyWithAlias = q => {
147
- return q && q.SELECT && !q.SELECT._transitions && q.SELECT.columns && q.SELECT.columns.some(hasAliasedColumns)
161
+ srv.on(event, async function (req) {
162
+ if (this.kind === 'odata-v2') return _handleV2ActionFunction(srv, def, req, event, this.kind)
163
+ return _handleUnboundActionFunction(srv, def, req, event)
164
+ })
148
165
  }
149
166
 
167
+ const _selectOnlyWithAlias = q => q?.SELECT && !q.SELECT._transitions && q.SELECT?.columns?.some(hasAliasedColumns)
168
+
150
169
  const resolvedTargetOfQuery = q => {
151
170
  const transitions = (typeof q === 'object' && (q.SELECT || q.INSERT || q.UPDATE || q.DELETE)._transitions) || []
152
171
  return transitions.length && [transitions.length - 1].target
153
172
  }
173
+
154
174
  let logged
155
175
  let sdkLoggerDisabled
176
+
177
+ const _resolveSelectionStrategy = options => {
178
+ if (typeof options?.selectionStrategy !== 'string') return
179
+ options.selectionStrategy = cloudSdkConnectivity().DestinationSelectionStrategies[options.selectionStrategy]
180
+
181
+ if (typeof options?.selectionStrategy !== 'function') {
182
+ throw new Error(`Unsupported destination selection strategy "${options.selectionStrategy}".`)
183
+ }
184
+ }
185
+
156
186
  class RemoteService extends cds.Service {
157
187
  init() {
158
- if (!this.options.credentials) {
159
- throw new Error(`No credentials configured for "${this.name}".`)
160
- }
188
+ this.kind = getKind(this.options) // TODO: Simplify
161
189
 
162
- this.datasource = this.options.datasource
163
- this.destinationOptions = this.options.destinationOptions
164
- this.destination =
165
- this.options.credentials.destination ||
166
- getDestination((this.definition && this.definition.name) || this.datasource, this.options.credentials)
167
- this.requestTimeout = this.options.credentials.requestTimeout
168
- if (this.requestTimeout == null) this.requestTimeout = 60000
169
- if (cds.env.features.fetch_csrf && !logged) {
170
- // for logging once for all remote services
171
- logged = true
172
- LOG._warn &&
173
- LOG.warn(
174
- 'Configuration option "cds.env.features.fetch_csrf" is deprecated.\n Please use "csrf"/"csrfInBatch" as described in https://cap.cloud.sap/docs/node.js/remote-services'
175
- )
176
- }
190
+ /*
191
+ * set up connectivity stuff if credentials are provided
192
+ * throw error if no credentials are provided and the service has at least one entity or one action/function
193
+ */
194
+ if (this.options.credentials) {
195
+ this.datasource = this.options.datasource
196
+ this.destinationOptions = this.options.destinationOptions
197
+ _resolveSelectionStrategy(this.destinationOptions)
198
+ this.destination =
199
+ this.options.credentials.destination ??
200
+ getDestination(this.definition?.name ?? this.datasource, this.options.credentials)
201
+ this.path = this.options.credentials.path
202
+
203
+ this.requestTimeout = this.options.credentials.requestTimeout
204
+ if (this.requestTimeout == null) this.requestTimeout = 60000
205
+
206
+ // REVISIT: remove cds.env.features.fetch_csrf in next major ^7
207
+ this.csrf = cds.env.features.fetch_csrf ?? this.options.csrf
208
+ this.csrfInBatch = this.options.csrfInBatch
209
+ if (cds.env.features.fetch_csrf && !logged) {
210
+ // for logging once for all remote services
211
+ logged = true
212
+ LOG._warn &&
213
+ LOG.warn(
214
+ 'Configuration option "cds.env.features.fetch_csrf" is deprecated.\n Please use "csrf"/"csrfInBatch" as described in https://cap.cloud.sap/docs/node.js/remote-services'
215
+ )
216
+ }
217
+
218
+ // REVISIT: use cds.log's logger in cloud sdk
177
219
 
178
- // REVISIT: use cds.log's logger in cloud sdk
179
-
180
- // disable sdk logger if not in debug mode
181
- if (!LOG._debug && !sdkLoggerDisabled) {
182
- try {
183
- // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
184
- const sdkUtils = require('@sap-cloud-sdk/util')
185
- sdkUtils.setGlobalLogLevel('error')
186
- // disable sdk logger once
187
- sdkLoggerDisabled = true
188
- } catch (err) {
189
- /* might fail in cds repl due to winston's exception handler, see cap/issues#10134 */
220
+ // disable sdk logger if not in debug mode
221
+ if (!LOG._debug && !sdkLoggerDisabled) {
222
+ try {
223
+ // eslint-disable-next-line cds/no-missing-dependencies -- needs to be added by app dev
224
+ const sdkUtils = require('@sap-cloud-sdk/util')
225
+ sdkUtils.setGlobalLogLevel('error')
226
+
227
+ // disable sdk logger once
228
+ sdkLoggerDisabled = true
229
+ } catch (err) {
230
+ /* might fail in cds repl due to winston's exception handler, see cap/issues#10134 */
231
+ }
190
232
  }
233
+ } else if ([...this.entities].length || [...this.operations].length) {
234
+ throw new Error(`No credentials configured for "${this.name}".`)
191
235
  }
192
- // REVISIT: remove cds.env.features.fetch_csrf in next major ^7
193
- this.csrf = cds.env.features.fetch_csrf || this.options.csrf
194
- this.csrfInBatch = this.options.csrfInBatch
195
- this.path = this.options.credentials.path
196
- this.kind = getKind(this.options) // TODO: Simplify
197
236
 
198
237
  const clearKeysFromData = function (req) {
199
238
  if (req.target && req.target.keys) for (const k of Object.keys(req.target.keys)) delete req.data[k]
@@ -235,10 +274,7 @@ class RemoteService extends cds.Service {
235
274
  }
236
275
 
237
276
  let result = await run(reqOptions, additionalOptions)
238
-
239
- result =
240
- typeof query === 'object' && query.SELECT && query.SELECT.one && Array.isArray(result) ? result[0] : result
241
-
277
+ result = typeof query === 'object' && query.SELECT?.one && Array.isArray(result) ? result[0] : result
242
278
  return result
243
279
  })
244
280
  }
@@ -286,7 +322,6 @@ class RemoteService extends cds.Service {
286
322
  // REVISIT: We need to provide target explicitly because it's cached already within ensure_target
287
323
  const newReq = new cds.Request({ query: q, target: t, headers: req.headers, _resolved: true, method: req.method })
288
324
  const result = await super.dispatch(newReq)
289
-
290
325
  return postProcess(q, result, this, true)
291
326
  }
292
327
 
@@ -27,14 +27,16 @@ const _executeHttpRequest = async ({ requestConfig, destination, destinationOpti
27
27
  const destinationName = typeof destination === 'string' && destination
28
28
 
29
29
  if (destinationName) {
30
- destination = { destinationName, ...(resolveDestinationOptions(destinationOptions, jwt) || {}) }
30
+ destination = { destinationName, ...(resolveDestinationOptions(destinationOptions, jwt) ?? {}) }
31
31
  } else if (destination.forwardAuthToken) {
32
32
  destination = {
33
33
  ...destination,
34
34
  headers: destination.headers ? { ...destination.headers } : {},
35
35
  authentication: 'NoAuthentication'
36
36
  }
37
+
37
38
  delete destination.forwardAuthToken
39
+
38
40
  if (jwt) {
39
41
  destination.headers.authorization = `Bearer ${jwt}`
40
42
  } else {
@@ -93,12 +95,12 @@ const getDestination = (name, credentials) => {
93
95
  * @returns {import('@sap-cloud-sdk/connectivity').DestinationFetchOptions}
94
96
  */
95
97
  const resolveDestinationOptions = function (options, jwt) {
96
- if (!options && !jwt) return undefined
98
+ if (!options && !jwt) return
97
99
 
98
- const resolvedOptions = Object.assign({}, options || {})
100
+ const resolvedOptions = Object.assign({}, options ?? {})
99
101
  resolvedOptions.jwt = jwt
100
102
 
101
- if (options && options.selectionStrategy) {
103
+ if (options?.selectionStrategy) {
102
104
  resolvedOptions.selectionStrategy = options.selectionStrategy
103
105
  }
104
106
 
@@ -304,12 +306,9 @@ const run = async (
304
306
  // > axios received status >= 400 -> gateway error
305
307
  const msg = e?.response?.data?.error?.message?.value ?? e?.response?.data?.error?.message ?? e.message
306
308
  e.message = msg ? 'Error during request to remote service: \n' + msg : 'Request to remote service failed.'
307
-
308
- const sanitizedError = _getSanitizedError(e, requestConfig, {
309
- suppressRemoteResponseBody: suppressRemoteResponseBody
310
- })
311
-
309
+ const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
312
310
  const err = Object.assign(new Error(e.message), { statusCode: 502, reason: sanitizedError })
311
+
313
312
  LOG._warn && LOG.warn(err)
314
313
  throw err
315
314
  }
@@ -327,11 +326,7 @@ const run = async (
327
326
  ) {
328
327
  const e = new Error("Received content-type 'text/html' which is not part of accepted content types")
329
328
  e.response = response
330
-
331
- const sanitizedError = _getSanitizedError(e, requestConfig, {
332
- suppressRemoteResponseBody: suppressRemoteResponseBody
333
- })
334
-
329
+ const sanitizedError = _getSanitizedError(e, requestConfig, { suppressRemoteResponseBody })
335
330
  const err = Object.assign(new Error(`Error during request to remote service: ${e.message}`), {
336
331
  statusCode: 502,
337
332
  reason: sanitizedError
@@ -381,6 +376,7 @@ const run = async (
381
376
  if (response.data.d) {
382
377
  return _purgeODataV2(response.data, resolvedTarget, returnType, requestConfig.headers)
383
378
  }
379
+
384
380
  return _purgeODataV4(response.data)
385
381
  }
386
382
 
@@ -388,12 +384,10 @@ const run = async (
388
384
  }
389
385
 
390
386
  const getJwt = req => {
391
- const headers = req && req.context && req.context.headers
392
- if (headers && headers.authorization) {
387
+ const headers = req?.context?.headers
388
+ if (headers?.authorization) {
393
389
  const token = headers.authorization.match(/^bearer (.+)/i)
394
- if (token) {
395
- return token[1]
396
- }
390
+ if (token) return token[1]
397
391
  }
398
392
 
399
393
  return null
@@ -467,6 +461,10 @@ const getReqOptions = (req, query, service) => {
467
461
  ? _stringToReqOptions(query, req.data, req.target)
468
462
  : _pathToReqOptions(req.method, req.path, req.data, req.target)
469
463
 
464
+ if (service.kind === 'odata-v2' && req.event === 'READ' && reqOptions.url?.match(/(\/any\()|(\/all\()/)) {
465
+ req.reject(501, 'Lambda expressions are not supported in OData v2')
466
+ }
467
+
470
468
  reqOptions.headers = { accept: 'application/json,text/plain' }
471
469
  reqOptions.timeout = service.requestTimeout
472
470
 
@@ -114,8 +114,10 @@ function executeSelectCQN(model, dbc, query, user, locale, txTimestamp) {
114
114
  ) {
115
115
  return expandV2(model, dbc, query, user, locale, txTimestamp, executeSelectCQN)
116
116
  }
117
+
117
118
  return _processExpand(model, dbc, query, user, locale, txTimestamp)
118
119
  }
120
+
119
121
  const { sql, values = [] } = sqlFactory(
120
122
  query,
121
123
  {
@@ -16,13 +16,14 @@ module.exports = async (_req, _res, next) => {
16
16
  result = await srv.dispatch(req)
17
17
 
18
18
  // 204 or 404?
19
- if (result === null && query.SELECT.one) {
19
+ if (result == null && query.SELECT.one) {
20
20
  if (_target.ref.length > 1) status = 204
21
21
  else throw { code: 404 }
22
22
  }
23
23
  } catch (e) {
24
24
  return next(e)
25
25
  }
26
+
26
27
  // compat for mtx returning strings instead of objects
27
28
  if (typeof result === 'object' && result !== null && '$count' in result) {
28
29
  result = {
@@ -9,7 +9,6 @@ const { deepCopyObject } = require('../../_runtime/common/utils/copy')
9
9
 
10
10
  module.exports = async (_req, _res, next) => {
11
11
  let { _srv: srv, _query: query, _target, _data, _params } = _req
12
-
13
12
  let result,
14
13
  status = 200
15
14
 
@@ -19,6 +18,7 @@ module.exports = async (_req, _res, next) => {
19
18
  try {
20
19
  // add the data (as copy, if upsert allowed)
21
20
  query.data(UPSERT_ALLOWED ? deepCopyObject(_data) : _data)
21
+
22
22
  // REVISIT: if PUT, req.method should be PUT -> Crud2Http maps UPSERT to PUT
23
23
  result = await srv.dispatch(new RestRequest({ query, _target, method: _req.method, params: _params }))
24
24
  if (_params && result) Object.assign(result, _params[_params.length - 1])
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sap/cds",
3
- "version": "6.7.2",
3
+ "version": "6.8.2",
4
4
  "description": "SAP Cloud Application Programming Model - CDS for Node.js",
5
5
  "homepage": "https://cap.cloud.sap/",
6
6
  "keywords": [