@cap-js/change-tracking 1.1.3 → 1.1.4

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/lib/change-log.js CHANGED
@@ -1,129 +1,117 @@
1
- const cds = require("@sap/cds")
2
- const getTemplate = require("@sap/cds/libx/_runtime/common/utils/template") // REVISIT: bad usage of internal stuff
3
- const {templateProcessor, retrieveFirstKey} = require("./template-processor")
4
- const LOG = cds.log("change-log")
5
-
6
- const {
7
- getNameFromPathVal,
8
- getUUIDFromPathVal,
9
- getCurObjFromReqData,
10
- getCurObjFromDbQuery,
11
- getObjectId,
12
- getDBEntity,
13
- getEntityByContextPath,
14
- getObjIdElementNamesInArray,
15
- getValueEntityType,
16
- splitPath,
17
- } = require("./entity-helper")
18
- const { localizeLogFields } = require("./localization")
19
- const isRoot = "change-tracking-isRootEntity"
1
+ const cds = require('@sap/cds');
2
+ const getTemplate = require('@sap/cds/libx/_runtime/common/utils/template'); // REVISIT: bad usage of internal stuff
3
+ const { templateProcessor, retrieveFirstKey } = require('./template-processor');
4
+ const LOG = cds.log('change-log');
20
5
 
6
+ const { getNameFromPathVal, getUUIDFromPathVal, getCurObjFromReqData, getCurObjFromDbQuery, getObjectId, getDBEntity, getEntityByContextPath, getObjIdElementNamesInArray, getValueEntityType, splitPath } = require('./entity-helper');
7
+ const { localizeLogFields } = require('./localization');
8
+ const isRoot = 'change-tracking-isRootEntity';
9
+ const hasParent = 'change-tracking-parentEntity';
21
10
 
22
11
  function formatDecimal(str, scale) {
23
- if (typeof str === "number" && !isNaN(str)) {
24
- str = String(str);
25
- } else return str;
12
+ if (typeof str === 'number' && !isNaN(str)) {
13
+ str = String(str);
14
+ } else return str;
26
15
 
27
- if (scale > 0) {
28
- let parts = str.split(".");
29
- let decimalPart = parts[1] || "";
16
+ if (scale > 0) {
17
+ let parts = str.split('.');
18
+ let decimalPart = parts[1] || '';
30
19
 
31
- while (decimalPart.length < scale) {
32
- decimalPart += "0";
33
- }
20
+ while (decimalPart.length < scale) {
21
+ decimalPart += '0';
22
+ }
34
23
 
35
- return `${parts[0]}.${decimalPart}`;
36
- }
24
+ return `${parts[0]}.${decimalPart}`;
25
+ }
37
26
 
38
- return str;
27
+ return str;
39
28
  }
40
29
 
41
30
  const _getRootEntityPathVals = function (txContext, entity, entityKey) {
42
- const serviceEntityPathVals = []
43
- const entityIDs = _getEntityIDs(txContext.params)
44
-
45
- let path = txContext.path.split('/')
46
-
47
- if (txContext.event === "CREATE") {
48
- const curEntityPathVal = `${entity.name}(${entityKey})`
49
- serviceEntityPathVals.push(curEntityPathVal)
50
- txContext.hasComp && entityIDs.pop();
51
- } else {
52
- // When deleting Composition of one node via REST API in draft-disabled mode,
53
- // the child node ID would be missing in URI
54
- if (txContext.event === "DELETE" && !entityIDs.find((x) => x === entityKey)) {
55
- entityIDs.push(entityKey)
56
- }
57
- const curEntity = getEntityByContextPath(path, txContext.hasComp)
58
- const curEntityID = entityIDs.pop()
59
- const curEntityPathVal = `${curEntity.name}(${curEntityID})`
60
- serviceEntityPathVals.push(curEntityPathVal)
61
- }
62
-
63
-
64
- while (_isCompositionContextPath(path, txContext.hasComp)) {
65
- const hostEntity = getEntityByContextPath(path = path.slice(0, -1), txContext.hasComp)
66
- const hostEntityID = entityIDs.pop()
67
- const hostEntityPathVal = `${hostEntity.name}(${hostEntityID})`
68
- serviceEntityPathVals.unshift(hostEntityPathVal)
69
- }
70
-
71
- return serviceEntityPathVals
72
- }
31
+ const serviceEntityPathVals = [];
32
+ const entityIDs = _getEntityIDs(txContext.params);
33
+
34
+ let path = txContext.path.split('/');
35
+
36
+ if (txContext.event === 'CREATE') {
37
+ const curEntityPathVal = `${entity.name}(${entityKey})`;
38
+ serviceEntityPathVals.push(curEntityPathVal);
39
+ txContext.hasComp && entityIDs.pop();
40
+ } else {
41
+ // When deleting Composition of one node via REST API in draft-disabled mode,
42
+ // the child node ID would be missing in URI
43
+ if (txContext.event === 'DELETE' && !entityIDs.find((x) => x === entityKey)) {
44
+ entityIDs.push(entityKey);
45
+ }
46
+ const curEntity = getEntityByContextPath(path, txContext.hasComp);
47
+ const curEntityID = entityIDs.pop();
48
+ const curEntityPathVal = `${curEntity.name}(${curEntityID})`;
49
+ serviceEntityPathVals.push(curEntityPathVal);
50
+ }
51
+
52
+ while (_isCompositionContextPath(path, txContext.hasComp)) {
53
+ const hostEntity = getEntityByContextPath((path = path.slice(0, -1)), txContext.hasComp);
54
+ const hostEntityID = entityIDs.pop();
55
+ const hostEntityPathVal = `${hostEntity.name}(${hostEntityID})`;
56
+ serviceEntityPathVals.unshift(hostEntityPathVal);
57
+ }
58
+
59
+ return serviceEntityPathVals;
60
+ };
73
61
 
