@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
@@ -0,0 +1,253 @@
1
+ const cds = require('@sap/cds');
2
+ const utils = require('../utils/change-tracking.js');
3
+ const config = cds.env.requires['change-tracking'];
4
+ const { getCompositionParentInfo, getGrandParentCompositionInfo } = require('../utils/composition-helpers.js');
5
+ const { getSkipCheckCondition, getElementSkipCondition, compositeKeyExpr, getValueExpr, getWhereCondition, getLabelExpr, buildObjectIDExpr } = require('./sql-expressions.js');
6
+ const { buildCompositionParentContext, buildParentLookupOrCreateSQL, buildCompositionOnlyBody } = require('./composition.js');
7
+
8
+ function buildTriggerContext(entity, objectIDs, rowRef, model, compositionParentInfo = null) {
9
+ const keys = utils.extractKeys(entity.keys);
10
+ return {
11
+ entityKeyExpr: compositeKeyExpr(keys.map((k) => `:${rowRef}.${k}`)),
12
+ objectIDExpr: buildObjectIDExpr(objectIDs, entity, rowRef, model),
13
+ parentLookupExpr: compositionParentInfo !== null ? 'parent_id' : null
14
+ };
15
+ }
16
+
17
+ function buildInsertSQL(entity, columns, modification, ctx, model) {
18
+ // Generate single UNION ALL query for all changed columns
19
+ const unionQuery = columns
20
+ .map((col) => {
21
+ const whereCondition = getWhereCondition(col, modification);
22
+ const elementSkipCondition = getElementSkipCondition(entity.name, col.name);
23
+ let fullWhere = `(${whereCondition}) AND ${elementSkipCondition}`;
24
+
25
+ // For composition-of-one columns, add deduplication check to prevent duplicate entries
26
+ // when child trigger has already created a composition entry for this transaction
27
+ if (col.type === 'cds.Composition' && ctx.entityKeyExpr) {
28
+ fullWhere += ` AND NOT EXISTS (
29
+ SELECT 1 FROM SAP_CHANGELOG_CHANGES
30
+ WHERE entity = '${entity.name}'
31
+ AND entityKey = ${ctx.entityKeyExpr}
32
+ AND attribute = '${col.name}'
33
+ AND valueDataType = 'cds.Composition'
34
+ AND transactionID = CURRENT_UPDATE_TRANSACTION()
35
+ )`;
36
+ }
37
+
38
+ const oldVal = modification === 'create' ? 'NULL' : getValueExpr(col, 'old');
39
+ const newVal = modification === 'delete' ? 'NULL' : getValueExpr(col, 'new');
40
+ const oldLabel = modification === 'create' ? 'NULL' : getLabelExpr(col, 'old', model);
41
+ const newLabel = modification === 'delete' ? 'NULL' : getLabelExpr(col, 'new', model);
42
+
43
+ return `SELECT '${col.name}' AS attribute, ${oldVal} AS valueChangedFrom, ${newVal} AS valueChangedTo, ${oldLabel} AS valueChangedFromLabel, ${newLabel} AS valueChangedToLabel, '${col.type}' AS valueDataType FROM SAP_CHANGELOG_CHANGE_TRACKING_DUMMY WHERE ${fullWhere}`;
44
+ })
45
+ .join('\nUNION ALL\n');
46
+
47
+ return `INSERT INTO SAP_CHANGELOG_CHANGES
48
+ (ID, parent_ID, attribute, valueChangedFrom, valueChangedTo, valueChangedFromLabel, valueChangedToLabel, entity, entityKey, objectID, createdAt, createdBy, valueDataType, modification, transactionID)
49
+ SELECT
50
+ SYSUUID,
51
+ ${ctx.parentLookupExpr},
52
+ attribute,
53
+ valueChangedFrom,
54
+ valueChangedTo,
55
+ valueChangedFromLabel,
56
+ valueChangedToLabel,
57
+ '${entity.name}',
58
+ ${ctx.entityKeyExpr},
59
+ ${ctx.objectIDExpr},
60
+ CURRENT_TIMESTAMP,
61
+ SESSION_CONTEXT('APPLICATIONUSER'),
62
+ valueDataType,
63
+ '${modification}',
64
+ CURRENT_UPDATE_TRANSACTION()
65
+ FROM (
66
+ ${unionQuery}
67
+ );`;
68
+ }
69
+
70
+ function wrapInSkipCheck(entityName, insertSQL, compositionParentContext = null) {
71
+ if (compositionParentContext) {
72
+ const { declares } = compositionParentContext;
73
+ return `${declares}
74
+ IF ${getSkipCheckCondition(entityName)} THEN
75
+ ${buildParentLookupOrCreateSQL(compositionParentContext)}
76
+ ${insertSQL}
77
+ END IF;`;
78
+ }
79
+ return `IF ${getSkipCheckCondition(entityName)} THEN
80
+ ${insertSQL}
81
+ END IF;`;
82
+ }
83
+
84
+ function generateCreateTrigger(entity, columns, objectIDs, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
85
+ const ctx = buildTriggerContext(entity, objectIDs, 'new', model, compositionParentInfo);
86
+
87
+ // Build context for composition parent entry if this is a tracked composition target
88
+ const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'create', 'new', model, grandParentCompositionInfo) : null;
89
+
90
+ // Handle composition-only triggers (no tracked columns, only composition parent entry)
91
+ let body;
92
+ if (columns.length === 0 && compositionParentContext) {
93
+ body = buildCompositionOnlyBody(entity.name, compositionParentContext);
94
+ } else {
95
+ const insertSQL = buildInsertSQL(entity, columns, 'create', ctx, model);
96
+ body = wrapInSkipCheck(entity.name, insertSQL, compositionParentContext);
97
+ }
98
+
99
+ return {
100
+ name: entity.name + '_CT_CREATE',
101
+ sql: `TRIGGER ${utils.transformName(entity.name)}_CT_CREATE AFTER INSERT
102
+ ON ${utils.transformName(entity.name)}
103
+ REFERENCING NEW ROW new
104
+ BEGIN
105
+ ${body}
106
+ END;`,
107
+ suffix: '.hdbtrigger'
108
+ };
109
+ }
110
+
111
+ function generateUpdateTrigger(entity, columns, objectIDs, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
112
+ const ctx = buildTriggerContext(entity, objectIDs, 'new', model, compositionParentInfo);
113
+
114
+ // Build context for composition parent entry if this is a tracked composition target
115
+ const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'update', 'new', model, grandParentCompositionInfo) : null;
116
+
117
+ // Handle composition-only triggers (no tracked columns, only composition parent entry)
118
+ let body;
119
+ if (columns.length === 0 && compositionParentContext) {
120
+ body = buildCompositionOnlyBody(entity.name, compositionParentContext);
121
+ } else {
122
+ const insertSQL = buildInsertSQL(entity, columns, 'update', ctx, model);
123
+ body = wrapInSkipCheck(entity.name, insertSQL, compositionParentContext);
124
+ }
125
+
126
+ // Build OF clause for targeted update trigger
127
+ const ofColumns = columns.flatMap((c) => {
128
+ if (!c.target) return [c.name];
129
+ if (c.foreignKeys) return c.foreignKeys.map((k) => `${c.name}_${k.replaceAll(/\./g, '_')}`);
130
+ if (c.on) return c.on.map((m) => m.foreignKeyField);
131
+ return [];
132
+ });
133
+ const ofClause = columns.length > 0 ? `OF ${ofColumns.join(', ')} ` : '';
134
+
135
+ return {
136
+ name: entity.name + '_CT_UPDATE',
137
+ sql: `TRIGGER ${utils.transformName(entity.name)}_CT_UPDATE AFTER UPDATE ${ofClause}
138
+ ON ${utils.transformName(entity.name)}
139
+ REFERENCING NEW ROW new, OLD ROW old
140
+ BEGIN
141
+ ${body}
142
+ END;`,
143
+ suffix: '.hdbtrigger'
144
+ };
145
+ }
146
+
147
+ function generateDeleteTriggerPreserve(entity, columns, objectIDs, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
148
+ const ctx = buildTriggerContext(entity, objectIDs, 'old', model, compositionParentInfo);
149
+
150
+ // Build context for composition parent entry if this is a tracked composition target
151
+ const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'delete', 'old', model, grandParentCompositionInfo) : null;
152
+
153
+ // Handle composition-only triggers (no tracked columns, only composition parent entry)
154
+ let body;
155
+ if (columns.length === 0 && compositionParentContext) {
156
+ body = buildCompositionOnlyBody(entity.name, compositionParentContext);
157
+ } else {
158
+ const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
159
+ body = wrapInSkipCheck(entity.name, insertSQL, compositionParentContext);
160
+ }
161
+
162
+ return {
163
+ name: entity.name + '_CT_DELETE',
164
+ sql: `TRIGGER ${utils.transformName(entity.name)}_CT_DELETE AFTER DELETE
165
+ ON ${utils.transformName(entity.name)}
166
+ REFERENCING OLD ROW old
167
+ BEGIN
168
+ ${body}
169
+ END;`,
170
+ suffix: '.hdbtrigger'
171
+ };
172
+ }
173
+
174
+ function generateDeleteTrigger(entity, columns, objectIDs, rootObjectIDs, model, compositionParentInfo = null, grandParentCompositionInfo = null) {
175
+ const keys = utils.extractKeys(entity.keys);
176
+ const entityKey = compositeKeyExpr(keys.map((k) => `:old.${k}`));
177
+
178
+ const ctx = buildTriggerContext(entity, objectIDs, 'old', model, compositionParentInfo);
179
+
180
+ const deleteSQL = `DELETE FROM SAP_CHANGELOG_CHANGES WHERE entity = '${entity.name}' AND entityKey = ${entityKey};`;
181
+
182
+ // Build context for composition parent entry if this is a tracked composition target
183
+ const compositionParentContext = compositionParentInfo ? buildCompositionParentContext(compositionParentInfo, rootObjectIDs, 'delete', 'old', model, grandParentCompositionInfo) : null;
184
+
185
+ // Special wrapping for delete - need variable declared if using composition
186
+ let body;
187
+ if (columns.length === 0 && compositionParentContext) {
188
+ // Composition-only case: only insert composition parent entry, no child column inserts
189
+ body = buildCompositionOnlyBody(entity.name, compositionParentContext, deleteSQL);
190
+ } else if (compositionParentContext) {
191
+ // Mixed case: both composition parent entry and child column inserts
192
+ const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
193
+ const { declares } = compositionParentContext;
194
+ body = `${declares}
195
+ IF ${getSkipCheckCondition(entity.name)} THEN
196
+ ${deleteSQL}
197
+ ${buildParentLookupOrCreateSQL(compositionParentContext)}
198
+ ${insertSQL}
199
+ END IF;`;
200
+ } else {
201
+ // No composition: standard delete with column inserts
202
+ const insertSQL = buildInsertSQL(entity, columns, 'delete', ctx, model);
203
+ body = wrapInSkipCheck(entity.name, `${deleteSQL}\n\t\t${insertSQL}`);
204
+ }
205
+
206
+ return {
207
+ name: entity.name + '_CT_DELETE',
208
+ sql: `TRIGGER ${utils.transformName(entity.name)}_CT_DELETE AFTER DELETE
209
+ ON ${utils.transformName(entity.name)}
210
+ REFERENCING OLD ROW old
211
+ BEGIN
212
+ ${body}
213
+ END;`,
214
+ suffix: '.hdbtrigger'
215
+ };
216
+ }
217
+
218
+ function generateHANATriggers(csn, entity, rootEntity = null, mergedAnnotations = null, rootMergedAnnotations = null, grandParentContext = {}) {
219
+ const triggers = [];
220
+ const { columns: trackedColumns } = utils.extractTrackedColumns(entity, csn, mergedAnnotations);
221
+
222
+ const objectIDs = utils.getObjectIDs(entity, csn, mergedAnnotations?.entityAnnotation);
223
+ const rootObjectIDs = utils.getObjectIDs(rootEntity, csn, rootMergedAnnotations?.entityAnnotation);
224
+
225
+ const keys = utils.extractKeys(entity.keys);
226
+ if (keys.length === 0 && trackedColumns.length > 0) return triggers;
227
+
228
+ // Check if this entity is a composition target with @changelog on the composition field
229
+ const compositionParentInfo = getCompositionParentInfo(entity, rootEntity, rootMergedAnnotations);
230
+
231
+ // Get grandparent info for deep linking (e.g., OrderItemNote -> OrderItem.notes -> Order.orderItems)
232
+ const { grandParentEntity, grandParentMergedAnnotations, grandParentCompositionField } = grandParentContext;
233
+ const grandParentCompositionInfo = getGrandParentCompositionInfo(rootEntity, grandParentEntity, grandParentMergedAnnotations, grandParentCompositionField);
234
+
235
+ // Skip if no tracked columns and not a composition target with tracked composition
236
+ if (trackedColumns.length === 0 && !compositionParentInfo) return triggers;
237
+
238
+ // Generate triggers - either for tracked columns or for composition-only tracking
239
+ if (!config?.disableCreateTracking) {
240
+ triggers.push(generateCreateTrigger(entity, trackedColumns, objectIDs, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo));
241
+ }
242
+ if (!config?.disableUpdateTracking) {
243
+ triggers.push(generateUpdateTrigger(entity, trackedColumns, objectIDs, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo));
244
+ }
245
+ if (!config?.disableDeleteTracking) {
246
+ const generateDeleteTriggerFunc = config?.preserveDeletes ? generateDeleteTriggerPreserve : generateDeleteTrigger;
247
+ triggers.push(generateDeleteTriggerFunc(entity, trackedColumns, objectIDs, rootObjectIDs, csn, compositionParentInfo, grandParentCompositionInfo));
248
+ }
249
+
250
+ return triggers;
251
+ }
252
+
253
+ module.exports = { generateHANATriggers };
@@ -1,135 +1,71 @@
1
1
  const cds = require('@sap/cds/lib');
