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

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 +36 -1
  2. package/README.md +45 -71
  3. package/_i18n/i18n.properties +12 -20
  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 +17 -260
  41. package/index.cds +199 -75
  42. package/lib/TriggerCQN2SQL.js +42 -0
  43. package/lib/h2/java-codegen.js +843 -0
  44. package/lib/h2/register.js +27 -0
  45. package/lib/h2/triggers.js +42 -0
  46. package/lib/hana/composition.js +250 -0
  47. package/lib/hana/register.js +28 -0
  48. package/lib/hana/sql-expressions.js +224 -0
  49. package/lib/hana/triggers.js +254 -0
  50. package/lib/localization.js +59 -115
  51. package/lib/model-enhancer.js +266 -0
  52. package/lib/postgres/composition.js +191 -0
  53. package/lib/postgres/register.js +44 -0
  54. package/lib/postgres/sql-expressions.js +271 -0
  55. package/lib/postgres/triggers.js +115 -0
  56. package/lib/skipHandlers.js +34 -0
  57. package/lib/sqlite/composition.js +235 -0
  58. package/lib/sqlite/register.js +28 -0
  59. package/lib/sqlite/sql-expressions.js +238 -0
  60. package/lib/sqlite/triggers.js +164 -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 +278 -0
  65. package/lib/utils/trigger-utils.js +94 -0
  66. package/package.json +57 -39
  67. package/lib/change-log.js +0 -575
  68. package/lib/entity-helper.js +0 -216
  69. package/lib/format-options.js +0 -66
  70. package/lib/template-processor.js +0 -115
