@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/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.5 - 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.4 - 03.12.25
|
|
16
|
+
|
|
17
|
+
### Fixed
|
|
18
|
+
- Server no longer crashes when after a DB migration the service name or attribute name change
|
|
19
|
+
- Fix crash when applications uses feature toogles or extensibility
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
## Version 1.1.3 - 27.10.25
|
|
23
|
+
|
|
24
|
+
### Changed
|
|
25
|
+
- Correctly handle changes on foreign keys when sending them via the document notation on an API level.
|
|
26
|
+
|
|
15
27
|
|
|
16
28
|
## Version 1.1.2 - 23.10.25
|
|
17
29
|
|
package/cds-plugin.js
CHANGED
|
@@ -1,260 +1,266 @@
|
|
|
1
|
-
const cds = require('@sap/cds')
|
|
2
|
-
const DEBUG = cds.debug('changelog')
|
|
1
|
+
const cds = require('@sap/cds');
|
|
2
|
+
const DEBUG = cds.debug('changelog');
|
|
3
3
|
|
|
4
|
-
const isRoot = 'change-tracking-isRootEntity'
|
|
5
|
-
const hasParent = 'change-tracking-parentEntity'
|
|
4
|
+
const isRoot = 'change-tracking-isRootEntity';
|
|
5
|
+
const hasParent = 'change-tracking-parentEntity';
|
|
6
6
|
|
|
7
7
|
const isChangeTracked = (entity) => {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
8
|
+
if (entity.query?.SET?.op === 'union') return false; // REVISIT: should that be an error or warning?
|
|
9
|
+
if (entity['@changelog']) return true;
|
|
10
|
+
if (Object.values(entity.elements).some((e) => e['@changelog'])) return true;
|
|
11
|
+
return false;
|
|
12
|
+
};
|
|
12
13
|
|
|
13
14
|
// Add the appropriate Side Effects attribute to the custom action
|
|
14
15
|
const addSideEffects = (actions, flag, element) => {
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
function setChangeTrackingIsRootEntity (entity, csn, val = true) {
|
|
36
|
-
if (csn.definitions?.[entity.name]) {
|
|
37
|
-
csn.definitions[entity.name][isRoot] = val
|
|
38
|
-
}
|
|
16
|
+
if (!flag && (element === undefined || element === null)) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
for (const se of Object.values(actions)) {
|
|
21
|
+
const target = flag ? 'TargetProperties' : 'TargetEntities';
|
|
22
|
+
const sideEffectAttr = se[`@Common.SideEffects.${target}`];
|
|
23
|
+
const property = flag ? 'changes' : { '=': `${element}.changes` };
|
|
24
|
+
if (sideEffectAttr?.length >= 0) {
|
|
25
|
+
sideEffectAttr.findIndex((item) => (item['='] ? item['='] : item) === (property['='] ? property['='] : property)) === -1 && sideEffectAttr.push(property);
|
|
26
|
+
} else {
|
|
27
|
+
se[`@Common.SideEffects.${target}`] = [property];
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function setChangeTrackingIsRootEntity(entity, csn, val = true) {
|
|
33
|
+
if (csn.definitions?.[entity.name]) {
|
|
34
|
+
csn.definitions[entity.name][isRoot] = val;
|
|
35
|
+
}
|
|
39
36
|
}
|
|
40
37
|
|
|
41
|
-
function checkAndSetRootEntity
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
38
|
+
function checkAndSetRootEntity(parentEntity, entity, csn) {
|
|
39
|
+
if (entity[isRoot] === false) {
|
|
40
|
+
return entity;
|
|
41
|
+
}
|
|
42
|
+
if (parentEntity) {
|
|
43
|
+
return compositionRoot(parentEntity, csn);
|
|
44
|
+
} else {
|
|
45
|
+
setChangeTrackingIsRootEntity(entity, csn);
|
|
46
|
+
return { ...csn.definitions?.[entity.name], name: entity.name };
|
|
47
|
+
}
|
|
51
48
|
}
|
|
52
49
|
|
|
53
|
-
function processEntities
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
50
|
+
function processEntities(m) {
|
|
51
|
+
for (let name in m.definitions) {
|
|
52
|
+
compositionRoot({ ...m.definitions[name], name }, m);
|
|
53
|
+
}
|
|
57
54
|
}
|
|
58
55
|
|
|
59
|
-
function compositionRoot
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
56
|
+
function compositionRoot(entity, csn) {
|
|
57
|
+
if (!entity || entity.kind !== 'entity') {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const parentEntity = compositionParent(entity, csn);
|
|
61
|
+
return checkAndSetRootEntity(parentEntity, entity, csn);
|
|
65
62
|
}
|
|
66
63
|
|
|
67
|
-
function compositionParent
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
64
|
+
function compositionParent(entity, csn) {
|
|
65
|
+
if (!entity || entity.kind !== 'entity') {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
const parentAssociation = compositionParentAssociation(entity, csn);
|
|
69
|
+
return parentAssociation ?? null;
|
|
73
70
|
}
|
|
74
71
|
|
|
75
|
-
function compositionParentAssociation
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
72
|
+
function compositionParentAssociation(entity, csn) {
|
|
73
|
+
if (!entity || entity.kind !== 'entity') {
|
|
74
|
+
return;
|
|
75
|
+
}
|
|
76
|
+
const elements = entity.elements ?? {};
|
|
77
|
+
|
|
78
|
+
// Add the change-tracking-isRootEntity attribute of the child entity
|
|
79
|
+
processCompositionElements(entity, csn, elements);
|
|
80
|
+
|
|
81
|
+
const hasChildFlag = entity[isRoot] !== false;
|
|
82
|
+
const hasParentEntity = entity[hasParent];
|
|
83
|
+
|
|
84
|
+
if (hasChildFlag || !hasParentEntity) {
|
|
85
|
+
// Find parent association of the entity
|
|
86
|
+
const parentAssociation = findParentAssociation(entity, csn, elements);
|
|
87
|
+
if (parentAssociation) {
|
|
88
|
+
const parentAssociationTarget = elements[parentAssociation]?.target;
|
|
89
|
+
if (hasChildFlag) setChangeTrackingIsRootEntity(entity, csn, false);
|
|
90
|
+
return {
|
|
91
|
+
...csn.definitions?.[parentAssociationTarget],
|
|
92
|
+
name: parentAssociationTarget
|
|
93
|
+
};
|
|
94
|
+
} else return;
|
|
95
|
+
}
|
|
96
|
+
return { ...csn.definitions?.[entity.name], name: entity.name };
|
|
100
97
|
}
|
|
101
98
|
|
|
102
|
-
function processCompositionElements
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
) {
|
|
113
|
-
continue
|
|
114
|
-
}
|
|
115
|
-
setChangeTrackingIsRootEntity({ ...definition, name: target }, csn, false)
|
|
116
|
-
}
|
|
99
|
+
function processCompositionElements(entity, csn, elements) {
|
|
100
|
+
for (const name in elements) {
|
|
101
|
+
const element = elements[name];
|
|
102
|
+
const target = element?.target;
|
|
103
|
+
const definition = csn.definitions?.[target];
|
|
104
|
+
if (element.type !== 'cds.Composition' || target === entity.name || !definition || definition[isRoot] === false) {
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
setChangeTrackingIsRootEntity({ ...definition, name: target }, csn, false);
|
|
108
|
+
}
|
|
117
109
|
}
|
|
118
110
|
|
|
119
|
-
function findParentAssociation
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
111
|
+
function findParentAssociation(entity, csn, elements) {
|
|
112
|
+
return Object.keys(elements).find((name) => {
|
|
113
|
+
const element = elements[name];
|
|
114
|
+
const target = element?.target;
|
|
115
|
+
if (element.type === 'cds.Association' && target !== entity.name) {
|
|
116
|
+
const parentDefinition = csn.definitions?.[target] ?? {};
|
|
117
|
+
const parentElements = parentDefinition?.elements ?? {};
|
|
118
|
+
return !!Object.keys(parentElements).find((parentEntityName) => {
|
|
119
|
+
const parentElement = parentElements?.[parentEntityName] ?? {};
|
|
120
|
+
if (parentElement.type === 'cds.Composition') {
|
|
121
|
+
const isCompositionEntity = parentElement.target === entity.name;
|
|
122
|
+
// add parent information in the current entity
|
|
123
|
+
if (isCompositionEntity) {
|
|
124
|
+
csn.definitions[entity.name][hasParent] = {
|
|
125
|
+
associationName: name,
|
|
126
|
+
entityName: target
|
|
127
|
+
};
|
|
128
|
+
}
|
|
129
|
+
return isCompositionEntity;
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
});
|
|
142
134
|
}
|
|
143
135
|
|
|
144
|
-
|
|
145
|
-
|
|
146
136
|
/**
|
|
147
137
|
* Returns an expression for the key of the given entity, which we can use as the right-hand-side of an ON condition.
|
|
148
138
|
*/
|
|
149
|
-
function entityKey4
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
139
|
+
function entityKey4(entity) {
|
|
140
|
+
const xpr = [];
|
|
141
|
+
for (let k in entity.elements) {
|
|
142
|
+
const e = entity.elements[k];
|
|
143
|
+
if (!e.key) continue;
|
|
144
|
+
if (xpr.length) xpr.push('||');
|
|
145
|
+
if (e.type === 'cds.Association') xpr.push({ ref: [k, e.keys?.[0]?.ref?.[0]] });
|
|
146
|
+
else xpr.push({ ref: [k] });
|
|
147
|
+
}
|
|
148
|
+
return xpr;
|
|
158
149
|
}
|
|
159
150
|
|
|
160
|
-
|
|
161
151
|
// Unfold @changelog annotations in loaded model
|
|
162
|
-
function enhanceModel
|
|
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
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
152
|
+
function enhanceModel(m) {
|
|
153
|
+
if (m.meta?.flavor !== 'inferred') {
|
|
154
|
+
// In MTX scenarios with extensibility the runtime model for deployed apps is not
|
|
155
|
+
// inferred but xtended and the logic requires inferred.
|
|
156
|
+
DEBUG?.(`Skipping model enhancement because model flavour is '${m.meta?.flavor}' and not 'inferred'`);
|
|
157
|
+
return;
|
|
158
|
+
}
|
|
159
|
+
const _enhanced = 'sap.changelog.enhanced';
|
|
160
|
+
if (m.meta?.[_enhanced]) return; // already enhanced
|
|
161
|
+
|
|
162
|
+
// Get definitions from Dummy entity in our models
|
|
163
|
+
const { 'sap.changelog.aspect': aspect } = m.definitions;
|
|
164
|
+
if (!aspect) return; // some other model
|
|
165
|
+
const {
|
|
166
|
+
'@UI.Facets': [facet],
|
|
167
|
+
elements: { changes }
|
|
168
|
+
} = aspect;
|
|
169
|
+
if (changes.on.length > 2) changes.on.pop(); // remove ID -> filled in below
|
|
170
|
+
|
|
171
|
+
processEntities(m); // REVISIT: why is that required ?!?
|
|
172
|
+
|
|
173
|
+
for (let name in m.definitions) {
|
|
174
|
+
const entity = m.definitions[name];
|
|
175
|
+
if (entity.kind === 'entity' && !entity['@cds.autoexposed'] && isChangeTracked(entity)) {
|
|
176
|
+
if (!entity['@changelog.disable_assoc']) {
|
|
177
|
+
// Add association to ChangeView...
|
|
178
|
+
const keys = entityKey4(entity);
|
|
179
|
+
if (!keys.length) continue; // If no key attribute is defined for the entity, the logic to add association to ChangeView should be skipped.
|
|
180
|
+
const assoc = new cds.builtin.classes.Association({ ...changes, on: [...changes.on, ...keys] });
|
|
181
|
+
|
|
182
|
+
// --------------------------------------------------------------------
|
|
183
|
+
// PARKED: Add auto-exposed projection on ChangeView to service if applicable
|
|
184
|
+
// const namespace = name.match(/^(.*)\.[^.]+$/)[1]
|
|
185
|
+
// const service = m.definitions[namespace]
|
|
186
|
+
// if (service) {
|
|
187
|
+
// const projection = {from:{ref:[assoc.target]}}
|
|
188
|
+
// m.definitions[assoc.target = namespace + '.' + Changes] = {
|
|
189
|
+
// '@cds.autoexposed':true, kind:'entity', projection
|
|
190
|
+
// }
|
|
191
|
+
// DEBUG?.(`\n
|
|
192
|
+
// extend service ${namespace} with {
|
|
193
|
+
// entity ${Changes} as projection on ${projection.from.ref[0]};
|
|
194
|
+
// }
|
|
195
|
+
// `.replace(/ {10}/g,''))
|
|
196
|
+
// }
|
|
197
|
+
// --------------------------------------------------------------------
|
|
198
|
+
|
|
199
|
+
DEBUG?.(
|
|
200
|
+
`\n
|
|
203
201
|
extend ${name} with {
|
|
204
|
-
changes : Association to many ${assoc.target} on ${
|
|
202
|
+
changes : Association to many ${assoc.target} on ${assoc.on.map((x) => x.ref?.join('.') || x).join(' ')};
|
|
205
203
|
}
|
|
206
|
-
`.replace(/ {8}/g,'')
|
|
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
|
-
|
|
204
|
+
`.replace(/ {8}/g, '')
|
|
205
|
+
);
|
|
206
|
+
const query = entity.projection || entity.query?.SELECT;
|
|
207
|
+
if (query) (query.columns ??= ['*']).push({ as: 'changes', cast: assoc });
|
|
208
|
+
else if (entity.elements) entity.elements.changes = assoc;
|
|
209
|
+
|
|
210
|
+
// Add UI.Facet for Change History List
|
|
211
|
+
if (!entity['@changelog.disable_facet']) entity['@UI.Facets']?.push(facet);
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (entity.actions) {
|
|
215
|
+
const hasParentInfo = entity[hasParent];
|
|
216
|
+
const entityName = hasParentInfo?.entityName;
|
|
217
|
+
const parentEntity = entityName ? m.definitions[entityName] : null;
|
|
218
|
+
const isParentRootAndHasFacets = parentEntity?.[isRoot] && parentEntity?.['@UI.Facets'];
|
|
219
|
+
if (entity[isRoot] && entity['@UI.Facets']) {
|
|
220
|
+
// Add side effects for root entity
|
|
221
|
+
addSideEffects(entity.actions, true);
|
|
222
|
+
} else if (isParentRootAndHasFacets) {
|
|
223
|
+
// Add side effects for child entity
|
|
224
|
+
addSideEffects(entity.actions, false, hasParentInfo?.associationName);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
(m.meta ??= {})[_enhanced] = true;
|
|
232
230
|
}
|
|
233
231
|
|
|
234
232
|
// Add generic change tracking handlers
|
|
235
233
|
function addGenericHandlers() {
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
234
|
+
const { track_changes, _afterReadChangeView } = require('./lib/change-log');
|
|
235
|
+
for (const srv of cds.services) {
|
|
236
|
+
if (srv instanceof cds.ApplicationService) {
|
|
237
|
+
let any = false;
|
|
238
|
+
for (const entity of Object.values(srv.entities)) {
|
|
239
|
+
if (isChangeTracked(entity)) {
|
|
240
|
+
cds.db.before('CREATE', entity, track_changes);
|
|
241
|
+
cds.db.before('UPDATE', entity, track_changes);
|
|
242
|
+
cds.db.before('DELETE', entity, track_changes);
|
|
243
|
+
any = true;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
if (any && srv.entities.ChangeView) {
|
|
247
|
+
srv.after('READ', srv.entities.ChangeView, _afterReadChangeView);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
253
251
|
}
|
|
254
252
|
|
|
255
|
-
|
|
256
253
|
// Register plugin hooks
|
|
257
|
-
cds.on('compile.for.runtime', csn => {
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
254
|
+
cds.on('compile.for.runtime', (csn) => {
|
|
255
|
+
DEBUG?.('on', 'compile.for.runtime');
|
|
256
|
+
enhanceModel(csn);
|
|
257
|
+
});
|
|
258
|
+
cds.on('compile.to.edmx', (csn) => {
|
|
259
|
+
DEBUG?.('on', 'compile.to.edmx');
|
|
260
|
+
enhanceModel(csn);
|
|
261
|
+
});
|
|
262
|
+
cds.on('compile.to.dbx', (csn) => {
|
|
263
|
+
DEBUG?.('on', 'compile.to.dbx');
|
|
264
|
+
enhanceModel(csn);
|
|
265
|
+
});
|
|
266
|
+
cds.on('served', addGenericHandlers);
|