@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
143 lines (127 loc) • 5.45 kB
JavaScript
// Kick-start: prepare to resolve all references
// Remark: it would probably be better to move this to extend.js
;
const { isBetaEnabled } = require('../base/specialOptions');
const {
setLink,
annotationVal,
annotationIsFalse,
forEachGeneric,
} = require('./utils');
function kickStart( model ) {
const { options } = model;
const { message } = model.$messageFunctions;
const { resolveUncheckedPath, createGapArtifact } = model.$functions;
// Set _service link (sorted to set it on parent first). Could be set
// directly, but beware a gap artifact 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._entityIncludes) &&
art._ancestors === undefined && art.kind === 'entity')
setProjectionAncestors( art ); // TODO: set to [] for other entities
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 ) { // TODO: rename
// 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 entity includes in extend.js,
// Remark: _ancestors are also tested in populate.js for minmal exposure
//
// Remark: @cds.autoexpose is considered on-demand in populate.js
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?.kind === 'entity' &&
(art.query?.from?.path || // direct select with one source
art._entityIncludes?.length) &&
!art._ancestors && art._ancestors !== 0) { // prevent inf-loop
setLink( art, '_ancestors', 0 ); // avoid infloop with cyclic from
if (art._entityIncludes?.length) {
if (art._entityIncludes.length === 1) {
chain.push( art );
art = art._entityIncludes[0];
continue;
}
setLink( art, '_ancestors', [] );
for (const incl of art._entityIncludes) {
if ((incl.query || incl._entityIncludes?.length) &&
incl._ancestors === undefined)
setProjectionAncestors( incl );
art._ancestors.push( ...(incl._ancestors || []), incl );
}
break;
}
chain.push( art );
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 postProcessArtifact( art ) {
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 );
}
}
}
module.exports = kickStart;