UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

1,161 lines (1,040 loc) 100 kB
'use strict'; /* eslint max-statements-per-line:off */ const { setProp, isBetaEnabled } = require('../base/model'); const { forEachDefinition, forEachGeneric, forEachMemberRecursively, isEdmPropertyRendered, getUtils, applyTransformations, applyTransformationsOnNonDictionary, transformAnnotationExpression, findAnnotationExpression, cardinality2str, } = require('../model/csnUtils'); const { isBuiltinType, isMagicVariable } = require('../base/builtins'); const edmUtils = require('./edmUtils.js'); const edmAnnoPreproc = require('./edmAnnoPreprocessor.js'); const { inboundQualificationChecks } = require('./edmInboundChecks.js'); const typesExposure = require('../transform/odata/typesExposure'); const expandCSNToFinalBaseType = require('../transform/odata/toFinalBaseType'); const { cloneCsnNonDict, cloneAnnotationValue } = require('../model/cloneCsn'); const { forEach, forEachKey } = require('../utils/objectUtils.js'); const NavResAnno = '@Capabilities.NavigationRestrictions.RestrictedProperties'; // Capabilities that can be pulled up to NavigationRestrictions const capabilities = Object.keys(require('../gen/Dictionary.json') .types['Capabilities.NavigationPropertyRestriction'].Properties) .filter(c => ![ 'NavigationProperty', 'Navigability' ].includes(c)) .map(c => `@Capabilities.${ c }`); /** * edmPreprocessor warms up the model so that it can be converted into an EDM document and * contains all late & application specific model transformations * that should NOT become persistent in the published CSN model but only * be presented in the resulting EDM files. These late tweaks or mods can * be dependent to EDM version. * * @param {CSN.Model} csn * @param {object} _options */ function initializeModel( csn, _options, messageFunctions, requestedServiceNames = undefined ) { const { info, warning, error, message, } = messageFunctions; const special$self = !csn?.definitions?.$self && '$self'; const csnUtils = getUtils(csn); // proxies are merged into the final model after all proxy elements are collected const proxyCache = []; // iterate only over those definitions that need to be preprocessed // instead of mangling through the whole model each time // preprocess steps removing adding to the model must co-modify this map const reqDefs = { definitions: Object.create(null) }; // make sure options are complete const options = edmUtils.validateOptions(_options); const [ serviceRoots, serviceRootNames, fallBackSchemaName, whatsMyServiceRootName ] = getAnOverviewOnTheServices(); if (serviceRootNames.length === 0) return [ serviceRoots, Object.create(null), reqDefs, whatsMyServiceRootName, fallBackSchemaName, options ]; if (requestedServiceNames === undefined) requestedServiceNames = options.serviceNames; if (requestedServiceNames === undefined) requestedServiceNames = serviceRootNames; function isMyServiceRequested( n ) { return requestedServiceNames.includes(whatsMyServiceRootName(n)); } // Structural CSN inbound QA checks inboundQualificationChecks(csn, options, messageFunctions, serviceRootNames, requestedServiceNames, isMyServiceRequested, whatsMyServiceRootName, csnUtils); // not needed at the moment // resolveForeignKeyRefs(); if (isBetaEnabled(options, undefined)) splitDottedDefinitionsIntoSeparateServices(); else /* Replace dots with underscores for all definitions below a context or a service and rewrite refs and targets. MUST be done before type exposure. */ renameDottedDefinitionsInsideServiceOrContext(); /* Final base type expansion is required here when: 1) The input CSN was already transformed for V4 but shall be rendered in V2 and the edmx generator is called directly (bypassing OData transformation) 2) The input CSN was already transformed for V4 and persisted (all non-enumerables are stripped of) 3) call via cdsc At the end of the day, this module must be called only here, in the renderer and removed as a step in the OData transformer with the goal to have a protocol agnostic OData CSN. */ if (csn.meta && csn.meta.options && csn.meta.options.odataVersion === 'v4' && options.isV2()) { const { toFinalBaseType } = require('../transform/transformUtils').getTransformers(csn, options, messageFunctions); expandCSNToFinalBaseType(csn, { toFinalBaseType }, csnUtils, serviceRootNames, options); } /* Enrich the CSN by de-anonymizing and exposing types that are required to make the service self contained. */ // add cds.Map structured type into definitions to allow types exposure to pull in this type if (options.isV4() && csn.definitions['cds.Map'] == null) { const cdsMap = { '@open': true, elements: Object.create(null) }; setProp(cdsMap, '$emtpyMapType', true); csn.definitions['cds.Map'] = cdsMap; } const schemas = typesExposure(csn, whatsMyServiceRootName, requestedServiceNames, fallBackSchemaName, options, csnUtils, { error }); if (options.isV4() && csn.definitions['cds.Map']?.$emptyMapType) csn.definitions['cds.Map'] = undefined; // Get an overview about all schemas (including the services) const schemaNames = [ ...serviceRootNames ]; schemaNames.push(...Object.keys(schemas)); // sort schemas in reverse order to allow longest match in whatsMySchemaName function schemaNames.sort((a, b) => b.length - a.length); function whatsMySchemaName( n ) { return schemaNames.reduce((rc, sn) => (!rc && n && n.startsWith(`${ sn }.`) ? sn : rc), undefined); } if (schemaNames.length) { forEachDefinition(csn, [ attachNameProperty, (def, defName) => { const mySchemaName = whatsMySchemaName(defName); if (mySchemaName) setProp(def, '$mySchemaName', mySchemaName); if (isMyServiceRequested(defName) && def.kind !== 'aspect' && def.kind !== 'event') reqDefs.definitions[defName] = def; }, linkAssociationTarget, ]); forEachGeneric(csn, 'vocabularies', (term, termName) => { const mySchemaName = whatsMySchemaName(termName); if (mySchemaName) setProp(term, '$mySchemaName', mySchemaName); }); // initialize requested services const skip = { skipArtifact: (_def, defName) => !isMyServiceRequested(defName) }; forEachDefinition({ definitions: serviceRoots }, initService, skip); // Create data structures for containments forEachDefinition(reqDefs, initContainments); // Initialize entities with parameters (add Parameter entity) forEachDefinition(reqDefs, initParameterizedEntityOrView); // Initialize structures forEachDefinition(csn, initStructure); // Initialize associations after _parent linking forEachDefinition(reqDefs, initConstraints); // Mute V4 elements depending on constraint preparation if (options.isV4()) forEachDefinition(reqDefs, ignoreProperties); // calculate constraints based on ignoreProperties and initConstraints forEachDefinition(reqDefs, finalizeConstraints); // convert exposed types into cross schema references if required // must be run before proxy exposure to avoid potential reference collisions convertExposedTypesOfOtherServicesIntoCrossReferences(); // create association target proxies (v4) // Decide if an entity set needs to be constructed or not forEachDefinition(reqDefs, [ exposeTargetsAsProxiesOrSchemaRefs, determineEntitySet, annotateOptionalActFuncParams, ]); // finalize proxy creation mergeProxiesIntoModel(); // Calculate NavPropBinding Target paths // Rewrite @Capabilities for containment mode if (options.isV4()) { forEachDefinition(reqDefs, [ initEdmNavPropBindingTargets, pullupCapabilitiesAnnotations, ]); } // Things that can be done in one pass // Create edmKeyRefPaths // Create V4 NavigationPropertyBindings, requires determineEntitySet & initEdmNavPropBindingTargets // Map /** doc comments */ to @CoreDescription forEachDefinition(reqDefs, [ initEdmKeyRefPaths, initEdmNavPropBindingPaths, finalize, ]); } return [ serviceRoots, schemas, reqDefs, whatsMyServiceRootName, fallBackSchemaName, options, ]; // //////////////////////////////////////////////////////////////////// // // Service initialization starts here // function getAnOverviewOnTheServices( ) { const defs = csn.definitions || {}; const sroots = Object.create(null); for (const defName in defs) { const def = defs[defName]; if (def && def.kind === 'service') sroots[defName] = Object.assign(def, { name: defName }); } // first of all we need to know about all 'real' user defined services const srootNames = Object.keys(sroots).sort((a, b) => b.length - a.length); // find a globally unambiguous schema name to collect all top level 'root' types // TODO: work on service basis (this requires post exposure renaming) let fbSchemaName = 'root'; let i = 1; const defNames = Object.keys(defs); // eslint-disable-next-line no-loop-func while (defNames.some((artName) => { const p = artName.split('.'); return p.length === 2 && p[0] === fbSchemaName; })) fbSchemaName = `root${ i++ }`; return [ sroots, srootNames, fbSchemaName, // whatsMyServiceRootName ( n, self = true ) => srootNames.reduce((rc, sn) => (!rc && n && n.startsWith(`${ sn }.`) || (n === sn && self) ? sn : rc), undefined), ]; } /* Replace dots in sub-service and sub-context definitions with underscores to be Odata ID compliant. Replace the definitions in csn.definitions (such that linkAssociationTarget works) All type refs and assoc targets must also be adjusted to refer to the new names. */ function renameDottedDefinitionsInsideServiceOrContext() { // Find the first definition above the current definition or undefined otherwise. // Definition can either be a context or a service function getRootDef( name ) { const scopeKinds = { service: 1, context: 1 }; let pos = name.lastIndexOf('.'); name = pos < 0 ? undefined : name.substring(0, pos); while (name && !((csn.definitions[name] && csn.definitions[name].kind) in scopeKinds)) { pos = name.lastIndexOf('.'); name = pos < 0 ? undefined : name.substring(0, pos); } return name; } const dotEntityNameMap = Object.create(null); const dotTypeNameMap = Object.create(null); const kinds = { entity: 1, type: 1, action: 1, function: 1, }; forEachDefinition(csn, (def, defName) => { if (def.kind in kinds) { const rootDef = getRootDef(defName); // if this definition has a root def and the root def is not the service/schema name // => service C { type D.E }, replace the prefix dots with underscores if (rootDef && defName !== rootDef && rootDef !== edmUtils.getSchemaPrefix(defName)) { const underscoredDefName = defName .replace(`${ rootDef }.`, '') .replace(/\./g, '_'); const newDefName = `${ rootDef }.${ underscoredDefName }`; // store renamed types in correlation maps for later renaming if (def.kind === 'entity') dotEntityNameMap[defName] = newDefName; if (def.kind === 'type') dotTypeNameMap[defName] = newDefName; // rename in csn.definitions const art = csn.definitions[newDefName]; if (art !== undefined) { error(null, [ 'definitions', defName ], { name: newDefName }, 'Artifact name containing dots can\'t be mapped to an OData compliant name because it conflicts with existing definition $(NAME)'); } else { csn.definitions[newDefName] = def; delete csn.definitions[defName]; } // dots are illegal in bound actions/functions, no actions required for them } } }); // rename type refs to new type names const rewrite = (def) => { const rewriteReferencesInActions = (act) => { if (act.params) { Object.values(act.params).forEach((param) => { param = param.items || param; if (param.type && (dotEntityNameMap[param.type] || dotTypeNameMap[param.type])) param.type = dotEntityNameMap[param.type] || dotTypeNameMap[param.type]; }); } if (act.returns) { const returnsObj = act.returns.items || act.returns; if (returnsObj.type && dotEntityNameMap[returnsObj.type] || dotTypeNameMap[returnsObj.type]) returnsObj.type = dotEntityNameMap[returnsObj.type] || dotTypeNameMap[returnsObj.type]; } }; const applyOnNode = (node) => { node = node.items || node; if (node.type && dotTypeNameMap[node.type]) node.type = dotTypeNameMap[node.type]; if (node.target && dotEntityNameMap[node.target]) node.target = dotEntityNameMap[node.target]; if (node.$path && dotEntityNameMap[node.$path[1]]) node.$path[1] = dotEntityNameMap[node.$path[1]]; if (node.projection || node.query) { applyTransformationsOnNonDictionary(node.projection || node.query, undefined, { ref: (parent, prop, ref) => { if (dotEntityNameMap[ref[0]]) parent.ref[0] = dotEntityNameMap[ref[0]]; }, }, { directDict: true }); } rewriteReferencesInActions(node); }; const applyOnAnnoXprs = (node) => { Object.keys(node).filter(pn => pn[0] === '@').forEach((anno) => { transformAnnotationExpression(node, anno, { ref: (elemRef, _prop, ref) => { if (dotEntityNameMap[ref[0]] || dotTypeNameMap[ref[0]]) elemRef.ref[0] = dotEntityNameMap[ref[0]] || dotTypeNameMap[ref[0]]; }, }); }); }; forEachMemberRecursively(def, [ applyOnNode, applyOnAnnoXprs ]); applyOnNode(def); applyOnAnnoXprs(def); // handle unbound action/function and params in views rewriteReferencesInActions(def); }; forEachDefinition(csn, rewrite); forEachGeneric(csn, 'vocabularies', rewrite); } /* Experimental: Move definitions with dots into separate (sub-)service that has the namespace of the definition prefix. As not all such services end up with entity sets, schemas should be packed after the preprocessing run in order to minimize the number of services. */ function splitDottedDefinitionsIntoSeparateServices() { forEachDefinition(csn, (def, defName) => { if (def.kind !== 'service') { const myServiceRoot = whatsMyServiceRootName(defName); const mySchemaPrefix = edmUtils.getSchemaPrefix(defName); if (myServiceRoot && options.isV4() && /* (options.odataProxies || options.odataXServiceRefs) && options.isStructFormat && */ defName !== myServiceRoot && myServiceRoot !== mySchemaPrefix) { const service = { kind: 'service', name: mySchemaPrefix }; serviceRoots[mySchemaPrefix] = service; serviceRootNames.push(mySchemaPrefix); } } }); serviceRootNames.sort((a, b) => b.length - a.length); } function attachNameProperty( def, defName ) { edmUtils.assignProp(def, 'name', defName); // Attach name to bound actions, functions and parameters forEachGeneric(def, 'actions', (a, an) => { edmUtils.assignProp(a, 'name', an); forEachGeneric(a, 'params', (p, pn) => { edmUtils.assignProp(p, 'name', pn); }); }); // Attach name unbound action parameters forEachGeneric(def, 'params', (p, pn) => { edmUtils.assignProp(p, 'name', pn); }); } // initialize the service itself function initService( serviceRoot ) { edmAnnoPreproc.setSAPSpecificV2AnnotationsToEntityContainer(options, serviceRoot); } // link association target to association and add @odata.contained to compositions in V4 function linkAssociationTarget( struct ) { forEachMemberRecursively(struct, (element, name, prop, subpath) => { if (element.target && !element._target) { const target = csn.definitions[element.target]; if (target) { setProp(element, '_target', target); // If target has parameters, xref assoc at target for redirection if (edmUtils.isParameterizedEntity(target)) { if (!target.$sources) setProp(target, '$sources', Object.create(null)); target.$sources[`${ struct.name }.${ name }`] = element; } } else { error(null, subpath, { target: element.target }, 'Target $(TARGET) can\'t be found in the model'); } } // in V4 tag all compositions to be containments if (options.odataContainment && options.isV4() && csnUtils.isComposition(element) && element['@odata.contained'] === undefined) element['@odata.contained'] = true; }); } // Perform checks and add attributes for "contained" sub-entities: // - A container is recognized by having an association/composition annotated with '@odata.contained'. // - All targets of such associations ("containees") are marked with a property // '$containerNames: []', having as value an array of container names (i.e. of entities // that have a '@odata.contained' association pointing to the containee). Note that this // may be multiple entities, possibly including the container itself. // - All associations in the containee pointing back to the container are marked with // a boolean property '_isToContainer : true', except if the association itself // has the annotation '@odata.contained' (indicating the top-down link in a hierarchy). // - Rewrite annotations that would be assigned to the containees entity set for the // non-containment rendering. If containment rendering is active, the containee has no // entity set. Instead try to rewrite the annotation in such a way that it is effective // on the containment navigation property. // $containeeAssociations stores the containees (children/outbound edges) // $containerNames stores the containers (parents/inbound edges) function initContainments( container ) { if (container.kind === 'entity') { if (!container.$containeeAssociations) setProp(container, '$containeeAssociations', []); forEachMemberRecursively(container, eachAssoc, [], true, { pathWithoutProp: true, elementsOnly: true }); } function eachAssoc( elt, _memberName, _prop, path ) { if (elt.target && elt['@odata.contained']) { // store all containment associations, required to create the containment paths later on container.$containeeAssociations.push( { assoc: elt, path }); // Let the containee know its container // (array because the contanee may contained more then once) const containee = elt._target; if (!containee.$containerNames) setProp(containee, '$containerNames', []); // add container only once per containee if (!containee.$containerNames.includes(container.name)) containee.$containerNames.push(container.name); // Mark associations in the containee pointing to the container (i.e. to this entity) forEachMemberRecursively(containee, markToContainer, [ 'definitions', containee.name ], true, { elementsOnly: true }); } else if (elt.type && !elt.elements) { // try to find elements to drill down further while (elt && !isBuiltinType(elt.type) && !elt.elements) elt = csn.definitions[elt.type]; if (elt && elt.elements && !elt.$visited) { setProp(elt, '$visited', true); forEachMemberRecursively(elt, eachAssoc, path, true, { pathWithoutProp: true, elementsOnly: true }); delete elt.$visited; } } } function markToContainer( elt ) { if (elt._target && elt._target.name) { // If this is an association that points to the container (but is not by itself contained, // which would indicate the top role in a hierarchy) mark it with '_isToContainer' if (elt._target.name === container.name && !elt['odata.contained']) setProp(elt, '_isToContainer', true); } else { // try to find elements to drill down further while (elt && !(isBuiltinType(elt.type) || elt.elements)) elt = csn.definitions[elt.type]; if (elt && elt.elements && !elt.$visited) { setProp(elt, '$visited', true); forEachMemberRecursively(elt, markToContainer, [], true, { elementsOnly: true }); delete elt.$visited; } } } } // Split an entity with parameters into two entity types with their entity sets, // one named <name>Parameter and one named <name>Type. Parameter contains Type. // Containment processing must take place before because it might be that this // artifact with parameters is already contained. In such a case the existing // containment chain must be propagated and reused. This requires that the // containment data structures must be manually added here. // As a param entity is a potential proxy candidate, this split must be performed on // all definitions function initParameterizedEntityOrView( entityCsn, entityName ) { if (!edmUtils.isParameterizedEntity(entityCsn)) return; // Naming rules for aggregated views with parameters // Parameters: EntityType <ViewName>Parameters, EntitySet <ViewName> // with NavigationProperty "Results" pointing to the entity set of type <ViewName>Result // Result: EntityType <ViewName>Result, EntitySet <ViewName>Results // Naming rules for non aggregated views with parameters // Parameters: EntityType <ViewName>Parameters, EntitySet <ViewName> // with NavigationProperty "Set" pointing to the entity set of type <ViewName>Type // Result: EntityType <ViewName>Type, EntitySet <ViewName>Set // Backlink Navigation Property "Parameters" to <ViewName>Parameters // this code can be extended for aggregated views const typeEntityName = `${ entityName }Type`; const typeEntitySetName = `${ entityName }Set`; const typeToParameterAssocName = 'Parameters'; const hasBacklink = true; // create the Parameter Definition const parameterCsn = createParameterEntity(entityCsn, entityName, false); setProp(parameterCsn, '_origin', entityCsn); // create the Type Definition // modify the original parameter entity with backlink and new name if (csn.definitions[typeEntityName]) error('odata-duplicate-definition', [ 'definitions', entityName ], { '#': 'std', name: typeEntityName }); else entityCsn.name = typeEntityName; setProp(entityCsn, '$entitySetName', typeEntitySetName); // add backlink association if (hasBacklink) { entityCsn.elements[typeToParameterAssocName] = { name: typeToParameterAssocName, target: parameterCsn.name, type: 'cds.Association', on: [ { ref: [ 'Parameters', 'Set' ] }, '=', { ref: [ '$self' ] } ], }; setProp(entityCsn.elements[typeToParameterAssocName], '_selfReferences', []); setProp(entityCsn.elements[typeToParameterAssocName], '_target', parameterCsn); setProp(entityCsn.elements[typeToParameterAssocName], '$path', [ 'definitions', entityName, 'elements', typeToParameterAssocName ] ); } /* <EntitySet Name="ZRHA_TEST_CDSSet" EntityType="ZRHA_TEST_CDS_CDS.ZRHA_TEST_CDSType" sap:creatable="false" sap:updatable="false" sap:deletable="false" sap:addressable="false" sap:content-version="1"/> */ edmUtils.assignProp(entityCsn, '_SetAttributes', { '@sap.creatable': false, '@sap.updatable': false, '@sap.deletable': false, '@sap.addressable': false, }); // redirect inbound associations/compositions to the parameter entity Object.keys(entityCsn.$sources || {}).forEach((n) => { // preserve the original target for constraint calculation setProp(entityCsn.$sources[n], '_originalTarget', entityCsn.$sources[n]._target); entityCsn.$sources[n]._target = parameterCsn; }); } function createParameterEntity( entityCsn, entityName, isProxy ) { const parameterEntityName = `${ entityName }Parameters`; const parameterToTypeAssocName = 'Set'; // Construct the parameter entity const parameterCsn = { name: parameterEntityName, kind: 'entity', elements: Object.create(null), '@sap.semantics': 'parameters', }; if (!isProxy) setProp(parameterCsn, '$entitySetName', entityName); if (entityCsn.$location) edmUtils.assignProp(parameterCsn, '$location', entityCsn.$location); /* <EntitySet Name="ZRHA_TEST_CDS" EntityType="ZRHA_TEST_CDS_CDS.ZRHA_TEST_CDSParameters" sap:creatable="false" sap:updatable="false" sap:deletable="false" sap:pageable="false" sap:content-version="1"/> */ edmUtils.assignProp(parameterCsn, '_SetAttributes', { '@sap.creatable': false, '@sap.updatable': false, '@sap.deletable': false, '@sap.pageable': false, }); setProp(parameterCsn, '$isParamEntity', true); setProp(parameterCsn, '$mySchemaName', entityCsn.$mySchemaName); forEachGeneric(entityCsn, 'params', (p, n) => { const elt = cloneCsnNonDict(p, options); elt.name = n; delete elt.kind; setProp(elt, '$path', [ 'definitions', parameterEntityName, 'elements', n ]); elt.key = true; // params become primary key in parameter entity /* Spec meeting decision 28.02.22: Annotation @sap.parameter allows two values "mandatory"/"optional". Question was how to deal with incompatible "optional". Only "mandatory" is allowed because in RAP all parameters are NOT NULL and so they are in CAP (all view parameters become primary keys which are not null). */ if (options.isV2()) edmUtils.assignAnnotation(elt, '@sap.parameter', 'mandatory'); else edmUtils.assignAnnotation(elt, '@Common.FieldControl', { '#': 'Mandatory' }); parameterCsn.elements[n] = elt; }); linkAssociationTarget(parameterCsn); initContainments(parameterCsn); // add assoc to result set, FIXME: is the cardinality correct? if (!isProxy) { parameterCsn.elements[parameterToTypeAssocName] = { '@odata.contained': true, name: parameterToTypeAssocName, target: entityCsn.name, type: 'cds.Association', cardinality: { src: 1, min: 0, max: '*' }, }; setProp(parameterCsn.elements[parameterToTypeAssocName], '_target', entityCsn); setProp(parameterCsn.elements[parameterToTypeAssocName], '$path', [ 'definitions', parameterEntityName, 'elements', parameterToTypeAssocName ] ); } [ '@odata.singleton', '@odata.singleton.nullable' ].forEach((a) => { if (entityCsn[a] != null) parameterCsn[a] = entityCsn[a]; delete entityCsn[a]; }); // initialize containment // propagate containment information, if containment is recursive, use parameterCsn.name as $containerNames if (entityCsn.$containerNames) { if (!parameterCsn.$containerNames) setProp(parameterCsn, '$containerNames', []); for (const c of entityCsn.$containerNames) parameterCsn.$containerNames.push((c === entityCsn.name) ? parameterCsn.name : c); } entityCsn.$containerNames = [ parameterCsn ]; if (!parameterCsn.$containeeAssociations) setProp(parameterCsn, '$containeeAssociations', [ ]); parameterCsn.$containeeAssociations.push( { assoc: parameterCsn.elements[parameterToTypeAssocName], path: [ parameterToTypeAssocName ], } ); // rewrite $path setProp(parameterCsn, '$path', [ 'definitions', parameterEntityName ]); // proxies are registered in model separately if (!isProxy) { if (csn.definitions[parameterCsn.name]) { error('odata-duplicate-definition', [ 'definitions', entityName ], { '#': 'std', name: parameterCsn.name }); } else { csn.definitions[parameterCsn.name] = parameterCsn; reqDefs.definitions[parameterCsn.name] = parameterCsn; } } return parameterCsn; } function initElement( element, name, struct ) { setProp(element, 'name', name); setProp(element, '_parent', struct); } // convert $path to path starting at main artifact function $path2path( p ) { const path = []; /** @type {any} */ let env = csn; for (let i = 0; p && env && i < p.length; i++) { const ps = p[i]; env = env[ps]; if (env && env.constructor === Object) { path.push(ps); // jump over many items but not if this is an element if (env.items) { env = env.items; if (p[i + 1] === 'items') i++; } if (env.type && !isBuiltinType(env.type) && !env.elements) env = csn.definitions[env.type]; } } return path; } // Initialize a structured artifact function initStructure( def ) { // Don't operate on any structured types other than type and entity // such as events and aspects if (!edmUtils.isStructuredArtifact(def)) return; const keys = Object.create(null); // eslint-disable-next-line const validFrom = []; const validKey = []; // Iterate all struct elements forEachMemberRecursively(def.items || def, (element, elementName, prop, _path, construct) => { if (prop !== 'elements') return; initElement(element, elementName, construct); // collect temporal information if (element['@cds.valid.key']) validKey.push(element); if (element['@cds.valid.from']) validFrom.push(element); // forward annotations from managed association element to its foreign keys const elements = construct.items?.elements || construct.elements; const assoc = elements[element['@odata.foreignKey4']]; if (assoc) { Object.keys(assoc).filter(pn => pn[0] === '@' && !findAnnotationExpression(assoc, pn)).forEach((pn) => { edmUtils.assignAnnotation(element, pn, assoc[pn]); }); } // and eventually remove some afterwards if (options.isV2()) edmAnnoPreproc.setSAPSpecificV2AnnotationsToAssociation(element); const absPath = $path2path(element.$path); // initialize an association if (element.target) { // in case this is a forward assoc, store the backlink partners here, _selfReferences.length > 1 => error edmUtils.assignProp(element, '_selfReferences', []); edmUtils.assignProp(element._target, '$proxies', []); // $abspath is used as partner path edmUtils.assignProp(element, '$abspath', absPath); } // Collect keys if (element.key) keys[elementName] = element; edmAnnoPreproc.applyAppSpecificLateCsnTransformationOnElement(options, element, def, error); }, [], true, { elementsOnly: true }); // if artifact has a cds.valid.key mention it as @Core.AlternateKey if (validKey.length) { const altKeys = [ { Key: [] } ]; validKey.forEach(vk => altKeys[0].Key.push( { Name: vk.name, Alias: vk.name } ) ); edmUtils.assignAnnotation(def, '@Core.AlternateKeys', altKeys); } // prepare the structure itself if (def.kind === 'entity') { edmUtils.assignProp(def, '_SetAttributes', Object.create(null)); edmUtils.assignProp(def, '$keys', keys); edmAnnoPreproc.applyAppSpecificLateCsnTransformationOnStructure(options, def, error); edmAnnoPreproc.setSAPSpecificV2AnnotationsToEntitySet(options, def); } } // Prepare the associations for the subsequent steps function initConstraints( def ) { if (!edmUtils.isStructuredArtifact(def)) return; forEachMemberRecursively(def.items || def, initConstraintsOnAssoc, [], true, { elementsOnly: true }); } function initConstraintsOnAssoc( element ) { if (element.target && !element._constraints) { // setup the constraints object setProp(element, '_constraints', { constraints: Object.create(null), selfs: [], _origins: [], termCount: 0, }); // and crack the ON condition edmUtils.resolveOnConditionAndPrepareConstraints(csn, element, messageFunctions); } } /* Do not render (ignore) elements as properties In V4: 1) If this is a foreign key of an association to a container which *is* used to establish the relation via composition and $self comparison. The $self comparison can only be evaluated after the ON conditions have been parsed in prepareConstraints(). 2) For all other foreign keys let isEdmPropertyRendered() decide. 3) If an element/association is annotated with @odata.containment.ignore and containment is active, assign @cds.api.ignore or @odata.navigable: false 4) All of this can be revoked with options.renderForeignKeys. */ function ignoreProperties( struct ) { if (!edmUtils.isStructuredArtifact(struct)) return; forEachMemberRecursively(struct.items || struct, (element) => { if (!element.target) { if (element['@odata.foreignKey4']) { let isContainerAssoc = false; let { elements } = struct.items || struct; let assoc; const paths = element['@odata.foreignKey4'].split('.'); for (const p of paths) { assoc = elements[p]; if (assoc) // could be that the @odata.foreignKey4 was propagated... elements = assoc.elements; } if (assoc) isContainerAssoc = !!(assoc._isToContainer && assoc._selfReferences.length || assoc['@odata.contained']); /* If this foreign key is NOT a container fk, let isEdmPropertyRendered() decide Else, if fk is container fk, omit it if it wasn't requested in structured mode */ if ((!isContainerAssoc && !isEdmPropertyRendered(element, options)) || (isContainerAssoc && !options.renderForeignKeys)) edmUtils.assignAnnotation(element, '@cds.api.ignore', true); // Only in containment: // If this element is a foreign key and if it is rendered, remove it from the key ref vector (if available) else if (options.odataContainment && isContainerAssoc && options.renderForeignKeys && struct.$keys) delete struct.$keys[element.name]; } // Only in containment: // Ignore this (foreign key) element if renderForeignKeys is false if (options.odataContainment && element['@odata.containment.ignore']) { if (!options.renderForeignKeys) edmUtils.assignAnnotation(element, '@cds.api.ignore', true); else if (struct.$keys) // If foreign keys shall be rendered, remove it from key ref vector (if available) delete struct.$keys[element.name]; } } // it's an association else if (element['@odata.containment.ignore'] && options.odataContainment && !options.renderForeignKeys) { // if this is an explicitly containment ignore tagged association, // ignore it if option odataContainment is true and no foreign keys should be rendered edmUtils.assignAnnotation(element, '@odata.navigable', false); } }, [], true, { elementsOnly: true }); } /* Calculate the final referential constraints based on the assignments done in mutePropertiesForV4() It may be that now a number of properties are not rendered and cannot act as constraints (see isConstraintCandidate()) in edmUtils */ function finalizeConstraints( def ) { if (!edmUtils.isStructuredArtifact(def)) return; forEachMemberRecursively(def.items || def, finalizeConstraintsOnAssoc, [], true, { elementsOnly: true }); } function finalizeConstraintsOnAssoc( element ) { if (element.target && element._constraints) { edmUtils.finalizeReferentialConstraints(csn, element, options, info); if (element._constraints?._partnerCsn) { // if this is a partnership and this assoc has a set target cardinality, assign it as source cardinality to the partner if (element._constraints._partnerCsn.cardinality) { // if the forward association has set a src cardinality and it deviates from the backlink target cardinality raise a warning // in V2 only, in V4 the source cardinality is rendered implicitly at the Type property if (element._constraints._partnerCsn.cardinality.src) { const partnerCsn = element._constraints._partnerCsn; // eslint-disable-next-line eqeqeq const srcMult = (partnerCsn.cardinality.src == 1) ? '0..1' : '*'; const newMult = (element.cardinality?.min == 1 && element.cardinality?.max == 1) // eslint-disable-line ? 1 : (element.cardinality?.max === '*' || element.cardinality?.max > 1) ? '*' : '0..1'; if (srcMult !== newMult) { // TODO: Message should probably list actual cardinalities and not "normalized" ones. warning('odata-unexpected-cardinality', element.$path, { value: srcMult, othervalue: newMult, name: `${ partnerCsn._parent.name }/${ partnerCsn.name }`, }, 'Explicit source cardinality $(VALUE) of $(NAME) conflicts with target cardinality $(OTHERVALUE)' ); } } else { // .. but only if the original assoc hasn't set src yet element._constraints._partnerCsn.cardinality.src = element.cardinality?.max ? element.cardinality.max : 1; if (element.cardinality?.min !== undefined && element._constraints._partnerCsn.cardinality?.srcmin === undefined) element._constraints._partnerCsn.cardinality.srcmin = element.cardinality.min; } } else { element._constraints._partnerCsn.cardinality = { src: element.cardinality?.max ? element.cardinality.max : 1 }; if (element.cardinality?.min !== undefined) element._constraints._partnerCsn.cardinality.srcmin = element.cardinality.min; } } setProp(element._constraints, '$finalized', true); } } /* convert sub schemas that represent another service into a service reference object and remove all sub artifacts exposed by the initial type exposure */ function convertExposedTypesOfOtherServicesIntoCrossReferences() { if (options.odataXServiceRefs && options.isV4()) { serviceRootNames.forEach((srn) => { schemaNames.forEach((fqSchemaName) => { if (fqSchemaName.startsWith(`${ srn }.`)) { const targetSchemaName = fqSchemaName.replace(`${ srn }.`, ''); if (serviceRootNames.includes(targetSchemaName)) { // remove all definitions starting with < fqSchemaName >. and add a schema reference forEachKey(csn.definitions, (dn) => { if (dn.startsWith(fqSchemaName)) { delete csn.definitions[dn]; delete reqDefs.definitions[dn]; } }); if (!schemas[fqSchemaName]) schemaNames.push(fqSchemaName); schemas[fqSchemaName] = edmUtils.createSchemaRef(serviceRoots, targetSchemaName); } } }); }); } schemaNames.sort((a, b) => b.length - a.length); } /* If an association targets an artifact outside the service, expose the target entity type as proxy. A proxy represents the identity (or primary key tuple) of the target entity. All proxies are registered in a sub context representing the schema, in which the proxy is to be rendered (see csn2edm for details). If the target resides outside any service, the schema is either it's CDS namespace if provided or as 'root'. If the target resides in another service, either a schema named by the target service is created (option: odataProxies), or a reference object is created representing the target service (option: odataExtReferences). If option odataExtReferences is used, 'root' proxies are still created. If the association leading to the proxy candidate refers to associations either directly or indirectly (via structured elements), these dependent entity types are (recursively) exposed (or referenced) as well to keep the navigation graph in tact. */ function exposeTargetsAsProxiesOrSchemaRefs( struct ) { if (struct.kind === 'context' || struct.kind === 'service' || struct.$proxy) return; // globalSchemaPrefix is the prefix for all proxy registrations and must not change // the service prefix is checked without '.' because we also want to inspect those // definitions which are directly below the root service ($mySchemaName is the root) const globalSchemaPrefix = whatsMyServiceRootName(struct.$mySchemaName); // if this artifact is a service member check its associations if (globalSchemaPrefix) { forEachGeneric(struct.items || struct, 'elements', (element) => { if (!edmUtils.isNavigable(element)) return; /* * Consider everything @cds.autoexpose: falsy to be a proxy candidate for now */ /* if(element._target['@cds.autoexpose'] === false) { // :TODO: Also ignore foreign keys to association? edmUtils.foreach(struct.elements, e => e['@odata.foreignKey4'] === element.name, e => e.$ignore = true); element.$ignore = true; info(null, ['definitions', struct.name, 'elements', element.name] `${element.type.replace('cds.', '')} "${element.name}" excluded, target "${element._target.name}" is annotated '@cds.autoexpose: ${element._target['@cds.autoexpose']}'` ); return; } */ // Create a proxy if the source schema and the target schema are different // That includes that the target doesn't have a schema. // If the target is in another schema, check if both the source and the target share the same service name. // If they share the same service name, then it is just a cross schema navigation within the same EDM, no // proxy required. // association must be managed and not unmanaged // odataProxies (P) and odataXServiceRefs (X) are evaluated as follows: // P | X | Action // 0 | 0 | No out bound navigation // 0 | 1 | Cross service references are generated // 1 | 0 | Proxies for all out bound navigation targets are created // 1 | 1 | Cross service references and proxies are generated const targetSchemaName = element._target.$mySchemaName; if (isProxyRequired(element)) { if (options.isV4() && (options.odataProxies || options.odataXServiceRefs)) { // must be a managed association with keys OR an unambiguous backlink to become a proxy const assocOK = element.keys || (element.on && element._constraints.selfs.length === 1 && element._constraints.termCount === 1); // reuse proxy if available let proxy = getProxyForTargetOf(element); if (!proxy) { if (targetSchemaName && options.odataXServiceRefs) proxy = createSchemaRefFor(targetSchemaName); // create a proxy for a 'good' association only else if (options.odataProxies && assocOK) proxy = createProxyFor(element, targetSchemaName); proxy = registerProxy(proxy, element); } else if (!assocOK) { // if there is already a proxy (generated by a 'good' association) // and this association is not a good one, don't expose this association. muteNavProp(element); return; } if (proxy) { // if a proxy was either already created or could be created and // if it's a 'real' proxy, link the _target to it and remove constraints // otherwise proxy is a schema reference, then do nothing setProp(element, '$noPartner', true); element._constraints.constraints = Object.create(null); if (proxy.kind === 'entity') { if (!proxy.$isParamEntity) populateProxyElements(element, proxy, getForeignKeyDefinitions(element)); element._target = proxy; } else { // No navigation property bindings on external references setProp(element, '$externalRef', true); } } else { muteNavProp(element); return; } } else { muteNavProp(element); return; } } }); } function muteNavProp( elt ) { edmUtils.assignAnnotation(elt, '@odata.navigable', false); if (elt._target['@cds.autoexpose'] !== false) info('odata-navigation', elt.$path, { target: elt._target.name, service: globalSchemaPrefix }); } function createSchemaRefFor( targetSchemaName ) { let ref = csn.definitions[`${ globalSchemaPrefix }.${ targetSchemaName }`]; if (!ref) ref = edmUtils.createSchemaRef(serviceRoots, targetSchemaName); return ref; } function createProxyFor( assoc, targetSchemaName ) { // If target is outside any service expose it in service of source entity // The proxySchemaName is not prepended with the service schema name to allow to share the proxy // if it is required in multiple services. The service schema name is prepended upon registration const proxySchemaName = targetSchemaName || edmUtils.getSchemaPrefix(assoc._target.name); // if the target is a parameter entity, it's easy just create the parameter stub const isParamProxy = edmUtils.isParameterizedEntity(assoc._target); // 1) construct the proxy definition // proxyDefinitionName: strip the serviceName and replace '.' with '_' const defName = assoc._target.name .replace(`${ proxySchemaName }.`, '') .replace(/\./g, '_'); // fullName: Prepend serviceName and if in same service add '_proxy' const proxy = isParamProxy ? createParameterEntity(assoc._target, `${ proxySchemaName }.${ defName }`, true) : { name: `${ proxySchemaName }.${ defName }`, kind: 'entity', elements: Object.create(null) }; // Final proxyShortName for all further processing const proxyShortName = defName + (isParamProxy ? 'Parameters' : ''); setProp(proxy, '$proxy', true); setProp(proxy, '$mySchemaName', proxySchemaName); setProp(proxy, '$proxyShortName', proxyShortName); setProp(proxy, '$keys', Object.create(null)); setProp(proxy, '$hasEntitySet', false); setProp(proxy, '$exposedTypes', Object.create(null)); // copy all annotations of the target to the proxy forEach(assoc._target, ( k, v ) => { if (k[0] === '@' && k !== '@open') proxy[k] = v; }); // 2) create the elements and $keys if (isParamProxy) { // Reset param proxy elements to expose element tree const { elements } = proxy; proxy.elements = Object.create(null); populateProxyElements(assoc, proxy, elements); } else { populateProxyElements(assoc, proxy, assoc._target.$keys); } return proxy; } // Return top level foreign key element definitions. The full top level // element is exposed instead of merging partial trees into the exposed type // def structure. function getForeignKeyDefinitions( e ) { return e.keys ? e.keys.map(fk => e._target.elements[fk.ref[0]]) : []; } // copy over the primary keys of the target and trigger the type exposure // if the element already exists we assume it was fully exposed function populateProxyElements( assoc, proxy, elements ) { Object.values(elements).forEach((e) => { if (isEdmPropertyRendered(e, options)) { let newElt = proxy.elements[e.name]; if (!newElt) { if (csnUtils.isAssocOrComposition(e)) { if (!e.on && e.keys) { newElt = createProxyOrSchemaRefForManagedAssoc(e); } else { info(null, [ 'definitions', struct.name, 'elements', assoc.name ], { name: proxy.nname, target: assoc._target.name }, 'Unmanaged associations are not supported as primary keys for proxy entity type $(NAME) of unexposed association target $(TARGET)'); } } el