@cap-js/change-tracking 1.0.7 → 1.0.8

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,6 +4,33 @@ 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.0.8 - 28.03.25
8
+
9
+ ### Added
10
+
11
+ - Added @UI.MultiLineText to value fields
12
+ - Added support for Multi-Tenancy
13
+ - Added configuration options to disable tracking of CREATE/UPDATE/DELETE operations on a project level
14
+
15
+ ### Fixed
16
+
17
+ - Handling of numeric and boolean fields was faulty, when an initial value of `0` for numeric or `false` for boolean was supplied
18
+ - Decimal values were handled differently for HANA and SQlite
19
+ - Missing UI Label for one attribute (`ChangeLog.ID`) of the Changes UI facet
20
+ - Support for @UI.HeaderInfo.TypeName as fallback for the UI Label of the key
21
+ - Compilation error when an association is used as a key
22
+ - Fixed handling of unmanaged composition of many
23
+ - Proper casing of the operation enum type
24
+
25
+
26
+ ### Changed
27
+
28
+ - Added warning and mitigation for multi-tenant deployments with MTX
29
+ - Added a disclaimer of upcoming new version having a minimum requirement of CDS 8.6 for multitenancy fix
30
+ - Changed the default limit on non-HANA databases from 255 to 5000 characters for all String values
31
+ - Updated peer dependency from CDS7 to CDS8
32
+
33
+
7
34
  ## Version 1.0.7 - 20.08.24
8
35
 
9
36
  ### Added
