@cap-js/change-tracking 2.0.0-beta.3 → 2.0.0-beta.4
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 +7 -1
- package/cds-plugin.js +1 -1
- package/index.cds +277 -29
- package/lib/csn-enhancements/annotations.js +77 -0
- package/lib/{model-enhancer.js → csn-enhancements/index.js} +7 -70
- package/lib/csn-enhancements/timezoneProperties.js +81 -0
- package/lib/hana/register.js +10 -0
- package/lib/postgres/register.js +12 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -4,7 +4,7 @@ All notable changes to this project will be documented in this file.
|
|
|
4
4
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
|
5
5
|
The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
6
6
|
|
|
7
|
-
## Version 2.0.0-beta.
|
|
7
|
+
## Version 2.0.0-beta.5 - tbd
|
|
8
8
|
|
|
9
9
|
### Added
|
|
10
10
|
|
|
@@ -12,6 +12,12 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/).
|
|
|
12
12
|
|
|
13
13
|
### Changed
|
|
14
14
|
|
|
15
|
+
## Version 2.0.0-beta.4 - 16.03.26
|
|
16
|
+
|
|
17
|
+
### Added
|
|
18
|
+
|
|
19
|
+
- Tracked Date, DateTime, Time and Timestamp properties are now correctly formatted again.
|
|
20
|
+
- If a tracked property is annotated with `@Common.Timezone` the changelog now considers the Timezone as well.
|
|
15
21
|
|
|
16
22
|
## Version 2.0.0-beta.3 - 13.03.26
|
|
17
23
|
|
package/cds-plugin.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
const cds = require('@sap/cds');
|
|
2
2
|
|
|
3
|
-
const { enhanceModel } = require('./lib/
|
|
3
|
+
const { enhanceModel } = require('./lib/csn-enhancements');
|
|
4
4
|
const { registerSessionVariableHandlers } = require('./lib/skipHandlers.js');
|
|
5
5
|
const { deploySQLiteTriggers } = require('./lib/sqlite/register.js');
|
|
6
6
|
const { registerPostgresCompilerHook, deployPostgresLabels } = require('./lib/postgres/register.js');
|
package/index.cds
CHANGED
|
@@ -42,9 +42,9 @@ view ChangeView as
|
|
|
42
42
|
on modificationI18n.ID = change.modification
|
|
43
43
|
and modificationI18n.locale = $user.locale
|
|
44
44
|
{
|
|
45
|
-
key change.ID
|
|
46
|
-
change.parent
|
|
47
|
-
change.children
|
|
45
|
+
key change.ID @UI.Hidden,
|
|
46
|
+
change.parent : redirected to ChangeView,
|
|
47
|
+
change.children : redirected to ChangeView,
|
|
48
48
|
change.attribute,
|
|
49
49
|
change.valueChangedFrom,
|
|
50
50
|
change.valueChangedTo,
|
|
@@ -63,7 +63,7 @@ view ChangeView as
|
|
|
63
63
|
ID = change.attribute
|
|
64
64
|
and locale = 'en'
|
|
65
65
|
), change.attribute
|
|
66
|
-
) as attributeLabel
|
|
66
|
+
) as attributeLabel : String(15) @title: '{i18n>Changes.attribute}',
|
|
67
67
|
COALESCE(
|
|
68
68
|
entityI18n.text, (
|
|
69
69
|
select text from i18nKeys
|
|
@@ -71,7 +71,7 @@ view ChangeView as
|
|
|
71
71
|
ID = change.entity
|
|
72
72
|
and locale = 'en'
|
|
73
73
|
), change.entity
|
|
74
|
-
) as entityLabel
|
|
74
|
+
) as entityLabel : String(24) @title: '{i18n>Changes.entity}',
|
|
75
75
|
COALESCE(
|
|
76
76
|
modificationI18n.text, (
|
|
77
77
|
select text from i18nKeys
|
|
@@ -79,24 +79,152 @@ view ChangeView as
|
|
|
79
79
|
ID = change.modification
|
|
80
80
|
and locale = 'en'
|
|
81
81
|
), change.modification
|
|
82
|
-
) as modificationLabel
|
|
82
|
+
) as modificationLabel : String(16) @title: '{i18n>Changes.modification}',
|
|
83
83
|
COALESCE(
|
|
84
84
|
change.valueChangedFromLabel, change.valueChangedFrom
|
|
85
|
-
) as valueChangedFromLabel
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
85
|
+
) as valueChangedFromLabel : String(5000) @(
|
|
86
|
+
title: '{i18n>Changes.valueChangedFrom}',
|
|
87
|
+
UI.MultiLineText
|
|
88
|
+
),
|
|
89
|
+
(
|
|
90
|
+
case
|
|
91
|
+
when valueDataType = 'cds.DateTime'
|
|
92
|
+
then COALESCE(
|
|
93
|
+
change.valueChangedFromLabel, change.valueChangedFrom
|
|
94
|
+
)
|
|
95
|
+
else null
|
|
96
|
+
end
|
|
97
|
+
) as valueChangedFromLabelDateTime : DateTime @(title: '{i18n>Changes.valueChangedFrom}',
|
|
98
|
+
),
|
|
99
|
+
(
|
|
100
|
+
case
|
|
101
|
+
when valueDataType = 'cds.DateTime' or valueDataType = 'cds.Timestamp'
|
|
102
|
+
then COALESCE(
|
|
103
|
+
change.valueChangedFromLabel, change.valueChangedFrom
|
|
104
|
+
)
|
|
105
|
+
else null
|
|
106
|
+
end
|
|
107
|
+
) as valueChangedFromLabelDateTimeWTZ : DateTime @(
|
|
108
|
+
title : '{i18n>Changes.valueChangedFrom}',
|
|
109
|
+
Common.Timezone: valueTimeZone
|
|
110
|
+
),
|
|
111
|
+
(
|
|
112
|
+
case
|
|
113
|
+
when valueDataType = 'cds.Time'
|
|
114
|
+
then COALESCE(
|
|
115
|
+
change.valueChangedFromLabel, change.valueChangedFrom
|
|
116
|
+
)
|
|
117
|
+
else null
|
|
118
|
+
end
|
|
119
|
+
) as valueChangedFromLabelTime : Time @(title: '{i18n>Changes.valueChangedFrom}',
|
|
120
|
+
),
|
|
121
|
+
(
|
|
122
|
+
case
|
|
123
|
+
when valueDataType = 'cds.Date'
|
|
124
|
+
then COALESCE(
|
|
125
|
+
change.valueChangedFromLabel, change.valueChangedFrom
|
|
126
|
+
)
|
|
127
|
+
else null
|
|
128
|
+
end
|
|
129
|
+
) as valueChangedFromLabelDate : Date @(title: '{i18n>Changes.valueChangedFrom}',
|
|
130
|
+
),
|
|
131
|
+
(
|
|
132
|
+
case
|
|
133
|
+
when valueDataType = 'cds.Timestamp'
|
|
134
|
+
then COALESCE(
|
|
135
|
+
change.valueChangedFromLabel, change.valueChangedFrom
|
|
136
|
+
)
|
|
137
|
+
else null
|
|
138
|
+
end
|
|
139
|
+
) as valueChangedFromLabelTimestamp : Timestamp @(title: '{i18n>Changes.valueChangedFrom}',
|
|
140
|
+
),
|
|
141
|
+
(
|
|
142
|
+
case
|
|
143
|
+
when valueDataType = 'cds.Decimal'
|
|
144
|
+
then COALESCE(
|
|
145
|
+
change.valueChangedFromLabel, change.valueChangedFrom
|
|
146
|
+
)
|
|
147
|
+
else null
|
|
148
|
+
end
|
|
149
|
+
) as valueChangedFromLabelDecimal : Decimal @(title: '{i18n>Changes.valueChangedFrom}',
|
|
150
|
+
),
|
|
89
151
|
COALESCE(
|
|
90
152
|
change.valueChangedToLabel, change.valueChangedTo
|
|
91
|
-
) as valueChangedToLabel
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
153
|
+
) as valueChangedToLabel : String(5000) @(
|
|
154
|
+
title: '{i18n>Changes.valueChangedTo}',
|
|
155
|
+
UI.MultiLineText
|
|
156
|
+
),
|
|
157
|
+
(
|
|
158
|
+
case
|
|
159
|
+
when valueDataType = 'cds.DateTime'
|
|
160
|
+
then COALESCE(
|
|
161
|
+
change.valueChangedToLabel, change.valueChangedTo
|
|
162
|
+
)
|
|
163
|
+
else null
|
|
164
|
+
end
|
|
165
|
+
) as valueChangedToLabelDateTime : DateTime @(title: '{i18n>Changes.valueChangedTo}',
|
|
166
|
+
),
|
|
167
|
+
(
|
|
168
|
+
case
|
|
169
|
+
when valueDataType = 'cds.DateTime' or valueDataType = 'cds.Timestamp'
|
|
170
|
+
then COALESCE(
|
|
171
|
+
change.valueChangedFromLabel, change.valueChangedTo
|
|
172
|
+
)
|
|
173
|
+
else null
|
|
174
|
+
end
|
|
175
|
+
) as valueChangedToLabelDateTimeWTZ : DateTime @(
|
|
176
|
+
title : '{i18n>Changes.valueChangedTo}',
|
|
177
|
+
Common.Timezone: valueTimeZone
|
|
178
|
+
),
|
|
179
|
+
(
|
|
180
|
+
case
|
|
181
|
+
when valueDataType = 'cds.Time'
|
|
182
|
+
then COALESCE(
|
|
183
|
+
change.valueChangedToLabel, change.valueChangedTo
|
|
184
|
+
)
|
|
185
|
+
else null
|
|
186
|
+
end
|
|
187
|
+
) as valueChangedToLabelTime : Time @(title: '{i18n>Changes.valueChangedTo}',
|
|
188
|
+
),
|
|
189
|
+
(
|
|
190
|
+
case
|
|
191
|
+
when valueDataType = 'cds.Date'
|
|
192
|
+
then COALESCE(
|
|
193
|
+
change.valueChangedToLabel, change.valueChangedTo
|
|
194
|
+
)
|
|
195
|
+
else null
|
|
196
|
+
end
|
|
197
|
+
) as valueChangedToLabelDate : Date @(title: '{i18n>Changes.valueChangedTo}',
|
|
198
|
+
),
|
|
199
|
+
(
|
|
200
|
+
case
|
|
201
|
+
when valueDataType = 'cds.Timestamp'
|
|
202
|
+
then COALESCE(
|
|
203
|
+
change.valueChangedToLabel, change.valueChangedTo
|
|
204
|
+
)
|
|
205
|
+
else null
|
|
206
|
+
end
|
|
207
|
+
) as valueChangedToLabelTimestamp : Timestamp @(title: '{i18n>Changes.valueChangedTo}',
|
|
208
|
+
),
|
|
209
|
+
(
|
|
210
|
+
case
|
|
211
|
+
when valueDataType = 'cds.Decimal'
|
|
212
|
+
then COALESCE(
|
|
213
|
+
change.valueChangedToLabel, change.valueChangedTo
|
|
214
|
+
)
|
|
215
|
+
else null
|
|
216
|
+
end
|
|
217
|
+
) as valueChangedToLabelDecimal : Decimal @(title: '{i18n>Changes.valueChangedTo}',
|
|
218
|
+
),
|
|
219
|
+
null as valueTimeZone : String @(
|
|
220
|
+
UI.Hidden,
|
|
221
|
+
Common.IsTimezone
|
|
222
|
+
),
|
|
95
223
|
// For the hierarchy
|
|
96
|
-
null as LimitedDescendantCount
|
|
97
|
-
null as DistanceFromRoot
|
|
98
|
-
null as DrillState
|
|
99
|
-
null as LimitedRank
|
|
224
|
+
null as LimitedDescendantCount : Int16 @UI.Hidden,
|
|
225
|
+
null as DistanceFromRoot : Int16 @UI.Hidden,
|
|
226
|
+
null as DrillState : String @UI.Hidden,
|
|
227
|
+
null as LimitedRank : Int16 @UI.Hidden,
|
|
100
228
|
};
|
|
101
229
|
|
|
102
230
|
entity i18nKeys {
|
|
@@ -105,7 +233,7 @@ entity i18nKeys {
|
|
|
105
233
|
text : String(5000);
|
|
106
234
|
}
|
|
107
235
|
|
|
108
|
-
// Dummy table necessary for HANA triggers
|
|
236
|
+
// Dummy table necessary for HANA triggers because DUMMY cannot be used in HDI
|
|
109
237
|
@cds.persistence.skip
|
|
110
238
|
entity CHANGE_TRACKING_DUMMY {
|
|
111
239
|
key X : String(5);
|
|
@@ -174,11 +302,15 @@ annotate ChangeView with @(UI: {
|
|
|
174
302
|
@UI.Importance: #Medium
|
|
175
303
|
},
|
|
176
304
|
{
|
|
177
|
-
|
|
305
|
+
$Type : 'UI.DataFieldForAnnotation',
|
|
306
|
+
Target : '@UI.FieldGroup#valueChangedTo',
|
|
307
|
+
Label : '{i18n>Changes.valueChangedTo}',
|
|
178
308
|
@UI.Importance: #High
|
|
179
309
|
},
|
|
180
310
|
{
|
|
181
|
-
|
|
311
|
+
$Type : 'UI.DataFieldForAnnotation',
|
|
312
|
+
Target : '@UI.FieldGroup#valueChangedFrom',
|
|
313
|
+
Label : '{i18n>Changes.valueChangedFrom}',
|
|
182
314
|
@UI.Importance: #High
|
|
183
315
|
},
|
|
184
316
|
{
|
|
@@ -191,13 +323,103 @@ annotate ChangeView with @(UI: {
|
|
|
191
323
|
},
|
|
192
324
|
],
|
|
193
325
|
DeleteHidden : true,
|
|
326
|
+
FieldGroup #valueChangedFrom : {
|
|
327
|
+
Label: '{i18n>Changes.valueChangedFrom}',
|
|
328
|
+
Data : [
|
|
329
|
+
{
|
|
330
|
+
Value : valueChangedFromLabel,
|
|
331
|
+
@UI.Hidden: ($self.valueDataType = 'cds.Decimal'
|
|
332
|
+
or $self.valueDataType = 'cds.DateTime'
|
|
333
|
+
or $self.valueDataType = 'cds.Date'
|
|
334
|
+
or $self.valueDataType = 'cds.Time'
|
|
335
|
+
or $self.valueDataType = 'cds.Timestamp')
|
|
336
|
+
},
|
|
337
|
+
{
|
|
338
|
+
Value : valueChangedFromLabelDateTime,
|
|
339
|
+
@UI.Hidden: ($self.valueDataType != 'cds.DateTime'
|
|
340
|
+
or $self.valueTimeZone != null)
|
|
341
|
+
},
|
|
342
|
+
{
|
|
343
|
+
Value : valueChangedFromLabelDateTimeWTZ,
|
|
344
|
+
@UI.Hidden: ($self.valueDataType != 'cds.DateTime'
|
|
345
|
+
or $self.valueTimeZone = null)
|
|
346
|
+
},
|
|
347
|
+
{
|
|
348
|
+
Value : valueChangedFromLabelDate,
|
|
349
|
+
@UI.Hidden: ($self.valueDataType != 'cds.Date')
|
|
350
|
+
},
|
|
351
|
+
{
|
|
352
|
+
Value : valueChangedFromLabelTime,
|
|
353
|
+
@UI.Hidden: ($self.valueDataType != 'cds.Time')
|
|
354
|
+
},
|
|
355
|
+
{
|
|
356
|
+
Value : valueChangedFromLabelTimestamp,
|
|
357
|
+
@UI.Hidden: ($self.valueDataType != 'cds.Timestamp')
|
|
358
|
+
},
|
|
359
|
+
{
|
|
360
|
+
Value : valueChangedFromLabelDecimal,
|
|
361
|
+
@UI.Hidden: ($self.valueDataType != 'cds.Decimal')
|
|
362
|
+
}
|
|
363
|
+
]
|
|
364
|
+
},
|
|
365
|
+
FieldGroup #valueChangedTo : {
|
|
366
|
+
Label: '{i18n>Changes.valueChangedTo}',
|
|
367
|
+
Data : [
|
|
368
|
+
{
|
|
369
|
+
Value : valueChangedToLabel,
|
|
370
|
+
@UI.Hidden: ($self.valueDataType = 'cds.Decimal'
|
|
371
|
+
or $self.valueDataType = 'cds.DateTime'
|
|
372
|
+
or $self.valueDataType = 'cds.Date'
|
|
373
|
+
or $self.valueDataType = 'cds.Time'
|
|
374
|
+
or $self.valueDataType = 'cds.Timestamp')
|
|
375
|
+
},
|
|
376
|
+
{
|
|
377
|
+
Value : valueChangedToLabelDateTime,
|
|
378
|
+
@UI.Hidden: ($self.valueDataType != 'cds.DateTime'
|
|
379
|
+
or $self.valueTimeZone != null)
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
Value : valueChangedToLabelDateTimeWTZ,
|
|
383
|
+
@UI.Hidden: ($self.valueDataType != 'cds.DateTime'
|
|
384
|
+
or $self.valueTimeZone = null)
|
|
385
|
+
},
|
|
386
|
+
{
|
|
387
|
+
Value : valueChangedToLabelDate,
|
|
388
|
+
@UI.Hidden: ($self.valueDataType != 'cds.Date')
|
|
389
|
+
},
|
|
390
|
+
{
|
|
391
|
+
Value : valueChangedToLabelTime,
|
|
392
|
+
@UI.Hidden: ($self.valueDataType != 'cds.Time')
|
|
393
|
+
},
|
|
394
|
+
{
|
|
395
|
+
Value : valueChangedToLabelTimestamp,
|
|
396
|
+
@UI.Hidden: ($self.valueDataType != 'cds.Timestamp')
|
|
397
|
+
},
|
|
398
|
+
{
|
|
399
|
+
Value : valueChangedToLabelDecimal,
|
|
400
|
+
@UI.Hidden: ($self.valueDataType != 'cds.Decimal')
|
|
401
|
+
}
|
|
402
|
+
]
|
|
403
|
+
}
|
|
194
404
|
}) {
|
|
195
|
-
valueChangedFrom
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
405
|
+
valueChangedFrom @UI.Hidden;
|
|
406
|
+
valueChangedFromLabelDate @UI.AdaptationHidden @UI.Hidden: ($self.valueDataType != 'cds.Date');
|
|
407
|
+
valueChangedFromLabelDateTime @UI.AdaptationHidden @UI.Hidden: ($self.valueDataType != 'cds.DateTime');
|
|
408
|
+
valueChangedFromLabelDateTimeWTZ @UI.AdaptationHidden @UI.Hidden: ($self.valueDataType != 'cds.DateTime');
|
|
409
|
+
valueChangedFromLabelTime @UI.AdaptationHidden @UI.Hidden: ($self.valueDataType != 'cds.Time');
|
|
410
|
+
valueChangedFromLabelTimestamp @UI.AdaptationHidden @UI.Hidden: ($self.valueDataType != 'cds.Timestamp');
|
|
411
|
+
valueChangedFromLabelDecimal @UI.AdaptationHidden @UI.Hidden: ($self.valueDataType != 'cds.Decimal');
|
|
412
|
+
valueChangedTo @UI.Hidden;
|
|
413
|
+
valueChangedToLabelDate @UI.AdaptationHidden @UI.Hidden: ($self.valueDataType != 'cds.Date');
|
|
414
|
+
valueChangedToLabelDateTime @UI.AdaptationHidden @UI.Hidden: ($self.valueDataType != 'cds.DateTime');
|
|
415
|
+
valueChangedToLabelDateTimeWTZ @UI.AdaptationHidden @UI.Hidden: ($self.valueDataType != 'cds.DateTime');
|
|
416
|
+
valueChangedToLabelTime @UI.AdaptationHidden @UI.Hidden: ($self.valueDataType != 'cds.Time');
|
|
417
|
+
valueChangedToLabelTimestamp @UI.AdaptationHidden @UI.Hidden: ($self.valueDataType != 'cds.Timestamp');
|
|
418
|
+
valueChangedToLabelDecimal @UI.AdaptationHidden @UI.Hidden: ($self.valueDataType != 'cds.Decimal');
|
|
419
|
+
parent @UI.Hidden;
|
|
420
|
+
entityKey @UI.Hidden;
|
|
421
|
+
entity @UI.Hidden;
|
|
422
|
+
attribute @UI.Hidden;
|
|
201
423
|
};
|
|
202
424
|
|
|
203
425
|
annotate ChangeView with @(
|
|
@@ -218,14 +440,40 @@ annotate ChangeView with @(
|
|
|
218
440
|
'LimitedDescendantCount',
|
|
219
441
|
'DistanceFromRoot',
|
|
220
442
|
'DrillState',
|
|
221
|
-
'LimitedRank'
|
|
443
|
+
'LimitedRank',
|
|
444
|
+
valueChangedFromLabelDate,
|
|
445
|
+
valueChangedFromLabelDateTime,
|
|
446
|
+
valueChangedFromLabelDateTimeWTZ,
|
|
447
|
+
valueChangedFromLabelTime,
|
|
448
|
+
valueChangedFromLabelTimestamp,
|
|
449
|
+
valueChangedFromLabelDecimal,
|
|
450
|
+
valueChangedToLabelDate,
|
|
451
|
+
valueChangedToLabelDateTime,
|
|
452
|
+
valueChangedToLabelDateTimeWTZ,
|
|
453
|
+
valueChangedToLabelTime,
|
|
454
|
+
valueChangedToLabelTimestamp,
|
|
455
|
+
valueChangedToLabelDecimal,
|
|
456
|
+
valueTimeZone
|
|
222
457
|
],
|
|
223
458
|
// Disallow sorting on these properties from Fiori UIs
|
|
224
459
|
Capabilities.SortRestrictions.NonSortableProperties : [
|
|
225
460
|
'LimitedDescendantCount',
|
|
226
461
|
'DistanceFromRoot',
|
|
227
462
|
'DrillState',
|
|
228
|
-
'LimitedRank'
|
|
463
|
+
'LimitedRank',
|
|
464
|
+
valueChangedFromLabelDate,
|
|
465
|
+
valueChangedFromLabelDateTime,
|
|
466
|
+
valueChangedFromLabelDateTimeWTZ,
|
|
467
|
+
valueChangedFromLabelTime,
|
|
468
|
+
valueChangedFromLabelTimestamp,
|
|
469
|
+
valueChangedFromLabelDecimal,
|
|
470
|
+
valueChangedToLabelDate,
|
|
471
|
+
valueChangedToLabelDateTime,
|
|
472
|
+
valueChangedToLabelDateTimeWTZ,
|
|
473
|
+
valueChangedToLabelTime,
|
|
474
|
+
valueChangedToLabelTimestamp,
|
|
475
|
+
valueChangedToLabelDecimal,
|
|
476
|
+
valueTimeZone
|
|
229
477
|
],
|
|
230
478
|
);
|
|
231
479
|
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
const cds = require('@sap/cds');
|
|
2
|
+
const LOG = cds.log('change-tracking');
|
|
3
|
+
const DEBUG = cds.debug('change-tracking');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Add side effects annotations for actions to refresh the changes association.
|
|
7
|
+
*/
|
|
8
|
+
function addSideEffects(actions, entityName, hierarchyMap, model) {
|
|
9
|
+
const isRootEntity = !hierarchyMap.has(entityName);
|
|
10
|
+
|
|
11
|
+
// If not a root entity, find the parent association name
|
|
12
|
+
let parentAssociationName = null;
|
|
13
|
+
if (!isRootEntity) {
|
|
14
|
+
const parentEntityName = hierarchyMap.get(entityName);
|
|
15
|
+
const parentEntity = model.definitions[parentEntityName];
|
|
16
|
+
if (parentEntity?.elements) {
|
|
17
|
+
// Find the composition element in the parent that points to this entity
|
|
18
|
+
for (const [elemName, elem] of Object.entries(parentEntity.elements)) {
|
|
19
|
+
if (elem.type === 'cds.Composition' && elem.target === entityName) {
|
|
20
|
+
parentAssociationName = elemName;
|
|
21
|
+
break;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
for (const se of Object.values(actions)) {
|
|
28
|
+
const target = isRootEntity ? 'TargetProperties' : 'TargetEntities';
|
|
29
|
+
const sideEffectAttr = se[`@Common.SideEffects.${target}`];
|
|
30
|
+
const property = isRootEntity ? 'changes' : { '=': `${parentAssociationName}.changes` };
|
|
31
|
+
if (sideEffectAttr?.length >= 0) {
|
|
32
|
+
sideEffectAttr.findIndex((item) => (item['='] ? item['='] : item) === (property['='] ? property['='] : property)) === -1 && sideEffectAttr.push(property);
|
|
33
|
+
} else {
|
|
34
|
+
se[`@Common.SideEffects.${target}`] = [property];
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function addUIFacet(entity, m) {
|
|
40
|
+
const { 'sap.changelog.aspect': aspect } = m.definitions;
|
|
41
|
+
const {
|
|
42
|
+
'@UI.Facets': [facet],
|
|
43
|
+
elements: { changes }
|
|
44
|
+
} = aspect;
|
|
45
|
+
if (entity['@changelog.disable_facet'] !== undefined) {
|
|
46
|
+
LOG.warn(
|
|
47
|
+
`@changelog.disable_facet is deprecated! You can just define your own Facet for the changes association or annotate the changes association on ${entity.name} with not readable via @Capabilities.NavigationRestrictions.RestrictedProperties`
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
let facets = entity['@UI.Facets'];
|
|
52
|
+
|
|
53
|
+
if (!facets) {
|
|
54
|
+
DEBUG?.(`${entity.name} does not have a @UI.Facets annotation and thus the change tracking section is not added.`);
|
|
55
|
+
}
|
|
56
|
+
// Add UI.Facet for Change History List
|
|
57
|
+
if (
|
|
58
|
+
facets &&
|
|
59
|
+
!entity['@changelog.disable_facet'] &&
|
|
60
|
+
!hasFacetForComp(changes, entity['@UI.Facets']) &&
|
|
61
|
+
!entity['@Capabilities.NavigationRestrictions.RestrictedProperties']?.some((restriction) => restriction.NavigationProperty?.['='] === 'changes' && restriction.ReadRestrictions?.Readable === false)
|
|
62
|
+
) {
|
|
63
|
+
facets.push(facet);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Check if a facet already exists for the changes composition.
|
|
69
|
+
*/
|
|
70
|
+
function hasFacetForComp(comp, facets) {
|
|
71
|
+
return facets.some((f) => f.Target === `${comp.name}/@UI.LineItem` || (f.Facets && hasFacetForComp(comp, f.Facets)));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
module.exports = {
|
|
75
|
+
addSideEffects,
|
|
76
|
+
addUIFacet
|
|
77
|
+
};
|
|
@@ -1,42 +1,9 @@
|
|
|
1
1
|
const cds = require('@sap/cds');
|
|
2
|
-
const LOG = cds.log('change-tracking');
|
|
3
2
|
const DEBUG = cds.debug('change-tracking');
|
|
4
3
|
|
|
5
|
-
const { isChangeTracked, getBaseEntity, analyzeCompositions, getService } = require('
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
* Add side effects annotations for actions to refresh the changes association.
|
|
9
|
-
*/
|
|
10
|
-
function addSideEffects(actions, entityName, hierarchyMap, model) {
|
|
11
|
-
const isRootEntity = !hierarchyMap.has(entityName);
|
|
12
|
-
|
|
13
|
-
// If not a root entity, find the parent association name
|
|
14
|
-
let parentAssociationName = null;
|
|
15
|
-
if (!isRootEntity) {
|
|
16
|
-
const parentEntityName = hierarchyMap.get(entityName);
|
|
17
|
-
const parentEntity = model.definitions[parentEntityName];
|
|
18
|
-
if (parentEntity?.elements) {
|
|
19
|
-
// Find the composition element in the parent that points to this entity
|
|
20
|
-
for (const [elemName, elem] of Object.entries(parentEntity.elements)) {
|
|
21
|
-
if (elem.type === 'cds.Composition' && elem.target === entityName) {
|
|
22
|
-
parentAssociationName = elemName;
|
|
23
|
-
break;
|
|
24
|
-
}
|
|
25
|
-
}
|
|
26
|
-
}
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
for (const se of Object.values(actions)) {
|
|
30
|
-
const target = isRootEntity ? 'TargetProperties' : 'TargetEntities';
|
|
31
|
-
const sideEffectAttr = se[`@Common.SideEffects.${target}`];
|
|
32
|
-
const property = isRootEntity ? 'changes' : { '=': `${parentAssociationName}.changes` };
|
|
33
|
-
if (sideEffectAttr?.length >= 0) {
|
|
34
|
-
sideEffectAttr.findIndex((item) => (item['='] ? item['='] : item) === (property['='] ? property['='] : property)) === -1 && sideEffectAttr.push(property);
|
|
35
|
-
} else {
|
|
36
|
-
se[`@Common.SideEffects.${target}`] = [property];
|
|
37
|
-
}
|
|
38
|
-
}
|
|
39
|
-
}
|
|
4
|
+
const { isChangeTracked, getBaseEntity, analyzeCompositions, getService } = require('../utils/entity-collector.js');
|
|
5
|
+
const { addSideEffects, addUIFacet } = require('./annotations.js');
|
|
6
|
+
const { enhanceChangeViewWithTimeZones } = require('./timezoneProperties.js');
|
|
40
7
|
|
|
41
8
|
/**
|
|
42
9
|
* Returns a CQN expression for the composite key of an entity.
|
|
@@ -82,13 +49,6 @@ function _replaceTablePlaceholders(on, tableName) {
|
|
|
82
49
|
});
|
|
83
50
|
}
|
|
84
51
|
|
|
85
|
-
/**
|
|
86
|
-
* Check if a facet already exists for the changes composition.
|
|
87
|
-
*/
|
|
88
|
-
function hasFacetForComp(comp, facets) {
|
|
89
|
-
return facets.some((f) => f.Target === `${comp.name}/@UI.LineItem` || (f.Facets && hasFacetForComp(comp, f.Facets)));
|
|
90
|
-
}
|
|
91
|
-
|
|
92
52
|
/**
|
|
93
53
|
* Enhance the CDS model with change tracking associations, facets, and side effects.
|
|
94
54
|
* Returns the updated hierarchyMap and collectedEntities for use by trigger generation.
|
|
@@ -107,7 +67,6 @@ function enhanceModel(m) {
|
|
|
107
67
|
const { 'sap.changelog.aspect': aspect } = m.definitions;
|
|
108
68
|
if (!aspect) return; // some other model
|
|
109
69
|
const {
|
|
110
|
-
'@UI.Facets': [facet],
|
|
111
70
|
elements: { changes }
|
|
112
71
|
} = aspect;
|
|
113
72
|
|
|
@@ -143,6 +102,7 @@ function enhanceModel(m) {
|
|
|
143
102
|
m.definitions['sap.changelog.ChangeView'].elements[parents.join('_') + '_' + 'entityKey'] = structuredClone(m.definitions['sap.changelog.ChangeView'].elements.entityKey);
|
|
144
103
|
m.definitions['sap.changelog.ChangeView'].elements[parents.join('_') + '_' + 'entity'] = structuredClone(m.definitions['sap.changelog.ChangeView'].elements.entity);
|
|
145
104
|
}
|
|
105
|
+
enhanceChangeViewWithTimeZones(m.definitions['sap.changelog.ChangeView'], m);
|
|
146
106
|
}
|
|
147
107
|
for (let name in m.definitions) {
|
|
148
108
|
const entity = m.definitions[name];
|
|
@@ -228,35 +188,12 @@ function enhanceModel(m) {
|
|
|
228
188
|
(query.columns ??= ['*']).push({ as: 'changes', cast: assoc });
|
|
229
189
|
entity.elements.changes = assoc;
|
|
230
190
|
}
|
|
231
|
-
|
|
232
|
-
if (entity['@changelog.disable_facet'] !== undefined) {
|
|
233
|
-
LOG.warn(
|
|
234
|
-
`@changelog.disable_facet is deprecated! You can just define your own Facet for the changes association or annotate the changes association on ${entity.name} with not readable via @Capabilities.NavigationRestrictions.RestrictedProperties`
|
|
235
|
-
);
|
|
236
|
-
}
|
|
237
|
-
|
|
238
|
-
let facets = entity['@UI.Facets'];
|
|
239
|
-
|
|
240
|
-
if (!facets) {
|
|
241
|
-
DEBUG?.(`${entity.name} does not have a @UI.Facets annotation and thus the change tracking section is not added.`);
|
|
242
|
-
}
|
|
243
|
-
// Add UI.Facet for Change History List
|
|
244
|
-
if (
|
|
245
|
-
facets &&
|
|
246
|
-
!entity['@changelog.disable_facet'] &&
|
|
247
|
-
!hasFacetForComp(changes, entity['@UI.Facets']) &&
|
|
248
|
-
!entity['@Capabilities.NavigationRestrictions.RestrictedProperties']?.some((restriction) => restriction.NavigationProperty?.['='] === 'changes' && restriction.ReadRestrictions?.Readable === false)
|
|
249
|
-
) {
|
|
250
|
-
facets.push(facet);
|
|
251
|
-
}
|
|
191
|
+
addUIFacet(entity, m);
|
|
252
192
|
}
|
|
253
193
|
|
|
254
194
|
if (entity.actions) {
|
|
255
|
-
const
|
|
256
|
-
|
|
257
|
-
const { baseRef: dbEntityName } = baseInfo;
|
|
258
|
-
addSideEffects(entity.actions, dbEntityName, hierarchyMap, m);
|
|
259
|
-
}
|
|
195
|
+
const { baseRef: dbEntityName } = baseInfo;
|
|
196
|
+
addSideEffects(entity.actions, dbEntityName, hierarchyMap, m);
|
|
260
197
|
}
|
|
261
198
|
}
|
|
262
199
|
}
|
|
@@ -0,0 +1,81 @@
|
|
|
1
|
+
const cds = require('@sap/cds');
|
|
2
|
+
const { compositeKeyExpr: compositeKeyExprSqlite } = require('../sqlite/sql-expressions');
|
|
3
|
+
const { compositeKeyExpr: compositeKeyExprHANA } = require('../hana/sql-expressions');
|
|
4
|
+
const { compositeKeyExpr: compositeKeyExprPG } = require('../postgres/sql-expressions');
|
|
5
|
+
const { isChangeTracked } = require('../utils/entity-collector');
|
|
6
|
+
|
|
7
|
+
function collectTrackedPropertiesWithTimezone(m) {
|
|
8
|
+
const timezoneProperties = [];
|
|
9
|
+
for (const name in m.definitions) {
|
|
10
|
+
const entity = m.definitions[name];
|
|
11
|
+
if (entity.kind !== 'entity' || entity.query || entity.projection || !isChangeTracked(entity)) {
|
|
12
|
+
continue;
|
|
13
|
+
}
|
|
14
|
+
for (const ele in entity.elements) {
|
|
15
|
+
const element = entity.elements[ele];
|
|
16
|
+
if (!element['@Common.Timezone'] || element._foreignKey4) {
|
|
17
|
+
continue;
|
|
18
|
+
}
|
|
19
|
+
timezoneProperties.push({
|
|
20
|
+
property: ele,
|
|
21
|
+
entity: name,
|
|
22
|
+
timezone: element['@Common.Timezone']?.['=']
|
|
23
|
+
? // Where condition is replaced when select is inserted into ChangeView
|
|
24
|
+
SELECT.from(name).alias('timezoneSubSelect').where('1 = 1').columns(element['@Common.Timezone']['='])
|
|
25
|
+
: element['@Common.Timezone']
|
|
26
|
+
});
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return timezoneProperties;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function isDeploy2Check(target) {
|
|
33
|
+
try {
|
|
34
|
+
cds.build?.register(target, class ABC extends cds.build.Plugin {});
|
|
35
|
+
} catch (err) {
|
|
36
|
+
if (err.message.match(/already registered/)) {
|
|
37
|
+
return true;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return false;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function enhanceChangeViewWithTimeZones(changeView, m) {
|
|
44
|
+
const compositeKeyExpr =
|
|
45
|
+
isDeploy2Check('hana') && m.meta.creator.match(/v6/)
|
|
46
|
+
? compositeKeyExprHANA
|
|
47
|
+
: isDeploy2Check('postgres') && m.meta.creator.match(/v6/)
|
|
48
|
+
? compositeKeyExprPG
|
|
49
|
+
: cds.env.requires?.db.kind === 'sqlite' && !cds.build
|
|
50
|
+
? compositeKeyExprSqlite
|
|
51
|
+
: cds.env.requires?.db.kind === 'postgres' && (!cds.build || cds.env.profiles.includes('pg'))
|
|
52
|
+
? compositeKeyExprPG
|
|
53
|
+
: compositeKeyExprHANA;
|
|
54
|
+
const timezoneProperties = collectTrackedPropertiesWithTimezone(m);
|
|
55
|
+
const timezoneColumn = changeView.query.SELECT.columns.find((c) => c.as && c.as === 'valueTimeZone');
|
|
56
|
+
if (timezoneProperties.length === 0) return;
|
|
57
|
+
delete timezoneColumn.val;
|
|
58
|
+
timezoneColumn.xpr = ['case'];
|
|
59
|
+
for (const timezoneProp of timezoneProperties) {
|
|
60
|
+
timezoneColumn.xpr.push('when', { ref: ['attribute'] }, '=', { val: timezoneProp.property }, 'and', { ref: ['entity'] }, '=', { val: timezoneProp.entity }, 'then');
|
|
61
|
+
if (timezoneProp.timezone.SELECT) {
|
|
62
|
+
const subSelect = structuredClone(timezoneProp.timezone);
|
|
63
|
+
const keys = Object.keys(m.definitions[timezoneProp.entity].elements).filter((e) => m.definitions[timezoneProp.entity].elements[e].key);
|
|
64
|
+
subSelect.SELECT.where = [
|
|
65
|
+
{ ref: ['change', 'entityKey'] },
|
|
66
|
+
'=',
|
|
67
|
+
// REVISIT: once HIERARCHY_COMPOSITE_ID is available on all DBs, use native CQN
|
|
68
|
+
compositeKeyExpr(keys)
|
|
69
|
+
];
|
|
70
|
+
timezoneColumn.xpr.push(subSelect);
|
|
71
|
+
} else {
|
|
72
|
+
timezoneColumn.xpr.push({ val: timezoneProp.timezone });
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
timezoneColumn.xpr.push('else', { val: null }, 'end');
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
module.exports = {
|
|
79
|
+
collectTrackedPropertiesWithTimezone,
|
|
80
|
+
enhanceChangeViewWithTimeZones
|
|
81
|
+
};
|
package/lib/hana/register.js
CHANGED
|
@@ -42,6 +42,16 @@ function registerHDICompilerHook() {
|
|
|
42
42
|
ret.definitions = ret.definitions.concat(triggers).concat(data);
|
|
43
43
|
return ret;
|
|
44
44
|
};
|
|
45
|
+
|
|
46
|
+
// REVISIT: Remove once time casting is fixed in cds-dbs
|
|
47
|
+
cds.on('serving', async () => {
|
|
48
|
+
if (cds.env.requires?.db.kind !== 'hana') return;
|
|
49
|
+
const db = await cds.connect.to('db');
|
|
50
|
+
db.before('*', () => {
|
|
51
|
+
// to_time conversion is necessary else HANA tries to convert to timestamp implicitly causing an SQL crash
|
|
52
|
+
db.class.CQN2SQL.OutputConverters.Time = (e) => `to_char(to_time(${e}), 'HH24:MI:SS')`;
|
|
53
|
+
});
|
|
54
|
+
});
|
|
45
55
|
}
|
|
46
56
|
|
|
47
57
|
module.exports = { registerHDICompilerHook };
|
package/lib/postgres/register.js
CHANGED
|
@@ -26,6 +26,18 @@ function registerPostgresCompilerHook() {
|
|
|
26
26
|
|
|
27
27
|
return ddl;
|
|
28
28
|
});
|
|
29
|
+
|
|
30
|
+
// REVISIT: Remove once time casting is fixed in cds-dbs
|
|
31
|
+
cds.on('serving', async () => {
|
|
32
|
+
if (cds.env.requires?.db.kind !== 'postgres') return;
|
|
33
|
+
const db = await cds.connect.to('db');
|
|
34
|
+
db.before('*', () => {
|
|
35
|
+
db.class.CQN2SQL.OutputConverters.Date = (e) => `to_date(${e}::text, 'YYYY-MM-DD')`;
|
|
36
|
+
db.class.CQN2SQL.OutputConverters.Time = (e) => `to_timestamp(${e}::text, 'HH24:MI:SS')::TIME`;
|
|
37
|
+
db.class.CQN2SQL.OutputConverters.DateTime = (e) => `to_timestamp(${e}::text, 'YYYY-MM-DD"T"HH24:MI:SS"Z"')::timestamp`;
|
|
38
|
+
db.class.CQN2SQL.OutputConverters.Timestamp = (e) => `to_timestamp(${e}::text, 'YYYY-MM-DD"T"HH24:MI:SS.US"Z"')::timestamp`;
|
|
39
|
+
});
|
|
40
|
+
});
|
|
29
41
|
}
|
|
30
42
|
|
|
31
43
|
async function deployPostgresLabels() {
|