@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
197 lines (174 loc) • 10.4 kB
JavaScript
;
const { isBuiltinType } = require('../base/builtins');
/**
* Processing the `@hierarchy` annotation on projections/views and generates:
* - the OData annotations: `@Aggregation.RecursiveHierarchy`, `@Hierarchy.RecursiveHierarchy`
* `@Capabilities.FilterRestrictions.NonFilterableProperties` and
* `@Capabilities.SortRestrictions.NonSortableProperties`
* - the computed elements: LimitedDescendantCount, DistanceFromRoot, DrillState, LimitedRank
* Logic is executed only when certain conditions are met:
* - annotated definition is a projection or view
* - if the `@hierarchy` annotation has a true value - exactly one managed association to self must exist
* - if the `@hierarchy` annotation has a reference as value - the reference must point to an existing managed association
* to self
* - the managed association has exactly one foreign key
* - the foreign key is of scalar type
* Otherwise, appropriate warnings/errors are raised.
*/
function generateFioriTreeViewAnnotationsAndFields(def, defName, messageFunctions, csnUtils, transformers, options) {
const { error, warning } = messageFunctions;
const { isAssociation } = csnUtils;
const { setAnnotation, addElement, createScalarElement } = transformers;
const isODataTransformation = options.transformation === 'odata';
if (Object.keys(def).some(key => key.startsWith('@hierarchy#'))) {
if (isODataTransformation)
error(null, [ 'definitions', defName ], {}, 'Assigning qualifier with the @hierarchy annotation is not supported');
return;
}
// supported only on projections or views
if (!(def.projection || def.query)) {
if (isODataTransformation)
warning(null, [ 'definitions', defName ], {}, 'Annotation @hierarchy is only supported on projections and views');
return;
}
// collect all managed and unmanaged associations pointing to self
const mngAssocsToSelf = [];
const unmgAssocsToSelf = [];
Object.entries(def.elements).forEach(([ name, elem ]) => {
// if (isManagedAssociation(elem) && elem.target === defName && ![ 'DraftAdministrativeData' ].includes(name))
if (isAssociation(elem) && elem.keys && elem.target === defName && ![ 'DraftAdministrativeData' ].includes(name))
mngAssocsToSelf.push([ name, elem ]);
else if (isAssociation(elem) && elem.on &&
elem.target === defName && ![ 'SiblingEntity' ].includes(name))
unmgAssocsToSelf.push([ name, elem ]);
});
// if the @hierarchy annotation has a true value, we need to make sure the definition has exacly one
// association pointing to self and has a single foreign key that is of scalar type
if (typeof def['@hierarchy'] === 'boolean' && def['@hierarchy'] === true) {
if (mngAssocsToSelf.length > 1) {
if (isODataTransformation)
warning(null, [ 'definitions', defName ], {}, 'Annotation @hierarchy with value true is ignored as multiple managed associations to self exist');
return;
}
else if (mngAssocsToSelf.length === 0) {
if (unmgAssocsToSelf.length === 1) {
if (isODataTransformation) {
error(null, unmgAssocsToSelf[0][1].$path || [ 'definitions', defName, 'elements', unmgAssocsToSelf[0][0] ],
{ name: unmgAssocsToSelf[0][0] }, 'Annotation @hierarchy is not supported for unmanaged association $(NAME)');
}
}
if (isODataTransformation)
warning(null, [ 'definitions', defName ], {}, 'Annotation @hierarchy with value true is ignored as no managed association to self exists');
return;
}
const assoc = mngAssocsToSelf[0][1];
const assocName = mngAssocsToSelf[0][0];
if (isValidHierarchyAssociation(assoc, assocName)) {
if (isODataTransformation)
addHierarchyAnnotations(def, defName, assoc.keys[0].ref.join(), assocName);
addHierarchyFields(def, defName);
}
}
else if (typeof def['@hierarchy'] === 'object' && def['@hierarchy']['=']) {
const assocName = def['@hierarchy']['='];
const unmngAssoc = unmgAssocsToSelf.find(a => a[0] === assocName);
if (unmngAssoc) {
if (isODataTransformation) {
error(null, unmngAssoc[1].$path || [ 'definitions', defName, 'elements', unmngAssoc[0] ],
{ name: unmngAssoc[0] }, 'Annotation @hierarchy is not supported for unmanaged association $(NAME)');
}
return;
}
const assoc = mngAssocsToSelf.find(a => a[0] === assocName);
if (!assoc) {
if (isODataTransformation)
warning(null, [ 'definitions', defName ], { name: assocName }, 'Annotation @hierarchy refers to a non-existing managed association $(NAME)');
return;
}
if (isValidHierarchyAssociation(assoc[1], assocName)) {
if (isODataTransformation)
addHierarchyAnnotations(def, defName, assoc[1].keys[0].ref.join(), assocName);
addHierarchyFields(def, defName);
}
}
// returns true if assoc has one key and that one key is scalar, otherwise report a warning and return false
function isValidHierarchyAssociation(assoc, assocName) {
// the association must have exactly one scalar foreign key
if (assoc.keys.length > 1) {
if (isODataTransformation) {
warning(null, assoc.$path || [ 'definitions', defName, 'elements', assocName ], { name: assocName },
'Annotation @hierarchy is ignored as the managed association $(NAME) has multiple foreign keys');
}
return false;
}
const fkName = assoc.keys[0].ref.join();
const defKey = Object.entries(def.elements).find(([ name, elem ]) => elem.key && name === fkName);
if (defKey && !isBuiltinType(defKey[1].type)) {
if (isODataTransformation) {
warning(null, assoc.$path || [ 'definitions', defName, 'elements', assocName ], { name: assocName },
'Annotation @hierarchy is ignored as the foreign key of the managed association $(NAME) is not of a scalar type');
}
return false;
}
return true;
}
function addHierarchyAnnotations(def, defName, defKeyName, assocName) {
const qualifier = `${ defName.split('.').pop() }Hierarchy`;
const hierarchyProps = [ 'LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank' ];
[ `@Aggregation.RecursiveHierarchy#${ qualifier }`,
`@Aggregation.RecursiveHierarchy#${ qualifier }.NodeProperty`,
`@Aggregation.RecursiveHierarchy#${ qualifier }.ParentNavigationProperty`,
`@Hierarchy.RecursiveHierarchy#${ qualifier }`,
`@Hierarchy.RecursiveHierarchy#${ qualifier }.LimitedDescendantCount`,
`@Hierarchy.RecursiveHierarchy#${ qualifier }.DistanceFromRoot`,
`@Hierarchy.RecursiveHierarchy#${ qualifier }.DrillState`,
`@Hierarchy.RecursiveHierarchy#${ qualifier }.LimitedRank` ].forEach(anno => checkAndReportErrorWhenAnnotationExists(def, defName, anno));
setAnnotation(def, `@Aggregation.RecursiveHierarchy#${ qualifier }.NodeProperty`, { '=': defKeyName });
setAnnotation(def, `@Aggregation.RecursiveHierarchy#${ qualifier }.ParentNavigationProperty`, { '=': assocName });
setAnnotation(def, `@Hierarchy.RecursiveHierarchy#${ qualifier }.LimitedDescendantCount`, { '=': 'LimitedDescendantCount' });
setAnnotation(def, `@Hierarchy.RecursiveHierarchy#${ qualifier }.DistanceFromRoot`, { '=': 'DistanceFromRoot' });
setAnnotation(def, `@Hierarchy.RecursiveHierarchy#${ qualifier }.DrillState`, { '=': 'DrillState' });
setAnnotation(def, `@Hierarchy.RecursiveHierarchy#${ qualifier }.LimitedRank`, { '=': 'LimitedRank' });
// if @Capabilities.FilterRestrictions.NonFilterableProperties or @Capabilities.SortRestrictions.NonSortableProperties
// are already defined, we append to the existing value the created elements
if (def['@Capabilities.FilterRestrictions.NonFilterableProperties'] && Array.isArray(def['@Capabilities.FilterRestrictions.NonFilterableProperties']))
def['@Capabilities.FilterRestrictions.NonFilterableProperties'].push(...hierarchyProps);
else
setAnnotation(def, '@Capabilities.FilterRestrictions.NonFilterableProperties', [ ...hierarchyProps ]);
if (def['@Capabilities.SortRestrictions.NonSortableProperties'] && Array.isArray(def['@Capabilities.SortRestrictions.NonSortableProperties']))
def['@Capabilities.SortRestrictions.NonSortableProperties'].push(...hierarchyProps);
else
setAnnotation(def, '@Capabilities.SortRestrictions.NonSortableProperties', [ ...hierarchyProps ]);
}
function addHierarchyFields(def, defName) {
// add elements in elements dictionary
const limitedDescendantCount = createScalarElement('LimitedDescendantCount', 'cds.Integer');
limitedDescendantCount.LimitedDescendantCount['@Core.Computed'] = true;
limitedDescendantCount.LimitedDescendantCount.$calc = { val: null };
addElement(limitedDescendantCount, def, defName);
const distanceFromRoot = createScalarElement('DistanceFromRoot', 'cds.Integer');
distanceFromRoot.DistanceFromRoot['@Core.Computed'] = true;
distanceFromRoot.DistanceFromRoot.$calc = { val: null };
addElement(distanceFromRoot, def, defName);
const drillState = createScalarElement('DrillState', 'cds.String');
drillState.DrillState['@Core.Computed'] = true;
drillState.DrillState.$calc = { val: null };
addElement(drillState, def, defName);
const limitedRank = createScalarElement('LimitedRank', 'cds.Integer');
limitedRank.LimitedRank['@Core.Computed'] = true;
limitedRank.LimitedRank.$calc = { val: null };
addElement(limitedRank, def, defName);
// add fields in the query columns
if (def.query && def.query.SELECT && def.query.SELECT.columns) {
def.query.SELECT.columns.push({ val: null, as: 'LimitedDescendantCount', cast: { type: 'cds.Integer' } });
def.query.SELECT.columns.push({ val: null, as: 'DistanceFromRoot', cast: { type: 'cds.Integer' } });
def.query.SELECT.columns.push({ val: null, as: 'DrillState', cast: { type: 'cds.String' } });
def.query.SELECT.columns.push({ val: null, as: 'LimitedRank', cast: { type: 'cds.Integer' } });
}
}
function checkAndReportErrorWhenAnnotationExists(def, defName, anno) {
if (def[anno])
error(null, [ 'definitions', defName ], { anno, name: defName }, 'Annotation $(ANNO) is already defined on the hierarchy entity $(NAME)');
}
}
module.exports = generateFioriTreeViewAnnotationsAndFields;