@cap-js/change-tracking 1.1.1 → 1.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +13 -1
- package/index.cds +3 -3
- package/lib/change-log.js +19 -14
- package/lib/entity-helper.js +5 -5
- package/lib/template-processor.js +19 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
-
## Version 1.1.
|
|
7
|
+
## Version 1.1.4 - TBD
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
@@ -12,6 +12,18 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
|
12
12
|
|
|
13
13
|
### Changed
|
|
14
14
|
|
|
15
|
+
## Version 1.1.3 - 27.10.25
|
|
16
|
+
|
|
17
|
+
### Changed
|
|
18
|
+
- Correctly handle changes on foreign keys when sending them via the document notation on an API level.
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
## Version 1.1.2 - 23.10.25
|
|
22
|
+
|
|
23
|
+
### Fixed
|
|
24
|
+
|
|
25
|
+
- Support single keys which are not named `ID`
|
|
26
|
+
|
|
15
27
|
|
|
16
28
|
## Version 1.1.1 - 17.10.25
|
|
17
29
|
|
package/index.cds
CHANGED
|
@@ -13,7 +13,7 @@ namespace sap.changelog;
|
|
|
13
13
|
}]) {
|
|
14
14
|
// Essentially: Association to many Changes on changes.changeLog.entityKey = ID;
|
|
15
15
|
changes : Association to many ChangeView on changes.entityKey = ID;
|
|
16
|
-
key ID :
|
|
16
|
+
key ID : String;
|
|
17
17
|
}
|
|
18
18
|
|
|
19
19
|
|
|
@@ -39,7 +39,7 @@ view ChangeView as
|
|
|
39
39
|
entity ChangeLog : managed, cuid {
|
|
40
40
|
serviceEntity : String(5000) @title: '{i18n>ChangeLog.serviceEntity}'; // definition name of target entity (on service level) - e.g. ProcessorsService.Incidents
|
|
41
41
|
entity : String(5000) @title: '{i18n>ChangeLog.entity}'; // definition name of target entity (on db level) - e.g. sap.capire.incidents.Incidents
|
|
42
|
-
entityKey :
|
|
42
|
+
entityKey : String @title: '{i18n>ChangeLog.entityKey}'; // primary key of target entity, e.g. Incidents.ID
|
|
43
43
|
createdAt : managed:createdAt @title : '{i18n>ChangeLog.createdAt}';
|
|
44
44
|
createdBy : managed:createdBy @title : '{i18n>ChangeLog.createdBy}';
|
|
45
45
|
changes : Composition of many Changes on changes.changeLog = $self;
|
|
@@ -64,7 +64,7 @@ entity Changes {
|
|
|
64
64
|
|
|
65
65
|
// Business meaningful parent object id
|
|
66
66
|
parentEntityID : String(5000) @title: '{i18n>Changes.parentEntityID}';
|
|
67
|
-
parentKey :
|
|
67
|
+
parentKey : String @title: '{i18n>Changes.parentKey}';
|
|
68
68
|
serviceEntityPath : String(5000) @title: '{i18n>Changes.serviceEntityPath}';
|
|
69
69
|
|
|
70
70
|
@title: '{i18n>Changes.modification}'
|
package/lib/change-log.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const cds = require("@sap/cds")
|
|
2
2
|
const getTemplate = require("@sap/cds/libx/_runtime/common/utils/template") // REVISIT: bad usage of internal stuff
|
|
3
|
-
const templateProcessor = require("./template-processor")
|
|
3
|
+
const {templateProcessor, retrieveFirstKey} = require("./template-processor")
|
|
4
4
|
const LOG = cds.log("change-log")
|
|
5
5
|
|
|
6
6
|
const {
|
|
@@ -173,7 +173,8 @@ const _getChildChangeObjId = async function (
|
|
|
173
173
|
change,
|
|
174
174
|
childNodeChange,
|
|
175
175
|
curNodePathVal,
|
|
176
|
-
reqData
|
|
176
|
+
reqData,
|
|
177
|
+
target
|
|
177
178
|
) {
|
|
178
179
|
const composition = cds.model.definitions[change.serviceEntity].elements[change.attribute]
|
|
179
180
|
const objIdElements = composition ? composition["@changelog"] : null
|
|
@@ -183,11 +184,12 @@ const _getChildChangeObjId = async function (
|
|
|
183
184
|
reqData,
|
|
184
185
|
curNodePathVal,
|
|
185
186
|
childNodeChange._path,
|
|
187
|
+
target,
|
|
186
188
|
objIdElementNames
|
|
187
189
|
)
|
|
188
190
|
}
|
|
189
191
|
|
|
190
|
-
const _formatCompositionContext = async function (changes, reqData) {
|
|
192
|
+
const _formatCompositionContext = async function (changes, reqData, target) {
|
|
191
193
|
const childNodeChanges = []
|
|
192
194
|
|
|
193
195
|
for (const change of changes) {
|
|
@@ -204,7 +206,8 @@ const _formatCompositionContext = async function (changes, reqData) {
|
|
|
204
206
|
change,
|
|
205
207
|
childNodeChange,
|
|
206
208
|
curNodePathVal,
|
|
207
|
-
reqData
|
|
209
|
+
reqData,
|
|
210
|
+
target
|
|
208
211
|
)
|
|
209
212
|
_formatCompositionValue(curChange, objId, childNodeChange, childNodeChanges)
|
|
210
213
|
}
|
|
@@ -261,17 +264,18 @@ const _getObjectIdByPath = async function (
|
|
|
261
264
|
reqData,
|
|
262
265
|
nodePathVal,
|
|
263
266
|
serviceEntityPath,
|
|
267
|
+
target,
|
|
264
268
|
/**optional*/ objIdElementNames
|
|
265
269
|
) {
|
|
266
270
|
const curObjFromReqData = getCurObjFromReqData(reqData, nodePathVal, serviceEntityPath)
|
|
267
271
|
const entityName = getNameFromPathVal(nodePathVal)
|
|
268
272
|
const entityUUID = getUUIDFromPathVal(nodePathVal)
|
|
269
|
-
const obj = await getCurObjFromDbQuery(entityName, entityUUID)
|
|
273
|
+
const obj = await getCurObjFromDbQuery(entityName, {[retrieveFirstKey(target)]: entityUUID})
|
|
270
274
|
const curObj = { curObjFromReqData, curObjFromDbQuery: obj }
|
|
271
275
|
return getObjectId(reqData, entityName, objIdElementNames, curObj)
|
|
272
276
|
}
|
|
273
277
|
|
|
274
|
-
const _formatObjectID = async function (changes, reqData) {
|
|
278
|
+
const _formatObjectID = async function (changes, reqData, target) {
|
|
275
279
|
const objectIdCache = new Map()
|
|
276
280
|
for (const change of changes) {
|
|
277
281
|
const path = splitPath(change.serviceEntityPath)
|
|
@@ -283,7 +287,8 @@ const _formatObjectID = async function (changes, reqData) {
|
|
|
283
287
|
curNodeObjId = await _getObjectIdByPath(
|
|
284
288
|
reqData,
|
|
285
289
|
curNodePathVal,
|
|
286
|
-
change.serviceEntityPath
|
|
290
|
+
change.serviceEntityPath,
|
|
291
|
+
target
|
|
287
292
|
)
|
|
288
293
|
objectIdCache.set(curNodePathVal, curNodeObjId)
|
|
289
294
|
}
|
|
@@ -293,7 +298,8 @@ const _formatObjectID = async function (changes, reqData) {
|
|
|
293
298
|
parentNodeObjId = await _getObjectIdByPath(
|
|
294
299
|
reqData,
|
|
295
300
|
parentNodePathVal,
|
|
296
|
-
change.serviceEntityPath
|
|
301
|
+
change.serviceEntityPath,
|
|
302
|
+
target
|
|
297
303
|
)
|
|
298
304
|
objectIdCache.set(parentNodePathVal, parentNodeObjId)
|
|
299
305
|
}
|
|
@@ -315,9 +321,9 @@ const _isCompositionContextPath = function (aPath, hasComp) {
|
|
|
315
321
|
}
|
|
316
322
|
|
|
317
323
|
const _formatChangeLog = async function (changes, req) {
|
|
318
|
-
await _formatObjectID(changes, req.data)
|
|
324
|
+
await _formatObjectID(changes, req.data, req.target)
|
|
319
325
|
await _formatAssociationContext(changes, req.data)
|
|
320
|
-
await _formatCompositionContext(changes, req.data)
|
|
326
|
+
await _formatCompositionContext(changes, req.data, req.target)
|
|
321
327
|
}
|
|
322
328
|
|
|
323
329
|
const _afterReadChangeView = function (data, req) {
|
|
@@ -326,13 +332,12 @@ const _afterReadChangeView = function (data, req) {
|
|
|
326
332
|
localizeLogFields(data, req.locale)
|
|
327
333
|
}
|
|
328
334
|
|
|
329
|
-
|
|
330
335
|
function _trackedChanges4 (srv, target, diff) {
|
|
331
336
|
const template = getTemplate("change-logging", srv, target, { pick: e => e['@changelog'] })
|
|
332
337
|
if (!template.elements.size) return
|
|
333
338
|
|
|
334
339
|
const changes = []
|
|
335
|
-
diff._path = `${target.name}(${diff
|
|
340
|
+
diff._path = `${target.name}(${diff[retrieveFirstKey(target)]})`
|
|
336
341
|
|
|
337
342
|
templateProcessor({
|
|
338
343
|
template, row: diff, processFn: ({ row, key, element }) => {
|
|
@@ -416,7 +421,7 @@ const _prepareChangeLogForComposition = async function (entity, entityKey, chang
|
|
|
416
421
|
.join('/')
|
|
417
422
|
|
|
418
423
|
for (const change of changes) {
|
|
419
|
-
change.parentEntityID = await _getObjectIdByPath(req.data, parentEntityPathVal, parentServiceEntityPath)
|
|
424
|
+
change.parentEntityID = await _getObjectIdByPath(req.data, parentEntityPathVal, parentServiceEntityPath, entity)
|
|
420
425
|
change.parentKey = parentKey
|
|
421
426
|
change.serviceEntityPath = serviceEntityPath
|
|
422
427
|
}
|
|
@@ -520,7 +525,7 @@ async function track_changes (req) {
|
|
|
520
525
|
async function trackChangesForDiff(diff, req, that){
|
|
521
526
|
let target = req.target
|
|
522
527
|
let compContext = null;
|
|
523
|
-
let entityKey = diff.
|
|
528
|
+
let entityKey = diff[retrieveFirstKey(req.target)]
|
|
524
529
|
const params = convertSubjectToParams(req.subject);
|
|
525
530
|
if (req.subject.ref.length === 1 && params.length === 1 && !target[isRoot]) {
|
|
526
531
|
compContext = await generatePathAndParams(req, entityKey);
|
package/lib/entity-helper.js
CHANGED
|
@@ -29,10 +29,10 @@ const getObjIdElementNamesInArray = function (elements) {
|
|
|
29
29
|
else return []
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
-
const getCurObjFromDbQuery = async function (entityName,
|
|
33
|
-
if (!
|
|
32
|
+
const getCurObjFromDbQuery = async function (entityName, whereXpr) {
|
|
33
|
+
if (!Object.keys(whereXpr)) return {}
|
|
34
34
|
// REVISIT: This always reads all elements -> should read required ones only!
|
|
35
|
-
const obj = await SELECT.one.from(entityName).where(
|
|
35
|
+
const obj = await SELECT.one.from(entityName).where(whereXpr)
|
|
36
36
|
return obj || {}
|
|
37
37
|
}
|
|
38
38
|
|
|
@@ -99,10 +99,10 @@ async function getObjectId (reqData, entityName, fields, curObj) {
|
|
|
99
99
|
// When multiple layers of child nodes are deleted at the same time, the deep layer of child nodes will lose the information of the upper nodes, so data needs to be extracted from the db.
|
|
100
100
|
const entityKeys = reqData ? Object.keys(reqData).filter(item => !Object.keys(assoc._target.keys).some(ele => item === ele)) : [];
|
|
101
101
|
if (!_db_data || JSON.stringify(_db_data) === '{}' || entityKeys.length === 0) {
|
|
102
|
-
_db_data = await getCurObjFromDbQuery(assoc._target, IDval
|
|
102
|
+
_db_data = IDval ? await getCurObjFromDbQuery(assoc._target, {[ID]: IDval}) : {};
|
|
103
103
|
}
|
|
104
104
|
} else {
|
|
105
|
-
_db_data = await getCurObjFromDbQuery(assoc._target, IDval
|
|
105
|
+
_db_data = IDval ? await getCurObjFromDbQuery(assoc._target, {[ID]: IDval}) : {};
|
|
106
106
|
}
|
|
107
107
|
} catch (e) {
|
|
108
108
|
LOG.error("Failed to generate object Id for an association entity.", e)
|
|
@@ -23,6 +23,18 @@ const _processElement = (processFn, row, key, elements, isRoot, pathSegments, pi
|
|
|
23
23
|
}
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
function retrieveFirstKey(target) {
|
|
27
|
+
const keys = [];
|
|
28
|
+
for (const key of Object.keys(target.keys)) {
|
|
29
|
+
if (target.keys[key].type !== 'cds.Association' && key !== 'IsActiveEntity') {
|
|
30
|
+
keys.push(key);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
//Revisit: For test 3.5
|
|
35
|
+
return keys.filter(k => !k.startsWith('up_'))[0]
|
|
36
|
+
}
|
|
37
|
+
|
|
26
38
|
const _processRow = (processFn, row, template, tKey, tValue, isRoot, pathOptions) => {
|
|
27
39
|
const { template: subTemplate, picked } = tValue;
|
|
28
40
|
const key = tKey.split(DELIMITER).pop();
|
|
@@ -47,7 +59,8 @@ const _processRow = (processFn, row, template, tKey, tValue, isRoot, pathOptions
|
|
|
47
59
|
* Construct path from root entity to current entity.
|
|
48
60
|
*/
|
|
49
61
|
const serviceNodeName = template.target.elements[key].target;
|
|
50
|
-
|
|
62
|
+
const serviceNode = template.target.elements[key]._target;
|
|
63
|
+
subRow._path = `${row._path}/${serviceNodeName}(${subRow[retrieveFirstKey(serviceNode)]})`;
|
|
51
64
|
}
|
|
52
65
|
});
|
|
53
66
|
|
|
@@ -88,6 +101,10 @@ const templateProcessor = ({ processFn, row, template, isRoot = true, pathOption
|
|
|
88
101
|
const segments = pathOptions.segments && [...pathOptions.segments];
|
|
89
102
|
|
|
90
103
|
for (const [tKey, tValue] of template.elements) {
|
|
104
|
+
// When a managed association is marked with @changelog, both foreign key and assoc are in the template. Skip the association as only the foreign key has a change.
|
|
105
|
+
if (template.target.elements[tKey]?.type === 'cds.Association') {
|
|
106
|
+
continue;
|
|
107
|
+
}
|
|
91
108
|
if (segments) {
|
|
92
109
|
pathOptions.segments = [...segments];
|
|
93
110
|
}
|
|
@@ -95,4 +112,4 @@ const templateProcessor = ({ processFn, row, template, isRoot = true, pathOption
|
|
|
95
112
|
}
|
|
96
113
|
};
|
|
97
114
|
|
|
98
|
-
module.exports = templateProcessor;
|
|
115
|
+
module.exports = {templateProcessor, retrieveFirstKey};
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/change-tracking",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.3",
|
|
4
4
|
"description": "CDS plugin providing out-of-the box support for automatic capturing, storing, and viewing of the change records of modeled entities.",
|
|
5
5
|
"repository": "cap-js/change-tracking",
|
|
6
6
|
"author": "SAP SE (https://www.sap.com)",
|