@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
334 lines (303 loc) • 14.3 kB
JavaScript
'use strict';
const {
applyTransformationsOnNonDictionary, isAssociationOperand, isDollarSelfOrProjectionOperand,
} = require('../../model/csnUtils');
const { setProp } = require('../../base/model');
const { forEach } = require('../../utils/objectUtils');
const { cloneCsnNonDict } = require('../../model/cloneCsn');
const { ModelError } = require('../../base/error');
/**
* Get a function that transforms $self backlinks
* @param {object} csnUtils
* @param {object} messageFunctions
* @param {CSN.Options} options
* @param {string} pathDelimiter
* @param {boolean} doA2J
* @returns {import('../../model/csnUtils').genericCallback} callback for forEachDefinition
*/
function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimiter, doA2J = true ) {
let prepend$self = false;
return transformSelfInBacklinks;
/**
* @param {CSN.Artifact} artifact
* @param {string} artifactName
* @param {any} dummy unused Parameter
* @param {CSN.Path} path
*/
function transformSelfInBacklinks( artifact, artifactName, dummy, path ) {
prepend$self = false;
// Fixme: For toHana mixins must be transformed, for toSql -d hana
// mixin elements must be transformed, why can't toSql also use mixins?
if (options.transformation === 'effective' && artifact.elements || artifact.kind === 'entity' || artifact.query || (options.forHana && options.sqlMapping === 'hdbcds' && artifact.kind === 'type'))
processDict(artifact.elements, path.concat([ 'elements' ]));
if (artifact.query?.SELECT?.mixin) {
prepend$self = options.transformation === 'effective';
processDict(artifact.query.SELECT.mixin, path.concat([ 'query', 'SELECT', 'mixin' ]));
}
/**
* Loop over the dict and start the processing.
*
* @param {object} dict .elements or .mixin
* @param {Array} subPath Path into the dict
*/
function processDict( dict, subPath ) {
forEach(dict, (elemName, elem) => {
if (elem.on && csnUtils.isAssocOrComposition(elem))
processBacklinkAssoc(elem, elemName, artifact, artifactName, subPath.concat([ elemName, 'on' ]));
});
}
}
/**
* If the association element 'elem' of 'art' is a backlink association, massage its ON-condition
* (in place) so that it
* - compares the generated foreign key fields of the corresponding forward
* association with their respective keys in 'art' (for managed forward associations)
* - contains the corresponding forward association's ON-condition in "reversed" form,
* i.e. as seen from 'elem' (for unmanaged associations)
* Otherwise, do nothing.
* @param {CSN.Element} elem
* @param {string} elemName
* @param {CSN.Artifact} art
* @param {string} artName
* @param {CSN.Path} pathToOn
*/
function processBacklinkAssoc( elem, elemName, art, artName, pathToOn ) {
// Don't add braces if it is a single expression (ignoring superfluous braces)
// TODO: This check is too simplistic and probably adds superfluous parentheses.
const multipleExprs = elem.on.length > 3;
elem.on = processExpressionArgs(elem.on, pathToOn);
const column = csnUtils.getColumn(elem);
if (column?.cast?.on) // avoid difference between column and element
column.cast.on = elem.on;
/**
* Process the args
*
* @param {Array} xprArgs
* @param {CSN.Path} path
* @returns {Array} Array of parsed expression
*/
function processExpressionArgs( xprArgs, path ) {
const result = [];
let i = 0;
while (i < xprArgs.length) {
// Only token tripel `<path>, '=', <path>` are of interest here
if (i < xprArgs.length - 2 && xprArgs[i + 1] === '=') {
// Check if one side is $self and the other an association
// (if so, replace all three tokens with the condition generated from the other side, in parentheses)
if (isDollarSelfOrProjectionOperand(xprArgs[i]) && isAssociationOperand(xprArgs[i + 2], path.concat([ i + 2 ]), csnUtils.inspectRef)) {
const assoc = csnUtils.inspectRef(path.concat([ i + 2 ])).art;
const backlinkName = xprArgs[i + 2].ref[xprArgs[i + 2].ref.length - 1];
const comparison = transformDollarSelfComparison(
xprArgs[i + 2], assoc, backlinkName,
elem, elemName, art, artName, path.concat([ i ])
);
if (multipleExprs)
result.push({ xpr: comparison });
else
result.push(...comparison);
i += 3;
attachBacklinkInformation(backlinkName);
}
else if (isDollarSelfOrProjectionOperand(xprArgs[i + 2]) && isAssociationOperand(xprArgs[i], path.concat([ i ]), csnUtils.inspectRef)) {
const assoc = csnUtils.inspectRef(path.concat([ i ])).art;
const backlinkName = xprArgs[i].ref[xprArgs[i].ref.length - 1];
const comparison = transformDollarSelfComparison(xprArgs[i], assoc, backlinkName, elem, elemName, art, artName, path.concat([ i + 2 ]));
if (multipleExprs)
result.push({ xpr: comparison });
else
result.push(...comparison);
i += 3;
attachBacklinkInformation(backlinkName);
}
// Otherwise take one (!) token unchanged
else {
result.push(xprArgs[i]);
i++;
}
}
// Process subexpressions - but keep them as subexpressions
else if (xprArgs[i].xpr) {
result.push({ xpr: processExpressionArgs(xprArgs[i].xpr, path.concat([ i, 'xpr' ])) });
i++;
}
// Take all other tokens unchanged
else {
result.push(xprArgs[i]);
i++;
}
}
return result;
/**
* The knowledge whether an association was an `<up_>` association in a
* `$self = <comp>.<up_>` comparison, is important for the foreign key constraints.
* By the time we generate them, such on-conditions are already transformed
* --> no more `$self` in the on-conditions, that is why we need to remember it here.
*
* @param {string} backlinkName name of `<up_>` in a `$self = <comp>.<up_>` comparison
*/
function attachBacklinkInformation( backlinkName ) {
if (elem.$selfOnCondition) {
elem.$selfOnCondition.up_.push(backlinkName);
}
else {
setProp(elem, '$selfOnCondition', {
up_: [ backlinkName ],
});
}
}
}
}
/**
* Return the condition to replace the comparison `<assocOp> = $self` in the ON-condition
* of element <elem> of artifact 'art'. If there is anything to complain, use location <loc>
*
* @param {any} assocOp
* @param {CSN.Element} assoc
* @param {string} assocName
* @param {CSN.Element} elem
* @param {string} elemName
* @param {CSN.Artifact} art
* @param {string} artifactName
* @param {CSN.Path} path
* @returns {Array} New on-condition
*/
function transformDollarSelfComparison( assocOp, assoc, assocName, elem, elemName, art, artifactName, path ) {
// Check: The forward link <assocOp> must point back to this artifact
// FIXME: Unfortunately, we can currently only check this for non-views (because when a view selects
// a backlink association element from an entity, the forward link will point to the entity,
// not to the view).
// FIXME: This also means that corresponding key fields should be in the select list etc ...
if (!art.query && !art.projection && assoc.target && assoc.target !== artifactName) {
messageFunctions.error( null, path, { id: '$self', name: artifactName, target: assoc.target },
'Expected association using $(ID) to point back to $(NAME) but found $(TARGET)' );
}
// Check: The forward link <assocOp> must not contain '$self' in its own ON-condition
if (assoc.on) {
const containsDollarSelf = assoc.on.some(isDollarSelfOrProjectionOperand);
if (containsDollarSelf)
messageFunctions.error('type-invalid-self', path, { name: '$self' });
}
if (!assoc.keys && !assoc.on) {
// Interpret no ON-condition/no keys like empty 'keys'.
if (options.transformation !== 'effective')
elem.$ignore = true;
return [];
}
if (assoc.keys) {
// Transform comparison of $self to managed association into AND-combined foreign key comparisons
if (assoc.keys.length > 0)
return transformDollarSelfComparisonWithManagedAssoc(assocOp, assoc, assocName, elemName, art, path);
if (options.transformation !== 'effective')
elem.$ignore = true;
return [];
}
else if (assoc.on) {
// Transform comparison of $self to unmanaged association into "reversed" ON-condition
return transformDollarSelfComparisonWithUnmanagedAssoc(assocOp, assoc, assocName, elemName, art, path);
}
throw new ModelError(`Expected either managed or unmanaged association in $self-comparison: ${ JSON.stringify(elem.on) }`);
}
/**
* For a condition `<elemName>.<assoc> = $self` in the ON-condition of element <elemName>,
* where <assoc> is a managed association, return a condition comparing the generated
* foreign key elements <elemName>.<assoc>_<fkey1..n> of <assoc> to the corresponding
* keys in this artifact.
* For example, `ON elem.ass = $self` becomes `ON elem.ass_key1 = key1 AND elem.ass_key2 = key2`
* (assuming that `ass` has the foreign keys `key1` and `key2`)
* @param {any} assocOp
* @param {CSN.Element} assoc
* @param {string} originalAssocName
* @param {string} elemName
* @param {CSN.Artifact} art
* @param {CSN.Path} path
* @returns {Array} New on-condition
*/
function transformDollarSelfComparisonWithManagedAssoc( assocOp, assoc, originalAssocName, elemName, art, path) {
const conditions = [];
// if the element was structured then it was flattened => change of the delimiter from '.' to '_'
// this is done in the flattening, but as we do not alter the onCond itself there should be done here as well
const assocName = originalAssocName.replace(/\./g, pathDelimiter);
elemName = elemName.replace(/\./g, pathDelimiter);
assoc.keys.forEach((k) => {
// Depending on naming conventions, the foreign key may two path steps (hdbcds) or be a single path step with a flattened name (plain, quoted)
// With to.hdbcds in conjunction with hdbcds naming, we need to NOT use the alias - else we get deployment errors
const keyName = k.as && doA2J ? [ k.as ] : k.ref;
const fKeyPath = !doA2J ? [ assocName, ...keyName ] : [ `${ assocName }${ pathDelimiter }${ keyName[0] }` ];
// FIXME: _artifact to the args ???
const a = [
{
ref: [ elemName, ...fKeyPath ],
},
{ ref: k.ref },
];
if (prepend$self)
a[1].ref = [ '$self', ...a[1].ref ];
// Not without a2j so we can rely on a certain model state
if (doA2J && prepend$self && art.elements[k.ref[1]] || !prepend$self && !art.elements[k.ref[0]])
messageFunctions.message('ref-missing-self-counterpart', path, { prop: k.ref[0], name: assocName });
conditions.push([ a[0], '=', a[1] ]);
});
return conditions.reduce((prev, current) => {
if (prev.length === 0)
return [ ...current ];
return [ ...prev, 'and', ...current ];
}, []);
}
/**
* For a condition `<elemName>.<assoc> = $self` in the ON-condition of element <elemName>,
* where <assoc> is an unmanaged association, return the ON-condition of <assoc> as it would
* be written from the perspective of the artifact containing association <elemName>.
* For example, `ON elem.ass = $self` becomes `ON a = elem.x AND b = elem.y`
* (assuming that `ass` has the ON-condition `ON ass.a = x AND ass.b = y`)
*
* @param {any} assocOp
* @param {CSN.Element} assoc
* @param {string} originalAssocName
* @param {string} elemName
* @param {CSN.Artifact} art
* @param {CSN.Path} path
* @returns {Array} New on-condition
*/
function transformDollarSelfComparisonWithUnmanagedAssoc( assocOp, assoc, originalAssocName, elemName, art, path ) {
// if the element was structured then it may have been flattened => change of the delimiter from '.' to '_'
// this is done in the flattening, but as we do not alter the onCond itself there should be done here as well
elemName = elemName.replace(/\./g, pathDelimiter);
const assocName = originalAssocName.replace(/\./g, pathDelimiter);
// clone the onCond for later use in the path transformation
const newOnCond = cloneCsnNonDict(assoc.on, options);
applyTransformationsOnNonDictionary({ on: newOnCond }, 'on', {
ref: (parent, prop, ref) => {
let sourceSide = false;
// we are in the "path" from the forwarding assoc => need to remove the first part of the path
if (ref[0] === assocName) {
ref.shift();
if (prepend$self)
ref.unshift('$self');
sourceSide = true;
}
else if (ref.length > 1 && ref[0] === '$self' && ref[1] === assocName) {
// We could also have a $self in front of the assoc name - so we would need to shift twice
ref.shift();
ref.shift();
if (prepend$self)
ref.unshift('$self');
sourceSide = true;
}
else { // we are in the backlink assoc "path" => need to push at the beginning the association's id
ref.unshift(elemName);
// if there was a $self identifier in the forwarding association onCond
// we do not need it anymore, as we prepended in the previous step the back association's id
if (ref[1] === '$self')
ref.splice(1, 1);
}
// Not without a2j so we can rely on a certain model state
if (doA2J && sourceSide && (prepend$self && !art.elements[ref[1]] || !prepend$self && !art.elements[ref[0]]))
messageFunctions.message('ref-missing-self-counterpart', path, { '#': 'unmanaged', prop: ref[0], name: assocName });
},
});
return newOnCond;
}
}
module.exports = {
getBacklinkTransformer,
};