@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 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.4 - tbd
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/model-enhancer.js');
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 @UI.Hidden,
46
- change.parent : redirected to ChangeView,
47
- change.children : redirected to ChangeView,
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 : String(15) @title: '{i18n>Changes.attribute}',
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 : String(24) @title: '{i18n>Changes.entity}',
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 : String(16) @title: '{i18n>Changes.modification}',
82
+ ) as modificationLabel : String(16) @title: '{i18n>Changes.modification}',
83
83
  COALESCE(
84
84
  change.valueChangedFromLabel, change.valueChangedFrom
85
- ) as valueChangedFromLabel : String(5000) @(
86
- title: '{i18n>Changes.valueChangedFrom}',
87
- UI.MultiLineText
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 : String(5000) @(
92
- title: '{i18n>Changes.valueChangedTo}',
93
- UI.MultiLineText
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 : Int16 @UI.Hidden,
97
- null as DistanceFromRoot : Int16 @UI.Hidden,
98
- null as DrillState : String @UI.Hidden,
99
- null as LimitedRank : Int16 @UI.Hidden,
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
- Value : valueChangedToLabel,
305
+ $Type : 'UI.DataFieldForAnnotation',
306
+ Target : '@UI.FieldGroup#valueChangedTo',
307
+ Label : '{i18n>Changes.valueChangedTo}',
178
308
  @UI.Importance: #High
179
309
  },
180
310
  {
181
- Value : valueChangedFromLabel,
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 @UI.Hidden;
196
- valueChangedTo @UI.Hidden;
197
- parent @UI.Hidden;
198
- entityKey @UI.Hidden;
199
- entity @UI.Hidden;
200
- attribute @UI.Hidden;
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('./utils/entity-collector.js');
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 baseInfo = getBaseEntity(entity, m);
256
- if (baseInfo) {
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
+ };
@@ -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 };
@@ -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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cap-js/change-tracking",
3
- "version": "2.0.0-beta.3",
3
+ "version": "2.0.0-beta.4",
4
4
  "publishConfig": {
5
5
  "access": "public",
6
6
  "tag": "beta"