@cap-js/change-tracking 1.1.4 → 2.0.0-beta.2

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 (70) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +45 -71
  3. package/_i18n/i18n.properties +10 -22
  4. package/_i18n/i18n_ar.properties +3 -3
  5. package/_i18n/i18n_bg.properties +3 -3
  6. package/_i18n/i18n_cs.properties +3 -3
  7. package/_i18n/i18n_da.properties +3 -3
  8. package/_i18n/i18n_de.properties +3 -3
  9. package/_i18n/i18n_el.properties +3 -3
  10. package/_i18n/i18n_en.properties +3 -3
  11. package/_i18n/i18n_en_US_saptrc.properties +3 -32
  12. package/_i18n/i18n_es.properties +3 -3
  13. package/_i18n/i18n_es_MX.properties +3 -3
  14. package/_i18n/i18n_fi.properties +3 -3
  15. package/_i18n/i18n_fr.properties +3 -3
  16. package/_i18n/i18n_he.properties +3 -3
  17. package/_i18n/i18n_hr.properties +3 -3
  18. package/_i18n/i18n_hu.properties +3 -3
  19. package/_i18n/i18n_it.properties +3 -3
  20. package/_i18n/i18n_ja.properties +3 -3
  21. package/_i18n/i18n_kk.properties +3 -3
  22. package/_i18n/i18n_ko.properties +3 -3
  23. package/_i18n/i18n_ms.properties +3 -3
  24. package/_i18n/i18n_nl.properties +3 -3
  25. package/_i18n/i18n_no.properties +3 -3
  26. package/_i18n/i18n_pl.properties +3 -3
  27. package/_i18n/i18n_pt.properties +3 -3
  28. package/_i18n/i18n_ro.properties +3 -3
  29. package/_i18n/i18n_ru.properties +3 -3
  30. package/_i18n/i18n_sh.properties +3 -3
  31. package/_i18n/i18n_sk.properties +3 -3
  32. package/_i18n/i18n_sl.properties +3 -3
  33. package/_i18n/i18n_sv.properties +3 -3
  34. package/_i18n/i18n_th.properties +3 -3
  35. package/_i18n/i18n_tr.properties +3 -3
  36. package/_i18n/i18n_uk.properties +3 -3
  37. package/_i18n/i18n_vi.properties +3 -3
  38. package/_i18n/i18n_zh_CN.properties +3 -3
  39. package/_i18n/i18n_zh_TW.properties +3 -3
  40. package/cds-plugin.js +16 -263
  41. package/index.cds +187 -76
  42. package/lib/TriggerCQN2SQL.js +42 -0
  43. package/lib/h2/java-codegen.js +833 -0
  44. package/lib/h2/register.js +27 -0
  45. package/lib/h2/triggers.js +41 -0
  46. package/lib/hana/composition.js +248 -0
  47. package/lib/hana/register.js +28 -0
  48. package/lib/hana/sql-expressions.js +213 -0
  49. package/lib/hana/triggers.js +253 -0
  50. package/lib/localization.js +53 -117
  51. package/lib/model-enhancer.js +266 -0
  52. package/lib/postgres/composition.js +190 -0
  53. package/lib/postgres/register.js +44 -0
  54. package/lib/postgres/sql-expressions.js +261 -0
  55. package/lib/postgres/triggers.js +113 -0
  56. package/lib/skipHandlers.js +34 -0
  57. package/lib/sqlite/composition.js +234 -0
  58. package/lib/sqlite/register.js +28 -0
  59. package/lib/sqlite/sql-expressions.js +228 -0
  60. package/lib/sqlite/triggers.js +163 -0
  61. package/lib/utils/change-tracking.js +394 -0
  62. package/lib/utils/composition-helpers.js +67 -0
  63. package/lib/utils/entity-collector.js +297 -0
  64. package/lib/utils/session-variables.js +276 -0
  65. package/lib/utils/trigger-utils.js +94 -0
  66. package/package.json +17 -7
  67. package/lib/change-log.js +0 -538
  68. package/lib/entity-helper.js +0 -217
  69. package/lib/format-options.js +0 -66
  70. package/lib/template-processor.js +0 -115
package/package.json CHANGED
@@ -1,6 +1,10 @@
1
1
  {
2
2
  "name": "@cap-js/change-tracking",
3
- "version": "1.1.4",
3
+ "version": "2.0.0-beta.2",
4
+ "publishConfig": {
5
+ "access": "public",
6
+ "tag": "beta"
7
+ },
4
8
  "description": "CDS plugin providing out-of-the box support for automatic capturing, storing, and viewing of the change records of modeled entities.",
5
9
  "repository": "cap-js/change-tracking",
6
10
  "author": "SAP SE (https://www.sap.com)",
@@ -16,6 +20,13 @@
16
20
  "scripts": {
17
21
  "lint": "npx eslint .",
18
22
  "test": "npx jest --silent",
23
+ "test:sqlite": "CDS_ENV=sqlite npx jest --silent",
24
+ "test:postgres": "npm run start:pg && CDS_ENV=pg npx jest --runInBand --silent",
25
+ "test:hana": "npm run start:hana && cds bind --exec -- npx jest --silent",
26
+ "start:pg": "cd tests/bookshop/ && docker compose -f pg-stack.yml up -d && npm run deploy:postgres && cd ../..",
27
+ "deploy:postgres": "CDS_ENV=pg cds build --production && cds deploy --profile pg",
28
+ "start:hana": "cd tests/bookshop/ && cds deploy -2 hana && cd ../..",
29
+ "test:all": "npm run test:sqlite && npm run test:postgres && npm run test:hana",
19
30
  "format": "npx -y prettier@3 . --write",
20
31
  "format:check": "npx -y prettier@3 --check ."
21
32
  },
@@ -26,22 +37,21 @@
26
37
  "node": ">=20.0.0"
27
38
  },
