@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.
- package/CHANGELOG.md +44 -0
- package/_i18n/i18n_ar.properties +3 -0
- package/_i18n/i18n_cs.properties +3 -0
- package/_i18n/i18n_da.properties +3 -0
- package/_i18n/i18n_es_MX.properties +3 -0
- package/_i18n/i18n_fi.properties +3 -0
- package/_i18n/i18n_hu.properties +6 -0
- package/_i18n/i18n_ko.properties +3 -0
- package/_i18n/i18n_ms.properties +3 -0
- package/_i18n/i18n_nl.properties +3 -0
- package/_i18n/i18n_no.properties +3 -0
- package/_i18n/i18n_ro.properties +3 -0
- package/_i18n/i18n_sv.properties +3 -0
- package/_i18n/i18n_th.properties +3 -0
- package/_i18n/i18n_tr.properties +6 -0
- package/_i18n/i18n_zh_TW.properties +3 -0
- package/bin/serve.js +5 -5
- package/lib/auth/basic-auth.js +1 -1
- package/lib/compile/cdsc.js +33 -6
- package/lib/compile/etc/_localized.js +14 -7
- package/lib/compile/for/lean_drafts.js +9 -0
- package/lib/compile/to/edm-files.js +116 -0
- package/lib/compile/to/edm.js +8 -1
- package/lib/compile/to/hdbtabledata.js +3 -3
- package/lib/compile/to/sql.js +4 -2
- package/lib/compile/to/yaml.js +22 -21
- package/lib/dbs/cds-deploy.js +5 -6
- package/lib/env/cds-env.js +7 -0
- package/lib/env/cds-requires.js +20 -1
- package/lib/env/defaults.js +21 -5
- package/lib/env/schemas/cds-package.js +1 -1
- package/lib/env/schemas/cds-rc.js +85 -4
- package/lib/index.js +1 -1
- package/lib/linked/classes.js +2 -2
- package/lib/linked/entities.js +10 -0
- package/lib/linked/models.js +1 -1
- package/lib/plugins.js +1 -1
- package/lib/ql/INSERT.js +17 -3
- package/lib/ql/Query.js +4 -0
- package/lib/ql/infer.js +1 -1
- package/lib/req/request.js +1 -1
- package/lib/srv/cds-serve.js +1 -0
- package/lib/srv/middlewares/cds-context.js +1 -1
- package/lib/srv/protocols/odata-v4.js +5 -6
- package/lib/srv/srv-models.js +9 -2
- package/lib/utils/cds-test.js +2 -0
- package/lib/utils/cds-utils.js +9 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/ODataRequest.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/action.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/create.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/delete.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/metadata.js +3 -6
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/read.js +22 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/request.js +4 -4
- package/libx/_runtime/cds-services/adapter/odata-v4/handlers/update.js +4 -3
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/ExpressionToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/applyToCQN.js +4 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/expandToCQN.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/index.js +38 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/odata-to-cqn/readToCQN.js +2 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +32 -21
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/data.js +1 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/metaInfo.js +1 -2
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/readAfterWrite.js +0 -10
- package/libx/_runtime/cds-services/adapter/odata-v4/utils/request.js +3 -1
- package/libx/_runtime/cds-services/services/utils/compareJson.js +2 -274
- package/libx/_runtime/{cds-services/services → common}/Service.js +39 -29
- package/libx/_runtime/common/generic/auth/autoexpose.js +41 -0
- package/libx/_runtime/common/generic/auth/index.js +2 -0
- package/libx/_runtime/common/generic/auth/readOnly.js +0 -11
- package/libx/_runtime/common/generic/auth/restrict.js +6 -5
- package/libx/_runtime/common/generic/auth/utils.js +1 -1
- package/libx/_runtime/common/generic/crud.js +5 -8
- package/libx/_runtime/common/generic/etag.js +8 -6
- package/libx/_runtime/common/generic/sorting.js +2 -2
- package/libx/_runtime/common/i18n/messages.properties +1 -0
- package/libx/_runtime/{cds-services/services → common}/utils/columns.js +4 -4
- package/libx/_runtime/common/utils/compareJson.js +274 -0
- package/libx/_runtime/common/utils/cqn2cqn4sql.js +1 -1
- package/libx/_runtime/{cds-services/services → common}/utils/differ.js +8 -8
- package/libx/_runtime/common/utils/ensureIEEE754.js +29 -0
- package/libx/_runtime/common/utils/{postProcessing.js → postProcess.js} +1 -3
- package/libx/_runtime/common/utils/resolveView.js +0 -16
- package/libx/_runtime/common/utils/rewriteAsterisks.js +1 -1
- package/libx/_runtime/common/utils/search2cqn4sql.js +1 -1
- package/libx/_runtime/common/utils/streamProp.js +9 -2
- package/libx/_runtime/common/utils/ucsn.js +1 -1
- package/libx/_runtime/db/expand/expandCQNToJoin.js +5 -3
- package/libx/_runtime/db/generic/rewrite.js +7 -13
- package/libx/_runtime/fiori/generic/activate.js +1 -1
- package/libx/_runtime/fiori/generic/edit.js +1 -1
- package/libx/_runtime/fiori/generic/prepare.js +1 -1
- package/libx/_runtime/fiori/lean-draft.js +151 -46
- package/libx/_runtime/fiori/utils/handler.js +1 -1
- package/libx/_runtime/hana/execute.js +6 -2
- package/libx/_runtime/hana/search2cqn4sql.js +1 -1
- package/libx/_runtime/messaging/enterprise-messaging-utils/registerEndpoints.js +1 -2
- package/libx/_runtime/messaging/event-broker.js +212 -0
- package/libx/_runtime/remote/Service.js +9 -32
- package/libx/_runtime/remote/utils/client.js +13 -21
- package/libx/_runtime/sqlite/convertAssocToOneManaged.js +7 -1
- package/libx/_runtime/sqlite/execute.js +8 -3
- package/libx/_runtime/ucl/Service.js +259 -0
- package/libx/common/assert/index.js +6 -11
- package/libx/common/assert/validation.js +6 -1
- package/libx/odata/index.js +47 -25
- package/libx/odata/middleware/batch.js +8 -7
- package/libx/odata/middleware/create.js +42 -16
- package/libx/odata/middleware/delete.js +18 -11
- package/libx/odata/middleware/metadata.js +15 -14
- package/libx/odata/middleware/operation.js +30 -40
- package/libx/odata/middleware/parse.js +2 -3
- package/libx/odata/middleware/read.js +59 -52
- package/libx/odata/middleware/service-document.js +7 -7
- package/libx/odata/middleware/stream.js +26 -24
- package/libx/odata/middleware/update.js +53 -92
- package/libx/odata/parse/afterburner.js +45 -47
- package/libx/odata/parse/grammar.peggy +3 -3
- package/libx/odata/parse/multipartToJson.js +10 -22
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/etag.js +13 -0
- package/libx/odata/utils/handler.js +120 -0
- package/libx/odata/utils/index.js +15 -2
- package/libx/odata/utils/metaInfo.js +410 -0
- package/libx/odata/utils/path.js +5 -2
- package/libx/odata/utils/readAfterWrite.js +23 -0
- package/libx/odata/utils/result.js +4 -5
- package/libx/rest/RestAdapter.js +4 -13
- package/libx/rest/middleware/parse.js +40 -7
- package/package.json +1 -1
- package/server.js +2 -1
- package/libx/_runtime/cds-services/util/dataProcessUtils.js +0 -93
- package/libx/_runtime/common/utils/thenable.js +0 -51
- package/libx/_runtime/rest/service.js +0 -2
- package/libx/odata/parse/parseToCqn.js +0 -39
- package/libx/rest/middleware/input.js +0 -54
- package/libx/rest/middleware/payload.js +0 -13
|
@@ -1,274 +1,2 @@
|
|
|
1
|
-
|
|
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('
|
|
1
|
+
const cds = require('../cds')
|
|
2
2
|
|
|
3
|
-
const { resolveView,
|
|
4
|
-
const
|
|
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('
|
|
32
|
+
require('./generic/auth').call(this)
|
|
32
33
|
}
|
|
33
34
|
|
|
34
35
|
static handle_etags() {
|
|
35
|
-
require('
|
|
36
|
+
require('./generic/etag').call(this)
|
|
36
37
|
}
|
|
37
38
|
|
|
38
39
|
static handle_validations() {
|
|
39
|
-
require('
|
|
40
|
+
require('./generic/input').call(this)
|
|
40
41
|
}
|
|
41
42
|
|
|
42
43
|
static handle_stream_property() {
|
|
43
|
-
require('
|
|
44
|
+
require('./generic/stream').call(this)
|
|
44
45
|
}
|
|
45
46
|
|
|
46
47
|
static handle_puts() {
|
|
47
|
-
require('
|
|
48
|
+
require('./generic/put').call(this)
|
|
48
49
|
}
|
|
49
50
|
|
|
50
51
|
static handle_temporal_data() {
|
|
51
|
-
require('
|
|
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('
|
|
64
|
-
require('
|
|
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('
|
|
69
|
+
if (cds.env.requires.extensibility?.code) require('./code-ext/handlers').call(this)
|
|
69
70
|
}
|
|
70
71
|
|
|
71
72
|
static handle_fiori() {
|
|
72
|
-
require('
|
|
73
|
+
require('../fiori/draft').impl.call(this)
|
|
73
74
|
}
|
|
74
75
|
|
|
75
76
|
static handle_crud() {
|
|
76
|
-
require('
|
|
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
|
-
|
|
83
|
-
|
|
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
|
-
|
|
94
|
+
target = findQueryTarget(q) || req.target // REVISIT: why is req.target not correct?
|
|
92
95
|
|
|
93
|
-
//
|
|
94
|
-
|
|
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
|
|
101
|
-
|
|
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
|
-
|
|
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
|
|
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('
|
|
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
|
-
|
|
148
|
-
|
|
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('
|
|
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('
|
|
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
|
-
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
|
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
|
|
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
|
|
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('
|
|
1
|
+
const cds = require('../../cds')
|
|
2
2
|
|
|
3
|
-
const { DRAFT_COLUMNS_UNION_MAP } = require('
|
|
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
|
|
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('
|
|
129
|
+
* @returns {import('../../types/api').ColumnRefs}
|
|
130
130
|
*/
|
|
131
131
|
const computeColumnsToBeSearched = (cqn, entity = { __searchableColumns: [] }, alias) => {
|
|
132
132
|
let toBeSearched = []
|