@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
@@ -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')
@@ -1,7 +1,8 @@
1
- const cds = require('../../cds')
1
+ const cds = require('../cds')
2
2
 
3
- const { resolveView, restoreLink, findQueryTarget } = require('../../common/utils/resolveView')
4
- const { postProcess } = require('../../common/utils/postProcessing')
3
+ const { resolveView, findQueryTarget } = require('./utils/resolveView')
4
+ const postProcess = require('./utils/postProcess')
5
+ const ensureIEEE754 = require('./utils/ensureIEEE754')
5
6
 
6
7
  const _isSimpleCqnQuery = q => typeof q === 'object' && q !== null && !Array.isArray(q) && Object.keys(q).length > 0
7
8
 
@@ -28,27 +29,27 @@ class ApplicationService extends cds.Service {
28
29
  }
29
30
 
30
31
  static handle_authorization() {
31
- require('../../common/generic/auth').call(this)
32
+ require('./generic/auth').call(this)
32
33
  }
33
34
 
34
35
  static handle_etags() {
35
- require('../../common/generic/etag').call(this)
36
+ require('./generic/etag').call(this)
36
37
  }
37
38
 
38
39
  static handle_validations() {
39
- require('../../common/generic/input').call(this)
40
+ require('./generic/input').call(this)
40
41
  }
41
42
 
42
43
  static handle_stream_property() {
43
- require('../../common/generic/stream').call(this)
44
+ require('./generic/stream').call(this)
44
45
  }
45
46
 
46
47
  static handle_puts() {
47
- require('../../common/generic/put').call(this)
48
+ require('./generic/put').call(this)
48
49
  }
49
50
 
50
51
  static handle_temporal_data() {
51
- require('../../common/generic/temporal').call(this)
52
+ require('./generic/temporal').call(this)
52
53
  }
53
54
 
