@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
package/index.cds CHANGED
@@ -1,107 +1,218 @@
1
- using { managed, cuid } from '@sap/cds/common';
1
+ using {
2
+ managed,
3
+ cuid
4
+ } from '@sap/cds/common';
5
+
2
6
  namespace sap.changelog;
3
7
 
4
8
  /**
5
9
  * Used in cds-plugin.js as template for tracked entities
6
10
  */
7
- @cds.persistence.skip entity aspect @(UI.Facets: [{
8
- $Type : 'UI.ReferenceFacet',
9
- ID : 'ChangeHistoryFacet',
10
- Label : '{i18n>ChangeHistory}',
11
- Target: 'changes/@UI.PresentationVariant',
12
- ![@UI.PartOfPreview]: false
11
+ @cds.persistence.skip
12
+ entity aspect @(UI.Facets: [{
13
+ $Type : 'UI.ReferenceFacet',
14
+ ID : 'ChangeHistoryFacet',
15
+ Label : '{i18n>ChangeHistory}',
16
+ Target : 'changes/@UI.PresentationVariant',
17
+ @UI.PartOfPreview: false
13
18
  }]) {
14
- // Essentially: Association to many Changes on changes.changeLog.entityKey = ID;
15
- changes : Association to many ChangeView on changes.entityKey = ID;
16
- key ID : String;
19
+ changes : Association to many ChangeView
20
+ on changes.entityKey = ID
21
+ and changes.entity = 'ENTITY';
22
+ key ID : String;
17
23
  }
18
24
 
19
25
 
20
26
  // This is a helper view to flatten the assoc path to the entityKey
27
+ // Locale fallback: tries exact locale first (e.g., en_GB), then falls back to 'en'
28
+ // REVISIT: When dropping CDS 8 support, use base locale extraction instead of hardcoded 'en' fallback:
29
+ // substring($user.locale, 0, (case when indexof($user.locale, '_') >= 0 then indexof($user.locale, '_') else length($user.locale) end))
30
+ // This would extract 'en' from 'en_GB', 'de' from 'de_DE', etc. (indexof was introduced in CDS 9)
21
31
  @readonly
32
+ @cds.autoexpose
22
33
  view ChangeView as
23
- select from Changes {
24
- *,
25
- entityID as objectID, // no clue why we have to rename this?
26
- parentEntityID as parentObjectID, // no clue why we have to rename this?
27
- changeLog.entityKey as entityKey, // flattening assoc path -> this is the main reason for having this helper view
28
- changeLog.createdAt as createdAt,
29
- changeLog.createdBy as createdBy,
30
- }
31
- excluding {
32
- entityID,
33
- parentEntityID,
34
+ select from Changes as change
35
+ left outer join i18nKeys as attributeI18n
36
+ on attributeI18n.ID = change.attribute
37
+ and (
38
+ attributeI18n.locale = $user.locale
39
+ or attributeI18n.locale = 'en'
40
+ )
41
+ left outer join i18nKeys as entityI18n
42
+ on entityI18n.ID = change.entity
43
+ and (
44
+ entityI18n.locale = $user.locale
45
+ or entityI18n.locale = 'en'
46
+ )
47
+ left outer join i18nKeys as modificationI18n
48
+ on modificationI18n.ID = change.modification
49
+ and (
50
+ modificationI18n.locale = $user.locale
51
+ or modificationI18n.locale = 'en'
52
+ )
53
+ {
54
+ key change.ID @UI.Hidden,
55
+ change.parent: redirected to ChangeView,
56
+ change.children: redirected to ChangeView,
57
+ change.attribute,
58
+ change.valueChangedFrom,
59
+ change.valueChangedTo,
60
+ change.entity,
61
+ change.entityKey,
62
+ change.objectID,
63
+ change.modification,
64
+ change.valueDataType,
65
+ change.createdAt,
66
+ change.createdBy,
67
+ change.transactionID,
68
+ COALESCE(
69
+ attributeI18n.text, change.attribute
70
+ ) as attributeLabel : String(15) @title: '{i18n>Changes.attribute}',
71
+ COALESCE(
72
+ entityI18n.text, change.entity
73
+ ) as entityLabel : String(24) @title: '{i18n>Changes.entity}',
74
+ COALESCE(
75
+ modificationI18n.text, change.modification
76
+ ) as modificationLabel : String(16) @title: '{i18n>Changes.modification}',
77
+ COALESCE(
78
+ change.valueChangedFromLabel, change.valueChangedFrom
79
+ ) as valueChangedFromLabel : String(5000) @title: '{i18n>Changes.valueChangedFrom}',
80
+ COALESCE(
81
+ change.valueChangedToLabel, change.valueChangedTo
82
+ ) as valueChangedToLabel : String(5000) @title: '{i18n>Changes.valueChangedTo}',
83
+ // For the hierarchy
84
+ null as LimitedDescendantCount : Int16 @UI.Hidden,
85
+ null as DistanceFromRoot : Int16 @UI.Hidden,
86
+ null as DrillState : String @UI.Hidden,
87
+ null as LimitedRank : Int16 @UI.Hidden,
34
88
  };
35
89
 
36
- /**
37
- * Top-level changes entity, e.g. UPDATE Incident by, at, ...
38
- */
39
- entity ChangeLog : managed, cuid {
40
- serviceEntity : String(5000) @title: '{i18n>ChangeLog.serviceEntity}'; // definition name of target entity (on service level) - e.g. ProcessorsService.Incidents
41
- entity : String(5000) @title: '{i18n>ChangeLog.entity}'; // definition name of target entity (on db level) - e.g. sap.capire.incidents.Incidents
42
- entityKey : String @title: '{i18n>ChangeLog.entityKey}'; // primary key of target entity, e.g. Incidents.ID
43
- createdAt : managed:createdAt @title : '{i18n>ChangeLog.createdAt}';
44
- createdBy : managed:createdBy @title : '{i18n>ChangeLog.createdBy}';
45
- changes : Composition of many Changes on changes.changeLog = $self;
90
+ entity i18nKeys {
91
+ key ID : String(5000);
92
+ key locale : String(100);
93
+ text : String(5000);
46
94
  }
47
95
 
48
- /**
49
- * Attribute-level Changes with simple capturing of one-level
50
- * composition trees in parent... elements.
51
- */
52
- entity Changes {
96
+ // Dummy table necessary for HANA triggers
97
+ @cds.persistence.skip
98
+ entity CHANGE_TRACKING_DUMMY {
99
+ key X : String(5);
100
+ }
53
101
 
54
- key ID : UUID @UI.Hidden;
55
- keys : String(5000) @title: '{i18n>Changes.keys}';
56
- attribute : String(5000) @title: '{i18n>Changes.attribute}';
57
- valueChangedFrom : String(5000) @title: '{i18n>Changes.valueChangedFrom}' @UI.MultiLineText;
58
- valueChangedTo : String(5000) @title: '{i18n>Changes.valueChangedTo}' @UI.MultiLineText;
102
+ entity Changes : cuid {
103
+ parent : Association to one Changes;
104
+ children : Composition of many Changes
105
+ on children.parent = $self;
59
106
 
60
- // Business meaningful object id
61
- entityID : String(5000) @title: '{i18n>Changes.entityID}';
62
- entity : String(5000) @title: '{i18n>Changes.entity}'; // similar to ChangeLog.entity, but could be nested entity in a composition tree
63
- serviceEntity : String(5000) @title: '{i18n>Changes.serviceEntity}'; // similar to ChangeLog.serviceEntity, but could be nested entity in a composition tree
107
+ attribute : String(127) @title: '{i18n>Changes.attribute}';
108
+ valueChangedFrom : String(5000) @title: '{i18n>Changes.valueChangedFrom}' @UI.MultiLineText;
109
+ valueChangedTo : String(5000) @title: '{i18n>Changes.valueChangedTo}' @UI.MultiLineText;
110
+ valueChangedFromLabel : String(5000) @title: '{i18n>Changes.valueChangedFrom}';
111
+ valueChangedToLabel : String(5000) @title: '{i18n>Changes.valueChangedTo}';
64
112
 
65
- // Business meaningful parent object id
66
- parentEntityID : String(5000) @title: '{i18n>Changes.parentEntityID}';
67
- parentKey : String @title: '{i18n>Changes.parentKey}';
68
- serviceEntityPath : String(5000) @title: '{i18n>Changes.serviceEntityPath}';
113
+ entity : String(150) @UI.Hidden; // target entity on db level
114
+ entityKey : String(5000) @title: '{i18n>Changes.entityKey}'; // primary key of target entity
69
115
 
70
- @title: '{i18n>Changes.modification}'
71
- modification : String enum {
72
- Create = 'create';
73
- Update = 'update';
74
- Delete = 'delete';
75
- };
116
+ // Business meaningful object id
117
+ objectID : String(5000) @title: '{i18n>Changes.objectID}';
118
+
119
+ @title: '{i18n>Changes.modification}'
120
+ modification : String(6) enum {
121
+ Create = 'create';
122
+ Update = 'update';
123
+ Delete = 'delete';
124
+ };
76
125
 
77
- valueDataType : String(5000) @title: '{i18n>Changes.valueDataType}';
78
- changeLog : Association to ChangeLog @title: '{i18n>ChangeLog.ID}' @UI.Hidden;
126
+ valueDataType : String(5000) @title: '{i18n>Changes.valueDataType}' @UI.Hidden;
127
+ createdAt : managed:createdAt @title: '{i18n>Changes.createdAt}';
128
+ createdBy : managed:createdBy @title: '{i18n>Changes.createdBy}';
129
+ transactionID : Int64 @title: '{i18n>Changes.transactionID}';
79
130
  }
80
131
 
81
132
  annotate ChangeView with @(UI: {
82
- PresentationVariant: {
133
+ PresentationVariant #ChangeHierarchy: {RecursiveHierarchyQualifier: 'ChangeHierarchy',
134
+ },
135
+ PresentationVariant : {
83
136
  Visualizations: ['@UI.LineItem'],
84
- RequestAtLeast: [
85
- parentKey,
86
- serviceEntity,
87
- serviceEntityPath,
88
- valueDataType
89
- ],
90
137
  SortOrder : [{
91
138
  Property : createdAt,
92
139
  Descending: true
93
140
  }],
94
141
  },
95
- LineItem : [
96
- { Value: modification, @HTML5.CssDefaults: {width:'9%'} },
97
- { Value: createdAt, @HTML5.CssDefaults: {width:'12%'} },
98
- { Value: createdBy, @HTML5.CssDefaults: {width:'9%'} },
99
- { Value: entity, @HTML5.CssDefaults: {width:'11%'} },
100
- { Value: objectID, @HTML5.CssDefaults: {width:'14%'} },
101
- { Value: attribute, @HTML5.CssDefaults: {width:'9%'} },
102
- { Value: valueChangedTo, @HTML5.CssDefaults: {width:'11%'} },
103
- { Value: valueChangedFrom, @HTML5.CssDefaults: {width:'11%'} },
104
- { Value: parentObjectID, @HTML5.CssDefaults: {width:'14%'}, ![@UI.Hidden]: true }
142
+ HeaderInfo : {
143
+ $Type : 'UI.HeaderInfoType',
144
+ TypeName : '{i18n>ChangeHistory}',
145
+ TypeNamePlural: '{i18n>ChangeHistory}',
146
+ },
147
+ LineItem : [
148
+ {
149
+ Value : modificationLabel,
150
+ @UI.Importance: #Low
151
+ },
152
+ {
153
+ Value : entityLabel,
154
+ @UI.Importance: #Medium
155
+ },
156
+ {
157
+ Value : objectID,
158
+ @UI.Importance: #Medium
159
+ },
160
+ {
161
+ Value : attributeLabel,
162
+ @UI.Importance: #Medium
163
+ },
164
+ {
165
+ Value : valueChangedToLabel,
166
+ @UI.Importance: #High
167
+ },
168
+ {
169
+ Value : valueChangedFromLabel,
170
+ @UI.Importance: #High
171
+ },
172
+ {
173
+ Value : createdAt,
174
+ @UI.Importance: #Low
175
+ },
176
+ {
177
+ Value : createdBy,
178
+ @UI.Importance: #High
179
+ },
180
+ ],
181
+ DeleteHidden : true,
182
+ }) {
183
+ valueChangedFrom @UI.Hidden;
184
+ valueChangedTo @UI.Hidden;
185
+ parent @UI.Hidden;
186
+ entityKey @UI.Hidden;
187
+ entity @UI.Hidden;
188
+ attribute @UI.Hidden;
189
+ };
190
+
191
+ annotate ChangeView with @(
192
+ Aggregation.RecursiveHierarchy #ChangeHierarchy : {
193
+ ParentNavigationProperty: parent, // navigates to a node's parent
194
+ NodeProperty : ID, // identifies a node, usually the key
195
+ },
196
+ Hierarchy.RecursiveHierarchyActions #ChangeHierarchy : {ChangeSiblingForRootsSupported: false,
197
+ },
198
+ Hierarchy.RecursiveHierarchy #ChangeHierarchy : {
199
+ LimitedDescendantCount: LimitedDescendantCount,
200
+ DistanceFromRoot : DistanceFromRoot,
201
+ DrillState : DrillState,
202
+ LimitedRank : LimitedRank
203
+ },
204
+ // Disallow filtering on these properties from Fiori UIs
205
+ Capabilities.FilterRestrictions.NonFilterableProperties: [
206
+ 'LimitedDescendantCount',
207
+ 'DistanceFromRoot',
208
+ 'DrillState',
209
+ 'LimitedRank'
210
+ ],
211
+ // Disallow sorting on these properties from Fiori UIs
212
+ Capabilities.SortRestrictions.NonSortableProperties : [
213
+ 'LimitedDescendantCount',
214
+ 'DistanceFromRoot',
215
+ 'DrillState',
216
+ 'LimitedRank'
105
217
  ],
106
- DeleteHidden : true,
107
- });
218
+ );
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Custom CQN2SQL implementation that handles trigger-specific columns (:old.*, :new.*)
3
+ * without wrapping them in quotes.
4
+ *
5
+ * Overrides the `val` method to handle trigger row references:
6
+ * - HANA: :old.column, :new.column
7
+ * - Postgres: OLD.column, NEW.column, rec.column
8
+ * - SQLite: old.column, new.column
9
+ *
10
+ * Also provides a dummy table definition for SAP_CHANGELOG_CHANGE_TRACKING_DUMMY
11
+ * to allow generating WHERE conditions without model validation.
12
+ */
13
+
14
+ function createTriggerCQN2SQL(BaseCQN2SQL) {
15
+ return class TriggerCQN2SQL extends BaseCQN2SQL {
16
+ val(x) {
17
+ // Check if this is a trigger row reference
18
+ if (x && x.val && typeof x.val === 'string') {
19
+ const triggerRefPattern = /^:?(?:old|new|OLD|NEW|rec)\.\w+$/;
20
+ if (triggerRefPattern.test(x.val)) {
21
+ return x.val;
22
+ }
23
+ }
24
+
25
+ // Fall back to the base implementation for all other cases
26
+ return super.val(x);
27
+ }
28
+
29
+ from(q) {
30
+ // Check if this is our dummy table
31
+ if (q && q.ref && q.ref[0] === 'SAP_CHANGELOG_CHANGE_TRACKING_DUMMY') {
32
+ // Return a minimal FROM clause without validation
33
+ return 'SAP_CHANGELOG_CHANGE_TRACKING_DUMMY';
34
+ }
35
+
36
+ // Fall back to the base implementation
37
+ return super.from(q);
38
+ }
39
+ };
40
+ }
41
+
42
+ module.exports = { createTriggerCQN2SQL };