@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
package/index.cds
CHANGED
|
@@ -1,107 +1,218 @@
|
|
|
1
|
-
using {
|
|
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
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
15
|
-
changes
|
|
16
|
-
|
|
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
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
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
|
-
|
|
38
|
-
|
|
39
|
-
|
|
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
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
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
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
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
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
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
|
-
|
|
66
|
-
|
|
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
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
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
|
-
|
|
78
|
-
|
|
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
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
{
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
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
|
-
|
|
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 };
|