28
39
  "devDependencies": {
29
- "@cap-js/attachments": "^2",
30
40
  "@cap-js/cds-test": "*",
31
- "@cap-js/change-tracking": "file:.",
32
- "@cap-js/sqlite": "^1 || ^2",
33
- "express": "^4"
41
+ "@cap-js/cds-types": "^0.16.0"
34
42
  },
35
43
  "cds": {
36
44
  "requires": {
37
45
  "change-tracking": {
38
46
  "model": "@cap-js/change-tracking",
39
- "considerLocalizedValues": false
47
+ "considerLocalizedValues": false,
48
+ "maxDisplayHierarchyDepth": 3,
49
+ "preserveDeletes": false
40
50
  }
41
51
  }
42
52
  },
43
53
  "jest": {
44
- "testTimeout": 10000
54
+ "testTimeout": 180000
45
55
  },
46
56
  "workspaces": [
47
57
  "tests/*"
package/lib/change-log.js DELETED
@@ -1,538 +0,0 @@
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 { 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';
10
-
11
- function formatDecimal(str, scale) {
12
- if (typeof str === 'number' && !isNaN(str)) {
13
- str = String(str);
14
- } else return str;
15
-
16
- if (scale > 0) {
17
- let parts = str.split('.');
18
- let decimalPart = parts[1] || '';
19
-
20
- while (decimalPart.length < scale) {
21
- decimalPart += '0';
22
- }
23
-
24
- return `${parts[0]}.${decimalPart}`;
25
- }
26
-
27
- return str;
28
- }
29
-
30
- const _getRootEntityPathVals = function (txContext, entity, entityKey) {
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
- };
61
-
62
- const _getAllPathVals = function (txContext) {
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
- };
76
-
77
- function convertSubjectToParams(subject) {
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;
97
- }
98
-
99
- const _getEntityIDs = function (txParams) {
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
- };
115
-
116
- /**
117
- *
118
- * @param {*} tx
119
- * @param {*} changes
120
- *
121
- * When consuming app implement '@changelog' on an association element,
122
- * change history will use attribute on associated entity which are specified instead of default technical foreign key.
123
- *
124
- * eg:
125
- * entity PurchasedProductFootprints @(cds.autoexpose): cuid, managed {
126
- * ...
127
- * '@changelog': [Plant.identifier]
128
- * '@mandatory' Plant : Association to one Plant;
129
- * ...
130
- * }
131
- */
132
- const _formatAssociationContext = async function (changes, reqData) {
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
- };
181
-
182
- const _formatCompositionContext = async function (changes, reqData, target) {
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
- };
229
-
230
- const _formatCompositionEntityType = function (change) {
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
- };
250
-
251
- const _formatObjectID = async function (changes, reqData, target) {
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
- };
275
-
276
- const _isCompositionContextPath = function (aPath, hasComp) {
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
- };
285
-
286
- const _formatChangeLog = async function (changes, req) {
287
- await _formatObjectID(changes, req.data, req.target);
288
- await _formatAssociationContext(changes, req.data);
289
- await _formatCompositionContext(changes, req.data, req.target);
290
- };
291
-
292
- const _afterReadChangeView = function (data, req) {
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;
365
- }
366
-
367
- const _prepareChangeLogForComposition = async function (entity, entityKey, changes, req) {
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;
415
- }
416
-
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
- };
441
- }
442
-
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 };
464
- }
465
-
466
- function isRootEntity(entity) {
467
- return entity[isRoot] || cds.model.definitions[entity.name]?.[isRoot] || false;
468
- }
469
-
470
- function isEmpty(value) {
471
- return value === null || value === undefined || value === '';
472
- }
473
-
474
- async function track_changes(req) {
475
- const config = cds.env.requires['change-tracking'];
476
-
477
- if ((req.event === 'UPDATE' && config?.disableUpdateTracking) || (req.event === 'CREATE' && config?.disableCreateTracking) || (req.event === 'DELETE' && config?.disableDeleteTracking)) {
478
- return;
479
- }
480
-
481
- let diff = await req.diff();
482
- if (!diff) return;
483
-
484
- const diffs = Array.isArray(diff) ? diff : [diff];
485
- const changes = (await Promise.all(diffs.map((item) => trackChangesForDiff(item, req, this)))).filter(Boolean);
486
-
487
- if (changes.length > 0) {
488
- await INSERT.into('sap.changelog.ChangeLog').entries(changes);
489
- }
490
- }
491
-
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
- };
536
- }
537
-
538
- module.exports = { track_changes, _afterReadChangeView };