@cap-js/change-tracking 2.0.0-beta.2 → 2.0.0-beta.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +21 -1
- package/README.md +87 -244
- package/cds-plugin.js +1 -1
- package/index.cds +326 -41
- package/lib/csn-enhancements/annotations.js +77 -0
- package/lib/{model-enhancer.js → csn-enhancements/index.js} +7 -70
- package/lib/csn-enhancements/timezoneProperties.js +81 -0
- package/lib/h2/java-codegen.js +1 -1
- package/lib/hana/register.js +36 -7
- package/lib/hana/sql-expressions.js +1 -1
- package/lib/postgres/register.js +12 -0
- package/lib/postgres/sql-expressions.js +1 -1
- package/lib/sqlite/sql-expressions.js +1 -1
- package/lib/utils/change-tracking.js +6 -2
- package/lib/utils/composition-helpers.js +4 -6
- package/lib/utils/entity-collector.js +67 -42
- package/package.json +1 -2
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const cds = require('@sap/cds');
|
|
2
|
+
const { compositeKeyExpr: compositeKeyExprSqlite } = require('../sqlite/sql-expressions');
|
|
3
|
+
const { compositeKeyExpr: compositeKeyExprHANA } = require('../hana/sql-expressions');
|
|
4
|
+
const { compositeKeyExpr: compositeKeyExprPG } = require('../postgres/sql-expressions');
|
|
5
|
+
const { isChangeTracked } = require('../utils/entity-collector');
|
|
6
|
+
|
|
7
|
+
function collectTrackedPropertiesWithTimezone(m) {
|
|
8
|
+
const timezoneProperties = [];
|
|
9
|
+
for (const name in m.definitions) {
|
|
10
|
+
const entity = m.definitions[name];
|
|
11
|
+
if (entity.kind !== 'entity' || entity.query || entity.projection || !isChangeTracked(entity)) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
for (const ele in entity.elements) {
|
|
15
|
+
const element = entity.elements[ele];
|
|
16
|
+
if (!element['@Common.Timezone'] || element._foreignKey4) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
timezoneProperties.push({
|
|
20
|
+
property: ele,
|
|
21
|
+
entity: name,
|
|
22
|
+
timezone: element['@Common.Timezone']?.['=']
|
|
23
|
+
? // Where condition is replaced when select is inserted into ChangeView
|
|
24
|
+
SELECT.from(name).alias('timezoneSubSelect').where('1 = 1').columns(element['@Common.Timezone']['='])
|
|
25
|
+
: element['@Common.Timezone']
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return timezoneProperties;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isDeploy2Check(target) {
|
|
33
|
+
try {
|
|
34
|
+
cds.build?.register(target, class ABC extends cds.build.Plugin {});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (err.message.match(/already registered/)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function enhanceChangeViewWithTimeZones(changeView, m) {
|
|
44
|
+
const compositeKeyExpr =
|
|
45
|
+
isDeploy2Check('hana') && m.meta.creator.match(/v6/)
|
|
46
|
+
? compositeKeyExprHANA
|
|
47
|
+
: isDeploy2Check('postgres') && m.meta.creator.match(/v6/)
|
|
48
|
+
? compositeKeyExprPG
|
|
49
|
+
: cds.env.requires?.db.kind === 'sqlite' && !cds.build
|
|
50
|
+
? compositeKeyExprSqlite
|
|
51
|
+
: cds.env.requires?.db.kind === 'postgres' && (!cds.build || cds.env.profiles.includes('pg'))
|
|
52
|
+
? compositeKeyExprPG
|
|
53
|
+
: compositeKeyExprHANA;
|
|
54
|
+
const timezoneProperties = collectTrackedPropertiesWithTimezone(m);
|
|
55
|
+
const timezoneColumn = changeView.query.SELECT.columns.find((c) => c.as && c.as === 'valueTimeZone');
|
|
56
|
+
if (timezoneProperties.length === 0) return;
|
|
57
|
+
delete timezoneColumn.val;
|
|
58
|
+
timezoneColumn.xpr = ['case'];
|
|
59
|
+
for (const timezoneProp of timezoneProperties) {
|
|
60
|
+
timezoneColumn.xpr.push('when', { ref: ['attribute'] }, '=', { val: timezoneProp.property }, 'and', { ref: ['entity'] }, '=', { val: timezoneProp.entity }, 'then');
|
|
61
|
+
if (timezoneProp.timezone.SELECT) {
|
|
62
|
+
const subSelect = structuredClone(timezoneProp.timezone);
|
|
63
|
+
const keys = Object.keys(m.definitions[timezoneProp.entity].elements).filter((e) => m.definitions[timezoneProp.entity].elements[e].key);
|
|
64
|
+
subSelect.SELECT.where = [
|
|
65
|
+
{ ref: ['change', 'entityKey'] },
|
|
66
|
+
'=',
|
|
67
|
+
// REVISIT: once HIERARCHY_COMPOSITE_ID is available on all DBs, use native CQN
|
|
68
|
+
compositeKeyExpr(keys)
|
|
69
|
+
];
|
|
70
|
+
timezoneColumn.xpr.push(subSelect);
|
|
71
|
+
} else {
|
|
72
|
+
timezoneColumn.xpr.push({ val: timezoneProp.timezone });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
timezoneColumn.xpr.push('else', { val: null }, 'end');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
collectTrackedPropertiesWithTimezone,
|
|
80
|
+
enhanceChangeViewWithTimeZones
|
|
81
|
+
};
|
package/lib/h2/java-codegen.js
CHANGED
|
@@ -709,7 +709,7 @@ function _generateObjectIDCalculation(objectIDs, entity, refRow, model) {
|
|
|
709
709
|
|
|
710
710
|
for (const oid of objectIDs) {
|
|
711
711
|
if (oid.included) {
|
|
712
|
-
parts.push(`SELECT CAST(? AS VARCHAR) AS val`);
|
|
712
|
+
parts.push(`SELECT COALESCE(CAST(? AS VARCHAR), '<empty>') AS val`);
|
|
713
713
|
bindings.push(`${refRow}.getString("${oid.name}")`);
|
|
714
714
|
} else {
|
|
715
715
|
// Sub-select needed (Lookup)
|
package/lib/hana/register.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
const cds = require('@sap/cds');
|
|
2
|
-
const { fs } = cds.utils;
|
|
3
2
|
|
|
4
|
-
const { prepareCSNForTriggers, generateTriggersForEntities,
|
|
3
|
+
const { prepareCSNForTriggers, generateTriggersForEntities, ensureUndeployJsonHasTriggerPattern } = require('../utils/trigger-utils.js');
|
|
4
|
+
const { getLabelTranslations } = require('../localization.js');
|
|
5
5
|
|
|
6
6
|
function registerHDICompilerHook() {
|
|
7
7
|
const _hdi_migration = cds.compiler.to.hdi.migration;
|
|
@@ -10,19 +10,48 @@ function registerHDICompilerHook() {
|
|
|
10
10
|
const { runtimeCSN, hierarchy, entities } = prepareCSNForTriggers(csn, true);
|
|
11
11
|
|
|
12
12
|
const triggers = generateTriggersForEntities(runtimeCSN, hierarchy, entities, generateHANATriggers);
|
|
13
|
-
|
|
13
|
+
const data = [];
|
|
14
14
|
if (triggers.length > 0) {
|
|
15
15
|
delete csn.definitions['sap.changelog.CHANGE_TRACKING_DUMMY']['@cds.persistence.skip'];
|
|
16
|
-
writeLabelsCSV(entities, runtimeCSN);
|
|
17
|
-
const dir = 'db/src/gen/data/';
|
|
18
|
-
fs.writeFileSync(`${dir}/sap.changelog-CHANGE_TRACKING_DUMMY.csv`, `X\n1`);
|
|
19
16
|
ensureUndeployJsonHasTriggerPattern();
|
|
17
|
+
|
|
18
|
+
const labels = getLabelTranslations(entities, runtimeCSN);
|
|
19
|
+
const header = 'ID;locale;text';
|
|
20
|
+
const escape = (v) => {
|
|
21
|
+
const s = String(v ?? '');
|
|
22
|
+
return s.includes(';') || s.includes('\n') || s.includes('"') ? `"${s.replace(/"/g, '""')}"` : s;
|
|
23
|
+
};
|
|
24
|
+
const rows = labels.map((row) => `${escape(row.ID)};${escape(row.locale)};${escape(row.text)}`);
|
|
25
|
+
const i18nContent = [header, ...rows].join('\n') + '\n';
|
|
26
|
+
|
|
27
|
+
data.push(
|
|
28
|
+
{
|
|
29
|
+
name: 'sap.changelog-CHANGE_TRACKING_DUMMY',
|
|
30
|
+
sql: 'X\n1',
|
|
31
|
+
suffix: '.csv'
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'sap.changelog-i18nKeys',
|
|
35
|
+
sql: i18nContent,
|
|
36
|
+
suffix: '.csv'
|
|
37
|
+
}
|
|
38
|
+
);
|
|
20
39
|
}
|
|
21
40
|
|
|
22
41
|
const ret = _hdi_migration(csn, options, beforeImage);
|
|
23
|
-
ret.definitions =
|
|
42
|
+
ret.definitions = ret.definitions.concat(triggers).concat(data);
|
|
24
43
|
return ret;
|
|
25
44
|
};
|
|
45
|
+
|
|
46
|
+
// REVISIT: Remove once time casting is fixed in cds-dbs
|
|
47
|
+
cds.on('serving', async () => {
|
|
48
|
+
if (cds.env.requires?.db.kind !== 'hana') return;
|
|
49
|
+
const db = await cds.connect.to('db');
|
|
50
|
+
db.before('*', () => {
|
|
51
|
+
// to_time conversion is necessary else HANA tries to convert to timestamp implicitly causing an SQL crash
|
|
52
|
+
db.class.CQN2SQL.OutputConverters.Time = (e) => `to_char(to_time(${e}), 'HH24:MI:SS')`;
|
|
53
|
+
});
|
|
54
|
+
});
|
|
26
55
|
}
|
|
27
56
|
|
|
28
57
|
module.exports = { registerHDICompilerHook };
|
|
@@ -152,7 +152,7 @@ function buildObjectIDExpr(objectIDs, entity, rowRef, model) {
|
|
|
152
152
|
const parts = [];
|
|
153
153
|
for (const oid of objectIDs) {
|
|
154
154
|
if (oid.included) {
|
|
155
|
-
parts.push(`TO_NVARCHAR(:${rowRef}.${oid.name})`);
|
|
155
|
+
parts.push(`COALESCE(TO_NVARCHAR(:${rowRef}.${oid.name}), '<empty>')`);
|
|
156
156
|
} else {
|
|
157
157
|
const where = keys.reduce((acc, k) => {
|
|
158
158
|
acc[k] = { val: `:${rowRef}.${k}` };
|
package/lib/postgres/register.js
CHANGED
|
@@ -26,6 +26,18 @@ function registerPostgresCompilerHook() {
|
|
|
26
26
|
|
|
27
27
|
return ddl;
|
|
28
28
|
});
|
|
29
|
+
|
|
30
|
+
// REVISIT: Remove once time casting is fixed in cds-dbs
|
|
31
|
+
cds.on('serving', async () => {
|
|
32
|
+
if (cds.env.requires?.db.kind !== 'postgres') return;
|
|
33
|
+
const db = await cds.connect.to('db');
|
|
34
|
+
db.before('*', () => {
|
|
35
|
+
db.class.CQN2SQL.OutputConverters.Date = (e) => `to_date(${e}::text, 'YYYY-MM-DD')`;
|
|
36
|
+
db.class.CQN2SQL.OutputConverters.Time = (e) => `to_timestamp(${e}::text, 'HH24:MI:SS')::TIME`;
|
|
37
|
+
db.class.CQN2SQL.OutputConverters.DateTime = (e) => `to_timestamp(${e}::text, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')::timestamp`;
|
|
38
|
+
db.class.CQN2SQL.OutputConverters.Timestamp = (e) => `to_timestamp(${e}::text, 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')::timestamp`;
|
|
39
|
+
});
|
|
40
|
+
});
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
async function deployPostgresLabels() {
|
|
@@ -137,7 +137,7 @@ function buildObjectIDAssignment(objectIDs, entity, keys, recVar, targetVar, mod
|
|
|
137
137
|
const parts = [];
|
|
138
138
|
for (const oid of objectIDs) {
|
|
139
139
|
if (oid.included) {
|
|
140
|
-
parts.push(
|
|
140
|
+
parts.push(`COALESCE(${recVar}.${oid.name}::TEXT, '<empty>')`);
|
|
141
141
|
} else {
|
|
142
142
|
const where = keys.reduce((acc, k) => {
|
|
143
143
|
acc[k] = { val: `${recVar}.${k}` };
|
|
@@ -148,7 +148,7 @@ function buildObjectIDSelect(objectIDs, entityName, entityKeys, refRow, model) {
|
|
|
148
148
|
objectID.selectSQL = toSQL(query, model);
|
|
149
149
|
}
|
|
150
150
|
|
|
151
|
-
const unionParts = objectIDs.map((id) => (id.included ? `SELECT ${refRow}.${id.name} AS value
|
|
151
|
+
const unionParts = objectIDs.map((id) => (id.included ? `SELECT COALESCE(${refRow}.${id.name}, '<empty>') AS value` : `SELECT (${id.selectSQL}) AS value`));
|
|
152
152
|
|
|
153
153
|
return `(SELECT GROUP_CONCAT(value, ', ') FROM (${unionParts.join('\nUNION ALL\n')}))`;
|
|
154
154
|
}
|
|
@@ -105,8 +105,12 @@ function extractTrackedColumns(entity, model = cds.context?.model ?? cds.model,
|
|
|
105
105
|
// Use override annotation if provided, otherwise use the element's own annotation
|
|
106
106
|
const changelogAnnotation = overrideAnnotations?.elementAnnotations?.[name] ?? col['@changelog'];
|
|
107
107
|
|
|
108
|
-
// Skip
|
|
109
|
-
if (
|
|
108
|
+
// Skip foreign key columns (we want the generated FKs instead)
|
|
109
|
+
if (col._foreignKey4 || col['@odata.foreignKey4']) continue;
|
|
110
|
+
|
|
111
|
+
// Auto-include composition elements, unless explicitly defined with @changelog: false
|
|
112
|
+
const isComposition = col.type === 'cds.Composition';
|
|
113
|
+
if ((!changelogAnnotation && !isComposition) || changelogAnnotation === false) continue;
|
|
110
114
|
|
|
111
115
|
// skip any PersonalData* annotation
|
|
112
116
|
const hasPersonalData = Object.keys(col).some((k) => k.startsWith('@PersonalData'));
|
|
@@ -2,8 +2,6 @@ const utils = require('./change-tracking.js');
|
|
|
2
2
|
|
|
3
3
|
/**
|
|
4
4
|
* Finds composition parent info for an entity.
|
|
5
|
-
* Checks if root entity has a @changelog annotation on a composition field pointing to this entity.
|
|
6
|
-
*
|
|
7
5
|
* Returns null if not found, or an object with:
|
|
8
6
|
* { parentEntityName, compositionFieldName, parentKeyBinding, isCompositionOfOne }
|
|
9
7
|
*/
|
|
@@ -13,9 +11,9 @@ function getCompositionParentInfo(entity, rootEntity, rootMergedAnnotations) {
|
|
|
13
11
|
for (const [elemName, elem] of Object.entries(rootEntity.elements)) {
|
|
14
12
|
if (elem.type !== 'cds.Composition' || elem.target !== entity.name) continue;
|
|
15
13
|
|
|
16
|
-
// Check if this composition has @changelog annotation
|
|
14
|
+
// Check if this composition has @changelog: false annotation
|
|
17
15
|
const changelogAnnotation = rootMergedAnnotations?.elementAnnotations?.[elemName] ?? elem['@changelog'];
|
|
18
|
-
if (
|
|
16
|
+
if (changelogAnnotation === false) continue;
|
|
19
17
|
|
|
20
18
|
// Found a tracked composition - get the FK binding from child to parent
|
|
21
19
|
const parentKeyBinding = utils.getCompositionParentBinding(entity, rootEntity);
|
|
@@ -46,12 +44,12 @@ function getCompositionParentInfo(entity, rootEntity, rootMergedAnnotations) {
|
|
|
46
44
|
function getGrandParentCompositionInfo(rootEntity, grandParentEntity, grandParentMergedAnnotations, grandParentCompositionField) {
|
|
47
45
|
if (!grandParentEntity || !grandParentCompositionField) return null;
|
|
48
46
|
|
|
49
|
-
// Check if the grandparent's composition field has @changelog annotation
|
|
47
|
+
// Check if the grandparent's composition field has @changelog: false annotation
|
|
50
48
|
const elem = grandParentEntity.elements?.[grandParentCompositionField];
|
|
51
49
|
if (!elem || elem.type !== 'cds.Composition' || elem.target !== rootEntity.name) return null;
|
|
52
50
|
|
|
53
51
|
const changelogAnnotation = grandParentMergedAnnotations?.elementAnnotations?.[grandParentCompositionField] ?? elem['@changelog'];
|
|
54
|
-
if (
|
|
52
|
+
if (changelogAnnotation === false) return null;
|
|
55
53
|
|
|
56
54
|
// Get FK binding from rootEntity to grandParentEntity
|
|
57
55
|
const grandParentKeyBinding = utils.getCompositionParentBinding(rootEntity, grandParentEntity);
|
|
@@ -8,6 +8,11 @@ function isChangeTracked(entity) {
|
|
|
8
8
|
return Object.values(entity.elements).some((e) => e['@changelog']);
|
|
9
9
|
}
|
|
10
10
|
|
|
11
|
+
function _hasTrackedElements(entity) {
|
|
12
|
+
if (!entity?.elements) return false;
|
|
13
|
+
return Object.values(entity.elements).some((e) => e['@changelog'] && e['@changelog'] !== false);
|
|
14
|
+
}
|
|
15
|
+
|
|
11
16
|
// Compares two @changelog annotation values for equality
|
|
12
17
|
function _annotationsEqual(a, b) {
|
|
13
18
|
// Handle null/undefined/false cases
|
|
@@ -97,20 +102,27 @@ function _mergeChangelogAnnotations(dbEntity, serviceEntities) {
|
|
|
97
102
|
};
|
|
98
103
|
}
|
|
99
104
|
|
|
100
|
-
function
|
|
101
|
-
const
|
|
102
|
-
const
|
|
105
|
+
function _extractServiceAnnotations(serviceEntity) {
|
|
106
|
+
const entityAnnotation = serviceEntity['@changelog'];
|
|
107
|
+
const elementAnnotations = {};
|
|
108
|
+
for (const element of serviceEntity.elements) {
|
|
109
|
+
if (element['@changelog'] !== undefined) {
|
|
110
|
+
elementAnnotations[element.name] = element['@changelog'];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return { entity: serviceEntity, entityAnnotation, elementAnnotations };
|
|
114
|
+
}
|
|
103
115
|
|
|
104
|
-
|
|
116
|
+
// Resolve collected service entities into DB entities with merged annotations
|
|
117
|
+
function _collectServiceEntities(model, collected, result, processed) {
|
|
105
118
|
for (const [dbEntityName, serviceEntityNames] of collected) {
|
|
106
|
-
|
|
119
|
+
processed.add(dbEntityName);
|
|
107
120
|
const dbEntity = model[dbEntityName];
|
|
108
121
|
if (!dbEntity) {
|
|
109
122
|
DEBUG?.(`DB entity ${dbEntityName} not found in model, skipping`);
|
|
110
123
|
continue;
|
|
111
124
|
}
|
|
112
125
|
|
|
113
|
-
// Resolve service entities and extract their annotations
|
|
114
126
|
const serviceEntities = [];
|
|
115
127
|
for (const name of serviceEntityNames) {
|
|
116
128
|
const serviceEntity = model[name];
|
|
@@ -118,21 +130,7 @@ function getEntitiesForTriggerGeneration(model, collected) {
|
|
|
118
130
|
DEBUG?.(`Service entity ${name} not found in model, skipping`);
|
|
119
131
|
continue;
|
|
120
132
|
}
|
|
121
|
-
|
|
122
|
-
// Extract @changelog annotations from the service entity
|
|
123
|
-
const entityAnnotation = serviceEntity['@changelog'];
|
|
124
|
-
const elementAnnotations = {};
|
|
125
|
-
for (const element of serviceEntity.elements) {
|
|
126
|
-
if (element['@changelog'] !== undefined) {
|
|
127
|
-
elementAnnotations[element.name] = element['@changelog'];
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
serviceEntities.push({
|
|
132
|
-
entity: serviceEntity,
|
|
133
|
-
entityAnnotation,
|
|
134
|
-
elementAnnotations
|
|
135
|
-
});
|
|
133
|
+
serviceEntities.push(_extractServiceAnnotations(serviceEntity));
|
|
136
134
|
}
|
|
137
135
|
|
|
138
136
|
try {
|
|
@@ -144,42 +142,68 @@ function getEntitiesForTriggerGeneration(model, collected) {
|
|
|
144
142
|
throw error;
|
|
145
143
|
}
|
|
146
144
|
}
|
|
145
|
+
}
|
|
147
146
|
|
|
148
|
-
|
|
147
|
+
// Include standalone DB entities that have @changelog but no service projection
|
|
148
|
+
function _collectStandaloneEntities(model, result, processed) {
|
|
149
149
|
for (const def of model) {
|
|
150
150
|
const isTableEntity = def.kind === 'entity' && !def.query && !def.projection;
|
|
151
|
-
if (!isTableEntity ||
|
|
151
|
+
if (!isTableEntity || processed.has(def.name)) continue;
|
|
152
152
|
|
|
153
153
|
if (isChangeTracked(def)) {
|
|
154
|
-
// No service entities collected, use null for mergedAnnotations (use entity's own annotations)
|
|
155
154
|
result.push({ dbEntityName: def.name, mergedAnnotations: null });
|
|
156
|
-
|
|
155
|
+
processed.add(def.name);
|
|
157
156
|
DEBUG?.(`Including DB entity ${def.name} directly (no service entities collected)`);
|
|
158
157
|
}
|
|
159
158
|
}
|
|
159
|
+
}
|
|
160
160
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
161
|
+
/**
|
|
162
|
+
* Auto-discover composition target entities up to the configured hierarchy depth
|
|
163
|
+
* Compositions are auto-tracked when the parent is tracked and field is not set to @changelog: false, and target has at least one @changelog element (or the field has an explicit @changelog)
|
|
164
|
+
*/
|
|
165
|
+
function _discoverCompositionTargets(model, result, processed) {
|
|
166
|
+
const maxDepth = cds.env.requires?.['change-tracking']?.maxDisplayHierarchyDepth ?? 3;
|
|
167
|
+
let currentEntities = [...result];
|
|
168
|
+
|
|
169
|
+
for (let depth = 1; depth < maxDepth; depth++) {
|
|
170
|
+
const newEntities = [];
|
|
164
171
|
|
|
165
|
-
for (const
|
|
166
|
-
|
|
172
|
+
for (const { dbEntityName, mergedAnnotations } of currentEntities) {
|
|
173
|
+
const dbEntity = model[dbEntityName];
|
|
174
|
+
if (!dbEntity) continue;
|
|
167
175
|
|
|
168
|
-
const
|
|
169
|
-
|
|
176
|
+
for (const element of Object.values(dbEntity.elements)) {
|
|
177
|
+
if (element.type !== 'cds.Composition' || !element.target) continue;
|
|
178
|
+
if (processed.has(element.target)) continue;
|
|
170
179
|
|
|
171
|
-
|
|
172
|
-
|
|
180
|
+
const changelogAnnotation = mergedAnnotations?.elementAnnotations?.[element.name] ?? element['@changelog'];
|
|
181
|
+
if (changelogAnnotation === false) continue;
|
|
173
182
|
|
|
174
|
-
|
|
175
|
-
|
|
183
|
+
const targetEntity = model[element.target];
|
|
184
|
+
if (!targetEntity) continue;
|
|
185
|
+
if (!changelogAnnotation && !_hasTrackedElements(targetEntity)) continue;
|
|
176
186
|
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
187
|
+
const entry = { dbEntityName: element.target, mergedAnnotations: null };
|
|
188
|
+
result.push(entry);
|
|
189
|
+
processed.add(element.target);
|
|
190
|
+
newEntities.push(entry);
|
|
191
|
+
DEBUG?.(`Including composition target ${element.target} for ${changelogAnnotation ? 'tracked' : 'auto-tracked'} composition ${element.name} on ${dbEntityName} (depth ${depth})`);
|
|
192
|
+
}
|
|
181
193
|
}
|
|
194
|
+
|
|
195
|
+
if (newEntities.length === 0) break;
|
|
196
|
+
currentEntities = newEntities;
|
|
182
197
|
}
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function getEntitiesForTriggerGeneration(model, collected) {
|
|
201
|
+
const result = [];
|
|
202
|
+
const processed = new Set();
|
|
203
|
+
|
|
204
|
+
_collectServiceEntities(model, collected, result, processed);
|
|
205
|
+
_collectStandaloneEntities(model, result, processed);
|
|
206
|
+
_discoverCompositionTargets(model, result, processed);
|
|
183
207
|
|
|
184
208
|
return result;
|
|
185
209
|
}
|
|
@@ -225,11 +249,12 @@ function analyzeCompositions(csn) {
|
|
|
225
249
|
|
|
226
250
|
// Second pass: build hierarchy with grandparent info
|
|
227
251
|
const hierarchy = new Map();
|
|
252
|
+
const maxDepth = cds.env.requires?.['change-tracking']?.maxDisplayHierarchyDepth ?? 3;
|
|
228
253
|
for (const [childName, parentInfo] of childParentMap) {
|
|
229
254
|
const { parent: parentName, compositionField } = parentInfo;
|
|
230
255
|
|
|
231
|
-
//
|
|
232
|
-
const grandParentInfo = childParentMap.get(parentName);
|
|
256
|
+
// Only include grandparent info if maxDisplayHierarchyDepth allows it (depth > 2 needed for grandparent)
|
|
257
|
+
const grandParentInfo = maxDepth > 2 ? childParentMap.get(parentName) : null;
|
|
233
258
|
|
|
234
259
|
hierarchy.set(childName, {
|
|
235
260
|
parent: parentName,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cap-js/change-tracking",
|
|
3
|
-
"version": "2.0.0-beta.
|
|
3
|
+
"version": "2.0.0-beta.4",
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public",
|
|
6
6
|
"tag": "beta"
|
|
@@ -44,7 +44,6 @@
|
|
|
44
44
|
"requires": {
|
|
45
45
|
"change-tracking": {
|
|
46
46
|
"model": "@cap-js/change-tracking",
|
|
47
|
-
"considerLocalizedValues": false,
|
|
48
47
|
"maxDisplayHierarchyDepth": 3,
|
|
49
48
|
"preserveDeletes": false
|
|
50
49
|
}
|