@sap/cds 7.8.2 → 7.9.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (136) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/_i18n/i18n_ar.properties +3 -0
  3. package/_i18n/i18n_cs.properties +3 -0
  4. package/_i18n/i18n_da.properties +3 -0
  5. package/_i18n/i18n_es_MX.properties +3 -0
  6. package/_i18n/i18n_fi.properties +3 -0
  7. package/_i18n/i18n_hu.properties +6 -0
  8. package/_i18n/i18n_ko.properties +3 -0
  9. package/_i18n/i18n_ms.properties +3 -0
  10. package/_i18n/i18n_nl.properties +3 -0
  11. package/_i18n/i18n_no.properties +3 -0
  12. package/_i18n/i18n_ro.properties +3 -0
  13. package/_i18n/i18n_sv.properties +3 -0
  14. package/_i18n/i18n_th.properties +3 -0
  15. package/_i18n/i18n_tr.properties +6 -0
  16. package/_i18n/i18n_zh_TW.properties +3 -0
  17. package/bin/serve.js +5 -5
  18. package/lib/auth/basic-auth.js +1 -1
  19. package/lib/compile/cdsc.js +33 -6
  20. package/lib/compile/etc/_localized.js +14 -7
  21. package/lib/compile/for/lean_drafts.js +9 -0
  22. package/lib/compile/to/edm-files.js +116 -0
  23. package/lib/compile/to/edm.js +8 -1
  24. package/lib/compile/to/hdbtabledata.js +3 -3
  25. package/lib/compile/to/sql.js +4 -2
  26. package/lib/compile/to/yaml.js +22 -21
  27. package/lib/dbs/cds-deploy.js +5 -6
  28. package/lib/env/cds-env.js +7 -0
  29. package/lib/env/cds-requires.js +20 -1
  30. package/lib/env/defaults.js +21 -5
  31. package/lib/env/schemas/cds-package.js +1 -1
  32. package/lib/env/schemas/cds-rc.js +85 -4
  33. package/lib/index.js +1 -1
  34. package/lib/linked/entities.js +10 -0
  35. package/lib/linked/models.js +1 -1
  36. package/lib/plugins.js +1 -1
  37. package/lib/ql/INSERT.js +17 -3
  38. package/lib/ql/Query.js +4 -0
  39. package/lib/ql/infer.js +1 -1
  40. package/lib/req/request.js +1 -1
  41. package/lib/srv/cds-serve.js +1 -0
  42. package/lib/srv/middlewares/cds-context.js +1 -1
  43. package/lib/srv/protocols/odata-v4.js +5 -6
  44. package/lib/srv/srv-models.js +9 -2
  45. package/lib/utils/cds-test.js +2 -0
  46. package/lib/utils/cds-utils.js +9 -4
  47. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  48. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  50. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
  52. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
  53. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
  54. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
  55. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  56. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
  57. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
  60. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
  61. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
  64. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
  65. package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
  66. package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
  67. package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
  68. package/libx/_runtime/common/generic/auth/index.js +2 -0
  69. package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
  70. package/libx/_runtime/common/generic/auth/restrict.js +6 -5
  71. package/libx/_runtime/common/generic/auth/utils.js +1 -1
  72. package/libx/_runtime/common/generic/crud.js +5 -8
  73. package/libx/_runtime/common/generic/etag.js +8 -6
  74. package/libx/_runtime/common/generic/sorting.js +2 -2
  75. package/libx/_runtime/common/i18n/messages.properties +1 -0
  76. package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
  77. package/libx/_runtime/common/utils/compareJson.js +274 -0
  78. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  79. package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
  80. package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
  81. package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
  82. package/libx/_runtime/common/utils/resolveView.js +0 -16
  83. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  84. package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
  85. package/libx/_runtime/common/utils/streamProp.js +9 -2
  86. package/libx/_runtime/common/utils/ucsn.js +1 -1
  87. package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
  88. package/libx/_runtime/db/generic/rewrite.js +7 -13
  89. package/libx/_runtime/fiori/generic/activate.js +1 -1
  90. package/libx/_runtime/fiori/generic/edit.js +1 -1
  91. package/libx/_runtime/fiori/generic/prepare.js +1 -1
  92. package/libx/_runtime/fiori/lean-draft.js +151 -46
  93. package/libx/_runtime/fiori/utils/handler.js +1 -1
  94. package/libx/_runtime/hana/execute.js +6 -2
  95. package/libx/_runtime/hana/search2cqn4sql.js +1 -1
  96. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
  97. package/libx/_runtime/messaging/event-broker.js +212 -0
  98. package/libx/_runtime/remote/Service.js +9 -32
  99. package/libx/_runtime/remote/utils/client.js +13 -21
  100. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
  101. package/libx/_runtime/sqlite/execute.js +8 -3
  102. package/libx/_runtime/ucl/Service.js +259 -0
  103. package/libx/common/assert/index.js +5 -11
  104. package/libx/common/assert/validation.js +6 -1
  105. package/libx/odata/index.js +47 -25
  106. package/libx/odata/middleware/batch.js +8 -7
  107. package/libx/odata/middleware/create.js +42 -16
  108. package/libx/odata/middleware/delete.js +18 -11
  109. package/libx/odata/middleware/metadata.js +15 -14
  110. package/libx/odata/middleware/operation.js +30 -40
  111. package/libx/odata/middleware/parse.js +2 -3
  112. package/libx/odata/middleware/read.js +59 -52
  113. package/libx/odata/middleware/service-document.js +7 -7
  114. package/libx/odata/middleware/stream.js +26 -24
  115. package/libx/odata/middleware/update.js +53 -92
  116. package/libx/odata/parse/afterburner.js +45 -47
  117. package/libx/odata/parse/grammar.peggy +3 -3
  118. package/libx/odata/parse/multipartToJson.js +10 -22
  119. package/libx/odata/parse/parser.js +1 -1
  120. package/libx/odata/utils/etag.js +13 -0
  121. package/libx/odata/utils/handler.js +120 -0
  122. package/libx/odata/utils/index.js +15 -2
  123. package/libx/odata/utils/metaInfo.js +410 -0
  124. package/libx/odata/utils/path.js +5 -2
  125. package/libx/odata/utils/readAfterWrite.js +23 -0
  126. package/libx/odata/utils/result.js +4 -5
  127. package/libx/rest/RestAdapter.js +4 -13
  128. package/libx/rest/middleware/parse.js +40 -7
  129. package/package.json +1 -1
  130. package/server.js +1 -0
  131. package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
  132. package/libx/_runtime/common/utils/thenable.js +0 -51
  133. package/libx/_runtime/rest/service.js +0 -2
  134. package/libx/odata/parse/parseToCqn.js +0 -39
  135. package/libx/rest/middleware/input.js +0 -54
  136. package/libx/rest/middleware/payload.js +0 -13