74
62
  const _getAllPathVals = function (txContext) {
75
- const pathVals = []
76
- const paths = txContext.path.split('/')
77
- const entityIDs = _getEntityIDs(txContext.params)
78
-
79
- for (let idx = 0; idx < paths.length; idx++) {
80
- const entity = getEntityByContextPath(paths.slice(0, idx + 1), txContext.hasComp)
81
- const entityID = entityIDs[idx]
82
- const entityPathVal = `${entity.name}(${entityID})`
83
- pathVals.push(entityPathVal)
84
- }
85
-
86
- return pathVals
87
- }
63
+ const pathVals = [];
64
+ const paths = txContext.path.split('/');
65
+ const entityIDs = _getEntityIDs(txContext.params);
66
+
67
+ for (let idx = 0; idx < paths.length; idx++) {
68
+ const entity = getEntityByContextPath(paths.slice(0, idx + 1), txContext.hasComp);
69
+ const entityID = entityIDs[idx];
70
+ const entityPathVal = `${entity.name}(${entityID})`;
71
+ pathVals.push(entityPathVal);
72
+ }
73
+
74
+ return pathVals;
75
+ };
88
76
 
89
77
  function convertSubjectToParams(subject) {
90
- let params = [];
91
- let subjectRef = [];
92
- subject?.ref?.forEach((item)=>{
93
- if (typeof item === 'string') {
94
- subjectRef.push(item)
95
- return
96
- }
97
-
98
- const keys = {}
99
- let id = item.id
100
- if (!id) return
101
- for (let j = 0; j < item?.where?.length; j = j + 4) {
102
- const key = item.where[j].ref[0]
103
- const value = item.where[j + 2].val
104
- if (key !== 'IsActiveEntity') keys[key] = value
105
- }
106
- params.push(keys);
107
- })
108
- return params.length > 0 ? params : subjectRef;
78
+ let params = [];
79
+ let subjectRef = [];
80
+ subject?.ref?.forEach((item) => {
81
+ if (typeof item === 'string') {
82
+ subjectRef.push(item);
83
+ return;
84
+ }
85
+
86
+ const keys = {};
87
+ let id = item.id;
88
+ if (!id) return;
89
+ for (let j = 0; j < item?.where?.length; j = j + 4) {
90
+ const key = item.where[j].ref[0];
91
+ const value = item.where[j + 2].val;
92
+ if (key !== 'IsActiveEntity') keys[key] = value;
93
+ }
94
+ params.push(keys);
95
+ });
96
+ return params.length > 0 ? params : subjectRef;
109
97
  }
110
98
 
111
99
  const _getEntityIDs = function (txParams) {
112
- const entityIDs = []
113
- for (const param of txParams) {
114
- let id = ""
115
- if (typeof param === "object" && !Array.isArray(param)) {
116
- id = param.ID
117
- }
118
- if (typeof param === "string") {
119
- id = param
120
- }
121
- if (id) {
122
- entityIDs.push(id)
123
- }
124
- }
125
- return entityIDs
126
- }
100
+ const entityIDs = [];
101
+ for (const param of txParams) {
102
+ let id = '';
103
+ if (typeof param === 'object' && !Array.isArray(param)) {
104
+ id = param.ID;
105
+ }
106
+ if (typeof param === 'string') {
107
+ id = param;
108
+ }
109
+ if (id) {
110
+ entityIDs.push(id);
111
+ }
112
+ }
113
+ return entityIDs;
114
+ };
127
115
 
128
116
  /**
129
117
  *
@@ -142,434 +130,409 @@ const _getEntityIDs = function (txParams) {
142
130
  * }
143
131
  */
