@cap-js/change-tracking 1.0.4 → 1.0.6

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,28 @@ 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.6 - TBD
8
+
9
+ ### Fixed
10
+
11
+ - Storage of wrong ObjectID in some special scenarios
12
+ - Missing localization of managed fields
13
+ - Views without keys won't get the association and UI facet pushed anymore
14
+
15
+ ### Added
16
+
17
+ - A method to disable automatic generation of the UI Facet
18
+
19
+ ### Changed
20
+
21
+ - Improved documentation of the @changelog Annotation
22
+
23
+ ## Version 1.0.5 - 15.01.24
24
+
25
+ ### Fixed
26
+
27
+ - Error on HANA when logging Boolean or Numeric Data
28
+
7
29
  ## Version 1.0.4 - 08.01.24
8
30
 
9
31
  ### Added
package/README.md CHANGED
@@ -10,12 +10,10 @@ The `@cap-js/change-tracking` package is a [CDS plugin](https://cap.cloud.sap/do
10
10
 
11
11
  <img width="1300" alt="change-history-loading" src="_assets/change-history.gif">
12
12
 
13
-
14
-
15
13
  ### Table of Contents
16
14
 
17
15
  - [Change Tracking Plugin for SAP Cloud Application Programming Model (CAP)](#change-tracking-plugin-for-sap-cloud-application-programming-model-cap)
18
- - [Table of Contents](#table-of-contents)
16
+ - [Table of Contents](#table-of-contents)
19
17
  - [Preliminaries](#preliminaries)
20
18
  - [Setup](#setup)
21
19
  - [Annotations](#annotations)
@@ -27,12 +25,16 @@ The `@cap-js/change-tracking` package is a [CDS plugin](https://cap.cloud.sap/do
27
25
  - [Customizations](#customizations)
28
26
  - [Altered table view](#altered-table-view)
29
27
  - [Disable lazy loading](#disable-lazy-loading)
28
+ - [Disable UI Facet generation](#disable-ui-facet-generation)
29
+ - [Disable Association to Changes Generation](#disable-association-to-changes-generation)
30
+ - [Modelling Samples](#modelling-samples)
31
+ - [Specify Object ID](#specify-object-id)
32
+ - [Tracing Changes](#tracing-changes)
33
+ - [Don'ts](#donts)
30
34
  - [Contributing](#contributing)
31
35
  - [Code of Conduct](#code-of-conduct)
32
36
  - [Licensing](#licensing)
33
37
 
34
-
35
-
36
38
  ## Preliminaries
37
39
 
38
40
  In this guide, we use the [Incidents Management reference sample app](https://github.com/cap-js/incidents-app) as the base to add change tracking to. Clone the repository and apply the step-by-step instructions:
@@ -55,8 +57,6 @@ npm i
55
57
  cds w samples/change-tracking
56
58
  ```
57
59
 
58
-
59
-
60
60
  ## Setup
61
61
 
62
62
  To enable change tracking, simply add this self-configuring plugin package to your project:
@@ -65,8 +65,6 @@ To enable change tracking, simply add this self-configuring plugin package to yo
65
65
  npm add @cap-js/change-tracking
66
66
  ```
67
67
 
68
-
69
-
70
68
  ## Annotations
71
69
 
72
70
  > [!WARNING]
@@ -92,7 +90,6 @@ The minimal annotation we require for change tracking is `@changelog` on element
92
90
 
93
91
  Additional identifiers or labels can be added to obtain more *human-readable* change records as described below.
94
92
 
95
-
96
93
  ### Human-readable Types and Fields
97
94
 
98
95
  By default the implementation looks up *Object Type* names or *Field* namesfrom respective `@title` or `@Common.Label` annotations, and applies i18n lookups. If no such annotations are given, the technical names of the respective CDS definitions are displayed.
@@ -111,7 +108,6 @@ We get a human-readable display for *Object Type*:
111
108
 
112
109
  <img width="1300" alt="change-history-type-hr" src="_assets/changes-type-hr-wbox.png">
113
110
 
114
-
115
111
  ### Human-readable IDs
116
112
 
117
113
  The changelog annotations for *Object ID* are defined at entity level.
@@ -123,6 +119,7 @@ For example, having a `@changelog` annotation without any additional identifiers
123
119
  ```cds
124
120
  annotate ProcessorService.Conversations {
125
121
  ```
122
+
126
123
  <img width="1300" alt="change-history-id" src="_assets/changes-id-wbox.png">
127
124
 
128
125
  However, this is not advisable as we cannot easily distinguish between changes. It is more appropriate to annotate as follows:
@@ -130,11 +127,11 @@ However, this is not advisable as we cannot easily distinguish between changes.
130
127
  ```cds
131
128
  annotate ProcessorService.Conversations with @changelog: [author, timestamp] {
132
129
  ```
130
+
133
131
  <img width="1300" alt="change-history-id-hr" src="_assets/changes-id-hr-wbox.png">
134
132
 
135
133
  Expanding the changelog annotation by additional identifiers `[author, timestamp]`, we can now better identify the `message` change events by their respective author and timestamp.
136
134
 
137
-
138
135
  ### Human-readable Values
139
136
 
140
137
  The changelog annotations for *New Value* and *Old Value* are defined at element level.
@@ -144,7 +141,7 @@ They are already human-readable by default, unless the `@changelog` definition c
144
141
  For example, having a `@changelog` annotation without any additional identifiers, changes to incident customer would show up as UUIDs:
145
142
 
146
143
  ```cds
147
- customer @changelog;
144
+ customer @changelog;
148
145
  ```
149
146
 
150
147
  <img width="1300" alt="change-history-value" src="_assets/changes-value-wbox.png">
@@ -152,20 +149,21 @@ For example, having a `@changelog` annotation without any additional identifiers
152
149
  Hence, here it is essential to add a unique identifier to obtain human-readable value columns:
153
150
 
154
151
  ```cds
155
- customer @changelog: [customer.name];
152
+ customer @changelog: [customer.name];
156
153
  ```
157
154
 
158
155
  <img width="1300" alt="change-history-value-hr" src="_assets/changes-value-hr-wbox.png">
159
156
 
160
-
161
157
  ## Test-drive locally
162
158
 
163
159
  With the steps above, we have successfully set up change tracking for our reference application. Let's see that in action.
164
160
 
165
161
  1. **Start the server**:
166
- ```sh
167
- cds watch
168
- ```
162
+
163
+ ```sh
164
+ cds watch
165
+ ```
166
+
169
167
  2. **Make a change** on your change-tracked elements. This change will automatically be persisted in the database table (`sap.changelog.ChangeLog`) and made available in a pre-defined view, namely the [Change History view](#change-history-view) for your convenience.
170
168
 
171
169
  ## Change History View
@@ -218,21 +216,332 @@ annotate sap.changelog.aspect @(UI.Facets: [{
218
216
  Target: 'changes/@UI.PresentationVariant',
219
217
  ![@UI.PartOfPreview]
220
218
  }]);
221
-
222
219
  ```
223
220
 
224
221
  The system now uses the SAPUI5 default setting `![@UI.PartOfPreview]: true`, such that the table will always shown when navigating to that respective Object page.
225
222
 
223
+ ### Disable UI Facet generation
224
+
225
+ If you do not want the UI facet added to a specific UI, you can annotate the service entity with `@changelog.disable_facet`. This will disable the automatic addition of the UI faced to this specific entity, but also all views or further projections up the chain.
226
+
227
+ ### Disable Association to Changes Generation
228
+
229
+ For some scenarios, e.g. when doing `UNION` and the `@changelog` annotion is still propageted, the automatic addition of the association to `changes` does not make sense. You can use `@changelog.disable_assoc`for this to be disabled on entity level.
230
+
231
+ > [!IMPORTANT]
232
+ > This will also supress the addition of the UI facet, since the change-view is not available as target entity anymore.
233
+
234
+ ## Modelling Samples
235
+
236
+ This chapter describes more modelling cases for further reference, from simple to complex, including but not limited to the followings.
237
+
238
+ ### Specify Object ID
239
+
240
+ Use cases for Object ID annotation
241
+
242
+ #### Use Case 1: Annotate single field/multiple fields of associated table(s) as the Object ID
243
+
244
+ Modelling in `db/schema.cds`
245
+
246
+ ```cds
247
+ entity Incidents : cuid, managed {
248
+ ...
249
+ customer : Association to Customers;
250
+ title : String @title: 'Title';
251
+ urgency : Association to Urgency default 'M';
252
+ status : Association to Status default 'N';
253
+ ...
254
+ }
255
+ ```
256
+
257
+ Add the following `@changelog` annotations in `srv/change-tracking.cds`
258
+
259
+ ```cds
260
+ annotate ProcessorService.Incidents with @changelog: [customer.name, urgency.code, status.criticality] {
261
+ title @changelog;
262
+ }
263
+ ```
264
+
265
+ ![AssociationID](_assets/AssociationID.png)
266
+
267
+ #### Use Case 2: Annotate single field/multiple fields of project customized types as the Object ID
268
+
269
+ Modelling in `db/schema.cds`
270
+
271
+ ```cds
272
+ entity Incidents : cuid, managed {
273
+ ...
274
+ customer : Association to Customers;
275
+ title : String @title: 'Title';
276
+ ...
277
+ }
278
+
279
+ entity Customers : cuid, managed {
280
+ ...
281
+ email : EMailAddress; // customized type
282
+ phone : PhoneNumber; // customized type
283
+ ...
284
+ }
285
+ ```
286
+
287
+ Add the following `@changelog` annotations in `srv/change-tracking.cds`
288
+
289
+ ```cds
290
+ annotate ProcessorService.Incidents with @changelog: [customer.email, customer.phone] {
291
+ title @changelog;
292
+ }
293
+ ```
294
+
295
+ ![CustomTypeID](_assets/CustomTypeID.png)
296
+
297
+ #### Use Case 3: Annotate chained associated entities from the current entity as the Object ID
298
+
299
+ Modelling in `db/schema.cds`
300
+
301
+ ```cds
302
+ entity Incidents : cuid, managed {
303
+ ...
304
+ customer : Association to Customers;
305
+ ...
306
+ }
307
+
308
+ entity Customers : cuid, managed {
309
+ ...
310
+ addresses : Association to Addresses;
311
+ ...
312
+ }
313
+ ```
314
+
315
+ Add the following `@changelog` annotations in `srv/change-tracking.cds`
316
+
317
+ ```cds
318
+ annotate ProcessorService.Incidents with @changelog: [customer.addresses.city, customer.addresses.postCode] {
319
+ title @changelog;
320
+ }
321
+ ```
322
+
323
+ ![ChainedAssociationID](_assets/ChainedAssociationID.png)
324
+
325
+ > Change-tracking supports annotating chained associated entities from the current entity as object ID of current entity in case the entity in consumer applications is a pure relation table. However, the usage of chained associated entities is not recommended due to performance cost.
326
+
327
+ ### Tracing Changes
328
+
329
+ Use cases for tracing changes
330
+
331
+ #### Use Case 1: Trace the changes of child nodes from the current entity and display the meaningful data from child nodes (composition relation)
332
+
333
+ Modelling in `db/schema.cds`
334
+
335
+ ```cds
336
+ entity Incidents : managed, cuid {
337
+ ...
338
+ title : String @title: 'Title';
339
+ conversation : Composition of many Conversation;
340
+ ...
341
+ }
342
+
343
+ aspect Conversation: managed, cuid {
344
+ ...
345
+ message : String;
346
+ }
347
+ ```
348
+
349
+ Add the following `@changelog` annotations in `srv/change-tracking.cds`
350
+
351
+ ```cds
352
+ annotate ProcessorService.Incidents with @changelog: [title] {
353
+ conversation @changelog: [conversation.message];
354
+ }
355
+ ```
356
+
357
+ ![CompositionChange](_assets/CompositionChange.png)
358
+
359
+ #### Use Case 2: Trace the changes of associated entities from the current entity and display the meaningful data from associated entities (association relation)
360
+
361
+ Modelling in `db/schema.cds`
362
+
363
+ ```cds
364
+ entity Incidents : cuid, managed {
365
+ ...
366
+ customer : Association to Customers;
367
+ title : String @title: 'Title';
368
+ ...
369
+ }
370
+
371
+ entity Customers : cuid, managed {
372
+ ...
373
+ email : EMailAddress;
374
+ ...
375
+ }
376
+ ```
377
+
378
+ Add the following `@changelog` annotations in `srv/change-tracking.cds`
379
+
380
+ ```cds
381
+ annotate ProcessorService.Incidents with @changelog: [title] {
382
+ customer @changelog: [customer.email];
383
+ }
384
+ ```
385
+
386
+ ![AssociationChange](_assets/AssociationChange.png)
387
+
388
+ #### Use Case 3: Trace the changes of fields defined by project customized types and display the meaningful data
389
+
390
+ Modelling in `db/schema.cds`
391
+
392
+ ```cds
393
+ type StatusType : Association to Status;
394
+
395
+ entity Incidents : cuid, managed {
396
+ ...
397
+ title : String @title: 'Title';
398
+ status : StatusType default 'N';
399
+ ...
400
+ }
401
+ ```
402
+
403
+ Add the following `@changelog` annotations in `srv/change-tracking.cds`
404
+
405
+ ```cds
406
+ annotate ProcessorService.Incidents with @changelog: [title] {
407
+ status @changelog: [status.code];
408
+ }
409
+ ```
410
+
411
+ ![CustomTypeChange](_assets/CustomTypeChange.png)
412
+
413
+ #### Use Case 4: Trace the changes of chained associated entities from the current entity and display the meaningful data from associated entities (association relation)
414
+
415
+ Modelling in `db/schema.cds`
416
+
417
+ ```cds
418
+ entity Incidents : cuid, managed {
419
+ ...
420
+ title : String @title: 'Title';
421
+ customer : Association to Customers;
422
+ ...
423
+ }
424
+
425
+ entity Customers : cuid, managed {
426
+ ...
427
+ addresses : Association to Addresses;
428
+ ...
429
+ }
430
+ ```
431
+
432
+ Add the following `@changelog` annotations in `srv/change-tracking.cds`
433
+
434
+ ```cds
435
+ annotate ProcessorService.Incidents with @changelog: [title] {
436
+ customer @changelog: [customer.addresses.city, customer.addresses.streetAddress];
437
+ }
438
+ ```
439
+
440
+ ![ChainedAssociationChange](_assets/ChainedAssociationChange.png)
441
+
442
+ > Change-tracking supports analyzing chained associated entities from the current entity in case the entity in consumer applications is a pure relation table. However, the usage of chained associated entities is not recommended due to performance cost.
443
+
444
+ #### Use Case 5: Trace the changes of union entity and display the meaningful data
445
+
446
+ `Payable.cds`:
447
+
448
+ ```cds
449
+ entity Payables : cuid {
450
+ displayId : String;
451
+ @changelog
452
+ name : String;
453
+ cryptoAmount : Decimal;
454
+ fiatAmount : Decimal;
455
+ };
456
+ ```
457
+
458
+ `Payment.cds`:
459
+
460
+ ```cds
461
+ entity Payments : cuid {
462
+ displayId : String; //readable ID
463
+ @changelog
464
+ name : String;
465
+ };
466
+ ```
467
+
468
+ Union entity in `BusinessTransaction.cds`:
469
+
470
+ ```cds
471
+ entity BusinessTransactions as(
472
+ select from payments.Payments{
473
+ key ID,
474
+ displayId,
475
+ name,
476
+ changes : Association to many ChangeView
477
+ on changes.objectID = ID AND changes.entity = 'payments.Payments'
478
+ }
479
+ )
480
+ union all
481
+ (
482
+ select from payables.Payables {
483
+ key ID,
484
+ displayId,
485
+ name,
486
+ changes : Association to many ChangeView
487
+ on changes.objectID = ID AND changes.entity = 'payables.Payables'
488
+ }
489
+ );
490
+ ```
491
+
492
+ ![UnionChange.png](_assets/UnionChange.png)
493
+
494
+ ### Don'ts
495
+
496
+ Don'ts
497
+
498
+ #### Use Case 1: Don't trace changes for field(s) with `Association to many`
499
+
500
+ ```cds
501
+ entity Customers : cuid, managed {
502
+ ...
503
+ incidents : Association to many Incidents on incidents.customer = $self;
504
+ }
505
+ ```
506
+
507
+ The reason is that: the relationship: `Association to many` is only for modelling purpose and there is no concrete field in database table. In the above sample, there is no column for incidents in the table Customers, but there is a navigation property of incidents in Customers OData entity metadata.
508
+
509
+ #### Use Case 2: Don't trace changes for field(s) with *Unmanaged Association*
510
+
511
+ ```cds
512
+ entity AggregatedBusinessTransactionData @(cds.autoexpose) : cuid {
513
+ FootprintInventory: Association to one FootprintInventories
514
+ on FootprintInventory.month = month
515
+ and FootprintInventory.year = year
516
+ and FootprintInventory.FootprintInventoryScope.ID = FootprintInventoryScope.ID;
517
+ ...
518
+ }
519
+ ```
520
+
521
+ The reason is that: When deploying to relational databases, Associations are mapped to foreign keys. Yet, when mapped to non-relational databases they're just references. More details could be found in [Prefer Managed Associations](https://cap.cloud.sap/docs/guides/domain-models#managed-associations). In the above sample, there is no column for FootprintInventory in the table AggregatedBusinessTransactionData, but there is a navigation property FootprintInventoryof in OData entity metadata.
522
+
523
+ #### Use Case 3: Don't trace changes for CUD on DB entity
524
+
525
+ ```cds
526
+ this.on("UpdateActivationStatus", async (req) =>
527
+ // PaymentAgreementsOutgoingDb is the DB entity
528
+ await UPDATE.entity(PaymentAgreementsOutgoingDb)
529
+ .where({ ID: paymentAgreement.ID })
530
+ .set({ ActivationStatus_code: ActivationCodes.ACTIVE });
531
+ );
532
+ ```
533
+
534
+ 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.
535
+
226
536
  ## Contributing
227
537
 
228
538
  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).
229
539
 
230
-
231
540
  ## Code of Conduct
232
541
 
233
542
  We as members, contributors, and leaders pledge to make participation in our community a harassment-free experience for everyone. By participating in this project, you agree to abide by its [Code of Conduct](CODE_OF_CONDUCT.md) at all times.
234
543
 
235
-
236
544
  ## Licensing
237
545
 
238
546
  Copyright 2023 SAP SE or an SAP affiliate company and contributors. Please see our [LICENSE](LICENSE) for copyright and license information. Detailed information including third-party components and their licensing/copyright information is available [via the REUSE tool](https://api.reuse.software/info/github.com/cap-js/change-tracking).
547
+
package/cds-plugin.js CHANGED
@@ -40,12 +40,19 @@ cds.on('loaded', m => {
40
40
  const keys = [], { elements: elms } = entity
41
41
  for (let e in elms) if (elms[e].key) keys.push(e)
42
42
 
43
+ // If no key attribute is defined for the entity, the logic to add association to ChangeView should be skipped.
44
+ if(keys.length === 0) {
45
+ continue;
46
+ }
47
+
43
48
  // Add association to ChangeView...
44
49
  const on = [...changes.on]; keys.forEach((k, i) => { i && on.push('||'); on.push({
45
50
  ref: k === 'up_' ? [k,'ID'] : [k] // REVISIT: up_ handling is a dirty hack for now
46
51
  })})
47
52
  const assoc = { ...changes, on }
48
53
  const query = entity.projection || entity.query?.SELECT
54
+ if(!entity['@changelog.disable_assoc'])
55
+ {
49
56
  if (query) {
50
57
  (query.columns ??= ['*']).push({ as: 'changes', cast: assoc })
51
58
  } else {
@@ -53,8 +60,9 @@ cds.on('loaded', m => {
53
60
  }
54
61
 
55
62
  // Add UI.Facet for Change History List
56
- entity['@UI.Facets']?.push(facet)
57
-
63
+ if(!entity['@changelog.disable_facet'])
64
+ entity['@UI.Facets']?.push(facet)
65
+ }
58
66
  // The changehistory list should be refreshed after the custom action is triggered
59
67
  if (entity.actions) {
60
68
 
package/index.cds CHANGED
@@ -40,8 +40,8 @@ entity ChangeLog : managed, cuid {
40
40
  serviceEntity : String @title: '{i18n>ChangeLog.serviceEntity}'; // definition name of target entity (on service level) - e.g. ProcessorsService.Incidents
41
41
  entity : String @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
- createdAt : managed:createdAt @title: 'On';
44
- createdBy : managed:createdBy @title: 'By';
43
+ createdAt : managed:createdAt;
44
+ createdBy : managed:createdBy;
45
45
  changes : Composition of many Changes on changes.changeLog = $self;
46
46
  }
47
47
 
package/lib/change-log.js CHANGED
@@ -97,7 +97,7 @@ const _getEntityIDs = function (txParams) {
97
97
  * ...
98
98
  * }
99
99
  */
100
- const _formatAssociationContext = async function (changes) {
100
+ const _formatAssociationContext = async function (changes, reqData) {
101
101
  for (const change of changes) {
102
102
  const a = cds.model.definitions[change.serviceEntity].elements[change.attribute]
103
103
  if (a?.type !== "cds.Association") continue
@@ -111,10 +111,10 @@ const _formatAssociationContext = async function (changes) {
111
111
  SELECT.one.from(a.target).where({ [ID]: change.valueChangedTo })
112
112
  ])
113
113
 
114
- const fromObjId = await getObjectId(a.target, semkeys, { curObjFromDbQuery: from || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
114
+ const fromObjId = await getObjectId(reqData, a.target, semkeys, { curObjFromDbQuery: from || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
115
115
  if (fromObjId) change.valueChangedFrom = fromObjId
116
116
 
117
- const toObjId = await getObjectId(a.target, semkeys, { curObjFromDbQuery: to || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
117
+ const toObjId = await getObjectId(reqData, a.target, semkeys, { curObjFromDbQuery: to || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
118
118
  if (toObjId) change.valueChangedTo = toObjId
119
119
 
120
120
  const isVLvA = a["@Common.ValueList.viaAssociation"]
@@ -219,7 +219,7 @@ const _getObjectIdByPath = async function (
219
219
  const entityUUID = getUUIDFromPathVal(nodePathVal)
220
220
  const obj = await getCurObjFromDbQuery(entityName, entityUUID)
221
221
  const curObj = { curObjFromReqData, curObjFromDbQuery: obj }
222
- return getObjectId(entityName, objIdElementNames, curObj)
222
+ return getObjectId(reqData, entityName, objIdElementNames, curObj)
223
223
  }
224
224
 
225
225
  const _formatObjectID = async function (changes, reqData) {
@@ -267,7 +267,7 @@ const _isCompositionContextPath = function (aPath) {
267
267
 
268
268
  const _formatChangeLog = async function (changes, req) {
269
269
  await _formatObjectID(changes, req.data)
270
- await _formatAssociationContext(changes)
270
+ await _formatAssociationContext(changes, req.data)
271
271
  await _formatCompositionContext(changes, req.data)
272
272
  }
273
273
 
@@ -301,8 +301,8 @@ function _trackedChanges4 (srv, target, diff) {
301
301
  entity: getDBEntity(element.parent).name,
302
302
  serviceEntity: element.parent.name,
303
303
  attribute: element["@odata.foreignKey4"] || key,
304
- valueChangedFrom: from || '',
305
- valueChangedTo: to || '',
304
+ valueChangedFrom: from?? '',
305
+ valueChangedTo: to?? '',
306
306
  valueDataType: element.type,
307
307
  modification: row._op,
308
308
  keys,
@@ -349,7 +349,7 @@ async function track_changes (req) {
349
349
  let isComposition = _isCompositionContextPath(req.context.path)
350
350
  let entityKey = diff.ID
351
351
 
352
- if (req.event === "DELETE") {
352
+ if (cds.transaction(req).context.event === "DELETE") {
353
353
  if (isDraftEnabled || !isComposition) {
354
354
  return await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey })
355
355
  }
@@ -367,7 +367,11 @@ async function track_changes (req) {
367
367
  entity: dbEntity.name,
368
368
  entityKey: entityKey,
369
369
  serviceEntity: target.name,
370
- changes: changes.filter(c => c.valueChangedFrom || c.valueChangedTo),
370
+ changes: changes.filter(c => c.valueChangedFrom || c.valueChangedTo).map((c) => ({
371
+ ...c,
372
+ valueChangedFrom: `${c.valueChangedFrom ?? ''}`,
373
+ valueChangedTo: `${c.valueChangedTo ?? ''}`,
374
+ })),
371
375
  })
372
376
  }
373
377
 
@@ -70,7 +70,7 @@ const getCurObjFromReqData = function (reqData, nodePathVal, pathVal) {
70
70
  }
71
71
 
72
72
 
73
- async function getObjectId (entityName, fields, curObj) {
73
+ async function getObjectId (reqData, entityName, fields, curObj) {
74
74
  let all = [], { curObjFromReqData: req_data={}, curObjFromDbQuery: db_data={} } = curObj
75
75
  let entity = cds.model.definitions[entityName]
76
76
  if (!fields?.length) fields = entity["@changelog"]?.map?.(k => k['='] || k) || []
@@ -81,13 +81,30 @@ async function getObjectId (entityName, fields, curObj) {
81
81
  while (path.length > 1) {
82
82
  let assoc = current.elements[path[0]]; if (!assoc?.isAssociation) break
83
83
  let foreignKey = assoc.keys?.[0]?.$generatedFieldName
84
- let IDval = req_data[foreignKey] || _db_data[foreignKey]
84
+ let IDval =
85
+ req_data[foreignKey] && current.name === entityName
86
+ ? req_data[foreignKey]
87
+ : _db_data[foreignKey]
85
88
  if (IDval) try {
86
89
  // REVISIT: This always reads all elements -> should read required ones only!
87
90
  let ID = assoc.keys?.[0]?.ref[0] || 'ID'
88
- // When parent/child nodes are created simultaneously, data is taken from draft table
89
- _db_data = await SELECT.one.from(assoc._target).where({[ID]: IDval}) ||
90
- await SELECT.one.from(`${assoc._target}.drafts`).where({[ID]: IDval}) || {}
91
+ const isComposition = hasComposition(assoc._target, current)
92
+ // Peer association and composition are distinguished by the value of isComposition.
93
+ if (isComposition) {
94
+ // This function can recursively retrieve the desired information from reqData without having to read it from db.
95
+ _db_data = _getCompositionObjFromReq(reqData, IDval)
96
+ // When multiple layers of child nodes are deleted at the same time, the deep layer of child nodes will lose the information of the upper nodes, so data needs to be extracted from the db.
97
+ if (!_db_data || JSON.stringify(_db_data) === '{}') {
98
+ _db_data =
99
+ (await SELECT.one
100
+ .from(assoc._target)
101
+ .where({ [ID]: IDval })) || {}
102
+ }
103
+ } else {
104
+ _db_data =
105
+ (await SELECT.one.from(assoc._target).where({ [ID]: IDval })) ||
106
+ {}
107
+ }
91
108
  } catch (e) {
92
109
  LOG.error("Failed to generate object Id for an association entity.", e)
93
110
  throw new Error("Failed to generate object Id for an association entity.", e)
@@ -96,7 +113,7 @@ async function getObjectId (entityName, fields, curObj) {
96
113
  path.shift()
97
114
  }
98
115
  field = path.join('_')
99
- let obj = _db_data[field] || req_data[field]
116
+ let obj = current.name === entityName && req_data[field] ? req_data[field] : _db_data[field]
100
117
  if (obj) all.push(obj)
101
118
  } else {
102
119
  let e = entity.elements[field]
@@ -134,6 +151,39 @@ const getValueEntityType = function (entityName, fields) {
134
151
  return types.join(', ')
135
152
  }
136
153
 
154
+ const hasComposition = function (parentEntity, subEntity) {
155
+ if (!parentEntity.compositions) {
156
+ return false
157
+ }
158
+
159
+ const compositions = Object.values(parentEntity.compositions);
160
+
161
+ for (const composition of compositions) {
162
+ if (composition.target === subEntity.name) {
163
+ return true;
164
+ }
165
+ }
166
+
167
+ return false
168
+ }
169
+
170
+ const _getCompositionObjFromReq = function (obj, targetID) {
171
+ if (obj.ID === targetID) {
172
+ return obj;
173
+ }
174
+
175
+ for (const key in obj) {
176
+ if (typeof obj[key] === "object" && obj[key] !== null) {
177
+ const result = _getCompositionObjFromReq(obj[key], targetID);
178
+ if (result) {
179
+ return result;
180
+ }
181
+ }
182
+ }
183
+
184
+ return null;
185
+ };
186
+
137
187
  module.exports = {
138
188
  getCurObjFromReqData,
139
189
  getCurObjFromDbQuery,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/change-tracking",
3
- "version": "1.0.4",
3
+ "version": "1.0.6",
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)",