@cap-js/change-tracking 1.1.4 → 2.0.0-beta.2

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.
Files changed (70) hide show
  1. package/CHANGELOG.md +40 -1
  2. package/README.md +45 -71
  3. package/_i18n/i18n.properties +10 -22
  4. package/_i18n/i18n_ar.properties +3 -3
  5. package/_i18n/i18n_bg.properties +3 -3
  6. package/_i18n/i18n_cs.properties +3 -3
  7. package/_i18n/i18n_da.properties +3 -3
  8. package/_i18n/i18n_de.properties +3 -3
  9. package/_i18n/i18n_el.properties +3 -3
  10. package/_i18n/i18n_en.properties +3 -3
  11. package/_i18n/i18n_en_US_saptrc.properties +3 -32
  12. package/_i18n/i18n_es.properties +3 -3
  13. package/_i18n/i18n_es_MX.properties +3 -3
  14. package/_i18n/i18n_fi.properties +3 -3
  15. package/_i18n/i18n_fr.properties +3 -3
  16. package/_i18n/i18n_he.properties +3 -3
  17. package/_i18n/i18n_hr.properties +3 -3
  18. package/_i18n/i18n_hu.properties +3 -3
  19. package/_i18n/i18n_it.properties +3 -3
  20. package/_i18n/i18n_ja.properties +3 -3
  21. package/_i18n/i18n_kk.properties +3 -3
  22. package/_i18n/i18n_ko.properties +3 -3
  23. package/_i18n/i18n_ms.properties +3 -3
  24. package/_i18n/i18n_nl.properties +3 -3
  25. package/_i18n/i18n_no.properties +3 -3
  26. package/_i18n/i18n_pl.properties +3 -3
  27. package/_i18n/i18n_pt.properties +3 -3
  28. package/_i18n/i18n_ro.properties +3 -3
  29. package/_i18n/i18n_ru.properties +3 -3
  30. package/_i18n/i18n_sh.properties +3 -3
  31. package/_i18n/i18n_sk.properties +3 -3
  32. package/_i18n/i18n_sl.properties +3 -3
  33. package/_i18n/i18n_sv.properties +3 -3
  34. package/_i18n/i18n_th.properties +3 -3
  35. package/_i18n/i18n_tr.properties +3 -3
  36. package/_i18n/i18n_uk.properties +3 -3
  37. package/_i18n/i18n_vi.properties +3 -3
  38. package/_i18n/i18n_zh_CN.properties +3 -3
  39. package/_i18n/i18n_zh_TW.properties +3 -3
  40. package/cds-plugin.js +16 -263
  41. package/index.cds +187 -76
  42. package/lib/TriggerCQN2SQL.js +42 -0
  43. package/lib/h2/java-codegen.js +833 -0
  44. package/lib/h2/register.js +27 -0
  45. package/lib/h2/triggers.js +41 -0
  46. package/lib/hana/composition.js +248 -0
  47. package/lib/hana/register.js +28 -0
  48. package/lib/hana/sql-expressions.js +213 -0
  49. package/lib/hana/triggers.js +253 -0
  50. package/lib/localization.js +53 -117
  51. package/lib/model-enhancer.js +266 -0
  52. package/lib/postgres/composition.js +190 -0
  53. package/lib/postgres/register.js +44 -0
  54. package/lib/postgres/sql-expressions.js +261 -0
  55. package/lib/postgres/triggers.js +113 -0
  56. package/lib/skipHandlers.js +34 -0
  57. package/lib/sqlite/composition.js +234 -0
  58. package/lib/sqlite/register.js +28 -0
  59. package/lib/sqlite/sql-expressions.js +228 -0
  60. package/lib/sqlite/triggers.js +163 -0
  61. package/lib/utils/change-tracking.js +394 -0
  62. package/lib/utils/composition-helpers.js +67 -0
  63. package/lib/utils/entity-collector.js +297 -0
  64. package/lib/utils/session-variables.js +276 -0
  65. package/lib/utils/trigger-utils.js +94 -0
  66. package/package.json +17 -7
  67. package/lib/change-log.js +0 -538
  68. package/lib/entity-helper.js +0 -217
  69. package/lib/format-options.js +0 -66
  70. package/lib/template-processor.js +0 -115
