@cap-js/change-tracking 2.0.0-beta.1 → 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.
@@ -3,26 +3,18 @@ const { CT_SKIP_VAR, getEntitySkipVarName, getElementSkipVarName } = require('..
3
3
  const { createTriggerCQN2SQL } = require('../TriggerCQN2SQL.js');
4
4
 
5
5
  const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
6
- let SQLiteCQN2SQL;
7
- let model;
6
+ const _cqn2sqlCache = new WeakMap();
8
7
 
9
- function setModel(m) {
10
- model = m;
11
- SQLiteCQN2SQL = null;
12
- }
13
-
14
- function getModel() {
15
- return model;
16
- }
17
-
18
- function toSQL(query) {
19
- if (!SQLiteCQN2SQL) {
8
+ function toSQL(query, model) {
9
+ let cqn2sql = _cqn2sqlCache.get(model);
10
+ if (!cqn2sql) {
20
11
  const SQLiteService = require('@cap-js/sqlite');
21
12
  const TriggerCQN2SQL = createTriggerCQN2SQL(SQLiteService.CQN2SQL);
22
- SQLiteCQN2SQL = new TriggerCQN2SQL({ model: model });
13
+ cqn2sql = new TriggerCQN2SQL({ model: model });
14
+ _cqn2sqlCache.set(model, cqn2sql);
23
15
  }
24
16
  const sqlCQN = cqn4sql(query, model);
25
- return SQLiteCQN2SQL.SELECT(sqlCQN);
17
+ return cqn2sql.SELECT(sqlCQN);
26
18
  }
27
19
 
28
20
  /**
@@ -103,7 +95,7 @@ function getWhereCondition(col, modification) {
103
95
  /**
104
96
  * Builds scalar subselect for association label lookup with locale awareness
105
97
  */
106
- function buildAssocLookup(column, refRow, entityKey) {
98
+ function buildAssocLookup(column, refRow, entityKey, model) {
107
99
  const where = column.foreignKeys
108
100
  ? column.foreignKeys.reduce((acc, k) => {
109
101
  acc[k] = { val: `${refRow}.${column.name}_${k}` };
@@ -125,35 +117,35 @@ function buildAssocLookup(column, refRow, entityKey) {
125
117
  const textsWhere = { ...where, locale: { func: 'session_context', args: [{ val: '$user.locale' }] } };
126
118
  const textsQuery = SELECT.one.from(localizedInfo.textsEntity).columns(columns).where(textsWhere);
127
119
  const baseQuery = SELECT.one.from(column.target).columns(columns).where(where);
128
- return `(SELECT COALESCE((${toSQL(textsQuery)}), (${toSQL(baseQuery)})))`;
120
+ return `(SELECT COALESCE((${toSQL(textsQuery, model)}), (${toSQL(baseQuery, model)})))`;
129
121
  }
130
122
 
131
123
  const query = SELECT.one.from(column.target).columns(columns).where(where);
132
- return `(${toSQL(query)})`;
124
+ return `(${toSQL(query, model)})`;
133
125
  }
134
126
 
135
127
  /**
136
128
  * Returns SQL expression for a column's label (looked-up value for associations)
137
129
  */
138
- function getLabelExpr(col, refRow, entityKey) {
130
+ function getLabelExpr(col, refRow, entityKey, model) {
139
131
  if (col.target && col.alt) {
140
- return buildAssocLookup(col, refRow, entityKey);
132
+ return buildAssocLookup(col, refRow, entityKey, model);
141
133
  }
142
134
  return 'NULL';
143
135
  }
144
136
 
145
137
  /**
146
138
  * Builds SQL expression for objectID (entity display name)
147
- * Uses @changelog annotation fields, falling back to entity name
139
+ * Uses @changelog annotation fields, falling back to entity keys
148
140
  */
149
- function buildObjectIDSelect(objectIDs, entityName, entityKeys, refRow) {
150
- if (objectIDs.length === 0) return `'${entityName}'`;
141
+ function buildObjectIDSelect(objectIDs, entityName, entityKeys, refRow, model) {
142
+ if (objectIDs.length === 0) return null;
151
143
 
152
144
  for (const objectID of objectIDs) {
153
145
  if (objectID.included) continue;
154
146
  const where = buildKeyWhere(entityKeys, refRow);
155
147
  const query = SELECT.one.from(entityName).columns(objectID.name).where(where);
156
- objectID.selectSQL = toSQL(query);
148
+ objectID.selectSQL = toSQL(query, model);
157
149
  }
158
150
 
159
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`));
@@ -161,16 +153,16 @@ function buildObjectIDSelect(objectIDs, entityName, entityKeys, refRow) {
161
153
  return `(SELECT GROUP_CONCAT(value, ', ') FROM (${unionParts.join('\nUNION ALL\n')}))`;
162
154
  }
163
155
 
164
- function buildTriggerContext(entity, objectIDs, refRow, compositionParentInfo = null) {
156
+ function buildTriggerContext(entity, objectIDs, refRow, model, compositionParentInfo = null) {
165
157
  const keys = utils.extractKeys(entity.keys);
166
158
  const entityKey = compositeKeyExpr(keys.map((k) => `${refRow}.${k}`));
167
- const objectID = buildObjectIDSelect(objectIDs, entity.name, keys, refRow) ?? entityKey;
159
+ const objectID = buildObjectIDSelect(objectIDs, entity.name, keys, refRow, model) ?? entityKey;
168
160
  const parentLookupExpr = compositionParentInfo ? 'PARENT_LOOKUP_PLACEHOLDER' : null;
169
161
 
170
162
  return { keys, entityKey, objectID, parentLookupExpr };
171
163
  }
172
164
 
173
- function buildInsertSQL(entity, columns, modification, ctx) {
165
+ function buildInsertSQL(entity, columns, modification, ctx, model) {
174
166
  const unionQuery = columns
175
167
  .map((col) => {
176
168
  const whereCondition = getWhereCondition(col, modification);
@@ -193,8 +185,8 @@ function buildInsertSQL(entity, columns, modification, ctx) {
193
185
 
194
186
  const oldVal = modification === 'create' ? 'NULL' : getValueExpr(col, 'old');
195
187
  const newVal = modification === 'delete' ? 'NULL' : getValueExpr(col, 'new');
196
- const oldLabel = modification === 'create' ? 'NULL' : getLabelExpr(col, 'old', ctx.entityKey);
197
- const newLabel = modification === 'delete' ? 'NULL' : getLabelExpr(col, 'new', ctx.entityKey);
188
+ const oldLabel = modification === 'create' ? 'NULL' : getLabelExpr(col, 'old', ctx.entityKey, model);
189
+ const newLabel = modification === 'delete' ? 'NULL' : getLabelExpr(col, 'new', ctx.entityKey, model);
198
190
 
199
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}`;
200
192
  })
@@ -223,8 +215,6 @@ function buildInsertSQL(entity, columns, modification, ctx) {
223
215
  }
224
216
 
225
217
  module.exports = {
226
- setModel,
227
- getModel,
228
218
  toSQL,
229
219
  getSkipCheckCondition,
230
220
  getElementSkipCondition,
@@ -1,12 +1,12 @@
1
1
  const utils = require('../utils/change-tracking.js');
2
2
  const config = require('@sap/cds').env.requires['change-tracking'];
3
3
  const { getCompositionParentInfo, getGrandParentCompositionInfo } = require('../utils/composition-helpers.js');
4
- const { setModel, getSkipCheckCondition, buildTriggerContext, buildInsertSQL } = require('./sql-expressions.js');
4
+ const { getSkipCheckCondition, buildTriggerContext, buildInsertSQL } = require('./sql-expressions.js');
5
5
  const { buildCompositionParentContext } = require('./composition.js');
6
6
 
7
- function generateCreateTrigger(entity, columns, objectIDs, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
8
- const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'create', 'new', grandParentCompositionInfo) : null;
9
- const ctx = buildTriggerContext(entity, objectIDs, 'new', compositionParentInfo);
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
10
 
11
11
  // Replace placeholder with actual parent lookup expression if needed
12
12
  if (compositionParentContext && ctx.parentLookupExpr) {
@@ -18,10 +18,10 @@ function generateCreateTrigger(entity, columns, objectIDs, rootObjectIDs, compos
18
18
  if (columns.length === 0 && compositionParentContext) {
19
19
  bodySQL = compositionParentContext.insertSQL;
20
20
  } else if (compositionParentContext) {
21
- const insertSQL = buildInsertSQL(entity, columns, 'create', ctx);
21
+ const insertSQL = buildInsertSQL(entity, columns, 'create', ctx, model);
22
22
  bodySQL = `${compositionParentContext.insertSQL}\n ${insertSQL}`;
23
23
  } else {
24
- bodySQL = buildInsertSQL(entity, columns, 'create', ctx);
24
+ bodySQL = buildInsertSQL(entity, columns, 'create', ctx, model);
25
25
  }
26
26
 
27
27
  return `CREATE TRIGGER IF NOT EXISTS ${utils.transformName(entity.name)}_ct_create AFTER INSERT
@@ -32,9 +32,9 @@ function generateCreateTrigger(entity, columns, objectIDs, rootObjectIDs, compos
32
32
  END;`;
33
33
  }
34
34
 
35
- function generateUpdateTrigger(entity, columns, objectIDs, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
36
- const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'update', 'new', grandParentCompositionInfo) : null;
37
- const ctx = buildTriggerContext(entity, objectIDs, 'new', compositionParentInfo);
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
38
 
39
39
  // Replace placeholder with actual parent lookup expression if needed
40
40
  if (compositionParentContext && ctx.parentLookupExpr) {
@@ -46,10 +46,10 @@ function generateUpdateTrigger(entity, columns, objectIDs, rootObjectIDs, compos
46
46
  if (columns.length === 0 && compositionParentContext) {
47
47
  bodySQL = compositionParentContext.insertSQL;
48
48
  } else if (compositionParentContext) {
49
- const insertSQL = buildInsertSQL(entity, columns, 'update', ctx);
49
+ const insertSQL = buildInsertSQL(entity, columns, 'update', ctx, model);
50
50
  bodySQL = `${compositionParentContext.insertSQL}\n ${insertSQL}`;
51
51
  } else {
52
- bodySQL = buildInsertSQL(entity, columns, 'update', ctx);
52
+ bodySQL = buildInsertSQL(entity, columns, 'update', ctx, model);
53
53
  }
54
54
 
55
55
  // Build OF clause for targeted update trigger
@@ -69,9 +69,9 @@ function generateUpdateTrigger(entity, columns, objectIDs, rootObjectIDs, compos
69
69
  END;`;
70
70
  }
71
71
 
72
- function generateDeleteTriggerPreserve(entity, columns, objectIDs, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
73
- const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'delete', 'old', grandParentCompositionInfo) : null;
74
- const ctx = buildTriggerContext(entity, objectIDs, 'old', compositionParentInfo);
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
75
 
76
76
  // Replace placeholder with actual parent lookup expression if needed
77
77
  if (compositionParentContext && ctx.parentLookupExpr) {
@@ -83,10 +83,10 @@ function generateDeleteTriggerPreserve(entity, columns, objectIDs, rootObjectIDs
83
83
  if (columns.length === 0 && compositionParentContext) {
84
84
  bodySQL = compositionParentContext.insertSQL;
85
85
  } else if (compositionParentContext) {
86
- const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx);
86
+ const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
87
87
  bodySQL = `${compositionParentContext.insertSQL}\n ${insertSQL}`;
88
88
  } else {
89
- bodySQL = buildInsertSQL(entity, columns, 'delete', ctx);
89
+ bodySQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
90
90
  }
91
91
 
92
92
  return `CREATE TRIGGER IF NOT EXISTS ${utils.transformName(entity.name)}_ct_delete AFTER DELETE
@@ -97,9 +97,9 @@ function generateDeleteTriggerPreserve(entity, columns, objectIDs, rootObjectIDs
97
97
  END;`;
98
98
  }
99
99
 
100
- function generateDeleteTrigger(entity, columns, objectIDs, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
101
- const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'delete', 'old', grandParentCompositionInfo) : null;
102
- const ctx = buildTriggerContext(entity, objectIDs, 'old', compositionParentInfo);
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
103
 
104
104
  // Replace placeholder with actual parent lookup expression if needed
105
105
  if (compositionParentContext && ctx.parentLookupExpr) {
@@ -113,10 +113,10 @@ function generateDeleteTrigger(entity, columns, objectIDs, rootObjectIDs, compos
113
113
  if (columns.length === 0 && compositionParentContext) {
114
114
  bodySQL = `${deleteSQL}\n ${compositionParentContext.insertSQL}`;
115
115
  } else if (compositionParentContext) {
116
- const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx);
116
+ const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
117
117
  bodySQL = `${deleteSQL}\n ${compositionParentContext.insertSQL}\n ${insertSQL}`;
118
118
  } else {
119
- const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx);
119
+ const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
120
120
  bodySQL = `${deleteSQL}\n ${insertSQL}`;
121
121
  }
122
122
 
@@ -129,7 +129,6 @@ function generateDeleteTrigger(entity, columns, objectIDs, rootObjectIDs, compos
129
129
  }
130
130
 
131
131
  function generateSQLiteTrigger(csn, entity, rootEntity, mergedAnnotations = null, rootMergedAnnotations = null, grandParentContext = {}) {
132
- setModel(csn);
133
132
  const triggers = [];
134
133
  const { columns: trackedColumns } = utils.extractTrackedColumns(entity, csn, mergedAnnotations);
135
134
  const objectIDs = utils.getObjectIDs(entity, csn, mergedAnnotations?.entityAnnotation);
@@ -147,14 +146,14 @@ function generateSQLiteTrigger(csn, entity, rootEntity, mergedAnnotations = null
147
146
 
148
147
  if (shouldGenerateTriggers) {
149
148
  if (!config?.disableCreateTracking) {
150
- triggers.push(generateCreateTrigger(entity, trackedColumns, objectIDs, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo));
149
+ triggers.push(generateCreateTrigger(entity, trackedColumns, objectIDs, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo));
151
150
  }
152
151
  if (!config?.disableUpdateTracking) {
153
- triggers.push(generateUpdateTrigger(entity, trackedColumns, objectIDs, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo));
152
+ triggers.push(generateUpdateTrigger(entity, trackedColumns, objectIDs, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo));
154
153
  }
155
154
  if (!config?.disableDeleteTracking) {
156
155
  const generateDeleteTriggerFunc = config?.preserveDeletes ? generateDeleteTriggerPreserve : generateDeleteTrigger;
157
- triggers.push(generateDeleteTriggerFunc(entity, trackedColumns, objectIDs, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo));
156
+ triggers.push(generateDeleteTriggerFunc(entity, trackedColumns, objectIDs, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo));
158
157
  }
159
158
  }
160
159
 
@@ -207,7 +207,7 @@ function getObjectIDs(entity, model = cds.context?.model ?? cds.model, overrideE
207
207
  if (!entity) return [];
208
208
  // Use override annotation if provided, otherwise use the entity's own annotation
209
209
  const entityAnnotation = overrideEntityAnnotation ?? entity['@changelog'];
210
- if (!entityAnnotation) return [];
210
+ if (!entityAnnotation || entityAnnotation === true) return [];
211
211
  const ids = [];
212
212
 
213
213
  for (const { ['=']: field } of entityAnnotation) {
@@ -9,19 +9,17 @@ const CT_SKIP_VAR = 'ct.skip';
9
9
  const CT_SKIP_ENTITY_PREFIX = 'ct.skip_entity.';
10
10
  const CT_SKIP_ELEMENT_PREFIX = 'ct.skip_element.';
11
11
 
12
- const _resolveTableName = (entity) => cds.db?.resolve?.table(entity) || _getTableNameFallback(entity);
13
-
14
- function _getTableNameFallback(entity) {
15
- const baseRef = entity.query?.SELECT?.from?.ref?.[0];
12
+ function _resolveView(entity) {
16
13
  const model = cds.context?.model ?? cds.model;
14
+ const baseRef = entity.query?._target;
17
15
  if (!baseRef || !model) return null;
18
16
 
19
- const baseEntity = model.definitions[baseRef];
17
+ const baseEntity = model.definitions[baseRef.name];
20
18
  if (!baseEntity) return null;
21
19
 
22
20
  // If base entity is also a projection, recurse
23
- if (baseEntity.query?.SELECT?.from?.ref) {
24
- return _getTableNameFallback(baseEntity);
21
+ if (baseEntity.query?._target) {
22
+ return _resolveView(baseEntity);
25
23
  }
26
24
 
27
25
  return baseEntity;
@@ -38,7 +36,7 @@ function getElementSkipVarName(entityName, elementName) {
38
36
  function _findServiceEntity(service, dbEntity) {
39
37
  if (!service || !dbEntity) return null;
40
38
  for (const def of service.entities) {
41
- const projectionTarget = _resolveTableName(def)?.name;
39
+ const projectionTarget = _resolveView(def)?.name;
42
40
  if (projectionTarget === dbEntity.name) return def;
43
41
  }
44
42
  return null;
@@ -66,7 +64,7 @@ function _collectDeepEntities(entity, data, service, toSkip) {
66
64
 
67
65
  function _collectSkipEntities(rootTarget, query, service) {
68
66
  const toSkip = new Set();
69
- const dbEntity = _resolveTableName(rootTarget);
67
+ const dbEntity = _resolveView(rootTarget);
70
68
 
71
69
  // Check root entity annotation
72
70
  if (rootTarget['@changelog'] === false || rootTarget['@changelog'] === null) {
@@ -105,7 +103,7 @@ function _collectSkipElements(serviceEntity, query, service) {
105
103
  const toSkip = [];
106
104
  if (!serviceEntity?.elements) return toSkip;
107
105
 
108
- const dbEntity = _resolveTableName(serviceEntity);
106
+ const dbEntity = _resolveView(serviceEntity);
109
107
  if (!dbEntity) return toSkip;
110
108
 
111
109
  for (const [elementName, element] of Object.entries(serviceEntity.elements)) {
@@ -241,7 +239,7 @@ function resetSkipSessionVariables(req) {
241
239
 
242
240
  // --- Auto-skip for non-opted-in service entities ---
243
241
  function _shouldSkipServiceEntity(serviceEntity, collectedEntities) {
244
- const dbEntityName = _resolveTableName(serviceEntity)?.name;
242
+ const dbEntityName = _resolveView(serviceEntity)?.name;
245
243
  if (!dbEntityName) return false;
246
244
 
247
245
  const srvEntities = collectedEntities.get(dbEntityName);
@@ -252,7 +250,7 @@ function _shouldSkipServiceEntity(serviceEntity, collectedEntities) {
252
250
  }
253
251
 
254
252
  function _setAutoSkipForServiceEntity(req) {
255
- const dbEntity = _resolveTableName(req.target);
253
+ const dbEntity = _resolveView(req.target);
256
254
  if (!dbEntity) return null;
257
255
 
258
256
  const varName = getEntitySkipVarName(dbEntity.name);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/change-tracking",
3
- "version": "2.0.0-beta.1",
3
+ "version": "2.0.0-beta.2",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "tag": "beta"