@sap/cds 6.1.3 → 6.2.1

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 (206) hide show
  1. package/CHANGELOG.md +77 -8
  2. package/apis/cds.d.ts +18 -6
  3. package/apis/connect.d.ts +1 -1
  4. package/apis/cqn.d.ts +1 -1
  5. package/apis/log.d.ts +23 -5
  6. package/apis/ql.d.ts +128 -61
  7. package/apis/services.d.ts +11 -0
  8. package/apis/test.d.ts +61 -0
  9. package/apis/utils.d.ts +15 -0
  10. package/app/fiori/preview.js +1 -0
  11. package/bin/build/buildTaskEngine.js +70 -22
  12. package/bin/build/buildTaskFactory.js +18 -11
  13. package/bin/build/buildTaskHandler.js +1 -1
  14. package/bin/build/buildTaskProviderFactory.js +3 -13
  15. package/bin/build/constants.js +0 -1
  16. package/bin/build/index.js +14 -6
  17. package/bin/build/provider/buildTaskHandlerEdmx.js +2 -3
  18. package/bin/build/provider/buildTaskHandlerFeatureToggles.js +2 -2
  19. package/bin/build/provider/buildTaskHandlerInternal.js +3 -6
  20. package/bin/build/provider/buildTaskProviderInternal.js +51 -39
  21. package/bin/build/provider/fiori/index.js +3 -3
  22. package/bin/build/provider/hana/2migration.js +1 -1
  23. package/bin/build/provider/hana/index.js +34 -27
  24. package/bin/build/provider/java/index.js +6 -7
  25. package/bin/build/provider/mtx/index.js +20 -18
  26. package/bin/build/provider/mtx/resourcesTarBuilder.js +8 -11
  27. package/bin/build/provider/mtx-sidecar/index.js +13 -17
  28. package/bin/build/provider/nodejs/index.js +8 -7
  29. package/bin/build/util.js +22 -4
  30. package/bin/cds.js +8 -4
  31. package/bin/deploy/to-hana/cfUtil.js +53 -18
  32. package/bin/mtx/in-cds.js +1 -0
  33. package/bin/serve.js +37 -30
  34. package/lib/auth/basic-auth.js +33 -0
  35. package/lib/auth/dummy-auth.js +7 -0
  36. package/lib/auth/ias-auth.js +2 -0
  37. package/lib/auth/index.js +31 -0
  38. package/lib/auth/jwt-auth.js +3 -0
  39. package/lib/auth/mocked-users.js +72 -0
  40. package/lib/auth/passport-basic.js +12 -0
  41. package/lib/auth/passport-digest.js +14 -0
  42. package/lib/auth/xsuaa-auth.js +3 -0
  43. package/lib/compile/cds-compile.js +3 -3
  44. package/lib/compile/to/cdl.js +5 -1
  45. package/lib/compile/to/edm.js +8 -0
  46. package/lib/compile/to/gql.js +1 -0
  47. package/lib/compile/to/json.js +30 -5
  48. package/lib/compile/to/sql.js +3 -1
  49. package/lib/core/index.js +5 -1
  50. package/lib/dbs/cds-deploy.js +36 -6
  51. package/lib/env/cds-env.js +15 -5
  52. package/lib/env/cds-requires.js +51 -58
  53. package/lib/env/defaults.js +1 -0
  54. package/lib/env/schemas/cds-package.json +4 -0
  55. package/lib/env/schemas/cds-rc.json +63 -77
  56. package/lib/i18n/localize.js +16 -5
  57. package/lib/index.js +9 -4
  58. package/lib/log/cds-error.js +4 -6
  59. package/lib/log/cds-log.js +89 -53
  60. package/lib/log/service/index.js +1 -0
  61. package/lib/ql/CREATE.js +2 -5
  62. package/lib/ql/DELETE.js +1 -1
  63. package/lib/ql/DROP.js +1 -3
  64. package/lib/ql/INSERT.js +3 -3
  65. package/lib/ql/Query.js +10 -23
  66. package/lib/ql/SELECT.js +1 -2
  67. package/lib/ql/UPDATE.js +2 -2
  68. package/lib/ql/Whereable.js +7 -15
  69. package/lib/ql/cds-ql.js +9 -3
  70. package/lib/req/cds-context.js +11 -3
  71. package/lib/req/context.js +29 -23
  72. package/lib/req/locale.js +9 -5
  73. package/lib/req/request.js +1 -0
  74. package/lib/req/user.js +2 -1
  75. package/lib/srv/cds-connect.js +1 -1
  76. package/lib/srv/cds-serve.js +21 -14
  77. package/lib/srv/middlewares/cds-context.js +29 -0
  78. package/lib/srv/middlewares/ctx-model.js +24 -0
  79. package/lib/srv/middlewares/errors.js +9 -0
  80. package/lib/srv/middlewares/index.js +22 -0
  81. package/lib/srv/middlewares/sap-statistics.js +13 -0
  82. package/lib/srv/middlewares/trace.js +102 -0
  83. package/lib/srv/protocols/_legacy.js +42 -0
  84. package/lib/srv/protocols/graphql.js +39 -0
  85. package/lib/srv/protocols/hcql.js +37 -0
  86. package/lib/srv/protocols/index.js +86 -0
  87. package/lib/srv/protocols/odata-v2-proxy.js +3767 -0
  88. package/lib/srv/protocols/odata-v2.js +26 -0
  89. package/lib/srv/protocols/odata-v4.js +16 -0
  90. package/lib/srv/protocols/rest.js +13 -0
  91. package/lib/srv/srv-api.js +5 -0
  92. package/lib/srv/srv-models.js +4 -6
  93. package/lib/utils/axios.js +3 -2
  94. package/lib/utils/cds-test.js +27 -21
  95. package/lib/utils/cds-utils.js +19 -20
  96. package/lib/utils/tar.js +175 -0
  97. package/libx/_runtime/audit/generic/personal/utils.js +18 -7
  98. package/libx/_runtime/audit/utils/v2.js +1 -0
  99. package/libx/_runtime/auth/index.js +4 -0
  100. package/libx/_runtime/auth/strategies/ias-auth.js +76 -0
  101. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/error.js +8 -3
  102. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +15 -4
  103. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  104. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/orderByToCQN.js +1 -1
  105. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/utils.js +1 -1
  106. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/ResourcePathParser.js +9 -0
  107. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriInfo.js +5 -1
  108. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-commons/uri/UriParser.js +12 -0
  109. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/utils/BufferedWriter.js +6 -2
  110. package/libx/_runtime/cds-services/adapter/odata-v4/okra/odata-server/validator/RequestValidator.js +47 -7
  111. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +1 -1
  112. package/libx/_runtime/cds-services/util/assert.js +4 -0
  113. package/libx/_runtime/common/aspects/relation.js +1 -1
  114. package/libx/_runtime/common/composition/data.js +61 -15
  115. package/libx/_runtime/common/composition/delete.js +0 -1
  116. package/libx/_runtime/common/composition/insert.js +0 -1
  117. package/libx/_runtime/common/composition/tree.js +4 -10
  118. package/libx/_runtime/common/composition/update.js +44 -21
  119. package/libx/_runtime/common/generic/auth/capabilities.js +8 -10
  120. package/libx/_runtime/common/generic/crud.js +1 -2
  121. package/libx/_runtime/common/generic/etag.js +4 -4
  122. package/libx/_runtime/common/generic/input.js +4 -4
  123. package/libx/_runtime/common/generic/paging.js +3 -3
  124. package/libx/_runtime/common/generic/put.js +3 -3
  125. package/libx/_runtime/common/generic/sorting.js +4 -4
  126. package/libx/_runtime/common/generic/temporal.js +3 -3
  127. package/libx/_runtime/common/i18n/messages.properties +0 -7
  128. package/libx/_runtime/common/utils/cqn2cqn4sql.js +11 -6
  129. package/libx/_runtime/common/utils/csn.js +0 -28
  130. package/libx/_runtime/common/utils/draft.js +8 -1
  131. package/libx/_runtime/common/utils/path.js +7 -1
  132. package/libx/_runtime/common/utils/resolveView.js +2 -3
  133. package/libx/_runtime/db/data-conversion/post-processing.js +3 -44
  134. package/libx/_runtime/db/generic/input.js +3 -3
  135. package/libx/_runtime/db/sql-builder/dataTypes.js +4 -0
  136. package/libx/_runtime/fiori/generic/activate.js +2 -2
  137. package/libx/_runtime/fiori/generic/before.js +40 -72
  138. package/libx/_runtime/fiori/generic/cancel.js +2 -2
  139. package/libx/_runtime/fiori/generic/delete.js +2 -2
  140. package/libx/_runtime/fiori/generic/edit.js +2 -2
  141. package/libx/_runtime/fiori/generic/new.js +2 -2
  142. package/libx/_runtime/fiori/generic/patch.js +49 -37
  143. package/libx/_runtime/fiori/generic/prepare.js +2 -2
  144. package/libx/_runtime/fiori/generic/read.js +27 -37
  145. package/libx/_runtime/fiori/utils/where.js +4 -2
  146. package/libx/_runtime/hana/Service.js +1 -3
  147. package/libx/_runtime/hana/conversion.js +3 -0
  148. package/libx/_runtime/hana/driver.js +33 -3
  149. package/libx/_runtime/hana/dynatrace.js +1 -0
  150. package/libx/_runtime/hana/search2Contains.js +12 -1
  151. package/libx/_runtime/hana/search2cqn4sql.js +10 -27
  152. package/libx/_runtime/hana/streaming.js +1 -0
  153. package/libx/_runtime/messaging/AMQPWebhookMessaging.js +4 -2
  154. package/libx/_runtime/messaging/common-utils/AMQPClient.js +1 -0
  155. package/libx/_runtime/messaging/enterprise-messaging-utils/getTenantInfo.js +5 -2
  156. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +2 -0
  157. package/libx/_runtime/messaging/enterprise-messaging.js +62 -3
  158. package/libx/_runtime/messaging/outbox/utils.js +1 -1
  159. package/libx/_runtime/messaging/redis-messaging.js +1 -0
  160. package/libx/_runtime/remote/Service.js +2 -2
  161. package/libx/_runtime/remote/utils/client.js +8 -3
  162. package/libx/_runtime/remote/utils/data.js +7 -2
  163. package/libx/_runtime/sqlite/Service.js +18 -7
  164. package/libx/_runtime/sqlite/conversion.js +3 -0
  165. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +3 -3
  166. package/libx/_runtime/sqlite/localized.js +8 -8
  167. package/libx/odata/afterburner.js +39 -7
  168. package/libx/odata/cqn2odata.js +6 -3
  169. package/libx/odata/grammar.pegjs +66 -18
  170. package/libx/odata/index.js +3 -2
  171. package/libx/odata/parser.js +1 -1
  172. package/libx/odata/utils.js +2 -0
  173. package/libx/rest/RestAdapter.js +62 -43
  174. package/libx/rest/middleware/parse.js +2 -1
  175. package/libx/rest/middleware/update.js +1 -1
  176. package/package.json +2 -2
  177. package/server.js +5 -4
  178. package/srv/mtx.cds +1 -1
  179. package/srv/mtx.js +4 -33
  180. package/lib/srv/adapters.js +0 -85
  181. package/lib/utils/resources/index.js +0 -48
  182. package/lib/utils/resources/tar.js +0 -49
  183. package/lib/utils/resources/utils.js +0 -11
  184. package/libx/_runtime/extensibility/activate.js +0 -69
  185. package/libx/_runtime/extensibility/add.js +0 -50
  186. package/libx/_runtime/extensibility/addExtension.js +0 -72
  187. package/libx/_runtime/extensibility/defaults.js +0 -34
  188. package/libx/_runtime/extensibility/handler/transformREAD.js +0 -121
  189. package/libx/_runtime/extensibility/handler/transformRESULT.js +0 -51
  190. package/libx/_runtime/extensibility/handler/transformWRITE.js +0 -64
  191. package/libx/_runtime/extensibility/linter/allowlist_checker.js +0 -373
  192. package/libx/_runtime/extensibility/linter/annotations_checker.js +0 -113
  193. package/libx/_runtime/extensibility/linter/checker_base.js +0 -20
  194. package/libx/_runtime/extensibility/linter/namespace_checker.js +0 -180
  195. package/libx/_runtime/extensibility/linter.js +0 -32
  196. package/libx/_runtime/extensibility/push.js +0 -118
  197. package/libx/_runtime/extensibility/service.js +0 -38
  198. package/libx/_runtime/extensibility/token.js +0 -57
  199. package/libx/_runtime/extensibility/utils.js +0 -131
  200. package/libx/_runtime/extensibility/validation.js +0 -50
  201. package/libx/_runtime/extensibility/views.js +0 -12
  202. package/srv/extensibility-service.cds +0 -60
  203. package/srv/extensibility-service.js +0 -1
  204. package/srv/extensions.cds +0 -8
  205. package/srv/model-provider.cds +0 -61
  206. package/srv/model-provider.js +0 -143
@@ -20,7 +20,7 @@ const _orderExpression = order => {
20
20
  lastSegment.getKind() === ResourceKind.ANY_EXPRESSION ||
21
21
  lastSegment.getKind() === ResourceKind.ALL_EXPRESSION
22
22
  ) {
23
- throw getError(501, 'ORDERBY_LAMBDA_UNSUPPORTED')
23
+ throw getError(501, '"$orderby" does not support lambda')
24
24
  }
25
25
  for (let i = 0; i < order.getExpression().getPathSegments().length; i++) {
26
26
  ref.push(_buildNavRef(order.getExpression().getPathSegments()[i]))
@@ -2,7 +2,7 @@ const { getFeatureNotSupportedError } = require('../../../util/errors')
2
2
 
3
3
  const { findCsnTargetFor } = require('../../../../common/utils/csn')
4
4
 
5
- const CDL_KEYWORDS = new Set(require('@sap/cds-compiler/lib/base/keywords').cdl)
5
+ const CDL_KEYWORDS = new Set(require('@sap/cds-compiler').to.cdl.keywords)
6
6
 
7
7
  // TODO: Which EDM Types are missing?
8
8
  const notToBeConvertedForCompiler = new Set([
@@ -334,6 +334,15 @@ class ResourcePathParser {
334
334
 
335
335
  if (err) throw err
336
336
 
337
+ try {
338
+ nextTokenizer.requireNext(TokenKind.ODataIdentifier)
339
+ const uriResources = this._parseBoundOperation(uriPathSegments, currentResource, nextTokenizer)
340
+ if (uriResources) {
341
+ return [currentResource].concat(uriResources)
342
+ }
343
+ } catch (error) {
344
+ }
345
+
337
346
  throw new UriSyntaxError(UriSyntaxError.Message.MUST_BE_COUNT_OR_REF_OR_BOUND_OPERATION, uriPathSegments[0])
338
347
  }
339
348
 
@@ -155,7 +155,11 @@ UriInfo.QueryOptions = {
155
155
  SKIP: '$skip',
156
156
  TOP: '$top',
157
157
  SKIPTOKEN: '$skiptoken',
158
- DELTATOKEN: '$deltatoken'
158
+ DELTATOKEN: '$deltatoken',
159
+ AT: "$at",
160
+ TO: "$to",
161
+ FROM: "$from",
162
+ TOINCLUSIVE: "$toInclusive"
159
163
  }
160
164
 
161
165
  module.exports = UriInfo
@@ -102,6 +102,18 @@ queryOptionParserMap.set(QueryOptions.EXPAND, (value, edm, referringType, crossj
102
102
  return expandOption
103
103
  })
104
104
 
105
+ queryOptionParserMap.set(QueryOptions.FROM, (value) => {
106
+ return value
107
+ })
108
+ queryOptionParserMap.set(QueryOptions.TO, (value) => {
109
+ return value
110
+ })
111
+ queryOptionParserMap.set(QueryOptions.TOINCLUSIVE, (value) => {
112
+ return value
113
+ })
114
+ queryOptionParserMap.set(QueryOptions.AT, (value) => {
115
+ return value
116
+ })
105
117
  /**
106
118
  * The UriParser is the main class to parse an OData URI.
107
119
  */
@@ -1,5 +1,6 @@
1
1
  'use strict'
2
2
 
3
+ const { AsyncResource } = require('node:async_hooks')
3
4
  const Transform = require('stream').Transform
4
5
 
5
6
  /**
@@ -23,14 +24,17 @@ class BufferedWriter extends Transform {
23
24
  }
24
25
  })
25
26
 
26
- this.on('finish', () => {
27
+ // REVISIT: AsyncResource.bind()
28
+ // We AsyncResource.bind() here (also for non middleware case!) to ensure subsequent handlers have access to cds.context -> this test would break if not, and there's an async handler before ours in the route: cds/tests/_runtime/odata/__tests__/integration/crud-with-mtx.test.js
29
+ // Yet, if we do so this test breaks because the implementation of srv.on('error') is pretty screwed: cds/tests/runtime/req.test.js
30
+ this.on('finish', AsyncResource.bind(() => {
27
31
  /**
28
32
  * Result event to emit the result data.
29
33
  * @event BufferedWriter#result
30
34
  * @type {Buffer}
31
35
  */
32
36
  this.emit('result', this.createResultBuffer())
33
- })
37
+ }))
34
38
 
