@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 +27 -0
- package/README.md +34 -2
- package/cds-plugin.js +127 -90
- package/index.cds +17 -18
- package/lib/change-log.js +59 -3
- package/lib/localization.js +1 -1
- package/package.json +2 -2
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
|
[](https://api.reuse.software/info/github.com/cap-js/change-tracking)
|
|
6
6
|
|
|
7
7
|
> [!IMPORTANT]
|
|
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
|
-
>
|
|
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
|
|
8
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
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
|
-
|
|
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
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
(
|
|
176
|
-
|
|
177
|
-
entity.
|
|
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
|
-
|
|
181
|
-
|
|
182
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
57
|
-
attribute : String
|
|
58
|
-
valueChangedFrom : String
|
|
59
|
-
valueChangedTo : String
|
|
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
|
|
63
|
-
entity : String
|
|
64
|
-
serviceEntity : String
|
|
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
|
|
66
|
+
parentEntityID : String(5000) @title: '{i18n>Changes.parentEntityID}';
|
|
68
67
|
parentKey : UUID @title: '{i18n>Changes.parentKey}';
|
|
69
|
-
serviceEntityPath : String
|
|
68
|
+
serviceEntityPath : String(5000) @title: '{i18n>Changes.serviceEntityPath}';
|
|
70
69
|
|
|
71
70
|
@title: '{i18n>Changes.modification}'
|
|
72
71
|
modification : String enum {
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
72
|
+
Create = 'create';
|
|
73
|
+
Update = 'update';
|
|
74
|
+
Delete = 'delete';
|
|
76
75
|
};
|
|
77
76
|
|
|
78
|
-
valueDataType : String
|
|
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 ===
|
|
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 ?? ''}`,
|
package/lib/localization.js
CHANGED
|
@@ -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.
|
|
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": ">=
|
|
21
|
+
"@sap/cds": ">=8"
|
|
22
22
|
},
|
|
23
23
|
"devDependencies": {
|
|
24
24
|
"@cap-js/change-tracking": "file:.",
|