@sap/cds-compiler
Version:
CDS (Core Data Services) compiler and backends
460 lines (418 loc) • 16.8 kB
JavaScript
// Propagate properties in XSN
// See also internalDoc/PropagatedCsn.md.
// As opposed to that document, the propagator here works on XSN, not CSN.
// We also do not deep-copy member dictionaries here, but create proxy members
// which get their properties via propagation: we use function `onlyViaParent`
// if that property would not be propagated otherwise.
'use strict';
const {
forEachDefinition,
forEachMember,
forEachGeneric,
} = require( '../base/model');
const {
setLink,
linkToOrigin,
withAssociation,
viewFromPrimary,
copyExpr,
} = require('./utils');
const { propagationRules } = require('../base/builtins');
const $inferred = Symbol.for( 'cds.$inferred' );
const { xprRewriteFns } = require('./xpr-rewrite');
// const { ref } = require( '../model/revealInternalProperties' )
// Note that propagation here is also used for deep-copying (function `onlyViaParent`)
function propagate( model ) {
const props = {
'@': annotation, // always except in 'items' (and parameters for entity return types)
doc: docComment, // like annotations, but guarded by option `propagateDocComments`
default: withKind, // always except in 'items'
virtual,
notNull,
targetElement: onlyViaParent, // in foreign keys
value: enumOrCalcValue, // enum symbol value, calculated element
// `value` is also used for column expression
// TODO(!): think of having an extra XSN property for calculated elements,
// replacing `value:…`+`$syntax:'calc'` and `$calc:…
$calc: enumOrCalcValue,
// masked: special = done in definer
// key: special = done in resolver
// actions: struct includes & primary source = in definer/resolver
type: notWithExpand,
length: always,
precision: always,
scale: always,
srid: always,
localized,
target: notWithExpand,
targetAspect,
cardinality: notWithExpand,
on: notWithExpand,
// "expensive" includes "notWithExpand"
// required for places where we don't handle associations, such as in parameters;
// otherwise already expanded and rewritten.
foreignKeys: expensive,
items,
// required for propagation in targetAspect; otherwise already expanded
elements: expensive,
// already expanded if necessary
// enum: expensive,
// params: expensive, // actually only with parent action
// returns,
$enclosed: annotation, // TODO: hm
};
const ruleToFunction = {
__proto__: null,
never,
onlyViaArtifact,
onlyViaParent,
notWithPersistenceTable,
};
for (const rule in propagationRules)
props[rule] = ruleToFunction[propagationRules[rule]];
const { rewriteAnnotationsRefs, rewriteRefsInExpression } = xprRewriteFns( model );
const { message, throwWithError } = model.$messageFunctions;
const { withLocalizedData } = model.$functions;
forEachDefinition( model, run );
forEachGeneric( model, 'vocabularies', run );
// TODO: move 'virtual' handling/checks to resolver
forEachDefinition( model, checkVirtual );
throwWithError();
return model;
function run( art ) {
if (!art)
return;
if (!checkAndSetStatus( art ) || art.kind === 'select') {
runMembers( art );
return;
}
// if (!art.builtin)console.log('RUN:', ref(art))
const chain = [];
let targets = [ art ];
while (targets.length) {
const news = [];
for (const target of targets) {
const origin = getOrigin( target );
if (origin && origin.kind !== '$self') {
// Calculated elements that are simple references: `calc = field;`.
// Respect sibling properties in inheritance.
if (target._calcOrigin?._origin && target.value?._artifact) {
chain.push({ target, source: target.value._artifact });
if (checkAndSetStatus( target.value._artifact ))
news.push( target.value._artifact );
}
chain.push( { target, source: origin } );
if (checkAndSetStatus( origin ))
news.push( origin );
}
for (const ref of target.includes || []) {
const include = ref._artifact;
if (!include)
continue;
chain.push( { target, source: include } );
if (checkAndSetStatus( include ))
news.push( include );
}
}
targets = news;
}
chain.reverse();
chain.forEach( step );
runMembers( art );
// if(!art.builtin)console.log('DONE:',ref(art),art.elements?Object.keys(art.elements):0);
}
function runMembers( art ) {
// if(!art.builtin)console.log('MEMBERS:',ref(art))
forEachMember( art, run ); // after propagation in parent!
// propagate to sub query elements even if not requested:
if (art.$queries)
art.$queries.forEach( run );
let obj = art;
if (art.returns) {
obj = art.returns;
run( obj );
}
if (obj.items)
run( obj.items );
obj = obj.targetAspect;
// if(obj)console.log('TA:',ref(art),!!getOrigin( obj ))
if (obj && isAnonymousAspect( obj ))
run( obj );
setLink( art, '_status', 'propagated' );
}
function isAnonymousAspect( aspect ) {
while (aspect) {
if (aspect.elements)
return true;
aspect = getOrigin( aspect );
}
return false;
}
function step({ target, source }) {
const viaType = target.type && // TODO: falsy $inferred value instead of 'cast'?
(!target.type.$inferred || target.type.$inferred === 'cast');
const keys = Object.keys( source );
// console.log('PROPS:',ref(source),'->',ref(target),keys.join('+'))
for (const prop of keys) {
// TODO: warning with competing props from multi-includes, but not in propagator.js
if (target[prop] !== undefined &&
(prop !== 'value' || !source.$calcDepElement || !target._main?.query) ||
source[prop] === undefined)
continue;
const transformer = props[prop] || props[prop.charAt(0)];
if (transformer)
transformer( prop, target, source, viaType );
}
}
function never() { /* no-op: don't propagate */ }
function always( prop, target, source ) {
const val = source[prop];
if (Array.isArray( val )) {
target[prop] = [ ...val ];
target[prop].$inferred = 'prop';
}
else if (prop.charAt(0) === '@' && val?.kind === '$annotation') {
target[prop] = Object.assign( copyExpr( val ), { $inferred: 'prop' } );
rewriteAnnotationsRefs( target, source, prop );
}
else {
target[prop] = Object.assign( {}, val, { $inferred: 'prop' } );
if (val._artifact !== undefined)
setLink( target[prop], '_artifact', val._artifact );
if (val._outer !== undefined)
setLink( target[prop], '_outer', val._outer );
if (val._parent !== undefined)
setLink( target[prop], '_parent', val._parent );
if (val._main !== undefined)
setLink( target[prop], '_main', val._main );
}
}
function availableAtType( prop, target, source ) {
if (target.kind === 'type')
return false;
const ref = target.type || source.type;
const type = ref && ref._artifact;
if (!type || type._main)
return false;
// We do not consider the $expand status, as elements are already expanded
// by the resolve()
run( type );
return type[prop];
}
// Expensive properties are not really propagated if they can be directly
// accessed at their type being a main artifact
// Expensive properties are also not propagated with `expand`:
// * `elements`: the compiler calculates its own `elements` for a structure
// ref with `expand`.
// * `params`: no element has parameters
// * `enum`: an enum cannot be used with `expand`
// * `keys`: should also not be propagated with `expand`
function expensive( prop, target, source ) {
// console.log('EXP:',prop,ref(source),'->',ref(target));
if (source.kind === 'builtin')
return;
if (target.expand) // do not propagate `keys` with expand
return;
if (prop !== 'foreignKeys' && availableAtType( prop, target, source ))
// foreignKeys must always be copied with target to avoid any confusion
// whether we have to generated implicit keys
return;
if (prop === 'params' && target.$inferred !== 'proxy' && target.$inferred !== 'include')
return;
// Remark: occurrences of `foreignKeys` which are not propagated already in
// tweak-assocs.js: inside `targetAspect` and parameters
const dict = source[prop];
if (prop === 'foreignKeys' && (!dict || target.on))
return; // e.g. published associations with filters, or `Association to many …`
const location = target.type && !target.type.$inferred && target.type.location ||
target.location ||
target._outer && target._outer.location;
target[prop] = Object.create( null ); // also propagate empty elements
const propagateKey = target.kind === 'aspect'; // anonymous aspect
for (const name in dict) {
const origin = dict[name];
const member = linkToOrigin( origin, name, target, prop, location );
if (propagateKey && origin.key)
member.key = Object.assign( { $inferred: 'expanded' }, origin.key );
member.$inferred = 'proxy';
if (prop === 'foreignKeys')
setLink( member, '_effectiveType', member );
else
setEffectiveType( member, dict[name] );
}
target[prop][$inferred] = 'prop';
}
// Only propagate if parent object (which is not necessarily `_parent`) was propagated.
function onlyViaParent( prop, target, source ) {
if (target.$inferred === 'proxy' || target.$inferred === 'expanded')
// assocs and enums do not have 'include'
always( prop, target, source );
}
function targetAspect( prop, target, source ) {
if (target.targetAspect)
return;
if (target.type?._artifact === model.definitions['cds.Association'])
return; // don't propagate targetAspect to associations (e.g. via $enclosed)
const ta = source.targetAspect;
if (!ta.elements && !ta._origin) { // _origin set for elements in source
notWithExpand( prop, target, source );
}
else {
const tat = { location: ta.location, $inferred: 'prop', kind: 'aspect' };
setLink( tat, '_origin', ta );
setLink( tat, '_outer', target );
setLink( tat, '_parent', target._parent );
setLink( tat, '_main', null );
target.targetAspect = tat;
// console.log('TAC:',ref(tat),'via',ref(ta))
}
}
function enumOrCalcValue( prop, destination, origin ) {
// Remark: with include, the calc expression has been copied early
if (prop === 'value' && !origin.$calcDepElement) {
onlyViaParent( prop, destination, origin ); // enum value
}
else if (destination.kind === 'element' &&
destination._main?.query && // query element
!destination.$calc && origin.$calc !== true &&
!model.options.noDollarCalc ) {
destination.$calc
= Object.assign( copyExpr( origin[prop] ), { $inferred: 'prop' } );
if (rewriteRefsInExpression( destination, origin, '$calc' ))
destination.$calc = true; // TODO: or { val: true }?
}
}
function notWithExpand( prop, target, source ) {
if (!target.expand || prop === 'type' && source.elements)
always( prop, target, source );
}
function notWithPersistenceTable( prop, target, source ) {
const tableAnno = target['@cds.persistence.table'];
if (!tableAnno || tableAnno.val === null)
annotation( prop, target, source );
}
function annotation( prop, target, source ) {
const anno = source[prop];
if (anno.val !== null)
withKind( prop, target, source ); // TODO: unfold
}
function docComment( prop, target, source ) {
if (model.options.propagateDocComments)
annotation( prop, target, source );
else // TODO: or just "never"
onlyViaParent( prop, target, source );
}
function onlyViaArtifact( prop, target, source ) {
const from = viewFromPrimary( target )?.path;
// do not propagate from member / if follow assoc in from or into `returns` of actions (v4)
if (!(from ? from[from.length - 1]._artifact : source)._main &&
!(target._parent && target._parent.returns === target))
annotation( prop, target, source );
}
/**
* Propagate `localized`, but not for a texts entity
* (as `null` in an Universal CSN).
*/
function localized( prop, target, source ) {
const main = target._main;
if (target.kind === 'element' && main.kind === 'entity' && !main.query) {
const { id } = main.name;
const base = id.endsWith( '.texts' ) &&
model.definitions[id.slice( 0, -'.texts'.length )];
if (base && withLocalizedData( base, main )) {
target.localized = { $inferred: 'NULL', val: undefined }; // null in UCSN
return;
}
}
withKind( prop, target, source );
}
function withKind( prop, target, source ) {
if (target.kind === 'param' && source.kind === 'entity')
return; // Don't propagate from entity types to parameters (+ return type).
if (target.kind)
always( prop, target, source ); // not in 'items'
}
function notNull( prop, target, source, _viaType ) {
// Really "reset" NOT NULL when ref has assoc with cardinality min: 0 (TODO: Universal CSN)
if (target.value && withAssociation( target.value, targetMinZero ))
target[prop] = { $inferred: 'NULL', val: undefined }; // set null value in Universal CSN
// $inferred: 'NULL' is only an issue for sub elements with a 'value' property;
// it only exists with nested projections, i.e. never with deprecated option enabled
else
always( prop, target, source );
}
function virtual( prop, target, source, viaType ) {
if (!viaType)
always( prop, target, source );
else // NULL would block strange propagation to sub element
target[prop] = { $inferred: 'NULL', val: undefined }; // set null value in Universal CSN
}
function checkVirtual( view ) {
if (view.query)
forEachGeneric( view, 'elements', checkNonVirtualElement );
}
function checkNonVirtualElement( elem ) {
// Not enough at all, but so are the current checks - a complete expression
// must be checked. Here we just check what might have worked before.
// TODO: Propagate 'virtual' in resolver.
const path = !elem.virtual && elem.value && elem.value.path;
if (!path || path.broken)
return;
for (const item of path) {
const art = item && item._artifact;
if (art?.virtual?.val) {
message( 'def-missing-virtual', [ item.location, elem ], { art, keyword: 'virtual' },
// eslint-disable-next-line @stylistic/max-len
'Prepend $(KEYWORD) to current select item - containing element $(ART) is virtual' );
return;
}
}
}
function returns( prop, target, source, ok ) {
if (ok || target.$inferred === 'proxy' || target.$inferred === 'include' ) {
target[prop] = { $inferred: 'proxy' };
setEffectiveType( target[prop], source[prop] );
setLink( target[prop], '_origin', source[prop] );
setLink( target[prop], '_outer', target._outer || target ); // for setMemberParent
}
}
function items( prop, target, source ) {
// usually considered expensive, except:
// - array of Entity
const line = availableAtType( prop, target, source );
if (!line ||
line.type && line.type._artifact && line.type._artifact.kind === 'entity')
returns( prop, target, source, true );
}
}
function targetMinZero( art ) {
// Semantics of associations without provided cardinality: [*,0..1]
return !(art.cardinality && art.cardinality.targetMin && art.cardinality.targetMin.val);
}
function getOrigin( art ) {
let origin = art._origin;
while (origin?.kind === 'select')
origin = origin._origin;
if (origin)
// Do not consider _origin if due to expand of table alias ref
return (!art.expand || origin.kind === 'element') && origin;
// Remark: a column with an 'inline' is never an element -> no need to check
// art.inline
return (art.type && (!art.type.$inferred || art.type.$inferred === 'cast'))
? art.type._artifact
: art._origin;
}
function checkAndSetStatus( art ) {
if (art._status === 'propagated' || art._status === 'propagating')
return false;
setLink( art, '_status', 'propagating' );
return true;
}
function setEffectiveType( target, source ) {
// TODO: when is this already set?
if (source._effectiveType !== undefined)
setLink( target, '_effectiveType', source._effectiveType );
}
module.exports = {
propagate,
};