144
132
  const _formatAssociationContext = async function (changes, reqData) {
145
- for (const change of changes) {
146
- const a = cds.model.definitions[change.serviceEntity].elements[change.attribute]
147
- if (a?.type !== "cds.Association") continue
148
-
149
- const semkeys = getObjIdElementNamesInArray(a["@changelog"])
150
- if (!semkeys.length) continue
151
-
152
- const ID = a.keys[0].ref[0] || 'ID'
153
- const [ [from], [to] ] = await cds.db.run (cds.env.requires['change-tracking'].considerLocalizedValues ? [
154
- SELECT.localized.from(a.target).where({ [ID]: change.valueChangedFrom }).limit(1),
155
- SELECT.localized.from(a.target).where({ [ID]: change.valueChangedTo }).limit(1)
156
- ] : [
157
- SELECT.from(a.target).where({ [ID]: change.valueChangedFrom }).limit(1),
158
- SELECT.from(a.target).where({ [ID]: change.valueChangedTo }).limit(1)
159
- ])
160
-
161
- const fromObjId = await getObjectId(reqData, a.target, semkeys, { curObjFromDbQuery: from || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
162
- if (fromObjId) change.valueChangedFrom = fromObjId
163
-
164
- const toObjId = await getObjectId(reqData, a.target, semkeys, { curObjFromDbQuery: to || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
165
- if (toObjId) change.valueChangedTo = toObjId
166
-
167
- const isVLvA = a["@Common.ValueList.viaAssociation"]
168
- if (!isVLvA) change.valueDataType = getValueEntityType(a.target, semkeys)
169
- }
170
- }
171
-
172
- const _getChildChangeObjId = async function (
173
- change,
174
- childNodeChange,
175
- curNodePathVal,
176
- reqData,
177
- target
178
- ) {
179
- const composition = cds.model.definitions[change.serviceEntity].elements[change.attribute]
180
- const objIdElements = composition ? composition["@changelog"] : null
181
- const objIdElementNames = getObjIdElementNamesInArray(objIdElements)
182
-
183
- return _getObjectIdByPath(
184
- reqData,
185
- curNodePathVal,
186
- childNodeChange._path,
187
- target,
188
- objIdElementNames
189
- )
190
- }
133
+ for (const change of changes) {
134
+ const a = cds.model.definitions[change.serviceEntity].elements[change.attribute];
135
+ if (a?.type !== 'cds.Association') continue;
136
+
137
+ const semkeys = getObjIdElementNamesInArray(a['@changelog']);
138
+ if (!semkeys.length) continue;
139
+
140
+ const ID = a.keys[0].ref[0] || 'ID';
141
+ const [[from], [to]] = await cds.db.run(
142
+ cds.env.requires['change-tracking'].considerLocalizedValues
143
+ ? [
144
+ SELECT.localized
145
+ .from(a.target)
146
+ .where({ [ID]: change.valueChangedFrom })
147
+ .limit(1),
148
+ SELECT.localized
149
+ .from(a.target)
150
+ .where({ [ID]: change.valueChangedTo })
151
+ .limit(1)
152
+ ]
153
+ : [
154
+ SELECT.from(a.target)
155
+ .where({ [ID]: change.valueChangedFrom })
156
+ .limit(1),
157
+ SELECT.from(a.target)
158
+ .where({ [ID]: change.valueChangedTo })
159
+ .limit(1)
160
+ ]
161
+ );
162
+
163
+ const fromObjId = await getObjectId(reqData, a.target, semkeys, { curObjFromDbQuery: from || undefined }); // Note: ... || undefined is important for subsequent object destructuring with defaults
164
+ if (fromObjId) change.valueChangedFrom = fromObjId;
165
+
166
+ const toObjId = await getObjectId(reqData, a.target, semkeys, { curObjFromDbQuery: to || undefined }); // Note: ... || undefined is important for subsequent object destructuring with defaults
167
+ if (toObjId) change.valueChangedTo = toObjId;
168
+
169
+ const isVLvA = a['@Common.ValueList.viaAssociation'];
170
+ if (!isVLvA) change.valueDataType = getValueEntityType(a.target, semkeys);
171
+ }
172
+ };
173
+
174
+ const _getChildChangeObjId = async function (change, childNodeChange, curNodePathVal, reqData, target) {
175
+ const composition = cds.model.definitions[change.serviceEntity].elements[change.attribute];
176
+ const objIdElements = composition ? composition['@changelog'] : null;
177
+ const objIdElementNames = getObjIdElementNamesInArray(objIdElements);
178
+
179
+ return _getObjectIdByPath(reqData, curNodePathVal, childNodeChange._path, target, objIdElementNames);
180
+ };
191
181
 
192
182
  const _formatCompositionContext = async function (changes, reqData, target) {
193
- const childNodeChanges = []
194
-
195
- for (const change of changes) {
196
- if (typeof change.valueChangedTo === "object" && change.valueChangedTo instanceof Date !== true) {
197
- if (!Array.isArray(change.valueChangedTo)) {
198
- change.valueChangedTo = [change.valueChangedTo]
199
- }
200
- for (const childNodeChange of change.valueChangedTo) {
201
- const curChange = Object.assign({}, change)
202
- const path = splitPath(childNodeChange._path)
203
- const curNodePathVal = path.pop()
204
- curChange.modification = childNodeChange._op
205
- const objId = await _getChildChangeObjId(
206
- change,
207
- childNodeChange,
208
- curNodePathVal,
209
- reqData,
210
- target
211
- )
212
- _formatCompositionValue(curChange, objId, childNodeChange, childNodeChanges)
213
- }
214
- change.valueChangedTo = undefined
215
- }
216
- }
217
- changes.push(...childNodeChanges)
218
- }
219
-
220
- const _formatCompositionValue = function (
221
- curChange,
222
- objId,
223
- childNodeChange,
224
- childNodeChanges
225
- ) {
226
- if (curChange.modification === undefined) {
227
- return
228
- } else if (curChange.modification === "delete") {
229
- curChange.valueChangedFrom = objId
230
- curChange.valueChangedTo = ""
231
- } else if (curChange.modification === "update") {
232
- curChange.valueChangedFrom = objId
233
- curChange.valueChangedTo = objId
234
- } else {
235
- curChange.valueChangedFrom = ""
236
- curChange.valueChangedTo = objId
237
- }
238
- curChange.valueDataType = _formatCompositionEntityType(curChange)
239
- // Since req.diff() will record the managed data, change history will filter those logs only be changed managed data
240
- const managedAttrs = ["modifiedAt", "modifiedBy"]
241
- if (curChange.modification === "update") {
242
- const rowOldAttrs = Object.keys(childNodeChange._old)
243
- const diffAttrs = rowOldAttrs.filter((attr) => managedAttrs.indexOf(attr) === -1)
244
- if (!diffAttrs.length) {
245
- return
246
- }
247
- }
248
- childNodeChanges.push(curChange)
249
- }
183
+ const childNodeChanges = [];
184
+
185
+ for (const change of changes) {
186
+ if (typeof change.valueChangedTo === 'object' && change.valueChangedTo instanceof Date !== true) {
187
+ if (!Array.isArray(change.valueChangedTo)) {
188
+ change.valueChangedTo = [change.valueChangedTo];
189
+ }
190
+ for (const childNodeChange of change.valueChangedTo) {
191
+ const curChange = Object.assign({}, change);
192
+ const path = splitPath(childNodeChange._path);
193
+ const curNodePathVal = path.pop();
194
+ curChange.modification = childNodeChange._op;
195
+ const objId = await _getChildChangeObjId(change, childNodeChange, curNodePathVal, reqData, target);
196
+ _formatCompositionValue(curChange, objId, childNodeChange, childNodeChanges);
197
+ }
198
+ change.valueChangedTo = undefined;
199
+ }
200
+ }
201
+ changes.push(...childNodeChanges);
202
+ };
203
+
204
+ const _formatCompositionValue = function (curChange, objId, childNodeChange, childNodeChanges) {
205
+ if (curChange.modification === undefined) {
206
+ return;
207
+ } else if (curChange.modification === 'delete') {
208
+ curChange.valueChangedFrom = objId;
209
+ curChange.valueChangedTo = '';
210
+ } else if (curChange.modification === 'update') {
211
+ curChange.valueChangedFrom = objId;
212
+ curChange.valueChangedTo = objId;
213
+ } else {
214
+ curChange.valueChangedFrom = '';
215
+ curChange.valueChangedTo = objId;
216
+ }
217
+ curChange.valueDataType = _formatCompositionEntityType(curChange);
218
+ // Since req.diff() will record the managed data, change history will filter those logs only be changed managed data
219
+ const managedAttrs = ['modifiedAt', 'modifiedBy'];
220
+ if (curChange.modification === 'update') {
221
+ const rowOldAttrs = Object.keys(childNodeChange._old);
222
+ const diffAttrs = rowOldAttrs.filter((attr) => managedAttrs.indexOf(attr) === -1);
223
+ if (!diffAttrs.length) {
224
+ return;
225
+ }
226
+ }
227
+ childNodeChanges.push(curChange);
228
+ };
250
229
 
251
230
  const _formatCompositionEntityType = function (change) {
252
- const composition = cds.model.definitions[change.serviceEntity].elements[change.attribute]
253
- const objIdElements = composition ? composition['@changelog'] : null
254
-
255
- if (Array.isArray(objIdElements)) {
256
- // In this case, the attribute is a composition
257
- const objIdElementNames = getObjIdElementNamesInArray(objIdElements)
258
- return getValueEntityType(composition.target, objIdElementNames)
259
- }
260
- return ""
261
- }
262
-
263
- const _getObjectIdByPath = async function (
264
- reqData,
265
- nodePathVal,
266
- serviceEntityPath,
267
- target,
268
- /**optional*/ objIdElementNames
269
- ) {
270
- const curObjFromReqData = getCurObjFromReqData(reqData, nodePathVal, serviceEntityPath)
271
- const entityName = getNameFromPathVal(nodePathVal)
272
- const entityUUID = getUUIDFromPathVal(nodePathVal)
273
- const obj = await getCurObjFromDbQuery(entityName, {[retrieveFirstKey(target)]: entityUUID})
274
- const curObj = { curObjFromReqData, curObjFromDbQuery: obj }
275
- return getObjectId(reqData, entityName, objIdElementNames, curObj)
276
- }
231
+ const composition = cds.model.definitions[change.serviceEntity].elements[change.attribute];
232
+ const objIdElements = composition ? composition['@changelog'] : null;
233
+
234
+ if (Array.isArray(objIdElements)) {
235
+ // In this case, the attribute is a composition
236
+ const objIdElementNames = getObjIdElementNamesInArray(objIdElements);
237
+ return getValueEntityType(composition.target, objIdElementNames);
238
+ }
239
+ return '';
240
+ };
241
+
242
+ const _getObjectIdByPath = async function (reqData, nodePathVal, serviceEntityPath, target, /**optional*/ objIdElementNames) {
243
+ const curObjFromReqData = getCurObjFromReqData(reqData, nodePathVal, serviceEntityPath);
244
+ const entityName = getNameFromPathVal(nodePathVal);
245
+ const entityUUID = getUUIDFromPathVal(nodePathVal);
246
+ const obj = await getCurObjFromDbQuery(entityName, { [retrieveFirstKey(target)]: entityUUID });
247
+ const curObj = { curObjFromReqData, curObjFromDbQuery: obj };
248
+ return getObjectId(reqData, entityName, objIdElementNames, curObj);
249
+ };
277
250
 
278
251
  const _formatObjectID = async function (changes, reqData, target) {
279
- const objectIdCache = new Map()
280
- for (const change of changes) {
281
- const path = splitPath(change.serviceEntityPath)
282
- const curNodePathVal = path.pop()
283
- const parentNodePathVal = path.pop()
284
-
285
- let curNodeObjId = objectIdCache.get(curNodePathVal)
286
- if (!curNodeObjId) {
287
- curNodeObjId = await _getObjectIdByPath(
288
- reqData,
289
- curNodePathVal,
290
- change.serviceEntityPath,
291
- target
292
- )
293
- objectIdCache.set(curNodePathVal, curNodeObjId)
294
- }
295
-
296
- let parentNodeObjId = objectIdCache.get(parentNodePathVal)
297
- if (!parentNodeObjId && parentNodePathVal) {
298
- parentNodeObjId = await _getObjectIdByPath(
299
- reqData,
300
- parentNodePathVal,
301
- change.serviceEntityPath,
302
- target
303
- )
304
- objectIdCache.set(parentNodePathVal, parentNodeObjId)
305
- }
306
-
307
- change.entityID = curNodeObjId
308
- change.parentEntityID = parentNodeObjId
309
- change.parentKey = getUUIDFromPathVal(parentNodePathVal)
310
- }
311
- }
252
+ const objectIdCache = new Map();
253
+ for (const change of changes) {
254
+ const path = splitPath(change.serviceEntityPath);
255
+ const curNodePathVal = path.pop();
256
+ const parentNodePathVal = path.pop();
257
+
258
+ let curNodeObjId = objectIdCache.get(curNodePathVal);
259
+ if (!curNodeObjId) {
260
+ curNodeObjId = await _getObjectIdByPath(reqData, curNodePathVal, change.serviceEntityPath, target);
261
+ objectIdCache.set(curNodePathVal, curNodeObjId);
262
+ }
263
+
264
+ let parentNodeObjId = objectIdCache.get(parentNodePathVal);
265
+ if (!parentNodeObjId && parentNodePathVal) {
266
+ parentNodeObjId = await _getObjectIdByPath(reqData, parentNodePathVal, change.serviceEntityPath, target);
267
+ objectIdCache.set(parentNodePathVal, parentNodeObjId);
268
+ }
269
+
270
+ change.entityID = curNodeObjId;
271
+ change.parentEntityID = parentNodeObjId;
272
+ change.parentKey = getUUIDFromPathVal(parentNodePathVal);
273
+ }
274
+ };
312
275
 
