Skip to content

Commit

Permalink
support complex keys
Browse files Browse the repository at this point in the history
  • Loading branch information
nils committed Sep 17, 2024
1 parent a8f2284 commit a20aeb4
Show file tree
Hide file tree
Showing 9 changed files with 337 additions and 93 deletions.
118 changes: 58 additions & 60 deletions lib/change-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,42 @@ const {
getObjIdElementNamesInArray,
getValueEntityType,
} = require("./entity-helper")

const {
getKey,
flattenKey,
} = require("./keys")
const { localizeLogFields } = require("./localization")
const isRoot = "change-tracking-isRootEntity"


const _getRootEntityPathVals = function (txContext, entity, entityKey) {
const serviceEntityPathVals = []
const entityIDs = _getEntityIDs(txContext.params)
const entityIDs = [...txContext.params]

let path = txContext.path.split('/')
let path = [...txContext.path]

if (txContext.event === "CREATE") {
const curEntityPathVal = `${entity.name}(${entityKey})`
const curEntityPathVal = {target: entity.name, key: entityKey};
serviceEntityPathVals.push(curEntityPathVal)
txContext.hasComp && entityIDs.pop();
} else {
// When deleting Composition of one node via REST API in draft-disabled mode,
// the child node ID would be missing in URI
if (txContext.event === "DELETE" && !entityIDs.find((x) => x === entityKey)) {
if (txContext.event === "DELETE" && !entityIDs.find(p => JSON.stringify(p) === JSON.stringify(entityKey))) {
entityIDs.push(entityKey)
}
const curEntity = getEntityByContextPath(path, txContext.hasComp)
const curEntityID = entityIDs.pop()
const curEntityPathVal = `${curEntity.name}(${curEntityID})`
const curEntityPathVal = {target: curEntity.name, key: curEntityID}
serviceEntityPathVals.push(curEntityPathVal)
}


while (_isCompositionContextPath(path, txContext.hasComp)) {
const hostEntity = getEntityByContextPath(path = path.slice(0, -1), txContext.hasComp)
const hostEntityID = entityIDs.pop()
const hostEntityPathVal = `${hostEntity.name}(${hostEntityID})`
const hostEntityPathVal = {target: hostEntity.name, key: hostEntityID}
serviceEntityPathVals.unshift(hostEntityPathVal)
}

Expand All @@ -53,13 +58,13 @@ const _getRootEntityPathVals = function (txContext, entity, entityKey) {

const _getAllPathVals = function (txContext) {
const pathVals = []
const paths = txContext.path.split('/')
const entityIDs = _getEntityIDs(txContext.params)
const paths = [...txContext.path]
const entityIDs = [...txContext.params]

for (let idx = 0; idx < paths.length; idx++) {
const entity = getEntityByContextPath(paths.slice(0, idx + 1), txContext.hasComp)
const entityID = entityIDs[idx]
const entityPathVal = `${entity.name}(${entityID})`
const entityPathVal = {target: entity.name, key: entityID};
pathVals.push(entityPathVal)
}

Expand Down Expand Up @@ -88,23 +93,6 @@ function convertSubjectToParams(subject) {
return params.length > 0 ? params : subjectRef;
}

const _getEntityIDs = function (txParams) {
const entityIDs = []
for (const param of txParams) {
let id = ""
if (typeof param === "object" && !Array.isArray(param)) {
id = param.ID
}
if (typeof param === "string") {
id = param
}
if (id) {
entityIDs.push(id)
}
}
return entityIDs
}

/**
*
* @param {*} tx
Expand All @@ -121,7 +109,7 @@ const _getEntityIDs = function (txParams) {
* ...
* }
*/
const _formatAssociationContext = async function (changes, reqData) {
const _formatAssociationContext = async function (changes, reqData, reqTarget) {
for (const change of changes) {
const a = cds.model.definitions[change.serviceEntity].elements[change.attribute]
if (a?.type !== "cds.Association") continue
Expand All @@ -135,10 +123,10 @@ const _formatAssociationContext = async function (changes, reqData) {
SELECT.one.from(a.target).where({ [ID]: change.valueChangedTo })
])

const fromObjId = await getObjectId(reqData, a.target, semkeys, { curObjFromDbQuery: from || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
const fromObjId = await getObjectId(reqData, reqTarget, a.target, semkeys, { curObjFromDbQuery: from || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
if (fromObjId) change.valueChangedFrom = fromObjId

const toObjId = await getObjectId(reqData, a.target, semkeys, { curObjFromDbQuery: to || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
const toObjId = await getObjectId(reqData, reqTarget, a.target, semkeys, { curObjFromDbQuery: to || undefined }) // Note: ... || undefined is important for subsequent object destructuring with defaults
if (toObjId) change.valueChangedTo = toObjId

const isVLvA = a["@Common.ValueList.viaAssociation"]
Expand All @@ -150,21 +138,23 @@ const _getChildChangeObjId = async function (
change,
childNodeChange,
curNodePathVal,
reqData
reqData,
reqTarget
) {
const composition = cds.model.definitions[change.serviceEntity].elements[change.attribute]
const objIdElements = composition ? composition["@changelog"] : null
const objIdElementNames = getObjIdElementNamesInArray(objIdElements)

return _getObjectIdByPath(
reqData,
reqTarget,
curNodePathVal,
childNodeChange._path,
objIdElementNames
)
}

const _formatCompositionContext = async function (changes, reqData) {
const _formatCompositionContext = async function (changes, reqData, reqTarget) {
const childNodeChanges = []

for (const change of changes) {
Expand All @@ -174,14 +164,15 @@ const _formatCompositionContext = async function (changes, reqData) {
}
for (const childNodeChange of change.valueChangedTo) {
const curChange = Object.assign({}, change)
const path = childNodeChange._path.split('/')
const path = [...childNodeChange._path]
const curNodePathVal = path.pop()
curChange.modification = childNodeChange._op
const objId = await _getChildChangeObjId(
change,
childNodeChange,
curNodePathVal,
reqData
reqData,
reqTarget
)
_formatCompositionValue(curChange, objId, childNodeChange, childNodeChanges)
}
Expand Down Expand Up @@ -234,6 +225,7 @@ const _formatCompositionEntityType = function (change) {

const _getObjectIdByPath = async function (
reqData,
reqTarget,
nodePathVal,
serviceEntityPath,
/**optional*/ objIdElementNames
Expand All @@ -243,20 +235,21 @@ const _getObjectIdByPath = async function (
const entityUUID = getUUIDFromPathVal(nodePathVal)
const obj = await getCurObjFromDbQuery(entityName, entityUUID)
const curObj = { curObjFromReqData, curObjFromDbQuery: obj }
return getObjectId(reqData, entityName, objIdElementNames, curObj)
return getObjectId(reqData, reqTarget, entityName, objIdElementNames, curObj)
}

const _formatObjectID = async function (changes, reqData) {
const _formatObjectID = async function (changes, reqData, reqTarget) {
const objectIdCache = new Map()
for (const change of changes) {
const path = change.serviceEntityPath.split('/')
const path = [...change.serviceEntityPath];
const curNodePathVal = path.pop()
const parentNodePathVal = path.pop()

let curNodeObjId = objectIdCache.get(curNodePathVal)
if (!curNodeObjId) {
curNodeObjId = await _getObjectIdByPath(
reqData,
reqTarget,
curNodePathVal,
change.serviceEntityPath
)
Expand All @@ -267,6 +260,7 @@ const _formatObjectID = async function (changes, reqData) {
if (!parentNodeObjId && parentNodePathVal) {
parentNodeObjId = await _getObjectIdByPath(
reqData,
reqTarget,
parentNodePathVal,
change.serviceEntityPath
)
Expand All @@ -281,7 +275,7 @@ const _formatObjectID = async function (changes, reqData) {

const _isCompositionContextPath = function (aPath, hasComp) {
if (!aPath) return
if (typeof aPath === 'string') aPath = aPath.split('/')
if (typeof aPath === 'string') aPath = JSON.parse(aPath)
if (aPath.length < 2) return false
const target = getEntityByContextPath(aPath, hasComp)
const parent = getEntityByContextPath(aPath.slice(0, -1), hasComp)
Expand All @@ -290,9 +284,9 @@ const _isCompositionContextPath = function (aPath, hasComp) {
}

const _formatChangeLog = async function (changes, req) {
await _formatObjectID(changes, req.data)
await _formatAssociationContext(changes, req.data)
await _formatCompositionContext(changes, req.data)
await _formatObjectID(changes, req.data, req.target)
await _formatAssociationContext(changes, req.data, req.target)
await _formatCompositionContext(changes, req.data, req.target)
}

const _afterReadChangeView = function (data, req) {
Expand All @@ -307,7 +301,7 @@ function _trackedChanges4 (srv, target, diff) {
if (!template.elements.size) return

const changes = []
diff._path = `${target.name}(${diff.ID})`
diff._path = [{target: target.name, key: getKey(target, diff)}];

templateProcessor({
template, row: diff, processFn: ({ row, key, element }) => {
Expand Down Expand Up @@ -363,13 +357,12 @@ const _prepareChangeLogForComposition = async function (entity, entityKey, chang

const parentEntityPathVal = rootEntityPathVals[rootEntityPathVals.length - 2]
const parentKey = getUUIDFromPathVal(parentEntityPathVal)
const serviceEntityPath = rootEntityPathVals.join('/')
const serviceEntityPath = [...rootEntityPathVals]
const parentServiceEntityPath = _getAllPathVals(req.context)
.slice(0, rootEntityPathVals.length - 2)
.join('/')

for (const change of changes) {
change.parentEntityID = await _getObjectIdByPath(req.data, parentEntityPathVal, parentServiceEntityPath)
change.parentEntityID = await _getObjectIdByPath(req.data, req.target, parentEntityPathVal, parentServiceEntityPath)
change.parentKey = parentKey
change.serviceEntityPath = serviceEntityPath
}
Expand All @@ -384,15 +377,15 @@ async function generatePathAndParams (req, entityKey) {
const { ID, foreignKey, parentEntity } = getAssociationDetails(target);
const hasParentAndForeignKey = parentEntity && data[foreignKey];
const targetEntity = hasParentAndForeignKey ? parentEntity : target;
const targetKey = hasParentAndForeignKey ? data[foreignKey] : entityKey;
const targetKey = hasParentAndForeignKey ? {ID: data[foreignKey]} : entityKey;

let compContext = {
path: hasParentAndForeignKey
? `${parentEntity.name}/${target.name}`
: `${target.name}`,
? [{target: parentEntity.name}, {target: target.name}]
: [{target: target.name}],
params: hasParentAndForeignKey
? [{ [ID]: data[foreignKey] }, { [ID]: entityKey }]
: [{ [ID]: entityKey }],
? [{ [ID]: data[foreignKey] }, entityKey]
: [ entityKey],
hasComp: true
};

Expand All @@ -404,7 +397,7 @@ async function generatePathAndParams (req, entityKey) {
while (parentAssoc && !parentAssoc.entity[isRoot]) {
parentAssoc = await processEntity(
parentAssoc.entity,
parentAssoc.ID,
parentAssoc.key,
compContext
);
}
Expand All @@ -418,15 +411,16 @@ async function processEntity (entity, entityKey, compContext) {
const parentResult =
(await SELECT.one
.from(entity.name)
.where({ [ID]: entityKey })
.where(entityKey)
.columns(foreignKey)) || {};
const hasForeignKey = parentResult[foreignKey];
if (!hasForeignKey) return;
compContext.path = `${parentEntity.name}/${compContext.path}`;
compContext.params.unshift({ [ID]: parentResult[foreignKey] });
const key = { [ID]: parentResult[foreignKey] };
compContext.path = [{target: parentEntity.name, key}, ...compContext.path];
compContext.params.unshift(key);
return {
entity: parentEntity,
[ID]: hasForeignKey ? parentResult[foreignKey] : undefined
key
};
}
}
Expand All @@ -441,28 +435,27 @@ function getAssociationDetails (entity) {
return { ID, foreignKey, parentEntity };
}


async function track_changes (req) {
let diff = await req.diff()
if (!diff) return

let target = req.target
let compContext = null;
let entityKey = diff.ID
let entityKey = getKey(req.target, diff)
const params = convertSubjectToParams(req.subject);
if (req.subject.ref.length === 1 && params.length === 1 && !target[isRoot]) {
compContext = await generatePathAndParams(req, entityKey);
}
let isComposition = _isCompositionContextPath(
compContext?.path || req.path,
compContext?.path || req.path.split("/").map(p => ({target: p})),
compContext?.hasComp
);
if (
req.event === "DELETE" &&
target[isRoot] &&
!cds.env.requires["change-tracking"]?.preserveDeletes
) {
return await DELETE.from(`sap.changelog.ChangeLog`).where({ entityKey });
return await DELETE.from(`sap.changelog.ChangeLog`).where({entityKey: flattenKey(entityKey)});
}

let changes = _trackedChanges4(this, target, diff)
Expand All @@ -471,9 +464,10 @@ async function track_changes (req) {
await _formatChangeLog(changes, req)
if (isComposition) {
let reqInfo = {
target: req.target,
data: req.data,
context: {
path: compContext?.path || req.path,
path: compContext?.path || req.path.split("/").map(p => ({target: p})),
params: compContext?.params || params,
event: req.event,
hasComp: compContext?.hasComp
Expand All @@ -482,12 +476,16 @@ async function track_changes (req) {
[ target, entityKey ] = await _prepareChangeLogForComposition(target, entityKey, changes, reqInfo)
}
const dbEntity = getDBEntity(target)


await INSERT.into("sap.changelog.ChangeLog").entries({
entity: dbEntity.name,
entityKey: entityKey,
entityKey: flattenKey(entityKey),
serviceEntity: target.name || target,
changes: changes.filter(c => c.valueChangedFrom || c.valueChangedTo).map((c) => ({
...c,
parentKey: flattenKey(c.parentKey),
entityKey: flattenKey(c.entityKey),
valueChangedFrom: `${c.valueChangedFrom ?? ''}`,
valueChangedTo: `${c.valueChangedTo ?? ''}`,
})),
Expand Down
Loading

0 comments on commit a20aeb4

Please sign in to comment.