@cap-js/change-tracking 2.0.0-beta.1 → 2.0.0-beta.3

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/index.cds CHANGED
@@ -17,8 +17,8 @@ entity aspect @(UI.Facets: [{
17
17
  @UI.PartOfPreview: false
18
18
  }]) {
19
19
  changes : Association to many ChangeView
20
- on changes.entityKey = ID
21
- and changes.entity = 'ENTITY';
20
+ on changes.entityKey = ID
21
+ and changes.entity = 'ENTITY';
22
22
  key ID : String;
23
23
  }
24
24
 
@@ -31,73 +31,72 @@ entity aspect @(UI.Facets: [{
31
31
  @readonly
32
32
  @cds.autoexpose
33
33
  view ChangeView as
34
- select from Changes as change {
35
- key change.ID @UI.Hidden,
36
- *,
37
- COALESCE(
38
- (
39
- select text from i18nKeys
40
- where
41
- ID = change.attribute
42
- and locale = $user.locale
43
- ), (
44
- select text from i18nKeys
45
- where
46
- ID = change.attribute
47
- and locale = 'en'
48
- ), change.attribute
49
- ) as attributeLabel : String(15) @title: '{i18n>Changes.attribute}',
50
- COALESCE(
51
- (
52
- select text from i18nKeys
53
- where
54
- ID = change.entity
55
- and locale = $user.locale
56
- ), (
57
- select text from i18nKeys
58
- where
59
- ID = change.entity
60
- and locale = 'en'
61
- ), change.entity
62
- ) as entityLabel : String(24) @title: '{i18n>Changes.entity}',
63
- COALESCE(
64
- (
65
- select text from i18nKeys
66
- where
67
- ID = change.modification
68
- and locale = $user.locale
69
- ), (
70
- select text from i18nKeys
71
- where
72
- ID = change.modification
73
- and locale = 'en'
74
- ), change.modification
75
- ) as modificationLabel : String(16) @title: '{i18n>Changes.modification}',
76
- COALESCE(
77
- (
78
- select text from i18nKeys
79
- where
80
- ID = change.objectID
81
- and locale = $user.locale
82
- ), (
83
- select text from i18nKeys
84
- where
85
- ID = change.objectID
86
- and locale = 'en'
87
- ), change.objectID
88
- ) as objectID : String(24) @title: '{i18n>Changes.objectID}',
89
- COALESCE(
90
- change.valueChangedFromLabel, change.valueChangedFrom
91
- ) as valueChangedFromLabel : String(5000) @title: '{i18n>Changes.valueChangedFrom}',
92
- COALESCE(
93
- change.valueChangedToLabel, change.valueChangedTo
94
- ) as valueChangedToLabel : String(5000) @title: '{i18n>Changes.valueChangedTo}',
95
-
96
- // For the hierarchy
97
- null as LimitedDescendantCount : Int16 @UI.Hidden,
98
- null as DistanceFromRoot : Int16 @UI.Hidden,
99
- null as DrillState : String @UI.Hidden,
100
- null as LimitedRank : Int16 @UI.Hidden,
34
+ select from Changes as change
35
+ left outer join i18nKeys as attributeI18n
36
+ on attributeI18n.ID = change.attribute
37
+ and attributeI18n.locale = $user.locale
38
+ left outer join i18nKeys as entityI18n
39
+ on entityI18n.ID = change.entity
40
+ and entityI18n.locale = $user.locale
41
+ left outer join i18nKeys as modificationI18n
42
+ on modificationI18n.ID = change.modification
43
+ and modificationI18n.locale = $user.locale
44
+ {
45
+ key change.ID @UI.Hidden,
46
+ change.parent : redirected to ChangeView,
47
+ change.children : redirected to ChangeView,
48
+ change.attribute,
49
+ change.valueChangedFrom,
50
+ change.valueChangedTo,
51
+ change.entity,
52
+ change.entityKey,
53
+ change.objectID,
54
+ change.modification,
55
+ change.valueDataType,
56
+ change.createdAt,
57
+ change.createdBy,
58
+ change.transactionID,
59
+ COALESCE(
60
+ attributeI18n.text, (
61
+ select text from i18nKeys
62
+ where
63
+ ID = change.attribute
64
+ and locale = 'en'
65
+ ), change.attribute
66
+ ) as attributeLabel : String(15) @title: '{i18n>Changes.attribute}',
67
+ COALESCE(
68
+ entityI18n.text, (
69
+ select text from i18nKeys
70
+ where
71
+ ID = change.entity
72
+ and locale = 'en'
73
+ ), change.entity
74
+ ) as entityLabel : String(24) @title: '{i18n>Changes.entity}',
75
+ COALESCE(
76
+ modificationI18n.text, (
77
+ select text from i18nKeys
78
+ where
79
+ ID = change.modification
80
+ and locale = 'en'
81
+ ), change.modification
82
+ ) as modificationLabel : String(16) @title: '{i18n>Changes.modification}',
83
+ COALESCE(
84
+ change.valueChangedFromLabel, change.valueChangedFrom
85
+ ) as valueChangedFromLabel : String(5000) @(
86
+ title: '{i18n>Changes.valueChangedFrom}',
87
+ UI.MultiLineText
88
+ ),
89
+ COALESCE(
90
+ change.valueChangedToLabel, change.valueChangedTo
91
+ ) as valueChangedToLabel : String(5000) @(
92
+ title: '{i18n>Changes.valueChangedTo}',
93
+ UI.MultiLineText
94
+ ),
95
+ // For the hierarchy
96
+ null as LimitedDescendantCount : Int16 @UI.Hidden,
97
+ null as DistanceFromRoot : Int16 @UI.Hidden,
98
+ null as DrillState : String @UI.Hidden,
99
+ null as LimitedRank : Int16 @UI.Hidden,
101
100
  };
102
101
 
103
102
  entity i18nKeys {
@@ -159,46 +158,46 @@ annotate ChangeView with @(UI: {
159
158
  },
160
159
  LineItem : [
161
160
  {
162
- Value : modificationLabel,
163
- @UI.Importance : #Low
161
+ Value : modificationLabel,
162
+ @UI.Importance: #Low
164
163
  },
165
164
  {
166
- Value : entityLabel,
167
- @UI.Importance : #Medium
165
+ Value : entityLabel,
166
+ @UI.Importance: #Medium
168
167
  },
169
168
  {
170
- Value : objectID,
171
- @UI.Importance : #Medium
169
+ Value : objectID,
170
+ @UI.Importance: #Medium
172
171
  },
173
172
  {
174
- Value : attributeLabel,
175
- @UI.Importance : #Medium
173
+ Value : attributeLabel,
174
+ @UI.Importance: #Medium
176
175
  },
177
176
  {
178
- Value : valueChangedToLabel,
179
- @UI.Importance : #High
177
+ Value : valueChangedToLabel,
178
+ @UI.Importance: #High
180
179
  },
181
180
  {
182
- Value : valueChangedFromLabel,
183
- @UI.Importance : #High
181
+ Value : valueChangedFromLabel,
182
+ @UI.Importance: #High
184
183
  },
185
184
  {
186
- Value : createdAt,
187
- @UI.Importance : #Low
185
+ Value : createdAt,
186
+ @UI.Importance: #Low
188
187
  },
189
188
  {
190
- Value : createdBy,
191
- @UI.Importance : #High
189
+ Value : createdBy,
190
+ @UI.Importance: #High
192
191
  },
193
192
  ],
194
193
  DeleteHidden : true,
195
194
  }) {
196
195
  valueChangedFrom @UI.Hidden;
197
- valueChangedTo @UI.Hidden;
198
- parent @UI.Hidden;
199
- entityKey @UI.Hidden;
200
- entity @UI.Hidden;
201
- attribute @UI.Hidden;
196
+ valueChangedTo @UI.Hidden;
197
+ parent @UI.Hidden;
198
+ entityKey @UI.Hidden;
199
+ entity @UI.Hidden;
200
+ attribute @UI.Hidden;
202
201
  };
203
202
 
204
203
  annotate ChangeView with @(
@@ -229,3 +228,28 @@ annotate ChangeView with @(
229
228
  'LimitedRank'
230
229
  ],
231
230
  );
231
+
232
+ // Annotations for searching
233
+ annotate ChangeView with @(cds.search: {
234
+ valueChangedFrom: false,
235
+ valueChangedTo : false,
236
+ entity : false,
237
+ attribute : false,
238
+ modification : false,
239
+ valueDataType : false,
240
+ modificationLabel,
241
+ entityLabel,
242
+ entityKey,
243
+ objectID,
244
+ attributeLabel,
245
+ valueChangedFromLabel,
246
+ valueChangedToLabel,
247
+ createdBy,
248
+ }) {
249
+ entityLabel @Search.ranking: HIGH;
250
+ attributeLabel @Search.ranking: HIGH;
251
+ objectID @Search.ranking: HIGH;
252
+
253
+ entityKey @Search.ranking: LOW;
254
+ modificationLabel @Search.ranking: LOW;
255
+ };
@@ -4,29 +4,21 @@ const { createTriggerCQN2SQL } = require('../TriggerCQN2SQL.js');
4
4
 
5
5
  const cqn4sql = require('@cap-js/db-service/lib/cqn4sql');
6
6
 
7
- let SQLiteCQN2SQL;
8
- let model;
7
+ const _cqn2sqlCache = new WeakMap();
9
8
 
10
- function setModel(m) {
11
- model = m;
12
- SQLiteCQN2SQL = null;
13
- }
14
-
15
- function getModel() {
16
- return model;
17
- }
18
-
19
- function _toSQL(query) {
20
- if (!SQLiteCQN2SQL) {
9
+ function _toSQL(query, model) {
10
+ let cqn2sql = _cqn2sqlCache.get(model);
11
+ if (!cqn2sql) {
21
12
  const SQLiteService = require('@cap-js/sqlite');
22
13
  const TriggerCQN2SQL = createTriggerCQN2SQL(SQLiteService.CQN2SQL);
23
- SQLiteCQN2SQL = new TriggerCQN2SQL({ model: model });
14
+ cqn2sql = new TriggerCQN2SQL({ model: model });
15
+ _cqn2sqlCache.set(model, cqn2sql);
24
16
  }
25
17
  const sqlCQN = cqn4sql(query, model);
26
- return SQLiteCQN2SQL.SELECT(sqlCQN);
18
+ return cqn2sql.SELECT(sqlCQN);
27
19
  }
28
20
 
29
- function handleAssocLookup(column, refRow) {
21
+ function handleAssocLookup(column, refRow, model) {
30
22
  let bindings = [];
31
23
  let where = {};
32
24
 
@@ -56,8 +48,8 @@ function handleAssocLookup(column, refRow) {
56
48
  const textsQuery = SELECT.one.from(localizedInfo.textsEntity).columns(columns).where(textsWhere);
57
49
  const baseQuery = SELECT.one.from(column.target).columns(columns).where(where);
58
50
 
59
- const textsSQL = _toSQL(textsQuery);
60
- const baseSQL = _toSQL(baseQuery);
51
+ const textsSQL = _toSQL(textsQuery, model);
52
+ const baseSQL = _toSQL(baseQuery, model);
61
53
 
62
54
  // Add locale binding (fetched from session variable @$user.locale)
63
55
  const textsBindings = [...bindings, 'locale'];
@@ -73,7 +65,7 @@ function handleAssocLookup(column, refRow) {
73
65
  const query = SELECT.one.from(column.target).columns(columns).where(where);
74
66
 
75
67
  return {
76
- sql: `(${_toSQL(query)})`,
68
+ sql: `(${_toSQL(query, model)})`,
77
69
  bindings: bindings
78
70
  };
79
71
  }
@@ -288,12 +280,12 @@ ${grandParentHelper}${parentIdHelper}
288
280
  }`;
289
281
  }
290
282
 
291
- function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
283
+ function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
292
284
  const reference = 'newRow';
293
285
 
294
286
  // Set modification type for grandparent entry creation (must be before keysCalc which uses it)
295
287
  const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "create";' : '';
296
- const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
288
+ const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
297
289
 
298
290
  // Composition parent handling - with deep linking support when grandParentCompositionInfo exists
299
291
  let parentIdSetup = '';
@@ -314,7 +306,7 @@ function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectI
314
306
  .map((col) => {
315
307
  // Prepare Value Expression
316
308
  const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
317
- const labelRes = _prepareLabelExpression(col, reference); // label expression
309
+ const labelRes = _prepareLabelExpression(col, reference, model); // label expression
318
310
 
319
311
  // SQL Statement - include PARENT_ID if composition parent info exists
320
312
  const insertSQL = compositionParentInfo
@@ -346,12 +338,12 @@ function _generateCreateBody(entity, columns, objectIDs, rootEntity, rootObjectI
346
338
  return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
347
339
  }
348
340
 
349
- function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
341
+ function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
350
342
  const reference = 'newRow';
351
343
 
352
344
  // Set modification type for grandparent entry creation (must be before keysCalc which uses it)
353
345
  const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "update";' : '';
354
- const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
346
+ const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
355
347
 
356
348
  // Composition parent handling - with deep linking support when grandParentCompositionInfo exists
357
349
  let parentIdSetup = '';
@@ -374,8 +366,8 @@ function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectI
374
366
  const newRes = _prepareValueExpression(col, 'newRow');
375
367
  const oldRes = _prepareValueExpression(col, 'oldRow');
376
368
  // Prepare new and old Label (lookup values if col.alt exists)
377
- const newLabelRes = _prepareLabelExpression(col, 'newRow');
378
- const oldLabelRes = _prepareLabelExpression(col, 'oldRow');
369
+ const newLabelRes = _prepareLabelExpression(col, 'newRow', model);
370
+ const oldLabelRes = _prepareLabelExpression(col, 'oldRow', model);
379
371
 
380
372
  // Check column values from ResultSet for Change Logic
381
373
  let checkCols = [col.name];
@@ -420,12 +412,12 @@ function _generateUpdateBody(entity, columns, objectIDs, rootEntity, rootObjectI
420
412
  return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
421
413
  }
422
414
 
423
- function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
415
+ function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
424
416
  const reference = 'oldRow';
425
417
 
426
418
  // Set modification type for grandparent entry creation (must be before keysCalc which uses it)
427
419
  const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "delete";' : '';
428
- const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
420
+ const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
429
421
 
430
422
  // First delete existing changelogs for this entity
431
423
  const deleteSQL = `DELETE FROM sap_changelog_Changes WHERE ENTITY = ? AND ENTITYKEY = ?`;
@@ -452,7 +444,7 @@ function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectI
452
444
  // Prepare Old Value (raw FK value)
453
445
  const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
454
446
  // Prepare Old Label (lookup value if col.alt exists)
455
- const labelRes = _prepareLabelExpression(col, reference);
447
+ const labelRes = _prepareLabelExpression(col, reference, model);
456
448
 
457
449
  // SQL Statement - include PARENT_ID if composition parent info exists
458
450
  const insertSQL = compositionParentInfo
@@ -487,12 +479,12 @@ function _generateDeleteBody(entity, columns, objectIDs, rootEntity, rootObjectI
487
479
  ${columnBlocks}`;
488
480
  }
489
481
 
490
- function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
482
+ function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
491
483
  const reference = 'oldRow';
492
484
 
493
485
  // Set modification type for grandparent entry creation (must be before keysCalc which uses it)
494
486
  const modificationTypeSetup = grandParentCompositionInfo ? 'String modificationType = "delete";' : '';
495
- const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
487
+ const keysCalc = _generateKeyCalculationJava(entity, rootEntity, reference, rootObjectIDs, model, compositionParentInfo, grandParentCompositionInfo);
496
488
 
497
489
  // Composition parent handling - with deep linking support when grandParentCompositionInfo exists
498
490
  let parentIdSetup = '';
@@ -514,7 +506,7 @@ function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, roo
514
506
  // Prepare Old Value (raw FK value)
515
507
  const { sqlExpr, bindings } = _prepareValueExpression(col, reference);
516
508
  // Prepare Old Label (lookup value if col.alt exists)
517
- const labelRes = _prepareLabelExpression(col, reference);
509
+ const labelRes = _prepareLabelExpression(col, reference, model);
518
510
 
519
511
  // SQL Statement - include PARENT_ID if composition parent info exists
520
512
  const insertSQL = compositionParentInfo
@@ -545,13 +537,13 @@ function _generateDeleteBodyPreserve(entity, columns, objectIDs, rootEntity, roo
545
537
  return `${modificationTypeSetup}\n${keysCalc}\n${parentIdSetup}\n${columnBlocks}`;
546
538
  }
547
539
 
548
- function _generateKeyCalculationJava(entity, rootEntity, ref, rootObjectIDs, compositionParentInfo = null, grandParentCompositionInfo = null) {
540
+ function _generateKeyCalculationJava(entity, rootEntity, ref, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
549
541
  // extract keys for entity (entity.keys is undefined)
550
542
  let keys = utils.extractKeys(entity.keys);
551
543
  const entityKeyExp = keys.map((k) => `${ref}.getString("${k}")`).join(' + "||" + ');
552
544
 
553
545
  const objectIDs = utils.getObjectIDs(entity, model);
554
- const objectIDBlock = _generateObjectIDCalculation(objectIDs, entity, ref);
546
+ const objectIDBlock = _generateObjectIDCalculation(objectIDs, entity, ref, model);
555
547
 
556
548
  // Add parent key calculation for composition parent linking
557
549
  let parentKeyBlock = '';
@@ -589,7 +581,7 @@ function _generateKeyCalculationJava(entity, rootEntity, ref, rootObjectIDs, com
589
581
  }
590
582
 
591
583
  // Compute parent objectID (the display name of the composition parent entity)
592
- const parentObjectIDCalcBlock = _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, ref, entity);
584
+ const parentObjectIDCalcBlock = _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, ref, entity, model);
593
585
  parentObjectIDBlock = parentObjectIDCalcBlock;
594
586
 
595
587
  if (grandParentCompositionInfo && !parentKeyBinding.type) {
@@ -683,9 +675,9 @@ function _prepareValueExpression(col, rowVar) {
683
675
  }
684
676
 
685
677
  // Returns label expression for a column
686
- function _prepareLabelExpression(col, rowVar) {
678
+ function _prepareLabelExpression(col, rowVar, model) {
687
679
  if (col.target && col.alt) {
688
- const { sql, bindings } = handleAssocLookup(col, rowVar);
680
+ const { sql, bindings } = handleAssocLookup(col, rowVar, model);
689
681
  return { sqlExpr: sql, bindings: bindings };
690
682
  }
691
683
  // No label for scalars or associations without @changelog override
@@ -704,7 +696,7 @@ function _wrapInTryCatch(sql, bindings) {
704
696
  }`;
705
697
  }
706
698
 
707
- function _generateObjectIDCalculation(objectIDs, entity, refRow) {
699
+ function _generateObjectIDCalculation(objectIDs, entity, refRow, model) {
708
700
  // fallback to entity name (will be translated via i18nKeys in ChangeView)
709
701
  if (!objectIDs || objectIDs.length === 0) {
710
702
  return `String objectID = "${entity.name}";`;
@@ -717,7 +709,7 @@ function _generateObjectIDCalculation(objectIDs, entity, refRow) {
717
709
 
718
710
  for (const oid of objectIDs) {
719
711
  if (oid.included) {
720
- parts.push(`SELECT CAST(? AS VARCHAR) AS val`);
712
+ parts.push(`SELECT COALESCE(CAST(? AS VARCHAR), '<empty>') AS val`);
721
713
  bindings.push(`${refRow}.getString("${oid.name}")`);
722
714
  } else {
723
715
  // Sub-select needed (Lookup)
@@ -727,7 +719,7 @@ function _generateObjectIDCalculation(objectIDs, entity, refRow) {
727
719
  }, {});
728
720
 
729
721
  const query = SELECT.one.from(entity.name).columns(oid.name).where(where);
730
- const sql = `(${_toSQL(query)})`;
722
+ const sql = `(${_toSQL(query, model)})`;
731
723
 
732
724
  parts.push(`SELECT CAST(${sql} AS VARCHAR) AS val`);
733
725
 
@@ -761,7 +753,7 @@ function _generateObjectIDCalculation(objectIDs, entity, refRow) {
761
753
  * This is used when a child entity has a tracked composition parent — the parent's
762
754
  * changelog entry needs a meaningful objectID rather than just the key.
763
755
  */
764
- function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, childEntity) {
756
+ function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, childEntity, model) {
765
757
  if (!rootObjectIDs || rootObjectIDs.length === 0) {
766
758
  const rootEntityName = rootEntity ? rootEntity.name : '';
767
759
  return `String parentObjectID = "${rootEntityName}";`;
@@ -798,7 +790,7 @@ function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, c
798
790
 
799
791
  const targetEntity = isCompositionOfOne ? binding.rootEntityName : rootEntity.name;
800
792
  const query = SELECT.one.from(targetEntity).columns(oid.name).where(where);
801
- const sql = `(${_toSQL(query)})`;
793
+ const sql = `(${_toSQL(query, model)})`;
802
794
 
803
795
  parts.push(`SELECT CAST(${sql} AS VARCHAR) AS val`);
804
796
 
@@ -833,8 +825,6 @@ function _generateParentObjectIDCalculation(rootObjectIDs, rootEntity, refRow, c
833
825
  }
834
826
 
835
827
  module.exports = {
836
- setModel,
837
- getModel,
838
828
  _generateJavaMethod,
839
829
  _generateCreateBody,
840
830
  _generateUpdateBody,
@@ -1,10 +1,9 @@
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, _generateJavaMethod, _generateCreateBody, _generateUpdateBody, _generateDeleteBody, _generateDeleteBodyPreserve } = require('./java-codegen.js');
4
+ const { _generateJavaMethod, _generateCreateBody, _generateUpdateBody, _generateDeleteBody, _generateDeleteBodyPreserve } = require('./java-codegen.js');
5
5
 
6
6
  function generateH2Trigger(csn, entity, rootEntity, mergedAnnotations = null, rootMergedAnnotations = null, grandParentContext = {}) {
7
- setModel(csn);
8
7
  const { columns: trackedColumns } = utils.extractTrackedColumns(entity, csn, mergedAnnotations);
9
8
  const objectIDs = utils.getObjectIDs(entity, csn, mergedAnnotations?.entityAnnotation);
10
9
  const rootObjectIDs = utils.getObjectIDs(rootEntity, csn, rootMergedAnnotations?.entityAnnotation);
@@ -21,13 +20,13 @@ function generateH2Trigger(csn, entity, rootEntity, mergedAnnotations = null, ro
21
20
  if (!shouldGenerateTriggers) return null;
22
21
 
23
22
  // Generate the Java code for each section
24
- const createBody = !config?.disableCreateTracking ? _generateCreateBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo) : '';
25
- const updateBody = !config?.disableUpdateTracking ? _generateUpdateBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo) : '';
23
+ const createBody = !config?.disableCreateTracking ? _generateCreateBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo) : '';
24
+ const updateBody = !config?.disableUpdateTracking ? _generateUpdateBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo) : '';
26
25
  let deleteBody = '';
27
26
  if (!config?.disableDeleteTracking) {
28
27
  deleteBody = config?.preserveDeletes
29
- ? _generateDeleteBodyPreserve(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo)
30
- : _generateDeleteBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, compositionParentInfo, grandParentCompositionInfo);
28
+ ? _generateDeleteBodyPreserve(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo)
29
+ : _generateDeleteBody(entity, trackedColumns, objectIDs, rootEntity, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo);
31
30
  }
32
31
 
33
32
  // Define the full Create Trigger SQL
@@ -1,14 +1,16 @@
1
1
  const utils = require('../utils/change-tracking.js');
2
- const { getModel, toSQL, compositeKeyExpr, buildGrandParentObjectIDExpr } = require('./sql-expressions.js');
2
+ const { toSQL, compositeKeyExpr, buildGrandParentObjectIDExpr } = require('./sql-expressions.js');
3
3
 
4
4
  /**
5
5
  * Builds rootObjectID select for composition of many
6
6
  */
7
- function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, refRow) {
8
- if (!rootObjectIDs || rootObjectIDs.length === 0) return `'${rootEntity.name}'`;
7
+ function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, refRow, model) {
8
+ const rootEntityKeyExpr = compositeKeyExpr(binding.map((k) => `:${refRow}.${k}`));
9
+
10
+ if (!rootObjectIDs || rootObjectIDs.length === 0) return rootEntityKeyExpr;
9
11
 
10
12
  const rootKeys = utils.extractKeys(rootEntity.keys);
11
- if (rootKeys.length !== binding.length) return `'${rootEntity.name}'`;
13
+ if (rootKeys.length !== binding.length) return rootEntityKeyExpr;
12
14
 
13
15
  const where = {};
14
16
  for (let i = 0; i < rootKeys.length; i++) {
@@ -18,11 +20,10 @@ function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, r
18
20
  const parts = [];
19
21
  for (const oid of rootObjectIDs) {
20
22
  const query = SELECT.one.from(rootEntity.name).columns(oid.name).where(where);
21
- parts.push(`COALESCE(TO_NVARCHAR((${toSQL(query)})), '')`);
23
+ parts.push(`COALESCE(TO_NVARCHAR((${toSQL(query, model)})), '')`);
22
24
  }
23
25
 
24
26
  const concatLogic = parts.join(" || ', ' || ");
25
- const rootEntityKeyExpr = compositeKeyExpr(binding.map((k) => `:${refRow}.${k}`));
26
27
 
27
28
  return `COALESCE(NULLIF(${concatLogic}, ''), ${rootEntityKeyExpr})`;
28
29
  }
@@ -32,8 +33,7 @@ function buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, binding, r
32
33
  * In composition of one, the parent entity has FK to the child (e.g., BookStores.registry_ID -> BookStoreRegistry.ID)
33
34
  * So we need to do a reverse lookup: find the parent record that has FK pointing to this child.
34
35
  */
35
- function buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef) {
36
- const model = getModel();
36
+ function buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, model) {
37
37
  const { parentEntityName, compositionFieldName, parentKeyBinding } = compositionParentInfo;
38
38
  const { compositionName, childKeys } = parentKeyBinding;
39
39
 
@@ -101,23 +101,21 @@ function buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs
101
101
  return { declares, insertSQL, parentEntityName, compositionFieldName, parentKeyExpr };
102
102
  }
103
103
 
104
- function buildCompositionParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, grandParentCompositionInfo = null) {
105
- const model = getModel();
104
+ function buildCompositionParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, model, grandParentCompositionInfo = null) {
106
105
  const { parentEntityName, compositionFieldName, parentKeyBinding } = compositionParentInfo;
107
106
 
108
107
  // Handle composition of one (parent has FK to child - need reverse lookup)
109
108
  if (parentKeyBinding.type === 'compositionOfOne') {
110
- return buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef);
109
+ return buildCompositionOfOneParentContext(compositionParentInfo, rootObjectIDs, modification, rowRef, model);
111
110
  }
112
111
 
113
112
  const parentKeyExpr = compositeKeyExpr(parentKeyBinding.map((k) => `:${rowRef}.${k}`));
114
113
 
115
114
  // Build rootObjectID expression for the parent entity
116
115
  const rootEntity = model.definitions[parentEntityName];
117
- const rootObjectIDExpr = buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, parentKeyBinding, rowRef);
116
+ const rootObjectIDExpr = buildCompOfManyRootObjectIDSelect(rootEntity, rootObjectIDs, parentKeyBinding, rowRef, model);
118
117
 
119
- let declares = 'DECLARE parent_id NVARCHAR(36);';
120
- let insertSQL;
118
+ let declares, insertSQL;
121
119
 
122
120
  if (grandParentCompositionInfo) {
123
121
  // When we have grandparent info, we need to:
@@ -134,7 +132,7 @@ function buildCompositionParentContext(compositionParentInfo, rootObjectIDs, mod
134
132
  // Build grandparent objectID expression
135
133
  const grandParentEntity = model.definitions[grandParentEntityName];
136
134
  const grandParentObjectIDs = utils.getObjectIDs(grandParentEntity, model);
137
- const grandParentObjectIDExpr = grandParentObjectIDs?.length > 0 ? buildGrandParentObjectIDExpr(grandParentObjectIDs, grandParentEntity, parentEntityName, parentKeyBinding, grandParentKeyBinding, rowRef) : grandParentKeyExpr;
135
+ const grandParentObjectIDExpr = grandParentObjectIDs?.length > 0 ? buildGrandParentObjectIDExpr(grandParentObjectIDs, grandParentEntity, parentEntityName, parentKeyBinding, grandParentKeyBinding, rowRef, model) : grandParentKeyExpr;
138
136
 
139
137
  // Add grandparent_id to declares
140
138
  declares = 'DECLARE parent_id NVARCHAR(36);\n\tDECLARE grandparent_id NVARCHAR(36);';
@@ -1,7 +1,7 @@
1
1
  const cds = require('@sap/cds');
2
- const { fs } = cds.utils;
3
2
 
4
- const { prepareCSNForTriggers, generateTriggersForEntities, writeLabelsCSV, ensureUndeployJsonHasTriggerPattern } = require('../utils/trigger-utils.js');
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,17 +10,36 @@ 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 = [...ret.definitions, ...triggers];
42
+ ret.definitions = ret.definitions.concat(triggers).concat(data);
24
43
  return ret;
25
44
  };
26
45
  }