@sap/cds 7.9.3 → 8.0.3

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 (278) hide show
  1. package/CHANGELOG.md +126 -3655
  2. package/_i18n/i18n_en_US_saptrc.properties +113 -0
  3. package/_i18n/i18n_zh_CN.properties +7 -4
  4. package/app/index.css +129 -0
  5. package/app/index.html +16 -64
  6. package/app/index.js +14 -9
  7. package/bin/args.js +34 -0
  8. package/bin/serve.js +18 -24
  9. package/bin/test.js +97 -0
  10. package/common.cds +5 -12
  11. package/eslint.config.mjs +133 -0
  12. package/lib/auth/basic-auth.js +16 -20
  13. package/lib/auth/dummy-auth.js +1 -1
  14. package/lib/auth/ias-auth.js +9 -41
  15. package/lib/auth/index.js +1 -14
  16. package/lib/auth/jwt-auth.js +10 -40
  17. package/lib/compile/cds-compile.js +1 -2
  18. package/lib/compile/cdsc.js +21 -26
  19. package/lib/compile/etc/_localized.js +1 -6
  20. package/lib/compile/etc/csv.js +1 -1
  21. package/lib/compile/etc/properties.js +1 -1
  22. package/lib/compile/for/java.js +1 -1
  23. package/lib/compile/for/lean_drafts.js +4 -6
  24. package/lib/compile/for/nodejs.js +1 -1
  25. package/lib/compile/parse.js +4 -0
  26. package/lib/compile/resolve.js +4 -4
  27. package/lib/compile/to/edm-files.js +16 -23
  28. package/lib/compile/to/hana.js +27 -0
  29. package/lib/compile/to/json.js +1 -1
  30. package/lib/compile/to/sql.js +5 -1
  31. package/lib/compile/to/yaml.js +3 -3
  32. package/lib/dbs/cds-deploy.js +4 -2
  33. package/lib/env/cds-env.js +10 -14
  34. package/lib/env/cds-requires.js +29 -13
  35. package/lib/env/defaults.js +46 -16
  36. package/lib/env/plugins.js +1 -1
  37. package/lib/env/schemas/cds-rc.js +8 -4
  38. package/lib/env/schemas/index.js +7 -7
  39. package/lib/env/serviceBindings.js +1 -1
  40. package/lib/index.js +12 -10
  41. package/lib/lazy.js +1 -1
  42. package/lib/linked/classes.js +36 -8
  43. package/lib/linked/entities.js +2 -10
  44. package/lib/linked/models.js +2 -1
  45. package/lib/linked/validate.js +292 -0
  46. package/lib/log/cds-error.js +0 -6
  47. package/lib/log/cds-log.js +3 -3
  48. package/lib/log/format/json.js +1 -1
  49. package/lib/log/service/index.js +0 -1
  50. package/lib/plugins.js +2 -2
  51. package/lib/ql/Query.js +2 -10
  52. package/lib/ql/SELECT.js +1 -1
  53. package/lib/ql/Whereable.js +3 -2
  54. package/lib/req/cds-context.js +14 -25
  55. package/lib/req/context.js +23 -25
  56. package/lib/req/request.js +1 -34
  57. package/lib/req/user.js +47 -35
  58. package/lib/srv/bindings.js +1 -1
  59. package/lib/srv/cds-connect.js +4 -4
  60. package/lib/srv/cds-serve.js +2 -2
  61. package/lib/srv/factory.js +1 -1
  62. package/lib/srv/middlewares/cds-context.js +11 -22
  63. package/lib/srv/middlewares/ctx-model.js +2 -3
  64. package/lib/srv/middlewares/errors.js +41 -8
  65. package/lib/srv/middlewares/index.js +3 -3
  66. package/lib/srv/middlewares/trace.js +0 -2
  67. package/lib/srv/protocols/hcql.js +15 -10
  68. package/lib/srv/protocols/http.js +44 -49
  69. package/lib/srv/protocols/index.js +1 -23
  70. package/lib/srv/protocols/odata-v4.js +12 -74
  71. package/lib/srv/protocols/rest.js +1 -13
  72. package/lib/srv/srv-api.js +0 -20
  73. package/lib/srv/srv-dispatch.js +3 -2
  74. package/lib/srv/srv-handlers.js +22 -11
  75. package/lib/srv/srv-methods.js +2 -2
  76. package/lib/srv/srv-models.js +3 -36
  77. package/lib/test/expect.js +343 -0
  78. package/lib/test/index.js +2 -0
  79. package/lib/test/reporter.js +176 -0
  80. package/lib/utils/axios.js +10 -9
  81. package/lib/utils/cds-test.js +85 -36
  82. package/lib/utils/cds-utils.js +54 -7
  83. package/lib/utils/check-version.js +0 -4
  84. package/lib/utils/colors.js +49 -0
  85. package/lib/utils/data.js +5 -4
  86. package/libx/_runtime/cds-services/adapter/odata-v4/OData.js +2 -7
  87. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +3 -30
  88. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +6 -12
  89. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +1 -3
  90. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +0 -1
  91. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -7
  92. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +12 -6
  93. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +2 -4
  94. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +1 -0
  95. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  96. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +0 -1
  97. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +1 -3
  98. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +1 -1
  99. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/edm/AbstractEdmStructuredType.js +1 -2
  100. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/deserializer/ResourceJsonDeserializer.js +5 -0
  101. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/serializer/ContextURLFactory.js +1 -1
  102. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +9 -43
  103. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +0 -1
  104. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +8 -3
  105. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +4 -2
  106. package/libx/_runtime/cds-services/adapter/odata-v4/utils/result.js +1 -3
  107. package/libx/_runtime/cds-services/util/assert.js +1 -1
  108. package/libx/_runtime/cds.js +10 -3
  109. package/libx/_runtime/common/Service.js +12 -32
  110. package/libx/_runtime/common/aspects/any.js +1 -0
  111. package/libx/_runtime/common/code-ext/execute.js +1 -1
  112. package/libx/_runtime/common/code-ext/worker.js +0 -1
  113. package/libx/_runtime/common/composition/data.js +0 -1
  114. package/libx/_runtime/common/composition/delete.js +0 -1
  115. package/libx/_runtime/common/composition/insert.js +2 -2
  116. package/libx/_runtime/common/composition/tree.js +0 -1
  117. package/libx/_runtime/common/composition/update.js +3 -3
  118. package/libx/_runtime/common/error/frontend.js +21 -12
  119. package/libx/_runtime/common/error/log.js +36 -0
  120. package/libx/_runtime/common/error/utils.js +2 -5
  121. package/libx/_runtime/common/generic/auth/autoexpose.js +18 -17
  122. package/libx/_runtime/common/generic/auth/expand.js +1 -1
  123. package/libx/_runtime/common/generic/auth/readOnly.js +1 -2
  124. package/libx/_runtime/common/generic/auth/restrict.js +23 -42
  125. package/libx/_runtime/common/generic/auth/restrictions.js +2 -7
  126. package/libx/_runtime/common/generic/auth/utils.js +91 -88
  127. package/libx/_runtime/common/generic/crud.js +6 -5
  128. package/libx/_runtime/common/generic/etag.js +7 -12
  129. package/libx/_runtime/common/generic/input.js +70 -68
  130. package/libx/_runtime/common/generic/paging.js +1 -0
  131. package/libx/_runtime/common/generic/sorting.js +1 -0
  132. package/libx/_runtime/common/generic/temporal.js +8 -2
  133. package/libx/_runtime/common/i18n/index.js +1 -1
  134. package/libx/_runtime/common/i18n/messages.properties +3 -1
  135. package/libx/_runtime/common/utils/binary.js +8 -2
  136. package/libx/_runtime/common/utils/compareJson.js +5 -1
  137. package/libx/_runtime/common/utils/copy.js +6 -11
  138. package/libx/_runtime/common/utils/cqn2cqn4sql.js +16 -14
  139. package/libx/_runtime/common/utils/differ.js +3 -6
  140. package/libx/_runtime/common/utils/keys.js +77 -18
  141. package/libx/_runtime/common/utils/postProcess.js +12 -15
  142. package/libx/_runtime/common/utils/propagateForeignKeys.js +0 -1
  143. package/libx/_runtime/common/utils/resolveView.js +2 -3
  144. package/libx/_runtime/common/utils/restrictions.js +45 -17
  145. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -8
  146. package/libx/_runtime/common/utils/stream.js +3 -16
  147. package/libx/_runtime/common/utils/streamProp.js +8 -18
  148. package/libx/_runtime/common/utils/structured.js +1 -1
  149. package/libx/_runtime/common/utils/ucsn.js +0 -2
  150. package/libx/_runtime/db/Service.js +0 -72
  151. package/libx/_runtime/db/data-conversion/post-processing.js +0 -1
  152. package/libx/_runtime/db/expand/expandCQNToJoin.js +9 -9
  153. package/libx/_runtime/db/expand/rawToExpanded.js +0 -8
  154. package/libx/_runtime/db/generic/input.js +3 -8
  155. package/libx/_runtime/db/generic/rewrite.js +1 -0
  156. package/libx/_runtime/db/query/read.js +2 -2
  157. package/libx/_runtime/db/sql-builder/ExpressionBuilder.js +0 -1
  158. package/libx/_runtime/db/sql-builder/InsertBuilder.js +1 -1
  159. package/libx/_runtime/db/utils/columns.js +2 -6
  160. package/libx/_runtime/fiori/lean-draft.js +138 -56
  161. package/libx/_runtime/hana/Service.js +0 -1
  162. package/libx/_runtime/hana/driver.js +1 -1
  163. package/libx/_runtime/hana/dynatrace.js +1 -2
  164. package/libx/_runtime/hana/pool.js +11 -21
  165. package/libx/_runtime/hana/streaming.js +0 -1
  166. package/libx/_runtime/messaging/common-utils/AMQPClient.js +0 -1
  167. package/libx/_runtime/messaging/common-utils/authorizedRequest.js +1 -1
  168. package/libx/_runtime/messaging/common-utils/normalizeIncomingMessage.js +1 -1
  169. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +1 -1
  170. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +19 -33
  171. package/libx/_runtime/messaging/event-broker.js +0 -12
  172. package/libx/_runtime/messaging/file-based.js +3 -3
  173. package/libx/_runtime/messaging/http-utils/token.js +1 -1
  174. package/libx/_runtime/messaging/kafka.js +2 -2
  175. package/libx/_runtime/messaging/redis-messaging.js +0 -1
  176. package/libx/_runtime/remote/Service.js +25 -25
  177. package/libx/_runtime/remote/utils/client.js +4 -5
  178. package/libx/_runtime/remote/utils/cloudSdkProvider.js +0 -3
  179. package/libx/_runtime/remote/utils/data.js +0 -1
  180. package/libx/_runtime/sqlite/Service.js +1 -2
  181. package/libx/_runtime/ucl/Service.js +37 -78
  182. package/libx/common/assert/index.js +22 -21
  183. package/libx/common/assert/type-relaxed.js +39 -0
  184. package/libx/common/assert/utils.js +3 -2
  185. package/libx/common/assert/validation.js +3 -8
  186. package/libx/common/utils/index.js +5 -0
  187. package/libx/common/utils/path.js +51 -0
  188. package/libx/odata/ODataAdapter.js +126 -0
  189. package/libx/odata/index.js +15 -2
  190. package/libx/odata/middleware/batch.js +261 -72
  191. package/libx/odata/middleware/body-parser.js +33 -0
  192. package/libx/odata/middleware/create.js +44 -59
  193. package/libx/odata/middleware/delete.js +23 -12
  194. package/libx/odata/middleware/error.js +30 -6
  195. package/libx/odata/middleware/metadata.js +38 -26
  196. package/libx/odata/middleware/operation.js +93 -69
  197. package/libx/odata/middleware/parse.js +6 -8
  198. package/libx/odata/middleware/read.js +117 -93
  199. package/libx/odata/middleware/service-document.js +22 -19
  200. package/libx/odata/middleware/stream.js +54 -56
  201. package/libx/odata/middleware/update.js +79 -87
  202. package/libx/odata/parse/afterburner.js +191 -175
  203. package/libx/odata/parse/cqn2odata.js +8 -8
  204. package/libx/odata/parse/grammar.peggy +27 -20
  205. package/libx/odata/parse/multipartToJson.js +17 -9
  206. package/libx/odata/parse/parser.js +1 -1
  207. package/libx/odata/utils/etag.js +14 -6
  208. package/libx/odata/utils/index.js +84 -12
  209. package/libx/odata/utils/metadata.js +161 -0
  210. package/libx/odata/utils/postProcess.js +89 -0
  211. package/libx/odata/utils/readAfterWrite.js +134 -17
  212. package/libx/odata/utils/result.js +36 -142
  213. package/libx/outbox/index.js +4 -3
  214. package/libx/rest/RestAdapter.js +115 -182
  215. package/libx/rest/middleware/create.js +28 -24
  216. package/libx/rest/middleware/delete.js +7 -10
  217. package/libx/rest/middleware/error.js +19 -16
  218. package/libx/rest/middleware/operation.js +48 -41
  219. package/libx/rest/middleware/parse.js +128 -126
  220. package/libx/rest/middleware/read.js +20 -27
  221. package/libx/rest/middleware/update.js +26 -31
  222. package/package.json +17 -8
  223. package/server.js +4 -2
  224. package/tasks/enterprise-messaging-deploy.js +1 -1
  225. package/apis/cds.d.ts +0 -3
  226. package/apis/core.d.ts +0 -21
  227. package/apis/cqn.d.ts +0 -18
  228. package/apis/csn.d.ts +0 -21
  229. package/apis/events.d.ts +0 -18
  230. package/apis/internal/inference.d.ts +0 -18
  231. package/apis/linked.d.ts +0 -18
  232. package/apis/log.d.ts +0 -20
  233. package/apis/models.d.ts +0 -18
  234. package/apis/ql.d.ts +0 -18
  235. package/apis/reflect.d.ts +0 -32
  236. package/apis/server.d.ts +0 -18
  237. package/apis/services.d.ts +0 -22
  238. package/bin/cds-serve.js +0 -56
  239. package/lib/compile/to/gql.js +0 -15
  240. package/lib/srv/protocols/_legacy.js +0 -44
  241. package/lib/utils/jest.js +0 -43
  242. package/libx/_runtime/auth/index.js +0 -193
  243. package/libx/_runtime/auth/strategies/JWT.js +0 -37
  244. package/libx/_runtime/auth/strategies/basic.js +0 -20
  245. package/libx/_runtime/auth/strategies/dummy.js +0 -14
  246. package/libx/_runtime/auth/strategies/ias-auth.js +0 -1
  247. package/libx/_runtime/auth/strategies/mock.js +0 -77
  248. package/libx/_runtime/auth/strategies/xssecUtils.js +0 -93
  249. package/libx/_runtime/auth/strategies/xsuaa.js +0 -38
  250. package/libx/_runtime/common/perf/index.js +0 -19
  251. package/libx/_runtime/common/utils/ensureIEEE754.js +0 -29
  252. package/libx/_runtime/fiori/draft.js +0 -2
  253. package/libx/_runtime/fiori/generic/activate.js +0 -190
  254. package/libx/_runtime/fiori/generic/before.js +0 -201
  255. package/libx/_runtime/fiori/generic/cancel.js +0 -19
  256. package/libx/_runtime/fiori/generic/delete.js +0 -21
  257. package/libx/_runtime/fiori/generic/edit.js +0 -157
  258. package/libx/_runtime/fiori/generic/index.js +0 -25
  259. package/libx/_runtime/fiori/generic/new.js +0 -82
  260. package/libx/_runtime/fiori/generic/patch.js +0 -101
  261. package/libx/_runtime/fiori/generic/prepare.js +0 -57
  262. package/libx/_runtime/fiori/generic/read.js +0 -1340
  263. package/libx/_runtime/fiori/generic/readOverDraft.js +0 -146
  264. package/libx/_runtime/fiori/utils/csn.js +0 -13
  265. package/libx/_runtime/fiori/utils/delete.js +0 -114
  266. package/libx/_runtime/fiori/utils/handler.js +0 -264
  267. package/libx/_runtime/fiori/utils/lockInfo.js +0 -27
  268. package/libx/_runtime/fiori/utils/req.js +0 -23
  269. package/libx/_runtime/fiori/utils/stream.js +0 -36
  270. package/libx/_runtime/fiori/utils/where.js +0 -254
  271. package/libx/_runtime/index.js +0 -22
  272. package/libx/odata/utils/handler.js +0 -120
  273. package/libx/odata/utils/metaInfo.js +0 -410
  274. package/libx/odata/utils/path.js +0 -75
  275. package/libx/rest/RestRequest.js +0 -32
  276. package/libx/rest/index.js +0 -3
  277. package/libx/rest/readme.md +0 -1
  278. /package/libx/common/assert/{type.js → type-strict.js} +0 -0