35
39
  this._internalBufferList = []
36
40
  }
@@ -67,16 +67,36 @@ const queryOptionsPerResourceKindWhiteList = new Map()
67
67
  QueryOptions.APPLY,
68
68
  QueryOptions.ORDERBY,
69
69
  QueryOptions.EXPAND,
70
- QueryOptions.SELECT
70
+ QueryOptions.SELECT,
71
+ QueryOptions.TO,
72
+ QueryOptions.AT,
73
+ QueryOptions.TOINCLUSIVE,
74
+ QueryOptions.FROM,
71
75
  ])
72
76
  .set(ResourceKind.ENTITY_COLLECTION + '/' + ResourceKind.COUNT, [
73
77
  QueryOptions.APPLY,
74
78
  QueryOptions.SEARCH,
75
79
  QueryOptions.FILTER
76
80
  ])
77
- .set(ResourceKind.ENTITY, [QueryOptions.EXPAND, QueryOptions.SELECT, QueryOptions.FORMAT])
81
+ .set(ResourceKind.ENTITY, [
82
+ QueryOptions.EXPAND,
83
+ QueryOptions.SELECT,
84
+ QueryOptions.FORMAT,
85
+ QueryOptions.TO,
86
+ QueryOptions.AT,
87
+ QueryOptions.TOINCLUSIVE,
88
+ QueryOptions.FROM
89
+ ])
78
90
  .set(ResourceKind.ENTITY + '/' + ResourceKind.VALUE, [QueryOptions.FORMAT])
