New Case Study:See how Anthropic automated 95% of dependency reviews with Socket.Learn More
Socket
Sign inDemoInstall
Socket

@cap-js/change-tracking

Package Overview
Dependencies
Maintainers
0
Versions
8
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@cap-js/change-tracking - npm Package Compare versions

Comparing version 1.0.6 to 1.0.7

148

cds-plugin.js
const cds = require('@sap/cds')
const isRoot = 'change-tracking-isRootEntity'
const hasParent = 'change-tracking-parentEntity'
const isChangeTracked = (entity) => (

@@ -10,2 +13,6 @@ (entity['@changelog']

const addSideEffects = (actions, flag, element) => {
if (!flag && (element === undefined || element === null)) {
return
}
for (const se of Object.values(actions)) {

@@ -27,3 +34,111 @@ const target = flag ? 'TargetProperties' : 'TargetEntities'

function setChangeTrackingIsRootEntity(entity, csn, val = true) {
if (csn.definitions?.[entity.name]) {
csn.definitions[entity.name][isRoot] = val;
}
}
function checkAndSetRootEntity(parentEntity, entity, csn) {
if (entity[isRoot] === false) {
return entity;
}
if (parentEntity) {
return compositionRoot(parentEntity, csn);
} else {
setChangeTrackingIsRootEntity(entity, csn);
return { ...csn.definitions?.[entity.name], name: entity.name };
}
}
function processEntities(m) {
for (let name in m.definitions) {
compositionRoot({...m.definitions[name], name}, m)
}
}
function compositionRoot(entity, csn) {
if (!entity || entity.kind !== 'entity') {
return;
}
const parentEntity = compositionParent(entity, csn);
return checkAndSetRootEntity(parentEntity, entity, csn);
}
function compositionParent(entity, csn) {
if (!entity || entity.kind !== 'entity') {
return;
}
const parentAssociation = compositionParentAssociation(entity, csn);
return parentAssociation ?? null;
}
function compositionParentAssociation(entity, csn) {
if (!entity || entity.kind !== 'entity') {
return;
}
const elements = entity.elements ?? {};
// Add the change-tracking-isRootEntity attribute of the child entity
processCompositionElements(entity, csn, elements);
const hasChildFlag = entity[isRoot] !== false;
const hasParentEntity = entity[hasParent];
if (hasChildFlag || !hasParentEntity) {
// Find parent association of the entity
const parentAssociation = findParentAssociation(entity, csn, elements);
if (parentAssociation) {
const parentAssociationTarget = elements[parentAssociation]?.target;
if (hasChildFlag) setChangeTrackingIsRootEntity(entity, csn, false);
return {
...csn.definitions?.[parentAssociationTarget],
name: parentAssociationTarget
};
} else return;
}
return { ...csn.definitions?.[entity.name], name: entity.name };
}
function processCompositionElements(entity, csn, elements) {
for (const name in elements) {
const element = elements[name];
const target = element?.target;
const definition = csn.definitions?.[target];
if (
element.type !== 'cds.Composition' ||
target === entity.name ||
!definition ||
definition[isRoot] === false
) {
continue;
}
setChangeTrackingIsRootEntity({ ...definition, name: target }, csn, false);
}
}
function findParentAssociation(entity, csn, elements) {
return Object.keys(elements).find((name) => {
const element = elements[name];
const target = element?.target;
if (element.type === 'cds.Association' && target !== entity.name) {
const parentDefinition = csn.definitions?.[target] ?? {};
const parentElements = parentDefinition?.elements ?? {};
return !!Object.keys(parentElements).find((parentEntityName) => {
const parentElement = parentElements?.[parentEntityName] ?? {};
if (parentElement.type === 'cds.Composition') {
const isCompositionEntity = parentElement.target === entity.name;
// add parent information in the current entity
if (isCompositionEntity) {
csn.definitions[entity.name][hasParent] = {
associationName: name,
entityName: target
};
}
return isCompositionEntity;
}
});
}
});
}
// Unfold @changelog annotations in loaded model

@@ -36,2 +151,5 @@ cds.on('loaded', m => {

changes.on.pop() // remove ID -> filled in below
// Process entities to define the relation
processEntities(m)

@@ -69,24 +187,16 @@ for (let name in m.definitions) {

}
// The changehistory list should be refreshed after the custom action is triggered
if (entity.actions) {
const hasParentInfo = entity[hasParent];
const entityName = hasParentInfo?.entityName;
const parentEntity = entityName ? m.definitions[entityName] : null;
// Update the changehistory list on the current entity when the custom action of the entity is triggered
if (entity['@UI.Facets']) {
addSideEffects(entity.actions, true)
}
const isParentRootAndHasFacets = parentEntity?.[isRoot] && parentEntity?.['@UI.Facets'];
// When the custom action of the child entity is performed, the change history list of the parent entity is updated
if (entity.elements) {
//ToDo: Revisit Breaklook with node.js Expert
breakLoop: for (const [ele, eleValue] of Object.entries(entity.elements)) {
const parentEntity = m.definitions[eleValue.target]
if (parentEntity && parentEntity['@UI.Facets'] && eleValue.type === 'cds.Association') {
for (const value of Object.values(parentEntity.elements)) {
if (value.target === name) {
addSideEffects(entity.actions, false, ele)
break breakLoop
}
}
}
}
if (entity[isRoot] && entity['@UI.Facets']) {
// Add side effects for root entity
addSideEffects(entity.actions, true);
} else if (isParentRootAndHasFacets) {
// Add side effects for child entity
addSideEffects(entity.actions, false, hasParentInfo?.associationName);
}

@@ -93,0 +203,0 @@ }

@@ -7,6 +7,32 @@ # Change Log

## Version 1.0.6 - TBD
## Version 1.0.7 - 20.08.24
### Added
- A global switch to preserve change logs for deleted data
- For hierarchical entities, a method to determine their structure and a flag to indicate whether it is a root entity was introduced. For child entities, information about the parent is recorded.
### Fixed
- CDS 8 does not support queries for draft-enabled entities on the application service anymore. This was causing: SqliteError: NOT NULL constraint failed: (...).DraftAdministrativeData_DraftUUID
- CDS 8 deprecated cds.transaction, causing change logs of nested documents to be wrong, replaced with req.event
- CDS 8 rejects all direct CRUD requests for auto-exposed Compositions in non-draft cases. This was affecting test cases, since the ChangeView falls into this category
- req._params and req.context are not official APIs and stopped working with CDS 8, replaced with official APIs
- When running test cases in CDS 8, some requests failed with a status code of 404
- ServiceEntity is not captured in the ChangeLog table in some cases
- When modeling an inline entity, a non-existent association and parent ID was recorded
- Fixed handling, when reqData was undefined
### Changed
- Peer dependency to @sap/cds changed to ">=7"
- Data marked as personal data using data privacy annotations won't get change-tracked anymore to satisfy product standards
- Restructured Documentation
## Version 1.0.6 - 29.04.24
### Fixed
- Storage of wrong ObjectID in some special scenarios

@@ -13,0 +39,0 @@ - Missing localization of managed fields

@@ -18,2 +18,3 @@ const cds = require("@sap/cds")

const { localizeLogFields } = require("./localization")
const isRoot = "change-tracking-isRootEntity"

@@ -30,2 +31,3 @@

serviceEntityPathVals.push(curEntityPathVal)
txContext.hasComp && entityIDs.pop();
} else {

@@ -37,3 +39,3 @@ // When deleting Composition of one node via REST API in draft-disabled mode,

}
const curEntity = getEntityByContextPath(path)
const curEntity = getEntityByContextPath(path, txContext.hasComp)
const curEntityID = entityIDs.pop()

@@ -45,4 +47,4 @@ const curEntityPathVal = `${curEntity.name}(${curEntityID})`

while (_isCompositionContextPath(path)) {
const hostEntity = getEntityByContextPath(path = path.slice(0, -1))
while (_isCompositionContextPath(path, txContext.hasComp)) {
const hostEntity = getEntityByContextPath(path = path.slice(0, -1), txContext.hasComp)
const hostEntityID = entityIDs.pop()

@@ -62,3 +64,3 @@ const hostEntityPathVal = `${hostEntity.name}(${hostEntityID})`

for (let idx = 0; idx < paths.length; idx++) {
const entity = getEntityByContextPath(paths.slice(0, idx + 1))
const entity = getEntityByContextPath(paths.slice(0, idx + 1), txContext.hasComp)
const entityID = entityIDs[idx]

@@ -72,2 +74,24 @@ const entityPathVal = `${entity.name}(${entityID})`

function convertSubjectToParams(subject) {
let params = [];
let subjectRef = [];
subject?.ref?.forEach((item)=>{
if (typeof item === 'string') {
subjectRef.push(item)
return
}
const keys = {}
let id = item.id
if (!id) return
for (let j = 0; j < item?.where?.length; j = j + 4) {
const key = item.where[j].ref[0]
const value = item.where[j + 2].val
if (key !== 'IsActiveEntity') keys[key] = value
}
params.push(keys);
})
return params.length > 0 ? params : subjectRef;
}
const _getEntityIDs = function (txParams) {

@@ -264,8 +288,8 @@ const entityIDs = []

const _isCompositionContextPath = function (aPath) {
const _isCompositionContextPath = function (aPath, hasComp) {
if (!aPath) return
if (typeof aPath === 'string') aPath = aPath.split('/')
if (aPath.length < 2) return false
const target = getEntityByContextPath(aPath)
const parent = getEntityByContextPath(aPath.slice(0, -1))
const target = getEntityByContextPath(aPath, hasComp)
const parent = getEntityByContextPath(aPath.slice(0, -1), hasComp)
if (!parent.compositions) return false

@@ -299,6 +323,22 @@ return Object.values(parent.compositions).some(c => c._target === target)

const to = row[key]
const eleParentKeys = element.parent.keys
if (from === to) return
const keys = Object.keys(element.parent.keys)
/**
*
* For the Inline entity such as Items,
* further filtering is required on the keys
* within the 'association' and 'foreign key' to ultimately retain the keys of the entity itself.
* entity Order : cuid {
* title : String;
* Items : Composition of many {
* key ID : UUID;
* quantity : Integer;
* }
* }
*/
const keys = Object.keys(eleParentKeys)
.filter(k => k !== "IsActiveEntity")
.filter(k => eleParentKeys[k]?.type !== "cds.Association") // Skip association
.filter(k => !eleParentKeys[k]?.["@odata.foreignKey4"]) // Skip foreign key
.map(k => `${k}=${row[k]}`)

@@ -350,3 +390,65 @@ .join(', ')

async function generatePathAndParams (req, entityKey) {
const { target, data } = req;
const { ID, foreignKey, parentEntity } = getAssociationDetails(target);
const hasParentAndForeignKey = parentEntity && data[foreignKey];
const targetEntity = hasParentAndForeignKey ? parentEntity : target;
const targetKey = hasParentAndForeignKey ? data[foreignKey] : entityKey;
let compContext = {
path: hasParentAndForeignKey
? `${parentEntity.name}/${target.name}`
: `${target.name}`,
params: hasParentAndForeignKey
? [{ [ID]: data[foreignKey] }, { [ID]: entityKey }]
: [{ [ID]: entityKey }],
hasComp: true
};
if (hasParentAndForeignKey && parentEntity[isRoot]) {
return compContext;
}
let parentAssoc = await processEntity(targetEntity, targetKey, compContext);
while (parentAssoc && !parentAssoc.entity[isRoot]) {
parentAssoc = await processEntity(
parentAssoc.entity,
parentAssoc.ID,
compContext
);
}
return compContext;
}
async function processEntity (entity, entityKey, compContext) {
const { ID, foreignKey, parentEntity } = getAssociationDetails(entity);
if (foreignKey && parentEntity) {
const parentResult =
(await SELECT.one
.from(entity.name)
.where({ [ID]: entityKey })
.columns(foreignKey)) || {};
const hasForeignKey = parentResult[foreignKey];
if (!hasForeignKey) return;
compContext.path = `${parentEntity.name}/${compContext.path}`;
compContext.params.unshift({ [ID]: parentResult[foreignKey] });
return {
entity: parentEntity,
[ID]: hasForeignKey ? parentResult[foreignKey] : undefined
};
}
}
function getAssociationDetails (entity) {
if (!entity) return {};
const assocName = entity['change-tracking-parentEntity']?.associationName;
const assoc = entity.elements[assocName];
const parentEntity = assoc?._target;
const foreignKey = assoc?.keys?.[0]?.$generatedFieldName;
const ID = assoc?.keys?.[0]?.ref[0] || 'ID';
return { ID, foreignKey, parentEntity };
}
async function track_changes (req) {

@@ -357,11 +459,19 @@ let diff = await req.diff()

let target = req.target
let isDraftEnabled = !!target.drafts
let isComposition = _isCompositionContextPath(req.context.path)
let compContext = null;
let entityKey = diff.ID
if (cds.transaction(req).context.event === "DELETE") {
if (isDraftEnabled || !isComposition) {
return await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey })
}
const params = convertSubjectToParams(req.subject);
if (req.subject.ref.length === 1 && params.length === 1 && !target[isRoot]) {
compContext = await generatePathAndParams(req, entityKey);
}
let isComposition = _isCompositionContextPath(
compContext?.path || req.path,
compContext?.hasComp
);
if (
req.event === "DELETE" &&
target[isRoot] &&
!cds.env.requires["change-tracking"]?.preserveDeletes
) {
return await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey });
}

@@ -373,3 +483,12 @@ let changes = _trackedChanges4(this, target, diff)

if (isComposition) {
[ target, entityKey ] = await _prepareChangeLogForComposition(target, entityKey, changes, this)
let reqInfo = {
data: req.data,
context: {
path: compContext?.path || req.path,
params: compContext?.params || params,
event: req.event,
hasComp: compContext?.hasComp
}
};
[ target, entityKey ] = await _prepareChangeLogForComposition(target, entityKey, changes, reqInfo)
}

@@ -380,3 +499,3 @@ const dbEntity = getDBEntity(target)

entityKey: entityKey,
serviceEntity: target.name,
serviceEntity: target.name || target,
changes: changes.filter(c => c.valueChangedFrom || c.valueChangedTo).map((c) => ({

@@ -383,0 +502,0 @@ ...c,

21

lib/entity-helper.js

@@ -14,3 +14,4 @@ const cds = require("@sap/cds")

const getEntityByContextPath = function (aPath) {
const getEntityByContextPath = function (aPath, hasComp = false) {
if (hasComp) return cds.model.definitions[aPath[aPath.length - 1]]
let entity = cds.model.definitions[aPath[0]]

@@ -89,3 +90,5 @@ for (let each of aPath.slice(1)) {

: _db_data[foreignKey]
if (IDval) try {
if (!IDval) {
_db_data = {};
} else try {
// REVISIT: This always reads all elements -> should read required ones only!

@@ -99,12 +102,8 @@ let ID = assoc.keys?.[0]?.ref[0] || 'ID'

// 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.
if (!_db_data || JSON.stringify(_db_data) === '{}') {
_db_data =
(await SELECT.one
.from(assoc._target)
.where({ [ID]: IDval })) || {}
const entityKeys = reqData ? Object.keys(reqData).filter(item => !Object.keys(assoc._target.keys).some(ele => item === ele)) : [];
if (!_db_data || JSON.stringify(_db_data) === '{}' || entityKeys.length === 0) {
_db_data = await getCurObjFromDbQuery(assoc._target, IDval, ID);
}
} else {
_db_data =
(await SELECT.one.from(assoc._target).where({ [ID]: IDval })) ||
{}
_db_data = await getCurObjFromDbQuery(assoc._target, IDval, ID);
}

@@ -174,3 +173,3 @@ } catch (e) {

const _getCompositionObjFromReq = function (obj, targetID) {
if (obj.ID === targetID) {
if (obj?.ID === targetID) {
return obj;

@@ -177,0 +176,0 @@ }

@@ -15,3 +15,5 @@ // Enhanced class based on cds v5.5.5 @sap/cds/libx/_runtime/common/utils/templateProcessor

if (plain) {
// do not change-track personal data
const isPersonalData = element && Object.keys(element).some(key => key.startsWith('@PersonalData'));
if (plain && !isPersonalData) {
/**

@@ -18,0 +20,0 @@ * @type import('../../types/api').templateProcessorProcessFnArgs

{
"name": "@cap-js/change-tracking",
"version": "1.0.6",
"version": "1.0.7",
"description": "CDS plugin providing out-of-the box support for automatic capturing, storing, and viewing of the change records of modeled entities.",

@@ -21,3 +21,3 @@ "repository": "cap-js/change-tracking",

"peerDependencies": {
"@sap/cds": "^7"
"@sap/cds": ">=7"
},

@@ -24,0 +24,0 @@ "devDependencies": {

# Change Tracking Plugin for SAP Cloud Application Programming Model (CAP)
a [CDS plugin](https://cap.cloud.sap/docs/node.js/cds-plugins#cds-plugin-packages) for automatic capturing, storing, and viewing of the change records of modeled entities
[![REUSE status](https://api.reuse.software/badge/github.com/cap-js/change-tracking)](https://api.reuse.software/info/github.com/cap-js/change-tracking)
The `@cap-js/change-tracking` package is a [CDS plugin](https://cap.cloud.sap/docs/node.js/cds-plugins#cds-plugin-packages) providing out-of-the box support for automatic capturing, storing, and viewing of the change records of modeled entities as simple as that:
> [!IMPORTANT]
> This release establishes compatibility with CDS 8.
>
> 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.
>
> See the changelog for a full list of changes
1. [Install the plugin: `npm add @cap-js/change-tracking`](#setup)
2. [Add `@changelog` annotations to your CDS models](#annotations)
3. [Et voilà:](#change-history-view)
<img width="1300" alt="change-history-loading" src="_assets/change-history.gif">
### Table of Contents
- [Change Tracking Plugin for SAP Cloud Application Programming Model (CAP)](#change-tracking-plugin-for-sap-cloud-application-programming-model-cap)
- [Table of Contents](#table-of-contents)
- [Preliminaries](#preliminaries)
- [Setup](#setup)
- [Annotations](#annotations)
- [Human-readable Types and Fields](#human-readable-types-and-fields)
- [Human-readable IDs](#human-readable-ids)
- [Human-readable Values](#human-readable-values)
- [Test-drive locally](#test-drive-locally)
- [Change History View](#change-history-view)
- [Customizations](#customizations)
- [Altered table view](#altered-table-view)
- [Disable lazy loading](#disable-lazy-loading)
- [Disable UI Facet generation](#disable-ui-facet-generation)
- [Try it Locally](#try-it-locally)
- [Detailed Explanation](#detailed-explanation)
- [Human-readable Types and Fields](#human-readable-types-and-fields)
- [Human-readable IDs](#human-readable-ids)
- [Human-readable Values](#human-readable-values)
- [Advanced Options](#advanced-options)
- [Altered Table View](#altered-table-view)
- [Disable Lazy Loading](#disable-lazy-loading)
- [Disable UI Facet generation](#disable-ui-facet-generation)
- [Disable Association to Changes Generation](#disable-association-to-changes-generation)
- [Modelling Samples](#modelling-samples)
- [Specify Object ID](#specify-object-id)
- [Tracing Changes](#tracing-changes)
- [Don'ts](#donts)
- [Contributing](#contributing)
- [Code of Conduct](#code-of-conduct)
- [Licensing](#licensing)
- [Examples](#examples)
- [Specify Object ID](#specify-object-id)
- [Tracing Changes](#tracing-changes)
- [Don&#39;ts](#donts)
- [Contributing](#contributing)
- [Code of Conduct](#code-of-conduct)
- [Licensing](#licensing)
## Preliminaries
## Try it Locally
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:
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.
1. [Prerequisites](#1-prerequisites)
2. [Setup](#2-setup)
3. [Annotations](#3-annotations)
4. [Testing](#4-testing)
### 1. Prerequisites
Clone the repository and apply the step-by-step instructions:
```sh

@@ -60,3 +66,3 @@ git clone https://github.com/cap-js/incidents-app

## Setup
### 2. Setup

@@ -69,6 +75,6 @@ To enable change tracking, simply add this self-configuring plugin package to your project:

## Annotations
### 3. Annotations
> [!WARNING]
> Please be aware that [**sensitive** or **personal** data](https://cap.cloud.sap/docs/guides/data-privacy/annotations#annotating-personal-data) should not be change tracked, since viewing the log allows users to circumvent [audit-logging](https://cap.cloud.sap/docs/guides/data-privacy/audit-logging#setup).
> Please be aware that [**sensitive** or **personal** data](https://cap.cloud.sap/docs/guides/data-privacy/annotations#annotating-personal-data) (annotated with `@PersonalData`) is not change tracked, since viewing the log allows users to circumvent [audit-logging](https://cap.cloud.sap/docs/guides/data-privacy/audit-logging#setup).

@@ -95,5 +101,29 @@ All we need to do is to identify what should be change-tracked by annotating respective entities and elements in our model with the `@changelog` annotation. Following the [best practice of separation of concerns](https://cap.cloud.sap/docs/guides/domain-modeling#separation-of-concerns), we do so in a separate file _srv/change-tracking.cds_:

### 4. Testing
With the steps above, we have successfully set up change tracking for our reference application. Let's see that in action.
1. **Start the server**:
```sh
cds watch
```
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.
#### Change History View
> [!IMPORTANT]
> To ensure proper lazy loading of the Change History table, please use **SAPUI5 version 1.120.0** or higher.`<br>`
> If you wish to *disable* this feature, please see the customization section on how to [disable lazy loading](#disable-lazy-loading).
<img width="1300" alt="change-history" src="_assets/changes.png">
If you have a Fiori Element application, the CDS plugin automatically provides and generates a view `sap.changelog.ChangeView`, the facet of which is automatically added to the Fiori Object Page of your change-tracked entities/elements. In the UI, this corresponds to the *Change History* table which serves to help you to view and search the stored change records of your modeled entities.
## Detailed Explanation
### Human-readable Types and Fields
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.
By default the implementation looks up *Object Type* names or *Field* names from 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.

@@ -122,6 +152,2 @@ For example, without the `@title` annotation, changes to conversation entries would show up with the technical entity name:

```cds
annotate ProcessorService.Conversations {
```
<img width="1300" alt="change-history-id" src="_assets/changes-id-wbox.png">

@@ -161,26 +187,4 @@

## Test-drive locally
## Advanced Options
With the steps above, we have successfully set up change tracking for our reference application. Let's see that in action.
1. **Start the server**:
```sh
cds watch
```
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.
## Change History View
> [!IMPORTANT]
> To ensure proper lazy loading of the Change History table, please use **SAPUI5 version 1.120.0** or higher.<br>
> If you wish to *disable* this feature, please see the customization section on how to [disable lazy loading](#disable-lazy-loading).
<img width="1300" alt="change-history" src="_assets/changes.png">
If you have a Fiori Element application, the CDS plugin automatically provides and generates a view `sap.changelog.ChangeView`, the facet of which is automatically added to the Fiori Object Page of your change-tracked entities/elements. In the UI, this corresponds to the *Change History* table which serves to help you to view and search the stored change records of your modeled entities.
## Customizations
### Altered table view

@@ -239,6 +243,43 @@

## Modelling Samples
### Preserve change logs of deleted data
This chapter describes more modelling cases for further reference, from simple to complex, including but not limited to the followings.
By default, deleting a record will also automatically delete all associated change logs. This helps reduce the impact on the size of the database.
You can turn this behavior off globally by adding the following switch to the `package.json` of your project
```
...
"cds": {
"requires": {
...
"change-tracking": {
"preserveDeletes": true
}
...
}
}
...
```
> [!IMPORTANT]
> Preserving the change logs of deleted data can have a significant impact on the size of the change logging table, since now such data also survives automated data retention runs.
> You must implement an own **data retention strategy** for the change logging table in order to manage the size and performance of your database.
## Examples
This section describes modelling cases for further reference, from simple to complex, including the following:
- [Specify Object ID](#specify-object-id)
- [Use Case 1: Annotate single field/multiple fields of associated table(s) as the Object ID](#use-case-1-annotate-single-fieldmultiple-fields-of-associated-tables-as-the-object-id)
- [Use Case 2: Annotate single field/multiple fields of project customized types as the Object ID](#use-case-2-annotate-single-fieldmultiple-fields-of-project-customized-types-as-the-object-id)
- [Use Case 3: Annotate chained associated entities from the current entity as the Object ID](#use-case-3-annotate-chained-associated-entities-from-the-current-entity-as-the-object-id)
- [Tracing Changes](#tracing-changes)
- [Use Case 1: Trace the changes of child nodes from the current entity and display the meaningful data from child nodes (composition relation)](#use-case-1-trace-the-changes-of-child-nodes-from-the-current-entity-and-display-the-meaningful-data-from-child-nodes-composition-relation)
- [Use Case 2: Trace the changes of associated entities from the current entity and display the meaningful data from associated entities (association relation)](#use-case-2-trace-the-changes-of-associated-entities-from-the-current-entity-and-display-the-meaningful-data-from-associated-entities-association-relation)
- [Use Case 3: Trace the changes of fields defined by project customized types and display the meaningful data](#use-case-3-trace-the-changes-of-fields-defined-by-project-customized-types-and-display-the-meaningful-data)
- [Use Case 4: Trace the changes of chained associated entities from the current entity and display the meaningful data from associated entities (association relation)](#use-case-4-trace-the-changes-of-chained-associated-entities-from-the-current-entity-and-display-the-meaningful-data-from-associated-entities-association-relation)
- [Use Case 5: Trace the changes of union entity and display the meaningful data](#use-case-5-trace-the-changes-of-union-entity-and-display-the-meaningful-data)
- [Don&#39;ts](#donts)
- [Use Case 1: Don&#39;t trace changes for field(s) with `Association to many`](#use-case-1-dont-trace-changes-for-fields-with-association-to-many)
- [Use Case 2: Don&#39;t trace changes for field(s) with *Unmanaged Association*](#use-case-2-dont-trace-changes-for-fields-with-unmanaged-association)
- [Use Case 3: Don&#39;t trace changes for CUD on DB entity](#use-case-3-dont-trace-changes-for-cud-on-db-entity)
### Specify Object ID

@@ -553,2 +594,1 @@

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).
SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc