UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

197 lines (174 loc) 10.4 kB
'use strict'; 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;