79
- .set(ResourceKind.SINGLETON, [QueryOptions.EXPAND, QueryOptions.SELECT, QueryOptions.FORMAT])
91
+ .set(ResourceKind.SINGLETON, [
92
+ QueryOptions.EXPAND,
93
+ QueryOptions.SELECT,
94
+ QueryOptions.FORMAT,
95
+ QueryOptions.TO,
96
+ QueryOptions.AT,
97
+ QueryOptions.TOINCLUSIVE,
98
+ QueryOptions.FROM
99
+ ])
80
100
  .set(ResourceKind.REF, [QueryOptions.FORMAT])
81
101
  .set(ResourceKind.REF_COLLECTION, [
82
102
  QueryOptions.SEARCH,
@@ -86,9 +106,21 @@ const queryOptionsPerResourceKindWhiteList = new Map()
86
106
  QueryOptions.SKIP,
87
107
  QueryOptions.SKIPTOKEN,
88
108
  QueryOptions.TOP,
89
- QueryOptions.FORMAT
109
+ QueryOptions.FORMAT,
110
+ QueryOptions.TO,
111
+ QueryOptions.AT,
112
+ QueryOptions.TOINCLUSIVE,
113
+ QueryOptions.FROM
114
+ ])
115
+ .set(ResourceKind.COMPLEX_PROPERTY, [
116
+ QueryOptions.EXPAND,
117
+ QueryOptions.SELECT,
118
+ QueryOptions.FORMAT,
119
+ QueryOptions.TO,
120
+ QueryOptions.AT,
121
+ QueryOptions.TOINCLUSIVE,
122
+ QueryOptions.FROM,
90
123
  ])
91
- .set(ResourceKind.COMPLEX_PROPERTY, [QueryOptions.EXPAND, QueryOptions.SELECT, QueryOptions.FORMAT])
92
124
  .set(ResourceKind.COMPLEX_COLLECTION_PROPERTY, [
93
125
  QueryOptions.APPLY,
94
126
  QueryOptions.FILTER,
@@ -99,7 +131,11 @@ const queryOptionsPerResourceKindWhiteList = new Map()
99
131
  QueryOptions.TOP,
100
132
  QueryOptions.EXPAND,
101
133
  QueryOptions.SELECT,
102
- QueryOptions.FORMAT
134
+ QueryOptions.FORMAT,
135
+ QueryOptions.TO,
136
+ QueryOptions.AT,
137
+ QueryOptions.TOINCLUSIVE,
138
+ QueryOptions.FROM,
103
139
  ])
104
140
  .set(ResourceKind.COMPLEX_COLLECTION_PROPERTY + '/' + ResourceKind.COUNT, [QueryOptions.APPLY, QueryOptions.FILTER])
105
141
  .set(ResourceKind.PRIMITIVE_PROPERTY, [QueryOptions.FORMAT])
@@ -110,7 +146,11 @@ const queryOptionsPerResourceKindWhiteList = new Map()
110
146
  QueryOptions.SKIP,
111
147
  QueryOptions.SKIPTOKEN,
112
148
  QueryOptions.TOP,
113
- QueryOptions.FORMAT
149
+ QueryOptions.FORMAT,
150
+ QueryOptions.TO,
151
+ QueryOptions.AT,
152
+ QueryOptions.TOINCLUSIVE,
153
+ QueryOptions.FROM
114
154
  ])
115
155
  .set(ResourceKind.PRIMITIVE_COLLECTION_PROPERTY + '/' + ResourceKind.COUNT, [QueryOptions.FILTER])