@@ -0,0 +1,228 @@
1
+ const utils = require('../utils/change-tracking.js');
2
+ const { CT_SKIP_VAR, getEntitySkipVarName, getElementSkipVarName } = require('../utils/session-variables.js');
3
+ const { createTriggerCQN2SQL } = require('../TriggerCQN2SQL.js');
4
+
5
+ const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
6
+ const _cqn2sqlCache = new WeakMap();
7
+
8
+ function toSQL(query, model) {
9
+ let cqn2sql = _cqn2sqlCache.get(model);
10
+ if (!cqn2sql) {
11
+ const SQLiteService = require('@cap-js/sqlite');
12
+ const TriggerCQN2SQL = createTriggerCQN2SQL(SQLiteService.CQN2SQL);
13
+ cqn2sql = new TriggerCQN2SQL({ model: model });
14
+ _cqn2sqlCache.set(model, cqn2sql);
15
+ }
16
+ const sqlCQN = cqn4sql(query, model);
17
+ return cqn2sql.SELECT(sqlCQN);
18
+ }
19
+
20
+ /**
21
+ * Builds WHERE clause for CQN query from entity keys
22
+ * Maps each key to a trigger row reference (e.g., new.ID, old.name)
23
+ */
24
+ function buildKeyWhere(keys, refRow) {
25
+ return keys.reduce((acc, k) => {
26
+ acc[k] = { val: `${refRow}.${k}` };
27
+ return acc;
28
+ }, {});
29
+ }
30
+
31
+ function getSkipCheckCondition(entityName) {
32
+ const entitySkipVar = getEntitySkipVarName(entityName);
33
+ return `(COALESCE(session_context('${CT_SKIP_VAR}'), 'false') != 'true' AND COALESCE(session_context('${entitySkipVar}'), 'false') != 'true')`;
34
+ }
35
+
36
+ function getElementSkipCondition(entityName, elementName) {
37
+ const varName = getElementSkipVarName(entityName, elementName);
38
+ return `COALESCE(session_context('${varName}'), 'false') != 'true'`;
39
+ }
40
+
41
+ function compositeKeyExpr(parts) {
42
+ if (parts.length <= 1) return parts[0];
43
+ return parts.map((p) => `LENGTH(CAST(${p} AS TEXT)) || ',' || CAST(${p} AS TEXT)`).join(" || ';' || ");
44
+ }
45
+
46
+ /**
47
+ * Truncates large strings in SQL: CASE WHEN LENGTH(val) > 5000 THEN SUBSTR(val, 1, 4997) || '...' ELSE val END
48
+ */
49
+ function wrapLargeString(val) {
50
+ return val === 'NULL' ? 'NULL' : `CASE WHEN LENGTH(${val}) > 5000 THEN SUBSTR(${val}, 1, 4997) || '...' ELSE ${val} END`;
51
+ }
52
+
53
+ /**
54
+ * Returns SQL expression for a column's raw value
55
+ */
56
+ function getValueExpr(col, refRow) {
57
+ if (col.type === 'cds.Boolean') {
58
+ return `CASE ${refRow}.${col.name} WHEN 0 THEN 'false' WHEN 1 THEN 'true' ELSE NULL END`;
59
+ }
60
+ if (col.target && col.foreignKeys?.length) {
61
+ return col.foreignKeys.map((fk) => `${refRow}.${col.name}_${fk}`).join(" || '||' || ");
62
+ }
63
+ if (col.target && col.on?.length) {
64
+ return col.on.map((m) => `${refRow}.${m.foreignKeyField}`).join(" || '||' || ");
65
+ }
66
+ let raw = `${refRow}.${col.name}`;
67
+ if (col.type === 'cds.String' || col.type === 'cds.LargeString') raw = wrapLargeString(raw);
68
+ return raw;
69
+ }
70
+
71
+ /**
72
+ * Returns SQL WHERE condition for detecting column changes
73
+ */
74
+ function getWhereCondition(col, modification) {
75
+ if (modification === 'update') {
76
+ if (col.target && col.foreignKeys?.length) {
77
+ return col.foreignKeys.map((fk) => `old.${col.name}_${fk} IS NOT new.${col.name}_${fk}`).join(' OR ');
78
+ }
79
+ if (col.target && col.on?.length) {
80
+ return col.on.map((m) => `old.${m.foreignKeyField} IS NOT new.${m.foreignKeyField}`).join(' OR ');
81
+ }
82
+ return `old.${col.name} IS NOT new.${col.name}`;
83
+ }
84
+ // CREATE or DELETE: check value is not null
85
+ const rowRef = modification === 'create' ? 'new' : 'old';
86
+ if (col.target && col.foreignKeys?.length) {
87
+ return col.foreignKeys.map((fk) => `${rowRef}.${col.name}_${fk} IS NOT NULL`).join(' OR ');
88
+ }
89
+ if (col.target && col.on?.length) {
90
+ return col.on.map((m) => `${rowRef}.${m.foreignKeyField} IS NOT NULL`).join(' OR ');
91
+ }
92
+ return `${rowRef}.${col.name} IS NOT NULL`;
93
+ }
94
+
95
+ /**
96
+ * Builds scalar subselect for association label lookup with locale awareness
97
+ */
98
+ function buildAssocLookup(column, refRow, entityKey, model) {
99
+ const where = column.foreignKeys
100
+ ? column.foreignKeys.reduce((acc, k) => {
101
+ acc[k] = { val: `${refRow}.${column.name}_${k}` };
102
+ return acc;
103
+ }, {})
104
+ : column.on?.reduce((acc, k) => {
105
+ // Composition of aspect has a targetKey object
106
+ acc[k.targetKey ?? k] = { val: entityKey };
107
+ return acc;
108
+ }, {});
109
+
110
+ // Drop the first part of column.alt (association name)
111
+ const alt = column.alt.map((s) => s.split('.').slice(1).join('.'));
112
+ const columns = alt.length === 1 ? alt[0] : utils.buildConcatXpr(alt);
113
+
114
+ // Check for localization
115
+ const localizedInfo = utils.getLocalizedLookupInfo(column.target, column.alt, model);
116
+ if (localizedInfo) {
117
+ const textsWhere = { ...where, locale: { func: 'session_context', args: [{ val: '$user.locale' }] } };
118
+ const textsQuery = SELECT.one.from(localizedInfo.textsEntity).columns(columns).where(textsWhere);
119
+ const baseQuery = SELECT.one.from(column.target).columns(columns).where(where);
120
+ return `(SELECT COALESCE((${toSQL(textsQuery, model)}), (${toSQL(baseQuery, model)})))`;
121
+ }
122
+
123
+ const query = SELECT.one.from(column.target).columns(columns).where(where);
124
+ return `(${toSQL(query, model)})`;
125
+ }
126
+
127
+ /**
128
+ * Returns SQL expression for a column's label (looked-up value for associations)
129
+ */
130
+ function getLabelExpr(col, refRow, entityKey, model) {
131
+ if (col.target && col.alt) {
132
+ return buildAssocLookup(col, refRow, entityKey, model);
133
+ }
134
+ return 'NULL';
135
+ }
136
+
137
+ /**
138
+ * Builds SQL expression for objectID (entity display name)
139
+ * Uses @changelog annotation fields, falling back to entity keys
140
+ */
141
+ function buildObjectIDSelect(objectIDs, entityName, entityKeys, refRow, model) {
142
+ if (objectIDs.length === 0) return null;
143
+
144
+ for (const objectID of objectIDs) {
145
+ if (objectID.included) continue;
146
+ const where = buildKeyWhere(entityKeys, refRow);
147
+ const query = SELECT.one.from(entityName).columns(objectID.name).where(where);
148
+ objectID.selectSQL = toSQL(query, model);
149
+ }
150
+
151
+ const unionParts = objectIDs.map((id) => (id.included ? `SELECT ${refRow}.${id.name} AS value WHERE ${refRow}.${id.name} IS NOT NULL` : `SELECT (${id.selectSQL}) AS value`));
152
+
153
+ return `(SELECT GROUP_CONCAT(value, ', ') FROM (${unionParts.join('\nUNION ALL\n')}))`;
154
+ }
155
+
156
+ function buildTriggerContext(entity, objectIDs, refRow, model, compositionParentInfo = null) {
157
+ const keys = utils.extractKeys(entity.keys);
158
+ const entityKey = compositeKeyExpr(keys.map((k) => `${refRow}.${k}`));
159
+ const objectID = buildObjectIDSelect(objectIDs, entity.name, keys, refRow, model) ?? entityKey;
160
+ const parentLookupExpr = compositionParentInfo ? 'PARENT_LOOKUP_PLACEHOLDER' : null;
161
+
162
+ return { keys, entityKey, objectID, parentLookupExpr };
163
+ }
164
+
165
+ function buildInsertSQL(entity, columns, modification, ctx, model) {
166
+ const unionQuery = columns
167
+ .map((col) => {
168
+ const whereCondition = getWhereCondition(col, modification);
169
+ const elementSkipCondition = getElementSkipCondition(entity.name, col.name);
170
+ let fullWhere = `(${whereCondition}) AND ${elementSkipCondition}`;
171
+
172
+ // For composition-of-one columns, add deduplication check to prevent duplicate entries
173
+ // when child trigger has already created a composition entry for this transaction
174
+ if (col.type === 'cds.Composition' && ctx.entityKey) {
175
+ fullWhere += ` AND NOT EXISTS (
176
+ SELECT 1 FROM sap_changelog_Changes
177
+ WHERE entity = '${entity.name}'
178
+ AND entityKey = ${ctx.entityKey}
179
+ AND attribute = '${col.name}'
180
+ AND valueDataType = 'cds.Composition'
181
+ AND createdAt = session_context('$now')
182
+ AND createdBy = session_context('$user.id')
183
+ )`;
184
+ }
185
+
186
+ const oldVal = modification === 'create' ? 'NULL' : getValueExpr(col, 'old');
187
+ const newVal = modification === 'delete' ? 'NULL' : getValueExpr(col, 'new');
188
+ const oldLabel = modification === 'create' ? 'NULL' : getLabelExpr(col, 'old', ctx.entityKey, model);
189
+ const newLabel = modification === 'delete' ? 'NULL' : getLabelExpr(col, 'new', ctx.entityKey, model);
190
+
191
+ return `SELECT '${col.name}' AS attribute, ${oldVal} AS valueChangedFrom, ${newVal} AS valueChangedTo, ${oldLabel} AS valueChangedFromLabel, ${newLabel} AS valueChangedToLabel, '${col.type}' AS valueDataType WHERE ${fullWhere}`;
192
+ })
193
+ .join('\nUNION ALL\n');
194
+
195
+ return `INSERT INTO sap_changelog_Changes (ID, parent_ID, attribute, valueChangedFrom, valueChangedTo, valueChangedFromLabel, valueChangedToLabel, entity, entityKey, objectID, createdAt, createdBy, valueDataType, modification, transactionID)
196
+ SELECT
197
+ hex(randomblob(16)),
198
+ ${ctx.parentLookupExpr ?? 'NULL'},
199
+ attribute,
200
+ valueChangedFrom,
201
+ valueChangedTo,
202
+ valueChangedFromLabel,
203
+ valueChangedToLabel,
204
+ '${entity.name}',
205
+ ${ctx.entityKey},
206
+ ${ctx.objectID},
207
+ session_context('$now'),
208
+ session_context('$user.id'),
209
+ valueDataType,
210
+ '${modification}',
211
+ session_context('$now')
212
+ FROM (
213
+ ${unionQuery}
214
+ );`;
215
+ }
216
+
217
+ module.exports = {
218
+ toSQL,
219
+ getSkipCheckCondition,
220
+ getElementSkipCondition,
221
+ compositeKeyExpr,
222
+ getValueExpr,
223
+ getWhereCondition,
224
+ getLabelExpr,
225
+ buildObjectIDSelect,
226
+ buildTriggerContext,
227
+ buildInsertSQL
228
+ };
@@ -0,0 +1,163 @@
1
+ const utils = require('../utils/change-tracking.js');
2
+ const config = require('@sap/cds').env.requires['change-tracking'];
3
+ const { getCompositionParentInfo, getGrandParentCompositionInfo } = require('../utils/composition-helpers.js');
4
+ const { getSkipCheckCondition, buildTriggerContext, buildInsertSQL } = require('./sql-expressions.js');
5
+ const { buildCompositionParentContext } = require('./composition.js');
6
+
7
+ function generateCreateTrigger(entity, columns, objectIDs, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
8
+ const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'create', 'new', model, grandParentCompositionInfo) : null;
9
+ const ctx = buildTriggerContext(entity, objectIDs, 'new', model, compositionParentInfo);
10
+
11
+ // Replace placeholder with actual parent lookup expression if needed
12
+ if (compositionParentContext && ctx.parentLookupExpr) {
13
+ ctx.parentLookupExpr = compositionParentContext.parentLookupExpr;
14
+ }
15
+
16
+ // Handle composition-only triggers (no tracked columns, only composition parent entry)
17
+ let bodySQL;
18
+ if (columns.length === 0 && compositionParentContext) {
19
+ bodySQL = compositionParentContext.insertSQL;
20
+ } else if (compositionParentContext) {
21
+ const insertSQL = buildInsertSQL(entity, columns, 'create', ctx, model);
22
+ bodySQL = `${compositionParentContext.insertSQL}\n ${insertSQL}`;
23
+ } else {
24
+ bodySQL = buildInsertSQL(entity, columns, 'create', ctx, model);
25
+ }
26
+
27
+ return `CREATE TRIGGER IF NOT EXISTS ${utils.transformName(entity.name)}_ct_create AFTER INSERT
28
+ ON ${utils.transformName(entity.name)}
29
+ WHEN ${getSkipCheckCondition(entity.name)}
30
+ BEGIN
31
+ ${bodySQL}
32
+ END;`;
33
+ }
34
+
35
+ function generateUpdateTrigger(entity, columns, objectIDs, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
36
+ const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'update', 'new', model, grandParentCompositionInfo) : null;
37
+ const ctx = buildTriggerContext(entity, objectIDs, 'new', model, compositionParentInfo);
38
+
39
+ // Replace placeholder with actual parent lookup expression if needed
40
+ if (compositionParentContext && ctx.parentLookupExpr) {
41
+ ctx.parentLookupExpr = compositionParentContext.parentLookupExpr;
42
+ }
43
+
44
+ // Handle composition-only triggers (no tracked columns, only composition parent entry)
45
+ let bodySQL;
46
+ if (columns.length === 0 && compositionParentContext) {
47
+ bodySQL = compositionParentContext.insertSQL;
48
+ } else if (compositionParentContext) {
49
+ const insertSQL = buildInsertSQL(entity, columns, 'update', ctx, model);
50
+ bodySQL = `${compositionParentContext.insertSQL}\n ${insertSQL}`;
51
+ } else {
52
+ bodySQL = buildInsertSQL(entity, columns, 'update', ctx, model);
53
+ }
54
+
55
+ // Build OF clause for targeted update trigger
56
+ const ofColumns = columns.flatMap((c) => {
57
+ if (!c.target) return [c.name];
58
+ if (c.foreignKeys) return c.foreignKeys.map((k) => `${c.name}_${k}`);
59
+ if (c.on) return c.on.map((m) => `${c.name}_${m.foreignKeyField}`);
60
+ return [];
61
+ });
62
+ const ofClause = columns.length > 0 ? `OF ${ofColumns.join(', ')} ` : '';
63
+
64
+ return `CREATE TRIGGER IF NOT EXISTS ${utils.transformName(entity.name)}_ct_update AFTER UPDATE ${ofClause}
65
+ ON ${utils.transformName(entity.name)}
66
+ WHEN ${getSkipCheckCondition(entity.name)}
67
+ BEGIN
68
+ ${bodySQL}
69
+ END;`;
70
+ }
71
+
72
+ function generateDeleteTriggerPreserve(entity, columns, objectIDs, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
73
+ const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'delete', 'old', model, grandParentCompositionInfo) : null;
74
+ const ctx = buildTriggerContext(entity, objectIDs, 'old', model, compositionParentInfo);
75
+
76
+ // Replace placeholder with actual parent lookup expression if needed
77
+ if (compositionParentContext && ctx.parentLookupExpr) {
78
+ ctx.parentLookupExpr = compositionParentContext.parentLookupExpr;
79
+ }
80
+
81
+ // Handle composition-only triggers (no tracked columns, only composition parent entry)
82
+ let bodySQL;
83
+ if (columns.length === 0 && compositionParentContext) {
84
+ bodySQL = compositionParentContext.insertSQL;
85
+ } else if (compositionParentContext) {
86
+ const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
87
+ bodySQL = `${compositionParentContext.insertSQL}\n ${insertSQL}`;
88
+ } else {
89
+ bodySQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
90
+ }
91
+
92
+ return `CREATE TRIGGER IF NOT EXISTS ${utils.transformName(entity.name)}_ct_delete AFTER DELETE
93
+ ON ${utils.transformName(entity.name)}
94
+ WHEN ${getSkipCheckCondition(entity.name)}
95
+ BEGIN
96
+ ${bodySQL}
97
+ END;`;
98
+ }
99
+
100
+ function generateDeleteTrigger(entity, columns, objectIDs, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
101
+ const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'delete', 'old', model, grandParentCompositionInfo) : null;
102
+ const ctx = buildTriggerContext(entity, objectIDs, 'old', model, compositionParentInfo);
103
+
104
+ // Replace placeholder with actual parent lookup expression if needed
105
+ if (compositionParentContext && ctx.parentLookupExpr) {
106
+ ctx.parentLookupExpr = compositionParentContext.parentLookupExpr;
107
+ }
108
+
109
+ const deleteSQL = `DELETE FROM ${utils.transformName('sap.changelog.Changes')} WHERE entity = '${entity.name}' AND entityKey = ${ctx.entityKey};`;
110
+
111
+ // Handle composition-only triggers (no tracked columns, only composition parent entry)
112
+ let bodySQL;
113
+ if (columns.length === 0 && compositionParentContext) {
114
+ bodySQL = `${deleteSQL}\n ${compositionParentContext.insertSQL}`;
115
+ } else if (compositionParentContext) {
116
+ const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
117
+ bodySQL = `${deleteSQL}\n ${compositionParentContext.insertSQL}\n ${insertSQL}`;
118
+ } else {
119
+ const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
120
+ bodySQL = `${deleteSQL}\n ${insertSQL}`;
121
+ }
122
+
123
+ return `CREATE TRIGGER IF NOT EXISTS ${utils.transformName(entity.name)}_ct_delete AFTER DELETE
124
+ ON ${utils.transformName(entity.name)}
125
+ WHEN ${getSkipCheckCondition(entity.name)}
126
+ BEGIN
127
+ ${bodySQL}
128
+ END;`;
129
+ }
130
+
131
+ function generateSQLiteTrigger(csn, entity, rootEntity, mergedAnnotations = null, rootMergedAnnotations = null, grandParentContext = {}) {
132
+ const triggers = [];
133
+ const { columns: trackedColumns } = utils.extractTrackedColumns(entity, csn, mergedAnnotations);
134
+ const objectIDs = utils.getObjectIDs(entity, csn, mergedAnnotations?.entityAnnotation);
135
+ const rootObjectIDs = utils.getObjectIDs(rootEntity, csn, rootMergedAnnotations?.entityAnnotation);
136
+
137
+ // Check if this entity is a tracked composition target (composition-of-many)
138
+ const compositionParentInfo = getCompositionParentInfo(entity, rootEntity, rootMergedAnnotations);
139
+
140
+ // Get grandparent info for deep linking (e.g., OrderItemNote -> OrderItem.notes -> Order.orderItems)
141
+ const { grandParentEntity, grandParentMergedAnnotations, grandParentCompositionField } = grandParentContext;
142
+ const grandParentCompositionInfo = getGrandParentCompositionInfo(rootEntity, grandParentEntity, grandParentMergedAnnotations, grandParentCompositionField);
143
+
144
+ // Generate triggers if we have tracked columns OR if this is a composition target
145
+ const shouldGenerateTriggers = trackedColumns.length > 0 || compositionParentInfo;
146
+
147
+ if (shouldGenerateTriggers) {
148
+ if (!config?.disableCreateTracking) {
149
+ triggers.push(generateCreateTrigger(entity, trackedColumns, objectIDs, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo));
150
+ }
151
+ if (!config?.disableUpdateTracking) {
152
+ triggers.push(generateUpdateTrigger(entity, trackedColumns, objectIDs, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo));
153
+ }
154
+ if (!config?.disableDeleteTracking) {
155
+ const generateDeleteTriggerFunc = config?.preserveDeletes ? generateDeleteTriggerPreserve : generateDeleteTrigger;
156
+ triggers.push(generateDeleteTriggerFunc(entity, trackedColumns, objectIDs, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo));
157
+ }
158
+ }
159
+
160
+ return triggers.length === 1 ? triggers[0] : triggers.length > 0 ? triggers : null;
161
+ }
162
+
163
+ module.exports = { generateSQLiteTrigger };