54
55
  static handle_localized_data() {
@@ -60,50 +61,59 @@ class ApplicationService extends cds.Service {
60
61
  }
61
62
 
62
63
  static handle_paging() {
63
- require('../../common/generic/paging').call(this) // > paging must be executed before sorting
64
- require('../../common/generic/sorting').call(this)
64
+ require('./generic/paging').call(this) // > paging must be executed before sorting
65
+ require('./generic/sorting').call(this)
65
66
  }
66
67
 
67
68
  static handle_code_ext() {
68
- if (cds.env.requires.extensibility?.code) require('../../common/code-ext/handlers').call(this)
69
+ if (cds.env.requires.extensibility?.code) require('./code-ext/handlers').call(this)
69
70
  }
70
71
 
71
72
  static handle_fiori() {
72
- require('../../fiori/draft').impl.call(this)
73
+ require('../fiori/draft').impl.call(this)
73
74
  }
74
75
 
75
76
  static handle_crud() {
76
- require('../../common/generic/crud').impl.call(this)
77
+ require('./generic/crud').impl.call(this)
77
78
  }
78
79
 
79
80
  // Overload .handle in order to resolve projections up to a definition that is known by the remote service instance.
80
81
  // Result is post processed according to the inverse projection in order to reflect the correct result of the original query.
81
82
  async handle(req) {
82
- // compat mode
83
- if (req._resolved) return super.handle(req)
84
- if (req.target && req.target.name && req.target.name.startsWith(this.namespace + '.')) return super.handle(req)
83
+ let result,
84
+ target = req.target
85
85
 
86
+ if (req._resolved || req.target?.name?.startsWith(this.namespace + '.')) {
87
+ result = await super.handle(req)
88
+ }
86
89
  // req.query can be:
87
90
  // - empty object in case of unbound action/function
88
91
  // - undefined/null in case of plain string queries
89
- if (this.model && _isSimpleCqnQuery(req.query)) {
92
+ else if (this.model && _isSimpleCqnQuery(req.query)) {
90
93
  const q = resolveView(req.query, this.model, this)
91
- const t = findQueryTarget(q) || req.target // REVISIT: why is req.target not correct?
94
+ target = findQueryTarget(q) || req.target // REVISIT: why is req.target not correct?
92
95
 
93
- // compat
94
- restoreLink(req)
95
- if (req.query.SELECT && req.query.SELECT._4odata) {
96
- Object.defineProperty(q.SELECT, '_4odata', { value: req.query.SELECT._4odata })
97
- }
96
+ // REVISIT: get rid of this
97
+ if (req.query.SELECT?._4odata) Object.defineProperty(q.SELECT, '_4odata', { value: true })
98
98
 
99
99
  // REVISIT: We need to provide target explicitly because it's cached already within ensure_target
100
- const newReq = new cds.Request({ query: q, target: t, _resolved: true })
101
- const result = await super.dispatch(newReq)
100
+ const _req = new cds.Request({ query: q, target, _resolved: true })
101
+ result = await super.dispatch(_req)
102
+ result = postProcess(q, result, this)
103
+ }
104
+ // default to super.handle()
105
+ else {
106
+ result = await super.handle(req)
107
+ }
102
108
 
103
- return postProcess(q, result, this)
109
+ // REVISIT: when do?
110
+ // const ieee754 = req.headers.accept?.match(/IEEE754Compatible=(\w+)/i)?.[1]
111
+ // if (cds.env.features.ensure_ieee754 || (cds.env.features.odata_new_adapter && ieee754 === 'true')) {
112
+ if (cds.env.features.ensure_ieee754) {
113
+ ensureIEEE754(target, this, result)
104
114
  }
105
115
 
106
- return super.handle(req)
116
+ return result
107
117
  }
108
118
 
109
119
  /**
@@ -125,7 +135,7 @@ class ApplicationService extends cds.Service {
125
135
  }
126
136
 
127
137
  // NOTE: getRestrictions is VERY INOFFICIAL!!!
128
- const { getRestrictions } = require('../../common/generic/auth/restrictions')
138
+ const { getRestrictions } = require('./generic/auth/restrictions')
129
139
  ApplicationService.prototype.getRestrictions = function (..._) {
130
140
  return getRestrictions.call(this, ..._)
131
141
  }
@@ -0,0 +1,41 @@
1
+ const cds = require('../../../../../lib')
2
+
3
+ function noah_handler(req) {
4
+ const target = req.target
5
+ if (!target) return
6
+
7
+ // direct changes to draft administrative data is forbidden
8
+ if (target.name.match(/\.DraftAdministrativeData$/) && req.event !== 'READ') {
9
+ req.reject(405, 'ENTITY_IS_AUTOEXPOSE_READONLY', [target.name])
10
+ }
11
+
12
+ // if draft enabled, allow direct access
13
+ if (target._isDraftEnabled || !target['@cds.autoexposed']) return
14
+
15
+ if (target['@cds.autoexpose']) {
16
+ if (req.event === 'READ') return //> allow read for value help requests
17
+ req.reject(405, 'ENTITY_IS_AUTOEXPOSE_READONLY', [target.name])
18
+ }
19
+
20
+ // here, we are autoexposed (i.e., a composition)
21
+ // -> reject all direct changes (i.e., only via navigation is allowed)
22
+ if (req.subject?.ref.length === 1) req.reject(405, 'ENTITY_IS_AUTOEXPOSED', [target.name])
23
+ }
24
+
25
+ const _isAutoexposed = entity => {
26
+ if (!entity) return false
27
+ return (entity['@cds.autoexpose'] && entity['@cds.autoexposed']) || entity.name.match(/\.DraftAdministrativeData$/)
28
+ }
29
+
30
+ // REVISIT: To be deleted after okra is removed
31
+ function okra_handler(req) {
32
+ const isAutoexposed = _isAutoexposed(req.target)
33
+ if (isAutoexposed && req.event !== 'READ') {
34
+ req.reject(405, 'ENTITY_IS_AUTOEXPOSE_READONLY', [req.target.name])
35
+ }
36
+ }
37
+
38
+ const handler = cds.env.features.odata_new_adapter ? noah_handler : okra_handler
39
+ handler._initial = true
40
+
41
+ module.exports = handler
@@ -4,6 +4,7 @@ const requiresHandler = require('./requires')
4
4
  const readOnlyHandler = require('./readOnly')
5
5
  const insertOnlyHandler = require('./insertOnly')
6
6
  const capabilitiesHandler = require('./capabilities')
7
+ const autoexposeHandler = require('./autoexpose')
7
8
  const restrictHandler = require('./restrict')
8
9
  const restrictExpandHandler = require('./expand')
9
10
 
@@ -19,6 +20,7 @@ module.exports = cds.service.impl(function authorization() {
19
20
  this.before('*', readOnlyHandler)
20
21
  this.before('*', insertOnlyHandler)
21
22
  this.before('*', capabilitiesHandler)
23
+ this.before('*', autoexposeHandler)
22
24
 
23
25
  /*
24
26
  * @restrict
@@ -2,18 +2,7 @@ const cds = require('../../../cds')
2
2
  const { getAuthRelevantEntity } = require('./utils')
3
3
  const { WRITE_EVENTS } = require('./constants')
4
4
 
5
- const _isAutoexposed = entity => {
6
- if (!entity) return false
7
- return (entity['@cds.autoexpose'] && entity['@cds.autoexposed']) || entity.name.match(/\.DraftAdministrativeData$/)
8
- }
9
-
10
5
  function handler(req) {
11
- // autoexposed
12
- const isAutoexposed = _isAutoexposed(req.target)
13
- if (isAutoexposed && req.event !== 'READ') {
14
- req.reject(405, 'ENTITY_IS_AUTOEXPOSED', [req.target.name])
15
- }
16
-
17
6
  // @read-only
18
7
  let entity = getAuthRelevantEntity(req, this.model, ['@readonly'])
19
8
  if (cds.env.fiori.lean_draft) entity = entity?.actives || entity
@@ -144,8 +144,12 @@ const _addRestrictionsToRead = async (req, model, resolvedApplicables) => {
144
144
  req.query._draftRestrictions = resolvedApplicables
145
145
  return
146
146
  }
147
- // in case of $apply take a query from sub SELECT//
148
- const query = req.query.SELECT.from.SELECT?.from?.ref ? req.query.SELECT.from : req.query
147
+
148
+ // in case of $apply take a query from sub SELECT
149
+ let query = req.query
150
+ while (query.SELECT.from.SELECT) {
151
+ query = query.SELECT.from
152
+ }
149
153
 
150
154
  query.SELECT.from.ref = _addWheresToRef(query.SELECT.from.ref, model, resolvedApplicables)
151
155
 
@@ -278,9 +282,6 @@ async function handler(req) {
278
282
  if (restrictedCount < unrestrictedCount) {
279
283
  reject(req, getRejectReason(req, '@restrict', definition, restrictedCount, unrestrictedCount))
280
284
  }
281
-
282
- // for minor optimization in generic crud handler
283
- req._authChecked = true
284
285
  }
285
286
 
286
287
  handler._initial = true
@@ -11,7 +11,7 @@ const reject = (req, reason = null) => {
11
11
  // REVISIT: challenges handling should be done in protocol adapter (i.e., express error middleware)
12
12
  // REVISIT: improve `req.http.req` check if this is an HTTP request
13
13
  if (req.http?.res && req.user._challenges && req.user._challenges.length > 0) {
14
- req.http.res.set('WWW-Authenticate', req.user._challenges.join(';'))
14
+ req.http.res.set('www-authenticate', req.user._challenges.join(';'))
15
15
  }
16
16
 
17
17
  return req.reject(401)
@@ -2,7 +2,7 @@ const cds = require('../../cds')
2
2
  const { SELECT } = cds.ql
3
3
 
4
4
  const { deepCopyArray } = require('../utils/copy')
5
- const { getColumns } = require('../../cds-services/services/utils/columns')
5
+ const { getColumns } = require('../utils/columns')
6
6
  const { enhanceStreamResult } = require('../utils/stream')
7
7
  const getError = require('../error')
8
8
 
@@ -73,16 +73,15 @@ exports.impl = cds.service.impl(function () {
73
73
  result = await cds.tx(req).run(req.query, req.data)
74
74
  }
75
75
 
76
- // regarding etag validation: we do not want to execute an additional select to distinguish between 412 and 404
77
-
78
76
  if (req.event === 'READ') {
77
+ // do not execute additional select to distinguish between 412 and 404
78
+ if (result == null && req._etagValidationType === 'if-match') req.reject(412)
79
+
79
80
  if ((result == null || result.length === 0) && pathExistsQuery) {
80
81
  const res = await pathExistsQuery
81
82
  if (res.length === 0) req.reject(404)
82
83
  }
83
84
 
84
- if (result == null && req._etagValidationType === 'if-match') req.reject(412)
85
-
86
85
  if (cds.env.features.stream_compat) {
87
86
  if (result !== undefined && req.query?._streaming && (result === null || result.pipe)) {
88
87
  return { value: result }
@@ -97,9 +96,7 @@ exports.impl = cds.service.impl(function () {
97
96
  return result
98
97
  }
99
98
 
100
- // case: no authorization check and payload more than just keys but no changes
101
- // -> affected rows === 0 -> no change or not exists?
102
- if (req.event === 'UPDATE' && result === 0 && !req._authChecked) {
99
+ if (req.event === 'UPDATE' && result === 0) {
103
100
  if (req._etagValidationType) req.reject(412)
104
101
  if (await _targetEntityDoesNotExist(req)) req.reject(404) // REVISIT: add a reasonable error message
105
102
  }
@@ -45,7 +45,7 @@ const _getValidationStmt = (ifMatchEtags, ifNoneMatchEtags, req, model) => {
45
45
  if (ifMatchEtags.length === 1) cond.push('=', { val: ifMatchEtags[0] })
46
46
  else cond.push('in', { list: ifMatchEtags.map(val => ({ val })) })
47
47
  } else {
48
- if (ifNoneMatchEtags.includes('*')) return false
48
+ if (req.event !== 'READ' && ifNoneMatchEtags.includes('*')) return false
49
49
  // if a malformed time value is present, it cannot match -> precondition true
50
50
  if (
51
51
  (etagElement.type === 'cds.Timestamp' || etagElement.type === 'cds.DateTime') &&
@@ -54,11 +54,13 @@ const _getValidationStmt = (ifMatchEtags, ifNoneMatchEtags, req, model) => {
54
54
  return true
55
55
  }
56
56
 
57
- cond.push({ ref: alias ? [alias, etagElement.name] : [etagElement.name] })
58
- if (ifNoneMatchEtags.length === 1) cond.push('!=', { val: ifNoneMatchEtags[0] })
59
- else cond.push('not', 'in', { list: ifNoneMatchEtags.map(val => ({ val })) })
57
+ if (req.event !== 'READ') {
58
+ cond.push({ ref: alias ? [alias, etagElement.name] : [etagElement.name] })
59
+ if (ifNoneMatchEtags.length === 1) cond.push('!=', { val: ifNoneMatchEtags[0] })
60
+ else cond.push('not', 'in', { list: ifNoneMatchEtags.map(val => ({ val })) })
61
+ }
60
62
  }
61
- if (!cond.length) return
63
+ if (!cond.length) return true
62
64
 
63
65
  return select.where(cond)
64
66
  }
@@ -77,7 +79,7 @@ const commonGenericValidateETag = async function (req) {
77
79
  if (req.protocol !== 'odata-v4') return
78
80
 
79
81
  // automatically add etag columns if not already there
80
- if (req.query.SELECT && !req.query._streaming) addEtagColumns(req.query.SELECT.columns, req.target)
82
+ if (req.query.SELECT) addEtagColumns(req.query.SELECT.columns, req.target)
81
83
 
82
84
  // querying a collection?
83
85
  if (req.event === 'READ' && !req.query.SELECT.one) return
@@ -6,11 +6,11 @@ const _getStaticOrders = req => {
6
6
  const defaultOrders = entity['@cds.default.order'] || entity['@odata.default.order'] || []
7
7
 
8
8
  if (entity['@cds.default.order']) {
9
- // Remove with cds 8
9
+ // Remove with cds^8
10
10
  cds.utils.deprecated({ kind: 'Annotation', old: '@cds.default.order of entity ' + entity.name })
11
11
  }
12
12
  if (entity['@odata.default.order']) {
13
- // Remove with cds 8
13
+ // Remove with cds^8
14
14
  cds.utils.deprecated({ kind: 'Annotation', old: '@odata.default.order of entity ' + entity.name })
15
15
  }
16
16
 
@@ -68,6 +68,7 @@ ENTITY_IS_READ_ONLY=Entity "{0}" is read-only
68
68
  ENTITY_IS_NOT_CRUD=Entity "{0}" is not {1}
69
69
  ENTITY_IS_NOT_CRUD_VIA_NAVIGATION=Entity "{0}" is not {1} via navigation "{2}"
70
70
  ENTITY_IS_AUTOEXPOSED=Entity "{0}" is not explicitly exposed as part of the service
71
+ ENTITY_IS_AUTOEXPOSE_READONLY=Entity "{0}" is explicitly exposed as readonly
71
72
  EXPAND_IS_RESTRICTED=Navigation property "{0}" is not allowed for expand operation
72
73
 
73
74
  # rest protocol adapter
@@ -1,6 +1,6 @@
1
- const cds = require('../../../cds')
1
+ const cds = require('../../cds')
2
2
 
3
- const { DRAFT_COLUMNS_UNION_MAP } = require('../../../common/constants/draft')
3
+ const { DRAFT_COLUMNS_UNION_MAP } = require('../constants/draft')
4
4
  const DEFAULT_SEARCHABLE_TYPE = 'cds.String'
5
5
 
6
6
  // REVISIT: Can we combine that with db/utils/columns.js?
@@ -113,7 +113,7 @@ const _getSearchableColumns = entity => {
113
113
  const defaultSearchFilteredColumns = searchableColumns.filter(column => column[defaultSearchElementTerm])
114
114
 
115
115
  if (defaultSearchFilteredColumns.length > 0) {
116
- // Remove with cds 8
116
+ // Remove with cds^8
117
117
  cds.utils.deprecated({
118
118
  kind: 'Annotation',
119
119
  old: '@Search.defaultSearchElement in entity ' + entity.name,
@@ -126,7 +126,7 @@ const _getSearchableColumns = entity => {
126
126
  }
127
127
 
128
128
  /**
129
- * @returns {import('../../../types/api').ColumnRefs}
129
+ * @returns {import('../../types/api').ColumnRefs}
130
130
  */
131
131
  const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, alias) => {
132
132
  let toBeSearched = []