@@ -15,7 +15,7 @@ const { getSapMessages } = require('../../../../common/error/frontend')
15
15
  /**
16
16
  * The handler that will be registered with odata-v4.
17
17
  *
18
- * @param {import('../../../services/Service')} service
18
+ * @param {import('../../../../common/Service')} service
19
19
  * @returns {function}
20
20
  */
21
21
  const create = service => {
@@ -11,7 +11,7 @@ const { validateResourcePath } = require('../utils/request')
11
11
  /**
12
12
  * The handler that will be registered with odata-v4.
13
13
  *
14
- * @param {import('../../../services/Service')} service
14
+ * @param {import('../../../../common/Service')} service
15
15
  * @returns {function}
16
16
  */
17
17
  const del = service => {
@@ -39,12 +39,9 @@ const metadata = service => {
39
39
  edmx = await mps.getEdmx({ tenant, model: service.model, service: service.definition.name, locale })
40
40
  }
41
41
  } else {
42
- edmx = cds.localize(
43
- service.model,
44
- locale,
45
- // REVISIT: we could cache this in model._cached
46
- cds.compile.to.edmx(service.model, { service: service.definition.name })
47
- )
42
+ edmx = await cds.compile.to.edmx.files.get(service)
43
+ edmx ??= cds.compile.to.edmx(service.model, { service: service.definition.name })
44
+ edmx = cds.localize(service.model, locale, edmx)
48
45
  }
49
46
 
50
47
  return next(null, toODataResult(edmx))
@@ -150,12 +150,22 @@ const _getResult = (nameArr, result) => {
150
150
  return _getResult(nameArr.slice(1), result[nameArr[0]])
151
151
  }
152
152
 
153
+ const validateIfNoneMatch = (req, result) => {
154
+ if (req.target._etag && req.headers['if-none-match']) {
155
+ let header = req.headers['if-none-match']
156
+ if (header.startsWith('W/')) header = header.substring(2)
157
+ if (header.startsWith('"') && header.endsWith('"')) header = header.substring(1, header.length - 1)
158
+ if (header === '*') return true
159
+ if (result[req.target._etag.name] === header) return true
160
+ }
161
+ }
162
+
153
163
  /**
154
164
  * Reads the entire entity or only property of it is alike.
155
165
  *
156
166
  * In case of an entity, odata-v4 wants the value an object structure, in case of a property as scalar.
157
167
  *
158
- * @param {import('../../../../cds-services/services/Service')} tx
168
+ * @param {import('../../../../common/Service')} tx
159
169
  * @param {import('../../../../cds-services/adapter/odata-v4/ODataRequest')} req
160
170
  * @param {Array<import('../okra/odata-commons/uri/UriResource')>} segments
161
171
  * @returns {Promise}
@@ -175,14 +185,15 @@ const _readEntityOrProperty = async (tx, req, segments) => {
175
185
  * If no entity is related, the service returns 204 No Content.
176
186
  */
177
187
  if (result == null) {
178
- if (req.headers['if-none-match']) {
179
- req._.odataRes.setStatusCode(304)
180
- return
181
- }
182
188
  if (_isNavigationToOne(segments)) return toODataResult(null)
183
189
  throw getError(404)
184
190
  }
185
191
 
192
+ if (validateIfNoneMatch(req, result)) {
193
+ req._.odataRes.setStatusCode(304)
194
+ return
195
+ }
196
+
186
197
  if (!Array.isArray(result)) result = [result]
187
198
 
188
199
  if (result.length === 0 && _isNavigationToOne(segments)) return toODataResult(null)
@@ -373,10 +384,6 @@ const _readStream = async (tx, req) => {
373
384
 
374
385
  // Reading one entity or a property of it should yield only a result length of one.
375
386
  if (result.length === 0 || result[0] === undefined) {
376
- if (req.headers['if-none-match']) {
377
- req._.odataRes.setStatusCode(304)
378
- return
379
- }
380
387
  throw getError(404)
381
388
  }
382
389
 
@@ -386,6 +393,11 @@ const _readStream = async (tx, req) => {
386
393
 
387
394
  result = result[0]
388
395
 
396
+ if (validateIfNoneMatch(req, result)) {
397
+ req._.odataRes.setStatusCode(304)
398
+ return
399
+ }
400
+
389
401
  const readable = cds.env.features.stream_compat
390
402
  ? result.value
391
403
  : Object.values(result).find(v => v instanceof Readable)
@@ -511,7 +523,7 @@ const _ensureStream = result => {
511
523
  * If the single entity to be read does not exist, calls next with error to return a 404.
512
524
  * In all other failure cases it calls next with error to return a 500.
513
525
  *
514
- * @param {import('../../../services/Service')} service
526
+ * @param {import('../../../../common/Service')} service
515
527
  * @returns {function}
516
528
  */
517
529
  const read = service => {
@@ -27,8 +27,8 @@ module.exports = srv => {
27
27
  // in case of $batch we need to challenge directly, as the header is not processed if in $batch response body
28
28
  if (containsRestrictions && path.endsWith('/$batch') && req.user._is_anonymous) {
29
29
  // NOTE: "return req._login()" would not invoke custom error handlers
30
- if (req._login) res.set('WWW-Authenticate', `Basic realm="Users"`)
31
- else if (user._challenges) res.set('WWW-Authenticate', user._challenges.join(';'))
30
+ if (req._login) res.set('www-authenticate', `Basic realm="Users"`)
31
+ else if (user._challenges) res.set('www-authenticate', user._challenges.join(';'))
32
32
  return next(ODATA_UNAUTHORIZED)
33
33
  }
34
34
 
@@ -37,8 +37,8 @@ module.exports = srv => {
37
37
  // > unauthorized or forbidden?
38
38
  if (req.user._is_anonymous) {
39
39
  // NOTE: "return req._login()" would not invoke custom error handlers
40
- if (req._login) res.set('WWW-Authenticate', `Basic realm="Users"`)
41
- else if (user._challenges) res.set('WWW-Authenticate', user._challenges.join(';'))
40
+ if (req._login) res.set('www-authenticate', `Basic realm="Users"`)
41
+ else if (user._challenges) res.set('www-authenticate', user._challenges.join(';'))
42
42
  return next(ODATA_UNAUTHORIZED)
43
43
  }
44
44
  return next(ODATA_FORBIDDEN)
@@ -105,9 +105,10 @@ const _updateThenCreate = async (req, odataReq, odataRes, tx) => {
105
105
  (e.code === 412 || e.status === 412 || e.statusCode === 412) && req.headers['if-none-match'] === '*'
106
106
  if ((is404 || isForcedInsert) && _isUpsertAllowed(req)) {
107
107
  // PUT/ PATCH with if-match header means "only if already exists", i.e., no insert if not
108
- if (req.headers['if-match'])
108
+ if (req.headers['if-match']) {
109
109
  throw Object.assign(new Error('412'), { statusCode: 412 })
110
- // REVISIT: remove error (and child?) from tx.context? -> would require a unique req.id
110
+ }
111
+ // REVISIT: remove error (and child?) from tx.context? -> would require a unique req.id
111
112
  ;[result, req] = await _create(req, odataReq, odataRes, tx)
112
113
  odataRes.setStatusCode(201, { overwrite: true })
113
114
  } else {
@@ -148,7 +149,7 @@ const _getStructValue = (prop, result, segments) => {
148
149
  * In case of success it calls next with the number of updated entries as result.
149
150
  * In case of error it calls next with error.
150
151
  *
151
- * @param {import('../../../services/Service')} service
152
+ * @param {import('../../../../common/Service')} service
152
153
  * @returns {function}
153
154
  */
154
155
  const update = service => {
@@ -57,7 +57,7 @@ class ExpressionToCQN {
57
57
  return { val: new Date(value).toISOString().replace(/\.\d\d\dZ$/, 'Z') }
58
58
  return { val: normalizeTimestamp(value) }
59
59
  } catch (e) {
60
- throw Object.assign(new Error(`The type 'Edm.DateTimeOffset' is not compatible with '${value}'`), {
60
+ throw Object.assign(new Error(`The type Edm.DateTimeOffset is not compatible with "${value}"`), {
61
61
  status: 400
62
62
  })
63
63
  }
@@ -84,6 +84,7 @@ const _addCount = aggregateExpression => {
84
84
 
85
85
  const _createColumnsForAggregateExpressions = (aggregateExpressions, entity) => {
86
86
  const columns = []
87
+ let defaultAggregation
87
88
  for (const aggregateExpression of aggregateExpressions) {
88
89
  // custom aggregates
89
90
  if (aggregateExpression.getPathSegments() && aggregateExpression.getPathSegments().length === 1) {
@@ -97,7 +98,9 @@ const _createColumnsForAggregateExpressions = (aggregateExpressions, entity) =>
97
98
  entity.elements[name][AGGREGATION_DEFAULT] &&
98
99
  entity.elements[name][AGGREGATION_DEFAULT]['#']
99
100
  ) {
100
- columns.push({ [`${entity.elements[name][AGGREGATION_DEFAULT]['#'].toLowerCase()}(${name})`]: name })
101
+ defaultAggregation = entity.elements[name][AGGREGATION_DEFAULT]['#'].toLowerCase()
102
+ if (defaultAggregation === 'count_distinct') defaultAggregation = 'countdistinct'
103
+ columns.push({ [`${defaultAggregation}(${name})`]: name })
101
104
  continue
102
105
  }
103
106
  }
@@ -9,7 +9,7 @@ const {
9
9
  const { getFeatureNotSupportedError } = require('../../../util/errors')
10
10
  const orderByToCQN = require('./orderByToCQN')
11
11
  const ExpressionToCQN = require('./ExpressionToCQN')
12
- const { getColumns } = require('../../../services/utils/columns')
12
+ const { getColumns } = require('../../../../common/utils/columns')
13
13
  const { addLimit, isSameArray } = require('./utils')
14
14
  const { findCsnTargetFor } = require('../../../../common/utils/csn')
15
15
  const getError = require('../../../../common/error')
@@ -9,7 +9,44 @@ const readToCQN = require('./readToCQN')
9
9
  const updateToCQN = require('./updateToCQN')
10
10
  const createToCQN = require('./createToCQN')
11
11
  const deleteToCQN = require('./deleteToCQN')
12
- const parseToCqn = require('../../../../../odata/parse/parseToCqn')
12
+
13
+ const parseToCqn = (component, service, target, data, odataReq, upsert) => {
14
+ let query = cds.odata.parse(odataReq.getIncomingRequest().url, { service })
15
+
16
+ // for concat
17
+ if (component === 'READ' && Array.isArray(query)) return query
18
+
19
+ const _target = query.SELECT && query.SELECT.from
20
+
21
+ const {
22
+ SELECT: { one }
23
+ } = query
24
+
25
+ switch (component) {
26
+ case 'CREATE':
27
+ // create
28
+ // error in cases like `POST Books(1)` i.e. `POST` with navigation to single entity
29
+ if (one && !upsert) cds.error('POST not allowed on entity', { code: 400 })
30
+ return INSERT.into(_target).entries(data)
31
+ case 'DELETE':
32
+ if (!one) cds.error('DELETE not allowed on collection', { code: 400 })
33
+ // eslint-disable-next-line no-case-declarations
34
+ const last = query._propertyAccess || (_target.ref && _target.ref[_target.ref.length - 1])
35
+ if (target.elements[last] || target.elements[query._propertyAccess]) {
36
+ // delete simple property
37
+ const ref = { ref: query._propertyAccess ? _target.ref : _target.ref.slice(0, -1) }
38
+ return UPDATE(ref).data({ [last]: null })
39
+ } else {
40
+ return DELETE.from(_target)
41
+ }
42
+ case 'UPDATE':
43
+ // eslint-disable-next-line no-throw-literal
44
+ if (!one) throw { statusCode: 400, code: '400', message: `INVALID_${odataReq.getMethod()}` }
45
+ return UPDATE(_target).data(data)
46
+ default:
47
+ return query
48
+ }
49
+ }
13
50
 
14
51
  /**
15
52
  * This method transforms an odata request into a CQN object.
@@ -217,12 +217,12 @@ const _checkViewWithParamCall = (isView, segments, kind, name) => {
217
217
  }
218
218
 
219
219
  if (segments.length < 2) {
220
- throw cds.error(`Invalid call to "${name}". You need to navigate to Set`, { status: 400, code: 400 })
220
+ throw cds.error(`Invalid call to "${name}". You need to navigate to Set`, { statusCode: 400, code: 400 })
221
221
  }
222
222
 
223
223
  // if the last segment is count, check if previous segment is Set, otherwise check if the last segment equals Set
224
224
  if (!_isSet(segments[segments.length - (_isCount(kind) ? 2 : 1)])) {
225
- throw cds.error(`Invalid call to "${name}". You need to navigate to Set`, { status: 400, code: 400 })
225
+ throw cds.error(`Invalid call to "${name}". You need to navigate to Set`, { statusCode: 400, code: 400 })
226
226
  }
227
227
  }
228
228
 
@@ -1,19 +1,29 @@
1
1
  const cds = require('../../../cds')
2
+ // prettier-ignore
3
+ const OkraAdapter = Object.assign (require('./OData'), {
4
+ for (srv, model, edm) {
5
+ return new this (edm, model, srv.options).addCDSServiceToChannel(srv)
6
+ }
7
+ })
2
8
 
3
9
  /**
4
10
  * This is the express handler for a specific OData endpoint.
5
11
  * Note: the same service can be served at different endpoints.
6
12
  */
7
- module.exports = srv => {
8
- const okra = new OkraAdapter(srv)
9
- return okra.process.bind(okra)
10
- }
11
-
12
- let OData
13
- function OkraAdapter(srv, model = srv.model) {
14
- const edm = cds.compile.to.edm(model, { service: srv.definition?.name || srv.name })
15
- OData ??= require('./OData')
16
- return new OData(edm, model, srv.options).addCDSServiceToChannel(srv)
13
+ // prettier-ignore
14
+ const adapter4 = module.exports = function (srv) {
15
+ const csn = srv.model
16
+ let okra, pre_compiled_edm = cds.compile.to.edmx.files.get (srv, 'edm.json', csn)
17
+ if (pre_compiled_edm) okra = { // using a lazy-loading okra proxy
18
+ process: (...args) => pre_compiled_edm.then (edm => {
19
+ okra = OkraAdapter.for (srv, csn, edm) // replacing the proxy for subsequent calls
20
+ return okra.process (...args) // delegating the first call
21
+ })
22
+ }; else {
23
+ let edm = cds.compile.to.edm (csn, { service: srv.definition?.name || srv.name })
24
+ okra = OkraAdapter.for (srv, csn, edm)
25
+ }
26
+ return (...args) => okra.process (...args)
17
27
  }
18
28
 
19
29
  //////////////////////////////////////////////////////////////////////////////
@@ -21,21 +31,22 @@ function OkraAdapter(srv, model = srv.model) {
21
31
  // REVISIT: Move to ExtensibilityService
22
32
  //
23
33
  if (cds.requires.extensibility || cds.requires.toggles) {
24
- let unique = 0
34
+ /**
35
+ * Creates new OkraAdapter for new models with tenant-specific extensions.
36
+ * The created adapters are cached in `model._cache`.
37
+ * Note: As the adapters cache is attached to the model, they get disposed
38
+ * when models are evicted from the models cache.
39
+ */
25
40
  module.exports = srv => {
26
41
  const id = `${++unique} - ${srv.path}` // REVISIT: this is to allow running multiple express apps serving same endpoints, as done by some questionable tests
42
+ // prettier-ignore
27
43
  return function ODataAdapter(req, res) {
28
44
  const model = cds.context?.model || srv.model
29
- if (!model._cached) Object.defineProperty(model, '_cached', { value: { touched: Date.now() } })
30
-
31
- // Note: cache is attached to model cache so they get disposed when models are evicted from cache
32
- let adapters = model._cached._odata_adapters || (model._cached._odata_adapters = {})
33
- let okra = adapters[id]
34
- if (!okra) {
35
- const _srv = { __proto__: srv, _real_srv: srv, model } // REVISIT: we need to do that better in new adapters
36
- okra = adapters[id] = new OkraAdapter(_srv, model)
37
- }
38
- return okra.process(req, res)
45
+ if (!model._cached) Object.defineProperty (model, '_cached', { value: { touched: Date.now() } })
46
+ const adapters = model._cached._odata_adapters ??= {}
47
+ const okra = adapters[id] ??= new adapter4 ({ __proto__: srv, _real_srv: srv, model })
48
+ return okra (req, res)
39
49
  }
40
50
  }
51
+ let unique = 0
41
52
  }
@@ -249,7 +249,7 @@ const _getCopiedData = (odataReq, segments, service, target) => {
249
249
  *
250
250
  * @param {string} component - odata-v4 component which processes this request
251
251
  * @param {import('../okra/odata-server/core/OdataRequest')} odataReq - OKRA's req
252
- * @param {import('../../../services/Service')} service - Service, which will process this request
252
+ * @param {import('../../../../common/Service')} service - Service, which will process this request
253
253
  * @returns {object | Array}
254
254
  * @private
255
255
  */
@@ -375,8 +375,7 @@ const _getQueryInfo = (query, service, data, eventType) => {
375
375
 
376
376
  // store original columns before they are polluted by drafts, db and so on
377
377
  const columns = _partialCopyColumns(Array.isArray(query) ? query[0] : query)
378
- const isServiceEntity =
379
- _findEdmNameFor(returnType, namespace) in model.entities(namespace) && !returnType._isContained
378
+ const isServiceEntity = _findEdmNameFor(returnType, namespace) in service.entities && !returnType._isContained
380
379
  const event = _getEvent(eventType, namespace, data, _pathInfo)
381
380
  return Object.assign(_pathInfo, {
382
381
  columns,
@@ -83,16 +83,6 @@ const readAfterWrite = async (req, srv, { operation, isBefore, columns } = { isB
83
83
  const _req = new Request({ query, event: 'READ', _: req._, params: req.params })
84
84
  result = await srv.dispatch(_req)
85
85
  if (result && req.target._isDraftEnabled) removeDraftUUIDIfNecessary(req)(result)
86
- if (result == null && !isBefore && (_isWriteWithResponse(req) || _isDraftAction(req))) {
87
- // > something must be written and no READ error <=> @restrict or static where
88
- _req.reject({
89
- code: 404,
90
- internal: {
91
- reason: `No data found for "READ" after "${req.method}" of "${query._target.name}"`,
92
- source: `"@restrict" or "where" of "${query._target.name}" or underlying entities`
93
- }
94
- })
95
- }
96
86
  } catch (e) {
97
87
  _handleReadError(e, req)
98
88
  result = null
@@ -49,7 +49,9 @@ const validateResourcePath = (odataReq, service) => {
49
49
  // combination GET && @cds.autoexposed && @cds.autoexpose is OK
50
50
  return
51
51
  }
52
- throw getError(405, 'ENTITY_IS_AUTOEXPOSED', [entity.name])
52
+ throw getError(405, entity['@cds.autoexpose'] ? 'ENTITY_IS_AUTOEXPOSE_READONLY' : 'ENTITY_IS_AUTOEXPOSED', [
53
+ entity.name
54
+ ])
53
55
  }
54
56
  }
55
57
  }
@@ -1,274 +1,2 @@
1
- const { DRAFT_COLUMNS_MAP } = require('../../../common/constants/draft')
2
-
3
- const _deepEqual = (val1, val2) => {
4
- if (val1 && typeof val1 === 'object' && val2 && typeof val2 === 'object') {
5
- for (const key in val1) {
6
- if (!_deepEqual(val1[key], val2[key])) return false
7
- }
8
- return true
9
- }
10
- return val1 === val2
11
- }
12
-
13
- const _getCorrespondingEntryWithSameKeys = (source, entry, keys) => {
14
- const idx = _getIdxCorrespondingEntryWithSameKeys(source, entry, keys)
15
- return idx !== -1 ? source[idx] : undefined
16
- }
17
-
18
- const _getIdxCorrespondingEntryWithSameKeys = (source, entry, keys) =>
19
- source.findIndex(sourceEntry => keys.every(key => _deepEqual(sourceEntry[key], entry[key])))
20
-
21
- const _getKeysOfEntity = entity =>
22
- Object.keys(entity.keys).filter(key => !(key in DRAFT_COLUMNS_MAP) && !entity.elements[key].isAssociation)
23
-
24
- const _getCompositionsOfEntity = entity => Object.keys(entity.elements).filter(e => entity.elements[e].isComposition)
25
-
26
- const _createToBeDeletedEntries = (oldEntry, entity, keys, compositions) => {
27
- const toBeDeletedEntry = {
28
- _op: 'delete'
29
- }
30
-
31
- for (const prop in oldEntry) {
32
- if (prop in DRAFT_COLUMNS_MAP) {
33
- continue
34
- }
35
- if (keys.includes(prop)) {
36
- toBeDeletedEntry[prop] = oldEntry[prop]
37
- } else if (compositions.includes(prop) && oldEntry[prop]) {
38
- toBeDeletedEntry[prop] = entity.elements[prop].is2one
39
- ? _createToBeDeletedEntries(
40
- oldEntry[prop],
41
- entity.elements[prop]._target,
42
- _getKeysOfEntity(entity.elements[prop]._target),
43
- _getCompositionsOfEntity(entity.elements[prop]._target)
44
- )
45
- : oldEntry[prop].map(entry =>
46
- _createToBeDeletedEntries(
47
- entry,
48
- entity.elements[prop]._target,
49
- _getKeysOfEntity(entity.elements[prop]._target),
50
- _getCompositionsOfEntity(entity.elements[prop]._target)
51
- )
52
- )
53
- } else {
54
- toBeDeletedEntry._old = toBeDeletedEntry._old || {}
55
- toBeDeletedEntry._old[prop] = oldEntry[prop]
56
- }
57
- }
58
-
59
- return toBeDeletedEntry
60
- }
61
-
62
- const _hasOpDeep = (entry, element) => {
63
- const entryArray = Array.isArray(entry) ? entry : [entry]
64
- for (const entry_ of entryArray) {
65
- if (entry_._op) return true
66
-
67
- if (element && element.isComposition) {
68
- const target = element._target
69
- for (const prop in entry_) {
70
- if (_hasOpDeep(entry_[prop], target.elements[prop])) {
71
- return true
72
- }
73
- }
74
- }
75
- }
76
-
77
- return false
78
- }
79
-
80
- const _addCompositionsToResult = (result, entity, prop, newValue, oldValue, opts) => {
81
- /*
82
- * REVISIT: the current impl results in {} instead of keeping null for compo to one.
83
- * unfortunately, many follow-up errors occur (e.g., prop in null checks) if changed.
84
- */
85
- let composition
86
- if (
87
- newValue[prop] &&
88
- typeof newValue[prop] === 'object' &&
89
- !Array.isArray(newValue[prop]) &&
90
- Object.keys(newValue[prop]).length === 0
91
- ) {
92
- composition = compareJsonDeep(entity.elements[prop]._target, undefined, oldValue && oldValue[prop], opts)
93
- } else {
94
- composition = compareJsonDeep(entity.elements[prop]._target, newValue[prop], oldValue && oldValue[prop], opts)
95
- }
96
- if (composition.some(c => _hasOpDeep(c, entity.elements[prop]))) {
97
- result[prop] = entity.elements[prop].is2one ? composition[0] : composition
98
- }
99
- }
100
-
101
- const _addPrimitiveValuesAndOperatorToResult = (result, prop, newValue, oldValue) => {
102
- result[prop] = newValue[prop]
103
-
104
- if (!result._op) {
105
- result._op = oldValue ? 'update' : 'create'
106
- }
107
-
108
- if (result._op === 'update') {
109
- result._old = result._old || {}
110
- result._old[prop] = oldValue[prop]
111
- }
112
- }
113
-
114
- const _addKeysToResult = (result, prop, newValue, oldValue) => {
115
- result[prop] = newValue[prop]
116
- if (!oldValue) {
117
- result._op = 'create'
118
- }
119
- }
120
-
121
- const _addToBeDeletedEntriesToResult = (results, entity, keys, newValues, oldValues) => {
122
- // add to be deleted entries
123
- for (const oldEntry of oldValues) {
124
- const entry = _getCorrespondingEntryWithSameKeys(newValues, oldEntry, keys)
125
-
126
- if (!entry) {
127
- // prepare to be deleted (deep) entry without manipulating oldData
128
- const toBeDeletedEntry = _createToBeDeletedEntries(oldEntry, entity, keys, _getCompositionsOfEntity(entity))
129
- results.push(toBeDeletedEntry)
130
- }
131
- }
132
- }
133
-
134
- const _normalizeToArray = value => (Array.isArray(value) ? value : value === null ? [] : [value])
135
-
136
- const _isUnManaged = element => {
137
- return element.on && !element._isSelfManaged
138
- }
139
-
140
- const _skip = (entity, prop) => entity.elements[prop]._target._hasPersistenceSkip
141
-
142
- const _skipToOne = (entity, prop) => {
143
- return (
144
- entity.elements[prop] && entity.elements[prop].is2one && _skip(entity, prop) && _isUnManaged(entity.elements[prop])
145
- )
146
- }
147
-
148
- const _skipToMany = (entity, prop) => {
149
- return entity.elements[prop] && entity.elements[prop].is2many && _skip(entity, prop)
150
- }
151
-
152
- // Returns all property names from the new entry and add missing managed elements
153
- const _propertiesAndManaged = (newEntry, entity) => {
154
- return [
155
- ...Object.getOwnPropertyNames(newEntry),
156
- ...Object.keys(entity.elements).filter(
157
- elementName => newEntry[elementName] === undefined && entity.elements[elementName]['@cds.on.update']
158
- )
159
- ]
160
- }
161
-
162
- const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity, opts) => {
163
- // On app-service layer, generated foreign keys are not enumerable,
164
- // include them here too.
165
- for (const prop of _propertiesAndManaged(newEntry, entity)) {
166
- if (keys.includes(prop)) {
167
- _addKeysToResult(result, prop, newEntry, oldEntry)
168
- continue
169
- }
170
-
171
- // if value did not change --> ignored
172
- if (newEntry[prop] === (oldEntry && oldEntry[prop]) || (opts.ignoreDraftColumns && prop in DRAFT_COLUMNS_MAP)) {
173
- continue
174
- }
175
-
176
- if (_skipToMany(entity, prop)) {
177
- continue
178
- }
179
-
180
- if (_skipToOne(entity, prop)) {
181
- continue
182
- }
183
-
184
- if (entity.elements[prop] && entity.elements[prop].isComposition) {
185
- _addCompositionsToResult(result, entity, prop, newEntry, oldEntry, opts)
186
- continue
187
- }
188
-
189
- _addPrimitiveValuesAndOperatorToResult(result, prop, newEntry, oldEntry)
190
- }
191
- }
192
-
193
- const compareJsonDeep = (entity, newValue = [], oldValue = [], opts) => {
194
- const resultsArray = []
195
- const keys = _getKeysOfEntity(entity)
196
-
197
- // normalize input
198
- const newValues = _normalizeToArray(newValue)
199
- const oldValues = _normalizeToArray(oldValue)
200
-
201
- // add to be created and to be updated entries
202
- for (const newEntry of newValues) {
203
- const result = {}
204
- const oldEntry = _getCorrespondingEntryWithSameKeys(oldValues, newEntry, keys)
205
- _iteratePropsInNewEntry(newEntry, keys, result, oldEntry, entity, opts)
206
- resultsArray.push(result)
207
- }
208
-
209
- _addToBeDeletedEntriesToResult(resultsArray, entity, keys, newValues, oldValues)
210
-
211
- return resultsArray
212
- }
213
-
214
- /**
215
- * Compares newValue with oldValues in a deep fashion.
216
- * Output format is newValue with additional administrative properties.
217
- * - "_op" provides info about the CRUD action to perform
218
- * - "_old" provides info about the current DB state
219
- *
220
- * Unchanged values are not part of the result.
221
- *
222
- * Output format is:
223
- * {
224
- * _op: 'update',
225
- * _old: { orderedAt: 'DE' },
226
- * ID: 1,
227
- * orderedAt: 'EN',
228
- * items: [
229
- * {
230
- * _op: 'update',
231
- * _old: { amount: 7 },
232
- * ID: 7,
233
- * amount: 8
234
- * },
235
- * {
236
- * _op: 'create',
237
- * ID: 8,
238
- * amount: 8
239
- * },
240
- * {
241
- * _op: 'delete',
242
- * _old: {
243
- * amount: 6
244
- * },
245
- * ID: 6
246
- * }
247
- * ]
248
- * }
249
- *
250
- *
251
- * If there is no change in an UPDATE, result is an object containing only the keys of the entity.
252
- *
253
- * @example
254
- * compareJson(csnEntity, [{ID: 1, col1: 'A'}], [{ID: 1, col1: 'B'}])
255
- *
256
- * @param oldValue
257
- * @param {object} entity
258
- * @param {Array | object} newValue
259
- * @param {Array} oldValues
260
- *
261
- * @returns {Array}
262
- */
263
- const compareJson = (newValue, oldValue, entity, opts = {}) => {
264
- const options = Object.assign({ ignoreDraftColumns: false }, opts)
265
- const result = compareJsonDeep(entity, newValue, oldValue, options)
266
-
267
- // in case of batch insert, result is an array
268
- // in all other cases it is an array with just one entry
269
- return Array.isArray(newValue) ? result : result[0]
270
- }
271
-
272
- module.exports = {
273
- compareJson
274
- }
1
+ // REVISIT: remove with cds^8
2
+ module.exports = require('../../../common/utils/compareJson')