@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.
- package/CHANGELOG.md +40 -1
- package/README.md +45 -71
- package/_i18n/i18n.properties +10 -22
- package/_i18n/i18n_ar.properties +3 -3
- package/_i18n/i18n_bg.properties +3 -3
- package/_i18n/i18n_cs.properties +3 -3
- package/_i18n/i18n_da.properties +3 -3
- package/_i18n/i18n_de.properties +3 -3
- package/_i18n/i18n_el.properties +3 -3
- package/_i18n/i18n_en.properties +3 -3
- package/_i18n/i18n_en_US_saptrc.properties +3 -32
- package/_i18n/i18n_es.properties +3 -3
- package/_i18n/i18n_es_MX.properties +3 -3
- package/_i18n/i18n_fi.properties +3 -3
- package/_i18n/i18n_fr.properties +3 -3
- package/_i18n/i18n_he.properties +3 -3
- package/_i18n/i18n_hr.properties +3 -3
- package/_i18n/i18n_hu.properties +3 -3
- package/_i18n/i18n_it.properties +3 -3
- package/_i18n/i18n_ja.properties +3 -3
- package/_i18n/i18n_kk.properties +3 -3
- package/_i18n/i18n_ko.properties +3 -3
- package/_i18n/i18n_ms.properties +3 -3
- package/_i18n/i18n_nl.properties +3 -3
- package/_i18n/i18n_no.properties +3 -3
- package/_i18n/i18n_pl.properties +3 -3
- package/_i18n/i18n_pt.properties +3 -3
- package/_i18n/i18n_ro.properties +3 -3
- package/_i18n/i18n_ru.properties +3 -3
- package/_i18n/i18n_sh.properties +3 -3
- package/_i18n/i18n_sk.properties +3 -3
- package/_i18n/i18n_sl.properties +3 -3
- package/_i18n/i18n_sv.properties +3 -3
- package/_i18n/i18n_th.properties +3 -3
- package/_i18n/i18n_tr.properties +3 -3
- package/_i18n/i18n_uk.properties +3 -3
- package/_i18n/i18n_vi.properties +3 -3
- package/_i18n/i18n_zh_CN.properties +3 -3
- package/_i18n/i18n_zh_TW.properties +3 -3
- package/cds-plugin.js +16 -263
- package/index.cds +187 -76
- package/lib/TriggerCQN2SQL.js +42 -0
- package/lib/h2/java-codegen.js +833 -0
- package/lib/h2/register.js +27 -0
- package/lib/h2/triggers.js +41 -0
- package/lib/hana/composition.js +248 -0
- package/lib/hana/register.js +28 -0
- package/lib/hana/sql-expressions.js +213 -0
- package/lib/hana/triggers.js +253 -0
- package/lib/localization.js +53 -117
- package/lib/model-enhancer.js +266 -0
- package/lib/postgres/composition.js +190 -0
- package/lib/postgres/register.js +44 -0
- package/lib/postgres/sql-expressions.js +261 -0
- package/lib/postgres/triggers.js +113 -0
- package/lib/skipHandlers.js +34 -0
- package/lib/sqlite/composition.js +234 -0
- package/lib/sqlite/register.js +28 -0
- package/lib/sqlite/sql-expressions.js +228 -0
- package/lib/sqlite/triggers.js +163 -0
- package/lib/utils/change-tracking.js +394 -0
- package/lib/utils/composition-helpers.js +67 -0
- package/lib/utils/entity-collector.js +297 -0
- package/lib/utils/session-variables.js +276 -0
- package/lib/utils/trigger-utils.js +94 -0
- package/package.json +17 -7
- package/lib/change-log.js +0 -538
- package/lib/entity-helper.js +0 -217
- package/lib/format-options.js +0 -66
- 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 };
|