@@ -64,7 +64,6 @@ const _createSubElement = (element, definitions) => {
64
64
  return subObject
65
65
  }
66
66
 
67
- // eslint-disable-next-line complexity
68
67
  const _getCompositionTreeRec = ({
69
68
  rootEntityName,
70
69
  definitions,
@@ -6,7 +6,7 @@ const { getDeepInsertCQNs } = require('./insert')
6
6
  const { getDeepDeleteCQNs } = require('./delete')
7
7
  const ctUtils = require('./utils')
8
8
  const { ensureNoDraftsSuffix } = require('../utils/draft')
9
- const { deepCopyObject } = require('../utils/copy')
9
+ const { deepCopy } = require('../utils/copy')
10
10
  const getError = require('../../common/error')
11
11
  const { getEntityNameFromUpdateCQN } = require('../utils/cqn')
12
12
 
@@ -310,8 +310,8 @@ const getDeepUpdateCQNs = async (model, req, selectData) => {
310
310
  const from = getEntityNameFromUpdateCQN(query)
311
311
  const entityName = ensureNoDraftsSuffix(from)
312
312
  const draft = entityName !== from
313
- const data = query.UPDATE.data ? deepCopyObject(query.UPDATE.data) : {}
314
- const withObj = query.UPDATE.with ? deepCopyObject(query.UPDATE.with) : {}
313
+ const data = query.UPDATE.data ? deepCopy(query.UPDATE.data) : {}
314
+ const withObj = query.UPDATE.with ? deepCopy(query.UPDATE.with) : {}
315
315
  const entity = model.definitions[entityName]
316
316
  const entry = Object.assign({}, data, withObj, ctUtils.key(entity, selectData[0]))
317
317
  const compositionTree = getCompositionTree({
@@ -1,3 +1,4 @@
1
+ // REVISIT: Requires thorough cleanup and refactoring, or elimination
1
2
  /*
2
3
  * OData spec:
3
4
  * This object MUST contain name/value pairs with the names code and message,
@@ -24,17 +25,15 @@ const ADDITIONAL_MSG_PROPERTIES = Object.keys(ADDITIONAL_MSG_PROPERTIES_MAP)
24
25
 
25
26
  const _getFiltered = err => {
26
27
  const error = {}
27
-
28
- Object.keys(err)
29
- .concat(['message'])
30
- .forEach(k => {
31
- if (k in ALLOWED_PROPERTIES_MAP || k.startsWith('@')) {
32
- error[k] = err[k]
33
- } else if (k === 'numericSeverity') {
34
- error['@Common.numericSeverity'] = err[k]
35
- }
36
- })
37
-
28
+ for (let k in err) {
29
+ // IMPORTANT: do not use Object.keys(err) here as that ignores enumerable properties from __proto__!
30
+ if (k in ALLOWED_PROPERTIES_MAP || k.startsWith('@')) {
31
+ error[k] = err[k]
32
+ } else if (k === 'numericSeverity') {
33
+ error['@Common.numericSeverity'] = err[k]
34
+ }
35
+ }
36
+ if ('message' in err) error.message = err.message
38
37
  return error
39
38
  }
40
39
 
@@ -173,8 +172,18 @@ const isClientError = e => {
173
172
  return numericCode >= 400 && numericCode < 500
174
173
  }
175
174
 
175
+ const unwrapMultipleErrors = error => {
176
+ // According to the Fiori Elements Failed Message specification, the format must be:
177
+ // Root level: First error, Details: Other errors
178
+ const [firstDetail, ...restDetails] = error.details
179
+ Object.assign(error, firstDetail)
180
+ if (restDetails.length) error.details = restDetails
181
+ else delete error.details
182
+ }
183
+
176
184
  module.exports = {
177
185
  normalizeError,
178
186
  getSapMessages,
179
- isClientError
187
+ isClientError,
188
+ unwrapMultipleErrors
180
189
  }
@@ -0,0 +1,36 @@
1
+ // REVISIT: I think we can remove this file. Log output doesn't need localization.
2
+ const cds = require('../../cds')
3
+ const LOG = cds.log()
4
+
5
+ let _i18n
6
+ const i18n = (...args) => {
7
+ if (!_i18n) _i18n = require('../i18n')
8
+ return _i18n(...args)
9
+ }
10
+
11
+ const { isClientError } = require('./frontend')
12
+
13
+ module.exports = err => {
14
+ // REVISIT: how does level behave compared to _log in (legacy) odata adapter?
15
+ const level = isClientError(err) ? 'warn' : 'error'
16
+ if ((level === 'warn' && !LOG._warn) || (level === 'error' && !LOG._error)) return
17
+
18
+ // replace messages in toLog with developer texts (i.e., undefined locale)
19
+ const _message = err.message
20
+ const _details = err.details
21
+ err.message = i18n(err.message || err.code, undefined, err.args) || err.message
22
+ if (err.details) {
23
+ const details = []
24
+ for (const d of err.details) {
25
+ details.push(Object.assign({}, d, { message: i18n(d.message || d.code, undefined, d.args) || d.message }))
26
+ }
27
+ err.details = details
28
+ }
29
+
30
+ // log it
31
+ LOG[level](err)
32
+
33
+ // restore
34
+ err.message = _message
35
+ if (_details) err.details = _details
36
+ }
@@ -12,11 +12,8 @@ const i18n = (...args) => {
12
12
  * @returns localized error message
13
13
  */
14
14
  function getErrorMessage(error, locale) {
15
- return (
16
- i18n(error.message || error.code || error.status || error.statusCode, locale, error.args) ||
17
- error.message ||
18
- `${error.code || error.status || error.statusCode}`
19
- )
15
+ const txt = i18n(error.message || error.code || error.status || error.statusCode, locale, error.args)
16
+ return txt || error.message || String(error.code || error.status || error.statusCode)
20
17
  }
21
18
 
22
19
  module.exports = {
@@ -1,25 +1,26 @@
1
1
  const cds = require('../../../../../lib')
2
2
 
3
3
  function noah_handler(req) {
4
- const target = req.target
5
- if (!target) return
6
-
7
- // direct changes to draft administrative data is forbidden
8
- if (target.name.match(/\.DraftAdministrativeData$/) && req.event !== 'READ') {
9
- req.reject(405, 'ENTITY_IS_AUTOEXPOSE_READONLY', [target.name])
4
+ if (!req.subject) return
5
+ const root = this.model.definitions[req.subject.ref[0].id || req.subject.ref[0]]
6
+ if (!root) return
7
+
8
+ /*
9
+ * For auto-exposed Compositions all direct CRUD requests are rejected in non-draft case.
10
+ * For other auto-exposed entities in non-draft case only C_UD are rejected. Direct READ is allowed.
11
+ * Draft case is an exception. Direct requests are allowed.
12
+ */
13
+ if (!root._isDraftEnabled && root['@cds.autoexposed']) {
14
+ if (root['@cds.autoexpose']) {
15
+ if (req.event === 'READ') return //> allow read for value help requests
16
+ req.reject(405, 'ENTITY_IS_AUTOEXPOSE_READONLY', [root.name])
17
+ }
18
+ req.reject(405, 'ENTITY_IS_AUTOEXPOSED', [root.name])
10
19
  }
11
-
12
- // if draft enabled, allow direct access
13
- if (target._isDraftEnabled || !target['@cds.autoexposed']) return
14
-
15
- if (target['@cds.autoexpose']) {
16
- if (req.event === 'READ') return //> allow read for value help requests
17
- req.reject(405, 'ENTITY_IS_AUTOEXPOSE_READONLY', [target.name])
20
+ const isAutoexposed = _isAutoexposed(req.target)
21
+ if (isAutoexposed && req.event !== 'READ') {
22
+ req.reject(405, 'ENTITY_IS_AUTOEXPOSE_READONLY', [req.target.name])
18
23
  }
19
-
20
- // here, we are autoexposed (i.e., a composition)
21
- // -> reject all direct changes (i.e., only via navigation is allowed)
22
- if (req.subject?.ref.length === 1) req.reject(405, 'ENTITY_IS_AUTOEXPOSED', [target.name])
23
24
  }
24
25
 
25
26
  const _isAutoexposed = entity => {
@@ -2,7 +2,7 @@ const cds = require('../../../cds')
2
2
 
3
3
  const { rewriteExpandAsterisk } = require('../../utils/rewriteAsterisks')
4
4
 
5
- const { ensureNoDraftsSuffix } = require('../../../fiori/utils/handler')
5
+ const { ensureNoDraftsSuffix } = require('../../utils/draft')
6
6
 
7
7
  const _getTarget = (ref, target, definitions) => {
8
8
  if (cds.env.effective.odata.proxies) {
@@ -1,11 +1,10 @@
1
- const cds = require('../../../cds')
2
1
  const { getAuthRelevantEntity } = require('./utils')
3
2
  const { WRITE_EVENTS } = require('./constants')
4
3
 
5
4
  function handler(req) {
6
5
  // @read-only
7
6
  let entity = getAuthRelevantEntity(req, this.model, ['@readonly'])
8
- if (cds.env.fiori.lean_draft) entity = entity?.actives || entity
7
+ entity = entity?.actives || entity
9
8
 
10
9
  if (!entity || !entity['@readonly']) return
11
10
  if (entity['@readonly'] && req.event in WRITE_EVENTS) req.reject(405, 'ENTITY_IS_READ_ONLY', [entity.name])
@@ -6,32 +6,32 @@ const { getNormalizedPlainRestrictions } = require('./restrictions')
6
6
 
7
7
  const { cqn2cqn4sql } = require('../../utils/cqn2cqn4sql')
8
8
 
9
- const { isActiveEntityRequested, removeIsActiveEntityRecursively } = require('../../../fiori/utils/where')
10
-
11
9
  const _getResolvedApplicables = (applicables, req) => {
12
10
  const resolvedApplicables = []
13
11
 
14
12
  // REVISIT: the static portion of "mixed wheres" could already grant access -> optimization potential
15
13
  for (const restrict of applicables) {
16
- // replace $user.x with respective values
17
- const resolved = resolveUserAttrs({ grant: restrict.grant, target: restrict.target, where: restrict.where }, req)
18
-
19
- // check for duplicates
20
- if (
21
- !resolvedApplicables.find(
22
- restrict =>
23
- resolved.grant === restrict.grant &&
24
- (!resolved.target || resolved.target === restrict.target) &&
25
- (!resolved.where || resolved.where === restrict.where)
26
- )
27
- ) {
28
- if (resolved.where) {
29
- resolved._xpr = cds.parse.expr(resolved.where).xpr
30
- if (!resolved._xpr)
31
- req.reject(400, `Exists predicate is missing in the association path "${resolved.where}" in @restrict.where`)
14
+ let resolved
15
+ if (restrict.where) {
16
+ let xpr
17
+ if (typeof restrict.where === 'string') {
18
+ xpr = cds.parse.expr(restrict.where).xpr
19
+ if (!xpr)
20
+ req.reject(400, `Exists predicate is missing in the association path "${restrict.where}" in @restrict.where`)
21
+ } else {
22
+ xpr = JSON.parse(JSON.stringify(restrict.where))
23
+ }
24
+
25
+ resolved = {
26
+ grant: restrict.grant,
27
+ target: restrict.target,
28
+ where: restrict.where,
29
+ // replace $user.x with respective values
30
+ _xpr: resolveUserAttrs(xpr, req)
32
31
  }
33
- resolvedApplicables.push(resolved)
34
32
  }
33
+
34
+ resolvedApplicables.push(resolved || restrict)
35
35
  }
36
36
 
37
37
  return resolvedApplicables
@@ -140,11 +140,6 @@ const _getRestrictionForTarget = (resolvedApplicables, target) => {
140
140
  }
141
141
 
142
142
  const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
143
- if (!cds.env.fiori.lean_draft && req.target._isDraftEnabled) {
144
- req.query._draftRestrictions = resolvedApplicables
145
- return
146
- }
147
-
148
143
  // in case of $apply take a query from sub SELECT
149
144
  let query = req.query
150
145
  while (query.SELECT.from.SELECT) {
@@ -159,17 +154,6 @@ const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
159
154
  query.where(restrictionForTarget)
160
155
  }
161
156
 
162
- const _getFromWithIsActiveEntityRemoved = from => {
163
- if (!from.ref) return from
164
- for (const element of from.ref) {
165
- if (element.where && isActiveEntityRequested(element.where)) {
166
- element.where = removeIsActiveEntityRecursively(element.where)
167
- }
168
- }
169
-
170
- return from
171
- }
172
-
173
157
  const _getUnrestrictedCount = async req => {
174
158
  const dbtx = cds.tx(req)
175
159
  const target =
@@ -208,8 +192,7 @@ const _getRestrictedCount = async (req, model, resolvedApplicables) => {
208
192
  return n
209
193
  }
210
194
 
211
- // eslint-disable-next-line complexity
212
- async function handler(req) {
195
+ async function check_roles(req) {
213
196
  if (req.user._is_privileged || DRAFT_EVENTS[req.event]) {
214
197
  // > skip checks (events in DRAFT_EVENTS are checked in draft handlers via InProcessByUser)
215
198
  return
@@ -265,15 +248,13 @@ async function handler(req) {
265
248
  // no modification -> nothing more to do
266
249
  if (!MOD_EVENTS[req.event]) return
267
250
 
268
- if (req.query.DELETE) req.query.DELETE.from = _getFromWithIsActiveEntityRemoved(req.query.DELETE.from)
269
- if (req.query.SELECT) req.query.SELECT.from = _getFromWithIsActiveEntityRemoved(req.query.SELECT.from)
270
-
271
251
  // REVISIT: selected data could be used for etag check, diff, etc.
272
252
 
273
253
  /*
274
254
  * Here we check if UPDATE/DELETE requests add additional restrictions
275
255
  * Note: Needs to happen sequentially because of side effects
276
256
  */
257
+ // REVISIT: Do we really need to do that? Always?
277
258
  const unrestrictedCount = await _getUnrestrictedCount(req)
278
259
  if (unrestrictedCount === 0) req.reject(404)
279
260
 
@@ -284,6 +265,6 @@ async function handler(req) {
284
265
  }
285
266
  }
286
267
 
287
- handler._initial = true
268
+ check_roles._initial = true
288
269
 
289
- module.exports = handler
270
+ module.exports = check_roles
@@ -1,6 +1,5 @@
1
1
  const WRITE_EVENTS = { CREATE: 1, NEW: 1, UPDATE: 1, PATCH: 1, DELETE: 1, CANCEL: 1, EDIT: 1 }
2
2
  const CRUD = Object.assign({ READ: 1 }, WRITE_EVENTS)
3
- const cds = require('../../../cds')
4
3
 
5
4
  /**
6
5
  * Returns the applicable restrictions for the current request as follows:
@@ -68,10 +67,7 @@ const _addNormalizedRestrictPerGrant = (grant, where, restrict, restricts, defin
68
67
  }
69
68
 
70
69
  const _addNormalizedRestrict = (restrict, restricts, definition) => {
71
- const where = restrict.where
72
- ? (restrict.where['='] || restrict.where).replace(/\$user/g, '$user.id').replace(/\$user\.id\./g, '$user.')
73
- : undefined
74
-
70
+ const where = restrict.where?.xpr ?? restrict.where
75
71
  restrict.grant = Array.isArray(restrict.grant) ? restrict.grant : [restrict.grant || '*']
76
72
  restrict.grant.forEach(grant => _addNormalizedRestrictPerGrant(grant, where, restrict, restricts, definition))
77
73
  }
@@ -117,8 +113,7 @@ const _isToAccessAllowed = (user, restrict) => restrict.to.some(role => user.is(
117
113
 
118
114
  const getApplicableRestrictions = (restrictions, event, user) => {
119
115
  return restrictions.filter(restrict => {
120
- const eventName = cds.env.fiori.lean_draft ? event : { NEW: 'CREATE' }[event] || event
121
- return _isGrantAccessAllowed(eventName, restrict) && _isToAccessAllowed(user, restrict)
116
+ return _isGrantAccessAllowed(event, restrict) && _isToAccessAllowed(user, restrict)
122
117
  })
123
118
  }
124
119
 
@@ -23,6 +23,7 @@ const reject = (req, reason = null) => {
23
23
  })
24
24
  }
25
25
 
26
+ // REVISIT: Do we really need that? -> if not, let's eliminate it
26
27
  const getRejectReason = (req, annotation, definition, restrictedCount, unrestrictedCount) => {
27
28
  if (!LOG._debug) return
28
29
  // it is not possible to specify the reason further than the source as there are multiple factors
@@ -35,109 +36,111 @@ const getRejectReason = (req, annotation, definition, restrictedCount, unrestric
35
36
  }
36
37
  }
37
38
 
38
- const _getCurrentSubClause = (next, restrict) => {
39
- const escaped = next[0].replace(/\$/g, '\\$').replace(/\./g, '\\.')
40
- const re1 = new RegExp(`([\\w\\.']*)\\s*=\\s*(${escaped})|(${escaped})\\s*=\\s*([\\w\\.']*)`)
41
- const re2 = new RegExp(`([\\w\\.']*)\\s*in\\s*(${escaped})|(${escaped})\\s*in\\s*([\\w\\.']*)`)
42
- const re3 = new RegExp(`(${escaped})\\s*is\\s*null`)
43
- const re4 = new RegExp(`(${escaped})\\s*is\\s*not\\s*null`)
44
- const clause =
45
- restrict.where.match(re3) || restrict.where.match(re4) || restrict.where.match(re1) || restrict.where.match(re2)
46
-
47
- if (clause) return clause
48
-
49
- // NOTE: arrayed attr with "=" as operator is some kind of legacy case
50
- throw new Error('user attribute array must be used with operator "=", "in", "is null", or "is not null"')
51
- }
52
-
53
- const _isNull = (userAttrs, attr) =>
54
- userAttrs[attr] == null || (Array.isArray(userAttrs[attr]) && userAttrs[attr].length === 0)
55
- const _isNotNull = (userAttrs, attr) =>
56
- userAttrs[attr] != null && Array.isArray(userAttrs[attr]) && userAttrs[attr].length > 0
57
-
58
- const _processUserAttr = (next, restrict, userAttrs, attr) => {
59
- const clause = _getCurrentSubClause(next, restrict)
60
- const valOrRef = clause[1] || clause[4]
61
-
62
- if (clause[0].match(/ is\s*null/)) {
63
- restrict.where = restrict.where.replace(clause[0], _isNull(userAttrs, attr) ? '1 = 1' : '1 = 2')
64
- } else if (clause[0].match(/ is\s*not\s*null/)) {
65
- restrict.where = restrict.where.replace(clause[0], _isNotNull(userAttrs, attr) ? '1 = 1' : '1 = 2')
66
- } else {
67
- if (_isNull(userAttrs, attr)) {
68
- restrict.where = restrict.where.replace(clause[0], '1 = 2')
69
- } else if (clause[0].match(/ in /)) {
70
- if (userAttrs[attr].length === 1) {
71
- restrict.where = restrict.where.replace(clause[0], `${valOrRef} = '${userAttrs[attr][0]}'`)
72
- } else {
73
- restrict.where = restrict.where.replace(
74
- clause[0],
75
- `${valOrRef} in (${userAttrs[attr].map(ele => `'${ele}'`).join(', ')})`
76
- )
39
+ const _isNull = element => element.val === null || element.list?.length === 0
40
+ const _isNotNull = element => element.val !== null && (!element.list || element.list.length)
41
+
42
+ const _processNullAttr = where => {
43
+ if (!where) return
44
+
45
+ for (let i = where.length - 1; i >= 0; i--) {
46
+ if (where[i] === 'null') {
47
+ if (where[i - 2] === 'is' && where[i - 1] === 'not' && _isNull(where[i - 3])) {
48
+ where.splice(i - 3, 4, { val: '1' }, '=', { val: '2' })
49
+ i = i - 3
50
+ } else if (where[i - 2] === 'is' && where[i - 1] === 'not' && _isNotNull(where[i - 3])) {
51
+ where.splice(i - 3, 4, { val: '1' }, '=', { val: '1' })
52
+ i = i - 3
53
+ } else if (where[i - 1] === 'is' && _isNull(where[i - 2])) {
54
+ where.splice(i - 2, 3, { val: '1' }, '=', { val: '1' })
55
+ i = i - 2
56
+ } else if (where[i - 1] === 'is' && _isNotNull(where[i - 2])) {
57
+ where.splice(i - 2, 3, { val: '1' }, '=', { val: '2' })
58
+ i = i - 2
77
59
  }
78
- } else if (valOrRef.startsWith("'") && userAttrs[attr].includes(valOrRef.split("'")[1])) {
79
- restrict.where = restrict.where.replace(clause[0], `${valOrRef} = ${valOrRef}`)
80
- } else {
81
- restrict.where = restrict.where.replace(
82
- clause[0],
83
- `(${userAttrs[attr].map(ele => `${valOrRef} = '${ele}'`).join(' or ')})`
84
- )
85
60
  }
86
61
  }
87
62
  }
88
63
 
89
- /*
90
- * for supporting xssec v3
91
- */
92
- const _getAttrsAsProxy = (attrs, additional = {}) => {
93
- return new Proxy(
94
- {},
95
- {
96
- get: function (_, attr) {
97
- if (attr in additional) return additional[attr]
98
- return attrs[attr]
64
+ // NOTE: arrayed attr with "=" as operator is a valid expression (see authorization guide)
65
+ const _arrayComparison = (arr, where, index) => {
66
+ if (arr.length === 0) where[index] = { val: null }
67
+ else if (arr.length === 1) where[index] = { val: arr[0] }
68
+ else {
69
+ let start, element
70
+ if (where[index - 1] === '=' && where[index - 2]) {
71
+ start = index - 2
72
+ element = where[index - 2]
73
+ } else if (where[index + 1] === '=' && where[index + 2]) {
74
+ start = index
75
+ element = where[index + 2]
76
+ }
77
+ if (start !== undefined) {
78
+ const expr = []
79
+ arr.forEach(el => {
80
+ if (expr.length) expr.push('or')
81
+ expr.push(element, '=', { val: el })
82
+ })
83
+ where.splice(start, 3, { xpr: expr })
84
+ } else {
85
+ if (where[index + 1] !== 'is')
86
+ throw new Error('user attribute array must be used with operator "=", "in", "is null", or "is not null"')
87
+ where[index] = {
88
+ list: arr.map(v => {
89
+ return { val: v }
90
+ })
99
91
  }
100
92
  }
101
- )
93
+ }
102
94
  }
103
95
 
104
- /*
105
- * resolves user attributes deeply, even though nested attributes are officially not supported
106
- */
107
- const resolveUserAttrs = (restrict, req) => {
108
- const _getNext = where => where.match(/\$user\.([\w.]*)/)
109
- let next = _getNext(restrict.where)
110
-
111
- while (next !== null) {
112
- const parts = next[1].split('.')
113
- let skip
114
- let val
115
- let attrs = _getAttrsAsProxy(req.user.attr, { id: req.user.id })
116
- let attr = parts.shift()
117
-
118
- while (attr) {
119
- if (attrs[attr] === undefined || Array.isArray(attrs[attr])) {
120
- _processUserAttr(next, restrict, attrs, attr)
121
- skip = true
122
- break
96
+ const _handleArray = (arr, where, index) => {
97
+ if (where[index - 1] === 'in') {
98
+ if (arr.length === 0) where[index] = { list: [{ val: '__dummy__' }] }
99
+ else
100
+ where[index] = {
101
+ list: arr.map(v => {
102
+ return { val: v }
103
+ })
123
104
  }
105
+ } else _arrayComparison(arr, where, index)
106
+ }
124
107
 
125
- val = !Number.isNaN(Number(attrs[attr])) && attr !== 'id' ? attrs[attr] : `'${attrs[attr]}'`
126
- if (val === null || val === undefined) break
127
-
128
- attrs = _getAttrsAsProxy(attrs[attr])
129
- attr = parts.shift()
108
+ const resolveUserAttrs = (where, req) => {
109
+ let non_existing
110
+ for (let i = 0; i < where.length; i++) {
111
+ const r = where[i]
112
+ if (r.xpr) r.xpr = resolveUserAttrs(r.xpr, req)
113
+ else if (r.SELECT?.where) r.SELECT.where = resolveUserAttrs(r.SELECT.where, req)
114
+ else if (r?.ref?.[0] === '$user') {
115
+ if (r.ref.length === 1 || r.ref[1] === 'id') r.val = req.user.id
116
+ else {
117
+ let val = req.user.attr
118
+ for (let j = 1; j < r.ref.length; j++) {
119
+ const attr = r.ref[j]
120
+ if (!Object.prototype.hasOwnProperty.call(val, attr)) {
121
+ non_existing = true
122
+ break
123
+ } else val = val?.[attr]
124
+ }
125
+ if (non_existing) break
126
+ if (val === undefined) val = null
127
+ if (val === null && where[i - 1] === 'in') where[i] = { list: [{ val: '__dummy__' }] }
128
+ else if (Array.isArray(val)) _handleArray(val, where, i)
129
+ else r.val = val
130
+ }
131
+ delete r.ref
132
+ } else if (r.ref) {
133
+ r.ref.forEach(el => {
134
+ if (el.where) el.where = resolveUserAttrs(el.where, req)
135
+ })
130
136
  }
137
+ }
131
138
 
132
- if (!skip) {
133
- const v = val === undefined ? null : typeof val === 'string' && val.match(/^\d*$/) ? `'${val}'` : val
134
- restrict.where = restrict.where.replace(next[0], v).replace('in null', 'is null')
135
- }
139
+ if (non_existing) return [{ val: '1' }, '=', { val: '2' }]
136
140
 
137
- next = _getNext(restrict.where)
138
- }
141
+ _processNullAttr(where)
139
142
 
140
- return restrict
143
+ return where
141
144
  }
142
145
 
143
146
  const _authDependsOnAncestor = (entity, annotations) => {
@@ -1,7 +1,7 @@
1
1
  const cds = require('../../cds')
2
2
  const { SELECT } = cds.ql
3
3
 
4
- const { deepCopyArray } = require('../utils/copy')
4
+ const { deepCopy } = require('../utils/copy')
5
5
  const { getColumns } = require('../utils/columns')
6
6
  const { enhanceStreamResult } = require('../utils/stream')
7
7
  const getError = require('../error')
@@ -12,7 +12,6 @@ const _targetEntityDoesNotExist = async req => {
12
12
  }
13
13
 
14
14
  exports.impl = cds.service.impl(function () {
15
- // eslint-disable-next-line complexity
16
15
  this.on(['CREATE', 'READ', 'UPDATE', 'DELETE', 'UPSERT'], '*', async function (req) {
17
16
  if (!req.query) {
18
17
  throw getError({
@@ -39,7 +38,7 @@ exports.impl = cds.service.impl(function () {
39
38
 
40
39
  const { ref } = (req.query.INSERT && req.query.INSERT.into) || (req.query.SELECT && req.query.SELECT.from) || {}
41
40
  // REVISIT: why is copy necessary?
42
- if (ref && ref.length > 1) pathExistsQuery = SELECT(1).from({ ref: deepCopyArray(ref.slice(0, -1)) })
41
+ if (ref && ref.length > 1) pathExistsQuery = SELECT(1).from({ ref: deepCopy(ref.slice(0, -1)) })
43
42
 
44
43
  if (req.event === 'CREATE' && pathExistsQuery) {
45
44
  const res = await pathExistsQuery
@@ -101,8 +100,10 @@ exports.impl = cds.service.impl(function () {
101
100
  if (await _targetEntityDoesNotExist(req)) req.reject(404) // REVISIT: add a reasonable error message
102
101
  }
103
102
 
104
- // flag to trigger read after write in protocol adapter
105
- req._.readAfterWrite = true
103
+ // flag to trigger read after write in legacy odata adapter
104
+ if (req.constructor.name in { ODataRequest: 1 }) req._.readAfterWrite = true
105
+ if (req.protocol?.match(/odata/)) req._.readAfterWrite = true //> REVISIT for noah
106
+
106
107
  return req.data
107
108
  })
108
109
 
@@ -73,10 +73,10 @@ const _getValidationStmt = (ifMatchEtags, ifNoneMatchEtags, req, model) => {
73
73
  const commonGenericValidateETag = async function (req) {
74
74
  /*
75
75
  * currently, etag is only supported for OData requests
76
- * REST requests should be added later
76
+ * REST requests should be added later -> REVISIT!!!
77
77
  * other protocols, such as graphql, need to be checked
78
78
  */
79
- if (req.protocol !== 'odata-v4') return
79
+ if (req.protocol !== 'odata') return
80
80
 
81
81
  // automatically add etag columns if not already there
82
82
  if (req.query.SELECT) addEtagColumns(req.query.SELECT.columns, req.target)
@@ -148,14 +148,10 @@ module.exports = cds.service.impl(function () {
148
148
  if (!entity._etag) continue
149
149
 
150
150
  if (entity._isDraftEnabled) {
151
- if (cds.env.fiori?.lean_draft) {
152
- this.before(['READ', 'DELETE'], entity, commonGenericValidateETag)
153
- // if draft compat is on, the read handler is automatically registered for <entity> and <entity>.drafts
154
- const events = cds.env.fiori.draft_compat ? ['UPDATE', 'CANCEL'] : ['READ', 'UPDATE', 'CANCEL']
155
- this.before(events, entity.drafts, commonGenericValidateETag)
156
- } else {
157
- this.before(['READ', 'PATCH', 'CANCEL', 'DELETE'], entity, commonGenericValidateETag)
158
- }
151
+ this.before(['READ', 'DELETE'], entity, commonGenericValidateETag)
152
+ // if draft compat is on, the read handler is automatically registered for <entity> and <entity>.drafts
153
+ const events = cds.env.fiori.draft_compat ? ['UPDATE', 'CANCEL'] : ['READ', 'UPDATE', 'CANCEL']
154
+ this.before(events, entity.drafts, commonGenericValidateETag)
159
155
  } else {
160
156
  this.before(['READ', 'UPDATE', 'DELETE'], entity, commonGenericValidateETag)
161
157
  }
@@ -164,8 +160,7 @@ module.exports = cds.service.impl(function () {
164
160
  // etag not applicable to functions and unbound actions
165
161
  if (entity.actions[action].kind !== 'action') continue
166
162
  if (entity._isDraftEnabled) {
167
- let _entity = entity
168
- if (cds.env.fiori?.lean_draft) _entity = action === 'draftEdit' ? entity : entity.drafts
163
+ const _entity = action === 'draftEdit' ? entity : entity.drafts
169
164
  this.before(action === 'draftEdit' ? 'EDIT' : action, _entity, commonGenericValidateETag)
170
165
  } else {
171
166
  this.before(action, entity, commonGenericValidateETag)