package/README.md CHANGED
@@ -5,12 +5,24 @@ a [CDS plugin](https://cap.cloud.sap/docs/node.js/cds-plugins#cds-plugin-package
5
5
  [![REUSE status](https://api.reuse.software/badge/github.com/cap-js/change-tracking)](https://api.reuse.software/info/github.com/cap-js/change-tracking)
6
6
 
7
7
  > [!IMPORTANT]
8
- > This release establishes compatibility with CDS 8.
8
+ > Following the CAP best practices, the new release now requires CDS8 as minimum version in the peer dependencies. If you want to use the plugin with an older version of CAP (CDS7), you will need to manually update the peer dependency in `package.json`. Please be aware that there will be no support for this version of the plugin with a CDS version below 8!
9
+
10
+ > [!IMPORTANT]
11
+ > This release establishes support for multi-tenant deployments using MTX and extensibility.
9
12
  >
10
- > Since the prior release was using **APIs deprecated in CDS8**, the code was modified significantly to enable compatibility. While we tested extensively, there may still be glitches or unexpected situations which we did not cover. So please **test this release extensively before applying it to productive** scenarios. Please also report any bugs or glitches, ideally by contributing a test-case for us to incorporate.
13
+ > To achieve this, the code was modified significantly. While we tested extensively, there still may be glitches or unexpected situations which we did not cover. So please **test this release extensively before applying it to productive** scenarios. Please also report any bugs or glitches, ideally by contributing a test-case for us to incorporate.
11
14
  >
12
15
  > See the changelog for a full list of changes
13
16
 
17
+ > [!Warning]
18
+ >
19
+ > Please note that if your project is multi-tenant, then the CDS version must be higher than 8.6 and the mtx version higher than 2.5 for change-tracking to work.
20
+
21
+ > [!Warning]
22
+ >
23
+ > When using multi-tenancy with MTX, the generated facets and associations have to be created by the model provider of the MTX component. Therefore, the plugin also must be added to the `package.json` of the MTX sidecar.
24
+ >Although we tested this scenario extensively, there still might be cases where the automatic generation will not work as expected. If this happends in your scenario, we suggest using the `@changelog.disable_assoc` ([see here](#disable-association-to-changes-generation)) for all tracked entities and to add the association and facet manually to the service entity.
25
+
14
26
 
15
27
  ### Table of Contents
16
28
 
@@ -70,6 +82,7 @@ To enable change tracking, simply add this self-configuring plugin package to yo
70
82
  ```sh
71
83
  npm add @cap-js/change-tracking
72
84
  ```
85
+ If you use multi-tenancy, please add the plugin also to the MTX poroject(The mtx version must be higher than 2.5).
73
86
 
74
87
  ### 3. Annotations
75
88
 
@@ -235,6 +248,24 @@ For some scenarios, e.g. when doing `UNION` and the `@changelog` annotion is sti
235
248
  > [!IMPORTANT]
236
249
  > This will also supress the addition of the UI facet, since the change-view is not available as target entity anymore.
237
250
 
251
+ ### Select types of changes to track
252
+
253
+ If you do not want to track some types of changes, you can disable them using `disableCreateTracking`, `disableUpdateTracking`
254
+ and `disableDeleteTracking` configs in your project settings:
255
+ ```json
256
+ {
257
+ "cds": {
258
+ "requires": {
259
+ "change-tracking": {
260
+ "disableCreateTracking": true,
261
+ "disableUpdateTracking": false,
262
+ "disableDeleteTracking": true
263
+ }
264
+ }
265
+ }
266
+ }
267
+ ```
268
+
238
269
  ### Preserve change logs of deleted data
239
270
 
240
271
  By default, deleting a record will also automatically delete all associated change logs. This helps reduce the impact on the size of the database.
@@ -574,6 +605,7 @@ this.on("UpdateActivationStatus", async (req) =>
574
605
 
575
606
  The reason is that: Application level services are by design the only place where business logic is enforced. This by extension means, that it also is the only point where e.g. change-tracking would be enabled. The underlying method used to do change tracking is `req.diff` which is responsible to read the necessary before-image from the database, and this method is not available on DB level.
576
607
 
608
+
577
609
  ## Contributing
578
610
 
579
611
  This project is open to feature requests/suggestions, bug reports etc. via [GitHub issues](https://github.com/cap-js/change-tracking/issues). Contribution and feedback are encouraged and always welcome. For more information about how to contribute, the project structure, as well as additional contribution information, see our [Contribution Guidelines](CONTRIBUTING.md).
package/cds-plugin.js CHANGED
@@ -1,12 +1,14 @@
1
1
  const cds = require('@sap/cds')
2
+ const DEBUG = cds.debug('changelog')
2
3
 
3
4
  const isRoot = 'change-tracking-isRootEntity'
4
5
  const hasParent = 'change-tracking-parentEntity'
5
6
 
6
- const isChangeTracked = (entity) => (
7
- (entity['@changelog']
8
- || entity.elements && Object.values(entity.elements).some(e => e['@changelog'])) && entity.query?.SET?.op !== 'union'
9
- )
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
+ }
10
12
 
11
13
  // Add the appropriate Side Effects attribute to the custom action
12
14
  const addSideEffects = (actions, flag, element) => {
@@ -30,179 +32,207 @@ const addSideEffects = (actions, flag, element) => {
30
32
  }
31
33
  }
32
34
 
33
- function setChangeTrackingIsRootEntity(entity, csn, val = true) {
35
+ function setChangeTrackingIsRootEntity (entity, csn, val = true) {
34
36
  if (csn.definitions?.[entity.name]) {
35
- csn.definitions[entity.name][isRoot] = val;
37
+ csn.definitions[entity.name][isRoot] = val
36
38
  }
37
39
  }
38
40
 
39
- function checkAndSetRootEntity(parentEntity, entity, csn) {
41
+ function checkAndSetRootEntity (parentEntity, entity, csn) {
40
42
  if (entity[isRoot] === false) {
41
- return entity;
43
+ return entity
42
44
  }
43
45
  if (parentEntity) {
44
- return compositionRoot(parentEntity, csn);
46
+ return compositionRoot(parentEntity, csn)
45
47
  } else {
46
- setChangeTrackingIsRootEntity(entity, csn);
47
- return { ...csn.definitions?.[entity.name], name: entity.name };
48
+ setChangeTrackingIsRootEntity(entity, csn)
49
+ return { ...csn.definitions?.[entity.name], name: entity.name }
48
50
  }
49
51
  }
50
52
 
51
- function processEntities(m) {
53
+ function processEntities (m) {
52
54
  for (let name in m.definitions) {
53
- compositionRoot({...m.definitions[name], name}, m)
55
+ compositionRoot({ ...m.definitions[name], name }, m)
54
56
  }
55
57
  }
56
58
 
57
- function compositionRoot(entity, csn) {
59
+ function compositionRoot (entity, csn) {
58
60
  if (!entity || entity.kind !== 'entity') {
59
- return;
61
+ return
60
62
  }
61
- const parentEntity = compositionParent(entity, csn);
62
- return checkAndSetRootEntity(parentEntity, entity, csn);
63
+ const parentEntity = compositionParent(entity, csn)
64
+ return checkAndSetRootEntity(parentEntity, entity, csn)
63
65
  }
64
66
 
65
- function compositionParent(entity, csn) {
67
+ function compositionParent (entity, csn) {
66
68
  if (!entity || entity.kind !== 'entity') {
67
- return;
69
+ return
68
70
  }
69
- const parentAssociation = compositionParentAssociation(entity, csn);
70
- return parentAssociation ?? null;
71
+ const parentAssociation = compositionParentAssociation(entity, csn)
72
+ return parentAssociation ?? null
71
73
  }
72
74
 
73
- function compositionParentAssociation(entity, csn) {
75
+ function compositionParentAssociation (entity, csn) {
74
76
  if (!entity || entity.kind !== 'entity') {
75
- return;
77
+ return
76
78
  }
77
- const elements = entity.elements ?? {};
79
+ const elements = entity.elements ?? {}
78
80
 
79
81
  // Add the change-tracking-isRootEntity attribute of the child entity
80
- processCompositionElements(entity, csn, elements);
82
+ processCompositionElements(entity, csn, elements)
81
83
 
82
- const hasChildFlag = entity[isRoot] !== false;
83
- const hasParentEntity = entity[hasParent];
84
+ const hasChildFlag = entity[isRoot] !== false
85
+ const hasParentEntity = entity[hasParent]
84
86
 
85
87
  if (hasChildFlag || !hasParentEntity) {
86
88
  // Find parent association of the entity
87
- const parentAssociation = findParentAssociation(entity, csn, elements);
89
+ const parentAssociation = findParentAssociation(entity, csn, elements)
88
90
  if (parentAssociation) {
89
- const parentAssociationTarget = elements[parentAssociation]?.target;
90
- if (hasChildFlag) setChangeTrackingIsRootEntity(entity, csn, false);
91
+ const parentAssociationTarget = elements[parentAssociation]?.target
92
+ if (hasChildFlag) setChangeTrackingIsRootEntity(entity, csn, false)
91
93
  return {
92
94
  ...csn.definitions?.[parentAssociationTarget],
93
95
  name: parentAssociationTarget
94
- };
95
- } else return;
96
+ }
97
+ } else return
96
98
  }
97
- return { ...csn.definitions?.[entity.name], name: entity.name };
99
+ return { ...csn.definitions?.[entity.name], name: entity.name }
98
100
  }
99
101
 
100
- function processCompositionElements(entity, csn, elements) {
102
+ function processCompositionElements (entity, csn, elements) {
101
103
  for (const name in elements) {
102
- const element = elements[name];
103
- const target = element?.target;
104
- const definition = csn.definitions?.[target];
104
+ const element = elements[name]
105
+ const target = element?.target
106
+ const definition = csn.definitions?.[target]
105
107
  if (
106
108
  element.type !== 'cds.Composition' ||
107
109
  target === entity.name ||
108
110
  !definition ||
109
111
  definition[isRoot] === false
110
112
  ) {
111
- continue;
113
+ continue
112
114
  }
113
- setChangeTrackingIsRootEntity({ ...definition, name: target }, csn, false);
115
+ setChangeTrackingIsRootEntity({ ...definition, name: target }, csn, false)
114
116
  }
115
117
  }
116
118
 
117
- function findParentAssociation(entity, csn, elements) {
119
+ function findParentAssociation (entity, csn, elements) {
118
120
  return Object.keys(elements).find((name) => {
119
- const element = elements[name];
120
- const target = element?.target;
121
+ const element = elements[name]
122
+ const target = element?.target
121
123
  if (element.type === 'cds.Association' && target !== entity.name) {
122
- const parentDefinition = csn.definitions?.[target] ?? {};
123
- const parentElements = parentDefinition?.elements ?? {};
124
+ const parentDefinition = csn.definitions?.[target] ?? {}
125
+ const parentElements = parentDefinition?.elements ?? {}
124
126
  return !!Object.keys(parentElements).find((parentEntityName) => {
125
- const parentElement = parentElements?.[parentEntityName] ?? {};
127
+ const parentElement = parentElements?.[parentEntityName] ?? {}
126
128
  if (parentElement.type === 'cds.Composition') {
127
- const isCompositionEntity = parentElement.target === entity.name;
129
+ const isCompositionEntity = parentElement.target === entity.name
128
130
  // add parent information in the current entity
129
131
  if (isCompositionEntity) {
130
132
  csn.definitions[entity.name][hasParent] = {
131
133
  associationName: name,
132
134
  entityName: target
133
- };
135
+ }
134
136
  }
135
- return isCompositionEntity;
137
+ return isCompositionEntity
136
138
  }
137
- });
139
+ })
138
140
  }
139
- });
141
+ })
142
+ }
143
+
144
+
145
+
146
+ /**
147
+ * Returns an expression for the key of the given entity, which we can use as the right-hand-side of an ON condition.
148
+ */
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
140
158
  }
141
159
 
160
+
142
161
  // Unfold @changelog annotations in loaded model
143
- cds.on('loaded', m => {
162
+ function enhanceModel (m) {
163
+
164
+ const _enhanced = 'sap.changelog.enhanced'
165
+ if (m.meta?.[_enhanced]) return // already enhanced
144
166
 
145
167
  // Get definitions from Dummy entity in our models
146
168
  const { 'sap.changelog.aspect': aspect } = m.definitions; if (!aspect) return // some other model
147
169
  const { '@UI.Facets': [facet], elements: { changes } } = aspect
148
- changes.on.pop() // remove ID -> filled in below
149
-
150
- // Process entities to define the relation
151
- processEntities(m)
170
+ if (changes.on.length > 2) changes.on.pop() // remove ID -> filled in below
171
+
172
+ processEntities(m) // REVISIT: why is that required ?!?
152
173
 
153
174
  for (let name in m.definitions) {
175
+
154
176
  const entity = m.definitions[name]
155
- if (isChangeTracked(entity)) {
177
+ if (entity.kind === 'entity' && !entity['@cds.autoexposed'] && isChangeTracked(entity)) {
156
178
 
157
- // Determine entity keys
158
- const keys = [], { elements: elms } = entity
159
- for (let e in elms) if (elms[e].key) keys.push(e)
179
+ if (!entity['@changelog.disable_assoc']) {
160
180
 
161
- // If no key attribute is defined for the entity, the logic to add association to ChangeView should be skipped.
162
- if(keys.length === 0) {
163
- continue;
164
- }
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 = { ...changes, on: [ ...changes.on, ...keys ] }
165
184
 
166
- // Add association to ChangeView...
167
- const on = [...changes.on]; keys.forEach((k, i) => { i && on.push('||'); on.push({
168
- ref: k === 'up_' ? [k,'ID'] : [k] // REVISIT: up_ handling is a dirty hack for now
169
- })})
170
- const assoc = { ...changes, on }
171
- const query = entity.projection || entity.query?.SELECT
172
- if(!entity['@changelog.disable_assoc'])
173
- {
174
- if (query) {
175
- (query.columns ??= ['*']).push({ as: 'changes', cast: assoc })
176
- } else {
177
- entity.elements.changes = assoc
178
- }
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
203
+ extend ${name} with {
204
+ changes : Association to many ${assoc.target} on ${ assoc.on.map(x => x.ref?.join('.') || x).join(' ') };
205
+ }
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
179
210
 
180
- // Add UI.Facet for Change History List
181
- if(!entity['@changelog.disable_facet'])
182
- entity['@UI.Facets']?.push(facet)
211
+ // Add UI.Facet for Change History List
212
+ if (!entity['@changelog.disable_facet'])
213
+ entity['@UI.Facets']?.push(facet)
183
214
  }
184
215
 
185
216
  if (entity.actions) {
186
- const hasParentInfo = entity[hasParent];
187
- const entityName = hasParentInfo?.entityName;
188
- const parentEntity = entityName ? m.definitions[entityName] : null;
189
-
190
- const isParentRootAndHasFacets = parentEntity?.[isRoot] && parentEntity?.['@UI.Facets'];
191
-
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']
192
221
  if (entity[isRoot] && entity['@UI.Facets']) {
193
222
  // Add side effects for root entity
194
- addSideEffects(entity.actions, true);
223
+ addSideEffects(entity.actions, true)
195
224
  } else if (isParentRootAndHasFacets) {
196
225
  // Add side effects for child entity
197
- addSideEffects(entity.actions, false, hasParentInfo?.associationName);
226
+ addSideEffects(entity.actions, false, hasParentInfo?.associationName)
198
227
  }
199
228
  }
200
229
  }
201
230
  }
202
- })
231
+ (m.meta ??= {})[_enhanced] = true
232
+ }
203
233
 
204
234
  // Add generic change tracking handlers
205
- cds.on('served', () => {
235
+ function addGenericHandlers() {
206
236
  const { track_changes, _afterReadChangeView } = require("./lib/change-log")
207
237
  for (const srv of cds.services) {
208
238
  if (srv instanceof cds.ApplicationService) {
@@ -220,4 +250,11 @@ cds.on('served', () => {
220
250
  }
221
251
  }
222
252
  }
223
- })
253
+ }
254
+
255
+
256
+ // 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)
package/index.cds CHANGED
@@ -4,7 +4,7 @@ namespace sap.changelog;
4
4
  /**
5
5
  * Used in cds-plugin.js as template for tracked entities
6
6
  */
7
- aspect aspect @(UI.Facets: [{
7
+ @cds.persistence.skip entity aspect @(UI.Facets: [{
8
8
  $Type : 'UI.ReferenceFacet',
9
9
  ID : 'ChangeHistoryFacet',
10
10
  Label : '{i18n>ChangeHistory}',
@@ -37,15 +37,14 @@ view ChangeView as
37
37
  * Top-level changes entity, e.g. UPDATE Incident by, at, ...
38
38
  */
39
39
  entity ChangeLog : managed, cuid {
40
- serviceEntity : String @title: '{i18n>ChangeLog.serviceEntity}'; // definition name of target entity (on service level) - e.g. ProcessorsService.Incidents
41
- entity : String @title: '{i18n>ChangeLog.entity}'; // definition name of target entity (on db level) - e.g. sap.capire.incidents.Incidents
40
+ serviceEntity : String(5000) @title: '{i18n>ChangeLog.serviceEntity}'; // definition name of target entity (on service level) - e.g. ProcessorsService.Incidents
41
+ entity : String(5000) @title: '{i18n>ChangeLog.entity}'; // definition name of target entity (on db level) - e.g. sap.capire.incidents.Incidents
42
42
  entityKey : UUID @title: '{i18n>ChangeLog.entityKey}'; // primary key of target entity, e.g. Incidents.ID
43
43
  createdAt : managed:createdAt;
44
44
  createdBy : managed:createdBy;
45
45
  changes : Composition of many Changes on changes.changeLog = $self;
46
46
  }
47
47
 
48
-
49
48
  /**
50
49
  * Attribute-level Changes with simple capturing of one-level
51
50
  * composition trees in parent... elements.
@@ -53,30 +52,30 @@ entity ChangeLog : managed, cuid {
53
52
  entity Changes {
54
53
 
55
54
  key ID : UUID @UI.Hidden;
56
- keys : String @title: '{i18n>Changes.keys}';
57
- attribute : String @title: '{i18n>Changes.attribute}';
58
- valueChangedFrom : String @title: '{i18n>Changes.valueChangedFrom}';
59
- valueChangedTo : String @title: '{i18n>Changes.valueChangedTo}';
55
+ keys : String(5000) @title: '{i18n>Changes.keys}';
56
+ attribute : String(5000) @title: '{i18n>Changes.attribute}';
57
+ valueChangedFrom : String(5000) @title: '{i18n>Changes.valueChangedFrom}' @UI.MultiLineText;
58
+ valueChangedTo : String(5000) @title: '{i18n>Changes.valueChangedTo}' @UI.MultiLineText;
60
59
 
61
60
  // Business meaningful object id
62
- entityID : String @title: '{i18n>Changes.entityID}';
63
- entity : String @title: '{i18n>Changes.entity}'; // similar to ChangeLog.entity, but could be nested entity in a composition tree
64
- serviceEntity : String @title: '{i18n>Changes.serviceEntity}'; // similar to ChangeLog.serviceEntity, but could be nested entity in a composition tree
61
+ entityID : String(5000) @title: '{i18n>Changes.entityID}';
62
+ entity : String(5000) @title: '{i18n>Changes.entity}'; // similar to ChangeLog.entity, but could be nested entity in a composition tree
63
+ serviceEntity : String(5000) @title: '{i18n>Changes.serviceEntity}'; // similar to ChangeLog.serviceEntity, but could be nested entity in a composition tree
65
64
 
66
65
  // Business meaningful parent object id
67
- parentEntityID : String @title: '{i18n>Changes.parentEntityID}';
66
+ parentEntityID : String(5000) @title: '{i18n>Changes.parentEntityID}';
68
67
  parentKey : UUID @title: '{i18n>Changes.parentKey}';
69
- serviceEntityPath : String @title: '{i18n>Changes.serviceEntityPath}';
68
+ serviceEntityPath : String(5000) @title: '{i18n>Changes.serviceEntityPath}';
70
69
 
71
70
  @title: '{i18n>Changes.modification}'
72
71
  modification : String enum {
73
- create = 'Create';
74
- update = 'Edit';
75
- delete = 'Delete';
72
+ Create = 'create';
73
+ Update = 'update';
74
+ Delete = 'delete';
76
75
  };
77
76
 
78
- valueDataType : String @title: '{i18n>Changes.valueDataType}';
79
- changeLog : Association to ChangeLog @title: '{i18n>ChangeLog.ID}';
77
+ valueDataType : String(5000) @title: '{i18n>Changes.valueDataType}';
78
+ changeLog : Association to ChangeLog @title: '{i18n>ChangeLog.ID}' @UI.Hidden;
80
79
  }
81
80
 
82
81
  annotate ChangeView with @(UI: {
package/lib/change-log.js CHANGED
@@ -18,6 +18,25 @@ const { localizeLogFields } = require("./localization")
18
18
  const isRoot = "change-tracking-isRootEntity"
19
19
 
20
20
 
21
+ function formatDecimal(str, scale) {
22
+ if (typeof str === "number" && !isNaN(str)) {
23
+ str = String(str);
24
+ } else return str;
25
+
26
+ if (scale > 0) {
27
+ let parts = str.split(".");
28
+ let decimalPart = parts[1] || "";
29
+
30
+ while (decimalPart.length < scale) {
31
+ decimalPart += "0";
32
+ }
33
+
34
+ return `${parts[0]}.${decimalPart}`;
35
+ }
36
+
37
+ return str;
38
+ }
39
+
21
40
  const _getRootEntityPathVals = function (txContext, entity, entityKey) {
22
41
  const serviceEntityPathVals = []
23
42
  const entityIDs = _getEntityIDs(txContext.params)
@@ -168,7 +187,7 @@ const _formatCompositionContext = async function (changes, reqData) {
168
187
  const childNodeChanges = []
169
188
 
170
189
  for (const change of changes) {
171
- if (typeof change.valueChangedTo === "object") {
190
+ if (typeof change.valueChangedTo === "object" && change.valueChangedTo instanceof Date !== true) {
172
191
  if (!Array.isArray(change.valueChangedTo)) {
173
192
  change.valueChangedTo = [change.valueChangedTo]
174
193
  }
@@ -197,7 +216,9 @@ const _formatCompositionValue = function (
197
216
  childNodeChange,
198
217
  childNodeChanges
199
218
  ) {
200
- if (curChange.modification === "delete") {
219
+ if (curChange.modification === undefined) {
220
+ return
221
+ } else if (curChange.modification === "delete") {
201
222
  curChange.valueChangedFrom = objId
202
223
  curChange.valueChangedTo = ""
203
224
  } else if (curChange.modification === "update") {
@@ -316,6 +337,28 @@ function _trackedChanges4 (srv, target, diff) {
316
337
  const eleParentKeys = element.parent.keys
317
338
  if (from === to) return
318
339
 
340
+ /**
341
+ *
342
+ * HANA driver always filling up the defined decimal places with zeros,
343
+ * need to skip the change log if the value is not changed.
344
+ * Example:
345
+ * entity Books : cuid {
346
+ * price : Decimal(11, 4);
347
+ * }
348
+ * When price is updated from 3000.0000 to 3000,
349
+ * the change log should not be created.
350
+ */
351
+ if (
352
+ row._op === "update" &&
353
+ element.type === "cds.Decimal" &&
354
+ cds.db.kind === "hana" &&
355
+ typeof to === "number"
356
+ ) {
357
+ const scaleNum = element.scale || 0;
358
+ if (from === formatDecimal(to, scaleNum))
359
+ return;
360
+ }
361
+
319
362
  /**
320
363
  *
321
364
  * For the Inline entity such as Items,
@@ -441,8 +484,21 @@ function getAssociationDetails (entity) {
441
484
  return { ID, foreignKey, parentEntity };
442
485
  }
443
486
 
487
+ function isEmpty(value) {
488
+ return value === null || value === undefined || value === "";
489
+ }
444
490
 
445
491
  async function track_changes (req) {
492
+ const config = cds.env.requires["change-tracking"];
493
+
494
+ if (
495
+ (req.event === 'UPDATE' && config?.disableUpdateTracking) ||
496
+ (req.event === 'CREATE' && config?.disableCreateTracking) ||
497
+ (req.event === 'DELETE' && config?.disableDeleteTracking)
498
+ ) {
499
+ return;
500
+ }
501
+
446
502
  let diff = await req.diff()
447
503
  if (!diff) return
448
504
 
@@ -486,7 +542,7 @@ async function track_changes (req) {
486
542
  entity: dbEntity.name,
487
543
  entityKey: entityKey,
488
544
  serviceEntity: target.name || target,
489
- changes: changes.filter(c => c.valueChangedFrom || c.valueChangedTo).map((c) => ({
545
+ changes: changes.filter(c => !isEmpty(c.valueChangedFrom) || !isEmpty(c.valueChangedTo)).map((c) => ({
490
546
  ...c,
491
547
  valueChangedFrom: `${c.valueChangedFrom ?? ''}`,
492
548
  valueChangedTo: `${c.valueChangedTo ?? ''}`,
@@ -98,7 +98,7 @@ const _getLabelI18nKeyOnEntity = function (entityName, /** optinal */ attribute)
98
98
  let def = cds.model.definitions[entityName];
99
99
  if (attribute) def = def?.elements[attribute]
100
100
  if (!def) return "";
101
- return def['@Common.Label'] || def['@title'];
101
+ return def['@Common.Label'] || def['@title'] || def['@UI.HeaderInfo.TypeName'];
102
102
  };
103
103
 
104
104
  const localizeLogFields = function (data, locale) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/change-tracking",
3
- "version": "1.0.7",
3
+ "version": "1.0.8",
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)",
@@ -18,7 +18,7 @@
18
18
  "test": "npx jest --silent"
19
19
  },
20
20
  "peerDependencies": {
21
- "@sap/cds": ">=7"
21
+ "@sap/cds": ">=8"
22
22
  },
23
23
  "devDependencies": {
24
24
  "@cap-js/change-tracking": "file:.",