@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
125 lines (108 loc) • 4.7 kB
JavaScript
;
const { applyTransformations, transformAnnotationExpression, implicitAs } = require('../../model/csnUtils');
/**
* If a path in an annotation expression can be interpreted as accessing a local foreign key, then
* the foreign key reference is replaced with the foreign key itself.
* Exception is when the association path step has a filter.
*
* @param {CSN} csn
* @param {Object} csnUtils
* @param {Object} iterateOptions
*/
function replaceForeignKeyRefsInExpressionAnnotations(csn, csnUtils, iterateOptions = {}) {
const transformers = {
'@': processRef,
};
applyTransformations(csn, transformers, [ processRef ], iterateOptions);
function processRef(parent, prop, _dict, path) {
transformAnnotationExpression(parent, prop, {
ref: (parent, _prop, ref, path, _p, _ppn, ctx) => {
const { art, links }
= parent._art && parent._links
? { art: parent._art, links: parent._links }
: csnUtils.inspectRef(path);
// if a reference points to a structure(managed assoc or structured element), then we do not process
// as we can't guess which specific foreign key is targeted
if (
!art ||
csnUtils.isManagedAssociation(art) ||
csnUtils.isStructured(art)
)
return;
const modifiedRef = replaceRefsWithFKs(ref, links, art);
// update the ref and string token to true if there was FK replacement
if (
modifiedRef.length !== ref.length ||
!modifiedRef.every((val, index) => val === ref[index])
) {
parent.ref = modifiedRef;
if (ctx?.annoExpr?.['='])
ctx.annoExpr['='] = true;
}
},
},
path);
}
// Replace references to foreign keys
function replaceRefsWithFKs(originalRef, links, expectedFkArt) {
let result = [ ...originalRef ];
// stringify the tail of the ref for finding the potential foreign key
const refTail = [ originalRef[originalRef.length - 1] ];
for (let i = originalRef.length - 2; i >= 0; i--) {
const currentRef = originalRef[i];
const currentLink = links[i].art;
// skip processing if the current reference is a filter
if (typeof currentRef !== 'string')
return result;
// check if the current link is a managed association
if (csnUtils.isManagedAssociation(currentLink)) {
const matchedForeignKey = findMatchingForeignKeyForAssoc(currentLink, currentRef, refTail, expectedFkArt);
if (matchedForeignKey) {
// update the result and refTailAsStr with the matched foreign key
result = [ ...result.slice(0, i), matchedForeignKey.name ];
refTail.unshift(currentRef);
}
else {
return result; // return if no matching foreign key is found
}
}
else {
// update refTail for non-association links
refTail.unshift(currentRef);
}
}
return result;
}
// Lookup the foreign key in the association's generated foreign keys
function findMatchingForeignKeyForAssoc(assoc, assocName, refTail, expectedFkArt) {
const expectedFkName = getExpectedForeignKeyName(assoc, assocName, refTail);
const matchedFk = assoc.$generatedForeignKeys?.find(fk => fk.source === expectedFkArt && fk.name === expectedFkName);
return matchedFk;
}
// Generate the expected foreign key name, considering aliases, tuple expansion name changes, etc.
function getExpectedForeignKeyName(assoc, assocName, refTail) {
const refAliasMapping = assoc.keys.reduce( (acc, key) => {
acc[key.ref.join('_')] = key.as || implicitAs(key.ref);
return acc;
}, {});
// generate the string representation of the reference tail
const refTailAsStr = replaceRefsIfAliased(refTail, refAliasMapping) || refTail.join('_');
return `${ assocName }_${ refTailAsStr }`;
}
// Check if any prefix of refTail matches an alias in refAliasMapping and replace it
function replaceRefsIfAliased(refTail, refAliasMapping) {
// loop through refTail and try to find a match in the refAliasMapping
// no need to look for the longest match as it is not allowed to declare
// duplicate key references in one FKs scope
let candidate = '';
for (let idx = 0; idx < refTail.length; idx++) {
candidate = candidate ? `${ candidate }_${ refTail[idx] }` : refTail[idx];
if (refAliasMapping[candidate]) {
refTail.splice(0, idx + 1, refAliasMapping[candidate]);
return refTail.join('_');
}
}
return undefined;
}
}
module.exports = replaceForeignKeyRefsInExpressionAnnotations;