@sap/cds-compiler
Advanced tools
Comparing version 4.9.0 to 4.9.2
@@ -581,18 +581,8 @@ #!/usr/bin/env node | ||
function displayNamedXsn( xsn, name ) { | ||
if (options.rawOutput) { | ||
if (options.rawOutput) | ||
writeToFileOrDisplay(options.out, `${name}_raw.txt`, util.inspect(reveal(xsn, options.rawOutput), false, null), true); | ||
} | ||
else if (options.internalMsg) { | ||
else if (options.internalMsg) | ||
writeToFileOrDisplay(options.out, `${name}_raw.txt`, util.inspect(reveal(xsn).messages, { depth: null, maxArrayLength: null }), true); | ||
} | ||
else if (!options.parseOnly) { // no output if parseOnly but not rawOutput | ||
const csn = compactModel(xsn, options); | ||
if (command === 'toCsn' && options.tenantDiscriminator) | ||
addTenantFields(csn, options); | ||
if (command === 'toCsn' && options.withLocalized) | ||
addLocalizationViews(csn, options); | ||
if (options.enrichCsn) | ||
enrichCsn( csn, options ); | ||
writeToFileOrDisplay(options.out, `${name}.json`, csn, true); | ||
} | ||
else if (!options.parseOnly) // no output if parseOnly but not rawOutput | ||
displayNamedCsn(compactModel(xsn, options), name); | ||
} | ||
@@ -607,2 +597,14 @@ | ||
return; | ||
if (command === 'toCsn' ) { | ||
// If requested, run some CSN postprocessing. | ||
if (options.tenantDiscriminator) | ||
addTenantFields(csn, options); // always _before_ localized convenience views are added | ||
if (options.withLocalized) | ||
addLocalizationViews(csn, options); | ||
} | ||
if (options.enrichCsn) | ||
enrichCsn( csn, options ); | ||
if (options.internalMsg) { | ||
@@ -612,4 +614,2 @@ writeToFileOrDisplay(options.out, `${name}_raw.txt`, options.messages, true); | ||
else if (!options.internalMsg) { | ||
if (command === 'toCsn' && options.withLocalized) | ||
addLocalizationViews(csn, options); | ||
writeToFileOrDisplay(options.out, `${name}.json`, csn, true); | ||
@@ -616,0 +616,0 @@ } |
@@ -210,3 +210,3 @@ // Consistency checker on model (XSN = augmented CSN) | ||
'$requireElementAccess', '_effectiveType', '$effectiveSeqNo', '_deps', | ||
'$calcDepElement', '$filtered', '_parent', | ||
'$calcDepElement', '$filtered', '$enclosed', '_parent', | ||
], | ||
@@ -265,2 +265,3 @@ schema: { | ||
$filtered: { kind: true, inherits: 'value' }, // for assoc+filter | ||
$enclosed: { kind: true, inherits: 'value' }, // for comp+filter | ||
params: { kind: true, inherits: 'definitions' }, | ||
@@ -267,0 +268,0 @@ _extendType: { kind: true, test: TODO }, |
@@ -1027,11 +1027,7 @@ // Compiler phase 1 = "define": transform dictionary of AST-like XSNs into XSN | ||
if (obj.foreignKeys) { | ||
error( 'type-unexpected-foreign-keys', [ obj.foreignKeys[$location], construct ], | ||
{}, | ||
'A managed aspect composition can\'t have a foreign keys specification' ); | ||
error( 'type-unexpected-foreign-keys', [ obj.foreignKeys[$location], construct ] ); | ||
delete obj.foreignKeys; // continuation semantics: not specified | ||
} | ||
if (obj.on && !obj.target) { | ||
error( 'type-unexpected-on-condition', [ obj.on.location, construct ], | ||
{}, | ||
'A managed aspect composition can\'t have a specified ON-condition' ); | ||
error( 'type-unexpected-on-condition', [ obj.on.location, construct ] ); | ||
delete obj.on; // continuation semantics: not specified | ||
@@ -1186,4 +1182,4 @@ } | ||
// Special case (hack) for calculated elements that use associations+filter: | ||
// See "Notes on `$filtered`" in `ExposingAssocWithFilter.md` for details. | ||
// Special case (hack) for calculated elements that use composition+filter: | ||
// See "Notes on `$enclosed`" in `ExposingAssocWithFilter.md` for details. | ||
if (elem.target && elem.value.path?.[elem.value.path.length - 1]?.where) { | ||
@@ -1190,0 +1186,0 @@ delete elem.type; |
@@ -620,4 +620,2 @@ // Populate views with elements, elements with association targets, ... | ||
// effectiveType() must not be called on $self, is unnecessary for mixins: | ||
// TODO: have a test for `select from E { a, $self.a as b, $self.{ b as c } }` | ||
// TODO: have a negative test for `select from E { $self.*, assoc.* }` | ||
// (we might have those already) | ||
@@ -624,0 +622,0 @@ if (alias.kind === 'mixin' || alias.kind === '$self') |
@@ -59,3 +59,4 @@ // Propagate properties in XSN | ||
returns, | ||
$filtered: annotation, | ||
$filtered: annotation, // TODO(v5): Remove | ||
$enclosed: annotation, | ||
}; | ||
@@ -285,3 +286,3 @@ const ruleToFunction = { | ||
if (target.type?._artifact === model.definitions['cds.Association']) | ||
return; // don't propagate targetAspect to associations (e.g. via $filtered) | ||
return; // don't propagate targetAspect to associations (e.g. via $enclosed) | ||
const ta = source.targetAspect; | ||
@@ -288,0 +289,0 @@ if (!ta.elements && !ta._origin) { // _origin set for elements in source |
@@ -8,2 +8,3 @@ // Tweak associations: rewrite keys and on conditions | ||
forEachInOrder, | ||
isBetaEnabled, | ||
} = require('../base/model'); | ||
@@ -22,2 +23,3 @@ const { dictLocation, weakLocation, weakRefLocation } = require('../base/location'); | ||
setExpandStatus, | ||
getUnderlyingBuiltinType, | ||
} = require('./utils'); | ||
@@ -44,2 +46,8 @@ const { Location } = require('../base/location'); | ||
Object.assign(model.$functions, { | ||
firstProjectionForPath, | ||
}); | ||
const isV5preview = isBetaEnabled( model.options, 'v5preview' ); | ||
// Phase 5: rewrite associations | ||
@@ -454,10 +462,23 @@ model._entities.forEach( rewriteArtifact ); | ||
elem.$filtered = { | ||
val: true, | ||
literal: 'boolean', | ||
location, | ||
$inferred: '$generated', | ||
}; | ||
if (!isV5preview) { // TODO(v5): Remove, only use $enclosed | ||
elem.$filtered = { | ||
val: true, | ||
literal: 'boolean', | ||
location, | ||
$inferred: '$generated', | ||
}; | ||
} | ||
const isComp = (getUnderlyingBuiltinType( assoc )?.name?.id === 'cds.Composition'); | ||
if (isComp) { | ||
elem.$enclosed = { | ||
val: true, | ||
literal: 'boolean', | ||
location, | ||
$inferred: '$generated', | ||
}; | ||
} | ||
} | ||
/** | ||
@@ -548,3 +569,3 @@ * Transform a filter on `assocPathStep` into an ON-condition. | ||
if (elem.$syntax !== 'calc') { // different to lhs! | ||
const projectedFk = firstProjectionForPath( rhs.path, nav.tableAlias, elem ); | ||
const projectedFk = firstProjectionForPath( rhs.path, 0, nav.tableAlias, elem ); | ||
rewritePath( rhs, projectedFk.item, elem, projectedFk.elem, elem.value.location ); | ||
@@ -604,3 +625,4 @@ } | ||
return; // are not allowed anyway - there was an error before | ||
const result = firstProjectionForPath( expr.path, tableAlias, assoc ); | ||
const startIndex = (root.kind === '$self' ? 1 : 0); | ||
const result = firstProjectionForPath( expr.path, startIndex, tableAlias, assoc ); | ||
// For `assoc[…]`, ensure that we don't rewrite to another projection on `assoc`. | ||
@@ -791,27 +813,34 @@ if (result.item && assoc._origin === result.item._artifact) | ||
/** | ||
* For a path `a.b.c.d`, return a projection for the first path item that is projected. | ||
* For a path `a.b.c.d`, return a projection for the first path item that is projected, | ||
* starting at `startIndex` in this path using the given navigation (table alias or | ||
* navigation element). | ||
* For example, if a query has multiple projections such as `a.b, a, a.b.c`, the | ||
* _first_ possible projection will be used and the caller can rewrite `a.b.c.d` to `b.c.d`. | ||
* This avoids that `extend`s affect the ON-condition. | ||
* This avoids `extend`s affect the ON-condition. | ||
* | ||
* The returned object `ret` has `ret.item`, which is the path item that is projected. | ||
* `ret.elem` is the element projection. | ||
* The returned object `ret` has `ret.item`, which is the path item at index `ret.index` | ||
* that is projected. `ret.elem` is the element projection. | ||
* | ||
* @param {any[]} path | ||
* @param {object} tableAlias | ||
* @param {object} assoc Preferred association that should be used if projected. | ||
* @param {number} startIndex | ||
* @param {object} nav | ||
* @param {object} elem Preferred association/element that should be used if projected. | ||
* @return {{elem: object, item: object}|null} | ||
*/ | ||
function firstProjectionForPath( path, tableAlias, assoc ) { | ||
const viaSelf = (path[0]._navigation || path[0]._artifact).kind === '$self'; | ||
const root = viaSelf ? 1 : 0; | ||
if (root >= path.length) // e.g. just `$self` path item | ||
function firstProjectionForPath( path, startIndex, nav, elem ) { | ||
if (startIndex >= path.length) // e.g. just `$self` path item | ||
return { item: undefined, elem: {} }; | ||
let tableAlias = nav; | ||
while (tableAlias.kind === '$navElement') | ||
tableAlias = tableAlias._parent; | ||
// We want to use the _first_ valid projection that is written by the user (if the preferred | ||
// `assoc` is not directly projected). To achieve that, look into the table alias' elements. | ||
// `assoc` is not directly projected). To achieve that, look into the query's elements. | ||
const selectedElements = Object.values(tableAlias._parent.elements); | ||
const proj = []; | ||
let navItem = tableAlias; | ||
for (const item of path.slice(root)) { | ||
let proj = null; | ||
let navItem = nav; | ||
for (let i = startIndex; i < path.length; ++i) { | ||
const item = path[i]; | ||
navItem = item?.id && navItem.elements?.[item.id]; | ||
@@ -822,10 +851,14 @@ if (!navItem) { | ||
else if (navItem._projections) { | ||
const elem = navProjection( navItem, assoc ); | ||
if (elem && elem === assoc) { | ||
const projElem = navProjection( navItem, elem ); | ||
if (projElem && projElem === elem) { | ||
// in case the specified association is found, _always_ use it. | ||
return { item, elem }; | ||
return { index: i, item, elem }; | ||
} | ||
else if (elem) { | ||
const index = selectedElements.indexOf(elem); | ||
proj.push({ item, elem, index }); | ||
else if (projElem) { | ||
const queryIndex = selectedElements.indexOf(projElem); | ||
if (!proj || queryIndex < proj.queryIndex) { | ||
proj = { | ||
index: i, item, elem: projElem, queryIndex, | ||
}; | ||
} | ||
} | ||
@@ -835,5 +868,3 @@ } | ||
return (proj.length === 0) | ||
? { item: path[root], elem: null } | ||
: proj.reduce( (acc, curr) => (acc.index > curr.index ? curr : acc), proj[0] ); // first | ||
return proj || { index: startIndex, item: path[startIndex], elem: null }; | ||
} | ||
@@ -840,0 +871,0 @@ |
@@ -101,6 +101,7 @@ // Rewrite paths in annotation expressions. | ||
// ---------------------- | ||
// A bare select item that gets an annotation via propagation from its origin behaves | ||
// similar to an element that gets it via an include. | ||
// 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. | ||
// | ||
@@ -111,2 +112,7 @@ // Instead of changing the path prefix, we need to check if the referenced path | ||
// | ||
// 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: | ||
@@ -163,2 +169,3 @@ // For now, we do not rewrite sub-structure elements. The whole structure needs | ||
isInFilter; | ||
tokenExpr; | ||
} | ||
@@ -173,2 +180,3 @@ | ||
resolvePathRoot, | ||
firstProjectionForPath, | ||
} = model.$functions; | ||
@@ -282,2 +290,7 @@ | ||
// 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; | ||
@@ -343,8 +356,37 @@ if (config.isViaType || config.isViaCalcElement) | ||
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._pathHead; | ||
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' ); | ||
// root item is element reference (others were already rejected) | ||
if (isAnnoRootArt( target )) | ||
return navigationEnv( target, null, null, 'nav' ); | ||
return navigationEnv( target._parent, 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' ); | ||
} | ||
@@ -359,6 +401,11 @@ | ||
function rewriteGenericAnnoPath( expr, config, refCtx ) { | ||
const rootIndex = isAnnoPathAbsolute( expr ) ? 1 : 0; | ||
let env = getRootEnv( expr, config ); | ||
const isAbsolute = isAnnoPathAbsolute( expr ); | ||
const rootIndex = isAbsolute ? 1 : 0; | ||
// reset artifact link | ||
// 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 | ||
setArtifactLink( expr, null ); | ||
@@ -368,3 +415,3 @@ | ||
const rootItem = expr.path[0]; | ||
if (rootIndex === 1) { | ||
if (isAbsolute) { | ||
delete rootItem._artifact; | ||
@@ -379,6 +426,6 @@ delete rootItem._navigation; | ||
let env = rootEnv; | ||
let art = rootItem._artifact; | ||
for (let i = rootIndex; i < expr.path.length; ++i) { | ||
const item = expr.path[i]; | ||
art = rewriteItem( expr, config, env, item ); | ||
art = rewriteItem( expr, config, env, i ); | ||
if (!art) | ||
@@ -593,44 +640,70 @@ return reportAnnoRewriteError( expr, config ); | ||
function rewriteItem( expr, config, env, item ) { | ||
const found = setArtifactLink( item, findRewriteTarget( item, env, config.target )); | ||
if (found) { | ||
if (item.id !== found.name.id) { | ||
// Path was rewritten; original token text string is no longer accurate | ||
config.tokenExpr.$tokenTexts = true; | ||
item.id = found.name.id; | ||
} | ||
return found; | ||
/** | ||
* 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 item = expr.path[index]; | ||
const rewriteTarget = findRewriteTarget( expr, index, env, config.target ); | ||
const found = setArtifactLink( item, rewriteTarget[0] ); | ||
if (!found) | ||
return null; | ||
if (item.id !== found.name.id) { | ||
// Path was rewritten; original token text string is no longer accurate | ||
config.tokenExpr.$tokenTexts = true; | ||
item.id = found.name.id; | ||
} | ||
return null; | ||
if (rewriteTarget[1] > index) | ||
expr.path.splice(index + 1, rewriteTarget[1] - index); | ||
return rewriteTarget[0]; | ||
} | ||
function findRewriteTarget( item, env, target ) { | ||
if (!env.query && env.kind !== 'select') | ||
return env.elements?.[item.id] || null; | ||
function findRewriteTarget( expr, index, env, target ) { | ||
if (env.kind === '$navElement' || env.kind === '$tableAlias') { | ||
const r = firstProjectionForPath( expr.path, index, env, target ); | ||
return [ r.elem, r.index ]; | ||
} | ||
const item = expr.path[index]; | ||
// Not a query -> no $navElement -> use `elements` | ||
if (!env.query && env.kind !== 'select') { | ||
if (env.elements?.[item.id]) | ||
return [ env.elements[item.id], index ]; | ||
return [ null, expr.path.length ]; | ||
} | ||
const items = (env._leadingQuery || env)._combined?.[item.id]; | ||
const navs = !items || Array.isArray(items) ? items : [ items ]; | ||
const allNavs = !items || Array.isArray(items) ? items : [ items ]; | ||
// If the annotation target itself has a table alias, require projections of that | ||
// table alias. Of course, that only works if we're talking about the same query. | ||
const tableAlias = (target._main?._origin === item._artifact._main && | ||
target.value?.path[0]?._navigation?.kind === '$tableAlias') | ||
? target.value.path[0]._navigation : null; | ||
// Look at all table aliase that could project `item` and only select | ||
// those that have actual projections. | ||
let projected = navs?.filter(p => p._origin === item._artifact && p._projections); | ||
if (!projected || projected.length === 0) | ||
return null; | ||
const navs = allNavs?.filter(p => p._origin === item._artifact && | ||
(!tableAlias || tableAlias === p._parent)); | ||
if (!navs || navs.length === 0) | ||
return [ null, expr.path.length ]; | ||
// If the annotation target itself has a table alias, prefer projections | ||
// of that table alias over others when rewriting. | ||
const tableAlias = target.value?.path[0]?._navigation; | ||
if (tableAlias?.kind === '$tableAlias') { | ||
// TODO: Is the _parent always a table alias? | ||
const taProjected = projected.filter(p => p._parent === tableAlias); | ||
if (taProjected.length) | ||
projected = taProjected; | ||
// If there are multiple navigations for the element, just use the first that matches. | ||
// In case of table aliases, it's just one. | ||
for (const nav of navs) { | ||
const r = firstProjectionForPath( expr.path, index, nav._parent, target ); | ||
if (r.elem) | ||
return [ r.elem, r.index ]; | ||
} | ||
// Of the possible entries, choose the first one | ||
projected = projected[0]; | ||
// If there are multiple projections, check if the annotation target is | ||
// projected as well, otherwise, simply take the first one. | ||
return projected._projections.find(proj => proj === target) || | ||
projected._projections[0] || null; | ||
return [ null, expr.path.length ]; | ||
} | ||
@@ -637,0 +710,0 @@ } |
@@ -251,4 +251,11 @@ 'use strict'; | ||
transform.in = (parent, prop, xpr) => { | ||
evalArgs({ min: 1 }, xpr[1].list, prop); | ||
parent.$In = [ xpr[0], ...xpr[1].list ]; | ||
let args = xpr[1].list; | ||
if (!args) { | ||
if (Array.isArray(xpr[1].xpr)) | ||
args = xpr[1].xpr; | ||
else | ||
args = [ xpr[1] ]; | ||
} | ||
evalArgs({ min: 1 }, args, prop); | ||
parent.$In = [ xpr[0], args ]; | ||
delete parent[prop]; | ||
@@ -297,5 +304,5 @@ transformExpression(parent, undefined, transform); | ||
transform.$Mul = noOp; | ||
transform['/'] = op('$Div'); | ||
transform.$Div = noOp; | ||
// $DivBy, $Mod are functions | ||
transform['/'] = op('$DivBy'); | ||
transform.$DivBy = noOp; | ||
// $Div, $Mod are functions | ||
//---------------------------------- | ||
@@ -580,2 +587,11 @@ // LITERALS | ||
else { | ||
// Error out for arbitrary types until we know better | ||
// probably todo: Check for reachability of arb type names such as namespace | ||
// reqDef entry etc... | ||
if (typeDef) { // eslint-disable-line no-lonely-if | ||
error('odata-anno-xpr-type', location, { | ||
anno, op: `${xpr}(…)`, type: `${typeDef}`, '#': 'edm', | ||
}); | ||
} | ||
/* | ||
typeFacets.forEach((facet) => { | ||
@@ -588,2 +604,3 @@ if (facet.args.length === 1 && facet.args[0].val) { | ||
}); | ||
*/ | ||
} | ||
@@ -655,4 +672,4 @@ | ||
Has: [ twoArgs, dollar ], | ||
$DivBy: twoArgs, | ||
DivBy: [ twoArgs, dollar ], | ||
$Div: twoArgs, | ||
Div: [ twoArgs, dollar ], | ||
$Mod: twoArgs, | ||
@@ -659,0 +676,0 @@ Mod: [ twoArgs, dollar ], |
@@ -96,3 +96,4 @@ // Transform XSN (augmented CSN) into CSN | ||
target, | ||
$filtered: value, // assoc+filter | ||
$filtered: value, // assoc+filter v4 | ||
$enclosed: value, // comp+filter v5 | ||
foreignKeys, | ||
@@ -99,0 +100,0 @@ enum: enumDict, |
@@ -920,7 +920,7 @@ // CSN functionality for resolving references | ||
function initColumnElement( col, colIndex, parentElementOrQueryCache ) { | ||
function initColumnElement( col, colIndex, parentElementOrQueryCache, externalElements ) { | ||
if (col === '*') | ||
return; | ||
if (col.inline) { | ||
col.inline.map( c => initColumnElement( c, null, parentElementOrQueryCache ) ); | ||
col.inline.map( c => initColumnElement( c, null, parentElementOrQueryCache, externalElements ) ); | ||
return; | ||
@@ -940,7 +940,14 @@ } | ||
type = type.items; | ||
if (!type.elements) { | ||
// in OData backend, the sub elements from a column with expand might have | ||
// been “externalized” into a named type. No backward _column link is | ||
// possible this way, of course... | ||
type = artifactRef( type.type ); | ||
externalElements = true; | ||
} | ||
const elem = setCache( col, '_element', type.elements[as] ); | ||
if (elem) // TODO to.sql: something is strange if this is not set | ||
if (elem && !externalElements) // TODO to.sql: something is strange if `elem` is not set | ||
setCache( elem, '_column', col ); | ||
if (col.expand) | ||
col.expand.map( c => initColumnElement( c, null, elem ) ); | ||
col.expand.map( c => initColumnElement( c, null, elem, externalElements ) ); | ||
} | ||
@@ -947,0 +954,0 @@ |
@@ -190,3 +190,9 @@ 'use strict'; | ||
*/ | ||
function attachConstraintsToDependentKeys( dependentKeys, parentKeys, parentTable, sourceAssociation, upLinkFor = null ) { | ||
function attachConstraintsToDependentKeys( | ||
dependentKeys, | ||
parentKeys, | ||
parentTable, | ||
sourceAssociation, | ||
upLinkFor = null | ||
) { | ||
while (dependentKeys.length > 0) { | ||
@@ -197,18 +203,30 @@ const dependentKeyValuePair = dependentKeys.pop(); | ||
// this is the case for <up_> associations in on-conditions of compositions | ||
if (Object.prototype.hasOwnProperty.call(dependentKey, '$foreignKeyConstraint')) | ||
const { $foreignKeyConstraint } = dependentKey; | ||
// in contrast to foreign keys which stem from managed associations, | ||
// a tenant foreign key column may have multiple parent keys as partners | ||
const tenantForeignKey = isTenant && dependentKeyValuePair[0] === 'tenant'; | ||
if ($foreignKeyConstraint && (!tenantForeignKey || $foreignKeyConstraint.upLinkFor)) { | ||
return; | ||
} | ||
else if ($foreignKeyConstraint && tenantForeignKey) { | ||
parentKeys.pop(); | ||
$foreignKeyConstraint.sourceAssociation.push(sourceAssociation); | ||
} | ||
else { | ||
const parentKeyValuePair = parentKeys.pop(); | ||
const parentKeyName = parentKeyValuePair[0]; | ||
const parentKeyValuePair = parentKeys.pop(); | ||
const parentKeyName = parentKeyValuePair[0]; | ||
const constraint = { | ||
parentKey: parentKeyName, | ||
parentTable, | ||
upLinkFor, | ||
sourceAssociation, | ||
onDelete: upLinkFor ? 'CASCADE' : 'RESTRICT', | ||
validated, | ||
enforced, | ||
}; | ||
dependentKey.$foreignKeyConstraint = constraint; | ||
const constraint = { | ||
parentKey: parentKeyName, | ||
parentTable, | ||
upLinkFor, | ||
sourceAssociation: tenantForeignKey | ||
? [ sourceAssociation ] | ||
: sourceAssociation, | ||
onDelete: upLinkFor ? 'CASCADE' : 'RESTRICT', | ||
validated, | ||
enforced, | ||
}; | ||
dependentKey.$foreignKeyConstraint = constraint; | ||
} | ||
} | ||
@@ -478,7 +496,7 @@ } | ||
/** | ||
* Creates the final referential constraints from all dependent key <-> parent key pairs stemming from the same $sourceAssociation | ||
* Creates the final referential constraints from all dependent key <-> parent key pairs stemming from the same sourceAssociation | ||
* and attaches it to the given artifact. | ||
* | ||
* Go over all elements with $foreignKeyConstraint property: | ||
* - Find all other elements in artifact with the same $sourceAssociation | ||
* - Find all other elements in artifact with the same sourceAssociation | ||
* - Create constraints with the information supplied by $parentKey, $parentTable and $onDelete | ||
@@ -491,2 +509,19 @@ * | ||
const referentialConstraints = Object.create(null); | ||
// tenant foreign keys may have multiple parent keys | ||
// process tenant foreign key first | ||
if (isTenant && artifact.elements?.tenant) { | ||
const element = artifact.elements.tenant; | ||
if (element.$foreignKeyConstraint) { | ||
const $foreignKeyConstraint = Object.assign({}, element.$foreignKeyConstraint); | ||
delete element.$foreignKeyConstraint; | ||
// create a foreign key constraint for the tenant column with each association in the dependent entity | ||
$foreignKeyConstraint.sourceAssociation.forEach((sourceAssociation) => { | ||
const copy = Object.assign({}, $foreignKeyConstraint); | ||
copy.sourceAssociation = sourceAssociation; | ||
createReferentialConstraints(copy, 'tenant'); | ||
}); | ||
} | ||
} | ||
for (const elementName in artifact.elements) { | ||
@@ -499,2 +534,20 @@ const element = artifact.elements[elementName]; | ||
delete element.$foreignKeyConstraint; | ||
createReferentialConstraints($foreignKeyConstraint, elementName); | ||
} | ||
if (Object.keys(referentialConstraints).length) { | ||
if (!('$tableConstraints' in artifact)) | ||
artifact.$tableConstraints = Object.create(null); | ||
artifact.$tableConstraints.referential = referentialConstraints; | ||
} | ||
/** | ||
* Creates referential constraints for database relationships. This function constructs constraints based on foreign key information and element names, | ||
* and determines deletion rules based on the existing constraints and options. It manages dependencies and names for constraints dynamically during | ||
* execution. | ||
* | ||
* @param {object} $foreignKeyConstraint - An object encapsulating details about the foreign key constraint | ||
* @param {string} elementName - The name of the dependent element or table that is linked by the foreign key. | ||
*/ | ||
function createReferentialConstraints($foreignKeyConstraint, elementName) { | ||
const { parentTable } = $foreignKeyConstraint; | ||
@@ -506,6 +559,6 @@ const parentKey = [ $foreignKeyConstraint.parentKey ]; | ||
forEach(artifact.elements, (foreignKeyName, foreignKey) => { | ||
// find all other `$foreignKeyConstraint`s with same `$sourceAssociation` and same `parentTable` | ||
// find all other `$foreignKeyConstraint`s with same `sourceAssociation` and same `parentTable` | ||
const matchingForeignKeyFound = foreignKey.$foreignKeyConstraint && | ||
foreignKey.$foreignKeyConstraint.sourceAssociation === $foreignKeyConstraint.sourceAssociation && | ||
foreignKey.$foreignKeyConstraint.parentTable === $foreignKeyConstraint.parentTable; | ||
foreignKey.$foreignKeyConstraint.sourceAssociation === $foreignKeyConstraint.sourceAssociation && | ||
foreignKey.$foreignKeyConstraint.parentTable === $foreignKeyConstraint.parentTable; | ||
if (!matchingForeignKeyFound) | ||
@@ -545,8 +598,2 @@ return; | ||
} | ||
if (Object.keys(referentialConstraints).length) { | ||
if (!('$tableConstraints' in artifact)) | ||
artifact.$tableConstraints = Object.create(null); | ||
artifact.$tableConstraints.referential = referentialConstraints; | ||
} | ||
} | ||
@@ -553,0 +600,0 @@ } |
@@ -245,3 +245,3 @@ 'use strict'; | ||
// setProp(parent, '$structRef', parent.ref); | ||
parent.ref = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes); | ||
[ parent.ref ] = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes); | ||
resolved.set(parent, { links, art, scope }); | ||
@@ -547,3 +547,3 @@ // Explicitly set implicit alias for things that are now flattened - but only in columns | ||
// Now we need to properly flatten the whole ref | ||
clone.ref = flattenStructStepsInRef(clone.ref, pathToKey); | ||
[ clone.ref ] = flattenStructStepsInRef(clone.ref, pathToKey); | ||
} | ||
@@ -550,0 +550,0 @@ if (!clone.as) { |
@@ -954,3 +954,3 @@ 'use strict'; | ||
// First, compute the name from the path, e.g ['s', 's1', 's2' ] will result in 'S_s1_s2' ... | ||
const refPath = flattenStructStepsInRef(val.ref, path); | ||
const [ refPath ] = flattenStructStepsInRef(val.ref, path); | ||
// ... and take this as the prefix for all elements | ||
@@ -971,3 +971,3 @@ const flattenedElems = flattenStructuredElement(art, refPath, [], ['definitions', artName, 'elements']); | ||
// The reference is not structured, so just replace it by a ref to the combined prefix path | ||
const refPath = flattenStructStepsInRef(val.ref, path); | ||
const [ refPath ] = flattenStructStepsInRef(val.ref, path); | ||
flattenedIndex.push({ ref: refPath }); | ||
@@ -974,0 +974,0 @@ } |
@@ -293,2 +293,3 @@ 'use strict'; | ||
function flattenAndPrefixExprPaths(carrier, propNames, csnPath, rootPrefix, typeIdx, refParentIsItems = false) { | ||
const refCheck = { | ||
@@ -320,14 +321,35 @@ ref: (elemref, prop, xpr, path) => { | ||
let refChanged = false; | ||
const absolutifier = { | ||
ref : (parent, prop, xpr) => { | ||
const head = xpr[0].id || xpr[0]; | ||
if (typeIdx < rootPrefix.length && head === '$self' && !isMagicVariable(head)) { | ||
const [xprHead, ...xprTail] = xpr.slice(1, xpr.length); | ||
if(xprHead) { | ||
if (xprHead.id) { | ||
xprHead.id = rootPrefix.slice(1, typeIdx).concat(xprHead.id).join('_'); | ||
parent[prop] = [ xprHead, ...xprTail ]; | ||
let isPrefixed = false; | ||
if(!isMagicVariable(head)) { | ||
if (head === '$self' && typeIdx < rootPrefix.length) { | ||
isPrefixed = true; | ||
const [xprHead, ...xprTail] = xpr.slice(1, xpr.length); | ||
if(xprHead) { | ||
if (xprHead.id) { | ||
xprHead.id = rootPrefix.slice(1, typeIdx).concat(xprHead.id).join('_'); | ||
parent[prop] = [ xprHead, ...xprTail ]; | ||
} | ||
else | ||
parent[prop] = [ rootPrefix.slice(1, typeIdx).concat(xprHead).join('_'), ...xprTail]; | ||
} | ||
} | ||
else if (head !== '$self' && !parent.param && rootPrefix.length > 2) { | ||
isPrefixed = true; | ||
const [xprHead, ...xprTail] = xpr; | ||
if (!refParentIsItems) { | ||
if (xprHead.id) { | ||
xprHead.id = rootPrefix.slice(1, -1).concat(xprHead.id).join('_'); | ||
parent[prop] = [ xprHead, ...xprTail ]; | ||
} | ||
else | ||
parent[prop] = [ rootPrefix.slice(1, -1).concat(xprHead).join('_'), ...xprTail]; | ||
} | ||
else | ||
parent[prop] = [ rootPrefix.slice(1, typeIdx).concat(xprHead).join('_'), ...xprTail]; | ||
parent[prop] = [ ...rootPrefix.slice(0, rootPrefix.length-1), ...xpr]; | ||
} | ||
if(isPrefixed) { | ||
if (carrier.$scope === 'params') | ||
@@ -339,31 +361,17 @@ parent.param = true; | ||
} | ||
else if (rootPrefix.length > 2 && head !== '$self' && !parent.param && !isMagicVariable(head)) { | ||
const [xprHead, ...xprTail] = xpr; | ||
if (!refParentIsItems) { | ||
if (xprHead.id) { | ||
xprHead.id = rootPrefix.slice(1, -1).concat(xprHead.id).join('_'); | ||
parent[prop] = [ xprHead, ...xprTail ]; | ||
} | ||
else | ||
parent[prop] = [ rootPrefix.slice(1, -1).concat(xprHead).join('_'), ...xprTail]; | ||
} | ||
else | ||
parent[prop] = [ ...rootPrefix.slice(0, rootPrefix.length-1), ...xpr]; | ||
if (carrier.$scope === 'params') | ||
parent.param = true; | ||
else | ||
parent[prop].unshift('$self'); | ||
} | ||
if(isPrefixed) | ||
refChanged = isPrefixed; | ||
} | ||
} | ||
propNames.forEach(pn => { | ||
refChanged = false; | ||
refCheck.anno = pn; | ||
transformExpression(carrier, pn, [ refCheck, refFlattener ], csnPath); | ||
adaptRefs.forEach(fn => | ||
{ if( fn(refParentIsItems)) refChanged = true }); | ||
adaptRefs.length = 0; | ||
transformExpression(carrier, pn, absolutifier, csnPath) | ||
if(refChanged && carrier[pn]['=']) | ||
carrier[pn]['='] = true; | ||
}); | ||
adaptRefs.forEach(fn => fn(refParentIsItems)); | ||
adaptRefs.length = 0; | ||
propNames.forEach(pn => { | ||
transformExpression(carrier, pn, absolutifier, csnPath) | ||
}) | ||
} | ||
@@ -443,9 +451,13 @@ | ||
Object.keys(member).filter(pn => pn[0] === '@').forEach(pn => { | ||
let refChanged = false; | ||
refCheck.anno = pn; | ||
transformExpression(member, pn, [ refCheck, refFlattener ], csnPath); | ||
adaptRefs.forEach(fn => { | ||
if (fn(true, 1)) refChanged = true }); | ||
adaptRefs.length = 0; | ||
if(refChanged && member[pn]['=']) | ||
member[pn]['='] = true; | ||
}); | ||
}, [ 'definitions', tn ]); | ||
}) | ||
adaptRefs.forEach(fn => fn(true, 1)); | ||
adaptRefs.length = 0; | ||
} | ||
@@ -475,2 +487,3 @@ | ||
ref: (parent, prop, ref, path) => { | ||
let refChanged = false; | ||
const { links, art, scope } = inspectRef(path); | ||
@@ -488,3 +501,3 @@ const resolvedLinkTypes = resolveLinkTypes(links); | ||
// setProp(parent, '$structRef', parent.ref); | ||
parent.ref = flattenStructStepsInRef(ref, | ||
[ parent.ref, refChanged ] = flattenStructStepsInRef(ref, | ||
scopedPath, links, scope, resolvedLinkTypes, | ||
@@ -505,5 +518,6 @@ suspend, suspendPos, parent.$bparam); | ||
parent.as = lastRef; | ||
} | ||
} | ||
return refChanged; | ||
} | ||
return false; | ||
/** | ||
@@ -510,0 +524,0 @@ * Return true if the path points inside columns |
@@ -329,3 +329,3 @@ 'use strict'; | ||
* @param {bool} [revokeAtSuspendPos] revoke suspension after suspendPos (binding parameter path use case) | ||
* @returns {string[]} | ||
* @returns [string[], bool] | ||
*/ | ||
@@ -335,3 +335,3 @@ function flattenStructStepsInRef(ref, path, links, scope, resolvedLinkTypes=new WeakMap(), suspend=false, suspendPos=0, revokeAtSuspendPos=false) { | ||
if (ref.length < 2 || (scope === '$self' && ref.length === 2)) { | ||
return ref; | ||
return [ ref, false ]; | ||
} | ||
@@ -347,3 +347,3 @@ | ||
if (scope === '$magic') | ||
return ref; | ||
return [ ref, false ]; | ||
@@ -359,3 +359,4 @@ // Don't process a leading $self - it will a .art with .elements! | ||
let flattenStep = false; | ||
let refChanged = false | ||
let flattenStep = false; | ||
suspend = !!art('items') || (suspend && i <= suspendPos); | ||
@@ -372,2 +373,3 @@ for(; i < links.length; i++) { | ||
} | ||
refChanged = true; | ||
// suspend flattening if the next path step has some 'items' | ||
@@ -389,3 +391,3 @@ suspend = !!art('items'); | ||
} | ||
return result; | ||
return [ result, refChanged ]; | ||
} | ||
@@ -392,0 +394,0 @@ |
@@ -283,3 +283,4 @@ 'use strict'; | ||
forEachValue(params, (param) => { | ||
const propagateToParams = typeof param.type === 'string' ? csn.definitions[param.type]?.kind !== 'entity' : true; | ||
const propagateToParams = (param.type !== '$self' || csn.definitions.$self) && | ||
(typeof param.type !== 'string' || csn.definitions[param.type]?.kind !== 'entity'); | ||
propagateMemberPropsFromOrigin(param, { | ||
@@ -343,4 +344,4 @@ '@': !propagateToParams, items: true, elements: true, enum: true, virtual: true, | ||
// For a `type of` with .items, we want to take stuff from types (which we skip for "normal" propagation, see specialItemsRules). | ||
// So for a type of we also propagate stuff from the virtual origin (which we don't give a "kind", therefore skipping that part of the check) | ||
if (target.type && target.type.ref) | ||
// So for a `type of` we also propagate stuff from the virtual origin (which we don't give a "kind", therefore skipping that part of the check) | ||
if (target.type?.ref) | ||
copyProperties(virtualOrigin, target, getMemberPropagationRuleFor, except); | ||
@@ -347,0 +348,0 @@ |
{ | ||
"name": "@sap/cds-compiler", | ||
"version": "4.9.0", | ||
"version": "4.9.2", | ||
"description": "CDS (Core Data Services) compiler and backends", | ||
@@ -5,0 +5,0 @@ "homepage": "https://cap.cloud.sap/", |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
4925002
101116