@cap-js/change-tracking 1.1.2 → 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/CHANGELOG.md +13 -1
- package/cds-plugin.js +229 -223
- package/lib/change-log.js +469 -506
- package/lib/entity-helper.js +187 -186
- package/lib/format-options.js +62 -62
- package/lib/localization.js +103 -95
- package/lib/template-processor.js +86 -82
- package/package.json +47 -39
package/lib/change-log.js
CHANGED
|
@@ -1,129 +1,117 @@
|
|
|
1
|
-
const cds = require(
|
|
2
|
-
const getTemplate = require(
|
|
3
|
-
const {templateProcessor, retrieveFirstKey} = require(
|
|
4
|
-
const LOG = cds.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
|
-
|
|
24
|
-
|
|
25
|
-
|
|
12
|
+
if (typeof str === 'number' && !isNaN(str)) {
|
|
13
|
+
str = String(str);
|
|
14
|
+
} else return str;
|
|
26
15
|
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
16
|
+
if (scale > 0) {
|
|
17
|
+
let parts = str.split('.');
|
|
18
|
+
let decimalPart = parts[1] || '';
|
|
30
19
|
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
20
|
+
while (decimalPart.length < scale) {
|
|
21
|
+
decimalPart += '0';
|
|
22
|
+
}
|
|
34
23
|
|
|
35
|
-
|
|
36
|
-
|
|
24
|
+
return `${parts[0]}.${decimalPart}`;
|
|
25
|
+
}
|
|
37
26
|
|
|
38
|
-
|
|
27
|
+
return str;
|
|
39
28
|
}
|
|
40
29
|
|
|
41
30
|
const _getRootEntityPathVals = function (txContext, entity, entityKey) {
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
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
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
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
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
)
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
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
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
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
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
const _getObjectIdByPath = async function (
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
)
|
|
270
|
-
|
|
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
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
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
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
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
|
-
|
|
325
|
-
|
|
326
|
-
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
function _trackedChanges4
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
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
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
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
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
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
|
|
487
|
-
|
|
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
|
-
|
|
471
|
+
return value === null || value === undefined || value === '';
|
|
498
472
|
}
|
|
499
473
|
|
|
500
|
-
async function track_changes
|
|
501
|
-
|
|
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
|
-
|
|
512
|
-
|
|
477
|
+
if ((req.event === 'UPDATE' && config?.disableUpdateTracking) || (req.event === 'CREATE' && config?.disableCreateTracking) || (req.event === 'DELETE' && config?.disableDeleteTracking)) {
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
513
480
|
|
|
514
|
-
|
|
515
|
-
|
|
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
|
-
|
|
520
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
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 };
|