313
276
  const _isCompositionContextPath = function (aPath, hasComp) {
314
- if (!aPath) return
315
- if (typeof aPath === 'string') aPath = aPath.split('/')
316
- if (aPath.length < 2) return false
317
- const target = getEntityByContextPath(aPath, hasComp)
318
- const parent = getEntityByContextPath(aPath.slice(0, -1), hasComp)
319
- if (!parent.compositions) return false
320
- return Object.values(parent.compositions).some(c => c._target === target)
321
- }
277
+ if (!aPath) return;
278
+ if (typeof aPath === 'string') aPath = aPath.split('/');
279
+ if (aPath.length < 2) return false;
280
+ const target = getEntityByContextPath(aPath, hasComp);
281
+ const parent = getEntityByContextPath(aPath.slice(0, -1), hasComp);
282
+ if (!parent.compositions) return false;
283
+ return Object.values(parent.compositions).some((c) => c._target === target);
284
+ };
322
285
 
323
286
  const _formatChangeLog = async function (changes, req) {
324
- await _formatObjectID(changes, req.data, req.target)
325
- await _formatAssociationContext(changes, req.data)
326
- await _formatCompositionContext(changes, req.data, req.target)
327
- }
287
+ await _formatObjectID(changes, req.data, req.target);
288
+ await _formatAssociationContext(changes, req.data);
289
+ await _formatCompositionContext(changes, req.data, req.target);
290
+ };
328
291
 
329
292
  const _afterReadChangeView = function (data, req) {
330
- if (!data) return
331
- if (!Array.isArray(data)) data = [data]
332
- localizeLogFields(data, req.locale)
333
- }
334
-
335
- function _trackedChanges4 (srv, target, diff) {
336
- const template = getTemplate("change-logging", srv, target, { pick: e => e['@changelog'] })
337
- if (!template.elements.size) return
338
-
339
- const changes = []
340
- diff._path = `${target.name}(${diff[retrieveFirstKey(target)]})`
341
-
342
- templateProcessor({
343
- template, row: diff, processFn: ({ row, key, element }) => {
344
- const from = row._old?.[key]
345
- const to = row[key]
346
- const eleParentKeys = element.parent.keys
347
- if (from === to) return
348
-
349
- /**
350
- *
351
- * HANA driver always filling up the defined decimal places with zeros,
352
- * need to skip the change log if the value is not changed.
353
- * Example:
354
- * entity Books : cuid {
355
- * price : Decimal(11, 4);
356
- * }
357
- * When price is updated from 3000.0000 to 3000,
358
- * the change log should not be created.
359
- */
360
- if (
361
- row._op === "update" &&
362
- element.type === "cds.Decimal" &&
363
- cds.db.kind === "hana" &&
364
- typeof to === "number"
365
- ) {
366
- const scaleNum = element.scale || 0;
367
- if (from === formatDecimal(to, scaleNum))
368
- return;
369
- }
370
-
371
- /**
372
- *
373
- * For the Inline entity such as Items,
374
- * further filtering is required on the keys
375
- * within the 'association' and 'foreign key' to ultimately retain the keys of the entity itself.
376
- * entity Order : cuid {
377
- * title : String;
378
- * Items : Composition of many {
379
- * key ID : UUID;
380
- * quantity : Integer;
381
- * }
382
- * }
383
- */
384
- const keys = Object.keys(eleParentKeys)
385
- .filter(k => k !== "IsActiveEntity")
386
- .filter(k => eleParentKeys[k]?.type !== "cds.Association") // Skip association
387
- .filter(k => !eleParentKeys[k]?.["@odata.foreignKey4"]) // Skip foreign key
388
- .map(k => `${k}=${row[k]}`)
389
- .join(', ')
390
-
391
- changes.push({
392
- serviceEntityPath: row._path,
393
- entity: getDBEntity(element.parent).name,
394
- serviceEntity: element.parent.name,
395
- attribute: element["@odata.foreignKey4"] || key,
396
- valueChangedFrom: from?? '',
397
- valueChangedTo: to?? '',
398
- valueDataType: element.type,
399
- modification: row._op,
400
- keys,
401
- })
402
- }
403
- })
404
-
405
- return changes.length && changes
293
+ if (!data) return;
294
+ if (!Array.isArray(data)) data = [data];
295
+ localizeLogFields(data, req.locale);
296
+ };
297
+
298
+ function _trackedChanges4(srv, target, diff) {
299
+ const template = getTemplate('change-logging', srv, target, { pick: (e) => e['@changelog'] });
300
+ if (!template.elements.size) return;
301
+
302
+ const changes = [];
303
+ diff._path = `${target.name}(${diff[retrieveFirstKey(target)]})`;
304
+
305
+ templateProcessor({
306
+ template,
307
+ row: diff,
308
+ processFn: ({ row, key, element }) => {
309
+ const from = row._old?.[key];
310
+ const to = row[key];
311
+ const eleParentKeys = element.parent.keys;
312
+ if (from === to) return;
313
+
314
+ /**
315
+ *
316
+ * HANA driver always filling up the defined decimal places with zeros,
317
+ * need to skip the change log if the value is not changed.
318
+ * Example:
319
+ * entity Books : cuid {
320
+ * price : Decimal(11, 4);
321
+ * }
322
+ * When price is updated from 3000.0000 to 3000,
323
+ * the change log should not be created.
324
+ */
325
+ if (row._op === 'update' && element.type === 'cds.Decimal' && cds.db.kind === 'hana' && typeof to === 'number') {
326
+ const scaleNum = element.scale || 0;
327
+ if (from === formatDecimal(to, scaleNum)) return;
328
+ }
329
+
330
+ /**
331
+ *
332
+ * For the Inline entity such as Items,
333
+ * further filtering is required on the keys
334
+ * within the 'association' and 'foreign key' to ultimately retain the keys of the entity itself.
335
+ * entity Order : cuid {
336
+ * title : String;
337
+ * Items : Composition of many {
338
+ * key ID : UUID;
339
+ * quantity : Integer;
340
+ * }
341
+ * }
342
+ */
343
+ const keys = Object.keys(eleParentKeys)
344
+ .filter((k) => k !== 'IsActiveEntity')
345
+ .filter((k) => eleParentKeys[k]?.type !== 'cds.Association') // Skip association
346
+ .filter((k) => !eleParentKeys[k]?.['@odata.foreignKey4']) // Skip foreign key
347
+ .map((k) => `${k}=${row[k]}`)
348
+ .join(', ');
349
+
350
+ changes.push({
351
+ serviceEntityPath: row._path,
352
+ entity: getDBEntity(element.parent).name,
353
+ serviceEntity: element.parent.name,
354
+ attribute: element['@odata.foreignKey4'] || key,
355
+ valueChangedFrom: from ?? '',
356
+ valueChangedTo: to ?? '',
357
+ valueDataType: element.type,
358
+ modification: row._op,
359
+ keys
360
+ });
361
+ }
362
+ });
363
+
364
+ return changes.length && changes;
406
365
  }