2
- const LOG = cds.log('change-log');
3
- const { formatOptions } = require('./format-options');
4
- const { getNameFromPathVal, getDBEntity, splitPath } = require('./entity-helper');
5
2
 
6
- const MODIF_I18N_MAP = {
7
- create: 'ChangeLog.modification.create',
8
- update: 'ChangeLog.modification.update',
9
- delete: 'ChangeLog.modification.delete'
10
- };
3
+ /**
4
+ * Generate i18n label translations for change tracking triggers
5
+ * Used by trigger generators to populate the sap.changelog.i18nKeys table
6
+ */
7
+ function getLabelTranslations(entities, model) {
8
+ // Create bundle from the passed model
9
+ const labelBundle = cds.i18n.bundle4(model);
10
+ const allLabels = labelBundle.translations4('all');
11
11
 
12
- const _localizeModification = function (change) {
13
- if (change.modification && MODIF_I18N_MAP[change.modification]) {
14
- change.modification = cds.i18n.labels.for(MODIF_I18N_MAP[change.modification]);
15
- }
16
- };
12
+ // Get translations for modification texts
13
+ const bundle = cds.i18n.bundle4({ folders: [cds.utils.path.join(__dirname, '..', '_i18n')] });
14
+ const modificationLabels = bundle.translations4('all');
17
15
 
18
- const _localizeDefaultObjectID = function (change) {
19
- if (!change.objectID) {
20
- change.objectID = change.entity ? change.entity : '';
21
- }
22
- if (change.objectID && change.serviceEntityPath && !change.parentObjectID && change.parentKey) {
23
- const path = splitPath(change.serviceEntityPath);
24
- const parentNodePathVal = path[path.length - 2];
25
- const parentEntityName = getNameFromPathVal(parentNodePathVal);
26
- const dbEntity = getDBEntity(parentEntityName);
27
- try {
28
- const labelI18nKey = getTranslationKey(dbEntity['@Common.Label'] || dbEntity['@title']);
29
- change.parentObjectID = cds.i18n.labels.for(labelI18nKey) || labelI18nKey || dbEntity.name;
30
- } catch (e) {
31
- LOG.error('Failed to localize parent object id', e);
32
- throw new Error('Failed to localize parent object id', e);
33
- }
34
- }
35
- };
16
+ const rows = new Map();
17
+ const addRow = (ID, locale, text) => {
18
+ const compositeKey = `${ID}::${locale}`;
19
+ rows.set(compositeKey, { ID, locale, text });
20
+ };
36
21
 
37
- const _localizeEntityType = function (change) {
38
- if (change.entity) {
39
- try {
40
- const labelI18nKey = _getLabelI18nKeyOnEntity(change.serviceEntity);
41
- change.entity = labelI18nKey || change.entity;
42
- } catch (e) {
43
- LOG.error('Failed to localize entity type', e);
44
- throw new Error('Failed to localize entity type', e);
45
- }
46
- }
47
- if (change.serviceEntity) {
48
- try {
49
- const labelI18nKey = _getLabelI18nKeyOnEntity(change.serviceEntity);
50
- change.serviceEntity = labelI18nKey || change.serviceEntity;
51
- } catch (e) {
52
- LOG.error('Failed to localize service entity', e);
53
- throw new Error('Failed to localize service entity', e);
54
- }
55
- }
56
- };
22
+ for (const { dbEntityName, mergedAnnotations } of entities) {
23
+ const entity = model.definitions[dbEntityName];
57
24
 
58
- const getTranslationKey = (value) => {
59
- if (typeof value != 'string') return value;
60
- const result = value.match(/(?<=\{@?(i18n>)).*(?=\})/g);
61
- return result ? result[0] : value;
62
- };
63
-
64
- const _localizeAttribute = function (change) {
65
- if (change.attribute && change.serviceEntity) {
66
- const model = cds.context?.model ?? cds.model;
67
- try {
68
- const serviceEntity = model.definitions[change.serviceEntity];
69
- if (!serviceEntity) {
70
- LOG.warn(`Cannot localize the attribute ${change.attribute} of ${change.serviceEntity}, because the service entity is not defined in "cds.model.definitions".`);
71
- return;
25
+ // Entity labels
26
+ const entityLabelKey = labelBundle.key4(entity);
27
+ if (entityLabelKey && entityLabelKey !== entity.name) {
28
+ for (const [locale, localeTranslations] of Object.entries(allLabels)) {
29
+ if (!locale) continue;
30
+ const text = localeTranslations[entityLabelKey] || entityLabelKey;
31
+ addRow(entity.name, locale, text);
72
32
  }
73
- let labelI18nKey = _getLabelI18nKeyOnEntity(change.serviceEntity, change.attribute);
74
- if (!labelI18nKey) {
75
- const element = serviceEntity.elements[change.attribute];
76
- if (!element) {
77
- LOG.warn(`Cannot localize the attribute ${change.attribute} of ${change.serviceEntity}, because the attribute does not exist on the entity.`);
78
- return;
33
+ }
34
+
35
+ // Attribute labels
36
+ for (const element of entity.elements) {
37
+ // Use merged annotation if available, otherwise use element's own annotation
38
+ const annotations = mergedAnnotations?.elementAnnotations?.[element.name] ?? element['@changelog'];
39
+ if (!annotations || element._foreignKey4) continue; // REVISIT: skip foreign keys
40
+ const attrKey = labelBundle.key4(element);
41
+ if (attrKey && attrKey !== element.name) {
42
+ for (const [locale, localeTranslations] of Object.entries(allLabels)) {
43
+ if (!locale) continue;
44
+ const text = localeTranslations[attrKey] || attrKey;
45
+ addRow(element.name, locale, text);
79
46
  }
80
- if (element.isAssociation) labelI18nKey = _getLabelI18nKeyOnEntity(element.target);
81
47
  }
82
- change.attribute = labelI18nKey || change.attribute;
83
- } catch (e) {
84
- LOG.error('Failed to localize change attribute', e);
85
- throw new Error('Failed to localize change attribute', e);
86
48
  }
87
49
  }
88
- };
89
-
90
- const _getLabelI18nKeyOnEntity = function (entityName, /** optinal */ attribute) {
91
- const model = cds.context?.model ?? cds.model;
92
- let def = model.definitions[entityName];
93
- if (attribute) def = def?.elements[attribute];
94
- if (!def) return '';
95
- const i18nKey = getTranslationKey(def['@Common.Label'] || def['@title'] || def['@UI.HeaderInfo.TypeName']);
96
- return cds.i18n.labels.for(i18nKey) || i18nKey;
97
- };
98
50
 
99
- const parseTime = (time, locale, options) => {
100
- const timeParts = time.split(':');
101
- const date = new Date();
102
- date.setHours(parseInt(timeParts[0], 10), parseInt(timeParts[1], 10), parseInt(timeParts[2], 10));
103
- return date.toLocaleTimeString(locale, options);
104
- };
51
+ // Modification labels (create, update, delete)
52
+ const MODIF_I18N_MAP = {
53
+ create: 'Changes.modification.create',
54
+ update: 'Changes.modification.update',
55
+ delete: 'Changes.modification.delete'
56
+ };
105
57
 
106
- const _localizeValue = (change, locale) => {
107
- if (change.valueDataType !== 'cds.Date' && change.valueDataType !== 'cds.DateTime' && change.valueDataType !== 'cds.Timestamp' && change.valueDataType !== 'cds.Time') {
108
- return;
58
+ for (const [locale, localeTranslations] of Object.entries(modificationLabels)) {
59
+ if (!locale) continue;
60
+ for (const [key, i18nKey] of Object.entries(MODIF_I18N_MAP)) {
61
+ const text = localeTranslations[i18nKey] || key;
62
+ addRow(key, locale, text);
63
+ }
109
64
  }
110
- const normalizedLocale = locale.replaceAll('_', '-');
111
- const options = formatOptions[change.valueDataType]?.[normalizedLocale] ?? formatOptions[change.valueDataType]?.['en'];
112
65
 
113
- if (change.valueDataType === 'cds.Time') {
114
- if (change.valueChangedFrom) change.valueChangedFrom = parseTime(change.valueChangedFrom, normalizedLocale, options);
115
- if (change.valueChangedTo) change.valueChangedTo = parseTime(change.valueChangedTo, normalizedLocale, options);
116
- } else {
117
- const formatter = change.valueDataType === 'cds.Date' ? 'toLocaleDateString' : 'toLocaleString';
118
- if (change.valueChangedFrom) change.valueChangedFrom = new Date(change.valueChangedFrom)[formatter](normalizedLocale, options);
119
- if (change.valueChangedTo) change.valueChangedTo = new Date(change.valueChangedTo)[formatter](normalizedLocale, options);
120
- }
121
- };
66
+ return Array.from(rows.values());
67
+ }
122
68
 
123
- const localizeLogFields = function (data, locale) {
124
- if (!locale) return;
125
- for (const change of data) {
126
- _localizeModification(change);
127
- _localizeAttribute(change);
128
- _localizeEntityType(change);
129
- _localizeDefaultObjectID(change);
130
- _localizeValue(change, locale);
131
- }
132
- };
133
69
  module.exports = {
134
- localizeLogFields
70
+ getLabelTranslations
135
71
  };