Socket
Socket
Sign inDemoInstall

@sap/cds-compiler

Package Overview
Dependencies
1
Maintainers
1
Versions
99
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 4.6.2 to 4.7.4

lib/checks/dbFeatureFlags.js

8

bin/cds_update_identifiers.js

@@ -35,2 +35,5 @@ #!/usr/bin/env node

if (filepath === '--help' || filepath === '-h')
exitError();
if (cliArgs.length !== 1)

@@ -127,6 +130,7 @@ exitError(`Expected exactly one argument, ${cliArgs.length} given`);

/**
* @param {string} msg
* @param {string} [msg]
*/
function exitError( msg ) {
console.error(msg);
if (msg)
console.error(msg);
usage();

@@ -133,0 +137,0 @@ process.exit(1);

@@ -562,3 +562,3 @@ #!/usr/bin/env node

const csn = compactModel(xsn, options);
if (command === 'toCsn' && options.tenantAsColumn)
if (command === 'toCsn' && options.tenantDiscriminator)
addTenantFields(csn, options);

@@ -565,0 +565,0 @@ if (command === 'toCsn' && options.withLocalized)

@@ -11,2 +11,8 @@ # ChangeLog of Beta Features for cdx compiler and backends

## Version 4.x.x - 2024-mm-dd
### Added `v5preview`
Sneak preview into incompatible changes that are about to be shipped with compiler version 5.
## Version 4.6.0 - 2024-01-26

@@ -13,0 +19,0 @@

@@ -26,6 +26,7 @@ /** @module API */

const trace = lazyload('./trace');
const cloneCsn = lazyload('../model/cloneCsn');
const { forEach, forEachKey } = require('../utils/objectUtils');
const { checkRemovedDeprecatedFlags } = require('../base/model');
const { makeMessageFunction } = require('../base/messages');
const { sortCsnForTests } = require('../model/cloneCsn');

@@ -142,3 +143,4 @@ /**

internalOptions.transformation = 'odata';
const oDataCsn = forOdataNew.transform4odataWithCsn(csn, internalOptions, messageFunctions);
let oDataCsn = forOdataNew.transform4odataWithCsn(csn, internalOptions, messageFunctions);
oDataCsn = sortCsnForTests(oDataCsn, internalOptions);
messageFunctions.setModel(oDataCsn);

@@ -192,3 +194,3 @@ attachTransformerCharacteristics(oDataCsn, 'odata', internalOptions, relevantOdataOptions, warnAboutMismatchOdata);

);
return internalOptions.testMode ? toCsn.sortCsn(transformedCsn, internalOptions) : transformedCsn;
return cloneCsn.sortCsnForTests(transformedCsn, internalOptions);
}

@@ -208,3 +210,3 @@

const internalOptions = prepareOptions.to.sql(options);
return csnForSql(csn, internalOptions, messageFunctions);
return csnForSql(csn, internalOptions, messageFunctions); // already sorted for test mode
}

@@ -227,3 +229,3 @@

);
return internalOptions.testMode ? toCsn.sortCsn(transformedCsn, internalOptions) : transformedCsn;
return cloneCsn.sortCsnForTests(transformedCsn, internalOptions);
}

@@ -245,3 +247,3 @@ /**

);
return internalOptions.testMode ? toCsn.sortCsn(hanaCsn, internalOptions) : hanaCsn;
return cloneCsn.sortCsnForTests(hanaCsn, internalOptions);
}

@@ -261,5 +263,13 @@

internalOptions.transformation = 'effective';
if (options.tenantDiscriminator) {
messageFunctions.error('api-invalid-option', null, {
'#': 'forbidden',
option: 'tenantDiscriminator',
module: 'for.effective',
});
messageFunctions.throwWithAnyError();
}
const eCsn = effective.effectiveCsn(csn, internalOptions, messageFunctions);
return internalOptions.testMode ? toCsn.sortCsn(eCsn, internalOptions) : eCsn;
return cloneCsn.sortCsnForTests(eCsn, internalOptions);
}

@@ -280,2 +290,4 @@

handleTenantDiscriminator(options, internalOptions, messageFunctions);
// we need the CSN for view sorting

@@ -305,2 +317,4 @@ const transformedCsn = csnForSql(csn, internalOptions, messageFunctions);

handleTenantDiscriminator(options, internalOptions, messageFunctions);
// we need the CSN for view sorting

@@ -410,2 +424,3 @@ const sqlCSN = forHdi(csn, options, messageFunctions);

const internalOptions = prepareOptions.to.sql(options);
handleTenantDiscriminator(options, internalOptions, messageFunctions);
const { error, throwWithError } = messageFunctions;

@@ -563,2 +578,3 @@

const internalOptions = prepareOptions.to.hdi(options);
handleTenantDiscriminator(options, internalOptions, messageFunctions);

@@ -653,2 +669,11 @@ // Prepare after-image.

if (options.tenantDiscriminator) {
messageFunctions.error('api-invalid-option', null, {
'#': 'forbidden',
option: 'tenantDiscriminator',
module: 'to.hdbcds',
});
messageFunctions.throwWithAnyError();
}
const hanaCsn = forHdbcds(csn, internalOptions, messageFunctions);

@@ -853,3 +878,2 @@ messageFunctions.setModel(hanaCsn);

*/
// @ts-ignore
function odataall( csn, options = {}, messageFunctions ) {

@@ -1044,3 +1068,3 @@ trace.traceApi('to.odata.all', options);

if (options.deprecated)
checkRemovedDeprecatedFlags( options, messageFunctions );
baseModel.checkRemovedDeprecatedFlags( options, messageFunctions );

@@ -1212,3 +1236,26 @@ checkOutdatedOptions( options, messageFunctions );

/**
* Error when tenantDiscriminator and withHanaAssociations is set by the user.
*
* Set withHanaAssociations to false when tenantDiscriminator is used.
*
* @param {object} options Options set by the user
* @param {object} internalOptions Options clone after we processed it
* @param {object} messageFunctions Message functions
*/
function handleTenantDiscriminator( options, internalOptions, messageFunctions ) {
if (options.tenantDiscriminator && options.withHanaAssociations) {
messageFunctions.error('api-invalid-combination', null, {
option: 'tenantDiscriminator',
prop: 'withHanaAssociations',
});
messageFunctions.throwWithAnyError();
}
else if (internalOptions.tenantDiscriminator) {
internalOptions.withHanaAssociations = false;
}
}
/**

@@ -1215,0 +1262,0 @@ * Option format used by the old API, where they are grouped thematically.

'use strict';
const { validate, generateStringValidator } = require('./validate');
const { makeMessageFunction } = require('../base/messages');

@@ -70,3 +71,3 @@ // TODO: there should be just one place where the options are defined with

'disableHanaComments', // in case of issues with hana comment rendering
'tenantAsColumn', // not published yet
'tenantDiscriminator', // not published yet
'localizedWithoutCoalesce', // deprecated version of 'localizedLanguageFallback', TODO(v5): Remove option

@@ -108,4 +109,5 @@ ];

// only "new-style" options are here
const messageFunctions = makeMessageFunction(null, options, moduleName);
validate(options,
moduleName,
messageFunctions,
// TODO: is there a better place to specify the type of option values?

@@ -116,3 +118,4 @@ Object.assign( {

}, customValidators ),
combinationValidators);
combinationValidators,
moduleName);

@@ -119,0 +122,0 @@ // Overwrite with the hardRequire options - like src: sql in to.sql()

'use strict';
const { makeMessageFunction } = require('../base/messages');
const { forEach } = require('../utils/objectUtils');

@@ -131,2 +130,3 @@

assertIntegrityType: generateStringValidator([ 'DB', 'RT' ]),
tenantDiscriminator: { validate: () => true }, // do it ourselves
};

@@ -156,3 +156,2 @@

/* eslint-disable jsdoc/no-undefined-types */
/**

@@ -163,3 +162,3 @@ * Run the validations for each option.

* @param {object} options Flat options object to validate
* @param {string} moduleName The called module, e.g. 'for.odata', 'to.hdi'. Needed to initialize the message functions
* @param {object} messageFunctions Message functions such as `error()`, `info()`, …
* @param {object} [customValidators] Map of custom validators to use

@@ -170,31 +169,23 @@ * @param {string[]} [combinationValidators] Validate option combinations

*/
function validate( options, moduleName, customValidators = {}, combinationValidators = [] ) {
// TODO: issuing messages in this function looks very strange...
{
const messageCollector = { messages: [] };
const { error, throwWithAnyError } = makeMessageFunction(null, messageCollector, moduleName);
function validate( options, messageFunctions, customValidators = {}, combinationValidators = [] ) {
const { error, throwWithError } = messageFunctions;
forEach(options, (optionName, optionValue) => {
const validator = customValidators[optionName] || validators[optionName] || booleanValidator;
forEach(options, (optionName, optionValue) => {
const validator = customValidators[optionName] || validators[optionName] || booleanValidator;
if (!validator.validate(optionValue)) {
error('api-invalid-option', null, {
'#': 'value',
prop: optionName,
value: validator.expected(optionValue),
othervalue: validator.found(optionValue),
});
}
});
throwWithAnyError();
}
if (!validator.validate(optionValue)) {
error('api-invalid-option', null, {
'#': 'value',
prop: optionName,
value: validator.expected(optionValue),
othervalue: validator.found(optionValue),
});
}
});
throwWithError();
const message = makeMessageFunction(null, options, moduleName);
for (const combinationValidatorName of combinationValidators.concat(alwaysRunValidators))
allCombinationValidators[combinationValidatorName](options, message);
message.throwWithAnyError();
allCombinationValidators[combinationValidatorName](options, messageFunctions);
throwWithError();
}
/* eslint-enable jsdoc/no-undefined-types */

@@ -201,0 +192,0 @@

@@ -30,2 +30,3 @@ // module- and csn/XSN-independent definitions

annotationExpressions: true,
odataPathsInAnnotationExpressions: true,
odataAnnotationExpressions: true,

@@ -43,2 +44,3 @@ assocsWithParams: true, // beta, because runtimes don't support it, yet.

vectorType: true,
v5preview: true,
// disabled by --beta-mode

@@ -45,0 +47,0 @@ nestedServices: false,

@@ -112,4 +112,5 @@ 'use strict';

/**
* Checks whether managed associations
* with cardinality 'to many' have an on-condition.
* Checks whether managed associations with cardinality 'to many' have no ON-condition.
* If there isn't, and if _all_ key elements on the target side are covered by the foreign key,
* then the association is effectively to-one -> warning.
*

@@ -121,21 +122,88 @@ * @param {CSN.Artifact} member The member (e.g. element artifact)

return;
// Implementation note: Imported services (i.e. external ones) may contain to-many associations
// with an empty foreign key list. If the user (in this case importer) explicitly sets an empty
// foreign key array, we won't emit a warning to avoid spamming the user.
const max = member.cardinality?.max ? member.cardinality.max : 1;
if (max !== 1 && !member.on && (!member.keys || member.keys.length > 0)) {
const isNoDb = this.artifact['@cds.persistence.skip'] || this.artifact['@cds.persistence.exists'];
this.warning(isNoDb ? 'to-many-no-on-noDB' : 'to-many-no-on', member.cardinality ? member.cardinality.$path : member.$path, {
value: cardinality2str(member, false),
'#': this.csnUtils.isComposition(member) ? 'comp' : 'std',
}, {
std: 'Expected association with target cardinality $(VALUE) to have an ON-condition',
comp: 'Expected composition with target cardinality $(VALUE) to have an ON-condition',
});
const max = member.cardinality?.max ?? 1;
if (max === 1 || member.on || !member.keys || member.keys.length === 0)
return;
// We use the fact that `key` is only supported top-level (warning otherwise).
// And if an element of a structured _type_ is "key", we get a warning for the key.
// However, we may get false negatives for our warning, which is acceptable, e.g. for
// `type T : { key i: String; }; entity A { id : T; };` with `… to many A { id };`
const target = typeof member.target === 'object' ? member.target : this.csnUtils.getCsnDef(member.target);
const targetKeys = Object.entries(target.elements || {}).filter(elem => !!elem[1].key);
const foreignKeys = structurizeForeignKeys(member.keys);
if (!coversAllTargetKeys.call(this, foreignKeys, targetKeys))
return; // foreign key does not cover at least one target key -> can be to-many
const isNoDb = this.artifact['@cds.persistence.skip'] || this.artifact['@cds.persistence.exists'];
this.warning(isNoDb ? 'to-many-no-on-noDB' : 'to-many-no-on', member.cardinality?.$path || member.$path, {
value: cardinality2str(member, false),
'#': this.csnUtils.isComposition(member) ? 'comp' : 'std',
}, {
std: 'Expected association with target cardinality $(VALUE) to have an ON-condition',
comp: 'Expected composition with target cardinality $(VALUE) to have an ON-condition',
});
}
/**
* Returns true if the foreign keys cover _all_ of the target keys.
* Returns false otherwise.
*
* @see checkManagedAssoc()
*
* @param {object} foreignKeys Structure returned by `structurizeForeignKeys()`.
* @param {Array} targetKeys Object.entries() value of target keys.
* @returns {boolean} Whether all target keys are covered
*/
function coversAllTargetKeys( foreignKeys, targetKeys ) {
if (foreignKeys.length < targetKeys.length || targetKeys.length === 0) {
// there are fewer foreign keys than keys on the target side
// or there are no keys on the target side, in which case there is no
// possibility to cover all keys.
return false;
}
for (const [ targetKeyName, targetKey ] of targetKeys) {
const foreignKey = foreignKeys.entries[targetKeyName];
if (!foreignKey)
return false; // foreign key does not cover this target key
if (foreignKey.length > 0) { // foreign key only selects sub-structures, not whole structured key
const elements = targetKey.elements || this.csnUtils.getFinalTypeInfo(targetKey.type)?.elements;
if (!elements)
return false; // model error (e.g. 'many type')
if (!coversAllTargetKeys( foreignKey, Object.entries(elements) ))
return false;
}
}
return true;
}
/**
* Structurizes a foreign key into an object that can be used to compare foreign
* keys against their corresponding keys on target side.
*
* For `Association to T { a.b, a.c, b }` this structure will be returned:
* `{ length: 2, entries: { a: { length: 2, entries: { b: { length: 0, … }, …} } }}`
*
* @param {object[]} keys Foreign key array.
* @returns {object} Structured foreign key. Custom format, i.e. not via `elements`.
*/
function structurizeForeignKeys( keys ) {
const map = { entries: Object.create(null), length: 0 };
for (const key of keys) {
let entry = map;
for (const step of key.ref) {
if (!entry.entries[step]) {
entry.entries[step] = { entries: Object.create(null), length: 0 };
++entry.length;
}
entry = entry.entries[step];
}
}
return map;
}
/**
*
* @param {CSN.Element} member The member

@@ -142,0 +210,0 @@ * @returns {boolean} Whether the member is managed composition

@@ -88,2 +88,3 @@ // This is very similar to lib/model/enrichCsn - but the goal and the execution differ a bit:

/**

@@ -98,3 +99,3 @@ * Transformer for things that are annotations. When we have a "=" plus an expression of some sorts,

function annotation( _parent, _prop, node ) {
if (options.processAnnotations) {
if (options.enrichAnnotations) {
if (node?.['='] !== undefined && xprInAnnoProperties.some(xProp => node[xProp] !== undefined)) {

@@ -217,3 +218,3 @@ standard(_parent, _prop, node);

* @typedef {object} enrichCsnOptions
* @property {boolean} [processAnnotations=false] Wether to process annotations and call custom transformers on them
* @property {boolean} [enrichAnnotations=false] Wether to process annotations and call custom transformers on them
*/

@@ -8,3 +8,3 @@ 'use strict';

} = require('../model/csnUtils');
const enrich = require('./enricher');
const enrichCsn = require('./enricher');

@@ -52,2 +52,3 @@ // forRelationalDB

} = require('./sql-snippets');
const dbFeatureFlags = require('./dbFeatureFlags');

@@ -70,4 +71,3 @@ const forRelationalDBMemberValidators

const forRelationalDBArtifactValidators
= [
const forRelationalDBArtifactValidators = [
checkPrimaryKey,

@@ -80,2 +80,4 @@ // @cds.persistence has no impact on odata

checkSqlAnnotationOnArtifact,
// strip down CSN to reduce it's size by removing non-sql relevant parts
stripNonDbRelevant,
];

@@ -89,2 +91,3 @@

checkPathsInStoredCalcElement,
dbFeatureFlags,
];

@@ -157,3 +160,5 @@ /**

iterateOptions = {} ) {
const { cleanup } = enrich(csn, { processAnnotations: that.options.tranformation === 'odata' });
const { cleanup } = enrichCsn(csn, that.options);
// TODO: Don't know if that's feasible? Do we really need to enrich annotations always?
// const { cleanup } = enrich(csn, { processAnnotations: that.options.tranformation === 'odata' });

@@ -286,2 +291,31 @@ applyTransformations(csn, mergeCsnValidators(csnValidators, that), [], { drillRef: true });

const dbRelevantKinds = {
entity: true,
type: true,
aspect: true,
service: true,
context: true,
};
/**
* Shrink the CSN by
* - deleting bound actions
* - turning all non-entity/type/aspect/service/context entities into dummies
*
* @param {CSN.Artifact} artifact
* @param {string} artifactName
*/
function stripNonDbRelevant( artifact, artifactName ) {
if (this.options.transformation !== 'effective') {
if ( !dbRelevantKinds[artifact.kind] ) {
this.csn.definitions[artifactName] = {
kind: 'action',
};
}
else if (artifact.actions) {
delete artifact.actions;
}
}
}
module.exports = { forRelationalDB, forOdata };

@@ -367,3 +367,3 @@ // Consistency checker on model (XSN = augmented CSN)

'scope', '_artifact', '$inferred', '$expand', '$inCycle', '$tableAliases', '_$next',
'_origin', '_effectiveType', '$effectiveSeqNo', '_extensions',
'_origin', '_effectiveType', '$effectiveSeqNo', '_extensions', '$contains',
],

