@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
787 lines (706 loc) • 29.1 kB
JavaScript
// Rewrite paths in annotation expressions.
//
// This module rewrites paths in expressions of propagated annotations.
// To properly rewrite paths, we need to consider where the annotation originates
// and where it is propagated to.
//
// Paths may need:
// 1. to have their prefix changed.
// This affects e.g. propagation due to type-of, where a prefix `$self.a` needs to
// be replaced by `$self.sub.elem` or even simple type references at parameters
// where `$self` needs to be replaced by `:P`.
// 2. to be rewritten due to projections
// This affects all paths that contain association steps, as their target may
// have been redirected, but also all annotations on projections.
//
// References referring to parameters are never rewritten.
//
// Via Includes
// ============
// Path prefixes don't change. However, we may need to reject the annotation if
// an included element was overridden and the type has changed.
//
// The path then needs to be rewritten due to associations.
// See section "Associations".
//
// Via Type
// ========
// If an annotation was written at a type or at an element of a type, we may need to
// adapt path prefixes at the type usage position.
//
// If an annotation is propagated from a type definition and the type is used at:
//
// - another (type) definition, no prefixes need to change.
// - another (type) definition as an include, see "Includes".
// - an element definition, `$self` needs to be replaced by the element name.
// Paths without `$self` on the type itself (not sub-elements), need to have
// the element name prepended.
// - a parameter, `$self` needs to be replaced by the parameter name.
// Paths without `$self` on the type itself (not sub-elements), need to have
// the parameter name prepended.
// - a return parameter, `$self` needs to be rejected, because there is no way
// to refer to `returns`. Paths without `$self` on the type itself (not sub-elements),
// need to be rejected as well.
//
// If elements in a structured type use `$self`, they, too, will need to be rewritten.
// The same rules as above apply. Because this would always end up in element
// expansion, this case is rejected and only possible with a beta flag.
// If no `$self` is used, no prefixes need to change, as the paths are already relative.
// Parameter references in types do not exist.
//
// The path then needs to be rewritten due to associations.
// See section "Associations".
//
// Via Type-Of
// ===========
// For type-ofs such as `E:sub.elem`, similar rules as for "type" are required, but
// before rewriting the paths, we need to check whether the path is valid.
//
// For `E:sub.elem`, all paths at element `elem` need to refer to sub-elements of
// `elem` or `elem` itself only. If siblings of `elem` or siblings of `sub` are
// referred to, the path can't be rewritten at the type-of usage location.
//
// Because non-relative references such as `$self` inside structures would always
// end up in element expansion, they are rejected and are only possible with a beta
// flag.
//
// If an annotation is propagated from an element `sub.elem` and the type-of is used at:
//
// - another type definition, the path may also not refer to element `sub.elem`
// itself, as it can't be rewritten to `$self` at a type definition.
// Paths starting with `$self.sub.elem` must be replaced by `$self`, i.e.
// the path up to the last path step in the type-of.
// - an element definition, `$self.sub.elem` needs to be replaced by the element name.
// Paths without `$self` on the "type-of element" itself need to have
// the first path step be replaced by the target element name.
// - a parameter, `$self.sub.elem` needs to be replaced by the parameter name.
// Paths without `$self` on the "type-of element" itself need to have
// the first path step be replaced by the target parameter name.
// - a return parameter, `$self` needs to be rejected, because there is no way
// to refer to `returns`. Paths without `$self` on the "type-of element" itself
// (not sub-elements), need to be rejected as well.
//
// The path then needs to be rewritten due to associations.
// See section "Associations".
//
// Associations
// ============
// All paths containing associations may need to be rewritten. Due to auto-exposure
// and auto-redirection, associations may be redirected to projections of their
// original targets. And those projections may rename elements or leave them out
// altogether. Therefore, all paths with associations need to be rewritten
// according to the rules in section "In Queries".
//
// In Queries
// ==========
// Both, propagation from source entity to query, but also from element to select item,
// need to respect renamed select items.
//
// Select Item via Origin
// ----------------------
// A bare select item of path length one, that gets an annotation via propagation from
// its origin, behaves similar to an element that gets it via an include.
// However, elements may have been renamed or may not be available at all.
// On top of that, they may be inside nested projections (expand).
// Or even simpler: sub-elements may have been selected.
//
// Instead of changing the path prefix, we need to check if the referenced path
// was projected or if a prefix was projected (e.g. for structures or associations).
// The same rules as for ON-condition rewriting apply.
//
// Furthermore, as the target is a select item, and this select item belongs to a table
// alias, we should rewrite all annotation paths only to projected elements of that
// table alias. Cross-rewriting between table aliases should not be done.
// This is the same we do for association rewriting.
//
// TODO:
// For now, we do not rewrite sub-structure elements. The whole structure needs
// to be projected or the select item isn't considered. That is, `expand {*}`
// is not considered, yet.
//
// Query Source
// ------------
// For propagation from query sources to the query, the same rules as for select
// items apply.
//
// Via Calculated Element Origin
// =============================
// Calculated elements behave just like `type-of`.
//
// Notes on $self
// ==============
// Because `$self` handling is complicated and will always result in type-expansion
// if used on/in a type definition, we reject it at such places.
// This module still resolves and rewrites them properly, though, if beta flag
// `rewriteAnnotationExpressionsViaType` is used.
//
// Notes on Propagator
// ===================
// If the compiler expands all elements (including those in `targetAspect`), then we
// can move the call to rewriteAnnotationRefs from the propagator into tweak-assocs.
// There, we need to go through _all_ definitions, not just `model._entities`.
// But until then, we rely on the propagator to properly propagate annotations.
'use strict';
const { weakLocation } = require('../base/location');
const {
setArtifactLink,
setLink,
setExpandStatusAnnotate,
} = require('./utils');
const { CompilerAssertion } = require('../base/error');
const { isBetaEnabled } = require('../base/model');
const { isSimpleCdlIdentifier } = require('../parsers/identifiers');
// Config object passed around all "rewrite" functions.
class AnnoRewriteConfig {
anno;
target;
targetRoot;
origin;
fromTargetType;
fromCalcElement;
expandedRoot;
expandedRootType;
isInFilter;
tokenExpr;
}
function xprRewriteFns( model ) {
const { error } = model.$messageFunctions;
const {
traverseExpr,
resolvePath,
navigationEnv,
resolvePathRoot,
cachedRedirectionChain,
findRewriteTarget,
} = model.$functions;
return {
rewriteAnnotationsRefs,
};
/**
* @param expr
* @param {AnnoRewriteConfig} config
* @param {string} [variant]
*/
function reportAnnoRewriteError( expr, config, variant = 'std' ) {
return error('anno-missing-rewrite', [
weakLocation( config.target.location ), config.target,
], {
'#': variant,
anno: config.anno,
art: config.origin,
elemref: expr,
});
}
/**
* Rewrite the propagated annotation relative to the target.
*
* @param {XSN.Artifact} target
* @param {XSN.Artifact} origin
* @param {string} annoName
*/
function rewriteAnnotationsRefs( target, origin, annoName ) {
// Make sure not to waste time if no inherited annotation has references:
if (!origin?.$contains?.$annotation?.$path)
return;
const anno = target[annoName];
// only annotations with expressions have a kind
// also, don't report errors twice
if (!anno.kind || anno.$invalidPaths)
return;
// Annotation comes from the target's type. That's important to know, because
// path prefixes need to be adapted.
const fromTargetType = target.type?._artifact === origin;
// Annotation comes from the target's calculated element. A special case propagation rule, e.g
// for `calcString: String = str;`. We also need to adapt path prefixes.
const fromCalcElement = !fromTargetType && target.$calcDepElement &&
target.value?._artifact === origin;
const { expandedRoot, expandedRootType } = !fromTargetType && getExpandRoot( target ) || {};
const config = {
__proto__: AnnoRewriteConfig.prototype,
anno: annoName,
target,
targetRoot: annoRootArt( target ),
origin,
fromTargetType,
fromCalcElement,
expandedRoot,
expandedRootType,
};
const hasError = rewriteAnnotationExpr( target[annoName], config );
target.$contains ??= {};
target.$contains.$annotation ??= {};
target.$contains.$annotation.$path ||= origin.$contains.$annotation.$path;
target.$contains.$annotation.$self ||= origin.$contains.$annotation.$self;
if (hasError)
anno.$invalidPaths = true; // avoid subsequent errors
}
/**
* @param {XSN.Expression} expr
* @param {AnnoRewriteConfig} config
* @returns {boolean}
*/
function rewriteAnnotationExpr( expr, config ) {
if (expr.literal === 'array') {
return !!expr.val.find(val => rewriteAnnotationExpr( val, config ));
}
else if (expr.literal === 'struct') {
const struct = Object.values(expr.struct);
return !!struct.find(val => rewriteAnnotationExpr( val, config ));
}
else if (expr.$tokenTexts) {
// used to set `$tokenText` to true in case of rewritten annotation
config.tokenExpr = expr;
return traverseExpr.STOP === traverseExpr(
expr, 'annoRewrite', config.target,
// eslint-disable-next-line @stylistic/max-len, @stylistic/function-paren-newline
(e, refCtx) => (rewriteAnnoExpr( e, config, refCtx ) ? traverseExpr.STOP : traverseExpr.SKIP) );
}
return false;
}
/**
* @param {XSN.Expression} expr
* @param {AnnoRewriteConfig} config
* @param {string} refCtx
* @returns {null|true} Returns true if the expression couldn't be rewritten.
*/
function rewriteAnnoExpr( expr, config, refCtx ) {
const root = expr.path && (expr.path[0]?._navigation || expr.path[0]?._artifact);
if (!root || !expr._artifact)
return null; // invalid path
const { target } = config;
// Report obsolete $parameters; parameters on non-actions not supported, yet.
if (root.kind === '$parameters' || (root.kind === 'param' && root._parent.kind !== 'action' &&
root._parent.kind !== 'function'))
return reportAnnoRewriteError( expr, config, 'unsupported' );
if (root.kind === 'key') {
// Foreign keys can't be renamed and since we don't have absolute references to foreign keys,
// i.e. `$self.assoc.target_id` always refers to the target side, we don't have to rewrite
// them.
return null;
}
// magic variables / replacement variables are never rewritten; they can't
// have filters nor can they point to elements.
if (expr._artifact?.kind === 'builtin')
return null;
let hasError = false;
if (config.fromTargetType || config.fromCalcElement)
hasError = adaptPathPrefixViaType( expr, config );
else if (config.expandedRoot)
hasError = adaptPathPrefixViaTypeExpansion( expr, config );
hasError ||= rewriteGenericAnnoPath( expr, config, refCtx );
if (hasError)
return true;
// TODO: Remove extra loop once filter traversal is added to traverseExpr (#12068)
for (const step of expr.path) {
if (step?._artifact && step.where && !Array.isArray( step._artifact ) ) {
// We must not prefix `$`-renamed variables with `$self`, as it would
// change meaning, see (#11775). Also, the path's target changes.
const assocTarget = step._artifact.target._artifact;
if (target) {
const filterConfig = { ...config, target: assocTarget, isInFilter: true };
if (traverseExpr.STOP === traverseExpr(
step.where, 'filter', step,
// eslint-disable-next-line @stylistic/max-len
(e, ctx) => expr.path && (rewriteGenericAnnoPath( e, filterConfig, ctx ) ? traverseExpr.STOP : traverseExpr.SKIP)
))
return true;
}
else {
// can't happen: rejected earlier by compiler
return reportAnnoRewriteError( expr, config, 'unsupported' );
}
}
}
if (expr.$tokenTexts === true) {
// TODO: do not do with Universal-CSN (and gensrc, but that does not matter)
// We rewrite the string value for backward compatibility with "old-school" tools that
// only understand the string representation. But we only do so for simple strings, which
// is the case if this path expression has $tokenTexts. It then is of the form `@a: (path)`.
const isSimpleStep = step => !step.where && !step.args && isSimpleCdlIdentifier( step.id );
if (expr.path.every(isSimpleStep))
expr.$tokenTexts = expr.path.map( step => step.id ).join('.');
}
if (model.options.testMode) {
// re-resolve the modified path; all paths steps must match what we rewrote
const ref = { ...expr, path: [ ...expr.path.map(item => ({ ...item })) ] };
if (!resolvePath( ref, refCtx, target ))
throw new CompilerAssertion(`rewritten anno path must be resolvable: ${ JSON.stringify(ref.path) }`);
for (let i = 0; i < ref.path.length; ++i) {
const actual = ref.path[i];
const expected = ref.path[i];
if (actual._artifact !== expected._artifact) {
throw new CompilerAssertion(`rewritten anno path contains incorrect artifact links: ${
JSON.stringify(ref.path) }; step ${ i }`);
}
else if (actual._navigation !== undefined && actual._navigation !== expected._navigation) {
throw new CompilerAssertion(`rewritten anno path contains incorrect navigation links: ${
JSON.stringify(ref.path) }; step ${ i }`);
}
}
}
return false;
}
/**
* @param {XSN.Expression} expr
* @param {AnnoRewriteConfig} config
* @returns {*}
*/
function getRootEnv( expr, config ) {
const { target } = config;
if (expr.scope === 'param') // path is absolute
return navigationEnv( config.targetRoot, null, null, 'nav' );
// On select items, use navigation elements or table alias
// TODO: Expand/inline paths don't have a `_navigation` property on their last
// path step, yet. We need to implement expand/inline.
const isSimpleSelectItem = target.value?.path && target._main?.query && !target._columnParent;
if (isSimpleSelectItem) {
const isSelfPath = (expr.path[0]?._navigation?.kind === '$self');
if (isSelfPath) {
// Path is absolute, use table alias to resolve it.
let tableAlias = target.value.path[0]._navigation;
while (tableAlias && tableAlias.kind === '$navElement')
tableAlias = tableAlias._parent;
if (tableAlias)
return tableAlias;
}
else {
// Path is relative
const nav = target.value.path[target.value.path.length - 1]._navigation?._parent;
if (nav)
return nav;
}
}
if (isSimpleSelectItem && model.options.testMode)
throw new CompilerAssertion(`select item has no table alias: ${ JSON.stringify(target.value.path) }`);
if (isAnnoPathAbsolute( expr ))
return navigationEnv( config.targetRoot, null, null, 'nav' );
// anno path is relative / element reference (others were already rejected)
// if the target is a root artifact, use it. Otherwise, use its parent.
return navigationEnv( isAnnoRootArt( target ) ? target : target._parent, null, null, 'nav' );
}
/**
* @param {XSN.Expression} expr
* @param {AnnoRewriteConfig} config
* @param {string} refCtx
* @returns {boolean}
*/
function rewriteGenericAnnoPath( expr, config, refCtx ) {
const isAbsolute = isAnnoPathAbsolute( expr );
const startIndex = isAbsolute ? 1 : 0;
// We get the root environment now, even though below we resolve the root item
// again if it was absolute (e.g. $self). We do so, because for queries, we
// want to respect the select item's corresponding table alias.
const rootEnv = getRootEnv( expr, config );
// reset artifact link; we'll set it again if there are no errors
setArtifactLink( expr, null );
if (isAbsolute) {
// Adapt absolute root path, as it isn't rewritten in rewriteItem
// The path-prefix was already adapted in rewriteAnnoExpr().
delete expr.path[0]._artifact;
delete expr.path[0]._navigation;
// TODO: What about `up_`? Shouldn't we set `_navigation` as well?
// TODO: Can we handle `$self` of anonymous-composition-of-aspect?
const root = resolvePathRoot( expr, refCtx, config.target );
if (!root)
return reportAnnoRewriteError( expr, config );
}
// Store the original artifact, so that we can use it to
// calculate a redirection chain later on.
expr.path.forEach((item) => {
if (item._artifact)
setLink( item, '_originalArtifact', item._artifact );
});
let env = rootEnv;
let art = expr.path[0]._artifact;
for (let i = startIndex; i < expr.path.length; ++i) {
if (i > startIndex && art.target) {
// if the current artifact is an association, we need to respect the redirection
// chain from original target to new one.
// FIXME: Won't work with associations in projected structures.
const origTarget = expr.path[i - 1]?._originalArtifact?.target?._artifact;
const chain = cachedRedirectionChain( art, origTarget );
if (!chain)
return reportAnnoRewriteError( expr, config );
for (const alias of chain) {
art = rewriteItem( expr, config, alias, i );
if (!art)
return reportAnnoRewriteError( expr, config );
}
}
art = rewriteItem( expr, config, env, i );
if (!art)
return reportAnnoRewriteError( expr, config );
// target, items, …
env = navigationEnv( art, null, null, 'nav' );
}
setArtifactLink( expr, art );
if (startIndex === 0 && expr.path[0].id.startsWith('$')) {
if (config.isInFilter) {
// In filters, we must not prepend `$self`, as that would change its meaning.
// We must reject it. See #11775
return reportAnnoRewriteError( expr, config );
}
// After rewriting, if an element starts with `$` -> add root prefix
prependRootPath( config.origin, config.targetRoot, expr );
}
return false;
}
/**
* Rewrite an expression that came via type propagation.
*
* @returns {boolean} Returns the expression if it couldn't be rewritten.
*/
function adaptPathPrefixViaType( expr, config ) {
const { target, origin } = config;
if (!target._main && !origin._main)
return false; // no need to rewrite; both are top-level
if (rejectOuterReference( expr, origin, config ))
return true;
// $self-paths via types from/to non-main artifacts always need to be rewritten.
config.tokenExpr.$tokenTexts = true;
const wasAbsolute = isAnnoPathAbsolute( expr );
stripAbsolutePathPrefix( expr, origin );
if (wasAbsolute) {
prependRootPath( origin, target, expr );
}
else if (!isAnnoRootArt( target )) { // target is element
const item = { id: target.name.id };
setArtifactLink( item, target );
prependToStrippedPath( origin, expr, [ item ] );
}
else if (target.kind === 'param') {
// annotations on parameters need a `:prefix`
prependRootPath( origin, target, expr );
}
else {
prependToStrippedPath( origin, expr, [ ] );
}
return false;
}
function adaptPathPrefixViaTypeExpansion( expr, config ) {
const root = expr.path[0]?._navigation;
if (root?.kind !== '$self') {
// non-self paths are always valid in expanded artifacts
// TODO: What about parameter references? Are they already always rejected?
return false;
}
// We reject $self-paths because they need to be rewritten.
// However, with a special flag, we allow rewriting it for testing purposes.
if (!isBetaEnabled( model.options, 'rewriteAnnotationExpressionsViaType' ))
return reportAnnoRewriteError( expr, config, 'unsupported' );
if (rejectOuterReference( expr, config.expandedRootType, config ))
return true;
stripAbsolutePathPrefix( expr, config.expandedRootType );
prependRootPath( config.expandedRootType, config.expandedRoot, expr );
setExpandStatusAnnotate( config.target, 'annotate' );
config.target[config.anno].$inferred = 'anno-rewrite';
// $self-paths via type expansion always need to be rewritten.
config.tokenExpr.$tokenTexts = true;
return false;
}
/**
* Prepend a path to `expr.path` or replace the root item.
* The path needs to have been run through stripPrefixToNewRoot(…)`.
* It is prepended if the root item is not the origin.
* Replaced otherwise.
*
* @param origin
* @param {XSN.Expression} expr
* @param path
*/
function prependToStrippedPath( origin, expr, path ) {
// If origin is a definition, we need to _prepend_ the path.
// Otherwise, we need to replace the root's name.
const rootArt = expr.path[0]._artifact;
if (rootArt === origin)
expr.path.shift();
expr.path.unshift(...path);
}
/**
* Strips a prefix path from `expr.path`. The prefix is defined
* by where `art` appears in the path.
*
* @param {XSN.Expression} expr
* @param {XSN.Artifact} art
*/
function stripAbsolutePathPrefix( expr, art ) {
const relativeRoot = findRelativeRoot( expr, art );
if (relativeRoot === -1 && isAnnoRootArt( art ))
return; // no $self; root item is element
if (relativeRoot >= 1)
expr.path = expr.path.slice(relativeRoot);
else if (relativeRoot === -1)
throw new CompilerAssertion('Error while rewriting annotation');
}
/**
* Returns false if the path can be propagated to the target without referring
* to any "outer" elements. It differentiates between the target being a main
* artifact and elements, because an element annotation referring to itself can't
* be propagated to a type:
*
* type T1 : { @a: (elem) elem: String; };
* type T2 : T1:elem; // invalid
*
* Also considers other targets such as `returns`, etc.
*
* @param {XSN.Expression} expr
* @param {XSN.Artifact} origin
* @param {AnnoRewriteConfig} config
* @returns {boolean}
*/
function rejectOuterReference( expr, origin, config ) {
if (!isAnnoPathAbsolute( expr ) && !origin._main)
return false;
const root = expr.path[0]?._navigation;
const found = findRelativeRoot( expr, origin );
const isInvalid = (found === -1) ||
// Can't use paths with `$self` in `returns`.
(root?.kind === '$self' && isReturnParam( config.targetRoot )) ||
// siblings are allowed for non-main artifacts, except for 'returns'
(!config.target._main || isReturnParam( config.target )) && (expr.path.length - found) <= 1;
if (isInvalid)
return reportAnnoRewriteError( expr, config );
return false;
}
/**
* Finds the path segment in expr which starts at `origin`.
* For example, for a path `$self.elem.b.c` on an element `b`, it will return 2.
* Returns -1 if `origin` isn't found in the path.
*
* @param {XSN.Expression} expr
* @param {XSN.Artifact} origin
* @returns {number}
*/
function findRelativeRoot( expr, origin ) {
if (!origin._main) // main artifacts can't have outer references
return expr.path[0]?._artifact === origin ? 0 : -1;
const { path } = expr;
for (let i = 0; i < path.length; ++i) {
const item = path[i];
if (item._artifact === origin)
return i;
}
return -1;
}
function prependRootPath( origin, art, expr ) {
const path = [];
while (!isAnnoRootArt( art )) {
const item = { id: art.name.id };
setArtifactLink( item, art );
do
art = art._parent;
while (art.kind === 'select');
path.push(item);
}
if (art.kind === 'param') {
const param = { id: art.name.id };
setArtifactLink( param, art );
path.push(param);
expr.scope = 'param';
}
else {
const self = makeDollarSelfItem( art );
path.push(self);
}
path.reverse();
prependToStrippedPath( origin, expr, path );
}
function makeDollarSelfItem( art ) {
const self = { id: '$self' };
setLink( self, '_artifact', art );
setLink( self, '_navigation', art.$tableAliases.$self );
return self;
}
/**
* Rewrite the item in `expr.path` at the given index.
* This function may splice the array if more than one path segment
* is replaced by a single item (e.g. in queries).
*
* @param {XSN.Expression} expr
* @param {AnnoRewriteConfig} config
* @param {object} env
* @param {number} index
* @returns {*|null}
*/
function rewriteItem( expr, config, env, index ) {
const rewriteTarget = findRewriteTarget( expr, index, env, config.target );
const found = setArtifactLink( expr.path[index], rewriteTarget[0] );
if (!found)
return null;
if (rewriteTarget[1] > index) {
// we keep the last segment, in case it has non-enumerable properties
expr.path[index] = expr.path[rewriteTarget[1]];
expr.path.splice(index + 1, rewriteTarget[1] - index);
}
const item = expr.path[index];
if (item.id !== found.name.id || (rewriteTarget[1] - index) !== 0) {
// Path was rewritten; original token text string is no longer accurate
config.tokenExpr.$tokenTexts = true;
item.id = found.name.id;
}
return setArtifactLink( expr.path[index], found );
}
}
/**
* @param {XSN.Expression} expr
* @returns {boolean}
*/
function isAnnoPathAbsolute( expr ) {
return expr.path[0]?._navigation?.kind === '$self' || expr.scope === 'param';
}
/**
* Returns true if the given artifact is a root artifact in terms of annotation paths.
* E.g. an element is never a root, but an entity is, as it can be referred to as `$self`,
* but also a param, as it can be referred to as `:P`.
*
* @param {XSN.Artifact} art
* @returns {boolean}
*/
function isAnnoRootArt( art ) {
return !art._parent || !art._main || art.kind === 'param';
}
/**
* Get the root artifact according to the rules of isAnnoRootArt(art).
*
* @param {XSN.Artifact} art
* @returns {XSN.Artifact}
*/
function annoRootArt( art ) {
while (art && !isAnnoRootArt( art ))
art = art._parent;
return art;
}
/**
* @param {XSN.Artifact} art
* @returns {boolean}
*/
function isReturnParam( art ) {
return art?.kind === 'param' && art.name.id === '';
}
/**
* Gets the artifact (e.g. element) that was expanded. `target` is a sub-artifact of that root and
* is an expanded element.
*
* - expandedRoot: Top-most structure that was expanded.
* - expandedRootType: The type of expandedRoot.
*
* @param {XSN.Artifact} target
* @returns { {expandedRoot: XSN.Artifact, expandedRootType: XSN.Artifact}}
*/
function getExpandRoot( target ) {
if (target.$inferred !== 'expanded' && target.$inferred !== 'rewrite')
return { expandedRoot: null, expandedRootType: null };
let expandedRoot = target;
// 'expanded' for structures, 'rewrite' for foreign keys
while (expandedRoot.$inferred === 'expanded' || expandedRoot.$inferred === 'rewrite')
expandedRoot = expandedRoot._parent;
// `items` may be inferred via a type, hence why we check `items.type` after `type`
let expandedRootType = expandedRoot?.type || expandedRoot?.items?.type;
expandedRootType = (!expandedRootType?.$inferred && expandedRootType?._artifact) || null;
const viaInclude = expandedRoot?.$inferred === 'include';
expandedRoot = !viaInclude && expandedRootType ? expandedRoot : false;
return { expandedRoot, expandedRootType };
}
module.exports = {
xprRewriteFns,
};