@cap-js/change-tracking 1.0.0

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.
@@ -0,0 +1,374 @@
1
+ const cds = require("@sap/cds")
2
+ const getTemplate = require("@sap/cds/libx/_runtime/common/utils/template") // REVISIT: bad usage of internal stuff
3
+ const templateProcessor = require("./template-processor")
4
+ const LOG = cds.log("change-log")
5
+
6
+ const {
7
+ getNameFromPathVal,
8
+ getUUIDFromPathVal,
9
+ getCurObjFromReqData,
10
+ getCurObjFromDbQuery,
11
+ getObjectId,
12
+ getDBEntity,
13
+ getEntityByContextPath,
14
+ getObjIdElementNamesInArray,
15
+ getValueEntityType,
16
+ } = require("./entity-helper")
17
+ const { localizeLogFields } = require("./localization")
18
+
19
+
20
+ const _getRootEntityPathVals = function (txContext, entity, entityKey) {
21
+ const serviceEntityPathVals = []
22
+ const entityIDs = _getEntityIDs(txContext.params)
23
+
24
+ let path = txContext.path.split('/')
25
+
26
+ if (txContext.event === "CREATE") {
27
+ const curEntityPathVal = `${entity.name}(${entityKey})`
28
+ serviceEntityPathVals.push(curEntityPathVal)
29
+ } else {
30
+ // When deleting Composition of one node via REST API in draft-disabled mode,
31
+ // the child node ID would be missing in URI
32
+ if (txContext.event === "DELETE" && !entityIDs.find((x) => x === entityKey)) {
33
+ entityIDs.push(entityKey)
34
+ }
35
+ const curEntity = getEntityByContextPath(path)
36
+ const curEntityID = entityIDs.pop()
37
+ const curEntityPathVal = `${curEntity.name}(${curEntityID})`
38
+ serviceEntityPathVals.push(curEntityPathVal)
39
+ }
40
+
41
+
42
+ while (_isCompositionContextPath(path)) {
43
+ const hostEntity = getEntityByContextPath(path = path.slice(0, -1))
44
+ const hostEntityID = entityIDs.pop()
45
+ const hostEntityPathVal = `${hostEntity.name}(${hostEntityID})`
46
+ serviceEntityPathVals.unshift(hostEntityPathVal)
47
+ }
48
+
49
+ return serviceEntityPathVals
50
+ }
51
+
52
+ const _getAllPathVals = function (txContext) {
53
+ const pathVals = []
54
+ const paths = txContext.path.split('/')
55
+ const entityIDs = _getEntityIDs(txContext.params)
56
+
57
+ for (let idx = 0; idx < paths.length; idx++) {
58
+ const entity = getEntityByContextPath(paths.slice(0, idx + 1))
59
+ const entityID = entityIDs[idx]
60
+ const entityPathVal = `${entity.name}(${entityID})`
61
+ pathVals.push(entityPathVal)
62
+ }
63
+
64
+ return pathVals
65
+ }
66
+
67
+ const _getEntityIDs = function (txParams) {
68
+ const entityIDs = []
69
+ for (const param of txParams) {
70
+ let id = ""
71
+ if (typeof param === "object" && !Array.isArray(param)) {
72
+ id = param.ID
73
+ }
74
+ if (typeof param === "string") {
75
+ id = param
76
+ }
77
+ if (id) {
78
+ entityIDs.push(id)
79
+ }
80
+ }
81
+ return entityIDs
82
+ }
83
+
84
+ /**
85
+ *
86
+ * @param {*} tx
87
+ * @param {*} changes
88
+ *
89
+ * When consuming app implement '@changelog' on an association element,
90
+ * change history will use attribute on associated entity which are specified instead of default technical foreign key.
91
+ *
92
+ * eg:
93
+ * entity PurchasedProductFootprints @(cds.autoexpose): cuid, managed {
94
+ * ...
95
+ * '@changelog': [Plant.identifier]
96
+ * '@mandatory' Plant : Association to one Plant;
97
+ * ...
98
+ * }
99
+ */
100
+ const _formatAssociationContext = async function (changes) {
101
+ for (const change of changes) {
102
+ const a = cds.model.definitions[change.serviceEntity].elements[change.attribute]
103
+ if (a?.type !== "cds.Association") continue
104
+
105
+ const semkeys = getObjIdElementNamesInArray(a["@changelog"])
106
+ if (!semkeys.length) continue
107
+
108
+ const ID = a.keys[0].ref[0] || 'ID'
109
+ const [ from, to ] = await cds.db.run ([
110
+ SELECT.one.from(a.target).where({ [ID]: change.valueChangedFrom }),
111
+ SELECT.one.from(a.target).where({ [ID]: change.valueChangedTo })
112
+ ])
113
+
114
+ const fromObjId = await getObjectId(a.target, semkeys, { curObjFromDbQuery: from || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
115
+ if (fromObjId) change.valueChangedFrom = fromObjId
116
+
117
+ const toObjId = await getObjectId(a.target, semkeys, { curObjFromDbQuery: to || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
118
+ if (toObjId) change.valueChangedTo = toObjId
119
+
120
+ const isVLvA = a["@Common.ValueList.viaAssociation"]
121
+ if (!isVLvA) change.valueDataType = getValueEntityType(a.target, semkeys)
122
+ }
123
+ }
124
+
125
+ const _getChildChangeObjId = async function (
126
+ change,
127
+ childNodeChange,
128
+ curNodePathVal,
129
+ reqData
130
+ ) {
131
+ const composition = cds.model.definitions[change.serviceEntity].elements[change.attribute]
132
+ const objIdElements = composition ? composition["@changelog"] : null
133
+ const objIdElementNames = getObjIdElementNamesInArray(objIdElements)
134
+
135
+ return _getObjectIdByPath(
136
+ reqData,
137
+ curNodePathVal,
138
+ childNodeChange._path,
139
+ objIdElementNames
140
+ )
141
+ }
142
+
143
+ const _formatCompositionContext = async function (changes, reqData) {
144
+ const childNodeChanges = []
145
+
146
+ for (const change of changes) {
147
+ if (typeof change.valueChangedTo === "object") {
148
+ if (!Array.isArray(change.valueChangedTo)) {
149
+ change.valueChangedTo = [change.valueChangedTo]
150
+ }
151
+ for (const childNodeChange of change.valueChangedTo) {
152
+ const curChange = Object.assign({}, change)
153
+ const path = childNodeChange._path.split('/')
154
+ const curNodePathVal = path.pop()
155
+ curChange.modification = childNodeChange._op
156
+ const objId = await _getChildChangeObjId(
157
+ change,
158
+ childNodeChange,
159
+ curNodePathVal,
160
+ reqData
161
+ )
162
+ _formatCompositionValue(curChange, objId, childNodeChange, childNodeChanges)
163
+ }
164
+ change.valueChangedTo = undefined
165
+ }
166
+ }
167
+ changes.push(...childNodeChanges)
168
+ }
169
+
170
+ const _formatCompositionValue = function (
171
+ curChange,
172
+ objId,
173
+ childNodeChange,
174
+ childNodeChanges
175
+ ) {
176
+ if (curChange.modification === "delete") {
177
+ curChange.valueChangedFrom = objId
178
+ curChange.valueChangedTo = ""
179
+ } else if (curChange.modification === "update") {
180
+ curChange.valueChangedFrom = objId
181
+ curChange.valueChangedTo = objId
182
+ } else {
183
+ curChange.valueChangedFrom = ""
184
+ curChange.valueChangedTo = objId
185
+ }
186
+ curChange.valueDataType = _formatCompositionEntityType(curChange)
187
+ // Since req.diff() will record the managed data, change history will filter those logs only be changed managed data
188
+ const managedAttrs = ["modifiedAt", "modifiedBy"]
189
+ if (curChange.modification === "update") {
190
+ const rowOldAttrs = Object.keys(childNodeChange._old)
191
+ const diffAttrs = rowOldAttrs.filter((attr) => managedAttrs.indexOf(attr) === -1)
192
+ if (!diffAttrs.length) {
193
+ return
194
+ }
195
+ }
196
+ childNodeChanges.push(curChange)
197
+ }
198
+
199
+ const _formatCompositionEntityType = function (change) {
200
+ const composition = cds.model.definitions[change.serviceEntity].elements[change.attribute]
201
+ const objIdElements = composition ? composition['@changelog'] : null
202
+
203
+ if (Array.isArray(objIdElements)) {
204
+ // In this case, the attribute is a composition
205
+ const objIdElementNames = getObjIdElementNamesInArray(objIdElements)
206
+ return getValueEntityType(composition.target, objIdElementNames)
207
+ }
208
+ return ""
209
+ }
210
+
211
+ const _getObjectIdByPath = async function (
212
+ reqData,
213
+ nodePathVal,
214
+ serviceEntityPath,
215
+ /**optional*/ objIdElementNames
216
+ ) {
217
+ const curObjFromReqData = getCurObjFromReqData(reqData, nodePathVal, serviceEntityPath)
218
+ const entityName = getNameFromPathVal(nodePathVal)
219
+ const entityUUID = getUUIDFromPathVal(nodePathVal)
220
+ const obj = await getCurObjFromDbQuery(entityName, entityUUID)
221
+ const curObj = { curObjFromReqData, curObjFromDbQuery: obj }
222
+ return getObjectId(entityName, objIdElementNames, curObj)
223
+ }
224
+
225
+ const _formatObjectID = async function (changes, reqData) {
226
+ const objectIdCache = new Map()
227
+ for (const change of changes) {
228
+ const path = change.serviceEntityPath.split('/')
229
+ const curNodePathVal = path.pop()
230
+ const parentNodePathVal = path.pop()
231
+
232
+ let curNodeObjId = objectIdCache.get(curNodePathVal)
233
+ if (!curNodeObjId) {
234
+ curNodeObjId = await _getObjectIdByPath(
235
+ reqData,
236
+ curNodePathVal,
237
+ change.serviceEntityPath
238
+ )
239
+ objectIdCache.set(curNodePathVal, curNodeObjId)
240
+ }
241
+
242
+ let parentNodeObjId = objectIdCache.get(parentNodePathVal)
243
+ if (!parentNodeObjId && parentNodePathVal) {
244
+ parentNodeObjId = await _getObjectIdByPath(
245
+ reqData,
246
+ parentNodePathVal,
247
+ change.serviceEntityPath
248
+ )
249
+ objectIdCache.set(parentNodePathVal, parentNodeObjId)
250
+ }
251
+
252
+ change.entityID = curNodeObjId
253
+ change.parentEntityID = parentNodeObjId
254
+ change.parentKey = getUUIDFromPathVal(parentNodePathVal)
255
+ }
256
+ }
257
+
258
+ const _isCompositionContextPath = function (aPath) {
259
+ if (!aPath) return
260
+ if (typeof aPath === 'string') aPath = aPath.split('/')
261
+ if (aPath.length < 2) return false
262
+ const target = getEntityByContextPath(aPath)
263
+ const parent = getEntityByContextPath(aPath.slice(0, -1))
264
+ if (!parent.compositions) return false
265
+ return Object.values(parent.compositions).some(c => c._target === target)
266
+ }
267
+
268
+ const _formatChangeLog = async function (changes, req) {
269
+ await _formatObjectID(changes, req.data)
270
+ await _formatAssociationContext(changes)
271
+ await _formatCompositionContext(changes, req.data)
272
+ }
273
+
274
+ const _afterReadChangeView = function (data, req) {
275
+ if (!data) return
276
+ if (!Array.isArray(data)) data = [data]
277
+ localizeLogFields(data, req.locale)
278
+ }
279
+
280
+
281
+ function _trackedChanges4 (srv, target, diff) {
282
+ const template = getTemplate("change-logging", srv, target, { pick: e => e['@changelog'] })
283
+ if (!template.elements.size) return
284
+
285
+ const changes = []
286
+ diff._path = `${target.name}(${diff.ID})`
287
+
288
+ templateProcessor({
289
+ template, row: diff, processFn: ({ row, key, element }) => {
290
+ const from = row._old?.[key]
291
+ const to = row[key]
292
+ if (from === to) return
293
+
294
+ const keys = Object.keys(element.parent.keys)
295
+ .filter(k => k !== "IsActiveEntity")
296
+ .map(k => `${k}=${row[k]}`)
297
+ .join(', ')
298
+
299
+ changes.push({
300
+ serviceEntityPath: row._path,
301
+ entity: getDBEntity(element.parent).name,
302
+ serviceEntity: element.parent.name,
303
+ attribute: element["@odata.foreignKey4"] || key,
304
+ valueChangedFrom: from || '',
305
+ valueChangedTo: to || '',
306
+ valueDataType: element.type,
307
+ modification: row._op,
308
+ keys,
309
+ })
310
+ }
311
+ })
312
+
313
+ return changes.length && changes
314
+ }
315
+
316
+ const _prepareChangeLogForComposition = async function (entity, entityKey, changes, req) {
317
+ const rootEntityPathVals = _getRootEntityPathVals(req.context, entity, entityKey)
318
+
319
+ if (rootEntityPathVals.length < 2) {
320
+ LOG.info("Parent entity doesn't exist.")
321
+ return
322
+ }
323
+
324
+ const parentEntityPathVal = rootEntityPathVals[rootEntityPathVals.length - 2]
325
+ const parentKey = getUUIDFromPathVal(parentEntityPathVal)
326
+ const serviceEntityPath = rootEntityPathVals.join('/')
327
+ const parentServiceEntityPath = _getAllPathVals(req.context)
328
+ .slice(0, rootEntityPathVals.length - 2)
329
+ .join('/')
330
+
331
+ for (const change of changes) {
332
+ change.parentEntityID = await _getObjectIdByPath(req.data, parentEntityPathVal, parentServiceEntityPath)
333
+ change.parentKey = parentKey
334
+ change.serviceEntityPath = serviceEntityPath
335
+ }
336
+
337
+ const rootEntity = getNameFromPathVal(rootEntityPathVals[0])
338
+ const rootEntityID = getUUIDFromPathVal(rootEntityPathVals[0])
339
+ return [ rootEntity, rootEntityID ]
340
+ }
341
+
342
+
343
+ async function track_changes (req) {
344
+ let diff = await req.diff()
345
+ if (!diff) return
346
+
347
+ let target = req.target
348
+ let isDraftEnabled = !!target.drafts
349
+ let isComposition = _isCompositionContextPath(req.context.path)
350
+ let entityKey = diff.ID
351
+
352
+ if (req.event === "DELETE") {
353
+ if (isDraftEnabled || !isComposition) {
354
+ return await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey })
355
+ }
356
+ }
357
+
358
+ let changes = _trackedChanges4(this, target, diff)
359
+ if (!changes) return
360
+
361
+ await _formatChangeLog(changes, req)
362
+ if (isComposition && !isDraftEnabled) {
363
+ [ target, entityKey ] = await _prepareChangeLogForComposition(target, entityKey, changes, this)
364
+ }
365
+ const dbEntity = getDBEntity(target)
366
+ await INSERT.into("sap.changelog.ChangeLog").entries({
367
+ entity: dbEntity.name,
368
+ entityKey: entityKey,
369
+ serviceEntity: target.name,
370
+ changes: changes.filter(c => c.valueChangedFrom || c.valueChangedTo),
371
+ })
372
+ }
373
+
374
+ module.exports = { track_changes, _afterReadChangeView }
@@ -0,0 +1,145 @@
1
+ const cds = require("@sap/cds")
2
+ const LOG = cds.log("change-log")
3
+
4
+
5
+ const getNameFromPathVal = function (pathVal) {
6
+ return /^(.+?)\(/.exec(pathVal)?.[1] || ""
7
+ }
8
+
9
+ const getUUIDFromPathVal = function (pathVal) {
10
+ const regRes = /\((.+?)\)/.exec(pathVal)
11
+ return regRes ? regRes[1] : ""
12
+ }
13
+
14
+ const getEntityByContextPath = function (aPath) {
15
+ let entity = cds.model.definitions[aPath[0]]
16
+ for (let each of aPath.slice(1)) {
17
+ entity = entity.elements[each]?._target
18
+ }
19
+ return entity
20
+ }
21
+
22
+ const getObjIdElementNamesInArray = function (elements) {
23
+ if (Array.isArray(elements)) return elements.map(e => {
24
+ const splitted = (e["="]||e).split('.')
25
+ splitted.shift()
26
+ return splitted.join('.')
27
+ })
28
+ else return []
29
+ }
30
+
31
+ const getCurObjFromDbQuery = async function (entityName, queryVal, /**optional*/ queryKey='ID') {
32
+ if (!queryVal) return {}
33
+ // REVISIT: This always reads all elements -> should read required ones only!
34
+ const obj = await SELECT.one.from(entityName).where({[queryKey]: queryVal})
35
+ return obj || {}
36
+ }
37
+
38
+ const getCurObjFromReqData = function (reqData, nodePathVal, pathVal) {
39
+ const pathVals = pathVal.split('/')
40
+ const rootNodePathVal = pathVals[0]
41
+ let curReqObj = reqData || {}
42
+
43
+ if (nodePathVal === rootNodePathVal) return curReqObj
44
+ else pathVals.shift()
45
+
46
+ let parentSrvObjName = getNameFromPathVal(rootNodePathVal)
47
+
48
+ for (const subNodePathVal of pathVals) {
49
+ const srvObjName = getNameFromPathVal(subNodePathVal)
50
+ const curSrvObjUUID = getUUIDFromPathVal(subNodePathVal)
51
+ const associationName = _getAssociationName(parentSrvObjName, srvObjName)
52
+ if (curReqObj) {
53
+ let associationData = curReqObj[associationName]
54
+ if (!Array.isArray(associationData)) associationData = [associationData]
55
+ curReqObj = associationData?.find(x => x?.ID === curSrvObjUUID) || {}
56
+ }
57
+ if (subNodePathVal === nodePathVal) return curReqObj || {}
58
+ parentSrvObjName = srvObjName
59
+ }
60
+
61
+ return curReqObj
62
+
63
+ function _getAssociationName(entity, target) {
64
+ const source = cds.model.definitions[entity]
65
+ const assocs = source.associations
66
+ for (const each in assocs) {
67
+ if (assocs[each].target === target) return each
68
+ }
69
+ }
70
+ }
71
+
72
+
73
+ async function getObjectId (entityName, fields, curObj) {
74
+ let all = [], { curObjFromReqData: req_data={}, curObjFromDbQuery: db_data={} } = curObj
75
+ let entity = cds.model.definitions[entityName]
76
+ if (!fields?.length) fields = entity["@changelog"]?.map?.(k => k['='] || k) || []
77
+ for (let field of fields) {
78
+ let path = field.split('.')
79
+ if (path.length > 1) {
80
+ let current = entity, _db_data = db_data
81
+ while (path.length > 1) {
82
+ let assoc = current.elements[path[0]]; if (!assoc?.isAssociation) break
83
+ let foreignKey = assoc.keys?.[0]?.$generatedFieldName
84
+ let IDval = req_data[foreignKey] || _db_data[foreignKey]
85
+ if (IDval) try {
86
+ // REVISIT: This always reads all elements -> should read required ones only!
87
+ let ID = assoc.keys?.[0]?.ref[0] || 'ID'
88
+ _db_data = await SELECT.one.from(assoc._target).where({[ID]: IDval}) || {}
89
+ } catch (e) {
90
+ LOG.error("Failed to generate object Id for an association entity.", e)
91
+ throw new Error("Failed to generate object Id for an association entity.", e)
92
+ }
93
+ current = assoc._target
94
+ path.shift()
95
+ }
96
+ field = path.join('_')
97
+ let obj = _db_data[field] || req_data[field]
98
+ if (obj) all.push(obj)
99
+ } else {
100
+ let e = entity.elements[field]
101
+ if (e?.isAssociation) field = e.keys?.[0]?.$generatedFieldName
102
+ let obj = req_data[field] || db_data[field]
103
+ if (obj) all.push(obj)
104
+ }
105
+ }
106
+ return all.join(', ')
107
+ }
108
+
109
+
110
+
111
+ const getDBEntity = (entity) => {
112
+ if (typeof entity === 'string') entity = cds.model.definitions[entity]
113
+ let proto = Reflect.getPrototypeOf(entity)
114
+ if (proto instanceof cds.entity) return proto
115
+ }
116
+
117
+ const getValueEntityType = function (entityName, fields) {
118
+ const types=[], entity = cds.model.definitions[entityName]
119
+ for (let field of fields) {
120
+ let current = entity, path = field.split('.')
121
+ if (path.length > 1) {
122
+ for (;;) {
123
+ let target = current.elements[path[0]]?._target
124
+ if (target) current = target; else break
125
+ path.shift()
126
+ }
127
+ field = path.join('_')
128
+ }
129
+ let e = current.elements[field]
130
+ if (e) types.push(e.type)
131
+ }
132
+ return types.join(', ')
133
+ }
134
+
135
+ module.exports = {
136
+ getCurObjFromReqData,
137
+ getCurObjFromDbQuery,
138
+ getObjectId,
139
+ getNameFromPathVal,
140
+ getUUIDFromPathVal,
141
+ getDBEntity,
142
+ getEntityByContextPath,
143
+ getObjIdElementNamesInArray,
144
+ getValueEntityType,
145
+ }
@@ -0,0 +1,122 @@
1
+ const cds = require("@sap/cds/lib");
2
+ const LOG = cds.log("change-log");
3
+ const { getNameFromPathVal, getDBEntity } = require("./entity-helper");
4
+ const OBJECT_TYPE_I18N_LABEL_KEY = "@Common.Label"
5
+ const OBJECT_TYPE_I18N_TITLE_KEY = "@title"
6
+
7
+ const MODIF_I18N_MAP = {
8
+ create: "{i18n>ChangeLog.modification.create}",
9
+ update: "{i18n>ChangeLog.modification.update}",
10
+ delete: "{i18n>ChangeLog.modification.delete}",
11
+ };
12
+
13
+ const _getLocalization = function (locale, i18nKey) {
14
+ //
15
+ //
16
+ //
17
+ //
18
+ // REVISIT!
19
+ // REVISIT!
20
+ // REVISIT!
21
+ // REVISIT!
22
+ // REVISIT!
23
+ //
24
+ //
25
+ //
26
+ //
27
+ return JSON.parse(cds.localize(cds.model, locale, JSON.stringify(i18nKey)));
28
+ };
29
+
30
+ const _localizeModification = function (change, locale) {
31
+ if (change.modification && MODIF_I18N_MAP[change.modification]) {
32
+ change.modification = _getLocalization(locale, MODIF_I18N_MAP[change.modification]);
33
+ }
34
+ };
35
+
36
+ const _localizeDefaultObjectID = function (change, locale) {
37
+ if (!change.objectID) {
38
+ change.objectID = change.entity ? change.entity : "";
39
+ }
40
+ if (change.objectID && change.serviceEntityPath && !change.parentObjectID && change.parentKey) {
41
+ const path = change.serviceEntityPath.split('/');
42
+ const parentNodePathVal = path[path.length - 2];
43
+ const parentEntityName = getNameFromPathVal(parentNodePathVal);
44
+ const dbEntity = getDBEntity(parentEntityName);
45
+ try {
46
+ const labelI18nKey = dbEntity[OBJECT_TYPE_I18N_LABEL_KEY];
47
+ const labelI18nValue = labelI18nKey ? _getLocalization(locale, labelI18nKey) : null;
48
+ change.parentObjectID = labelI18nValue ? labelI18nValue : dbEntity.name;
49
+ } catch (e) {
50
+ LOG.error("Failed to localize parent object id", e);
51
+ throw new Error("Failed to localize parent object id", e);
52
+ }
53
+ }
54
+ };
55
+
56
+ const _localizeEntityType = function (change, locale) {
57
+ if (change.entity) {
58
+ try {
59
+ const labelI18nKey = _getLabelI18nKeyOnEntity(change.entity);
60
+ const labelI18nValue = labelI18nKey ? _getLocalization(locale, labelI18nKey) : null;
61
+
62
+ change.entity = labelI18nValue ? labelI18nValue : change.entity;
63
+ } catch (e) {
64
+ LOG.error("Failed to localize entity type", e);
65
+ throw new Error("Failed to localize entity type", e);
66
+ }
67
+ }
68
+ if (change.serviceEntity) {
69
+ try {
70
+ const labelI18nKey = _getLabelI18nKeyOnEntity(change.serviceEntity);
71
+ const labelI18nValue = labelI18nKey ? _getLocalization(locale, labelI18nKey) : null;
72
+
73
+ change.serviceEntity = labelI18nValue ? labelI18nValue : change.serviceEntity;
74
+ } catch (e) {
75
+ LOG.error("Failed to localize service entity", e);
76
+ throw new Error("Failed to localize service entity", e);
77
+ }
78
+ }
79
+ };
80
+
81
+ const _localizeAttribute = function (change, locale) {
82
+ if (change.attribute && change.serviceEntity) {
83
+ try {
84
+ const serviceEntity = cds.model.definitions[change.serviceEntity];
85
+ let labelI18nKey = _getLabelI18nKeyOnEntity(change.serviceEntity, change.attribute);
86
+ const element = serviceEntity.elements[change.attribute];
87
+ if (element.isAssociation && !labelI18nKey) {
88
+ labelI18nKey = _getLabelI18nKeyOnEntity(element.target);
89
+ }
90
+ const labelI18nValue = labelI18nKey ? _getLocalization(locale, labelI18nKey) : null;
91
+
92
+ change.attribute = labelI18nValue ? labelI18nValue : change.attribute;
93
+ } catch (e) {
94
+ LOG.error("Failed to localize change attribute", e);
95
+ throw new Error("Failed to localize change attribute", e);
96
+ }
97
+ }
98
+ };
99
+
100
+ const _getLabelI18nKeyOnEntity = function (entityName, /** optinal */ attribute) {
101
+ const entity = cds.model.definitions[entityName];
102
+ if (!entity) return "";
103
+ if (attribute) {
104
+ const element = entity.elements[attribute] ? entity.elements[attribute] : {};
105
+ return element[OBJECT_TYPE_I18N_LABEL_KEY];
106
+ }
107
+ const entityLabel = entity[OBJECT_TYPE_I18N_LABEL_KEY] ? entity[OBJECT_TYPE_I18N_LABEL_KEY] : entity[OBJECT_TYPE_I18N_TITLE_KEY];
108
+ return entityLabel;
109
+ };
110
+
111
+ const localizeLogFields = function (data, locale) {
112
+ if (!locale) return
113
+ for (const change of data) {
114
+ _localizeModification(change, locale);
115
+ _localizeAttribute(change, locale);
116
+ _localizeEntityType(change, locale);
117
+ _localizeDefaultObjectID(change, locale);
118
+ }
119
+ };
120
+ module.exports = {
121
+ localizeLogFields,
122
+ };