-
Notifications
You must be signed in to change notification settings - Fork 8
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
nils
wants to merge
15
commits into
cap-js:main
Choose a base branch
from
nils:feat/non-ID-keys
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+429
−100
Open
"Non-ID" keys #117
Changes from 11 commits
Commits
Show all changes
15 commits
Select commit
Hold shift + click to select a range
a20aeb4
support complex keys
nils b217bc2
use getAssociationKey in more places
nils 2d42031
Update entity-helper.js
nkaputnik 31d60e5
Update complex-keys.test.js
nkaputnik f032969
Update complex-keys.test.js
nkaputnik 1d84b98
Update keys.js
nkaputnik 8f15562
parsePath/serializePath
nils aab3597
Merge branch 'feat/non-ID-keys' of https://github.com/nils/change-tra…
nils c33719f
remove unused/commented variables
nils feb4c18
serviceEntityPath tests
nils 4ccdf9e
complex keys composition deletion testcase
nils 74991dc
Merge branch 'main' into feat/non-ID-keys
nkaputnik c1badf2
fix ut error.
Sv7enNowitzki 9e728cf
Merge branch 'main' into feat/non-ID-keys
Sv7enNowitzki ecc2d30
Merge branch 'main' into feat/non-ID-keys
nkaputnik File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -14,37 +14,44 @@ const { | |
getObjIdElementNamesInArray, | ||
getValueEntityType, | ||
} = require("./entity-helper") | ||
|
||
const { | ||
getKey, | ||
stringifyKey, | ||
stringifyPath, | ||
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) | ||
} | ||
|
||
|
@@ -53,13 +60,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) | ||
} | ||
|
||
|
@@ -88,23 +95,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 | ||
|
@@ -121,7 +111,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 | ||
|
@@ -135,10 +125,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"] | ||
|
@@ -150,21 +140,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) { | ||
|
@@ -174,14 +166,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) | ||
} | ||
|
@@ -234,6 +227,7 @@ const _formatCompositionEntityType = function (change) { | |
|
||
const _getObjectIdByPath = async function ( | ||
reqData, | ||
reqTarget, | ||
nodePathVal, | ||
serviceEntityPath, | ||
/**optional*/ objIdElementNames | ||
|
@@ -243,20 +237,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 | ||
) | ||
|
@@ -267,6 +262,7 @@ const _formatObjectID = async function (changes, reqData) { | |
if (!parentNodeObjId && parentNodePathVal) { | ||
parentNodeObjId = await _getObjectIdByPath( | ||
reqData, | ||
reqTarget, | ||
parentNodePathVal, | ||
change.serviceEntityPath | ||
) | ||
|
@@ -281,7 +277,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) | ||
|
@@ -290,9 +286,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) { | ||
|
@@ -307,7 +303,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 }) => { | ||
|
@@ -363,13 +359,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 | ||
} | ||
|
@@ -381,18 +376,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 | ||
}; | ||
|
||
|
@@ -404,29 +399,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 | ||
}; | ||
} | ||
} | ||
|
@@ -437,32 +432,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: stringifyKey(entityKey)}); | ||
} | ||
|
||
let changes = _trackedChanges4(this, target, diff) | ||
|
@@ -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 | ||
|
@@ -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: stringifyKey(entityKey), | ||
serviceEntity: target.name || target, | ||
changes: changes.filter(c => c.valueChangedFrom || c.valueChangedTo).map((c) => ({ | ||
...c, | ||
serviceEntityPath: stringifyPath(c.serviceEntityPath), | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. As discussed, if there is a single-key situation, we now serialize paths to the 'old' format before saving to the db. |
||
parentKey: stringifyKey(c.parentKey), | ||
valueChangedFrom: `${c.valueChangedFrom ?? ''}`, | ||
valueChangedTo: `${c.valueChangedTo ?? ''}`, | ||
})), | ||
|
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
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
instead of
"RootEntity(...)/Level1Entity(...)"