407
366
 
408
367
  const _prepareChangeLogForComposition = async function (entity, entityKey, changes, req) {
409
- const rootEntityPathVals = _getRootEntityPathVals(req.context, entity, entityKey)
410
-
411
- if (rootEntityPathVals.length < 2) {
412
- LOG.info("Parent entity doesn't exist.")
413
- return
414
- }
415
-
416
- const parentEntityPathVal = rootEntityPathVals[rootEntityPathVals.length - 2]
417
- const parentKey = getUUIDFromPathVal(parentEntityPathVal)
418
- const serviceEntityPath = rootEntityPathVals.join('/')
419
- const parentServiceEntityPath = _getAllPathVals(req.context)
420
- .slice(0, rootEntityPathVals.length - 2)
421
- .join('/')
422
-
423
- for (const change of changes) {
424
- change.parentEntityID = await _getObjectIdByPath(req.data, parentEntityPathVal, parentServiceEntityPath, entity)
425
- change.parentKey = parentKey
426
- change.serviceEntityPath = serviceEntityPath
427
- }
428
-
429
- const rootEntity = getNameFromPathVal(rootEntityPathVals[0])
430
- const rootEntityID = getUUIDFromPathVal(rootEntityPathVals[0])
431
- return [ rootEntity, rootEntityID ]
368
+ const rootEntityPathVals = _getRootEntityPathVals(req.context, entity, entityKey);
369
+
370
+ if (rootEntityPathVals.length < 2) {
371
+ LOG.info("Parent entity doesn't exist.");
372
+ return;
373
+ }
374
+
375
+ const parentEntityPathVal = rootEntityPathVals[rootEntityPathVals.length - 2];
376
+ const parentKey = getUUIDFromPathVal(parentEntityPathVal);
377
+ const serviceEntityPath = rootEntityPathVals.join('/');
378
+ const parentServiceEntityPath = _getAllPathVals(req.context)
379
+ .slice(0, rootEntityPathVals.length - 2)
380
+ .join('/');
381
+
382
+ for (const change of changes) {
383
+ change.parentEntityID = await _getObjectIdByPath(req.data, parentEntityPathVal, parentServiceEntityPath, entity);
384
+ change.parentKey = parentKey;
385
+ change.serviceEntityPath = serviceEntityPath;
386
+ }
387
+
388
+ const rootEntity = getNameFromPathVal(rootEntityPathVals[0]);
389
+ const rootEntityID = getUUIDFromPathVal(rootEntityPathVals[0]);
390
+ return [rootEntity, rootEntityID];
391
+ };
392
+
393
+ async function generatePathAndParams(req, entityKey) {
394
+ const { target, data } = req;
395
+ const { ID, foreignKey, parentEntity } = getAssociationDetails(target);
396
+ const hasParentAndForeignKey = parentEntity && data[foreignKey];
397
+ const targetEntity = hasParentAndForeignKey ? parentEntity : target;
398
+ const targetKey = hasParentAndForeignKey ? data[foreignKey] : entityKey;
399
+
400
+ let compContext = {
401
+ path: hasParentAndForeignKey ? `${parentEntity.name}/${target.name}` : `${target.name}`,
402
+ params: hasParentAndForeignKey ? [{ [ID]: data[foreignKey] }, { [ID]: entityKey }] : [{ [ID]: entityKey }],
403
+ hasComp: true
404
+ };
405
+
406
+ if (hasParentAndForeignKey && isRootEntity(parentEntity)) {
407
+ return compContext;
408
+ }
409
+
410
+ let parentAssoc = await processEntity(targetEntity, targetKey, compContext);
411
+ while (parentAssoc && !isRootEntity(parentAssoc.entity)) {
412
+ parentAssoc = await processEntity(parentAssoc.entity, parentAssoc.ID, compContext);
413
+ }
414
+ return compContext;
432
415
  }
