@sap/cds 7.8.1 → 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 (137) hide show
  1. package/CHANGELOG.md +44 -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/classes.js +2 -2
  35. package/lib/linked/entities.js +10 -0
  36. package/lib/linked/models.js +1 -1
  37. package/lib/plugins.js +1 -1
  38. package/lib/ql/INSERT.js +17 -3
  39. package/lib/ql/Query.js +4 -0
  40. package/lib/ql/infer.js +1 -1
  41. package/lib/req/request.js +1 -1
  42. package/lib/srv/cds-serve.js +1 -0
  43. package/lib/srv/middlewares/cds-context.js +1 -1
  44. package/lib/srv/protocols/odata-v4.js +5 -6
  45. package/lib/srv/srv-models.js +9 -2
  46. package/lib/utils/cds-test.js +2 -0
  47. package/lib/utils/cds-utils.js +9 -4
  48. package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
  49. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
  50. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
  51. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
  52. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
  53. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
  54. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
  55. package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
  56. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
  57. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
  58. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
  59. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
  60. package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
  61. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
  62. package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
  63. package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
  64. package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
  65. package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
  66. package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
  67. package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
  68. package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
  69. package/libx/_runtime/common/generic/auth/index.js +2 -0
  70. package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
  71. package/libx/_runtime/common/generic/auth/restrict.js +6 -5
  72. package/libx/_runtime/common/generic/auth/utils.js +1 -1
  73. package/libx/_runtime/common/generic/crud.js +5 -8
  74. package/libx/_runtime/common/generic/etag.js +8 -6
  75. package/libx/_runtime/common/generic/sorting.js +2 -2
  76. package/libx/_runtime/common/i18n/messages.properties +1 -0
  77. package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
  78. package/libx/_runtime/common/utils/compareJson.js +274 -0
  79. package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
  80. package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
  81. package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
  82. package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
  83. package/libx/_runtime/common/utils/resolveView.js +0 -16
  84. package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
  85. package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
  86. package/libx/_runtime/common/utils/streamProp.js +9 -2
  87. package/libx/_runtime/common/utils/ucsn.js +1 -1
  88. package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
  89. package/libx/_runtime/db/generic/rewrite.js +7 -13
  90. package/libx/_runtime/fiori/generic/activate.js +1 -1
  91. package/libx/_runtime/fiori/generic/edit.js +1 -1
  92. package/libx/_runtime/fiori/generic/prepare.js +1 -1
  93. package/libx/_runtime/fiori/lean-draft.js +151 -46
  94. package/libx/_runtime/fiori/utils/handler.js +1 -1
  95. package/libx/_runtime/hana/execute.js +6 -2
  96. package/libx/_runtime/hana/search2cqn4sql.js +1 -1
  97. package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
  98. package/libx/_runtime/messaging/event-broker.js +212 -0
  99. package/libx/_runtime/remote/Service.js +9 -32
  100. package/libx/_runtime/remote/utils/client.js +13 -21
  101. package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
  102. package/libx/_runtime/sqlite/execute.js +8 -3
  103. package/libx/_runtime/ucl/Service.js +259 -0
  104. package/libx/common/assert/index.js +6 -11
  105. package/libx/common/assert/validation.js +6 -1
  106. package/libx/odata/index.js +47 -25
  107. package/libx/odata/middleware/batch.js +8 -7
  108. package/libx/odata/middleware/create.js +42 -16
  109. package/libx/odata/middleware/delete.js +18 -11
  110. package/libx/odata/middleware/metadata.js +15 -14
  111. package/libx/odata/middleware/operation.js +30 -40
  112. package/libx/odata/middleware/parse.js +2 -3
  113. package/libx/odata/middleware/read.js +59 -52
  114. package/libx/odata/middleware/service-document.js +7 -7
  115. package/libx/odata/middleware/stream.js +26 -24
  116. package/libx/odata/middleware/update.js +53 -92
  117. package/libx/odata/parse/afterburner.js +45 -47
  118. package/libx/odata/parse/grammar.peggy +3 -3
  119. package/libx/odata/parse/multipartToJson.js +10 -22
  120. package/libx/odata/parse/parser.js +1 -1
  121. package/libx/odata/utils/etag.js +13 -0
  122. package/libx/odata/utils/handler.js +120 -0
  123. package/libx/odata/utils/index.js +15 -2
  124. package/libx/odata/utils/metaInfo.js +410 -0
  125. package/libx/odata/utils/path.js +5 -2
  126. package/libx/odata/utils/readAfterWrite.js +23 -0
  127. package/libx/odata/utils/result.js +4 -5
  128. package/libx/rest/RestAdapter.js +4 -13
  129. package/libx/rest/middleware/parse.js +40 -7
  130. package/package.json +1 -1
  131. package/server.js +2 -1
  132. package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
  133. package/libx/_runtime/common/utils/thenable.js +0 -51
  134. package/libx/_runtime/rest/service.js +0 -2
  135. package/libx/odata/parse/parseToCqn.js +0 -39
  136. package/libx/rest/middleware/input.js +0 -54
  137. package/libx/rest/middleware/payload.js +0 -13
@@ -53,6 +53,7 @@ class Test extends require('./axios') {
53
53
 
54
54
  // launch cds server...
55
55
  before (async ()=>{
56
+ process.env.cds_test_temp = cds.utils.path.resolve(cds.root,'_out',''+process.pid)
56
57
  if (!args.includes('--port')) args.push('--port', '0')
57
58
  let { server, url } = await cds.exec(...args)
58
59
  this.server = server
@@ -66,6 +67,7 @@ class Test extends require('./axios') {
66
67
  // delete cds.services
67
68
  // delete cds.plugins
68
69
  // delete cds.env
70
+ return cds.utils.rimraf(process.env.cds_test_temp)
69
71
  })
70
72
 
71
73
  return this
@@ -1,7 +1,7 @@
1
1
  const cwd = process.env._original_cwd || process.cwd()
2
2
  const cds = require('../index')
3
3
 
4
- const ux = module.exports = exports = new class {
4
+ module.exports = exports = new class {
5
5
  get inflect() { return super.inflect = require('./inflect') }
6
6
  get inspect() { return super.inspect = require('util').inspect }
7
7
  get uuid() { return super.uuid = require('crypto').randomUUID }
@@ -11,7 +11,7 @@ const ux = module.exports = exports = new class {
11
11
  }
12
12
 
13
13
  const path = exports.path = require('path'), { dirname, extname, join, resolve, relative } = path
14
- const fs = exports.fs = Object.assign (ux,require('fs')) //> for compatibility
14
+ const fs = exports.fs = Object.assign (exports,require('fs')) //> for compatibility
15
15
 
16
16
 
17
17
  /**
@@ -98,7 +98,12 @@ exports.readdir = async function (x) {
98
98
  exports.read = async function read (file, _encoding) {
99
99
  const f = resolve (cds.root,file)
100
100
  const src = await fs.promises.readFile (f, _encoding !== 'json' && _encoding || 'utf8')
101
- return _encoding === 'json' || !_encoding && f.endsWith('.json') ? JSON.parse(src) : src
101
+ if (_encoding === 'json' || !_encoding && f.endsWith('.json')) try {
102
+ return JSON.parse(src)
103
+ } catch(e) {
104
+ throw new Error (`Failed to parse JSON in ${f}: ${e.message}`)
105
+ }
106
+ else return src
102
107
  }
103
108
 
104
109
  exports.write = function write (file, data, o) {
@@ -206,7 +211,7 @@ exports.deprecated = (fn, { kind = 'Method', old = fn.name+'()', use } = {}) =>
206
211
  '\nDEPRECATED:', old, '\n',
207
212
  '\n ', (kind ? `${kind} ${old}` : old), 'is deprecated and will be removed in upcoming releases!',
208
213
  use ? `\n => Please use ${use} instead.` : '', '\n',
209
- o.stack.replace(/^Error\n\s*at.*\n/,'\n'), '\n',
214
+ o.stack.replace(/^Error:\s*at.*\n/,'\n'), '\n',
210
215
  '\n------------------------------------------------------------------------------\n',
211
216
  reset
212
217
  )
@@ -95,7 +95,7 @@ function _handleStreamProperties(target, query, model) {
95
95
  * @extends cds.Request
96
96
  *
97
97
  * @param {string} type - The OData request type (a.k.a. "Component")
98
- * @param {import('../../services/Service')} service - The underlying CAP service
98
+ * @param {import('../../../common/Service')} service - The underlying CAP service
99
99
  * @param {import('./okra/odata-server/core/OdataRequest')} odataReq - OKRA's req
100
100
  * @param {import('./okra/odata-server/core/OdataResponse')} odataRes - OKRA's res
101
101
  */
@@ -29,7 +29,7 @@ const _postProcess = async (req, odataReq, odataRes, tx, result) => {
29
29
  /**
30
30
  * The handler that will be registered with odata-v4.
31
31
  *
32
- * @param {import('../../../services/Service')} service
32
+ * @param {import('../../../../common/Service')} service
33
33
  * @returns {function}
34
34
  */
35
35
  const action = service => {
@@ -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
  }