116
156
  .set(ResourceKind.PRIMITIVE_PROPERTY + '/' + ResourceKind.VALUE, [QueryOptions.FORMAT])
@@ -26,7 +26,7 @@ if (cds.mtx || cds.requires.extensibility || cds.requires.toggles)
26
26
  const id = `${++unique} - ${srv.path}` // REVISIT: this is to allow running multiple express apps serving same endpoints, as done by some questionable tests
27
27
  return function ODataAdapter(req, res) {
28
28
  const model = cds.context?.model || srv.model
29
- if (!model._cached) Object.defineProperty(model, '_cached', { value: {} })
29
+ if (!model._cached) Object.defineProperty(model, '_cached', { value: { touched: Date.now() } })
30
30
 
31
31
  // Note: cache is attached to model cache so they get disposed when models are evicted from cache
32
32
  let adapters = model._cached._odata_adapters || (model._cached._odata_adapters = {})
@@ -147,7 +147,11 @@ const CDS_TYPE_CHECKS = {
147
147
  'cds.UUID': _checkUUID,
148
148
  'cds.Boolean': _checkBoolean,
149
149
  'cds.Integer': _checkInteger,
150
+ 'cds.UInt8': _checkInteger,
151
+ 'cds.Int16': _checkInteger,
152
+ 'cds.Int32': _checkInteger,
150
153
  'cds.Integer64': _checkInteger,
154
+ 'cds.Int64': _checkInteger,
151
155
  'cds.Decimal': _checkDecimal,
152
156
  'cds.DecimalFloat': _checkNumber,
153
157
  'cds.Double': _checkNumber,
@@ -23,7 +23,7 @@ module.exports = class Relation {
23
23
  }
24
24
 
25
25
  _has(prop) {
26
- return Reflect.has(this, prop) && !this._elements[prop]
26
+ return Reflect.has(this, prop)
27
27
  }
28
28
 
29
29
  get _elements() {
@@ -123,21 +123,40 @@ const _subData = (data, prop) =>
123
123
  return result
124
124
  }, [])
125
125
 
126
+ const _getWhereObj = (row, links) => {
127
+ return links.reduce((res, currentLink) => {
128
+ if (Object.prototype.hasOwnProperty.call(row, currentLink.targetKey) && row[currentLink.targetKey] !== null)
129
+ res[currentLink.entityKey] = row[currentLink.targetKey]
130
+ return res
131
+ }, {})
132
+ }
133
+
126
134
  const _subWhere = (result, element) => {
127
135
  let where
128
136
  const links = [...element.backLinks, ...element.customBackLinks]
129
- if (links && links.length > 0) {
130
- where = []
131
- for (const row of result) {
132
- const whereObj = links.reduce((res, currentLink) => {
133
- if (Object.prototype.hasOwnProperty.call(row, currentLink.targetKey) && row[currentLink.targetKey] !== null)
134
- res[currentLink.entityKey] = row[currentLink.targetKey]
135
- return res
136
- }, {})
137
- const whereCQN = ctUtils.whereKey(whereObj)
138
- if (whereCQN.length) {
139
- if (where.length > 0) where.push('or')
140
- where.push({ xpr: [...whereCQN] })
137
+ if (result.length && links && links.length > 0) {
138
+ where = {}
139
+ const chunkSize = cds.env.features.chunk_deep
140
+ const keys0 = Object.keys(_getWhereObj(result[0], links))
141
+ if (chunkSize && result.length > chunkSize && keys0.length) {
142
+ const keys = { list: keys0.map(pk => ({ ref: [pk] })) }
143
+ for (let i = 0; i < result.length; i += chunkSize) {
144
+ const values = {
145
+ list: result
146
+ .slice(i, i + chunkSize)
147
+ .map(row => ({ list: keys0.map(k => ({ val: _getWhereObj(row, links)[k] || null })) }))
148
+ }
149
+ where[i] = [keys, 'in', values]
150
+ }
151
+ } else {
152
+ where = []
153
+ for (const row of result) {
154
+ const whereObj = _getWhereObj(row, links)
155
+ const whereCQN = ctUtils.whereKey(whereObj)
156
+ if (whereCQN.length) {
157
+ if (where.length > 0) where.push('or')
158
+ where.push({ xpr: [...whereCQN] })
159
+ }
141
160
  }
142
161
  }
143
162
  }
@@ -214,8 +233,36 @@ const _select = ({
214
233
 
215
234
  const _selectDeepUpdateData = async args => {
216
235
  const { model, compositionTree, entityName, data, root, selectData, tx, selectAllColumns } = args
217
- const selectCQN = _select(args)
218
- const result = await tx.run(selectCQN)
236
+ let result = []
237
+ const chunkSize = cds.env.features.chunk_deep
238
+ if (
239
+ chunkSize &&
240
+ !args.where &&
241
+ args.parentKeys &&
242
+ args.parentKeys.length > chunkSize &&
243
+ Object.keys(args.parentKeys[0]).length
244
+ ) {
245
+ const keys0 = Object.keys(args.parentKeys[0])
246
+ const keys = { list: keys0.map(pk => ({ ref: [pk] })) }
247
+ for (let i = 0; i < args.parentKeys.length; i += chunkSize) {
248
+ const values = {
249
+ list: args.parentKeys.slice(i, i + chunkSize).map(row => ({ list: keys0.map(k => ({ val: row[k] || null })) }))
250
+ }
251
+ const _args = { ...args, where: [keys, 'in', values] }
252
+ const selectCQN = _select(_args)
253
+ result.push(...(await tx.run(selectCQN)))
254
+ }
255
+ } else if (chunkSize && args.where && !Array.isArray(args.where)) {
256
+ for (let where of Object.values(args.where)) {
257
+ const _args = { ...args, where }
258
+ const selectCQN = _select(_args)
259
+ result.push(...(await tx.run(selectCQN)))
260
+ }
261
+ } else {
262
+ const selectCQN = _select(args)
263
+ result = await tx.run(selectCQN)
264
+ }
265
+
219
266
  if (!result.length) return Promise.resolve(result)
220
267
 
221
268
  const keys = _keys(model.definitions[entityName], result)
@@ -274,7 +321,6 @@ const selectDeepUpdateData = (service, model, req, selectAllColumns = false) =>
274
321
  const compositionTree = getCompositionTree({
275
322
  definitions: model.definitions,
276
323
  rootEntityName: entityName, // REVISIT: drafts are resolved too eagerly
277
- checkRoot: false,
278
324
  resolveViews: !draft,
279
325
  service
280
326
  })
@@ -291,7 +291,6 @@ const getDeepDeleteCQNs = async (model, req, dbQuery) => {
291
291
  const compositionTree = getCompositionTree({
292
292
  definitions: model.definitions,
293
293
  rootEntityName: entityName,
294
- checkRoot: false,
295
294
  resolveViews: !draft,
296
295
  service: cds.db
297
296
  })
@@ -84,7 +84,6 @@ const getDeepInsertCQNs = (model, cqn) => {
84
84
  const compositionTree = getCompositionTree({
85
85
  definitions: model.definitions,
86
86
  rootEntityName: entityName,
87
- checkRoot: false,
88
87
  resolveViews: !draft,
89
88
  service: cds.db
90
89
  })
@@ -1,10 +1,8 @@
1
1
  const cds = require('../../cds')
2
2
 
3
3
  const { ensureNoDraftsSuffix } = require('../utils/draft')
4
- const { isRootEntity } = require('../utils/csn')
5
4
  const { getTransition, getDBTable } = require('../utils/resolveView')
6
5
 
7
- const getError = require('../../common/error')
8
6
  const { prefixForStruct } = require('../../common/utils/csn')
9
7
 
10
8
  /*
@@ -186,12 +184,9 @@ const _removeLocalizedTextsFromDraftTree = (compositionTree, definitions, checke
186
184
  }
187
185
  }
188
186
 
189
- const _getCompositionTree = ({ definitions, rootEntityName, checkRoot = true, resolveViews = false, service }) => {
187
+ const _getCompositionTree = ({ definitions, rootEntityName, resolveViews = false, service }) => {
190
188
  const rootName = resolveViews ? _resolvedEntityName(rootEntityName, definitions) : rootEntityName
191
189
 
192
- if (checkRoot && !isRootEntity(definitions, rootEntityName)) {
193
- throw getError(400, `Entity "${rootEntityName}" is not root entity`)
194
- }
195
190
  const compositionTree = {}
196
191
  _getCompositionTreeRec({
197
192
  rootEntityName: rootName,
@@ -238,8 +233,8 @@ const _cacheCompositionParentsOfOne = ({ definitions }) => {
238
233
 
239
234
  const _memoizeGetCompositionTree = fn => {
240
235
  const cache = new Map()
241
- return ({ definitions, rootEntityName, checkRoot = true, resolveViews = false, service }) => {
242
- const key = [rootEntityName, checkRoot].join('#')
236
+ return ({ definitions, rootEntityName, resolveViews = false, service }) => {
237
+ const key = rootEntityName
243
238
 
244
239
  // use ApplicationService as cache key for extensibility
245
240
  // REVISIT: context._tx is not a stable API -> pls do not rely on that
@@ -249,7 +244,7 @@ const _memoizeGetCompositionTree = fn => {
249
244
  const cachedResult = map && map.get(key)
250
245
  if (cachedResult) return cachedResult
251
246
  _cacheCompositionParentsOfOne({ definitions })
252
- const compTree = fn({ definitions, rootEntityName, checkRoot, resolveViews, service })
247
+ const compTree = fn({ definitions, rootEntityName, resolveViews, service })
253
248
 
254
249
  const _map = map || new Map()
255
250
  _map.set(key, compTree)
@@ -289,7 +284,6 @@ const getCompositionRoot = (definitions, entity) => {
289
284
  *
290
285
  * @param {object} definitions Definitions of the reflected model
291
286
  * @param {string} rootEntityName Name of the root entity
292
- * @param {boolean} checkRoot Check is provided entity is a root
293
287
  * @returns {object} tree of all compositions
294
288
  * @throws Error if no valid root entity provided
295
289
  */
@@ -34,17 +34,39 @@ const _dataByKey = (entity, data) => {
34
34
  return dataByKey
35
35
  }
36
36
 
37
- function _addSubDeepUpdateCQNForDelete({ entity, data, selectData, deleteCQN }) {
37
+ function _addSubDeepUpdateCQNForDelete({ entity, data, selectData, entityName, deleteCQNs }) {
38
+ const chunkSize = cds.env.features.chunk_deep
38
39
  const dataByKey = _dataByKey(entity, data)
39
- for (const selectEntry of selectData) {
40
- if (!selectEntry) continue
41
- const dataEntry = dataByKey.get(_serializedKey(entity, selectEntry))
42
- if (!dataEntry) {
43
- if (deleteCQN.DELETE.where.length > 0) {
44
- deleteCQN.DELETE.where.push('or')
40
+ if (chunkSize && selectData.length > chunkSize && Object.keys(selectData[0]).length) {
41
+ // REVISIT: Usage of "where in" syntax would be better
42
+ for (let j = 0; j < selectData.length; j += chunkSize) {
43
+ const deleteCQN = { DELETE: { from: entityName, where: [] } }
44
+ for (let i = j; i < j + chunkSize; i++) {
45
+ const selectEntry = selectData[i]
46
+ if (!selectEntry) continue
47
+ const dataEntry = dataByKey.get(_serializedKey(entity, selectEntry))
48
+ if (!dataEntry) {
49
+ if (deleteCQN.DELETE.where.length > 0) {
50
+ deleteCQN.DELETE.where.push('or')
51
+ }
52
+ deleteCQN.DELETE.where.push({ xpr: [...ctUtils.whereKey(ctUtils.key(entity, selectEntry))] })
53
+ }
54
+ }
55
+ deleteCQNs.push(deleteCQN)
56
+ }
57
+ } else {
58
+ const deleteCQN = { DELETE: { from: entityName, where: [] } }
59
+ for (const selectEntry of selectData) {
60
+ if (!selectEntry) continue
61
+ const dataEntry = dataByKey.get(_serializedKey(entity, selectEntry))
62
+ if (!dataEntry) {
63
+ if (deleteCQN.DELETE.where.length > 0) {
64
+ deleteCQN.DELETE.where.push('or')
65
+ }
66
+ deleteCQN.DELETE.where.push({ xpr: [...ctUtils.whereKey(ctUtils.key(entity, selectEntry))] })
45
67
  }
46
- deleteCQN.DELETE.where.push({ xpr: [...ctUtils.whereKey(ctUtils.key(entity, selectEntry))] })
47
68
  }
69
+ deleteCQNs.push(deleteCQN)
48
70
  }
49
71
  }
50
72
 
@@ -131,7 +153,7 @@ function _addSubDeepUpdateCQNForUpdateInsert({ entity, entityName, data, selectD
131
153
  return deepUpdateData
132
154
  }
133
155
 
134
- async function _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, deleteCQN, req) {
156
+ async function _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, deleteCQNs, req) {
135
157
  if (updateCQNs.length > 0) {
136
158
  cqns[0] = cqns[0] || []
137
159
  cqns[0].push(...updateCQNs)
@@ -152,15 +174,17 @@ async function _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, d
152
174
  })
153
175
  }
154
176
 
155
- if (deleteCQN.DELETE.where.length > 0) {
156
- cqns[0] = cqns[0] || []
157
- const deepDeleteCQNs = await getDeepDeleteCQNs(model, req, deleteCQN)
158
- deepDeleteCQNs.forEach((deleteCQNs, index) => {
159
- deleteCQNs.forEach(el => {
160
- cqns[index] = cqns[index] || []
161
- cqns[index].push(el)
177
+ for (let deleteCQN of deleteCQNs) {
178
+ if (deleteCQN.DELETE.where.length > 0) {
179
+ cqns[0] = cqns[0] || []
180
+ const deepDeleteCQNs = await getDeepDeleteCQNs(model, req, deleteCQN)
181
+ deepDeleteCQNs.forEach((delCQNs, index) => {
182
+ delCQNs.forEach(el => {
183
+ cqns[index] = cqns[index] || []
184
+ cqns[index].push(el)
185
+ })
162
186
  })
163
- })
187
+ }
164
188
  }
165
189
  }
166
190
 
@@ -219,8 +243,8 @@ const _addSubDeepUpdateCQN = async ({ model, compositionTree, data, selectData,
219
243
  const entityName = ctUtils.addDraftSuffix(draft, entity.name)
220
244
  const updateCQNs = []
221
245
  const insertCQN = { INSERT: { into: entityName, entries: [] } }
222
- const deleteCQN = { DELETE: { from: entityName, where: [] } }
223
- _addSubDeepUpdateCQNForDelete({ entity, data, selectData, deleteCQN })
246
+ const deleteCQNs = []
247
+ _addSubDeepUpdateCQNForDelete({ entity, data, selectData, entityName, deleteCQNs })
224
248
  const deepUpdateData = _addSubDeepUpdateCQNForUpdateInsert({
225
249
  entity,
226
250
  entityName,
@@ -231,7 +255,7 @@ const _addSubDeepUpdateCQN = async ({ model, compositionTree, data, selectData,
231
255
  model
232
256
  })
233
257
 
234
- await _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, deleteCQN, req)
258
+ await _addSubDeepUpdateCQNCollect(model, cqns, updateCQNs, insertCQN, deleteCQNs, req)
235
259
 
236
260
  if (deepUpdateData.length === 0) return Promise.resolve()
237
261
 
@@ -287,7 +311,6 @@ const getDeepUpdateCQNs = async (model, req, selectData) => {
287
311
  const compositionTree = getCompositionTree({
288
312
  definitions: model.definitions,
289
313
  rootEntityName: entityName,
290
- checkRoot: false,
291
314
  resolveViews: !draft,
292
315
  service: cds.db
293
316
  })
@@ -10,16 +10,14 @@ const _isRestricted = (req, capability, capabilityReadByKey) => {
10
10
 
11
11
  const _isNavigationRestricted = (target, path, annotation, req) => {
12
12
  if (!target) return
13
- const parts = annotation.split('.')
14
- if (target && Array.isArray(target['@Capabilities.NavigationRestrictions.RestrictedProperties'])) {
15
- for (const r of target['@Capabilities.NavigationRestrictions.RestrictedProperties']) {
16
- if (r.NavigationProperty['='] === path && r[parts[0]]) {
17
- return _isRestricted(
18
- req,
19
- r[parts[0]][parts[1]],
20
- r.ReadRestrictions && r.ReadRestrictions['ReadByKeyRestrictions.Readable']
21
- )
22
- }
13
+ if (!Array.isArray(target['@Capabilities.NavigationRestrictions.RestrictedProperties'])) return
14
+
15
+ const [restriction, operation] = annotation.split('.')
16
+ for (const r of target['@Capabilities.NavigationRestrictions.RestrictedProperties']) {
17
+ // prefix check to support both notations: { InsertRestrictions: { Insertable: false } } and { InsertRestrictions.Insertable: false }
18
+ if (r.NavigationProperty['='] === path && Object.keys(r).some(k => k.startsWith(restriction))) {
19
+ const capability = r[annotation] ?? r[restriction]?.[operation]
20
+ return _isRestricted(req, capability, r.ReadRestrictions?.['ReadByKeyRestrictions.Readable'])
23
21
  }
24
22
  }
25
23
  }
@@ -30,8 +30,7 @@ exports.impl = cds.service.impl(function () {
30
30
  if (typeof req.query !== 'string' && req.target && req.target._hasPersistenceSkip) {
31
31
  throw getError({
32
32
  code: 501,
33
- message: 'PERSISTENCE_SKIP_NO_GENERIC_CRUD',
34
- args: [req.target.name]
33
+ message: `Entity "${req.target.name}" is annotated with "@cds.persistence.skip" and cannot be served generically.`
35
34
  })
36
35
  }
37
36
 
@@ -69,7 +69,7 @@ const _isConcurrentODataReq = req => {
69
69
  *
70
70
  * @param req
71
71
  */
72
- const _handler = async function (req) {
72
+ const commonGenericEtag = async function (req) {
73
73
  // REVISIT: The check for ODataRequest should be removed after etag logic is moved
74
74
  // from okra to commons and etag handling is also allowed for rest.
75
75
 
@@ -104,7 +104,7 @@ const _handler = async function (req) {
104
104
  */
105
105
  /* istanbul ignore next */
106
106
  module.exports = cds.service.impl(function () {
107
- _handler._initial = true
107
+ commonGenericEtag._initial = true
108
108
 
109
109
  for (const k in this.entities) {
110
110
  const entity = this.entities[k]
@@ -119,10 +119,10 @@ module.exports = cds.service.impl(function () {
119
119
  events = ['READ', 'NEW', 'DELETE', 'PATCH', 'EDIT', 'CANCEL']
120
120
  }
121
121
 
122
- this.before(events, entity, _handler)
122
+ this.before(events, entity, commonGenericEtag)
123
123
 
124
124
  for (const action in entity.actions) {
125
- this.before(action, entity, _handler)
125
+ this.before(action, entity, commonGenericEtag)
126
126
  }
127
127
  }
128
128
  })
@@ -153,7 +153,7 @@ const _processCategory = (req, category, value, elementInfo, assertMap) => {
153
153
  }
154
154
 
155
155
  // generate UUIDs
156
- if (category === 'uuid' && !value.val && (event !== 'UPDATE' || !isRoot)) {
156
+ if (category === 'uuid' && !value.val && ((event !== 'UPDATE' && event !== 'PATCH') || !isRoot)) {
157
157
  value.val = row[key] = cds.utils.uuid()
158
158
  }
159
159
 
@@ -248,7 +248,7 @@ const _getBoundActionBindingParameter = req => {
248
248
  return (actions && actions[action] && actions[action]['@cds.odata.bindingparameter.name']) || 'in'
249
249
  }
250
250
 
251
- async function _handler(req) {
251
+ async function commonGenericInput(req) {
252
252
  if (!req.query) return // FIXME: the code below expects req.query to be defined
253
253
  if (!req.target) return
254
254
 
@@ -386,11 +386,11 @@ function _actionFunctionHandler(req) {
386
386
  _callError(req, errors)
387
387
  }
388
388
 
389
- _handler._initial = true
389
+ commonGenericInput._initial = true
390
390
  _actionFunctionHandler._initial = true
391
391
 
392
392
  module.exports = cds.service.impl(function () {
393
- this.before(['CREATE', 'UPDATE', 'NEW', 'PATCH'], '*', _handler)
393
+ this.before(['CREATE', 'UPDATE', 'NEW', 'PATCH'], '*', commonGenericInput)
394
394
  const operationNames = []
395
395
 
396
396
  for (const operation of this.operations) {
@@ -1,7 +1,7 @@
1
1
  const cds = require('../../cds')
2
2
  const { getDefaultPageSize, getMaxPageSize } = require('../utils/page')
3
3
 
4
- const _handler = function (req) {
4
+ const commonGenericPaging = function (req) {
5
5
  // only if http request
6
6
  if (!req._.req) return
7
7
 
@@ -24,6 +24,6 @@ const _addPaging = function (query, target) {
24
24
  * handler registration
25
25
  */
26
26
  module.exports = cds.service.impl(function () {
27
- _handler._initial = true
28
- this.before('READ', '*', _handler)
27
+ commonGenericPaging._initial = true
28
+ this.before('READ', '*', commonGenericPaging)
29
29
  })
@@ -55,7 +55,7 @@ const _pick = element => {
55
55
  }
56
56
  }
57
57
 
58
- function _handler(req) {
58
+ function commonGenericPut(req) {
59
59
  if (req.method !== 'PUT') return
60
60
  if (!req.query) return // FIXME: the code below expects req.query to be defined
61
61
  if (!req.target) return
@@ -87,8 +87,8 @@ function _handler(req) {
87
87
  setDataFromCQN(req)
88
88
  }
89
89
 
90
- _handler._initial = true
90
+ commonGenericPut._initial = true
91
91
 
92
92
  module.exports = cds.service.impl(function () {
93
- this.before(['UPDATE'], '*', _handler)
93
+ this.before(['UPDATE'], '*', commonGenericPut)
94
94
  })