433
416
 
434
- async function generatePathAndParams (req, entityKey) {
435
- const { target, data } = req;
436
- const { ID, foreignKey, parentEntity } = getAssociationDetails(target);
437
- const hasParentAndForeignKey = parentEntity && data[foreignKey];
438
- const targetEntity = hasParentAndForeignKey ? parentEntity : target;
439
- const targetKey = hasParentAndForeignKey ? data[foreignKey] : entityKey;
440
-
441
- let compContext = {
442
- path: hasParentAndForeignKey
443
- ? `${parentEntity.name}/${target.name}`
444
- : `${target.name}`,
445
- params: hasParentAndForeignKey
446
- ? [{ [ID]: data[foreignKey] }, { [ID]: entityKey }]
447
- : [{ [ID]: entityKey }],
448
- hasComp: true
449
- };
450
-
451
- if (hasParentAndForeignKey && parentEntity[isRoot]) {
452
- return compContext;
453
- }
454
-
455
- let parentAssoc = await processEntity(targetEntity, targetKey, compContext);
456
- while (parentAssoc && !parentAssoc.entity[isRoot]) {
457
- parentAssoc = await processEntity(
458
- parentAssoc.entity,
459
- parentAssoc.ID,
460
- compContext
461
- );
462
- }
463
- return compContext;
417
+ async function processEntity(entity, entityKey, compContext) {
418
+ if (!entity || !entityKey || !compContext) return;
419
+
420
+ const { ID, foreignKey, parentEntity } = getAssociationDetails(entity);
421
+
422
+ if (!foreignKey || !parentEntity) return;
423
+
424
+ const parentResult = await SELECT.one
425
+ .from(entity.name)
426
+ .where({ [ID]: entityKey })
427
+ .columns(foreignKey);
428
+
429
+ if (!parentResult || typeof parentResult !== 'object') return;
430
+
431
+ const hasForeignKey = parentResult[foreignKey];
432
+ if (!hasForeignKey) return;
433
+
434
+ compContext.path = `${parentEntity.name}/${compContext.path}`;
435
+ compContext.params.unshift({ [ID]: hasForeignKey });
436
+
437
+ return {
438
+ entity: parentEntity,
439
+ [ID]: hasForeignKey
440
+ };
464
441
  }
465
442
 
466
- async function processEntity (entity, entityKey, compContext) {
467
- const { ID, foreignKey, parentEntity } = getAssociationDetails(entity);
468
-
469
- if (foreignKey && parentEntity) {
470
- const parentResult =
471
- (await SELECT.one
472
- .from(entity.name)
473
- .where({ [ID]: entityKey })
474
- .columns(foreignKey)) || {};
475
- const hasForeignKey = parentResult[foreignKey];
476
- if (!hasForeignKey) return;
477
- compContext.path = `${parentEntity.name}/${compContext.path}`;
478
- compContext.params.unshift({ [ID]: parentResult[foreignKey] });
479
- return {
480
- entity: parentEntity,
481
- [ID]: hasForeignKey ? parentResult[foreignKey] : undefined
482
- };
483
- }
443
+ function getAssociationDetails(entity) {
444
+ if (!entity || typeof entity !== 'object') return {};
445
+
446
+ const { name } = entity;
447
+ if (!name || typeof name !== 'string') return {};
448
+
449
+ const definition = cds.model.definitions[name];
450
+ if (!definition) return {};
451
+
452
+ const assocName = entity[hasParent]?.associationName ?? definition[hasParent]?.associationName;
453
+ if (!assocName) return {};
454
+
455
+ const elements = entity.elements || {};
456
+ const assoc = elements[assocName];
457
+ if (!assoc) return {};
458
+
459
+ const parentEntity = assoc._target;
460
+ const foreignKey = assoc.keys?.[0]?.$generatedFieldName;
461
+ const ID = assoc.keys?.[0]?.ref?.[0] ?? 'ID';
462
+
463
+ return { ID, foreignKey, parentEntity };
484
464
  }
485
465
 
486
- function getAssociationDetails (entity) {
487
- if (!entity) return {};
488
- const assocName = entity['change-tracking-parentEntity']?.associationName;
489
- const assoc = entity.elements[assocName];
490
- const parentEntity = assoc?._target;
491
- const foreignKey = assoc?.keys?.[0]?.$generatedFieldName;
492
- const ID = assoc?.keys?.[0]?.ref[0] || 'ID';
493
- return { ID, foreignKey, parentEntity };
466
+ function isRootEntity(entity) {
467
+ return entity[isRoot] || cds.model.definitions[entity.name]?.[isRoot] || false;
494
468
  }
495
469
 
496
470
  function isEmpty(value) {
497
- return value === null || value === undefined || value === "";
471
+ return value === null || value === undefined || value === '';
498
472
  }
499
473
 
500
- async function track_changes (req) {
501
- const config = cds.env.requires["change-tracking"];
502
-
503
- if (
504
- (req.event === 'UPDATE' && config?.disableUpdateTracking) ||
505
- (req.event === 'CREATE' && config?.disableCreateTracking) ||
506
- (req.event === 'DELETE' && config?.disableDeleteTracking)
507
- ) {
508
- return;
509
- }
474
+ async function track_changes(req) {
475
+ const config = cds.env.requires['change-tracking'];
510
476
 
511
- let diff = await req.diff()
512
- if (!diff) return
477
+ if ((req.event === 'UPDATE' && config?.disableUpdateTracking) || (req.event === 'CREATE' && config?.disableCreateTracking) || (req.event === 'DELETE' && config?.disableDeleteTracking)) {
478
+ return;
479
+ }
513
480
 
514
- const diffs = Array.isArray(diff) ? diff : [diff];
515
- const changes = (
516
- await Promise.all(diffs.map(item => trackChangesForDiff(item, req, this)))
517
- ).filter(Boolean);
481
+ let diff = await req.diff();
482
+ if (!diff) return;
518
483
 
519
- if (changes.length > 0) {
520
- await INSERT.into("sap.changelog.ChangeLog").entries(changes);
521
- }
484
+ const diffs = Array.isArray(diff) ? diff : [diff];
485
+ const changes = (await Promise.all(diffs.map((item) => trackChangesForDiff(item, req, this)))).filter(Boolean);
522
486
 
487
+ if (changes.length > 0) {
488
+ await INSERT.into('sap.changelog.ChangeLog').entries(changes);
489
+ }
523
490
  }
524
491
 
525
- async function trackChangesForDiff(diff, req, that){
526
- let target = req.target
527
- let compContext = null;
528
- let entityKey = diff[retrieveFirstKey(req.target)]
529
- const params = convertSubjectToParams(req.subject);
530
- if (req.subject.ref.length === 1 && params.length === 1 && !target[isRoot]) {
531
- compContext = await generatePathAndParams(req, entityKey);
532
- }
533
- let isComposition = _isCompositionContextPath(
534
- compContext?.path || req.path,
535
- compContext?.hasComp
536
- );
537
- if (
538
- req.event === "DELETE" &&
539
- target[isRoot] &&
540
- !cds.env.requires["change-tracking"]?.preserveDeletes
541
- ) {
542
- await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey });
543
- return;
544
- }
545
-
546
- let changes = _trackedChanges4(that, target, diff)
547
- if (!changes) return
548
-
549
- await _formatChangeLog(changes, req)
550
- if (isComposition) {
551
- let reqInfo = {
552
- data: req.data,
553
- context: {
554
- path: compContext?.path || req.path,
555
- params: compContext?.params || params,
556
- event: req.event,
557
- hasComp: compContext?.hasComp
558
- }
559
- };
560
- [ target, entityKey ] = await _prepareChangeLogForComposition(target, entityKey, changes, reqInfo)
561
- }
562
- const dbEntity = getDBEntity(target)
563
- return {
564
- entity: dbEntity.name,
565
- entityKey: entityKey,
566
- serviceEntity: target.name || target,
567
- changes: changes.filter(c => !isEmpty(c.valueChangedFrom) || !isEmpty(c.valueChangedTo)).map((c) => ({
568
- ...c,
569
- valueChangedFrom: `${c.valueChangedFrom ?? ''}`,
570
- valueChangedTo: `${c.valueChangedTo ?? ''}`,
571
- })),
572
- };
492
+ async function trackChangesForDiff(diff, req, that) {
493
+ let target = req.target;
494
+ let compContext = null;
495
+ let entityKey = diff[retrieveFirstKey(target)];
496
+ let isTopLevel = isRootEntity(target);
497
+ const params = convertSubjectToParams(req.subject);
498
+ if (req.subject.ref.length === 1 && params.length === 1 && !isTopLevel) {
499
+ compContext = await generatePathAndParams(req, entityKey);
500
+ }
501
+ let isComposition = _isCompositionContextPath(compContext?.path || req.path, compContext?.hasComp);
502
+ if (req.event === 'DELETE' && isTopLevel && !cds.env.requires['change-tracking']?.preserveDeletes) {
503
+ await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey });
504
+ return;
505
+ }
506
+
507
+ let changes = _trackedChanges4(that, target, diff);
508
+ if (!changes) return;
509
+
510
+ await _formatChangeLog(changes, req);
511
+ if (isComposition) {
512
+ let reqInfo = {
513
+ data: req.data,
514
+ context: {
515
+ path: compContext?.path || req.path,
516
+ params: compContext?.params || params,
517
+ event: req.event,
518
+ hasComp: compContext?.hasComp
519
+ }
520
+ };
521
+ [target, entityKey] = await _prepareChangeLogForComposition(target, entityKey, changes, reqInfo);
522
+ }
523
+ const dbEntity = getDBEntity(target);
524
+ return {
525
+ entity: dbEntity.name,
526
+ entityKey: entityKey,
527
+ serviceEntity: target.name || target,
528
+ changes: changes
529
+ .filter((c) => !isEmpty(c.valueChangedFrom) || !isEmpty(c.valueChangedTo))
530
+ .map((c) => ({
531
+ ...c,
532
+ valueChangedFrom: `${c.valueChangedFrom ?? ''}`,
533
+ valueChangedTo: `${c.valueChangedTo ?? ''}`
534
+ }))
535
+ };
573
536
  }
574
537
 
575
- module.exports = { track_changes, _afterReadChangeView }
538
+ module.exports = { track_changes, _afterReadChangeView };