@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 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.3 - TBD
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
- 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
- }
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
- if (!flag && (element === undefined || element === null)) {
16
- return
17
- }
18
-
19
- for (const se of Object.values(actions)) {
20
- const target = flag ? 'TargetProperties' : 'TargetEntities'
21
- const sideEffectAttr = se[`@Common.SideEffects.${target}`]
22
- const property = flag ? 'changes' : { '=': `${element}.changes` }
23
- if (sideEffectAttr?.length >= 0) {
24
- sideEffectAttr.findIndex(
25
- (item) =>
26
- (item['='] ? item['='] : item) ===
27
- (property['='] ? property['='] : property)
28
- ) === -1 && sideEffectAttr.push(property)
29
- } else {
30
- se[`@Common.SideEffects.${target}`] = [property]
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 (parentEntity, entity, csn) {
42
- if (entity[isRoot] === false) {
43
- return entity
44
- }
45
- if (parentEntity) {
46
- return compositionRoot(parentEntity, csn)
47
- } else {
48
- setChangeTrackingIsRootEntity(entity, csn)
49
- return { ...csn.definitions?.[entity.name], name: entity.name }
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 (m) {
54
- for (let name in m.definitions) {
55
- compositionRoot({ ...m.definitions[name], name }, m)
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 (entity, csn) {
60
- if (!entity || entity.kind !== 'entity') {
61
- return
62
- }
63
- const parentEntity = compositionParent(entity, csn)
64
- return checkAndSetRootEntity(parentEntity, entity, csn)
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 (entity, csn) {
68
- if (!entity || entity.kind !== 'entity') {
69
- return
70
- }
71
- const parentAssociation = compositionParentAssociation(entity, csn)
72
- return parentAssociation ?? null
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 (entity, csn) {
76
- if (!entity || entity.kind !== 'entity') {
77
- return
78
- }
79
- const elements = entity.elements ?? {}
80
-
81
- // Add the change-tracking-isRootEntity attribute of the child entity
82
- processCompositionElements(entity, csn, elements)
83
-
84
- const hasChildFlag = entity[isRoot] !== false
85
- const hasParentEntity = entity[hasParent]
86
-
87
- if (hasChildFlag || !hasParentEntity) {
88
- // Find parent association of the entity
89
- const parentAssociation = findParentAssociation(entity, csn, elements)
90
- if (parentAssociation) {
91
- const parentAssociationTarget = elements[parentAssociation]?.target
92
- if (hasChildFlag) setChangeTrackingIsRootEntity(entity, csn, false)
93
- return {
94
- ...csn.definitions?.[parentAssociationTarget],
95
- name: parentAssociationTarget
96
- }
97
- } else return
98
- }
99
- return { ...csn.definitions?.[entity.name], name: entity.name }
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 (entity, csn, elements) {
103
- for (const name in elements) {
104
- const element = elements[name]
105
- const target = element?.target
106
- const definition = csn.definitions?.[target]
107
- if (
108
- element.type !== 'cds.Composition' ||
109
- target === entity.name ||
110
- !definition ||
111
- definition[isRoot] === false
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 (entity, csn, elements) {
120
- return Object.keys(elements).find((name) => {
121
- const element = elements[name]
122
- const target = element?.target
123
- if (element.type === 'cds.Association' && target !== entity.name) {
124
- const parentDefinition = csn.definitions?.[target] ?? {}
125
- const parentElements = parentDefinition?.elements ?? {}
126
- return !!Object.keys(parentElements).find((parentEntityName) => {
127
- const parentElement = parentElements?.[parentEntityName] ?? {}
128
- if (parentElement.type === 'cds.Composition') {
129
- const isCompositionEntity = parentElement.target === entity.name
130
- // add parent information in the current entity
131
- if (isCompositionEntity) {
132
- csn.definitions[entity.name][hasParent] = {
133
- associationName: name,
134
- entityName: target
135
- }
136
- }
137
- return isCompositionEntity
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 (entity) {
150
- const xpr = []
151
- for (let k in entity.elements) {
152
- const e = entity.elements[k]; if (!e.key) continue
153
- if (xpr.length) xpr.push('||')
154
- if (e.type === 'cds.Association') xpr.push({ ref: [k, e.keys?.[0]?.ref?.[0]] })
155
- else xpr.push({ ref:[k] })
156
- }
157
- return xpr
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 (m) {
163
-
164
- const _enhanced = 'sap.changelog.enhanced'
165
- if (m.meta?.[_enhanced]) return // already enhanced
166
-
167
- // Get definitions from Dummy entity in our models
168
- const { 'sap.changelog.aspect': aspect } = m.definitions; if (!aspect) return // some other model
169
- const { '@UI.Facets': [facet], elements: { changes } } = aspect
170
- if (changes.on.length > 2) changes.on.pop() // remove ID -> filled in below
171
-
172
- processEntities(m) // REVISIT: why is that required ?!?
173
-
174
- for (let name in m.definitions) {
175
-
176
- const entity = m.definitions[name]
177
- if (entity.kind === 'entity' && !entity['@cds.autoexposed'] && isChangeTracked(entity)) {
178
-
179
- if (!entity['@changelog.disable_assoc']) {
180
-
181
- // Add association to ChangeView...
182
- const keys = entityKey4(entity); if (!keys.length) continue // If no key attribute is defined for the entity, the logic to add association to ChangeView should be skipped.
183
- const assoc = new cds.builtin.classes.Association({ ...changes, on: [ ...changes.on, ...keys ] })
184
-
185
- // --------------------------------------------------------------------
186
- // PARKED: Add auto-exposed projection on ChangeView to service if applicable
187
- // const namespace = name.match(/^(.*)\.[^.]+$/)[1]
188
- // const service = m.definitions[namespace]
189
- // if (service) {
190
- // const projection = {from:{ref:[assoc.target]}}
191
- // m.definitions[assoc.target = namespace + '.' + Changes] = {
192
- // '@cds.autoexposed':true, kind:'entity', projection
193
- // }
194
- // DEBUG?.(`\n
195
- // extend service ${namespace} with {
196
- // entity ${Changes} as projection on ${projection.from.ref[0]};
197
- // }
198
- // `.replace(/ {10}/g,''))
199
- // }
200
- // --------------------------------------------------------------------
201
-
202
- DEBUG?.(`\n
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 ${ assoc.on.map(x => x.ref?.join('.') || x).join(' ') };
202
+ changes : Association to many ${assoc.target} on ${assoc.on.map((x) => x.ref?.join('.') || x).join(' ')};
205
203
  }
206
- `.replace(/ {8}/g,''))
207
- const query = entity.projection || entity.query?.SELECT
208
- if (query) (query.columns ??= ['*']).push({ as: 'changes', cast: assoc })
209
- else if (entity.elements) entity.elements.changes = assoc
210
-
211
- // Add UI.Facet for Change History List
212
- if (!entity['@changelog.disable_facet'])
213
- entity['@UI.Facets']?.push(facet)
214
- }
215
-
216
- if (entity.actions) {
217
- const hasParentInfo = entity[hasParent]
218
- const entityName = hasParentInfo?.entityName
219
- const parentEntity = entityName ? m.definitions[entityName] : null
220
- const isParentRootAndHasFacets = parentEntity?.[isRoot] && parentEntity?.['@UI.Facets']
221
- if (entity[isRoot] && entity['@UI.Facets']) {
222
- // Add side effects for root entity
223
- addSideEffects(entity.actions, true)
224
- } else if (isParentRootAndHasFacets) {
225
- // Add side effects for child entity
226
- addSideEffects(entity.actions, false, hasParentInfo?.associationName)
227
- }
228
- }
229
- }
230
- }
231
- (m.meta ??= {})[_enhanced] = true
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
- const { track_changes, _afterReadChangeView } = require("./lib/change-log")
237
- for (const srv of cds.services) {
238
- if (srv instanceof cds.ApplicationService) {
239
- let any = false
240
- for (const entity of Object.values(srv.entities)) {
241
- if (isChangeTracked(entity)) {
242
- cds.db.before("CREATE", entity, track_changes)
243
- cds.db.before("UPDATE", entity, track_changes)
244
- cds.db.before("DELETE", entity, track_changes)
245
- any = true
246
- }
247
- }
248
- if (any && srv.entities.ChangeView) {
249
- srv.after("READ", srv.entities.ChangeView, _afterReadChangeView)
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 => { DEBUG?.('on','compile.for.runtime'); enhanceModel(csn) })
258
- cds.on('compile.to.edmx', csn => { DEBUG?.('on','compile.to.edmx'); enhanceModel(csn) })
259
- cds.on('compile.to.dbx', csn => { DEBUG?.('on','compile.to.dbx'); enhanceModel(csn) })
260
- cds.on('served', addGenericHandlers)
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);