UNPKG

@sap/cds-compiler

Version:

CDS (Core Data Services) compiler and backends

158 lines (141 loc) 5.98 kB
// Kick-start: prepare to resolve all references 'use strict'; const { builtinLocation } = require('../base/location'); const { isBetaEnabled, forEachGeneric } = require('../base/model'); const { setLink, annotationVal, annotationIsFalse, isDirectComposition, } = require('./utils'); function kickStart( model ) { const { options } = model; const { message } = model.$messageFunctions; const { resolveUncheckedPath, initMainArtifact } = model.$functions; // Set _service link (sorted to set it on parent first). Could be set // directly, but beware a namespace becoming a service later. Object.keys( model.definitions ).sort().forEach( setAncestorsAndService ); forEachGeneric( model, 'definitions', postProcessArtifact ); return; /** * Set projection ancestors, and _service link for artifact with absolute name 'name': * - not set: internal artifact * - null: not within service * - service: the artifact of the embedding service * This function must be called ordered: parent first * * Remark: _service links are not already set in define.js, because we might * have a @cds.redirection.service in the future * * @param {string} name Artifact name */ function setAncestorsAndService( name ) { const art = model.definitions[name]; if (art._parent === undefined) return; // nothing to do for builtins and redefinitions if (art.query && art._ancestors === undefined && art.kind === 'entity') setProjectionAncestors( art ); let parent = art._parent; if (parent === model.definitions.localized) parent = model.definitions[name.substring( 'localized.'.length )]; const service = parent && (parent._service || parent.kind === 'service' && parent); setLink( art, '_service', service ); if (!parent || !service) return; // To be removed when nested services are allowed if (!isBetaEnabled( options, 'nestedServices' ) && art.kind === 'service') { message( 'service-nested-service', [ art.name.location, art ], { art: service }, 'A service can\'t be nested within a service $(ART)' ); } else if (art.kind === 'context') { // TODO: remove this error message( 'service-nested-context', [ art.name.location, art ], { art: service }, 'A context can\'t be nested within a service $(ART)' ); } } function setProjectionAncestors( art ) { // Must be run after processLocalizedData() as we could have a projection // on a generated entity. // TODO: do not do implicit redirection across services, i.e. Service2.E is // no redirection target for E if Service2.E = projection on Service1.E and // Service1.E = projection on E // Remark: _ancestors are also set with includes, and there also for aspects, // types and events (TODO: entity only) // // Remark: _ancestors are also tested in populate.js for minmal exposure const chain = []; const autoexposed = annotationVal( art['@cds.autoexposed'] ); // no need to set preferredRedirectionTarget in the while loop as we would // use the projection having @cds.redirection.target anyhow instead of // `art` anyway (if we do the no-x-service-implicit-redirection TODO above) while (art?.query?.from?.path && // direct select with one source art._ancestors !== 0) { // prevent inf-loop chain.push( art ); setLink( art, '_ancestors', 0 ); // avoid infloop with cyclic from const name = resolveUncheckedPath( art.query.from, 'from', art ); art = name && (model.definitions[name] || createGapArtifact( name )); if (autoexposed) break; // only direct projection for auto-exposed } let ancestors = art && (!autoexposed && art._ancestors || []); chain.reverse(); for (const a of chain) { ancestors = (ancestors ? [ ...ancestors, art ] : []); setLink( a, '_ancestors', ancestors ); art = a; } } function createGapArtifact( name, location = builtinLocation() ) { // TODO: make it probably part of define.js // TODO: make it work without location (or value undefined/null) // TODO: change the location later if overwritten const art = { kind: 'namespace', name: { id: name, location }, location, }; model.definitions[name] = art; initMainArtifact( art ); return art; } function postProcessArtifact( art ) { tagCompositionTargets( art ); if (art.$queries) { for (const query of art.$queries) { if (query.mixin) forEachGeneric( query, 'mixin', tagCompositionTargets ); } } if (!art._ancestors || art.kind !== 'entity') return; // redirections only to entities const service = art._service; if (!service) return; const sname = service.name.id; art._ancestors.forEach( expose ); return; function expose( ancestor ) { if (ancestor._service === service || annotationIsFalse( art['@cds.redirection.target'] )) return; const desc = ancestor._descendants || setLink( ancestor, '_descendants', Object.create( null ) ); if (!desc[sname]) desc[sname] = [ art ]; else desc[sname].push( art ); } } function tagCompositionTargets( elem ) { // TODO: together with test for targetIsTargetAspect() if (elem.target && isDirectComposition( elem )) { // A target aspect would have already moved to property `targetAspect` in // define.js (hm... more something for kick-start.js...) // TODO: for safety, just use resolveUncheckedPath() const target = resolveUncheckedPath( elem.target, 'target', elem ); if (target) model.$compositionTargets[target] = true; } if (elem.targetAspect?.elements) elem = elem.targetAspect; forEachGeneric( elem, 'elements', tagCompositionTargets ); } } module.exports = kickStart;