@sap/cds 8.3.1 → 8.4.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +34 -1
- package/bin/serve.js +9 -2
- package/lib/auth/ias-auth.js +4 -1
- package/lib/auth/jwt-auth.js +4 -1
- package/lib/compile/cdsc.js +1 -1
- package/lib/compile/extend.js +23 -23
- package/lib/compile/to/srvinfo.js +3 -1
- package/lib/{linked → core}/classes.js +8 -6
- package/lib/{linked/models.js → core/linked-csn.js} +4 -0
- package/lib/env/defaults.js +4 -1
- package/lib/i18n/localize.js +2 -2
- package/lib/index.js +43 -59
- package/lib/log/cds-error.js +21 -21
- package/lib/ql/cds-ql.js +5 -5
- package/lib/req/cds-context.js +5 -0
- package/lib/req/context.js +2 -2
- package/lib/req/locale.js +25 -21
- package/lib/srv/cds-serve.js +1 -1
- package/lib/srv/middlewares/errors.js +20 -7
- package/lib/srv/protocols/hcql.js +106 -43
- package/lib/srv/protocols/http.js +2 -2
- package/lib/srv/protocols/index.js +14 -10
- package/lib/srv/protocols/odata-v4.js +2 -26
- package/lib/srv/protocols/okra.js +24 -0
- package/lib/srv/srv-models.js +6 -8
- package/lib/{utils → test}/cds-test.js +5 -5
- package/lib/utils/check-version.js +8 -15
- package/lib/utils/extend.js +20 -0
- package/lib/utils/lazify.js +33 -0
- package/lib/utils/tar.js +39 -1
- package/libx/_runtime/cds-services/adapter/odata-v4/to.js +0 -1
- package/libx/_runtime/common/generic/auth/restrict.js +1 -3
- package/libx/_runtime/common/generic/sorting.js +1 -1
- package/libx/_runtime/common/utils/compareJson.js +139 -53
- package/libx/_runtime/common/utils/compareJsonOLD.js +280 -0
- package/libx/_runtime/common/utils/differ.js +9 -1
- package/libx/_runtime/common/utils/resolveView.js +19 -23
- package/libx/_runtime/fiori/lean-draft.js +2 -2
- package/libx/_runtime/messaging/kafka.js +7 -1
- package/libx/_runtime/remote/utils/data.js +30 -24
- package/libx/odata/ODataAdapter.js +17 -7
- package/libx/odata/middleware/batch.js +4 -1
- package/libx/odata/middleware/error.js +6 -0
- package/libx/odata/middleware/operation.js +8 -0
- package/libx/odata/parse/afterburner.js +5 -6
- package/libx/odata/parse/grammar.peggy +3 -4
- package/libx/odata/parse/multipartToJson.js +60 -10
- package/libx/odata/parse/parser.js +1 -1
- package/libx/odata/utils/metadata.js +31 -1
- package/libx/outbox/index.js +5 -1
- package/package.json +3 -4
- package/server.js +18 -0
- package/lib/lazy.js +0 -51
- package/lib/test/index.js +0 -2
- /package/lib/{linked → core}/entities.js +0 -0
- /package/lib/{linked → core}/types.js +0 -0
- /package/lib/{linked → req}/validate.js +0 -0
- /package/lib/{utils → test}/axios.js +0 -0
- /package/lib/{utils → test}/data.js +0 -0
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
const cds = require('../../cds')
|
|
1
2
|
const { DRAFT_COLUMNS_MAP } = require('../constants/draft')
|
|
2
3
|
|
|
3
4
|
const _deepEqual = (val1, val2) => {
|
|
@@ -18,12 +19,7 @@ const _getCorrespondingEntryWithSameKeys = (source, entry, keys) => {
|
|
|
18
19
|
const _getIdxCorrespondingEntryWithSameKeys = (source, entry, keys) =>
|
|
19
20
|
source.findIndex(sourceEntry => keys.every(key => _deepEqual(sourceEntry[key], entry[key])))
|
|
20
21
|
|
|
21
|
-
const
|
|
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) => {
|
|
22
|
+
const _createToBeDeletedEntries = (oldEntry, entity, keys, compositions, metaCache) => {
|
|
27
23
|
const toBeDeletedEntry = {
|
|
28
24
|
_op: 'delete'
|
|
29
25
|
}
|
|
@@ -35,20 +31,18 @@ const _createToBeDeletedEntries = (oldEntry, entity, keys, compositions) => {
|
|
|
35
31
|
if (keys.includes(prop)) {
|
|
36
32
|
toBeDeletedEntry[prop] = oldEntry[prop]
|
|
37
33
|
} else if (compositions.includes(prop) && oldEntry[prop]) {
|
|
34
|
+
const target = entity.elements[prop]._target
|
|
35
|
+
const cache = metaCache.get(target)
|
|
38
36
|
toBeDeletedEntry[prop] = entity.elements[prop].is2one
|
|
39
37
|
? _createToBeDeletedEntries(
|
|
40
38
|
oldEntry[prop],
|
|
41
39
|
entity.elements[prop]._target,
|
|
42
|
-
|
|
43
|
-
|
|
40
|
+
cache.keys,
|
|
41
|
+
cache.compositions,
|
|
42
|
+
metaCache
|
|
44
43
|
)
|
|
45
44
|
: 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
|
-
)
|
|
45
|
+
_createToBeDeletedEntries(entry, target, cache.keys, cache.compositions, metaCache)
|
|
52
46
|
)
|
|
53
47
|
} else {
|
|
54
48
|
toBeDeletedEntry._old = toBeDeletedEntry._old || {}
|
|
@@ -77,7 +71,7 @@ const _hasOpDeep = (entry, element) => {
|
|
|
77
71
|
return false
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
const _addCompositionsToResult = (result, entity, prop, newValue, oldValue, opts) => {
|
|
74
|
+
const _addCompositionsToResult = (result, entity, prop, newValue, oldValue, opts, buckets, metaCache) => {
|
|
81
75
|
/*
|
|
82
76
|
* REVISIT: the current impl results in {} instead of keeping null for compo to one.
|
|
83
77
|
* unfortunately, many follow-up errors occur (e.g., prop in null checks) if changed.
|
|
@@ -89,9 +83,23 @@ const _addCompositionsToResult = (result, entity, prop, newValue, oldValue, opts
|
|
|
89
83
|
!Array.isArray(newValue[prop]) &&
|
|
90
84
|
Object.keys(newValue[prop]).length === 0
|
|
91
85
|
) {
|
|
92
|
-
composition = compareJsonDeep(
|
|
86
|
+
composition = compareJsonDeep(
|
|
87
|
+
entity.elements[prop]._target,
|
|
88
|
+
undefined,
|
|
89
|
+
oldValue && oldValue[prop],
|
|
90
|
+
opts,
|
|
91
|
+
buckets,
|
|
92
|
+
metaCache
|
|
93
|
+
)
|
|
93
94
|
} else {
|
|
94
|
-
composition = compareJsonDeep(
|
|
95
|
+
composition = compareJsonDeep(
|
|
96
|
+
entity.elements[prop]._target,
|
|
97
|
+
newValue[prop],
|
|
98
|
+
oldValue && oldValue[prop],
|
|
99
|
+
opts,
|
|
100
|
+
buckets,
|
|
101
|
+
metaCache
|
|
102
|
+
)
|
|
95
103
|
}
|
|
96
104
|
if (composition.some(c => _hasOpDeep(c, entity.elements[prop]))) {
|
|
97
105
|
result[prop] = entity.elements[prop].is2one ? composition[0] : composition
|
|
@@ -118,14 +126,17 @@ const _addKeysToResult = (result, prop, newValue, oldValue) => {
|
|
|
118
126
|
}
|
|
119
127
|
}
|
|
120
128
|
|
|
121
|
-
const _addToBeDeletedEntriesToResult = (results, entity, keys, newValues, oldValues) => {
|
|
129
|
+
const _addToBeDeletedEntriesToResult = (results, entity, keys, newValues, oldValues, newBucketMap, metaCache) => {
|
|
130
|
+
const cache = metaCache.get(entity)
|
|
122
131
|
// add to be deleted entries
|
|
123
132
|
for (const oldEntry of oldValues) {
|
|
124
|
-
const entry =
|
|
133
|
+
const entry = cds.env.features.diff_optimization
|
|
134
|
+
? _getCorrespondingEntryWithSameKeysFromBucket(newBucketMap, oldEntry, entity, keys, cache)
|
|
135
|
+
: _getCorrespondingEntryWithSameKeys(newValues, oldEntry, keys)
|
|
125
136
|
|
|
126
137
|
if (!entry) {
|
|
127
138
|
// prepare to be deleted (deep) entry without manipulating oldData
|
|
128
|
-
const toBeDeletedEntry = _createToBeDeletedEntries(oldEntry, entity, keys,
|
|
139
|
+
const toBeDeletedEntry = _createToBeDeletedEntries(oldEntry, entity, keys, cache.compositions, metaCache)
|
|
129
140
|
results.push(toBeDeletedEntry)
|
|
130
141
|
}
|
|
131
142
|
}
|
|
@@ -149,54 +160,115 @@ const _skipToMany = (entity, prop) => {
|
|
|
149
160
|
return entity.elements[prop] && entity.elements[prop].is2many && _skip(entity, prop)
|
|
150
161
|
}
|
|
151
162
|
|
|
152
|
-
|
|
153
|
-
const
|
|
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
|
-
}
|
|
163
|
+
const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity, opts, buckets, metaCache) => {
|
|
164
|
+
const cache = metaCache.get(entity)
|
|
161
165
|
|
|
162
|
-
const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity, opts) => {
|
|
163
166
|
// On app-service layer, generated foreign keys are not enumerable,
|
|
164
167
|
// include them here too.
|
|
165
|
-
for (const prop of
|
|
166
|
-
if (keys.includes(prop)) {
|
|
168
|
+
for (const prop of cache.props) {
|
|
169
|
+
if (cache.keys.includes(prop)) {
|
|
167
170
|
_addKeysToResult(result, prop, newEntry, oldEntry)
|
|
168
171
|
continue
|
|
169
172
|
}
|
|
170
173
|
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
(oldEntry
|
|
175
|
-
(opts.ignoreDraftColumns && prop in DRAFT_COLUMNS_MAP)
|
|
176
|
-
) {
|
|
174
|
+
if (newEntry[prop] === undefined && !cache.onUpdate.includes(prop)) continue
|
|
175
|
+
|
|
176
|
+
if (cache.compositions.includes(prop)) {
|
|
177
|
+
_addCompositionsToResult(result, entity, prop, newEntry, oldEntry, opts, buckets, metaCache)
|
|
177
178
|
continue
|
|
178
179
|
}
|
|
179
180
|
|
|
180
|
-
if
|
|
181
|
-
|
|
181
|
+
// if value did not change --> ignored
|
|
182
|
+
if (newEntry[prop] === (oldEntry && oldEntry[prop])) continue
|
|
183
|
+
|
|
184
|
+
// existing immutable --> ignored
|
|
185
|
+
if (oldEntry && cache.immutables.includes(prop)) continue
|
|
186
|
+
|
|
187
|
+
_addPrimitiveValuesAndOperatorToResult(result, prop, newEntry, oldEntry)
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
const _isSimpleKey = element => !element._isStructured && element.type != 'cds.Binary'
|
|
192
|
+
|
|
193
|
+
const _getMetaCache = (entity, metaCache, opts) => {
|
|
194
|
+
if (metaCache.get(entity)) return
|
|
195
|
+
|
|
196
|
+
const cache = { keys: [], props: [], compositions: [], immutables: [], onUpdate: [] }
|
|
197
|
+
metaCache.set(entity, cache)
|
|
198
|
+
for (let prop in entity.elements) {
|
|
199
|
+
const element = entity.elements[prop] || {}
|
|
200
|
+
if (prop in entity.keys && !(prop in DRAFT_COLUMNS_MAP) && !element.isAssociation) cache.keys.push(prop)
|
|
201
|
+
if (_skipToMany(entity, prop) || _skipToOne(entity, prop)) continue
|
|
202
|
+
if (opts.ignoreDraftColumns && prop in DRAFT_COLUMNS_MAP) continue
|
|
203
|
+
|
|
204
|
+
if (element?.isComposition) {
|
|
205
|
+
cache.compositions.push(prop)
|
|
206
|
+
_getMetaCache(element._target, metaCache, opts)
|
|
182
207
|
}
|
|
183
208
|
|
|
184
|
-
if (
|
|
185
|
-
|
|
209
|
+
if (element?.['@Core.Immutable']) cache.immutables.push(prop)
|
|
210
|
+
if (element?.['@cds.on.update']) cache.onUpdate.push(prop)
|
|
211
|
+
|
|
212
|
+
cache.props.push(prop)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
let getKeyHash
|
|
216
|
+
if (cache.keys.length === 1 && _isSimpleKey(entity.elements[cache.keys[0]])) {
|
|
217
|
+
getKeyHash = (entry, keys) => entry[keys[0]].toString()
|
|
218
|
+
} else if (cache.keys.map(key => entity.elements[key]).every(key => _isSimpleKey(key))) {
|
|
219
|
+
getKeyHash = (entry, keys) => keys.reduce((hash, key) => `${hash},${key}=${entry[key].toString()}`, '')
|
|
220
|
+
} else {
|
|
221
|
+
getKeyHash = (entry, keys) => {
|
|
222
|
+
const keyObj = keys.reduce((hash, key) => {
|
|
223
|
+
hash[key] = entry[key]
|
|
224
|
+
return hash
|
|
225
|
+
}, {})
|
|
226
|
+
|
|
227
|
+
return JSON.stringify(keyObj)
|
|
186
228
|
}
|
|
229
|
+
}
|
|
230
|
+
cache.getKeyHash = getKeyHash
|
|
231
|
+
}
|
|
187
232
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
233
|
+
const _addBucket = (entity, entry, bucketMap, metaCache) => {
|
|
234
|
+
if (!entry) return
|
|
235
|
+
const entries = _normalizeToArray(entry)
|
|
236
|
+
const cache = metaCache.get(entity)
|
|
237
|
+
|
|
238
|
+
entries.forEach(e => {
|
|
239
|
+
const keyHash = cache.getKeyHash(e, cache.keys)
|
|
240
|
+
let entityMap = bucketMap.get(entity)
|
|
241
|
+
if (!entityMap) {
|
|
242
|
+
entityMap = new Map()
|
|
243
|
+
bucketMap.set(entity, entityMap)
|
|
191
244
|
}
|
|
245
|
+
entityMap.set(keyHash, e)
|
|
192
246
|
|
|
193
|
-
|
|
194
|
-
|
|
247
|
+
for (const prop of cache.props) {
|
|
248
|
+
if (cache.compositions.includes(prop)) _addBucket(entity.elements[prop]._target, e[prop], bucketMap, metaCache)
|
|
249
|
+
}
|
|
250
|
+
})
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
const _getBucketMap = (value, entity, metaCache) => {
|
|
254
|
+
const bucketMap = new Map()
|
|
255
|
+
_addBucket(entity, value, bucketMap, metaCache)
|
|
256
|
+
|
|
257
|
+
return bucketMap
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
const _getCorrespondingEntryWithSameKeysFromBucket = (bucketMap, entry, entity, keys, cache) => {
|
|
261
|
+
const bucket = bucketMap.get(entity)
|
|
262
|
+
if (!bucket) return
|
|
263
|
+
|
|
264
|
+
const keyHash = cache.getKeyHash(entry, keys)
|
|
265
|
+
return bucket.get(keyHash)
|
|
195
266
|
}
|
|
196
267
|
|
|
197
|
-
const compareJsonDeep = (entity, newValue = [], oldValue = [], opts) => {
|
|
268
|
+
const compareJsonDeep = (entity, newValue = [], oldValue = [], opts, buckets, metaCache) => {
|
|
198
269
|
const resultsArray = []
|
|
199
|
-
const
|
|
270
|
+
const cache = metaCache.get(entity)
|
|
271
|
+
const keys = cache.keys
|
|
200
272
|
|
|
201
273
|
// normalize input
|
|
202
274
|
const newValues = _normalizeToArray(newValue)
|
|
@@ -205,12 +277,17 @@ const compareJsonDeep = (entity, newValue = [], oldValue = [], opts) => {
|
|
|
205
277
|
// add to be created and to be updated entries
|
|
206
278
|
for (const newEntry of newValues) {
|
|
207
279
|
const result = {}
|
|
208
|
-
|
|
209
|
-
|
|
280
|
+
let oldEntry
|
|
281
|
+
if (oldValues.length) {
|
|
282
|
+
oldEntry = cds.env.features.diff_optimization
|
|
283
|
+
? _getCorrespondingEntryWithSameKeysFromBucket(buckets.oldBucketMap, newEntry, entity, keys, cache)
|
|
284
|
+
: _getCorrespondingEntryWithSameKeys(oldValues, newEntry, keys)
|
|
285
|
+
}
|
|
286
|
+
_iteratePropsInNewEntry(newEntry, keys, result, oldEntry, entity, opts, buckets, metaCache)
|
|
210
287
|
resultsArray.push(result)
|
|
211
288
|
}
|
|
212
289
|
|
|
213
|
-
_addToBeDeletedEntriesToResult(resultsArray, entity, keys, newValues, oldValues)
|
|
290
|
+
_addToBeDeletedEntriesToResult(resultsArray, entity, keys, newValues, oldValues, buckets.newBucketMap, metaCache)
|
|
214
291
|
|
|
215
292
|
return resultsArray
|
|
216
293
|
}
|
|
@@ -266,7 +343,16 @@ const compareJsonDeep = (entity, newValue = [], oldValue = [], opts) => {
|
|
|
266
343
|
*/
|
|
267
344
|
const compareJson = (newValue, oldValue, entity, opts = {}) => {
|
|
268
345
|
const options = Object.assign({ ignoreDraftColumns: false }, opts)
|
|
269
|
-
|
|
346
|
+
|
|
347
|
+
let newBucketMap,
|
|
348
|
+
oldBucketMap,
|
|
349
|
+
metaCache = new Map()
|
|
350
|
+
_getMetaCache(entity, metaCache, opts)
|
|
351
|
+
if (oldValue && (!Array.isArray(oldValue) || oldValue.length) && cds.env.features.diff_optimization) {
|
|
352
|
+
newBucketMap = _getBucketMap(newValue, entity, metaCache)
|
|
353
|
+
oldBucketMap = _getBucketMap(oldValue, entity, metaCache)
|
|
354
|
+
}
|
|
355
|
+
const result = compareJsonDeep(entity, newValue, oldValue, options, { newBucketMap, oldBucketMap }, metaCache)
|
|
270
356
|
|
|
271
357
|
// in case of batch insert, result is an array
|
|
272
358
|
// in all other cases it is an array with just one entry
|
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
// copied from @sap/cds@8.3.1
|
|
2
|
+
|
|
3
|
+
const { DRAFT_COLUMNS_MAP } = require('../constants/draft')
|
|
4
|
+
|
|
5
|
+
const _deepEqual = (val1, val2) => {
|
|
6
|
+
if (val1 && typeof val1 === 'object' && val2 && typeof val2 === 'object') {
|
|
7
|
+
for (const key in val1) {
|
|
8
|
+
if (!_deepEqual(val1[key], val2[key])) return false
|
|
9
|
+
}
|
|
10
|
+
return true
|
|
11
|
+
}
|
|
12
|
+
return val1 === val2
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const _getCorrespondingEntryWithSameKeys = (source, entry, keys) => {
|
|
16
|
+
const idx = _getIdxCorrespondingEntryWithSameKeys(source, entry, keys)
|
|
17
|
+
return idx !== -1 ? source[idx] : undefined
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
const _getIdxCorrespondingEntryWithSameKeys = (source, entry, keys) =>
|
|
21
|
+
source.findIndex(sourceEntry => keys.every(key => _deepEqual(sourceEntry[key], entry[key])))
|
|
22
|
+
|
|
23
|
+
const _getKeysOfEntity = entity =>
|
|
24
|
+
Object.keys(entity.keys).filter(key => !(key in DRAFT_COLUMNS_MAP) && !entity.elements[key].isAssociation)
|
|
25
|
+
|
|
26
|
+
const _getCompositionsOfEntity = entity => Object.keys(entity.elements).filter(e => entity.elements[e].isComposition)
|
|
27
|
+
|
|
28
|
+
const _createToBeDeletedEntries = (oldEntry, entity, keys, compositions) => {
|
|
29
|
+
const toBeDeletedEntry = {
|
|
30
|
+
_op: 'delete'
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
for (const prop in oldEntry) {
|
|
34
|
+
if (prop in DRAFT_COLUMNS_MAP) {
|
|
35
|
+
continue
|
|
36
|
+
}
|
|
37
|
+
if (keys.includes(prop)) {
|
|
38
|
+
toBeDeletedEntry[prop] = oldEntry[prop]
|
|
39
|
+
} else if (compositions.includes(prop) && oldEntry[prop]) {
|
|
40
|
+
toBeDeletedEntry[prop] = entity.elements[prop].is2one
|
|
41
|
+
? _createToBeDeletedEntries(
|
|
42
|
+
oldEntry[prop],
|
|
43
|
+
entity.elements[prop]._target,
|
|
44
|
+
_getKeysOfEntity(entity.elements[prop]._target),
|
|
45
|
+
_getCompositionsOfEntity(entity.elements[prop]._target)
|
|
46
|
+
)
|
|
47
|
+
: oldEntry[prop].map(entry =>
|
|
48
|
+
_createToBeDeletedEntries(
|
|
49
|
+
entry,
|
|
50
|
+
entity.elements[prop]._target,
|
|
51
|
+
_getKeysOfEntity(entity.elements[prop]._target),
|
|
52
|
+
_getCompositionsOfEntity(entity.elements[prop]._target)
|
|
53
|
+
)
|
|
54
|
+
)
|
|
55
|
+
} else {
|
|
56
|
+
toBeDeletedEntry._old = toBeDeletedEntry._old || {}
|
|
57
|
+
toBeDeletedEntry._old[prop] = oldEntry[prop]
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return toBeDeletedEntry
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const _hasOpDeep = (entry, element) => {
|
|
65
|
+
const entryArray = Array.isArray(entry) ? entry : [entry]
|
|
66
|
+
for (const entry_ of entryArray) {
|
|
67
|
+
if (entry_._op) return true
|
|
68
|
+
|
|
69
|
+
if (element && element.isComposition) {
|
|
70
|
+
const target = element._target
|
|
71
|
+
for (const prop in entry_) {
|
|
72
|
+
if (_hasOpDeep(entry_[prop], target.elements[prop])) {
|
|
73
|
+
return true
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
return false
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const _addCompositionsToResult = (result, entity, prop, newValue, oldValue, opts) => {
|
|
83
|
+
/*
|
|
84
|
+
* REVISIT: the current impl results in {} instead of keeping null for compo to one.
|
|
85
|
+
* unfortunately, many follow-up errors occur (e.g., prop in null checks) if changed.
|
|
86
|
+
*/
|
|
87
|
+
let composition
|
|
88
|
+
if (
|
|
89
|
+
newValue[prop] &&
|
|
90
|
+
typeof newValue[prop] === 'object' &&
|
|
91
|
+
!Array.isArray(newValue[prop]) &&
|
|
92
|
+
Object.keys(newValue[prop]).length === 0
|
|
93
|
+
) {
|
|
94
|
+
composition = compareJsonDeep(entity.elements[prop]._target, undefined, oldValue && oldValue[prop], opts)
|
|
95
|
+
} else {
|
|
96
|
+
composition = compareJsonDeep(entity.elements[prop]._target, newValue[prop], oldValue && oldValue[prop], opts)
|
|
97
|
+
}
|
|
98
|
+
if (composition.some(c => _hasOpDeep(c, entity.elements[prop]))) {
|
|
99
|
+
result[prop] = entity.elements[prop].is2one ? composition[0] : composition
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const _addPrimitiveValuesAndOperatorToResult = (result, prop, newValue, oldValue) => {
|
|
104
|
+
result[prop] = newValue[prop]
|
|
105
|
+
|
|
106
|
+
if (!result._op) {
|
|
107
|
+
result._op = oldValue ? 'update' : 'create'
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (result._op === 'update') {
|
|
111
|
+
result._old = result._old || {}
|
|
112
|
+
result._old[prop] = oldValue[prop]
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const _addKeysToResult = (result, prop, newValue, oldValue) => {
|
|
117
|
+
result[prop] = newValue[prop]
|
|
118
|
+
if (!oldValue) {
|
|
119
|
+
result._op = 'create'
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
const _addToBeDeletedEntriesToResult = (results, entity, keys, newValues, oldValues) => {
|
|
124
|
+
// add to be deleted entries
|
|
125
|
+
for (const oldEntry of oldValues) {
|
|
126
|
+
const entry = _getCorrespondingEntryWithSameKeys(newValues, oldEntry, keys)
|
|
127
|
+
|
|
128
|
+
if (!entry) {
|
|
129
|
+
// prepare to be deleted (deep) entry without manipulating oldData
|
|
130
|
+
const toBeDeletedEntry = _createToBeDeletedEntries(oldEntry, entity, keys, _getCompositionsOfEntity(entity))
|
|
131
|
+
results.push(toBeDeletedEntry)
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
const _normalizeToArray = value => (Array.isArray(value) ? value : value === null ? [] : [value])
|
|
137
|
+
|
|
138
|
+
const _isUnManaged = element => {
|
|
139
|
+
return element.on && !element._isSelfManaged
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
const _skip = (entity, prop) => entity.elements[prop]._target._hasPersistenceSkip
|
|
143
|
+
|
|
144
|
+
const _skipToOne = (entity, prop) => {
|
|
145
|
+
return (
|
|
146
|
+
entity.elements[prop] && entity.elements[prop].is2one && _skip(entity, prop) && _isUnManaged(entity.elements[prop])
|
|
147
|
+
)
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
const _skipToMany = (entity, prop) => {
|
|
151
|
+
return entity.elements[prop] && entity.elements[prop].is2many && _skip(entity, prop)
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// Returns all property names from the new entry and add missing managed elements
|
|
155
|
+
const _propertiesAndManaged = (newEntry, entity) => {
|
|
156
|
+
return [
|
|
157
|
+
...Object.getOwnPropertyNames(newEntry),
|
|
158
|
+
...Object.keys(entity.elements).filter(
|
|
159
|
+
elementName => newEntry[elementName] === undefined && entity.elements[elementName]['@cds.on.update']
|
|
160
|
+
)
|
|
161
|
+
]
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const _iteratePropsInNewEntry = (newEntry, keys, result, oldEntry, entity, opts) => {
|
|
165
|
+
// On app-service layer, generated foreign keys are not enumerable,
|
|
166
|
+
// include them here too.
|
|
167
|
+
for (const prop of _propertiesAndManaged(newEntry, entity)) {
|
|
168
|
+
if (keys.includes(prop)) {
|
|
169
|
+
_addKeysToResult(result, prop, newEntry, oldEntry)
|
|
170
|
+
continue
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// if value did not change --> ignored
|
|
174
|
+
if (
|
|
175
|
+
newEntry[prop] === (oldEntry && oldEntry[prop]) ||
|
|
176
|
+
(oldEntry && entity.elements[prop]?.['@Core.Immutable']) ||
|
|
177
|
+
(opts.ignoreDraftColumns && prop in DRAFT_COLUMNS_MAP)
|
|
178
|
+
) {
|
|
179
|
+
continue
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
if (_skipToMany(entity, prop)) {
|
|
183
|
+
continue
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (_skipToOne(entity, prop)) {
|
|
187
|
+
continue
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
if (entity.elements[prop] && entity.elements[prop].isComposition) {
|
|
191
|
+
_addCompositionsToResult(result, entity, prop, newEntry, oldEntry, opts)
|
|
192
|
+
continue
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
_addPrimitiveValuesAndOperatorToResult(result, prop, newEntry, oldEntry)
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const compareJsonDeep = (entity, newValue = [], oldValue = [], opts) => {
|
|
200
|
+
const resultsArray = []
|
|
201
|
+
const keys = _getKeysOfEntity(entity)
|
|
202
|
+
|
|
203
|
+
// normalize input
|
|
204
|
+
const newValues = _normalizeToArray(newValue)
|
|
205
|
+
const oldValues = _normalizeToArray(oldValue)
|
|
206
|
+
|
|
207
|
+
// add to be created and to be updated entries
|
|
208
|
+
for (const newEntry of newValues) {
|
|
209
|
+
const result = {}
|
|
210
|
+
const oldEntry = _getCorrespondingEntryWithSameKeys(oldValues, newEntry, keys)
|
|
211
|
+
_iteratePropsInNewEntry(newEntry, keys, result, oldEntry, entity, opts)
|
|
212
|
+
resultsArray.push(result)
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
_addToBeDeletedEntriesToResult(resultsArray, entity, keys, newValues, oldValues)
|
|
216
|
+
|
|
217
|
+
return resultsArray
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* Compares newValue with oldValues in a deep fashion.
|
|
222
|
+
* Output format is newValue with additional administrative properties.
|
|
223
|
+
* - "_op" provides info about the CRUD action to perform
|
|
224
|
+
* - "_old" provides info about the current DB state
|
|
225
|
+
*
|
|
226
|
+
* Unchanged values are not part of the result.
|
|
227
|
+
*
|
|
228
|
+
* Output format is:
|
|
229
|
+
* {
|
|
230
|
+
* _op: 'update',
|
|
231
|
+
* _old: { orderedAt: 'DE' },
|
|
232
|
+
* ID: 1,
|
|
233
|
+
* orderedAt: 'EN',
|
|
234
|
+
* items: [
|
|
235
|
+
* {
|
|
236
|
+
* _op: 'update',
|
|
237
|
+
* _old: { amount: 7 },
|
|
238
|
+
* ID: 7,
|
|
239
|
+
* amount: 8
|
|
240
|
+
* },
|
|
241
|
+
* {
|
|
242
|
+
* _op: 'create',
|
|
243
|
+
* ID: 8,
|
|
244
|
+
* amount: 8
|
|
245
|
+
* },
|
|
246
|
+
* {
|
|
247
|
+
* _op: 'delete',
|
|
248
|
+
* _old: {
|
|
249
|
+
* amount: 6
|
|
250
|
+
* },
|
|
251
|
+
* ID: 6
|
|
252
|
+
* }
|
|
253
|
+
* ]
|
|
254
|
+
* }
|
|
255
|
+
*
|
|
256
|
+
*
|
|
257
|
+
* If there is no change in an UPDATE, result is an object containing only the keys of the entity.
|
|
258
|
+
*
|
|
259
|
+
* @example
|
|
260
|
+
* compareJson(csnEntity, [{ID: 1, col1: 'A'}], [{ID: 1, col1: 'B'}])
|
|
261
|
+
*
|
|
262
|
+
* @param oldValue
|
|
263
|
+
* @param {object} entity
|
|
264
|
+
* @param {Array | object} newValue
|
|
265
|
+
* @param {Array} oldValues
|
|
266
|
+
*
|
|
267
|
+
* @returns {Array}
|
|
268
|
+
*/
|
|
269
|
+
const compareJson = (newValue, oldValue, entity, opts = {}) => {
|
|
270
|
+
const options = Object.assign({ ignoreDraftColumns: false }, opts)
|
|
271
|
+
const result = compareJsonDeep(entity, newValue, oldValue, options)
|
|
272
|
+
|
|
273
|
+
// in case of batch insert, result is an array
|
|
274
|
+
// in all other cases it is an array with just one entry
|
|
275
|
+
return Array.isArray(newValue) ? result : result[0]
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
module.exports = {
|
|
279
|
+
compareJson
|
|
280
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
1
|
const cds = require('../../cds')
|
|
2
2
|
const { SELECT } = cds.ql
|
|
3
3
|
|
|
4
|
-
const { compareJson } = require('./compareJson')
|
|
5
4
|
const { selectDeepUpdateData } = require('../composition')
|
|
6
5
|
const { ensureDraftsSuffix } = require('./draft')
|
|
7
6
|
|
|
@@ -10,9 +9,18 @@ const { cqn2cqn4sql, convertPathExpressionToWhere } = require('./cqn2cqn4sql')
|
|
|
10
9
|
const { revertData } = require('./resolveView')
|
|
11
10
|
const { enrichDataWithKeysFromWhere } = require('./keys')
|
|
12
11
|
|
|
12
|
+
let compareJson
|
|
13
|
+
|
|
13
14
|
module.exports = class Differ {
|
|
14
15
|
constructor(srv) {
|
|
15
16
|
this._srv = srv
|
|
17
|
+
|
|
18
|
+
// REVISIT: remove with cds^9
|
|
19
|
+
// use old compareJson if not using a @cap-js db
|
|
20
|
+
compareJson ??=
|
|
21
|
+
cds.env.requires.db?.impl && !cds.env.requires.db.impl.startsWith('@cap-js')
|
|
22
|
+
? require('./compareJsonOLD').compareJson
|
|
23
|
+
: require('./compareJson').compareJson
|
|
16
24
|
}
|
|
17
25
|
|
|
18
26
|
_createSelectColumnsForDelete(entity) {
|
|
@@ -65,9 +65,7 @@ const revertData = (data, transition, service) => {
|
|
|
65
65
|
: _newData(data, inverseTransition, true, service)
|
|
66
66
|
}
|
|
67
67
|
|
|
68
|
-
const _newSubData = (
|
|
69
|
-
const val = newData[key]
|
|
70
|
-
|
|
68
|
+
const _newSubData = (val, key, transition, el, inverse, service) => {
|
|
71
69
|
if ((!Array.isArray(val) && typeof val === 'object') || (Array.isArray(val) && val.length !== 0)) {
|
|
72
70
|
let mapped = transition.mapping.get(key)
|
|
73
71
|
if (!mapped) {
|
|
@@ -81,11 +79,12 @@ const _newSubData = (newData, key, transition, el, inverse, service) => {
|
|
|
81
79
|
}
|
|
82
80
|
|
|
83
81
|
if (Array.isArray(val)) {
|
|
84
|
-
|
|
82
|
+
return val.map(singleVal => _newData(singleVal, mapped.transition, inverse, service))
|
|
85
83
|
} else {
|
|
86
|
-
|
|
84
|
+
return _newData(val, mapped.transition, inverse, service)
|
|
87
85
|
}
|
|
88
86
|
}
|
|
87
|
+
return val //Case of empty array
|
|
89
88
|
}
|
|
90
89
|
|
|
91
90
|
const _newNestedData = (queryTarget, newData, ref, value) => {
|
|
@@ -112,40 +111,37 @@ const _newData = (data, transition, inverse, service) => {
|
|
|
112
111
|
// no transition -> nothing to do
|
|
113
112
|
if (transition.target && transition.target.name === transition.queryTarget.name) return data
|
|
114
113
|
|
|
115
|
-
|
|
116
|
-
const newData = { ...data }
|
|
114
|
+
const newData = {}
|
|
117
115
|
const queryTarget = transition.queryTarget
|
|
118
116
|
|
|
119
|
-
for (const key in
|
|
117
|
+
for (const key in data) {
|
|
120
118
|
const el = queryTarget && queryTarget?.elements[key]
|
|
121
119
|
const isAssoc = el && el.isAssociation
|
|
122
120
|
|
|
123
|
-
if (isAssoc) {
|
|
124
|
-
if (newData[key] || (newData[key] === null && service.name === 'db')) {
|
|
125
|
-
_newSubData(newData, key, transition, el, inverse, service)
|
|
126
|
-
}
|
|
127
|
-
}
|
|
128
|
-
|
|
129
121
|
const mapped = transition.mapping.get(key)
|
|
130
122
|
if (!mapped) {
|
|
131
|
-
//
|
|
132
|
-
if (
|
|
133
|
-
|
|
123
|
+
//In this condition the data is needed
|
|
124
|
+
if (
|
|
125
|
+
((typeof data[key] === 'object' && data[key] !== null) || transition.target.elements[key]) &&
|
|
126
|
+
newData[key] === undefined
|
|
127
|
+
)
|
|
128
|
+
newData[key] = data[key]
|
|
134
129
|
continue
|
|
135
130
|
}
|
|
131
|
+
let value = data[key]
|
|
132
|
+
if (isAssoc) {
|
|
133
|
+
if (value || (value === null && service.name === 'db')) {
|
|
134
|
+
value = _newSubData(value, key, transition, el, inverse, service)
|
|
135
|
+
}
|
|
136
|
+
}
|
|
136
137
|
|
|
137
138
|
if (!isAssoc && mapped.transition) {
|
|
138
|
-
_newSubData(
|
|
139
|
-
const value = newData[key]
|
|
140
|
-
delete newData[key]
|
|
139
|
+
value = _newSubData(value, key, transition, el, inverse)
|
|
141
140
|
Object.assign(newData, value)
|
|
142
141
|
}
|
|
143
142
|
|
|
144
143
|
if (mapped.ref) {
|
|
145
|
-
const value = newData[key]
|
|
146
|
-
delete newData[key]
|
|
147
144
|
const { ref } = mapped
|
|
148
|
-
|
|
149
145
|
if (ref.length === 1) {
|
|
150
146
|
newData[ref[0]] = value
|
|
151
147
|
if (mapped.alternatives) mapped.alternatives.forEach(({ ref }) => (newData[ref[0]] = value))
|
|
@@ -389,7 +389,7 @@ cds.ApplicationService.prototype.handle = async function (req) {
|
|
|
389
389
|
return req._messages
|
|
390
390
|
}
|
|
391
391
|
})
|
|
392
|
-
if (req.tx) _req.tx = req.tx
|
|
392
|
+
if (req.tx && !_req.tx) _req.tx = req.tx
|
|
393
393
|
|
|
394
394
|
return _req
|
|
395
395
|
}
|
|
@@ -1209,7 +1209,7 @@ function _cleanseParams(params, target) {
|
|
|
1209
1209
|
if (key === 'IsActiveEntity') {
|
|
1210
1210
|
const value = params[key]
|
|
1211
1211
|
delete params[key]
|
|
1212
|
-
Object.defineProperty(params, key, { value, enumerable: false })
|
|
1212
|
+
Object.defineProperty(params, key, { value, enumerable: false, writeable: true })
|
|
1213
1213
|
}
|
|
1214
1214
|
}
|
|
1215
1215
|
}
|