package/index.cds CHANGED
@@ -1,107 +1,231 @@
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 {
34
+ select from Changes as change {
35
+ key change.ID @UI.Hidden,
24
36
  *,
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,
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
101
  };
35
102
 
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;
103
+ entity i18nKeys {
104
+ key ID : String(5000);
105
+ key locale : String(100);
106
+ text : String(5000);
46
107
  }
47
108
 
48
- /**
49
- * Attribute-level Changes with simple capturing of one-level
50
- * composition trees in parent... elements.
51
- */
52
- entity Changes {
109
+ // Dummy table necessary for HANA triggers
110
+ @cds.persistence.skip
111
+ entity CHANGE_TRACKING_DUMMY {
112
+ key X : String(5);
113
+ }
53
114
 
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;
115
+ entity Changes : cuid {
116
+ parent : Association to one Changes;
117
+ children : Composition of many Changes
118
+ on children.parent = $self;
59
119
 
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
120
+ attribute : String(127) @title: '{i18n>Changes.attribute}';
121
+ valueChangedFrom : String(5000) @title: '{i18n>Changes.valueChangedFrom}' @UI.MultiLineText;
122
+ valueChangedTo : String(5000) @title: '{i18n>Changes.valueChangedTo}' @UI.MultiLineText;
123
+ valueChangedFromLabel : String(5000) @title: '{i18n>Changes.valueChangedFrom}';
124
+ valueChangedToLabel : String(5000) @title: '{i18n>Changes.valueChangedTo}';
64
125
 
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}';
126
+ entity : String(150) @UI.Hidden; // target entity on db level
127
+ entityKey : String(5000) @title: '{i18n>Changes.entityKey}'; // primary key of target entity
69
128
 
70
- @title: '{i18n>Changes.modification}'
71
- modification : String enum {
72
- Create = 'create';
73
- Update = 'update';
74
- Delete = 'delete';
75
- };
129
+ // Business meaningful object id
130
+ objectID : String(5000) @title: '{i18n>Changes.objectID}';
131
+
132
+ @title: '{i18n>Changes.modification}'
133
+ modification : String(6) enum {
134
+ Create = 'create';
135
+ Update = 'update';
136
+ Delete = 'delete';
137
+ };
76
138
 
77
- valueDataType : String(5000) @title: '{i18n>Changes.valueDataType}';
78
- changeLog : Association to ChangeLog @title: '{i18n>ChangeLog.ID}' @UI.Hidden;
139
+ valueDataType : String(5000) @title: '{i18n>Changes.valueDataType}' @UI.Hidden;
140
+ createdAt : managed:createdAt @title: '{i18n>Changes.createdAt}';
141
+ createdBy : managed:createdBy @title: '{i18n>Changes.createdBy}';
142
+ transactionID : Int64 @title: '{i18n>Changes.transactionID}';
79
143
  }
80
144
 
81
145
  annotate ChangeView with @(UI: {
82
- PresentationVariant: {
146
+ PresentationVariant #ChangeHierarchy: {RecursiveHierarchyQualifier: 'ChangeHierarchy',
147
+ },
148
+ PresentationVariant : {
83
149
  Visualizations: ['@UI.LineItem'],
84
- RequestAtLeast: [
85
- parentKey,
86
- serviceEntity,
87
- serviceEntityPath,
88
- valueDataType
89
- ],
90
150
  SortOrder : [{
91
151
  Property : createdAt,
92
152
  Descending: true
93
153
  }],
94
154
  },
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 }
155
+ HeaderInfo : {
156
+ $Type : 'UI.HeaderInfoType',
157
+ TypeName : '{i18n>ChangeHistory}',
158
+ TypeNamePlural: '{i18n>ChangeHistory}',
159
+ },
160
+ LineItem : [
161
+ {
162
+ Value : modificationLabel,
163
+ @UI.Importance : #Low
164
+ },
165
+ {
166
+ Value : entityLabel,
167
+ @UI.Importance : #Medium
168
+ },
169
+ {
170
+ Value : objectID,
171
+ @UI.Importance : #Medium
172
+ },
173
+ {
174
+ Value : attributeLabel,
175
+ @UI.Importance : #Medium
176
+ },
177
+ {
178
+ Value : valueChangedToLabel,
179
+ @UI.Importance : #High
180
+ },
181
+ {
182
+ Value : valueChangedFromLabel,
183
+ @UI.Importance : #High
184
+ },
185
+ {
186
+ Value : createdAt,
187
+ @UI.Importance : #Low
188
+ },
189
+ {
190
+ Value : createdBy,
191
+ @UI.Importance : #High
192
+ },
193
+ ],
194
+ DeleteHidden : true,
195
+ }) {
196
+ valueChangedFrom @UI.Hidden;
197
+ valueChangedTo @UI.Hidden;
198
+ parent @UI.Hidden;
199
+ entityKey @UI.Hidden;
200
+ entity @UI.Hidden;
201
+ attribute @UI.Hidden;
202
+ };
203
+
204
+ annotate ChangeView with @(
205
+ Aggregation.RecursiveHierarchy #ChangeHierarchy : {
206
+ ParentNavigationProperty: parent, // navigates to a node's parent
207
+ NodeProperty : ID, // identifies a node, usually the key
208
+ },
209
+ Hierarchy.RecursiveHierarchyActions #ChangeHierarchy : {ChangeSiblingForRootsSupported: false,
210
+ },
211
+ Hierarchy.RecursiveHierarchy #ChangeHierarchy : {
212
+ LimitedDescendantCount: LimitedDescendantCount,
213
+ DistanceFromRoot : DistanceFromRoot,
214
+ DrillState : DrillState,
215
+ LimitedRank : LimitedRank
216
+ },
217
+ // Disallow filtering on these properties from Fiori UIs
218
+ Capabilities.FilterRestrictions.NonFilterableProperties: [
219
+ 'LimitedDescendantCount',
220
+ 'DistanceFromRoot',
221
+ 'DrillState',
222
+ 'LimitedRank'
223
+ ],
224
+ // Disallow sorting on these properties from Fiori UIs
225
+ Capabilities.SortRestrictions.NonSortableProperties : [
226
+ 'LimitedDescendantCount',
227
+ 'DistanceFromRoot',
228
+ 'DrillState',
229
+ 'LimitedRank'
105
230
  ],
106
- DeleteHidden : true,
107
- });
231
+ );
@@ -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 };