Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

"Non-ID" keys #117

Open
wants to merge 15 commits into
base: main
Choose a base branch
from
129 changes: 63 additions & 66 deletions lib/change-log.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,37 +14,43 @@ const {
getObjIdElementNamesInArray,
getValueEntityType,
} = require("./entity-helper")

const {
getKey,
flattenKey,
getAssociationKey,
} = 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 +59,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};
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This illustrates one main change: paths are expressed as JSON arrays like

[{target: "RootEntity", key: {ID: "..."}}, {target: "Level1Entity", key: {ID: "..."}}]

instead of

"RootEntity(...)/Level1Entity(...)"

pathVals.push(entityPathVal)
}

Expand Down Expand Up @@ -88,23 +94,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 +110,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 +124,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 +139,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 +165,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 +226,7 @@ const _formatCompositionEntityType = function (change) {

const _getObjectIdByPath = async function (
reqData,
reqTarget,
nodePathVal,
serviceEntityPath,
/**optional*/ objIdElementNames
Expand All @@ -243,20 +236,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 +261,7 @@ const _formatObjectID = async function (changes, reqData) {
if (!parentNodeObjId && parentNodePathVal) {
parentNodeObjId = await _getObjectIdByPath(
reqData,
reqTarget,
parentNodePathVal,
change.serviceEntityPath
)
Expand All @@ -281,7 +276,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 +285,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 +302,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 +358,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 @@ -381,18 +375,18 @@ const _prepareChangeLogForComposition = async function (entity, entityKey, chang

async function generatePathAndParams (req, entityKey) {
const { target, data } = req;
const { ID, foreignKey, parentEntity } = getAssociationDetails(target);
const { foreignKey, parentEntity, assoc } = 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 }],
? [ getAssociationKey(assoc, data), entityKey]
: [ entityKey],
hasComp: true
};

Expand All @@ -404,29 +398,29 @@ async function generatePathAndParams (req, entityKey) {
while (parentAssoc && !parentAssoc.entity[isRoot]) {
parentAssoc = await processEntity(
parentAssoc.entity,
parentAssoc.ID,
parentAssoc.key,
compContext
);
}
return compContext;
}

async function processEntity (entity, entityKey, compContext) {
const { ID, foreignKey, parentEntity } = getAssociationDetails(entity);
const { foreignKey, parentEntity, assoc } = getAssociationDetails(entity);

if (foreignKey && parentEntity) {
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 = getAssociationKey(assoc, parentResult)
if (!key) return;
compContext.path = [{target: parentEntity.name, key}, ...compContext.path];
compContext.params.unshift(key);
return {
entity: parentEntity,
[ID]: hasForeignKey ? parentResult[foreignKey] : undefined
key
};
}
}
Expand All @@ -437,32 +431,30 @@ function getAssociationDetails (entity) {
const assoc = entity.elements[assocName];
const parentEntity = assoc?._target;
const foreignKey = assoc?.keys?.[0]?.$generatedFieldName;
const ID = assoc?.keys?.[0]?.ref[0] || 'ID';
return { ID, foreignKey, parentEntity };
return { foreignKey, parentEntity, assoc };
}


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 +463,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 +475,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
Loading