@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.
Files changed (59) hide show
  1. package/CHANGELOG.md +34 -1
  2. package/bin/serve.js +9 -2
  3. package/lib/auth/ias-auth.js +4 -1
  4. package/lib/auth/jwt-auth.js +4 -1
  5. package/lib/compile/cdsc.js +1 -1
  6. package/lib/compile/extend.js +23 -23
  7. package/lib/compile/to/srvinfo.js +3 -1
  8. package/lib/{linked → core}/classes.js +8 -6
  9. package/lib/{linked/models.js → core/linked-csn.js} +4 -0
  10. package/lib/env/defaults.js +4 -1
  11. package/lib/i18n/localize.js +2 -2
  12. package/lib/index.js +43 -59
  13. package/lib/log/cds-error.js +21 -21
  14. package/lib/ql/cds-ql.js +5 -5
  15. package/lib/req/cds-context.js +5 -0
  16. package/lib/req/context.js +2 -2
  17. package/lib/req/locale.js +25 -21
  18. package/lib/srv/cds-serve.js +1 -1
  19. package/lib/srv/middlewares/errors.js +20 -7
  20. package/lib/srv/protocols/hcql.js +106 -43
  21. package/lib/srv/protocols/http.js +2 -2
  22. package/lib/srv/protocols/index.js +14 -10
  23. package/lib/srv/protocols/odata-v4.js +2 -26
  24. package/lib/srv/protocols/okra.js +24 -0
  25. package/lib/srv/srv-models.js +6 -8
  26. package/lib/{utils → test}/cds-test.js +5 -5
  27. package/lib/utils/check-version.js +8 -15
  28. package/lib/utils/extend.js +20 -0
  29. package/lib/utils/lazify.js +33 -0
  30. package/lib/utils/tar.js +39 -1
  31. package/libx/_runtime/cds-services/adapter/odata-v4/to.js +0 -1
  32. package/libx/_runtime/common/generic/auth/restrict.js +1 -3
  33. package/libx/_runtime/common/generic/sorting.js +1 -1
  34. package/libx/_runtime/common/utils/compareJson.js +139 -53
  35. package/libx/_runtime/common/utils/compareJsonOLD.js +280 -0
  36. package/libx/_runtime/common/utils/differ.js +9 -1
  37. package/libx/_runtime/common/utils/resolveView.js +19 -23
  38. package/libx/_runtime/fiori/lean-draft.js +2 -2
  39. package/libx/_runtime/messaging/kafka.js +7 -1
  40. package/libx/_runtime/remote/utils/data.js +30 -24
  41. package/libx/odata/ODataAdapter.js +17 -7
  42. package/libx/odata/middleware/batch.js +4 -1
  43. package/libx/odata/middleware/error.js +6 -0
  44. package/libx/odata/middleware/operation.js +8 -0
  45. package/libx/odata/parse/afterburner.js +5 -6
  46. package/libx/odata/parse/grammar.peggy +3 -4
  47. package/libx/odata/parse/multipartToJson.js +60 -10
  48. package/libx/odata/parse/parser.js +1 -1
  49. package/libx/odata/utils/metadata.js +31 -1
  50. package/libx/outbox/index.js +5 -1
  51. package/package.json +3 -4
  52. package/server.js +18 -0
  53. package/lib/lazy.js +0 -51
  54. package/lib/test/index.js +0 -2
  55. /package/lib/{linked → core}/entities.js +0 -0
  56. /package/lib/{linked → core}/types.js +0 -0
  57. /package/lib/{linked → req}/validate.js +0 -0
  58. /package/lib/{utils → test}/axios.js +0 -0
  59. /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 _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) => {
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
- _getKeysOfEntity(entity.elements[prop]._target),
43
- _getCompositionsOfEntity(entity.elements[prop]._target)
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(entity.elements[prop]._target, undefined, oldValue && oldValue[prop], opts)
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(entity.elements[prop]._target, newValue[prop], oldValue && oldValue[prop], opts)
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 = _getCorrespondingEntryWithSameKeys(newValues, oldEntry, keys)
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, _getCompositionsOfEntity(entity))
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
- // 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
- }
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 _propertiesAndManaged(newEntry, entity)) {
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
- // if value did not change --> ignored
172
- if (
173
- newEntry[prop] === (oldEntry && oldEntry[prop]) ||
174
- (oldEntry && entity.elements[prop]?.['@Core.Immutable']) ||
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 (_skipToMany(entity, prop)) {
181
- continue
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 (_skipToOne(entity, prop)) {
185
- continue
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
- if (entity.elements[prop] && entity.elements[prop].isComposition) {
189
- _addCompositionsToResult(result, entity, prop, newEntry, oldEntry, opts)
190
- continue
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
- _addPrimitiveValuesAndOperatorToResult(result, prop, newEntry, oldEntry)
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 keys = _getKeysOfEntity(entity)
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
- const oldEntry = _getCorrespondingEntryWithSameKeys(oldValues, newEntry, keys)
209
- _iteratePropsInNewEntry(newEntry, keys, result, oldEntry, entity, opts)
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
- const result = compareJsonDeep(entity, newValue, oldValue, options)
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 = (newData, key, transition, el, inverse, service) => {
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
- newData[key] = val.map(singleVal => _newData(singleVal, mapped.transition, inverse, service))
82
+ return val.map(singleVal => _newData(singleVal, mapped.transition, inverse, service))
85
83
  } else {
86
- newData[key] = _newData(val, mapped.transition, inverse, service)
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
- // REVISIT this does not copy deep
116
- const newData = { ...data }
114
+ const newData = {}
117
115
  const queryTarget = transition.queryTarget
118
116
 
119
- for (const key in newData) {
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
- // if there is no mapping and no element with the same name in the target, then we don't need the data
132
- if ((typeof newData[key] !== 'object' || newData[key] === null) && !transition.target.elements[key])
133
- delete newData[key]
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(newData, key, transition, el, inverse)
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
  }