@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
|
@@ -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 };
|
package/lib/localization.js
CHANGED
|
@@ -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
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
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
|
-
|
|
13
|
-
|
|
14
|
-
|
|
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
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
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
|
|
38
|
-
|
|
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
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
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
|
-
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
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
|
|
107
|
-
|
|
108
|
-
|
|
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
|
-
|
|
114
|
-
|
|
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
|
-
|
|
70
|
+
getLabelTranslations
|
|
135
71
|
};
|