@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.
- package/CHANGELOG.md +11 -0
- package/LICENSE +201 -0
- package/README.md +176 -0
- package/_i18n/i18n.properties +51 -0
- package/_i18n/i18n_de.properties +51 -0
- package/_i18n/i18n_en.properties +51 -0
- package/_i18n/i18n_es.properties +51 -0
- package/_i18n/i18n_fr.properties +51 -0
- package/_i18n/i18n_it.properties +51 -0
- package/_i18n/i18n_ja.properties +47 -0
- package/_i18n/i18n_pl.properties +51 -0
- package/_i18n/i18n_pt.properties +51 -0
- package/_i18n/i18n_ru.properties +51 -0
- package/_i18n/i18n_zh_CN.properties +51 -0
- package/cds-plugin.js +60 -0
- package/index.cds +108 -0
- package/lib/change-log.js +374 -0
- package/lib/entity-helper.js +145 -0
- package/lib/localization.js +122 -0
- package/lib/template-processor.js +96 -0
- package/package.json +42 -0
|
@@ -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
|
+
};
|