@@ -467,3 +467,3 @@ },

},
query: { requires: [ 'query', 'location' ], optional: [ 'stored' ] },
query: { requires: [ 'query', 'location' ], optional: [ 'stored', '$parens' ] },
},

@@ -486,3 +486,3 @@ literal: { // TODO: check value against literal

'$tokenTexts', 'op', 'args', 'func', '_artifact', 'type', '$typeArgs',
'scale', 'srid', 'length', 'precision',
'scale', 'srid', 'length', 'precision', 'scope',
],

@@ -496,3 +496,3 @@ // TODO: restrict path to #simplePath

optional: [
'name', '$duplicate', '$expected', 'args', 'suffix',
'name', '$duplicate', '$expected', 'args', 'suffix', '$parens',
'param', 'scope', // for dynamic parameter '?'

@@ -499,0 +499,0 @@ ],

@@ -594,6 +594,7 @@ // Checks on XSN performed during compile() that are useful for the user

* @param {XSN.Artifact} user User for semantic location
* @returns {void}
* @param {string} [context] where the expression is used, e.g. 'anno'
*/
function checkGenericExpression( xpr, user ) {
checkExpressionNotVirtual( xpr, user );
function checkGenericExpression( xpr, user, context ) {
if (context !== 'anno')
checkExpressionNotVirtual( xpr, user );
checkExpressionAssociationUsage( xpr, user, false );

@@ -907,3 +908,3 @@ if (xpr.op?.val === 'cast') {

if (anno.$tokenTexts) {
checkGenericExpression( anno, art );
checkGenericExpression( anno, art, 'anno' );
}

@@ -910,0 +911,0 @@ else if (anno.literal === 'array') {

@@ -949,4 +949,4 @@ // Compiler phase 1 = "define": transform dictionary of AST-like XSNs into XSN

// TODO: the SQL backend should probably delete `excluding` when expanding `*`
// TODO: use `parent` for semantic location, use `-excluding`
warning( 'query-ignoring-exclude', [ parent.excludingDict[$location], user ],
// TODO: use `parent` for semantic location; requires `_parent`/... links.
warning( 'query-ignoring-excluding', [ parent.excludingDict[$location], user ],
{ prop: '*' },

@@ -953,0 +953,0 @@ 'Excluding elements without wildcard $(PROP) has no effect' );

@@ -785,3 +785,3 @@ // Generate: localized data and managed compositions

/**
* Copy the annotations `@cds.persistence.skip`/`@cds.persistence.exists` from
* Copy relevant annotations from
* source to target if present on source but not target.

@@ -800,2 +800,3 @@ *

copy( '@cds.persistence.skip' );
copy( '@cds.tenant.independent' );

@@ -802,0 +803,0 @@ function copy( anno ) {

@@ -30,7 +30,8 @@ // Propagate properties in XSN

'@com.sap.gtt.core.CoreModel.Indexable': never,
'@cds.persistence.exists': never,
'@cds.persistence.exists': never, // also copied in generate.js
'@cds.persistence.table': never,
'@cds.persistence.calcview': never,
'@cds.persistence.udf': never,
'@cds.persistence.skip': notWithPersistenceTable,
'@cds.persistence.skip': notWithPersistenceTable, // also copied in generate.js
// '@cds.tenant.independent' is propagated as normal, but also copied in generate.js
'@sql.append': never,

@@ -110,11 +111,2 @@ '@sql.prepend': never,

news.push( target.value._artifact );
if (target.value?._artifact.$inferred !== 'include') {
// If the referred to element is not inferred, it is a new one and not the original.
// The new one was not originally referred to => error;
// TODO: no messages in propagator.js
warning( 'ref-unexpected-override', [ target.name.location, target ],
{ id: target.name.id, target: target.value?._artifact },
'Calculated element $(ID) does not originally refer to $(TARGET)' );
}
}

@@ -121,0 +113,0 @@ chain.push( { target, source: origin } );

@@ -163,8 +163,19 @@ // Tweak associations: rewrite keys and on conditions

return traverseExpr( expr, 'annoRewrite', user, checkAnnotationRef );
if (expr.literal === 'array')
return expr.val.find( val => checkAnnotationForRefs( val, user ) );
if (expr.literal === 'array') {
for (const val of expr.val) {
const found = checkAnnotationForRefs( val, user );
if (found)
return found;
}
return null;
}
if (expr.literal !== 'struct')
return null;
const struct = Object.values( expr.struct );
return struct.find( val => checkAnnotationForRefs( val, user ) );
for (const val of struct) {
const found = checkAnnotationForRefs( val, user );
if (found)
return found;
}
return null;
}

@@ -312,2 +323,4 @@

forEachGeneric( elem, 'elements', rewriteAssociation );
if (elem.targetAspect?.elements)
forEachGeneric( elem.targetAspect, 'elements', rewriteAssociation );
if (!originTarget( elem ))

@@ -456,7 +469,3 @@ return;

// filter was copied in original element already
// TODO: not correct for e.g. composition-of-inline-aspect (#12223)
if (elem.$inferred !== 'include')
addConditionFromAssocPublishing( elem, assoc, nav );
addConditionFromAssocPublishing( elem, assoc, nav );
elem.on.$inferred = 'rewrite';

@@ -474,2 +483,6 @@ }

function addConditionFromAssocPublishing( elem, assoc, nav ) {
if (elem.$inferred || elem._main?.$inferred === 'composition-entity') {
// filter was copied in original element already
return;
}
const publishAssoc = (elem._main?.query || elem.$syntax === 'calc') &&

@@ -508,6 +521,6 @@ elem.value?.path?.length > 0;

elem.on = {
op: { val: 'and', location },
op: { val: 'ixpr', location },
args: [
// TODO: Get rid of $parens
{ ...elem.on, $parens: [ assoc.location ] },
{ val: 'and', literal: 'token', location },
filterToCondition( lastStep, elem, nav ),

@@ -534,3 +547,2 @@ ],

const cond = copyExpr( assocPathStep.where );
// TODO: Get rid of $parens
cond.$parens = [ assocPathStep.location ];

@@ -593,3 +605,3 @@ traverseExpr( cond, 'rewrite-filter', elem, (expr) => {

{ id: assoc.name.id, location: elem.name.location },
...copyExpr( fKey.targetElement.path ),
...copyExpr( fKey.targetElement.path, weakLocation( elem.name.location ) ),
],

@@ -607,3 +619,3 @@ location: elem.name.location,

{ id: assoc.name.id, location: elem.name.location },
...copyExpr( fKey.targetElement.path ),
...copyExpr( fKey.targetElement.path, weakLocation( elem.name.location ) ),
],

@@ -773,3 +785,3 @@ location: elem.name.location,

else if (i) {
state = rewriteItem( state, i, i.id, assoc );
state = rewriteItem( state, i, i.id, assoc, false );
if (!state || state === true)

@@ -813,5 +825,6 @@ break;

// and have one message for all
error( 'rewrite-undefined-key', [ weakLocation( culprit.location ), assoc ],
{ id: item.id, art: alias._main },
'Foreign key $(ID) has not been found in target $(ART)' );
error( 'rewrite-undefined-key', [ weakLocation( culprit.location ), assoc ], {
'#': 'std', id: item.id, target: alias._main, name: assoc.name.id,
});
// ''
return null;

@@ -826,9 +839,15 @@ }

env = env.target._artifact?._effectiveType;
elem = setArtifactLink( item, env?.elements?.[name] );
const found = setArtifactLink( item, env?.elements?.[name] );
if (found)
return found;
if (elem)
return elem;
// TODO: better (extra message), TODO: do it
error( 'query-undefined-element', [ item.location, assoc ],
{ id: name || item.id, '#': 'redirected' } );
const isExplicit = elem.target && !elem.target.$inferred;
const loc = isExplicit ? elem.target.location : item.location;
error( 'query-undefined-element', [ loc, assoc ], {
'#': isExplicit ? 'redirected' : 'std',
id: name || item.id,
name: elem.name.id,
target: elem.target._artifact,
keyword: 'redirected to',
} );
return null;

@@ -835,0 +854,0 @@ }

@@ -312,2 +312,5 @@ 'use strict';

}
const [ head, ...tail ] = xpr;
if ((head.id || head) === '$self')
xpr = tail;
parentparent[parentprop] = { $Path: xpr.map(ps => ps.id || ps).join('/') };

@@ -314,0 +317,0 @@ };

'use strict';
const { makeMessageFunction } = require('../../base/messages.js');
const { forEachDefinition, forEachGeneric } = require('../../model/csnUtils.js');

@@ -17,4 +16,4 @@

*/
function preprocessAnnotations( csn, serviceName, options ) {
const { message } = makeMessageFunction(csn, options);
function preprocessAnnotations( csn, serviceName, options, messageFunctions ) {
const { message } = messageFunctions;
const fkSeparator = '_';

@@ -241,16 +240,14 @@

// the (single) non-key string field, if there is one
let textField = null;
let stringFields = [];
const Identification = vlEntity['@UI.Identification'];
if (Identification && Identification[0] && Identification[0]['=']) {
textField = Identification[0]['='];
stringFields.push(Identification[0]['=']);
}
else if (Identification && Identification[0] && Identification[0].Value && Identification[0].Value['=']) {
textField = Identification[0].Value['='];
stringFields.push(Identification[0].Value['=']);
}
else {
const stringFields = Object.keys(vlEntity.elements).filter(
stringFields = Object.keys(vlEntity.elements).filter(
x => !vlEntity.elements[x].key && vlEntity.elements[x].type === 'cds.String'
);
if (stringFields.length === 1)
textField = stringFields[0];
}

@@ -266,8 +263,8 @@

} ];
if (textField) {
parameters[1] = {
stringFields.forEach((n) => {
parameters.push({
$Type: 'Common.ValueListParameterDisplayOnly',
ValueListProperty: textField,
};
}
ValueListProperty: n,
});
});
}

@@ -274,0 +271,0 @@

@@ -14,3 +14,3 @@ 'use strict';

const {
cloneCsnNonDict, isEdmPropertyRendered, isBuiltinType, getUtils,
isEdmPropertyRendered, isBuiltinType, getUtils,
} = require('../model/csnUtils');

@@ -24,3 +24,4 @@ const { checkCSNVersion } = require('../json/csnVersion');

const { getEdm } = require('./edm.js');
const { cloneFullCsn } = require('../model/cloneCsn');
const { forEach, forEachValue } = require('../utils/objectUtils.js');
/*

@@ -51,3 +52,3 @@ OData V2 spec 06/01/2017 PDF version is available here:

// get us a fresh model copy that we can work with
const csn = cloneCsnNonDict(_csn, _options);
const csn = cloneFullCsn(_csn, _options);
const special$self = !csn?.definitions?.$self && '$self';

@@ -91,2 +92,4 @@ messageFunctions.setModel(csn);

const reqDefsUtils = getUtils(reqDefs);
if (serviceNames === undefined)

@@ -205,3 +208,3 @@ serviceNames = options.serviceNames;

// Add additional schema containers as sub contexts to the service
Object.entries(allSchemas).forEach(([ fqName, art ]) => {
forEach(allSchemas, (fqName, art) => {
if (serviceCsn.name === whatsMyServiceRootName(fqName) &&

@@ -258,3 +261,3 @@ fqName.startsWith(`${serviceCsn.name}.`)) {

if (c._ec) {
Object.entries(c._ec._registry).forEach((([ setName, arr ]) => {
forEach(c._ec._registry, ( setName, arr ) => {
if (arr.length > 1) {

@@ -267,3 +270,3 @@ error(null, null, {

}
}));
});
}

@@ -276,3 +279,3 @@ });

// for the type cross check
Object.values(xServiceRefs).forEach((ref) => {
forEachValue(xServiceRefs, (ref) => {
const r = new Edm.Reference(v, ref.ref);

@@ -316,3 +319,3 @@ r.append(new Edm.Include(v, ref.inc));

function populateSchemas( schemas ) {
Object.entries(reqDefs.definitions).forEach(([ fqName, art ]) => {
forEach(reqDefs.definitions, ( fqName, art ) => {
// Identify service members by their definition name only, this allows

@@ -424,3 +427,3 @@ // to let the internal object.name have the sub-schema name.

edmUtils.foreach(schemaCsn.definitions,
a => edmUtils.isStructuredType(a) && a.name.startsWith(schemaNamePrefix),
a => edmUtils.isStructuredType(a) && a.name.startsWith(schemaNamePrefix) && !a.$ignoreInAPI,
[ createComplexType, markRendered ]);

@@ -483,3 +486,3 @@

Object.entries(NamesInSchemaXRef).forEach(([ name, refs ]) => {
forEach(NamesInSchemaXRef, ( name, refs ) => {
if (refs.length > 1) {

@@ -499,3 +502,3 @@ error(null, [ 'definitions', `${Schema._edmAttributes.Namespace}.${name}` ], { name: Schema._edmAttributes.Namespace },

const location = [ 'definitions', entityCsn.name ];
const location = reqDefs.definitions[entityCsn.name] ? [ 'definitions', entityCsn.name ] : entityCsn.$path;
const type = `${schema.name}.${EntityTypeName}`;

@@ -519,3 +522,3 @@ if (properties.length === 0)

if (options.isV2() && p._isCollection && !p._csn.target)
if (options.isV2() && p.$isCollection && !p._csn.target)
message('odata-spec-violation-array', pLoc, { version: '2.0' });

@@ -570,3 +573,3 @@

if (entityCsn.actions) {
Object.entries(entityCsn.actions).forEach(([ n, a ]) => {
forEach(entityCsn.actions, ( n, a ) => {
if (options.isV4())

@@ -602,3 +605,3 @@ createActionV4(a, n, entityCsn);

if (options.isV2()) {
if (p._isCollection && !p._csn.target)
if (p.$isCollection && !p._csn.target)
message('odata-spec-violation-array', pLoc, { version: '2.0' });

@@ -630,3 +633,3 @@

if (elementsCsn.elements) {
Object.entries(elementsCsn.elements).forEach(([ elementName, elementCsn ]) => {
forEach(elementsCsn.elements, ( elementName, elementCsn ) => {
if (!elementCsn._edmParentCsn)

@@ -702,6 +705,6 @@ setProp(elementCsn, '_edmParentCsn', edmParentCsn);

if (!edmUtils.isODataSimpleIdentifier(attributes.Name))
message('odata-spec-violation-id', [ 'definitions', typeCsn.name ], { id: attributes.Name });
message('odata-spec-violation-id', typeCsn.$path, { id: attributes.Name });
const typeDef = new Edm.TypeDefinition(v, attributes, typeCsn );
edmTypeCompatibilityCheck(typeDef, [ 'definitions', typeCsn.name ]);
edmTypeCompatibilityCheck(typeDef, typeCsn.$path);
Schema.append(typeDef);

@@ -720,2 +723,3 @@ }

if (!edmUtils.isODataSimpleIdentifier(attributes.Name))

@@ -800,3 +804,3 @@ message('odata-spec-violation-id', location, { id: attributes.Name });

if (actionCsn.params) {
Object.entries(actionCsn.params).forEach(([ parameterName, parameterCsn ]) => {
forEach(actionCsn.params, ( parameterName, parameterCsn ) => {
const p = new Edm.Parameter(v, { Name: parameterName }, parameterCsn );

@@ -879,3 +883,3 @@ const pLoc = [ ...location, 'params', p._edmAttributes.Name ];

// is this still required?
Object.entries(actionCsn).forEach(([ key, val ]) => {
forEach(actionCsn, ( key, val ) => {
if (key.match(/^@sap\./))

@@ -911,3 +915,3 @@ functionImport.setXml( { [`sap:${key.slice(5).replace(/\./g, '-')}`]: val });

if (param._isCollection)
if (param.$isCollection)
message('odata-spec-violation-array', pLoc, { version: '2.0' });

@@ -948,3 +952,3 @@

}
if (action.returns._isCollection)
if (action.returns.$isCollection)
type = `Collection(${type})`;

@@ -1063,3 +1067,3 @@ }

// Add ReferentialConstraints if any
if (!navigationProperty._isCollection && Object.keys(constraints.constraints).length > 0) {
if (!navigationProperty.$isCollection && Object.keys(constraints.constraints).length > 0) {
// A managed composition is treated as association

@@ -1094,4 +1098,3 @@ if (navigationProperty._csn.type === 'cds.Composition' && navigationProperty._csn.on) {

function addAnnotations2XServiceRefs( ) {
const { getFinalTypeInfo } = getUtils(csn);
options.getFinalTypeInfo = getFinalTypeInfo;
options.getFinalTypeInfo = reqDefsUtils.getFinalTypeInfo;
const { annos, usedVocabularies, xrefs } = translate.csn2annotationEdm(reqDefs, csn.vocabularies, serviceCsn.name, Edm, options, messageFunctions, mergedVocabularies);

@@ -1098,0 +1101,0 @@ // distribute edm:Annotations into the schemas

@@ -667,3 +667,3 @@ 'use strict';

// Set the collection property if this is either an element, parameter or a term def
this._isCollection = (csn.kind === undefined || csn.kind === 'annotation') ? csn._isCollection : false;
this.$isCollection = (csn.kind === undefined || csn.kind === 'annotation') ? csn.$isCollection : false;

@@ -679,3 +679,3 @@ if (options.whatsMySchemaName && this._edmAttributes[typeName]) {

// decorate for XML (not for Complex/EntityType)
if (this._isCollection && this._edmAttributes[typeName])
if (this.$isCollection && this._edmAttributes[typeName])
this._edmAttributes[typeName] = `Collection(${this._edmAttributes[typeName]})`;

@@ -699,4 +699,4 @@ }

if (this._isCollection)
json.$Collection = this._isCollection;
if (this.$isCollection)
json.$Collection = this.$isCollection;

@@ -854,3 +854,3 @@ return json;

// From the Spec: In OData 4.01 responses a collection-valued property MUST specify a value for the Nullable attribute.
if (this._isCollection)
if (this.$isCollection)
this._edmAttributes.Nullable = !this.isNotNullable();

@@ -1007,3 +1007,3 @@

this._type = attributes.Type;
this._isCollection = this.isToMany();
this.$isCollection = this.isToMany();
this._targetCsn = csn._target;

@@ -1016,3 +1016,3 @@

// either csn has multiplicity or we have to use the multiplicity of the backlink
if (this._isCollection) {
if (this.$isCollection) {
this._edmAttributes.Type = `Collection(${attributes.Type})`;

@@ -1091,3 +1091,3 @@ // attribute Nullable is not allowed in combination with Collection (see Spec)

isToMany() {
return (this._isCollection || this._csn._constraints._multiplicity[1] === '*');
return (this.$isCollection || this._csn._constraints._multiplicity[1] === '*');
}

@@ -1094,0 +1094,0 @@

'use strict';
const { setProp } = require('../base/model');
const { setProp, isBetaEnabled } = require('../base/model');
const {
isBuiltinType, isEdmPropertyRendered, applyTransformations, cloneAnnotationValue,
isBuiltinType, isEdmPropertyRendered, applyTransformations,
} = require('../model/csnUtils');
const { escapeString, hasControlCharacters, hasUnpairedUnicodeSurrogate } = require('../render/utils/stringEscapes');
const { CompilerAssertion } = require('../base/error');
const { cloneAnnotationValue } = require('../model/cloneCsn');

@@ -34,2 +35,15 @@ /* eslint max-statements-per-line:off */

if (isBetaEnabled(options, 'v5preview')) {
if (!options.severities)
options.severities = {};
options.severities['odata-spec-violation-array'] ??= 'Error';
options.severities['odata-spec-violation-assoc'] ??= 'Error';
options.severities['odata-spec-violation-namespace'] ??= 'Error';
options.severities['odata-spec-violation-param'] ??= 'Error';
options.severities['odata-spec-violation-returns'] ??= 'Error';
options.severities['odata-spec-violation-type-unknown'] ??= 'Error';
options.severities['odata-spec-violation-no-key'] ??= 'Error';
options.severities['odata-spec-violation-key-type'] ??= 'Error';
options.severities['odata-spec-violation-property-name'] ??= 'Error';
}
return options;

@@ -579,2 +593,6 @@ }

node.setEdmAttribute('MaxLength', csn.length);
if (csn.precision != null)
node.setEdmAttribute('Precision', csn.precision);
// else if (csn.type === 'cds.hana.SMALLDECIMAL' && !isV2)
// node.Precision = 16;
if (csn.scale !== undefined)

@@ -584,7 +602,2 @@ node.setEdmAttribute('Scale', csn.scale);

// node._edmAttributes.Scale = 'floating';
if (csn.precision != null)
node.setEdmAttribute('Precision', csn.precision);
// else if (csn.type === 'cds.hana.SMALLDECIMAL' && !isV2)
// node.Precision = 16;
else if (csn.type === 'cds.Timestamp' && node._edmAttributes.Type === 'Edm.DateTimeOffset')

@@ -591,0 +604,0 @@ node.setEdmAttribute('Precision', 7);

@@ -17,3 +17,3 @@ // Transform XSN (augmented CSN) into CSN

const { pathName } = require('../compiler/utils');
const { CompilerAssertion, ModelError } = require('../base/error');
const { CompilerAssertion } = require('../base/error');

@@ -216,3 +216,3 @@ const compilerVersion = require('../../package.json').version;

const csnDictionaries = [
'args', 'params', 'enum', 'mixin', 'elements', 'actions', 'definitions',
'args', 'params', 'enum', 'mixin', 'elements', 'actions', 'definitions', 'vocabularies',
];

@@ -222,108 +222,2 @@ const csnDirectValues = [ 'val' ]; // + all starting with '@' - TODO: still relevant

/**
* Sort property names of CSN according to sequence which is also used by the compactModel function
* Only returns enumerable properties, except for certain hidden properties
* if requested (cloneOptions != false): $location, elements.
*
* If cloneOptions is false or if either cloneOptions.testMode or cloneOptions.testSortCsn
* are set, definitions are also sorted.
*
* @param {object} csn
* @param {CSN.Options|false} cloneOptions
*/
function sortCsn( csn, cloneOptions = false ) {
if (cloneOptions && typeof cloneOptions === 'object')
initModuleVars( cloneOptions );
if (Array.isArray(csn))
return csn.map( v => (!v || typeof v !== 'object' ? v : sortCsn(v, cloneOptions) ) );
const r = {};
for (const n of Object.keys(csn).sort( compareProperties ) ) {
const sortDict = n === 'definitions' &&
(!cloneOptions || cloneOptions.testMode || cloneOptions.testSortCsn);
const val = csn[n];
if (!val || typeof val !== 'object' || csnDirectValues.includes(n))
r[n] = val;
else if (n.charAt(0) === '@')
r[n] = cloneAnnotationValue( val );
else if (csnDictionaries.includes(n) && !Array.isArray(val))
// Array check for property `args` which may either be a dictionary or an array.
r[n] = csnDictionary( val, sortDict, cloneOptions );
else
r[n] = sortCsn(val, cloneOptions);
}
if (cloneOptions && typeof csn === 'object') {
if ({}.hasOwnProperty.call( csn, '$sources' ) && !r.$sources)
setHidden( r, '$sources', csn.$sources );
if ({}.hasOwnProperty.call( csn, '$location' ) && !r.$location)
setHidden( r, '$location', csn.$location );
if ({}.hasOwnProperty.call( csn, '$path' )) // used in generic reference flattener
setHidden( r, '$path', csn.$path );
if ({}.hasOwnProperty.call( csn, '$paths' )) // used in generic reference flattener
setHidden( r, '$paths', csn.$paths );
if (hasNonEnumerable( csn, 'elements' ) && !r.elements) // non-enumerable 'elements'
setHidden( r, 'elements', csnDictionary( csn.elements, false, cloneOptions ) );
if (hasNonEnumerable( csn, '$tableConstraints' ) && !r.$tableConstraints)
setHidden( r, '$tableConstraints', csn.$tableConstraints );
if (cloneOptions.hiddenPropertiesToClone) {
cloneOptions.hiddenPropertiesToClone.forEach((property) => {
if ({}.hasOwnProperty.call( csn, property )) // used in generic reference flattener
setHidden( r, property, csn[property] );
});
}
}
return r;
}
function cloneAnnotationValue( val ) {
if (typeof val !== 'object') // scalar
return val;
return JSON.parse( JSON.stringify( val ) );
}
/**
* Check whether the given object has non enumerable property.
* Ensure that we don't take it from the prototype, only "directly" - we accidentally
* cloned elements with a cds.linked input otherwise.
*
* @param {object} object
* @param {string} property
* @returns
*/
function hasNonEnumerable( object, property ) {
return {}.hasOwnProperty.call( object, property ) &&
!{}.propertyIsEnumerable.call( object, property );
}
/**
* @param {object} csn
* @param {boolean} sort
* @param {CSN.Options | false} cloneOptions If != false,
* cloneOptions.dictionaryPrototype is used and cloneOptions are
* passed to sort().
* @returns {object}
*/
function csnDictionary( csn, sort, cloneOptions = false ) {
if (!csn || Array.isArray(csn)) // null or strange CSN
return csn;
const proto = cloneOptions && (typeof cloneOptions === 'object') &&
cloneOptions.dictionaryPrototype;
// eslint-disable-next-line no-nested-ternary
const dictProto = (typeof proto === 'object') // including null
? proto
: (proto) ? Object.prototype : null;
const r = Object.create( dictProto );
for (const n of (sort) ? Object.keys(csn).sort() : Object.keys(csn)) {
// CSN does not allow any dictionary that are not objects.
// The compiler handles it, but a pre-transformed OData CSN won't trigger recompilation.
if (csn[n] && typeof csn[n] === 'object')
r[n] = sortCsn(csn[n], cloneOptions);
else
throw new ModelError(`Found non-object dictionary entry: "${ n }" of type "${ typeof csn[n] }"`);
}
return r;
}
/**
* Compact the given XSN model and transform it into CSN.

@@ -1326,3 +1220,3 @@ *

if (node.query)
return query( node.query, null, null, null, 1 );
return query( node.query, null, null, null, (node.$parens ? 1 - node.$parens.length : 1) );
if (!node.op) // parse error

@@ -1343,3 +1237,3 @@ return { xpr: [] };

return extra( { list: node.args.map( expression ) }, node, 0 );
default: { // '=', 'and', CSN v0 input: binary (n-ary) and unary prefix
default: { // CSN v0 input (A2J: '='/'and'): binary (n-ary) and unary prefix
if (!node.args.length)

@@ -1353,3 +1247,3 @@ return { xpr: [] };

op: { val },
args: (nary.length > 2 ? nary.slice(1) : nary), // length 1,2 only with CSN v0
args: (nary.length > 2 ? nary.slice(1) : nary),
$parens: node.$parens,

@@ -1370,2 +1264,3 @@ };

return array;
// nary: [ ‹a›, '+', ‹b›, '+', ‹c› ] → [ [ ‹a›, '+', ‹b› ], '+', ‹c› ]
let left = array.slice( 0, 3 );

@@ -1622,10 +1517,8 @@ let index = 3;

module.exports = {
cloneCsnDictionary: (csn, options) => csnDictionary(csn, false, options),
cloneAnnotationValue,
compactModel,
compactQuery,
compactExpr,
sortCsn,
csnDictionaries,
csnDirectValues,
csnPropertyOrder: propertyOrder,
};

@@ -106,2 +106,3 @@ // Generic ANTLR parser class with AST-building functions

pushXprToken,
pushOpToken,
argsExpression,

@@ -364,2 +365,4 @@ valuePathAst,

}
if (!this._ctx.stop)
return art;

@@ -644,2 +647,4 @@ // The last token (this._ctx.stop) may be a multi-line string literal, in which

expr.$parens = [ location ];
if (expr.$opPrecedence)
expr.$opPrecedence = null;
return (asQuery) ? { query: expr, location } : expr;

@@ -649,6 +654,6 @@ }

function tokensToStringRepresentation( matchedRule ) {
function tokensToStringRepresentation( start, stop ) {
const tokens = this._input.getTokens(
matchedRule.start.tokenIndex,
matchedRule.stop.tokenIndex + 1, null
start.tokenIndex,
stop.tokenIndex + 1, null
).filter(tok => tok.channel === antlr4.Token.DEFAULT_CHANNEL);

@@ -779,29 +784,2 @@ if (tokens.length === 0)

// only to be used in @after
// TODO: remove compatible stuff (A2J/checks use op: 'and'/'=')
function argsExpression( args, nary, location ) {
// console.log('AE:',args);
if (args.length === 1)
return args[0];
if (nary && args.length === 3 && args[1]?.val === nary) {
return this.attachLocation( {
op: { val: nary, location: args[1].location },
args: [ args[0], args[2] ],
location: undefined,
} );
}
// eslint-disable-next-line no-nested-ternary
const val = nary === '?:' ? nary
: (nary && nary !== '=' ? 'nary' : 'ixpr');
const op = {
val, // there is no n-ary in rule conditionTerm
location: this.startLocation(),
};
return this.attachLocation( {
op,
args,
location: location && { __proto__: CsnLocation.prototype, ...location },
} );
}
function pushXprToken( args ) {

@@ -918,7 +896,7 @@ const token = this._input.LT(-1);

function expressionAsAnnotationValue( assignment, cond ) {
if (!cond.cond) // parse error
function expressionAsAnnotationValue( assignment, cond, start, stop ) {
if (!cond) // parse error
return;
Object.assign(assignment, cond.cond);
assignment.$tokenTexts = this.tokensToStringRepresentation(cond);
Object.assign(assignment, cond);
assignment.$tokenTexts = this.tokensToStringRepresentation( start, stop );
}

@@ -1039,3 +1017,18 @@

function relevantDigits( val ) {
return val.replace( /0*(e.+)?$/i, '' ).replace( /\./, '' ).replace( /^[-+0]+/, '' );
val = val.replace( /e.+$/i, '' );
// To avoid the super-linear RegEx `0+$`, use the non-backtracking version and
// simply check if we're at the end.
const trailingZeroes = /0+/g;
let re;
while ((re = trailingZeroes.exec(val)) !== null) {
if (trailingZeroes.lastIndex === val.length) {
val = val.slice(0, re.index);
break;
}
}
return val
.replace( /\./, '' )
.replace( /^[-+0]+/, '' );
}

@@ -1287,23 +1280,88 @@

const operatorPrecedences = {
// query:
union: 1,
except: 1,
minus: 1,
intersect: 2,
};
// Create AST node for binary operator `op` and arguments `args`
function leftAssocBinaryOp( left, opToken, eToken, right, extraProp = 'quantifier' ) {
const op = this.valueWithTokenLocation( opToken.text.toLowerCase(), opToken);
function leftAssocBinaryOp( expr, right, opToken, eToken, extraProp ) {
if (!right)
return expr;
const op = this.valueWithTokenLocation( opToken.text.toLowerCase(), opToken );
const extra = eToken
? this.valueWithTokenLocation( eToken.text.toLowerCase(), eToken )
: undefined;
if (!left.$parens &&
(left.op && left.op.val) === (op && op.val) &&
(left[extraProp] && left[extraProp].val) === (extra && extra.val)) {
left.args.push( right );
return left;
if (!expr.$parens && expr.op?.val === op.val && expr[extraProp]?.val === extra?.val) {
expr.args.push( right );
return expr;
}
else if (extra) {
return {
op, [extraProp]: extra, args: [ left, right ], location: left.location,
};
const opPrec = operatorPrecedences[op.val] || 0;
let left = expr;
let args;
while (opPrec > nodePrecedence( left )) {
args = left.args;
left = args[args.length - 1];
}
// TODO: location correct?
const node = (extra) // eslint-disable-next-line
? { op, [extraProp]: extra, args: [ left, right ], location: left.location }
: { op, args: [ left, right ], location: left.location };
if (!args)
return node;
args[args.length - 1] = node;
return expr;
}
return { op, args: [ left, right ], location: left.location };
function nodePrecedence( node ) {
const { op } = node;
return op && !node.$parens && operatorPrecedences[op.val] || Infinity;
}
function pushOpToken( args, precedence ) { // for nary only; uses LT(-1) as operator token
let node = null;
let left = args;
while (left?.$opPrecedence && left.$opPrecedence < precedence) {
args = left;
node = args[args.length - 1]; // last sub node of left side
left = node.args;
}
if (left?.$opPrecedence === precedence ) { // nary
args = left;
}
else if (node) {
const sub = this.argsExpression( [ node, null ], true );
args[args.length - 1] = sub;
args = sub.args;
args.length = 1;
}
else if (args.length > 1) { // new top-level op & op on left
args[0] = this.argsExpression( [ ...args ], args.$opPrecedence != null ); // finish expresion
args.length = 1;
}
args.$opPrecedence = precedence;
// TODO (if necessary): `location` for sub expessions, top-level is be properly set
this.pushXprToken( args );
return args;
}
// only to be used in @after or via pushOpToken
function argsExpression( args, nary ) {
if (args.length === 1) // args.length === 0 is ok (for OVER…)
return args[0];
const $parens = args[0]?.$parens;
const loc = ($parens) ? $parens[$parens.length - 1] : args[0]?.location;
const location = loc ? { __proto__: CsnLocation.prototype, ...loc } : this.startLocation();
// console.log('AE:',args);
const op = {
// eslint-disable-next-line no-nested-ternary
val: nary === '?:' ? nary : nary ? 'nary' : 'ixpr',
location,
};
return this.attachLocation( { op, args, location } );
}
const maxCardinalityKeywords = { 1: 'one', '*': 'many' };

@@ -1310,0 +1368,0 @@

@@ -174,2 +174,4 @@ // CSN functionality for resolving references

// TODO: some `name` property would be useful (also set with `initDefinition`)
// Properties in cache:

@@ -900,5 +902,10 @@ //

*
* The callback is called on the following XSN nodes inside the query `query`:
* - a query node, which has property `SET` or `SELECT` (or `projection`),
* - a query source node inside `from` if it has property `ref`,
* - NOT on a `join` node inside `from`.
*
* @param {CSN.Query} query
* @param {CSN.QuerySelect} fromSelect
* @param {CSN.Query} parentQuery
* @param {CSN.QuerySelect} fromSelect: for query in `from`
* @param {CSN.Query} parentQuery: for a sub query (ex those in `from`)
* @param {(query: CSN.Query&CSN.QueryFrom, select: CSN.QuerySelectEnriched, parentQuery: CSN.Query) => void} callback

@@ -1011,3 +1018,4 @@ */

function implicitAs( ref ) {
const id = pathId( ref[ref.length - 1] );
const item = ref[ref.length - 1];
const id = (typeof item === 'string') ? item : item.id; // inlined `pathId`
return id.substring( id.lastIndexOf('.') + 1 );

@@ -1014,0 +1022,0 @@ }

'use strict';
const { csnRefs, implicitAs, pathId } = require('./csnRefs');
const { applyTransformations, applyTransformationsOnNonDictionary, applyTransformationsOnDictionary } = require('../transform/db/applyTransformations');
const {
transformExpression,
applyTransformations,
applyTransformationsOnNonDictionary,
applyTransformationsOnDictionary,
} = require('../transform/db/applyTransformations');
const { isBuiltinType, isMagicVariable, isAnnotationExpression } = require('../compiler/builtins.js');
const {
sortCsn,
cloneCsnDictionary: _cloneCsnDictionary,
cloneAnnotationValue: _cloneAnnotationValue,
} = require('../json/to-csn');
const { ModelError, CompilerAssertion } = require('../base/error');

@@ -15,2 +15,3 @@ const { typeParameters } = require('../compiler/builtins');

const { version } = require('../../package.json');
const { cloneAnnotationValue } = require('./cloneCsn');

@@ -446,50 +447,4 @@ // Low-level utility functions to work with compact CSN.

/**
* Deeply clone the given CSN model and return it.
* In testMode (or with testSortCsn), definitions are sorted.
*
* This function is CSN aware! Don't put annotation values into it, or
* keys such as "elements" will be interpreted according to CSN rules!
*
* @see cloneAnnotationValue
* @see cloneCsnDictionary
*
* @param {object} csn Top-level CSN. You can pass non-dictionary values.
* @param {CSN.Options} options CSN Options, only used for `dictionaryPrototype`, `testMode`, and `testSortCsn`
*/
function cloneCsnNonDict( csn, options ) {
return sortCsn(csn, options);
}
/**
* Deeply clone the given CSN dictionary and return it.
* This function does _not_ sort the given dictionary.
* See cloneCsnNonDict() if you want sorted definitions.
*
* This function is CSN aware! Don't put annotation values into it, or
* keys such as "elements" will be interpreted according to CSN rules!
*
* @see cloneAnnotationValue
* @see cloneCsnNonDict
*
* @param {object} csn
* @param {CSN.Options} options Only cloneOptions.dictionaryPrototype is
* used and cloneOptions are passed to sortCsn().
*/
function cloneCsnDictionary( csn, options ) {
return _cloneCsnDictionary(csn, options);
}
/**
* Clones the given annotation _value_. `value` must not be an object with annotations,
* but the annotation value itself, e.g. `[ { a: 1 } ]`, not `@anno: [...]`.
*
* @param {any} value
* @returns {any}
*/
function cloneAnnotationValue( value ) {
return _cloneAnnotationValue( value );
}
/**
* Apply function `callback` to all artifacts in dictionary

@@ -1097,13 +1052,15 @@ * `model.definitions`. See function `forEachGeneric` for details.

function copyAnnotations( fromNode, toNode, overwrite = false, excludes = {}, annoNames = undefined ) {
// Ignore if no toNode (in case of errors)
if (!toNode)
return;
const copiedAnnoNames = [];
if (toNode) {
if (annoNames == null)
annoNames = Object.keys(fromNode).filter(key => key.startsWith('@'));
if (annoNames == null)
annoNames = Object.keys(fromNode).filter(key => key.startsWith('@'));
annoNames.forEach((anno) => {
if ((toNode[anno] === undefined || overwrite) && !excludes[anno])
toNode[anno] = cloneAnnotationValue(fromNode[anno]);
});
annoNames.forEach((anno) => {
if ((toNode[anno] === undefined || overwrite) && !excludes[anno]) {
toNode[anno] = cloneAnnotationValue(fromNode[anno]);
copiedAnnoNames.push(anno);
}
});
}
return copiedAnnoNames;
}

@@ -1281,20 +1238,3 @@

/**
* Sorts the definition dictionary in tests mode.
*
* @param {CSN.Model} csn
* @param {CSN.Options} options
*/
function sortCsnDefinitionsForTests( csn, options ) {
if (!options.testMode && !options.testSortCsn)
return;
const sorted = Object.create(null);
Object.keys(csn.definitions || {}).sort().forEach((name) => {
sorted[name] = csn.definitions[name];
});
csn.definitions = sorted;
}
/**
* Return an array of non-abstract service names contained in CSN

@@ -1459,8 +1399,24 @@ *

/**
* Return true if prop is an annotation and the annotation value has an expression
*
* @param {object} node
* @param {string} prop
* @returns {boolean}
*/
function findAnnotationExpression( node, prop ) {
let isExpr = false;
if (prop[0] === '@') {
transformExpression(node, prop, {
'=': (p) => {
isExpr ||= isAnnotationExpression(p);
},
});
}
return isExpr;
}
module.exports = {
getUtils,
cloneCsn: cloneCsnNonDict, // Umbrella relies on this name
cloneCsnNonDict,
cloneCsnDictionary,
cloneAnnotationValue,
isBuiltinType,

@@ -1481,2 +1437,3 @@ isMagicVariable,

getElementDatabaseNameOf,
transformExpression,
applyTransformations,

@@ -1500,3 +1457,2 @@ applyTransformationsOnNonDictionary,

getNamespace,
sortCsnDefinitionsForTests,
getServiceNames,

@@ -1512,2 +1468,3 @@ walkCsnPath,

pathName,
findAnnotationExpression,
};

@@ -44,3 +44,3 @@ 'use strict';

.option(' --localized-without-coalesce')
.option(' --tenant-as-column')
.option(' --tenant-discriminator')
.option(' --default-binary-length <length>')

@@ -105,3 +105,3 @@ .option(' --default-string-length <length>')

with name = "+", write complete XSN, long!
--tenant-as-column Add tenant fields to entities
--tenant-discriminator Add tenant fields to entities
--internal-msg Write raw messages with call stack to <stdout>/<stderr>

@@ -108,0 +108,0 @@ --beta-mode Enable all unsupported, incomplete (beta) features

@@ -14,3 +14,3 @@

} = require('./utils/sql');
const { sortCsn } = require('../json/to-csn');
const { sortCsn } = require('../model/cloneCsn');

@@ -17,0 +17,0 @@ /**

@@ -601,6 +601,5 @@ // Common render functions for toCdl.js, toHdbcds.js and toSql.js

* @param {ExpressionConfiguration} rendererBase
* @param {boolean} [adaptPath] If true, `env.path` will be adapted for lists and subExpr.
* @returns {ExpressionRenderer} Expression rendering utility
*/
function createExpressionRenderer( rendererBase, adaptPath = false ) {
function createExpressionRenderer( rendererBase ) {
const renderer = Object.create(rendererBase);

@@ -620,3 +619,2 @@ renderer.visitExpr = visitExpr;

renderObj.isNestedXpr = false;
renderObj.adaptPath = adaptPath;
return renderObj.visitExpr(x);

@@ -633,3 +631,2 @@ };

renderObj.isNestedXpr = true;
renderObj.adaptPath = adaptPath;
return renderObj.visitExpr(x);

@@ -660,8 +657,6 @@ };

const tokens = x.map((item, i) => {
if (this.adaptPath) {
// We want to keep the prototype of the original env.
const env = Object.assign(Object.create(Object.getPrototypeOf(this.env || {})), this.env, { path: [ ...this.env.path, i ] });
return this.renderSubExpr(item, env);
}
return this.renderSubExpr(item);
this.env.path.push( i );
const result = this.renderSubExpr(item, this.env);
this.env.path.length -= 1;
return result;
});

@@ -680,8 +675,6 @@ return beautifyExprArray(tokens);

return `(${x.list.map((item, i) => {
if (this.adaptPath) {
// We want to keep the prototype of the original env.
const env = Object.assign(Object.create(Object.getPrototypeOf(this.env || {})), this.env, { path: [ ...this.env.path, 'list', i ] });
return this.renderExpr(item, env);
}
return this.renderExpr(item);
this.env.path.push('list', i);
const result = this.renderExpr(item, this.env);
this.env.path.length -= 2;
return result;
}).join(', ')})`;

@@ -688,0 +681,0 @@ }

@@ -1,2 +0,2 @@

// Add tenant field MANDT to entities
// Add tenant field to entities, check validity

@@ -9,9 +9,8 @@ // Prerequisites:

// TODO entities without MANDT:
// TODO clarify:
//
// - do we have to do something for secondary keys?
// - cache whether structure type contains (managed) association to entity with MANDT
// - disallow use of such a type in entity without MANDT
// Implementation remark:
//
// - the functions `forEachDefinition` & friends in csnUtils.js have become quite

@@ -23,6 +22,7 @@ // (too) general and are probably slow → not used here

const { createMessageFunctions } = require( '../base/messages' );
const { traverseQuery } = require( '../model/csnRefs' );
const { csnRefs, traverseQuery, implicitAs } = require( '../model/csnRefs' );
const fieldName = 'tenant';
const fieldDef = {
const annoTenantIndep = '@cds.tenant.independent';
const tenantDef = {
key: true,

@@ -35,8 +35,24 @@ type: 'cds.String',

function addTenantFields( csn, options ) {
const { error, throwWithError } = createMessageFunctions( options, 'tenant', csn );
const { tenantDiscriminator } = options;
const tenantName = tenantDiscriminator === true ? 'tenant' : tenantDiscriminator;
if (tenantName !== 'tenant') {
error( 'api-invalid-option', null, {
'#': 'value2',
option: 'tenantDiscriminator',
value: 'tenant',
rawvalue: true,
othervalue: tenantName,
} );
throwWithError();
}
const { definitions } = csn;
if (!definitions)
return csn;
const { error, throwWithError } = createMessageFunctions( options, 'tenant', csn );
const { initDefinition, artifactRef, effectiveType } = csnRefs( csn );
const typeCache = new WeakMap();
const csnPath = [ 'definitions', '' ];
let independent;
let projection;

@@ -46,23 +62,45 @@

const art = definitions[name];
if (art?.kind !== 'entity')
continue;
initDefinition( art );
csnPath[1] = name;
independent = art[annoTenantIndep];
projection = art.query || art.projection && art;
if (art['@cds.tenant.independent'] != null) {
error( null, csnPath, { anno: '@cds.tenant.independent' },
'Can\'t yet add annotation $(ANNO) to an entity' );
if (art.kind === 'entity') {
independent = !!independent; // value should not influence message variant
if (independent && art.includes && !checkIncludes( art ))
continue;
handleElements( art );
if (projection)
traverseQuery( projection, null, null, handleQuery );
}
if (!handleElements( art ))
continue;
projection = art.query || art.projection && art;
if (projection)
else if (!independent && independent != null) {
error( 'tenant-invalid-anno-value', csnPath, { anno: annoTenantIndep, value: independent },
// eslint-disable-next-line max-len
'Can\'t add $(ANNO) with value $(VALUE) to a non-entity, which is always tenant-independent' );
}
else if (art.includes) {
independent = art.kind; // might be used for message variant
checkIncludes( art ); // recompile should work
}
else if (projection) { // events - TODO: mention in doc
independent = art.kind; // might be used for message variant
// recompile should work: no new `tenant` source element for `select *`
traverseQuery( projection, null, null, handleQuery );
}
}
// Finally add the `tenant` element (do separately in order not to confuse
// the cache of csnRefs):
for (const name in definitions) {
const art = definitions[name];
if (isTenantDepEntity( art ))
art.elements = { [tenantName]: { ...tenantDef }, ...art.elements };
// consider non-enumerable `elements` of subqueries if that is supported
}
(csn.extensions || []).forEach( (ext, idx) => {
const tenant = ext.elements?.[fieldName];
(csn.extensions || []).forEach( ( ext, idx ) => {
const tenant = ext.elements?.[tenantName];
const name = ext.annotate || ext.extend; // extend should not happen
if (tenant && definitions[name]?.kind === 'entity') { // TODO: ok for tenant-independent
error( null, [ 'extensions', idx, 'elements', 'tenant' ],
{ name: fieldName },
if (tenant && isTenantDepEntity( definitions[name] )) {
error( 'tenant-unexpected-ext', [ 'extensions', idx, 'elements', 'tenant' ],
{ name: tenantName },
'Can\'t annotate element $(NAME) of a tenant-dependent entity' );

@@ -73,38 +111,47 @@ }

throwWithError();
csn.meta ??= {};
csn.meta.tenantDiscriminator = tenantName;
return csn; // input CSN changed by side effect
function checkIncludes( art ) {
const names = art.includes
.filter( name => isTenantDepEntity( csn.definitions[name] ) );
if (names.length) {
error( 'tenant-invalid-include', csnPath, { names }, {
// eslint-disable-next-line max-len
std: 'Can\'t include the tenant-dependent entities $(NAMES) into a tenant-independent definition',
// eslint-disable-next-line max-len
one: 'Can\'t include the tenant-dependent entity $(NAMES) into a tenant-independent definition',
} );
}
return !names.length;
}
function handleElements( art ) {
const { elements } = art;
if (elements[fieldName]) {
error( null, [ ...csnPath, 'elements', fieldName ], { name: fieldName },
'Can\'t add tenant field to entity having an element $(NAME)' );
return false;
if (elements[tenantName]) {
error( 'tenant-unexpected-element', [ ...csnPath, 'elements', tenantName ],
{ name: tenantName, option: 'tenantDiscriminator' },
'Can\'t have entity with element $(NAME) when using option $(OPTION)' );
}
if (!Object.values( elements ).some( e => e.key )) {
error( null, csnPath, {},
else if (!independent && !Object.values( elements ).some( e => e.key )) {
error( 'tenant-expecting-key', csnPath, {},
'There must be a key in a tenant-dependent entity' );
return false;
}
handleAssociations( art );
art.elements = { [fieldName]: { ...fieldDef }, ...elements };
return true;
else {
traverse( art, handleAssociations );
}
}
// Queries --------------------------------------------------------------------
function handleQuery( query ) {
// TODO: errors are temporary: start with simple projections only = no better
// message $location necessary yet
if (!projection) // error already reported
// message $location necessary yet - better: set `name` in csnRefs
if (!projection || query.ref && handleQuerySource( query ))
return;
if (query.ref) {
if ((query.as || implicitAs( query.ref )) === fieldName) {
error( null, csnPath, { name: fieldName },
'Can\'t have a table alias named $(NAME) in a tenant-dependent entity' );
}
return;
}
const select = query.SELECT || query.projection;
if (query.SET || query !== projection || !select?.from?.ref) {
error( null, csnPath, {},
'Can\'t add tenant columns to non-simple query entities' );
if (query !== projection && !independent) {
error( 'tenant-unsupported-query', csnPath, {},
'Can\'t yet have tenant-dependent non-simple query entities' );
projection = null;

@@ -116,29 +163,61 @@ return;

csnPath.push( 'projection' );
else if (query.SELECT)
csnPath.push( 'query', 'SELECT' );
else
csnPath.push( 'query', 'SELECT' );
return; // query.SET or query.join
const select = query.SELECT || query.projection;
if (select.mixin)
handleMixins( select.mixin );
if (select.excluding)
checkExcluding( select.excluding );
if (select.columns)
handleColumns( select.columns );
// TODO: for subqueries, we might need to adapt the inferred elements
// TODO: where exists ref -
// TODO: select and query clauses, especially with aggregation functions
handleGroupBy( select );
checkMixins( select.mixin );
if (!independent) {
if (select.excluding)
checkExcluding( select.excluding );
if (select.columns)
handleColumns( select.columns );
// TODO: when we allow subqueries, we must also check for published in redirected assocs
// TODO: for subqueries, we might need to adapt the inferred elements
// TODO: where exists ref -
// TODO: select and query clauses, especially with aggregation functions
handleGroupBy( select );
}
else if (query !== projection && select.columns) {
checkColumnCasts( select.columns );
}
csnPath.length = 2;
}
function handleMixins( mixin ) {
function handleQuerySource( query ) {
if (independent) {
const art = query.ref[0]; // yes, the base
if (csn.definitions[art][annoTenantIndep])
return true;
error( 'tenant-invalid-query-source', csnPath, { art, '#': independent }, {
std: 'Can\'t use a tenant-dependent query source $(ART) in a tenant-independent entity',
event: 'Can\'t use a tenant-dependent query source $(ART) in an event',
} );
return true;
}
if (query !== (projection.SELECT || projection.projection)?.from) // with `join`
return false;
if ((query.as || implicitAs( query.ref )) === tenantName) {
error( 'tenant-invalid-alias-name', csnPath,
{ name: tenantName, '#': (query.as ? 'std' : 'implicit') } );
}
const art = artifactRef.from( query );
if (art[annoTenantIndep]) {
error( 'tenant-expecting-tenant-source', csnPath, { art: query },
// TODO: better the final entity name of assoc navigation in FROM
// eslint-disable-next-line max-len
'Expecting the query source $(ART) to be tenant-dependent for a tenant-dependent query entity' );
}
return true;
}
function checkMixins( mixin ) {
csnPath.push( 'mixin', '' );
for (const name in mixin) {
csnPath[csnPath.length - 1] = name;
if (name !== fieldName) {
addToCondition( mixin[name], name );
}
else {
error( null, csnPath, { name },
'Can\'t define a mixin named $(NAME) in a tenant-dependent entity' );
}
if (name === tenantName && !independent)
error( 'tenant-invalid-alias-name', csnPath, { name, '#': 'mixin' } );
handleAssociations( mixin[name], null );
}

@@ -149,5 +228,5 @@ csnPath.length -= 2;

function checkExcluding( excludeList ) {
if (excludeList.includes( fieldName )) {
error( null, csnPath, { name: fieldName },
'Can\'t exclude $(NAME) from query source' );
if (excludeList.includes( tenantName )) {
error( 'tenant-invalid-excluding', csnPath, { name: tenantName },
'Can\'t exclude $(NAME) from the query source of a tenant-dependent entity' );
}

@@ -163,3 +242,3 @@ }

if (select.groupBy)
select.groupBy.unshift( { ref: [ fieldName ] } );
select.groupBy.unshift( { ref: [ tenantName ] } );
}

@@ -173,3 +252,3 @@

if (col.expand || col.inline) {
error( null, csnPath, {},
error( 'tenant-unsupported-expand-inline', csnPath, {},
'Can\'t use expand/inline in a tenant-dependent entity' );

@@ -179,12 +258,122 @@ }

specifiedKey = true;
if (col.cast?.on) // REDIRECTED TO with explicit ON - TODO (low prio): less $self
addToCondition( col.cast, col.as || implicitAs( col.ref ) );
// REDIRECTED TO: also check new target here? (main query: already checked via elements)
}
csnPath.length -= 2;
columns.unshift( specifiedKey
? { key: true, ref: [ fieldName ] }
: { ref: [ fieldName ] } );
? { key: true, ref: [ tenantName ] }
: { ref: [ tenantName ] } );
}
function handleAssociations( elem ) {
function checkColumnCasts( columns, prop = 'columns' ) {
csnPath.push( prop, -1 );
for (const col of columns) {
++csnPath[csnPath.length - 1];
if (col.cast?.target)
handleAssociations( col.cast, null );
else if (col.expand)
checkColumnCasts( col.expand, 'expand' );
else if (col.inline)
checkColumnCasts( col.inline, 'inline' );
}
csnPath.length -= 2;
}
// Associations ---------------------------------------------------------------
function handleAssociations( elem, afterRecursion ) {
if (afterRecursion != null)
return null;
if (elem.target) {
if (!csn.definitions[elem.target][annoTenantIndep]) {
if (independent)
error( 'tenant-invalid-target', csnPath, { target: elem.target } );
}
else if (!independent && isComposition( elem )) {
error( 'tenant-invalid-composition', csnPath, { target: elem.target } );
}
}
else if (elem.type && (independent || !elem.elements && !elem.items)) {
// check type, but not with expanded elements in dependent entity, because
// composition could have redirected tenant-dependent target
const dep = typeDependency( artifactRef( elem.type, null ) );
if (independent) {
if (!dep || dep === 'Composition')
return true; // check elements (assocs could be redirected)
error( 'tenant-invalid-target', csnPath, { type: elem.type, '#': 'type' } );
}
else if (dep && dep !== 'dependent') {
error( 'tenant-invalid-composition', csnPath, { type: elem.type, '#': 'type' } );
}
}
else {
return true;
}
return null;
}
/**
* Returns “type dependency”, a string, for type `assoc`:
*
* - '': type does not contain associations other than non-composition associations to
* tenant-independent entities
* - 'Composition': type contains associations (and at least one composition) to
* tenant-independent entities, and no associations to tenant-dependent entities
* - 'dependent': type contains associations, at least one to a tenant-dependent entity,
* but no compositions to tenant-independent entities
* - 'ERR': type contains associations, at least one to a tenant-dependent entity,
* and at least one composition to a tenant-independent entity
*
* Type references are followed, but only without sibling `elements` or `items`.
*/
function typeDependency( assoc ) {
if (!assoc || !(assoc = effectiveType( assoc )))
return '';
const assocDep = typeCache.get( assoc );
if (assocDep != null)
return assocDep;
let parentDep = '';
traverse( assoc, typeCallback );
return parentDep;
function typeCallback( type, savedDep ) {
let currentDep = typeCache.get( type );
if (currentDep != null) {
// nothing
}
else if (savedDep != null) {
currentDep = parentDep;
parentDep = savedDep;
}
else if (type.target) {
const annoDep = !csn.definitions[type.target][annoTenantIndep];
currentDep = (annoDep) ? 'dependent' : isComposition( type ) && 'Composition';
}
else if (type.elements || type.items) {
savedDep = parentDep;
parentDep = '';
return savedDep || ''; // recurse
}
else if (type.type) {
currentDep = typeDependency( artifactRef( type.type, null ) );
}
else {
currentDep = '';
}
typeCache.set( type, currentDep );
if (!currentDep || !parentDep)
parentDep ||= currentDep;
else if (currentDep !== parentDep)
parentDep = 'ERR';
return null; // do not (further) recurse
}
}
// Generic functions ----------------------------------------------------------
function traverse( elem, callback ) {
const recurse = callback( elem, null );
if (recurse == null)
return;
const { elements } = elem;

@@ -195,41 +384,27 @@ if (elements) {

csnPath[csnPath.length - 1] = name;
handleAssociations( elements[name] );
traverse( elements[name], callback );
}
csnPath.length -= 2;
}
else if (elem.target) {
if (elem.on) {
addToCondition( elem, csnPath[csnPath.length - 1] );
}
else {
error( null, csnPath, {},
'Can\'t yet use managed associations in a tenant-dependent entity' );
}
}
else if (elem.items) {
csnPath.push( 'items' );
handleAssociations( elem.items );
traverse( elem.items, callback );
--csnPath.length;
}
callback( elem, recurse );
}
function addToCondition( elem, assoc ) {
if (!elem.on)
return;
const withSelf = csnPath.length > 4;
elem.on = [
{ ref: [ assoc, fieldName ] }, // TODO: consider assoc name starting with '$'
'=',
{ ref: (withSelf) ? [ '$self', fieldName ] : [ fieldName ] },
'and',
// TODO: avoid (...) for standard AND-ed EQ-comparisons ?
{ xpr: elem.on },
];
function isComposition( assoc ) {
while (assoc && assoc.type !== 'cds.Association') {
const { type } = assoc;
if (type === 'cds.Composition')
return true;
assoc = artifactRef( type, null );
}
return false;
}
}
function implicitAs( ref ) {
const item = ref[ref.length - 1];
const id = (typeof item === 'string') ? item : item.id;
return id.substring( id.lastIndexOf('.') + 1 );
function isTenantDepEntity( art ) {
return art?.kind === 'entity' && !art[annoTenantIndep];
}

@@ -236,0 +411,0 @@

@@ -310,2 +310,12 @@ 'use strict';

function transformExpression( parent, propName, transformers, path = [] ) {
const callT = (t, cpn, child) => {
const ct = t[cpn];
if (ct) {
const ppn = propName;
if (Array.isArray(ct))
ct.forEach(cti => cti(child, cpn, child[cpn], path, parent, ppn));
else
ct(child, cpn, child[cpn], path, parent, ppn);
}
};
if (propName != null) {

@@ -323,11 +333,6 @@ const child = parent[propName];

for (const cpn of Object.getOwnPropertyNames( child )) {
const ct = transformers[cpn];
if (ct) {
const ppn = propName;
if (Array.isArray(ct))
ct.forEach(cti => cti(child, cpn, child[cpn], path, parent, ppn));
else
ct(child, cpn, child[cpn], path, parent, ppn);
}
if (Array.isArray(transformers))
transformers.forEach(t => callT(t, cpn, child));
else
callT(transformers, cpn, child);
transformExpression(child, cpn, transformers, path);

@@ -334,0 +339,0 @@ }

'use strict';
const {
cloneCsnNonDict, applyTransformationsOnNonDictionary, isAssociationOperand, isDollarSelfOrProjectionOperand,
applyTransformationsOnNonDictionary, isAssociationOperand, isDollarSelfOrProjectionOperand,
} = require('../../model/csnUtils');

@@ -10,2 +10,3 @@

const { forEach } = require('../../utils/objectUtils');
const { cloneCsnNonDict } = require('../../model/cloneCsn');

@@ -12,0 +13,0 @@ /**

@@ -5,3 +5,2 @@ 'use strict';

const { applyTransformations, hasAnnotationValue, getResultingName } = require('../../model/csnUtils');
const { csnRefs } = require('../../model/csnRefs');
const { forEach, forEachKey } = require('../../utils/objectUtils');

@@ -20,2 +19,3 @@ const { CompilerAssertion } = require('../../base/error');

function createReferentialConstraints( csn, options ) {
const isTenant = options.tenantDiscriminator === true;
let validated = true;

@@ -29,3 +29,2 @@ let enforced = true;

const { inspectRef } = csnRefs(csn);
// prepare the functions with the compositions and associations across all entities first

@@ -37,3 +36,3 @@ // and execute it afterwards.

applyTransformations(csn, {
elements: (parent, prop, elements, path) => {
elements: (art, prop, elements, path) => {
// Step I: iterate compositions, enrich dependent keys for <up_> association in target entity of composition

@@ -46,3 +45,3 @@ for (const elementName in elements) {

fn: () => {
foreignKeyConstraintForUpLinkOfComposition(element, parent, ePath);
foreignKeyConstraintForUpLinkOfComposition(element, art, ePath);
},

@@ -60,3 +59,4 @@ });

fn: () => {
foreignKeyConstraintForAssociation(element, ePath );
// parent entity of assoc becomes dependent table
foreignKeyConstraintForAssociation( element, art, ePath );
},

@@ -68,16 +68,22 @@ });

const { on } = element;
const target = csn.definitions[element.target];
const textsEntity = csn.definitions[element.target];
// `texts` entities have a key named "locale"
const targetSideHasLocaleKey = target.elements.locale?.key;
if (targetSideHasLocaleKey && !skipConstraintGeneration(parent, target, { /* there is no assoc */ })) {
const sourceElements = Array.from(elementsOfSourceSide(on, elements));
const targetElements = Array.from(elementsOfTargetSide(on, target.elements));
const targetSideHasLocaleKey = textsEntity.elements.locale?.key;
if (targetSideHasLocaleKey && !skipConstraintGeneration(art, textsEntity, { /* there is no assoc */ })) {
// Note: a `texts` composition in `Foo` will co-modify `Foo.texts` and create the constraints over there
const { dependentKeys, parentKeys } = extractKeys(on, textsEntity.elements, elements);
const keysInParent = Object.values(elements).filter(e => e.key);
if (keysInParent.length !== dependentKeys.length)
continue;
// `texts` entities have all the keys the original entity has
const allElementsAreKeysAndHaveTheSameName = targetElements.length &&
targetElements.every(
([ targetKey, e ]) => e.key &&
sourceElements.some(([ sourceKey, sourceElement ]) => sourceElement.key && targetKey === sourceKey )
);
const allElementsAreKeysAndHaveTheSameName = parentKeys.length &&
parentKeys
.every(
([ targetKey, e ]) => e.key &&
dependentKeys.some(([ sourceKey, sourceElement ]) => sourceElement.key && targetKey === sourceKey )
);
if (allElementsAreKeysAndHaveTheSameName)
attachConstraintsToDependentKeys(targetElements, sourceElements, path[path.length - 1], 'texts', { texts: true });
attachConstraintsToDependentKeys(dependentKeys, parentKeys, path[path.length - 1], 'texts', { texts: true });
}

@@ -87,3 +93,3 @@ }

},
}, [], { skipIgnore: false, skipArtifact: a => a.query || a.kind !== 'entity' });
}, [], { skipIgnore: false, skipArtifact: a => !!(a.query || a.kind !== 'entity' ) });

@@ -109,3 +115,3 @@ // create constraints on foreign keys

function foreignKeyConstraintForUpLinkOfComposition( composition, parent, path ) {
const dependent = csn.definitions[path[1]];
const dependent = csn.definitions[composition.target];
if (skipConstraintGeneration(parent, dependent, composition))

@@ -119,3 +125,3 @@ return;

if (up_.keys && isToOne(up_)) // no constraint for unmanaged / to-many up_ links
foreignKeyConstraintForAssociation(up_, [ 'definitions', composition.target, 'elements', upLinkName ], path[path.length - 1]);
foreignKeyConstraintForAssociation(up_, dependent, [ 'definitions', composition.target, 'elements', upLinkName ], path[path.length - 1] );
}

@@ -133,24 +139,49 @@ else if (!onCondition && composition.keys.length > 0) {

* @param {CSN.Association} association for that a constraint should be generated
* @param {CSN.Entity} dependent the entity for which a constraint will be generated
* @param {CSN.Path} path
* @param {CSN.PathSegment} upLinkFor the name of the composition which used this association in a `$self = <comp>.<up_>` comparison
*/
function foreignKeyConstraintForAssociation( association, path, upLinkFor = null ) {
function foreignKeyConstraintForAssociation( association, dependent, path, upLinkFor = null ) {
const parent = csn.definitions[association.target];
const dependent = csn.definitions[path[1]];
if (skipConstraintGeneration(parent, dependent, association))
return;
const { elements } = csn.definitions[path[1]];
const onCondition = association.on;
if (onCondition && hasConstraintCompliantOnCondition(association, elements, path)) {
const { elements } = dependent;
if (association.keys) {
// 1. cds.Association has constraint compliant on-condition
// mark each dependent key - in the entity containing the association - referenced in the on-condition
const dependentKeys = Array.from(elementsOfSourceSide(onCondition, elements));
const parentKeys = Array.from(elementsOfTargetSide(onCondition, parent.elements));
const parentKeys = [];
const dependentKeys = [];
association.keys.forEach( (k) => {
dependentKeys.push( [ k.$generatedFieldName, elements[k.$generatedFieldName] ] );
const parentKey = parent.elements[k.ref[0]];
if (parentKey.key) // only keys are valid references in foreign key constraints
parentKeys.push( [ k.ref[0], parent.elements[k.ref[0]] ] );
});
if (isTenant && elements.tenant && parent.elements.tenant) { // `tenant` is not part of on
dependentKeys.push([ 'tenant', elements.tenant ]);
parentKeys.push([ 'tenant', parent.elements.tenant ]);
}
const allKeysCovered = parentKeys.length === Object.values(parent.elements).filter(e => e.key).length;
// sanity check; do not generate constraints for on-conditions like "dependent.idOne = id AND dependent.idTwo = id"
if (dependentKeys.length === parentKeys.length)
if (allKeysCovered && dependentKeys.length === parentKeys.length)
attachConstraintsToDependentKeys(dependentKeys, parentKeys, association.target, path[path.length - 1], upLinkFor);
}
else if (!onCondition && association.keys.length > 0) {
throw new CompilerAssertion('Please debug me, an on-condition was expected here, but only found keys');
}
/**
* Extracts dependent keys and their parent keys based on an 'on' condition.
*
* @param {CSN.OnCondition} on - on condition from which dependent keys and their parent keys are extracted.
* @param {CSN.Elements} elements - The elements of the dependent entity, containing the foreign keys.
* @param {CSN.Elements} parentElements - The elements of the parent entity, containing the referenced parent keys.
* @returns {object} An object containing dependent keys and the parent keys which they reference.
*/
function extractKeys( on, elements, parentElements ) {
const dependentKeys = Array.from(elementsOfSourceSide(on, elements));
const parentKeys = Array.from(elementsOfTargetSide(on, parentElements));
if (isTenant && elements.tenant && parentElements.tenant) { // `tenant` is not part of on
dependentKeys.push([ 'tenant', elements.tenant ]);
parentKeys.push([ 'tenant', parentElements.tenant ]);
}
return { dependentKeys, parentKeys };
}

@@ -196,55 +227,2 @@

/**
* Constraints can only be generated if the full primary key of the target is referenced by the foreign key in an on-condition.
* 1. on-condition only contains AND as logical operator
* 2. each part of the on-condition must either:
* - reference a valid field in the dependent entity:
* a) for cds.Composition this is in the target entity
* b) for cds.Association this is the entity, where the association is defined
* - reference a key element in the parent entity:
* a) for cds.Composition this is the entity, where the composition itself is defined
* b) for cds.Association this is the target entity
* 3. parent keys must be the full primary key tuple
*
* @param {CSN.Association} element
* @param {CSN.Elements} siblingElements
* @param {CSN.Path} path the path to the element
* @returns {boolean} indicating whether the association / composition is a constraint candidate
*/
function hasConstraintCompliantOnCondition( element, siblingElements, path ) {
const onCondition = element.on;
const allowedTokens = [ '=', 'and', '(', ')' ];
// on condition must only contain logical operator 'AND'
if (onCondition.some(step => typeof step === 'string' && !allowedTokens.includes(step)))
return false;
// on-condition like ... TemplateAuthGroupAssignments.isTemplate = true; is not allowed
if (onCondition.some(step => typeof step === 'object' && Object.prototype.hasOwnProperty.call(step, 'val')))
return false;
// no magic vars in on-condition
// e.g. for localized: ... and localized.locale = $user.locale; -> not a valid on-condition
if (onCondition.some((step, index) => typeof step === 'object' && inspectRef(path.concat([ 'on', index ])).scope === '$magic'))
return false;
// for cds.Associations the parent keys are in the associations target entity
// for cds.Composition the parent keys are in the entity, where the composition is defined
const parentElements = csn.definitions[element.target].elements;
const parentKeys = elementsOfTargetSide(onCondition, parentElements);
const referencesNonPrimaryKeyField = Array.from(parentKeys.values()).some(parentKey => !parentKey.key);
if (referencesNonPrimaryKeyField)
return false;
// returns true if the parentKeys found in the on-condition are covering the full primary key tuple in the parent entity
return Array.from(parentKeys.entries())
// check if primary key found in on-condition is present in association target / composition source
.filter(([ keyName, pk ]) => pk.key && parentElements[keyName].key).length ===
Object.keys(parentElements)
// compare that with the length of the primary key tuple found in association target / composition source
.filter(key => parentElements[key].key &&
parentElements[key].type !== ASSOCIATION &&
parentElements[key].type !== COMPOSITION)
.length;
}
/**
* Skip referential constraint if the parent table (association target, or artifact where composition is defined)

@@ -251,0 +229,0 @@ * of the relation is:

@@ -14,2 +14,3 @@ 'use strict';

const { killNonrequiredAnno } = require('./killAnnotations');
const { featureFlags } = require('./featureFlags');

@@ -30,4 +31,7 @@ /**

rewriteExpandInline();
if (options.transformation === 'odata' || options.transformation === 'effective' || csn.meta?.[featureFlags]?.$expandInline)
rewriteExpandInline();
throwWithAnyError();
const transformers = {

@@ -194,6 +198,2 @@ keys: (parent, name, keys, path) => {

// We would be broken if we continue with assoc usage to now skipped
throwWithAnyError();
for (const {

@@ -621,3 +621,3 @@ parent, target, path,

// TODO: Remove this line in case foreign key annotations should
// be adressed via full path into target instead of using alias
// be addressed via full path into target instead of using alias
// names. See flattening.js::flattenAllStructStepsInRefs()

@@ -624,0 +624,0 @@ // apply transformations on `ref` counterpart comment.

@@ -5,4 +5,4 @@ 'use strict';

applyTransformations, applyTransformationsOnNonDictionary,
isBuiltinType, cloneCsnNonDict,
copyAnnotations, implicitAs, isDeepEqual,
isBuiltinType, cardinality2str,
copyAnnotations, implicitAs, isDeepEqual, findAnnotationExpression,
} = require('../../model/csnUtils');

@@ -13,4 +13,4 @@ const transformUtils = require('../transformUtils');

const { forEach } = require('../../utils/objectUtils');
const { cardinality2str, isAnnotationExpression } = require('../../model/csnUtils');
const { transformExpression } = require('./applyTransformations');
const { cloneCsnNonDict } = require('../../model/cloneCsn');
/**

@@ -824,3 +824,3 @@ * Strip off leading $self from refs where applicable

if (options.transformation === 'odata' || options.transformation === 'effective') {
const validAnnoNames = Object.keys(element).filter(pn => pn[0] === '@' && !isAnnotationExpression(element[pn]));
const validAnnoNames = Object.keys(element).filter(pn => pn[0] === '@' && !findAnnotationExpression(element, pn));
copyAnnotations(element, fk[1], true, {}, validAnnoNames);

@@ -827,0 +827,0 @@ }

@@ -8,2 +8,3 @@ 'use strict';

'@cds.persistence.journal': true, // Build checks on it
'@cds.tenant.independent': true,
'@sql.append': true,

@@ -10,0 +11,0 @@ '@sql.prepend': true,

@@ -10,6 +10,6 @@ 'use strict';

implicitAs,
cloneCsnNonDict,
} = require('../../model/csnUtils');
const { getBranches } = require('./flattening');
const { getColumnMap } = require('./views');
const { cloneCsnNonDict } = require('../../model/cloneCsn');

@@ -309,3 +309,3 @@ const cloneCsnOptions = { hiddenPropertiesToClone: [ '_art', '_links', '$env', '$scope' ] };

function replaceInRef( oldValue, newValue, isInXpr, refBase, linksBase ) {
const clone = { value: cloneCsnNonDict({ value: newValue }, cloneCsnOptions).value };
const clone = { value: cloneCsnNonDict(newValue, cloneCsnOptions) };
if (oldValue.stored)

@@ -312,0 +312,0 @@ clone.value.stored = oldValue.stored;

@@ -311,5 +311,17 @@ 'use strict';

const extension = root.keys ? translateManagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current) : translateUnmanagedAssocToWhere(root, target, isPrefixedWithTableAlias, base, current);
if (options.tenantDiscriminator) {
const targetEntity = csn.definitions[root.target];
if (!targetEntity['@cds.tenant.independent']) {
subselect.SELECT.where.push(
{ ref: [ target, 'tenant' ] }, '=', { ref: [ base, 'tenant' ] }, 'AND'
);
}
}
// TODO: parentheses around sub expressions are to be represented by inner `xpr`
if (extension.length > 3)
subselect.SELECT.where.push('('); // add braces around the on-condition part to ensure precedence is kept
// TODO: add tenant comparison here ?
subselect.SELECT.where.push(...extension);

@@ -316,0 +328,0 @@

'use strict';
const {
getUtils, cloneCsnNonDict, applyTransformationsOnNonDictionary, forEachDefinition,
getUtils, applyTransformationsOnNonDictionary, forEachDefinition,
} = require('../../model/csnUtils');

@@ -9,2 +9,3 @@ const { implicitAs, columnAlias, pathId } = require('../../model/csnRefs');

const { setProp } = require('../../base/model');
const { cloneCsnNonDict } = require('../../model/cloneCsn');

@@ -332,2 +333,3 @@ /**

function transformViewOrEntity( query, artifact, artName, path ) {
const ignoreAssociations = options.sqlDialect === 'hana' && options.withHanaAssociations === false;
csnUtils.initDefinition(artifact);

@@ -357,2 +359,3 @@ const { elements } = queryOrMain(query, artifact);

const elem = elements[elemName];
if (isSelect) {

@@ -386,3 +389,3 @@ if (!columnMap[elemName])

// Build new columns from the column map - bring elements and columns back in sync basically
query.SELECT.columns = Object.keys(elements).filter(elem => !elements[elem].$ignore).map(key => stripLeadingSelf(columnMap[key]));
query.SELECT.columns = Object.keys(elements).filter(elem => !elements[elem].$ignore && !(elements[elem].target && ignoreAssociations)).map(key => stripLeadingSelf(columnMap[key]));
// If following an association, explicitly set the implicit alias

@@ -389,0 +392,0 @@ // due to an issue with HANA - this seems to only have an effect on ref files with hdbcds-hdbcds, so only run then

@@ -24,3 +24,4 @@ 'use strict';

* @param {CSN.Options} options
* @param {Array} [services] Will be calculated JIT if not provided
* @param {string[]|undefined} services Will be calculated JIT if not provided
* @param {object} [messageFunctions]
* @returns {CSN.Model} Returns the transformed input model

@@ -30,4 +31,6 @@ * @todo should be done by the compiler - Check associations for valid foreign keys

*/
function generateDrafts( csn, options, services ) {
const messageFunctions = makeMessageFunction(csn, options, 'for.odata');
function generateDrafts( csn, options, services, messageFunctions ) {
// TEMP(2024-02-26): Temporary! Umbrella uses this file directly in cds/lib/compile/for/drafts.js#L1
messageFunctions ??= makeMessageFunction(csn, options, 'odata-drafts');
const { error, info } = messageFunctions;

@@ -82,4 +85,3 @@ const {

// Nothing to do if already draft-enabled (composition traversal may have circles)
if ((artifact['@Common.DraftRoot.PreparationAction'] || artifact['@Common.DraftNode.PreparationAction']) &&
artifact.actions && artifact.actions.draftPrepare)
if (filterDict[artifactName])
return;

@@ -200,3 +202,2 @@

// Generate the actions into the draft-enabled artifact (only draft roots can be activated/edited)

@@ -203,0 +204,0 @@

@@ -6,6 +6,7 @@ 'use strict';

const {
applyTransformations, forEachDefinition, forEachMemberRecursively, implicitAs, cloneCsnNonDict, forEachMember, applyTransformationsOnNonDictionary,
applyTransformations, forEachDefinition, forEachMemberRecursively, implicitAs, forEachMember, applyTransformationsOnNonDictionary,
} = require('../../model/csnUtils');
const associations = require('../db/associations');
const backlinks = require('../db/backlinks');
const { cloneCsnNonDict } = require('../../model/cloneCsn');

@@ -12,0 +13,0 @@

@@ -5,3 +5,3 @@ 'use strict';

const { CompilerAssertion } = require('../../base/error');
const { cloneCsnNonDict, getUtils, isAspect } = require('../../model/csnUtils');
const { getUtils, isAspect } = require('../../model/csnUtils');
const transformUtils = require('../transformUtils');

@@ -19,2 +19,3 @@ const flattening = require('../db/flattening');

const { rewriteCalculatedElementsInViews, processCalculatedElementsInEntities } = require('../db/rewriteCalculatedElements');
const { cloneFullCsn } = require('../../model/cloneCsn');

@@ -37,3 +38,3 @@ /**

const csn = cloneCsnNonDict(model, options);
const csn = cloneFullCsn(model, options);
delete csn.vocabularies; // must not be set for effective CSN

@@ -40,0 +41,0 @@ messageFunctions.setModel(csn);

'use strict';
const {
cloneCsnNonDict, applyTransformations, applyTransformationsOnNonDictionary, cloneCsnDictionary, applyTransformationsOnDictionary,
applyTransformations,
applyTransformationsOnNonDictionary,
applyTransformationsOnDictionary,
} = require('../../model/csnUtils');
const { forEachKey } = require('../../utils/objectUtils');
const { cloneCsnDict, cloneCsnNonDict } = require('../../model/cloneCsn');

@@ -59,3 +62,3 @@ /**

if (!parent.elements)
parent.elements = cloneCsnDictionary(final.elements);
parent.elements = cloneCsnDict(final.elements);
delete parent.type;

@@ -106,3 +109,3 @@ }

if (!_parent.elements)
_parent.elements = cloneCsnDictionary(finalSub.elements);
_parent.elements = cloneCsnDict(finalSub.elements);
delete _parent.type;

@@ -109,0 +112,0 @@ stack.push( _parent );

@@ -5,4 +5,3 @@ 'use strict';

const transformUtils = require('./transformUtils');
const { cloneCsnNonDict,
forEachDefinition,
const { forEachDefinition,
forEachMemberRecursively,

@@ -22,3 +21,3 @@ applyTransformationsOnNonDictionary,

const enrichUniversalCsn = require('./universalCsn/universalCsnEnricher');
const flattening = require('./db/flattening');
const flattening = require('./odata/flattening');
const associations = require('./db/associations')

@@ -30,2 +29,4 @@ const expansion = require('./db/expansion');

const { addLocalizationViews } = require('./localized');
const { cloneFullCsn } = require('../model/cloneCsn');
const { csnRefs } = require('../model/csnRefs');

@@ -78,3 +79,3 @@ // Transformation for ODATA. Expects a CSN 'inputModel', processes it for ODATA.

// copy the model as we don't want to change the input model
let csn = cloneCsnNonDict(inputModel, options);
let csn = cloneFullCsn(inputModel, options);
messageFunctions.setModel(csn);

@@ -125,3 +126,3 @@

if (options.tenantAsColumn)
if (options.tenantDiscriminator)
addTenantFields(csn, options);

@@ -141,2 +142,5 @@

// Rewrite paths in annotations only if beta modes are set
options.enrichAnnotations = true;
const cleanup = validate.forOdata(csn, {

@@ -174,7 +178,7 @@ message, error, warning, info, inspectRef, effectiveType, getFinalTypeInfo, artifactRef,

// rendering which may has to publish external definitions
expandToFinalBaseType(csn, transformers, csnUtils, services, options);
expandToFinalBaseType(csn, transformers, csnUtils, services, options, error);
// - Generate artificial draft fields on a structured CSN if requested, flattening and struct
// expansion do their magic including foreign key generation and annotation propagation.
generateDrafts(csn, options, services);
generateDrafts(csn, options, services, messageFunctions);

@@ -190,17 +194,26 @@ // Check if structured elements and managed associations are compared in an expression

const resolved = new WeakMap();
// No refs with struct-steps exist anymore
flattening.flattenAllStructStepsInRefs(csn, options, messageFunctions, resolved, '_', { skipArtifact: isExternalServiceMember });
// No type references exist anymore
// Needs to happen exactly between flattenAllStructStepsInRefs and flattenElements to keep model resolvable.
// OData doesn't resolve type chains after the first 'items'
flattening.resolveTypeReferences(csn, options, messageFunctions, resolved, '_',
{ skip: [ 'action', 'aspect', 'event', 'function', 'type'],
skipArtifact: isExternalServiceMember, skipStandard: { items: true } });
// No structured elements exists anymore
flattening.flattenElements(csn, options, messageFunctions, '_',
{ skip: ['action', 'aspect', 'event', 'function', 'type'],
const { inspectRef, effectiveType } = csnRefs(csn);
const { adaptRefs, transformer: refFlattener } = flattening.getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, options, resolved, '_');
flattening.allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternalServiceMember, error, csnUtils, options);
flattening.flattenAllStructStepsInRefs(csn, refFlattener, adaptRefs,
{ //skip: ['action', 'aspect', 'event', 'function', 'type'],
skipArtifact: isExternalServiceMember,
skipStandard: { items: true }, // don't drill further into .items
skipDict: { actions: true } }); // don't drill further into .actions -> bound actions, action-artifacts are handled by skip
});
// replace structured with flat dictionaries that contain
// rewritten path expressions
forEachDefinition(csn, (def) => {
['elements', 'params'].forEach(dictName => {
if(def[`$${dictName}`])
def[dictName] = def[`$${dictName}`];
})
if(def.$flatAnnotations) {
Object.entries(def.$flatAnnotations).forEach(([an, av]) => {
def[an] = av;
})
}
});
}

@@ -260,3 +273,3 @@

// If we have a 'preserved dotted name' (i.e. we are a result of flattening), use that for the @cds.persistence.name annotation
member['@cds.persistence.name'] = getElementDatabaseNameOf(member._flatElementNameWithDots || memberName, options.sqlMapping, 'hana'); // hana to allow "hdbcds"
member['@cds.persistence.name'] = getElementDatabaseNameOf(member.$defPath?.slice(1).join('.') || memberName, options.sqlMapping, 'hana'); // hana to allow "hdbcds"
}

@@ -294,9 +307,11 @@

cleanup();
// Throw exception in case of errors
throwWithAnyError();
cleanup();
if (options.testMode) csn = cloneCsnNonDict(csn, options); // sort, keep hidden properties
timetrace.stop('OData transformation');
return csn;
//--------------------------------------------------------------------
// HELPER SECTION STARTS HERE
// Mark elements that are annotated with @odata.on.insert/update with the annotation @Core.Computed.

@@ -303,0 +318,0 @@ function annotateCoreComputed(node) {

'use strict';
const { setProp, isBetaEnabled } = require('../base/model');
const { cloneCsnNonDict,
forEachMemberRecursively, forAllQueries, applyTransformationsOnNonDictionary,
const { forEachMemberRecursively, forAllQueries, applyTransformationsOnNonDictionary,
getArtifactDatabaseNameOf, getElementDatabaseNameOf, isBuiltinType, applyTransformations,

@@ -35,2 +34,4 @@ isAspect, walkCsnPath, isPersistedOnDatabase

const { getDefaultTypeLengths } = require('../render/utils/common');
const { featureFlags } = require('./db/featureFlags');
const { cloneCsnNonDict, cloneFullCsn } = require('../model/cloneCsn');

@@ -112,6 +113,6 @@ // By default: Do not process non-entities/views

/** @type {CSN.Model} */
csn = cloneCsnNonDict(csn, options);
csn = cloneFullCsn(csn, options);
timetrace.stop('Clone CSN');
if (options.tenantAsColumn)
if (options.tenantDiscriminator)
addTenantFields(csn, options);

@@ -166,3 +167,4 @@

rewriteCalculatedElementsInViews(csn, options, csnUtils, pathDelimiter, messageFunctions);
if(csn.meta?.[featureFlags]?.$calculatedElements)
rewriteCalculatedElementsInViews(csn, options, csnUtils, pathDelimiter, messageFunctions);

@@ -203,3 +205,2 @@ // Needs to happen before tuple expansion, so the newly generated WHERE-conditions have it applied

// TODO: Instead of 3 separate applyTransformations, we could have each of them just return the "listeners", merge them into

@@ -402,14 +403,16 @@ // one big listener that then gets passed into one single applyTransformations. Each listener would then have to return an array of callbacks to call.

function killParent(parent, a, b, path){
if(path.length > 2) {
const tail = path[path.length-1];
const parentPath = path.slice(0, -1)
const parentParent = walkCsnPath(csn, parentPath);
delete parentParent[tail];
} else {
delete parent.$ignore;
}
}
const killers = {
// Used to ignore actions etc from processing and remove associations/elements
'$ignore': function (parent, a, b, path){
if(path.length > 2) {
const tail = path[path.length-1];
const parentPath = path.slice(0, -1)
const parentParent = walkCsnPath(csn, parentPath);
delete parentParent[tail];
} else {
delete parent.$ignore;
}
},
'$ignore': killParent,
// Still used in flattenStructuredElements - in db/flattening.js

@@ -450,4 +453,8 @@ '_flatElementNameWithDots': killProp,

applyTransformations(csn, killers, [], { skipIgnore: false});
if(options.sqlDialect === 'hana' && options.withHanaAssociations === false && doA2J) {
killers.target = killParent;
}
applyTransformations(csn, killers, [], { skipIgnore: false });
redoProjections.forEach(fn => fn());

@@ -457,3 +464,2 @@

timetrace.stop('HANA transformation');
return csn;

@@ -558,11 +564,12 @@

// have already been reported.
let selectDepth = 0;
traverseQuery(artifact.query, null, null, (query, fromSelect) => {
if (!query.ref && !query.as && fromSelect) {
// Use +1; for UNION, it's the next select, for SELECT, it's increased later.
query.as = `$_select_${selectDepth + 1}__`;
}
if (query.SELECT)
++selectDepth;
});
if(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds') {
let selectDepth = 0;
traverseQuery(artifact.query, null, null, (query, fromSelect) => {
if (!query.ref && !query.as && fromSelect) {
// Use +1; for UNION, it's the next select, for SELECT, it's increased later.
query.as = `$_select_${selectDepth + 1}__`;
}
if (query.SELECT) ++selectDepth;
});
}

@@ -569,0 +576,0 @@ const process = (parent, prop, query, path) => {

@@ -8,4 +8,2 @@ 'use strict';

const {
cloneCsnDictionary,
cloneCsnNonDict,
applyAnnotationsFromExtensions,

@@ -15,5 +13,10 @@ forEachDefinition,

forAllQueries,
} = require('../model/csnUtils');
const { CompilerAssertion } = require('../base/error');
const {
cloneCsnDict,
cloneCsnNonDict,
sortCsnDefinitionsForTests,
} = require('../model/csnUtils');
const {CompilerAssertion} = require('../base/error');
sortCsn
} = require('../model/cloneCsn');

@@ -109,3 +112,3 @@ /**

function _addLocalizationViews(csn, options, config) {
const messageFunctions = makeMessageFunction(csn, options);
const messageFunctions = makeMessageFunction(csn, options, 'localized');
if (checkExistingLocalizationViews(csn, options, messageFunctions))

@@ -123,3 +126,3 @@ return csn;

applyAnnotationsForLocalizedViews();
sortCsnDefinitionsForTests(csn, options);
sortLocalizedForTests(csn, options);
messageFunctions.throwWithError();

@@ -208,3 +211,3 @@ return csn;

},
elements: cloneCsnDictionary(entity.elements, options),
elements: cloneCsnDict(entity.elements, options),
[_isViewForEntity]: true,

@@ -288,3 +291,3 @@ };

convenienceView.elements = cloneCsnDictionary(view.elements, options);
convenienceView.elements = cloneCsnDict(view.elements, options);
convenienceView[_isViewForView] = true;

@@ -298,3 +301,3 @@ copyLocation(convenienceView, view);

if (view.params)
convenienceView.params = cloneCsnDictionary(view.params, options);
convenienceView.params = cloneCsnDict(view.params, options);

@@ -852,2 +855,19 @@ return convenienceView;

/**
* For tests (testMode), sort the generated localized definitions, i.e. their props,
* but also sort 'csn.definitions' if requested.
*
* @param {CSN.Model} csn
* @param {CSN.Options} options
*/
function sortLocalizedForTests(csn, options) {
if (options.testMode) {
for (const defName in csn.definitions) {
if (defName.startsWith('localized.'))
csn.definitions[defName] = sortCsn(csn.definitions[defName], options);
}
}
sortCsnDefinitionsForTests(csn, options);
}
module.exports = {

@@ -854,0 +874,0 @@ addLocalizationViews,

'use strict';
const { isBetaEnabled } = require('../../base/model');
const { setProp, isBetaEnabled } = require('../../base/model');
const {
forEachDefinition, forEachGeneric, forEachMemberRecursively,
isBuiltinType, cloneCsnDictionary, cloneCsnNonDict,
transformExpression,
forEachDefinition,
forEachGeneric,
forEachMemberRecursively,
isBuiltinType,
findAnnotationExpression,
} = require('../../model/csnUtils');
const { isArtifactInSomeService, isArtifactInService } = require('./utils');
const { cloneCsnDict, cloneCsnNonDict } = require('../../model/cloneCsn');
function expandToFinalBaseType(csn, transformers, csnUtils, services, options) {
function expandToFinalBaseType(csn, transformers, csnUtils, services, options, error) {
const isV4 = options.odataVersion === 'v4';

@@ -15,3 +20,3 @@ const special$self = !csn?.definitions?.$self && '$self';

// Unravel derived type chains to final one for elements, actions, action parameters (propagating annotations)
forEachMemberRecursively(def, (member) => {
forEachMemberRecursively(def, (member, _memberName) => {
expandToFinalBaseType(member, defName);

@@ -89,3 +94,3 @@ expandToFinalBaseType(member.items, defName);

if (finalBaseType?.elements) {
def.items.elements = cloneCsnDictionary(finalBaseType.elements, options);
def.items.elements = cloneCsnDict(finalBaseType.elements, options);
delete def.items.type;

@@ -143,3 +148,3 @@ }

else if (csnUtils.isStructured(finalBaseType)) {
cloneElements(finalBaseType);
cloneElements(node, finalBaseType);
}

@@ -175,4 +180,4 @@ else if (node.type && node.items)

}
else if (node.type && node.type.ref) {
cloneElements(finalBaseType);
else if (node.type.ref) {
cloneElements(node, finalBaseType);
}

@@ -196,13 +201,18 @@ }

function cloneElements(finalBaseType) {
// cloneCsn only works correctly if we start "from the top"
function cloneElements(node, finalBaseType) {
let clone;
// do the clone only if really needed
if((finalBaseType.items && !node.items) ||
(finalBaseType.elements && !node.elements))
clone = cloneCsnNonDict({ definitions: { 'TypeDef': finalBaseType } }, options);
(finalBaseType.elements && !node.elements)) {
// use the 'real' type, don't clone a clone
let _type = node._type;
while(_type._type && !_type.items && !_type.elements)
_type = _type._type;
clone = cloneCsnNonDict(_type, { ...options, hiddenPropertiesToClone: [ '_type' ] });
fitClonedElementsIntoParent(clone, node, _type.$path);
}
if (finalBaseType.items) {
delete node.type;
if(!node.items)
Object.assign(node, { items: clone.definitions.TypeDef.items });
Object.assign(node, { items: clone.items });
}

@@ -213,6 +223,53 @@ if (finalBaseType.elements) {

if(!node.elements)
Object.assign(node, { elements: clone.definitions.TypeDef.elements });
Object.assign(node, { elements: clone.elements });
}
}
function fitClonedElementsIntoParent(clone, node, typeRefCsnPath) {
const f = (p) => {
const [h, ...t ] = p;
return `${h}:${t.join('.')}`;
}
const typeRefStr = f(node.type.ref);
const typeRefRootPath = $path2path(typeRefCsnPath);
forEachMemberRecursively(clone, (elt, eltName, prop, location) => {
const [ xprANames /*nxprANames */ ] = Object.keys(elt).reduce((acc, pn) => {
if (pn[0] === '@')
acc[findAnnotationExpression(elt, pn) ? 0 : 1].push(pn);
return acc;
}, [ [], [] ]);
const usingPositionStr = f($path2path(location));
const eltRootPath = $path2path(elt.$path);
xprANames.forEach(xprAName => {
//let isSubTreeSpan = true;
transformExpression(elt, xprAName, {
ref: (parent, prop, xpr, csnPath) => {
let prefixMatch = true;
const head = xpr[0].id || xpr[0];
if (head === '$self') {
for (let i = 1; i < typeRefRootPath.length && prefixMatch; i++){
prefixMatch = (xpr[i].id || xpr[i]) === typeRefRootPath[i];
}
if(prefixMatch && xpr.length > typeRefRootPath.length) {
parent[prop] = [ ...xpr.slice(eltRootPath.length-1)];
}
else {
error('odata-anno-xpr-ref', csnPath,
{ anno: xprAName, elemref: xpr.join('.'), name: usingPositionStr, code: typeRefStr });
//isSubTreeSpan = false;
}
}
},
}, elt.$path);
// if(!isSubTreeSpan) {
// // remove annotation and sub annotations from cloned element
// delete elt[xprAName];
// nxprANames.filter(an => an.startsWith(`${xprAName}.`)).forEach((nxprAName) => {
// delete elt[nxprAName];
// });
// }
});
setProp(elt, '$path', location);
}, node.$path);
}
/*

@@ -236,2 +293,25 @@ Check, if a type needs to be expanded into the service

}
// convert $path to path starting at main artifact
function $path2path( p ) {
const path = [];
let env = csn;
for (let i = 0; p && env && i < p.length; i++) {
const ps = p[i];
env = env[ps];
if (env && env.constructor === Object) {
path.push(ps);
// jump over many items but not if this is an element
if (env.items) {
env = env.items;
if (p[i + 1] === 'items')
i++;
}
if (env.type && !isBuiltinType(env.type) && !env.elements)
env = csn.definitions[env.type];
}
}
return path;
}
}

@@ -238,0 +318,0 @@ }

@@ -11,6 +11,6 @@ 'use strict';

const { defNameWithoutServiceOrContextName, isArtifactInService } = require('./utils');
const { getNamespace, copyAnnotations, cloneCsnNonDict,
isBuiltinType, isAnnotationExpression,
forEachDefinition, forEachMember, forEachGeneric } = require('../../model/csnUtils');
const { getNamespace, copyAnnotations, isBuiltinType, findAnnotationExpression,
forEachDefinition, forEachMember, forEachGeneric, isEdmPropertyRendered } = require('../../model/csnUtils');
const { CompilerAssertion } = require('../../base/error');
const { cloneCsnNonDict } = require('../../model/cloneCsn');

@@ -148,3 +148,3 @@ /**

*/
function exposeTypeOf(node, isKey, memberName, defName, serviceName, newTypeName, path, parentName, isTermDef=false) {
function exposeTypeOf(node, isKey, memberName, defName, serviceName, newTypeName, path, parentName, isTermDef=false, ignoreInAPI=false) {
const { isExposable, typeDef, typeName, elements, isAnonymous } = isTypeExposable();

@@ -188,6 +188,8 @@ if (isExposable) {

// if using node enforces open/closed, set it on type
if(node['@open'] !== undefined)
if (node['@open'] !== undefined)
newType['@open'] = node['@open']
if (node.$location)
setProp(newType, '$location', node.$location);
ignoreInAPI ||= !isEdmPropertyRendered(node, options);
setProp(newType, '$ignoreInAPI', ignoreInAPI);

@@ -204,3 +206,3 @@ csn.definitions[fullQualifiedNewTypeName] = newType;

const { isExposable, typeDef, typeName } = exposeTypeOf(newElem, isKey, elemName, defName, serviceName,
getNewTypeName(newElem, elemName, newTypeName, serviceName), path, fullQualifiedNewTypeName, isTermDef);
getNewTypeName(newElem, elemName, newTypeName, serviceName), path, fullQualifiedNewTypeName, isTermDef, ignoreInAPI);
// if the type for the newElem was not exposed it may be a scalar type def from an external service that hasn't

@@ -230,3 +232,3 @@ // been caught by expandToFinalBaseType() (forODataNew must not modify external imported services)

if (pn[0] === '@')
acc[isAnnotationExpression(typeDef[pn]) ? 0 : 1].push(pn);
acc[findAnnotationExpression(typeDef, pn) ? 0 : 1].push(pn);
return acc;

@@ -233,0 +235,0 @@ }, [ [], [] ]);

@@ -10,6 +10,7 @@ 'use strict';

const { copyAnnotations, applyTransformations, isDollarSelfOrProjectionOperand } = require('../model/csnUtils');
const { cloneCsnNonDict, cloneCsnDictionary, getUtils } = require('../model/csnUtils');
const { getUtils } = require('../model/csnUtils');
const { typeParameters, isBuiltinType } = require('../compiler/builtins');
const { ModelError, CompilerAssertion} = require('../base/error');
const { forEach } = require('../utils/objectUtils');
const { cloneCsnNonDict, cloneCsnDict } = require('../model/cloneCsn');

@@ -345,3 +346,3 @@ const RestrictedOperators = ['<', '>', '>=', '<='];

let i = scope === '$self' ? 1 : 0;
// read property from resolved path link

@@ -352,3 +353,3 @@ const art = (propName) =>

(resolvedLinkTypes.get(links[i])||{})[propName]);
let flattenStep = false;

@@ -376,3 +377,3 @@ let nextIsItems = !!art('items') || (refParentIsItems && i === 0);

flattenStep = !links[i].art?.kind &&
flattenStep = !links[i].art?.kind &&
!links[i].art?.SELECT &&

@@ -448,3 +449,3 @@ !links[i].art?.from &&

if (typeRef.elements && !(options.transformation === 'hdbcds' && options.sqlMapping === 'hdbcds'))
nodeWithType.elements = cloneCsnDictionary(typeRef.elements, options);
nodeWithType.elements = cloneCsnDict(typeRef.elements, options);
else if (typeRef.items)

@@ -457,3 +458,3 @@ nodeWithType.items = cloneCsnNonDict(typeRef.items, options);

if (typeRef.enum && nodeWithType.enum === undefined)
nodeWithType.enum = cloneCsnDictionary(typeRef.enum, options);
nodeWithType.enum = cloneCsnDict(typeRef.enum, options);

@@ -460,0 +461,0 @@ // Copy type and type arguments (+ localized)

@@ -73,2 +73,15 @@ 'use strict';

/**
* Sets a property as "hidden" (a.k.a. non-enumerable).
*
* @param {object} obj
* @param {string} prop
* @param {any} val
*/
function setHidden( obj, prop, val ) {
Object.defineProperty( obj, prop, {
value: val, configurable: true, writable: true, enumerable: false,
} );
}
module.exports = {

@@ -80,2 +93,3 @@ copyPropIfExist,

forEachKey,
setHidden,
};
{
"name": "@sap/cds-compiler",
"version": "4.6.2",
"version": "4.7.4",
"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

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 not supported yet

Sorry, the diff of this file is not supported yet

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

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc