Socket
Socket
Sign inDemoInstall

@sap/cds-compiler

Package Overview
Dependencies
Maintainers
1
Versions
106
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sap/cds-compiler - npm Package Compare versions

Comparing version 1.35.0 to 1.39.0

doc/CHANGELOG_BETA.md

6

bin/cdsc.js

@@ -122,5 +122,8 @@ #!/usr/bin/env node

'assocsWithParams': true,
'uniqueconstraints': true,
'cleanCsn': false,
'hanaAssocRealCardinality': true,
'originalKeysForTemporal': true,
'odataDefaultValues': true,
'mapAssocToJoinCardinality': true,
'dontRenderVirtualElements': true,
}

@@ -271,2 +274,3 @@ }

displayNamedXsnOrCsn(hanaResult._augmentedCsn, hanaResult.csn, 'hana_csn', options);
model.messages = hanaResult.messages;
return model;

@@ -273,0 +277,0 @@ }

2

bin/cdsse.js

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

function select( xsnOrErr ) {
const xsn = (xsnOrErr instanceof Error) ? xsnOrErr.model : xsnOrErr;
const xsn = (xsnOrErr instanceof Error) ? xsnOrErr['model'] : xsnOrErr;
if (!xsn)

@@ -100,0 +100,0 @@ return true;

@@ -6,5 +6,96 @@ # ChangeLog for cdx compiler and backends

Note: `beta` fixes, changes and features are usually not listed in this ChangeLog.
Note: `beta` fixes, changes and features are usually not listed in this ChangeLog but [here](doc/CHANGELOG_BETA.md).
The compiler behaviour concerning `beta` features can change at any time without notice.
## Version 1.39.0 - 2020-08-26
### Added
- If the first CDS source (CDL or CSN) provided to the compiler
has a `namespace` declaration/property, then
that namespace name is put into the property `namespace` of the returned CSN.
- An event payload type can now be defined with a type/entity reference or
or projection (instead of providing the elements directly).
- Aspects can now be included when specifying the elements of an event payload type,
as it is known for type, entity and aspect definitions.
### Fixed
- Fix a bug in explicit JOIN cardinality CDL parsing
- to.hdbcds/hdi/sql: Identifiers are checked and warnings are raised if the identifier exceeds a length limitation which would result in a deployment error.
- OData: Service, entity and element identifiers are checked and warnings are raised if an identifier is not compliant with the OData specification.
## Version 1.38.0 - 2020-08-25
### Changed
- CSN: The property `payload` of an `event` has been renamed to `elements`.
### Fixed
- to.hdbcds/hdi/sql: Correctly handle local-scope refs in on-conditions when flattening structures.
- Run checks for associations inside of `many` or `array of` only on entities and views.
## Version 1.37.0 - 2020-08-21
### Added
- Projections columns can now use expressions like select items,
both for `entity … as projection on` and `extend projection … with`.
- OData: `array of <structure>` or `many <structure>` is now allowed in OData V4, flat format.
### Changed
- to.hdbcds/hdi/sql:
+ Messages of id "query-no-undefined" are now raised as errors.
+ Aspects/types/abstract entities containing anonymous aspect compositions
must not be used as types and are removed during transformation.
### Fixed
- to.cdl: Events are rendered.
- to.cds: Anonymous aspect composition are now rendered correctly.
- to.hdbcds/hdi/sql:
+ Events are ignored.
+ local-scope references in on-conditions are now handled correctly during flattening.
+ Removed duplicate messages.
- A model with multilevel `composition of <aspect>` (spread across several aspect declarations,
composing one another) is now processed successfully with the OData backend.
- The CSN parser supports explicit join cardinalities.
- Prefix a `@assert.unique` table constraint with the table name to avoid name clashes.
## Version 1.36.0 - 2020-08-07
### Added
- Query select items can now be declared to be virtual.
- CQL now allows to specify a join cardinality. Allowed are any combinations of
`{ [ EXACT ] ONE | MANY } TO { [ EXACT ] ONE | MANY }` for
`{ INNER | { LEFT | RIGHT | FULL } [ OUTER ] }` joins.
The cardinality is added in the for HANA generated `CREATE VIEW` statements.
- Support the creation of unique constraints by assigning `@assert.unique.<constraintName>` to
non-query entities or query entities annotated with `@cds.persistence.table`. The value of the
annotation is an array of paths referring to elements in the entity. The path leaf may
be an element of a scalar, structured or managed association type. Individual foreign keys or
unmanaged associations can not be accessed. In case the path points to a structured element,
the unique constraint will contain all columns stemming from the structured type. In case
the path points to a managed association, the unique constraint will contain all foreign key
columns stemming from this managed association.
For HANA a `UNIQUE INVERTED INDEX` and for SQLite a `named unique table constraint` is generated.
### Changed
- OData: Update vocabularies 'Common', 'UI'
- The association to join transformation treats foreign key accesses with priority. If a foreign key
of a managed association can be accessed without joins, no joins are generated.
The priority handling can be turned of with option `joinfk`.
### Fixed
- Semantic location in messages is now more precise.
## Version 1.35.0 - 2020-07-31

@@ -11,0 +102,0 @@

@@ -26,2 +26,3 @@ /** @module API */

const relevantOdataOptions = [ 'sqlMapping', 'odataFormat' ];
const relevantSqlOptions = [ 'sqlMapping', 'sqlDialect' ];
const warnAboutMismatchOdata = [ 'odataVersion' ];

@@ -178,4 +179,13 @@

cloneCsnMessages(csn, options, internalOptions);
const intermediateResult = backends.toSqlWithCsn(csn, internalOptions);
let intermediateResult;
if (isBetaEnabled(options, 'pretransformedCSN') && isPreTransformed(csn, 'sql')) {
checkPreTransformedCsn(csn, internalOptions, relevantSqlOptions, warnAboutMismatchOdata);
intermediateResult = backends.renderSqlWithCsn(csn, internalOptions);
}
else {
intermediateResult = backends.toSqlWithCsn(csn, internalOptions);
}
processMessages(intermediateResult, options);

@@ -182,0 +192,0 @@

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

'sqlMapping',
'joinfk',
'magicVars',

@@ -23,3 +24,5 @@ // ODATA

'odataContainment',
'odataForeignKeys',
'odataProxies',
'odataDefaultValues',
'service',

@@ -30,2 +33,3 @@ ];

const privateOptions = [
'renderVirtualElements',
'lintMode',

@@ -32,0 +36,0 @@ 'fuzzyCsnError',

@@ -724,2 +724,26 @@ 'use strict';

timetrace.start('toSqlWithCsn');
const transformedOptions = transformSQLOptions(model, options);
options = transformedOptions.options;
const forSqlCsn = transformForHanaWithCsn(model, mergeOptions(options, { forHana : transformedOptions.forHanaOptions } ));
// Assemble result
/** @type {object} */
let result = {};
if (options.toSql.src) {
result = toSqlDdl(forSqlCsn, forSqlCsn.options);
}
if (options.toSql.csn) {
result.csn = options.testMode ? sortCsn(forSqlCsn) : forSqlCsn;
}
// Transfer warnings (errors would have resulted in an exception before we come here)
if (forSqlCsn.messages && forSqlCsn.messages.length > 0) {
result.messages = forSqlCsn.messages;
}
timetrace.stop();
return result;
}
function transformSQLOptions(model, options){
// when toSql is invoked via the CLI - toSql options are under model.options

@@ -791,21 +815,4 @@ // ensure the desired format of the user option

let forSqlCsn = transformForHanaWithCsn(model, mergeOptions(options, { forHana : forHanaOptions } ));
return {options, forHanaOptions};
// Assemble result
/** @type {object} */
let result = {};
if (options.toSql.src) {
result = toSqlDdl(forSqlCsn, forSqlCsn.options);
}
if (options.toSql.csn) {
result.csn = options.testMode ? sortCsn(forSqlCsn) : forSqlCsn;
}
// Transfer warnings (errors would have resulted in an exception before we come here)
if (forSqlCsn.messages && forSqlCsn.messages.length > 0) {
result.messages = forSqlCsn.messages;
}
timetrace.stop();
return result;
// If among the options user, user.id or user.locale are specified via the CLI or

@@ -819,3 +826,2 @@ // via the API, then ensure that at the end there is a user option, which is an object and has(have)

}
// move the locale option(if provided) under user.locale

@@ -828,5 +834,16 @@ if (options.locale) {

}
}
}
/**
* Render the given CSN - assuming that it was correctly transformed for toSQL
* @param {CSN.Model} csn SQL-transformed CSN
* @param {object} options Options - same as for toSQLWithCSN
*/
function renderSqlWithCsn(csn, options){
const transformedOptions = transformSQLOptions(csn, options);
options = transformedOptions.options;
// Make the options passed to the renderer just like the original toSQLWithCSN
return toSqlDdl(csn, mergeOptions(options, { forHana : transformedOptions.forHanaOptions } ));
}
// ----------- toRename -----------

@@ -996,2 +1013,3 @@

toSqlWithCsn,
renderSqlWithCsn,
toCsn,

@@ -998,0 +1016,0 @@ getDefaultBackendOptions,

const { CompileMessage, DebugCompileMessage } = require('../base/messages');
const { analyseCsnPath, traverseQuery } = require('../model/csnRefs');

@@ -60,17 +61,201 @@ /**

/** @param {CSN.Path} semanticLocation */
function beautifySemanticLocation(semanticLocation){
if(!semanticLocation){
return semanticLocation;
/** @param {CSN.Path} csnPath */
function beautifySemanticLocation(csnPath){
if(!csnPath){
return csnPath;
}
const copy = [...semanticLocation];
return constructSemanticLocation([...csnPath], model);
}
}
const art = model.definitions[copy[1]];
copy[1] = ((art && art.kind) ? art.kind : 'artifact') + ':' + quoted(copy[1])
function constructSemanticLocation(csnPath, model) {
const csnDictionaries = [
'args', 'params', 'enum', 'mixin', 'elements', 'actions', 'definitions',
];
let { query } = analyseCsnPath(
csnPath,
model
);
return copy.slice(1).join('/');
// remove definitions
csnPath.shift();
const artName = csnPath.shift();
let currentThing = model.definitions[artName];
let result = ((currentThing && currentThing.kind) ? currentThing.kind : 'artifact') + ':' + quoted(artName);
if(query) {
query = queryDepth(currentThing.query, query);
}
let elements = [];
let inCsnDict, inElement, inAction, inParam, inKeys, inRef, inEnum, inQuery, inColumn, inMixin, inItems = false;
for(const [index, step] of csnPath.entries()){
currentThing = currentThing[step];
if(csnDictionaries.includes(step) && !inCsnDict) {
inCsnDict = true;
switch (step) {
case 'elements':
if(!inElement) {
inElement = true;
}
break;
case 'actions':
inAction = true;
break;
case 'params':
inParam = true;
break;
case 'enum':
inElement = false;
inEnum = true;
break
case 'mixin':
inMixin = true;
inQuery = false;
break;
default:
if (inElement) {
// close element
result += element();
inElement = false;
}
}
}
else if( inQuery ){
if(step === 'SELECT') {
if(!csnPath[index + 1]) {
result += select();
} else if (['from', 'where', 'groupBy', 'having', 'orderBy', 'limit', 'offset'].includes(csnPath[index + 1]) && !csnPath[index + 2]) {
let clause = csnPath[index + 1];
result += select();
result += '/' + clause;
}
}
else if(step === 'columns'){
result += select();
result += '/column';
inColumn = true;
inQuery = false;
}
}
else if( inMixin ) {
if(step === 'on') {
result += '/on';
break;
} else {
result += selectAndMixin(step);
}
}
else if(inEnum) {
result += elementAndEnum(step);
}
else if(!inElement && step === 'query'){
inQuery = true;
}
else if(inElement && step === 'keys') {
// close element
result += element() + '/key';
inElement = false;
inKeys = true;
}
else if(inElement && step === 'on') {
// close element
result += element() + '/on';
inElement = false;
break;
}
else if(inElement && step === 'items') {
// this is an element called items
if(csnPath[index - 1] === 'elements' && elements[elements.length - 1] !== 'elements'){
elements.push(step);
}
else {
inElement = false;
inItems = true;
}
}
else if(inElement && step === 'elements') {
// this is an element called elements
if(csnPath[index - 1] === 'elements'){
elements.push(step);
}
}
else if(inItems && step === 'elements') {
inElement = true;
inItems = false;
}
else if( inKeys || inColumn){
if (typeof step === 'number') {
if(currentThing.as) {
result += ':' + quoted(currentThing.as);
}else {
result += inRef ? ':' + quoted(currentThing) : currentThing.ref ? ':' + quoted(currentThing.ref.join('.')) : '';
}
break;
}
if( step === 'ref'){
inRef = true;
}
}
else if(inAction && step === 'returns') {
result += '/' + step;
break;
}
else if(inCsnDict) {
if (inElement)
elements.push(step);
else if(inParam){
result += param(step);
} else if(inAction){
result += func(step);
}
inCsnDict = false;
}
}
if( inElement ) result += element();
if( inItems ) result += element() + '/items';
return result;
function select() {
let s = '/select';
s += query.isOnlySelect ? '' : ':' + query.depth;
return s;
}
function selectAndMixin(name) {
return `${select()}/mixin:${quoted(name)}`;
}
function element() {
return `/element:${quoted(elements.join('.'))}`;
}
function param(name) {
return `/param:${quoted(name)}`;
}
function func(name) {
return `/function:${quoted(name)}`;
}
function elementAndEnum(name) {
return `${element()}/enum:${quoted(name)}`;
}
/**
* Traverse rootQuery until targetQuery is found and count the depth,
* check if targetQuery is only select in entity.
*/
function queryDepth (rootQuery, targetQuery) {
let targetQueryDepth = 1;
let totalQueryDepth = 0;
let isFound = false;
traverseQuery(rootQuery, null, function countSelect(q, select) {
if( select ) totalQueryDepth += 1;
if( select && !isFound) targetQueryDepth += 1;
if(q === targetQuery) isFound = true;
});
return { depth: targetQueryDepth, isOnlySelect: totalQueryDepth === 1 };
}
}
function quoted( name ) {

@@ -77,0 +262,0 @@ return (name) ? '"' + name.replace( /"/g, '""' ) + '"' : '<?>'; // sync ";

@@ -179,2 +179,3 @@ // Functions and classes for syntax messages

this.location = normalizeLocation( location );
this.validNames = null;
if (home) // semantic location, e.g. 'entity:"E"/element:"x"'

@@ -211,2 +212,3 @@ this.home = home;

this.location = normalizeLocation( location );
this.validNames = null;
if (home) // semantic location, e.g. 'entity:"E"/element:"x"'

@@ -416,2 +418,18 @@ this.home = home;

/**
* Return message hash which is either the message string without the file location,
* or the full message string if no semantic location is provided.
*
* @param {CSN.Message} msg
* @returns {string} can be used to uniquely identify a message
*/
function messageHash(msg) {
// parser messages do not provide semantic location, therefore we need to use the file location
if(!msg.home)
return messageString(msg);
const copy = {...msg};
copy.location = undefined;
return messageString(copy);
}
/**
* Return message string with location if present.

@@ -538,16 +556,17 @@ *

function deduplicateMessages( messages ) {
const seen = new Set();
const uniqueMessages = messages.filter((msg) => {
if (!msg.location)
return true;
const hash = messageString(msg);
const keep = !seen.has(hash);
seen.add(hash);
return keep;
});
const seen = new Map();
for (const msg of messages) {
const hash = messageHash(msg);
const present = seen.has(hash);
if(!present){
seen.set(hash, msg);
} else if( msg.location && msg.location.end ) {
// message already present, replace only if current msg is the more precise one
if( msg.location.end.column > msg.location.start.column )
seen.set(hash, msg);
}
}
messages.length = 0;
for (const msg of uniqueMessages) {
messages.push(msg);
}
seen.forEach(msg => messages.push(msg));
}

@@ -554,0 +573,0 @@

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

function checkPrimaryKey(elem, model) {
const { error, signal } = alerts(model);
const message = getMessageFunction(model);
let type = '';

@@ -27,3 +27,3 @@ // apparently this is the resolved type (over an derived type chain)

if(element.onCond) {
signal(error`Unmanaged associations cannot be used as primary key`, elem.key.location, ['Error'], 'unmanaged-as-key');
message('unmanaged-as-key', elem.key.location, elem, {}, ['Error'], 'Unmanaged associations cannot be used as primary key');
}

@@ -47,3 +47,3 @@

if(element.items){
signal(error`Array-like types cannot be used as primary key`, element.location);
message(null, element.location, element, {}, 'Error', 'Array-like types cannot be used as primary key');
}

@@ -65,3 +65,3 @@

if (element._parent && element._parent.kind === 'element') {
signal('Keyword "localized" is ignored for sub elements', element.localized.location, 'Warning', 'localized-sub-element');
message('localized-sub-element', element.localized.location, element, {}, 'Warning', 'Keyword "localized" is ignored for sub elements');
}

@@ -72,3 +72,3 @@ }

if(['cds.hana.ST_POINT', 'cds.hana.ST_GEOMETRY'].includes(type)){
signal(error`Type ${type} cannot be used as primary key`, elem.location);
message(null, elem.location, elem, { type }, 'Error', 'Type $(TYPE) cannot be used as primary key');
}

@@ -391,5 +391,7 @@

// Min cardinality must be a non-negative number (already checked by parser)
if (elem.cardinality.targetMin) {
if (!(elem.cardinality.targetMin.literal === 'number' && elem.cardinality.targetMin.val >= 0)) {
signal(error`Element "${elem.name.absolute}.${elem.name.id}": Illegal value "${elem.cardinality.targetMin.val}" for min cardinality (must be a non-negative number)`, elem.cardinality.targetMin.location);
for(let prop of ['sourceMin', 'targetMin']) {
if (elem.cardinality[prop]) {
if (!(elem.cardinality[prop].literal === 'number' && elem.cardinality[prop].val >= 0)) {
signal(error`Element "${elem.name.absolute}.${elem.name.id}": Illegal value "${elem.cardinality[prop].val}" for min cardinality (must be a non-negative number)`, elem.cardinality[prop].location);
}
}

@@ -399,6 +401,9 @@ }

// If provided, min cardinality must not exceed max cardinality (note that '*' is considered to be >= any number)
if (elem.cardinality.targetMin && elem.cardinality.targetMax && elem.cardinality.targetMax.literal === 'number'
&& elem.cardinality.targetMin.val > elem.cardinality.targetMax.val) {
signal(error`Element "${elem.name.absolute}.${elem.name.id}": Target minimum cardinality must not be greater than target maximum cardinality`, elem.cardinality.location);
}
const pair = [[ 'sourceMin', 'sourceMax', 'Source'], [ 'targetMin', 'targetMax', 'Target' ]];
pair.forEach(p => {
if (elem.cardinality[p[0]] && elem.cardinality[p[1]] && elem.cardinality[p[1]].literal === 'number'
&& elem.cardinality[p[0]].val > elem.cardinality[p[1]].val) {
signal(error`Element "${elem.name.absolute}.${elem.name.id}": ${p[2]} minimum cardinality must not be greater than ${p[2].toLowerCase()} maximum cardinality`, elem.cardinality.location);
}
});
}

@@ -405,0 +410,0 @@

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

// TODO: Check if the on-condition only references things inside of the .items
this.signal(this.error`Unmanaged associations in "array of" or "many" are not allowed.`, element.$path)
this.signal(this.error`Unmanaged associations in "array of" or "many" are not allowed.`, member.$path)
}

@@ -33,3 +33,3 @@ }

}
if(member && member.items){
if(this.artifact && ( this.artifact.kind === 'entity' || this.artifact.query ) && member && member.items){
if(member.items.type && member.items.type.ref){

@@ -36,0 +36,0 @@ validate(this.artifactRef(member.items.type));

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

enum: dictionary,
payload: dictionary,
mixin: dictionary,
ref: pathRef,

@@ -35,3 +35,9 @@ type: simpleRef,

}
let cleanupCallbacks = [];
const cleanup = () => {
cleanupCallbacks.forEach(fn => fn());
cleanupCallbacks = [];
};
const { inspectRef, artifactRef } = csnRefs( csn );

@@ -41,3 +47,3 @@ const csnPath = [];

dictionary( csn, 'definitions', csn.definitions );
return csn;
return { csn, cleanup };

@@ -47,5 +53,6 @@ function standard( parent, prop, node ) {

return;
setProp(node, '$path', [...csnPath])
csnPath.push( prop );
setProp(node, '$path', [...csnPath])
cleanupCallbacks.push(() => delete node.$path);

@@ -66,9 +73,13 @@ if (node instanceof Array) {

setProp(node, '$path', [...csnPath])
cleanupCallbacks.push(() => delete node.$path);
csnPath.push( prop );
csnPath.push( prop );
for (let name of Object.getOwnPropertyNames( dict )) {
standard( dict, name, dict[name] );
}
if (!Object.prototype.propertyIsEnumerable.call( node, prop ))
if (!Object.prototype.propertyIsEnumerable.call( node, prop )){
setProp(node, '$' + prop, dict);
cleanupCallbacks.push(() => delete node['$' + prop]);
}
csnPath.pop();

@@ -79,10 +90,15 @@ }

setProp(node, '$path', [...csnPath])
cleanupCallbacks.push(() => delete node.$path);
let ref = node[prop];
if (typeof ref === 'string') {
const art = artifactRef( ref, null );
if (art || !ref.startsWith( 'cds.'))
if (art || !ref.startsWith( 'cds.')){
setProp(node, '_' + prop, art);
cleanupCallbacks.push(() => delete node['_' + prop]);
}
}
else if (Array.isArray( ref )) {
setProp(node,'_' + prop,ref.map( r => artifactRef( r, null ) ));
setProp(node, '_' + prop, ref.map( r => artifactRef( r, null ) ));
cleanupCallbacks.push(() => delete node['_' + prop]);
}

@@ -93,6 +109,14 @@ }

const { links, art, scope } = inspectRef( csnPath );
if (links) setProp(node, '_links', links);
if (art) setProp(node, '_art', art );
if (links) {
setProp(node, '_links', links);
cleanupCallbacks.push(() => delete node._links);
}
if (art) {
setProp(node, '_art', art );
cleanupCallbacks.push(() => delete node._art);
}
setProp(node, "$scope", scope)
cleanupCallbacks.push(() => delete node.$scope);
setProp(node, '$path', [...csnPath])
cleanupCallbacks.push(() => delete node.$path);

@@ -99,0 +123,0 @@ csnPath.push( prop );

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

function validate(csn, that, memberValidators=[], artifactValidators=[], queryValidators=[]){
enrich(csn);
const { cleanup } = enrich(csn);
forEachDefinition(csn, (artifact, artifactName, prop, path) => {
artifactValidators.forEach(artifactValidator => artifactValidator.bind(that)(artifact, artifactName, prop, path));
that.artifact = artifact;
if(memberValidators.length)

@@ -27,4 +28,6 @@ forEachMemberRecursively( artifact, memberValidators.map(v=>v.bind(that)), path );

});
return cleanup;
}
module.exports = validate;

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

optional: [
'on', 'kind', 'name',
'on', 'kind', 'name', 'cardinality',
'$tableAliases', 'queries', '$combined',

@@ -493,4 +493,5 @@ '_block', '_parent', '_main', '_leadingQuery', '_$next', '_deps',

requires: [ 'location' ],
optional: [ 'sourceMax', 'targetMin', 'targetMax' ],
optional: [ 'sourceMin', 'sourceMax', 'targetMin', 'targetMax' ],
},
sourceMin: { test: locationVal( isNumber ) },
sourceMax: { test: locationVal( isNumber ), also: [ '*' ] },

@@ -497,0 +498,0 @@ targetMin: { test: locationVal( isNumber ) },

@@ -150,3 +150,10 @@ // Compiler functions and utilities shared across all phases

function rejectNonStruct( art ) {
return ([ 'type', 'entity' ].includes( art.kind ) && art.elements && !art.query && !art.params)
// TODO - better
// - always Error: with params, with query, no elements
// - in aspect: Warning for not aspect
// - in type: Warning for not aspect or type
// - in entity: Warning for not aspect or entity
// - in event: Warning for not aspect or event (or entity?)
return ([ 'type', 'entity', 'event' ].includes( art.kind ) &&
art.elements && !art.query && !art.params)
? undefined

@@ -153,0 +160,0 @@ : 'expected-struct';

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

function checkOdataTerm(ns) {
const simpleIdentifiers = ns.split('.');
simpleIdentifiers.forEach((identifier) => {
if(!edmUtils.isSimpleIdentifier(identifier)){
message(signal.warning, context,
`OData annotation term "${identifier}" must consist of one or more dot separated simple identifiers (each starting with a letter or underscore, followed by at most 127 letters)`)
}
})
}
// termName may contain a qualifier: @UI.FieldGroup#shippingStatus

@@ -651,3 +661,8 @@ // -> remove qualifier from termName and set Qualifier attribute in newAnno

let termNameWithoutQualifiers = p[0];
if (p.length>1) {
if (p.length > 1) {
checkOdataTerm(p[0]);
if (!edmUtils.isSimpleIdentifier(p[1])) {
message(signal.warning, context,
`OData annotation qualifier "${p[1]}" must start with a letter or underscore, followed by at most 127 letters, underscores or digits`);
}
newAnno.Term = termNameWithoutQualifiers;

@@ -654,0 +669,0 @@ newAnno.Qualifier = p[1];

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

// create the complex types (don't render aspects by using $syntax hack until kind='aspect' is available)
edmUtils.foreach(csn.definitions, a => !(a.kind === 'aspect' || a.$syntax === 'aspect') && edmUtils.isStructuredType(a) && a.name.startsWith(serviceName + '.'), createComplexType);
edmUtils.foreach(csn.definitions, a => !(['aspect','event'].includes(a.kind) || a.$syntax === 'aspect') && edmUtils.isStructuredType(a) && a.name.startsWith(serviceName + '.'), createComplexType);

@@ -133,3 +133,3 @@ if(options.isV4())

// V4: No referential constraints for Containment Relationships
if(!np.isContainment() && !np.isToMany())
if((!np.isContainment() || (options.renderForeignKeys)) && !np.isToMany())
np.addReferentialConstraintNodes();

@@ -550,3 +550,3 @@ }

reuseAssoc = !!forwardAssocCsn._NavigationProperty;
constraints = edmUtils.getReferentialConstraints(forwardAssocCsn, signal, options.isFlatFormat);
constraints = edmUtils.getReferentialConstraints(forwardAssocCsn, signal, options);
constraints._multiplicity = edmUtils.determineMultiplicity(forwardAssocCsn);

@@ -553,0 +553,0 @@ }

@@ -7,2 +7,3 @@ // @ts-nocheck

const { isBuiltinType } = require('../model/csnUtils.js');
const { isBetaEnabled } = require('../base/model');

@@ -136,3 +137,3 @@ module.exports = function (options) {

if (typeof this[p] !== 'object')
tmpStr += ' ' + p + '="' + escapeString(this[p]) + '"'
tmpStr += ' ' + p + '="' + edmUtils.escapeString(this[p]) + '"'
}

@@ -142,18 +143,5 @@ for (let p in this._xmlOnlyAttributes)

if (typeof this._xmlOnlyAttributes[p] !== 'object')
tmpStr += ' ' + p + '="' + escapeString(this._xmlOnlyAttributes[p]) + '"'
tmpStr += ' ' + p + '="' + edmUtils.escapeString(this._xmlOnlyAttributes[p]) + '"'
}
return tmpStr;
function escapeString(s)
{
// first regex: replace & if not followed by apos; or quot; or gt; or lt; or amp; or #
// Do not escape > as it is a marker for {bi18n>...} translated string values
return (typeof s === 'string') ?
s.replace(/&(?!(?:apos|quot|[gl]t|amp);|#)/g, '&amp;')
//.replace(/>/g, '&gt;')
.replace(/</g, '&lt;')
.replace(/"/g, '&quot;')
.replace(/\r\n|\n/g, '&#xa;') :
s;
}
}

@@ -848,2 +836,7 @@

}
if (csn.default && isBetaEnabled(options, 'odataDefaultValues'))
this[`Default${this.v4 ? 'Value' : ''}`] = ['cds.Boolean', 'cds.Binary', 'cds.LargeBinary', 'cds.Integer64', 'cds.Integer'].includes(csn.type)
? csn.default.val
: edmUtils.escapeString(csn.default.val);
}

@@ -1241,10 +1234,4 @@

let xml = indent + '<' + kind + this.toXMLattributes();
xml += (this._value !== undefined ? '>' + escapeString(this._value) + '</' + kind + '>' : '/>');
xml += (this._value !== undefined ? '>' + edmUtils.escapeString(this._value) + '</' + kind + '>' : '/>');
return xml;
function escapeString(s)
{
// first regex: replace & if not followed by apos; or quot; or gt; or lt; or amp; or #
return (typeof s === 'string') ? s.replace(/&(?!(?:apos|quot|[gl]t|amp);|#)/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;') : s;
}
}

@@ -1251,0 +1238,0 @@

@@ -16,5 +16,7 @@ 'use strict';

isStructuredArtifact,
isEntityOrView,
isParameterizedEntityOrView,
isActionOrFunction,
getReferentialConstraints,
isSimpleIdentifier,
} = require('./edmUtils.js');

@@ -88,3 +90,3 @@

// create edmKeyRefPaths
foreach(csn.definitions, isStructuredArtifact, initializeEdmKeyRefPaths);
foreach(csn.definitions, isEntityOrView, initializeEdmKeyRefPaths);

@@ -94,2 +96,5 @@ // decide if an entity set needs to be constructed or not

// Check the artifact identifier for compliance with the odata specification
forAll(csn.definitions, checkArtifactIdentifier);
// 1. let all doc props become @Core.Descriptions

@@ -123,4 +128,20 @@ // 2. mark a member that will become a collection

function initializeService(service) {
checkServiceIdentifier(service.name);
setSAPSpecificV2AnnotationsToEntityContainer(options, service);
}
function checkServiceIdentifier(serviceName) {
if (serviceName.length > 511) {
// don't show long service name in message
signalIllegalIdentifier(false, ['definitions', serviceName], 'namespace', 'must not exceed 511 characters');
}
const simpleIdentifiers = serviceName.split('.');
simpleIdentifiers.forEach((identifier) => {
if (!isSimpleIdentifier(identifier)) {
// don't show long service name in message
signalIllegalIdentifier(false, ['definitions', serviceName], 'namespace',
'must consist of one or more dot separated simple identifiers (each starting with a letter or underscore, followed by at most 127 letters)');
}
});
}
// link association target to association and add @odata.contained to compositions in V4

@@ -201,2 +222,32 @@ function linkAssociationTarget(struct) {

function signalIllegalIdentifier(identifier, path, kind, msg) {
signal(signal.warning`OData ${kind} ${identifier ? `: "${identifier}"` : ''} ${ msg ? msg : 'must start with a letter or underscore, followed by at most 127 letters, underscores or digits' }`, path);
}
// Check the artifact identifier for compliance with the odata specification
function checkArtifactIdentifier(artifact) {
const serviceName = whatsMyServiceName(artifact.name);
if(serviceName) {
const artifactName = artifact.name.replace(serviceName + '.', '');
if(artifact.kind === 'action' || artifact.kind === 'function'){
checkActionOrFunctionIdentifier(artifact, artifactName);
} else if(!isSimpleIdentifier(artifactName)){
signalIllegalIdentifier(artifactName, ['definitions', artifact.name], 'entity name');
}
}
function checkActionOrFunctionIdentifier(actionOrFunction, actionOrFunctionName) {
if(!isSimpleIdentifier(actionOrFunctionName)){
signalIllegalIdentifier(actionOrFunctionName, actionOrFunction.$path, 'function or action name');
}
if(actionOrFunction.params) {
forAll(actionOrFunction.params, (param) => {
if(!isSimpleIdentifier(param.name)){
signalIllegalIdentifier(param.name, param.$path, 'function or action parameter name');
}
});
}
}
}
// Split an entity with parameters into two entity types with their entity sets,

@@ -338,2 +389,10 @@ // one named <name>Parameter and one named <name>Type. Parameter contains Type.

initElement(element, elementName, struct);
if(!isSimpleIdentifier(elementName)) {
signal(
signal.warning`OData property name: "${elementName}" must start with a letter or underscore, followed by at most 127 letters, underscores or digits`,
['definitions', struct.name, 'elements', elementName]
);
}
if(element['@cds.valid.key']) {

@@ -384,18 +443,21 @@ validKey.push(element);

*/
if(isContainerAssoc || options.isStructFormat)
/*
If this foreign key is NOT a container fk, let isEdmPropertyRendered() decide
Else, if fk is container fk, omit it if it wasn't requested in structured mode
*/
if((!isContainerAssoc && !isEdmPropertyRendered(element, options)) ||
(isContainerAssoc && !options.renderForeignKeys))
assignAnnotation(element, '@cds.api.ignore', true);
}
// if this is an explicitly containment ignore tagged element,
// ignore it if parent is containee
else {
if(element['@odata.containment.ignore'] &&
element._parent._containerEntity && element._parent._containerEntity.length)
assignAnnotation(element, '@cds.api.ignore', true);
}
// if this is an containment ignore tagged element,
// ignore it if option odataContainment is true and no foreign keys should be rendered
if(element['@odata.containment.ignore'] && options.odataContainment && !options.renderForeignKeys)
assignAnnotation(element, '@cds.api.ignore', true);
}
// it's an association
else if(element['@odata.containment.ignore'] &&
(element._isToContainer || element['@odata.contained'])) {
// if this is an association in the containee
// that is marked with containment ignore then don't render it
else if(element['@odata.containment.ignore'] && options.odataContainment && !options.renderForeignKeys) {
// if this is an explicitly containment ignore tagged association,
// ignore it if option odataContainment is true and no foreign keys should be rendered
assignAnnotation(element, '@odata.navigable', false);

@@ -412,29 +474,37 @@ }

// if artifact has a cds.valid.key make this the only primary key and
// add all @cds.valid.from + original primary keys as alternate keys
// @Core.AlternateKeys: [{ Key: [ { Name: 'slID', Alias: 'slID' }, { Name: 'validFrom', Alias: 'validFrom'} ] }]
if(validKey.length) {
let altKeys = [{ Key: [] }];
forAll(keys, (k, kn) => {
altKeys[0].Key.push( { Name: kn, Alias: kn } );
delete k.key;
});
validFrom.forEach(e => {
altKeys[0].Key.push( { Name: e.name, Alias: e.name } );
});
assignAnnotation(struct, '@Core.AlternateKeys', altKeys);
keys = Object.create(null);
validKey.forEach(e => {
e.key = true;
keys[e.name] = e;
});
if(isBetaEnabled(options, 'originalKeysForTemporal')) {
// if artifact has a cds.valid.key mention it as @Core.AlternateKey
if(validKey.length) {
let altKeys = [{ Key: [] }];
validKey.forEach(vk => altKeys[0].Key.push( { Name: vk.name, Alias: vk.name } ) );
assignAnnotation(struct, '@Core.AlternateKeys', altKeys);
}
}
else {
validFrom.forEach(e => {
e.key = true;
keys[e.name] = e;
});
// if artifact has a cds.valid.key make this the only primary key and
// add all @cds.valid.from + original primary keys as alternate keys
// @Core.AlternateKeys: [{ Key: [ { Name: 'slID', Alias: 'slID' }, { Name: 'validFrom', Alias: 'validFrom'} ] }]
if(validKey.length) {
let altKeys = [{ Key: [] }];
forAll(keys, (k, kn) => {
altKeys[0].Key.push( { Name: kn, Alias: kn } );
delete k.key;
});
validFrom.forEach(e => {
altKeys[0].Key.push( { Name: e.name, Alias: e.name } );
});
assignAnnotation(struct, '@Core.AlternateKeys', altKeys);
keys = Object.create(null);
validKey.forEach(e => {
e.key = true;
keys[e.name] = e;
});
}
else {
validFrom.forEach(e => {
e.key = true;
keys[e.name] = e;
});
}
}
assignProp(struct, '_SetAttributes', Object.create(null));

@@ -487,3 +557,3 @@ assignProp(struct, '$keys', keys);

if (element._ignore) return;
setProp(element, '_constraints', getReferentialConstraints(element, signal, options.isFlatFormat));
setProp(element, '_constraints', getReferentialConstraints(element, signal, options));

@@ -511,10 +581,8 @@ // only in V2 we must set the target cardinality of the backlink to the forward:

}
function whatsMyServiceName(n) {
return serviceNames.reduce((rc, sn) => n.startsWith(sn + '.') ? rc = sn : rc, undefined);
}
// For defining service create proxy target, if original target is outside of defining service
function redirectDanglingAssociationsToProxyTargets(struct) {
function whatsMyServiceName(n) {
return serviceNames.reduce((rc, sn) => n.startsWith(sn + '.') ? rc = sn : rc, undefined);
}
let myServiceName = whatsMyServiceName(struct.name);

@@ -713,2 +781,5 @@ // if this artifact is a service member check its associations

establish the containment (_isToContainer=tue).
* If in Structured V4, 'odataForeignKeys' is true, render all @foreignKey4,
and do not render associations (this will include the foreign keys of
the _isToContainer association).
*/

@@ -718,8 +789,15 @@ function initializeEdmKeyRefPaths(struct) {

// for all key elements that shouldn't be ignored produce the paths
foreach(struct.$keys, kn => !kn._ignore && !kn._isToContainer, (k, kn) => {
foreach(struct.$keys, k => !k._ignore && !k._isToContainer, (k, kn) => {
if(isEdmPropertyRendered(k, options) &&
!(options.isV2() && k['@Core.MediaType'])) {
if(options.isV4() && options.isStructFormat)
if(options.isV4() && options.isStructFormat) {
// This is structured OData ONLY
struct.$edmKeyPaths.push(...produceKeyRefPaths(k, kn));
// if the foreign keys are explictly requested, ignore associations and use the flat foreign keys instead
if(options.renderForeignKeys && !k.target)
struct.$edmKeyPaths.push([kn]);
// else produce paths (isEdmPropertyRendered() has filtered @odata.foreignKey4 already)
else if(!options.renderForeignKeys)
struct.$edmKeyPaths.push(...produceKeyRefPaths(k, kn));
}
// In v2/v4 flat, associations are never rendered
else if(!k.target) {

@@ -726,0 +804,0 @@ struct.$edmKeyPaths.push([kn]);

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

@@ -20,2 +20,10 @@ /* eslint max-statements-per-line:off */

options.odataContainment = options.toOdata.odataContainment;
if(options.toOdata.odataForeignKeys)
options.odataForeignKeys = options.toOdata.odataForeignKeys;
// global flag that indicates wether or not FKs shall be rendered in general
// V2/V4 flat: yes
// V4/struct: dending on odataForeignKyes
options.renderForeignKeys =
options.version === 'v4' ? options.odataFormat === 'structured' && !!options.odataForeignKeys : true;
}

@@ -140,3 +148,3 @@

function getReferentialConstraints(assocCsn, signal, isFlatFormat)
function getReferentialConstraints(assocCsn, signal, options)
{

@@ -175,7 +183,10 @@ let result = { constraints: Object.create(null), selfs: [], termCount: 0 };

if(isAssociationOrComposition(originAssocCsn)) {
// if the assoc is marked as primary key, add all its foreign keys as constraint
// as they are primary keys of the other entity as well
if(!assocCsn._target.$isParamEntity && originAssocCsn.key) {
if(originAssocCsn.keys && isFlatFormat) {
for(let fk of originAssocCsn.keys) {
// if the origin assoc is marked as primary key and if it's managed, add all its foreign keys as constraint
// as they are also primary keys of the origin entity as well
if(!assocCsn._target.$isParamEntity && originAssocCsn.key && originAssocCsn.keys) {
for(let fk of originAssocCsn.keys) {
let realFk = originAssocCsn._parent.elements[fk.$generatedFieldName];
let pk = assocCsn._parent.elements[fk.ref[0]];
if(isConstraintCandidate(pk) && isConstraintCandidate(realFk))
{
const c = [ [ fk.ref[0] ], [ fk.$generatedFieldName ] ];

@@ -233,4 +244,4 @@ const key = c.join(',');

// in structured mode only resolve top level element (path rewriting is done elsewhere)
let fk = dependentEntity.elements[ ( isFlatFormat ? c[0].join('_') : c[0][0] )];
let pk = principalEntity.$keys[ ( isFlatFormat ? c[1].join('_') : c[1][0] )];
let fk = dependentEntity.elements[ ( options.isFlatFormat ? c[0].join('_') : c[0][0] )];
let pk = principalEntity.$keys[ ( options.isFlatFormat ? c[1].join('_') : c[1][0] )];
return !(isConstraintCandidate(fk) && isConstraintCandidate(pk));

@@ -251,3 +262,3 @@ },

// there are no constraints for them.
if(isFlatFormat && !assocCsn._target.$isParamEntity && assocCsn.keys) {
if(!assocCsn._target.$isParamEntity && assocCsn.keys) {
for(let fk of assocCsn.keys) {

@@ -275,9 +286,7 @@ let realFk = assocCsn._parent.elements[fk.$generatedFieldName];

/*
* In Flat Mode an element is a constraint candidate if it
* is of scalar type
* In Flat Mode an element is a constraint candidate if it is of scalar type.
* In Structured mode, it eventually can be of a named type (which is
* by the construction standards for OData either a complex type or a
* type definition (alias to a scalar type).
* The element must never be an association or composition and
* must not be hidden with @cds.api.ignore
* The element must never be an association or composition and be renderable.
*/

@@ -287,5 +296,5 @@ function isConstraintCandidate(elt) {

elt.type &&
(!isFlatFormat || isFlatFormat && isBuiltinType(elt.type)) &&
(!options.isFlatFormat || options.isFlatFormat && isBuiltinType(elt.type)) &&
!['cds.Association', 'cds.Composition'].includes(elt.type) &&
!elt['@cds.api.ignore']);
isEdmPropertyRendered(elt, options));
return rc;

@@ -541,2 +550,23 @@ }

// From the odata specification: [Simple Identifier] starts with a letter or underscore, followed by at most 127 letters, underscores or digits.
function isSimpleIdentifier(identifier){
return identifier && identifier.match(/^[a-zA-Z_]{1}[a-zA-Z0-9_]{0,127}$/);
}
function escapeString(str) {
// first regex: replace & if not followed by apos; or quot; or gt; or lt; or amp; or #
// Do not always escape > as it is a marker for {i18n>...} translated string values
let result = str;
if (typeof str === 'string') {
result = str.replace(/&(?!(?:apos|quot|[gl]t|amp);|#)/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/"/g, '&quot;')
.replace(/\r\n|\n/g, '&#xa;');
if (!result.startsWith('{i18n>') && !result.startsWith('{bi18n'))
result = result.replace(/>/g, '&gt;')
}
return result;
}
module.exports = {

@@ -562,3 +592,5 @@ validateOptions,

mapCdsToEdmType,
addTypeFacets
addTypeFacets,
isSimpleIdentifier,
escapeString,
}

@@ -167,5 +167,5 @@ // CSN frontend - transform CSN into XSN

validKinds: [ 'element' ],
inKind: [ 'element', 'type', 'entity', 'param', 'annotation', 'annotate', 'extend' ],
inKind: [ 'element', 'type', 'entity', 'param', 'annotation', 'event', 'annotate', 'extend' ],
},
payload: {
payload: { // keep it for a while, TODO: remove with v2
dictionaryOf: definition,

@@ -242,3 +242,3 @@ type: renameTo( 'elements', dictionary ),

optional: [ 'ref', 'global' ],
inKind: [ 'element', 'type', 'param', 'mixin', 'annotation' ],
inKind: [ 'element', 'type', 'param', 'mixin', 'event', 'annotation' ],
},

@@ -255,3 +255,3 @@ targetAspect: {

},
cardinality: {
cardinality: { // there is an extra def for 'from'
type: object,

@@ -288,2 +288,5 @@ optional: [ 'src', 'min', 'max' ],

},
srcmin: { // in 'cardinality'
type: renameTo( 'sourceMin', natnum ),
},
src: { // in 'cardinality'

@@ -384,3 +387,3 @@ class: 'natnumOrStar',

optional: [ 'SELECT', 'SET' ],
inKind: [ 'entity' ],
inKind: [ 'entity', 'event' ],
},

@@ -423,8 +426,13 @@ SELECT: {

type: from, // XSN TODO: no array anymore, then type: object
optional: [ 'ref', 'global', 'join', 'args', 'on', 'SELECT', 'SET', 'as' ],
optional: [ 'ref', 'global', 'join', 'cardinality', 'args', 'on', 'SELECT', 'SET', 'as' ],
schema: {
cardinality: {
type: object,
optional: [ 'srcmin', 'src', 'min', 'max' ],
onlyWith: 'join',
},
args: {
arrayOf: object,
minLength: 2,
optional: [ 'ref', 'global', 'join', 'args', 'on', 'SELECT', 'SET', 'as' ],
optional: [ 'ref', 'global', 'join', 'cardinality', 'args', 'on', 'SELECT', 'SET', 'as' ],
onlyWith: 'join',

@@ -518,3 +526,3 @@ schema: {}, // 'args' in 'args' in 'from' is same as 'args' in 'from'

type: boolOrNull,
inKind: [ 'element' ],
inKind: [ 'element', '$column' ],
},

@@ -528,7 +536,7 @@ cast: {

class: 'expression',
inKind: [ 'element' ],
inKind: [ 'element', 'param' ],
},
includes: {
arrayOf: stringRef,
inKind: [ 'entity', 'type', 'extend' ],
inKind: [ 'entity', 'type', 'event', 'extend' ],
},

@@ -568,4 +576,4 @@ returns: {

},
namespace: { // unspecified top-level property by Umbrella
type: ignore,
namespace: {
type: stringRef,
},

@@ -572,0 +580,0 @@ meta: { // meta information

@@ -81,2 +81,3 @@ // Transform augmented CSN into compact "official" CSN

// special: top-level, cardinality -----------------------------------------
sources: ignore,
definitions: sortedDict,

@@ -86,2 +87,3 @@ extensions, // is array

options: ignore,
sourceMin: renameTo( 'srcmin', value ),
sourceMax: renameTo( 'src', value ), // TODO XSN: rename?

@@ -139,3 +141,5 @@ targetMin: renameTo( 'min', value ),

limit: [ 'rows' ], // 'offset',
elements: [ 'payload', '$elements' ],
elements: [ '$elements' ], // $elements for --enrich-csn
sources: [ 'namespace' ],
sourceMin: [ 'srcmin' ],
sourceMax: [ 'src' ],

@@ -183,3 +187,3 @@ targetMin: [ 'min' ],

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

@@ -243,7 +247,15 @@ const csnDirectValues = [ 'val', 'messages' ]; // + all starting with '@'

const csn = {};
const sources = model.sources || Object.create(null);
if (options.parseCdl) {
const using = usings( model.sources || {} );
const using = usings( sources );
if (using.length)
csn.requires = using;
}
// 'namespace' for complete model is 'namespace' of first source
for (const first in sources) {
const { namespace } = sources[first];
if (namespace && namespace.path)
csn.namespace = namespace.path.map( i => i.id ).join('.');
break;
}
set( 'definitions', csn, model );

@@ -402,6 +414,3 @@ const exts = extensions( model.extensions || [], csn, model );

return undefined;
if (node.kind !== 'event')
return insertOrderDict( dict );
csn.payload = insertOrderDict( dict );
return undefined;
return insertOrderDict( dict );
}

@@ -864,3 +873,5 @@

srcs = [ ...srcs[0].args, ...srcs.slice(1) ];
const join = { join: joinTrans[node.join] || node.join, args: node.args.map( from ) };
const join = { join: joinTrans[node.join] || node.join };
set( 'cardinality', join, node );
join.args = node.args.map( from );
set( 'on', join, node );

@@ -893,2 +904,3 @@ return extra( join, node );

gensrcFlavor = gensrcFlavor || 'column';
set( 'virtual', col, elem );
set( 'key', col, elem );

@@ -895,0 +907,0 @@ addExplicitAs( assignExpression( col, elem.value ),

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

noSemicolonHere,
setLocalKeyword,
excludeExpected,

@@ -161,2 +162,12 @@ isStraightBefore,

function setLocalKeyword( string, notBefore ) {
const ll1 = this.getCurrentToken();
if (ll1.type === this.constructor.Identifier &&
ll1.text.toUpperCase() === string) {
// console.log('LT2:',this._input.LT(2).text.toUpperCase() , !notBefore.includes( this._input.LT(2).text.toUpperCase() ))
if (!notBefore || !notBefore.includes( this._input.LT(2).text.toUpperCase() ))
ll1.type = this.constructor[string];
}
}
// // Special function for rule `requiredSemi` before return $ctx

@@ -163,0 +174,0 @@ // function braceForSemi() {

@@ -32,3 +32,3 @@ // CSN functionality for resolving references

const artifactProperties
= ['elements', 'columns', 'keys', 'mixin', 'enum', 'params', 'actions', 'payload', 'definitions', 'extensions'];
= ['elements', 'columns', 'keys', 'mixin', 'enum', 'params', 'actions', 'definitions', 'extensions'];

@@ -170,3 +170,9 @@ function csnRefs( csn ) {

const source = select._sources[ obj.$env ];
return expandRefPath( path, source.elements[ head ], 'source' );
// Had a case where a obj.$env was the name of a mixin
if(source)
return expandRefPath( path, source.elements[ head ], 'source' );
else if(select.mixin && select.mixin[obj.$env])
return expandRefPath( path, select.mixin[ head ], 'source' );
else
throw new Error('No source found!');
}

@@ -359,3 +365,4 @@

csnRefs.implicitAs = implicitAs;
csnRefs.analyseCsnPath = analyseCsnPath
module.exports = csnRefs;

@@ -463,3 +463,2 @@ 'use strict'

forEachGeneric( obj, 'elements', callback, obj_path );
forEachGeneric( obj, 'payload', callback, obj_path );
forEachGeneric( obj, 'enum', callback, obj_path );

@@ -658,3 +657,8 @@ forEachGeneric( obj, 'foreignKeys', callback, obj_path );

function isEdmPropertyRendered(elementCsn, options) {
let isStructuredFormat = options.toOdata && options.toOdata.odataFormat === 'structured' || options.isStructFormat;
if(options.toOdata)
options = options.toOdata;
// FKs are rendered in
// V2/V4 flat: always on
// V4 struct: on/off
const renderForeignKey = (options.version === 'v4' && options.odataFormat === 'structured') ? !!options.odataForeignKeys : true;
const isNotIgnored = !elementCsn.target ? !elementCsn['@cds.api.ignore'] : true;

@@ -664,4 +668,8 @@ const isNavigable = elementCsn.target ?

elementCsn['@odata.navigable'] !== undefined && (elementCsn['@odata.navigable'] === null || elementCsn['@odata.navigable'] === true)) : true;
return(isNotIgnored && isNavigable &&
!(elementCsn['@odata.foreignKey4'] && isStructuredFormat))
// Foreign Keys can be ignored
if(elementCsn['@odata.foreignKey4'])
return isNotIgnored && renderForeignKey;
// ordinary elements can be ignored and isNavigable is always true for them
// assocs cannot be ignored but not navigable
return isNotIgnored && isNavigable;
}

@@ -731,2 +739,94 @@

/**
* Loop through the model, applying the custom transformations on the node's matching.
*
* Each transformer gets:
* - the parent having the property
* - the name of the property
* - the value of the property
* - the path to the property
*
* @param {object} csn CSN to enrich in-place
* @param {Map} customTransformers Map of prop to transform and function to apply
* @returns {object} CSN with transformations applied
*/
function applyTransformations( csn, customTransformers={}, artifactTransformers=[] ) {
const transformers = {
elements: dictionary,
definitions: dictionary,
actions: dictionary,
params: dictionary,
enum: dictionary,
mixin: dictionary,
ref: pathRef,
//type: simpleRef,
//target: simpleRef,
//includes: simpleRef,
}
const csnPath = [];
if (csn.definitions)
definitions( csn, 'definitions', csn.definitions );
return csn;
function standard( parent, prop, node ) {
if (!node || typeof node !== 'object' || !{}.propertyIsEnumerable.call( parent, prop ) || (typeof prop === 'string' && prop.startsWith('@')) || node._ignore)
return;
csnPath.push( prop );
if (node instanceof Array) {
node.forEach( (n, i) => standard( node, i, n ) );
}
else {
for (let name of Object.getOwnPropertyNames( node )) {
const trans = transformers[name] || standard;
if(customTransformers[name])
customTransformers[name](node, name, node[name], csnPath);
trans( node, name, node[name], csnPath );
}
}
csnPath.pop();
}
function dictionary( node, prop, dict ) {
csnPath.push( prop );
for (let name of Object.getOwnPropertyNames( dict )) {
standard( dict, name, dict[name] );
}
if (!Object.prototype.propertyIsEnumerable.call( node, prop ))
setProp(node, '$' + prop, dict);
csnPath.pop();
}
function definitions( node, prop, dict ) {
csnPath.push( prop );
for (let name of Object.getOwnPropertyNames( dict )) {
artifactTransformers.forEach(fn => fn(dict, name, dict[name]));
standard( dict, name, dict[name] );
}
if (!Object.prototype.propertyIsEnumerable.call( node, prop ))
setProp(node, '$' + prop, dict);
csnPath.pop();
}
//Keep looping through the pathRef
function pathRef( node, prop, path ) {
csnPath.push( prop );
path.forEach( function step( s, i ) {
if (s && typeof s === 'object') {
csnPath.push( i );
if (s.args)
standard( s, 'args', s.args );
if (s.where)
standard( s, 'where', s.where );
csnPath.pop();
}
} );
csnPath.pop();
}
}
module.exports = {

@@ -747,2 +847,3 @@ getUtils,

getElementDatabaseNameOf,
applyTransformations
};

@@ -42,3 +42,2 @@ // For testing: reveal non-enumerable properties in CSN, display result of csnRefs

mixin: dictionary,
payload: dictionary,
ref: pathRef,

@@ -45,0 +44,0 @@ type: simpleRef,

@@ -125,2 +125,6 @@ 'use strict'

function isAspect(node) {
return node && node.kind === 'type' && node.$syntax === 'aspect';
}
/**

@@ -533,2 +537,3 @@ * Check whether the given artifact has type information. An artifact has type

isElementWithType,
isAspect,
isArtifact,

@@ -535,0 +540,0 @@ isContainerArtifact,

@@ -93,4 +93,7 @@ const { createOptionProcessor } = require('./base/optionProcessorHelper');

odataProxies
uniqueconstraints
hanaAssocRealCardinality
originalKeysForTemporal
odataDefaultValues
mapAssocToJoinCardinality
dontRenderVirtualElements
--old-transformers Use the old transformers that work on XSN instead of CSN

@@ -128,2 +131,4 @@ --hana-flavor Compile with backward compatibility for HANA CDS (incomplete)

.option('-a, --associations <proc>', ['assocs', 'joins'])
.option(' --render-virtual')
.option(' --joinfk')
.option('-u, --user <user>')

@@ -153,2 +158,4 @@ .option('-s, --src')

joins : Transform associations to joins
--render-virtual Render virtual elements in views and draft tables
--joinfk Create JOINs for foreign key accesses
-u, --user <user> Value for the "$user" variable

@@ -168,2 +175,3 @@ -s, --src (default) Generate HANA CDS source files "<artifact>.hdbcds"

.option(' --odata-proxies')
.option(' --odata-foreign-keys')
.option('-c, --csn')

@@ -194,2 +202,3 @@ .option('-f, --odata-format <format>', ['flat', 'structured'])

--odata-proxies (highly experimental) Generate Proxies for out-of-service navigation targets.
--odata-foreign-keys Render foreign keys in structured format (V4 only)
-n, --names <style> Annotate artifacts and elements with "@cds.persistence.name", which is

@@ -234,2 +243,4 @@ the corresponding database name (see "--names" for "toHana or "toSql")

.option('-a, --associations <proc>', ['assocs', 'joins'])
.option(' --render-virtual')
.option(' --joinfk')
.option('-d, --dialect <dialect>', ['hana', 'sqlite'])

@@ -265,2 +276,4 @@ .option('-u, --user <user>')

joins : (default) Transform associations to joins
--render-virtual Render virtual elements in views and draft tables
--joinfk Create JOINs for foreign key accesses
-d, --dialect <dialect> SQL dialect to be generated:

@@ -267,0 +280,0 @@ hana : SQL with HANA specific language features

@@ -292,3 +292,3 @@

else {
result += ',\n' + childEnv.indent + 'CONSTRAINT ' + cn + ' UNIQUE (' +
result += ',\n' + childEnv.indent + 'CONSTRAINT ' + `${quoteSqlId( absoluteCdsName(artifactName) + '_' + cn )}` + ' UNIQUE (' +
c.map(cpath => quoteSqlId(cpath.ref[0])).join(', ') + ')';

@@ -407,2 +407,3 @@ }

// is not an association.
// Any change to the cardinality rendering must be reflected in A2J mapAssocToJoinCardinality() as well.
function renderAssociationElement(elementName, elm, env) {

@@ -541,3 +542,6 @@ let result = '';

for (let i = 1; i < source.args.length; i++) {
result = `(${result} ${source.join.toUpperCase()} JOIN ${renderViewSource(artifactName, source.args[i], env)}`
result = `(${result} ${source.join.toUpperCase()} `;
if(options.toSql.dialect === 'hana')
result += renderJoinCardinality(source.cardinality);
result += `JOIN ${renderViewSource(artifactName, source.args[i], env)}`
if (source.on) {

@@ -560,2 +564,17 @@ result += ` ON ${renderExpr(source.on, env)}`;

function renderJoinCardinality(card) {
let result = '';
if(card) {
if(card.srcmin && card.srcmin === 1)
result += 'EXACT ';
result += card.src && card.src === 1 ? 'ONE ' : 'MANY ';
result += 'TO ';
if(card.min && card.min === 1)
result += 'EXACT ';
if(card.max)
result += (card.max === 1) ? 'ONE ' : 'MANY ';
}
return result;
}
// Render a path that starts with an absolute name (as used for the source of a query),

@@ -660,4 +679,6 @@ // possibly with an alias, with plain or quoted names, depending on options. Expects an object 'path' that has a

if(leaf && env._artifact.elements[leaf] && env._artifact.elements[leaf].virtual) {
// render a virtual column 'null as <alias>'
result += env.indent + 'NULL AS ' + quoteSqlId(col.as || leaf);
const renderVirtual = options.forHana && isBetaEnabled(options, 'dontRenderVirtualElements') ? !!options.forHana.renderVirtualElements : true;
if(renderVirtual)
// render a virtual column 'null as <alias>'
result += env.indent + 'NULL AS ' + quoteSqlId(col.as || leaf);
} else if (col.cast && !(options && options.toSql && options.toSql.dialect === 'sqlite')) {

@@ -817,3 +838,3 @@ result = env.indent + 'CAST(' + renderExpr(col, env, true) + ' AS ';

// Structured type
let elements = Object.keys(art.elements).map(name => renderElement(artifactName, art, name, art.elements[name], definitionsDuplicateChecker, null, childEnv))
let elements = Object.keys(art.elements).map(name => renderElement(artifactName, name, art.elements[name], definitionsDuplicateChecker, null, childEnv))
.filter(s => s !== '')

@@ -820,0 +841,0 @@ .join(',\n') + '\n';

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

const { isArtifactInSomeService, getServiceOfArtifact, isLocalizedArtifactInService } = require('./odata/utils');
const typesExposure = require('./odata/typesExposure');
const ReferenceFlattener = require('./odata/referenceFlattener');
const structureFlattener = require('./odata/structureFlattener');
const processForeignKeys = require('./odata/foreignKeys');
// Transformation for ODATA. Expects a CSN 'inputModel', processes it for ODATA.

@@ -98,3 +104,3 @@ // The twin of forOdata.js

addElement, createAction,
addAction, copyAndAddElement,
addAction,
checkForeignKeys, extractValidFromToKeyElement,

@@ -117,3 +123,2 @@ checkAssignment, checkMultipleAssignments,

isAssociation,
isManagedAssociationElement,
isStructured,

@@ -128,6 +133,2 @@ inspectRef,

// exposed struct types (see function exposeStructType)
// They have to be memorized to know if a given type is exposed or if there is a name clash
let exposedStructTypes = {};
// collect all declared non-abstract services from the model

@@ -138,9 +139,5 @@ // use the array when there is a need to identify if an artifact is in a service or not

//handles reference flattening
let referenceFlattener;
if (!structuredOData) {
let ReferenceFlattener = require('./referenceFlattener');
referenceFlattener = new ReferenceFlattener();
referenceFlattener.attachPaths(csn);
referenceFlattener.resolveAllReferences(csn, inspectRef, isStructured);
}
let referenceFlattener = new ReferenceFlattener();
referenceFlattener.attachPaths(csn);
referenceFlattener.resolveAllReferences(csn, inspectRef, isStructured);

@@ -182,2 +179,4 @@ // (0) Semantic checks before flattening regarding temporal data and array of

referenceFlattener.attachPaths(csn);
validator(csn, {

@@ -249,4 +248,7 @@ error, warning, info, signal, inspectRef, effectiveType, artifactRef, csn,

// type Foo: array of { qux: Integer };
if (def.kind === 'type' && def.items && isArtifactInSomeService(defName, services))
expandFirstLevelOfArrayed(def);
if (def.kind === 'type' && def.items && isArtifactInSomeService(defName, services)) {
if(expandFirstLevelOfArrayed(def)) {
referenceFlattener.attachPaths(def,['definitions',defName]);
}
}
});

@@ -262,108 +264,21 @@

if (!structuredOData) {
let structureFlattener = require('./odata/structureFlattener');
structureFlattener(csn, { csnUtils, cloneCsn, error, signal, forEachDefinition, setProp, referenceFlattener, copyAnnotations })
}
if (referenceFlattener) {
referenceFlattener.attachPaths(csn);
}
// structure flattener reports errors, further processing is not safe -> throw exception in case of errors
handleMessages(inputModel, options);
forEachDefinition(csn, (def, defName, propertyName, path) => {
// (1.5) Flatten structs - for entities and views only (might result in new elements)
if (def.kind === 'entity' || def.kind === 'view') {
for (let elemName in def.elements) {
let elem = def.elements[elemName];
if (isStructured(elem)) {
if (structuredOData) {
if (!isArtifactInSomeService(defName, services)) return;
let serviceName = getServiceName(defName);
exposeStructTypeOf(elem, serviceName, `${defNameWithoutServiceName(defName, serviceName).replace(/\./g, '_')}_${elemName}`, elemName);
}
}
if (getServiceName(defName) && !elem.type && !elem.items && !elem.elements) {
signal(error`Element "${defName}.${elemName}" does not have a type: Elements of ODATA entities must have a type`, path.concat(['elements', elemName]));
}
}
}
// (1.6) Expose (named or anonymous) structured types used in structured types
else if (isArtifactInSomeService(defName, services) && def.kind === 'type') {
for (let elemName in def.elements) {
let elem = def.elements[elemName];
// Expose structured types used in exposed structured types
exposeStructTypeOf(elem, getServiceOfArtifact(defName, services), `${defName.replace(/\./g, '_')}_${elemName}`, `${defName}.${elemName}`);
}
}
});
// Expose user-defined types and anonymous types
typesExposure(csn, services, options, csnUtils, signal, referenceFlattener);
// flatten references, attach new paths
if (referenceFlattener) {
referenceFlattener.flattenAllReferences(csn);
referenceFlattener.attachPaths(csn);
}
referenceFlattener.flattenAllReferences(csn);
referenceFlattener.attachPaths(csn);
// For exposed actions and functions that use non-exposed or anonymous structured types, create
// artificial exposing types.
// Expose types for 'array of/many' declarations
forEachDefinition(csn, (def, defName) => {
let service = getServiceOfArtifact(defName, services);
if (service) {
// unbound actions
if (def.kind === 'action' || def.kind === 'function') {
exposeTypesForAction(def, defName, service);
}
// Process associations - expand, generate foreign keys
processForeignKeys(csn, { referenceFlattener, csnUtils, transformers })
// bound actions
for (let actionName in def.actions || {}) {
exposeTypesForAction(def.actions[actionName], `${defName}_${actionName}`, service);
}
if (def.kind === 'entity' || def.kind === 'view') {
let isAction = false;
// If a member is of type 'array of T' where T is either user defined structured type outside of the service or anonymous type,
// then expose T and assign it do the member.
forEachMemberRecursively(def, (member, memberName, propertyName, path) => {
// we do apply array of exposure logic on actions/functions
// and on params and returns of action/function always
if (member.kind === 'action' || member.kind === 'function') isAction = true;
// anonymous defined 'array of'
if (member.items || (member.type && getFinalTypeDef(member.type).items)) {
if (structuredOData)
exposeArrayOfTypeOf(member, service, `${defNameWithoutServiceName(defName, service)}_${memberName}`, memberName);
else if (options.toOdata.version === 'v4' && !isAction) {
// in flat mode only 'array of <scalar>' is allowed
if (member.items && !isStructured(member.items))
exposeArrayOfTypeOf(member, service, `${defNameWithoutServiceName(defName, service)}_${memberName}`, memberName);
else
signal(error`"${memberName}": Only "array of <scalar>" is allowed in OData V4 flat mode`, path);
}
}
}, ['definitions', defName]);
}
}
});
// Third walk: Process associations
// (3.1) Generate foreign key fields for managed associations (must be done
// after struct flattening, otherwise we might encounter already generated foreign
// key fields in types we have already processed)
// (3.2) Flatten on-conditions in unmanaged associations
// Flatten on-conditions in unmanaged associations
// This must be done before 4.1, since all composition targets are annotated with @odata.draft.enabled in this step
const sortByAssociationDependency = require('./odata/sortByAssociationDependency');
const sortedAssociations = sortByAssociationDependency(csn);
const { generateForeignKeys, flattenForeignKeys } = require('./odata/foreignKeys');
sortedAssociations.forEach(item => {
const { element } = item;
if (isManagedAssociationElement(element) && element.keys) {
flattenForeignKeys(element, csnUtils, referenceFlattener);
}
})
if (referenceFlattener)
referenceFlattener.attachPaths(csn);
generateForeignKeys(transformers, sortedAssociations);
forEachDefinition(csn, processOnCond);

@@ -654,151 +569,8 @@

delete def.items.type;
return true;
}
}
return false;
}
// (2.1) If 'action' uses structured types as parameters or return values that are not exposed in 'service'
// (because the types are anonymous or have a definition outside of 'service'), create equivalent types
// in 'service' and make 'action' use them instead
function exposeTypesForAction(action, actionName, service) {
if (action.returns && isArreyed(action.returns))
exposeArrayOfTypeOf(action.returns, service, `return_${actionName.replace(/\./g, '_')}`, actionName);
else
exposeStructTypeOf(action.returns, service, `return_${actionName.replace(/\./g, '_')}`, actionName);
for (let paramName in action.params || {}) {
if (isArreyed(action.params[paramName]))
exposeArrayOfTypeOf(action.params[paramName], service, `param_${actionName.replace(/\./g, '_')}_${paramName}`, actionName);
else
exposeStructTypeOf(action.params[paramName], service, `param_${actionName.replace(/\./g, '_')}_${paramName}`, actionName);
}
}
function isArreyed(node) {
return node.items || (node.type && getFinalTypeDef(node.type).items);
}
// If a member is of type "array of <named type|anonymous type>", we expose the arrayed type,
// like we expose structures in structured mode
function exposeArrayOfTypeOf(node, service, artificialName, memberName) {
// if anonymously defined in place -> we always expose the type
// this would be definition like 'elem: array of { ... }'
// and we use the artificial name for the new type name
if (node.items && !node.type) {
exposeStructTypeOf(node, service, artificialName, memberName);
}
// we can have both of the 'type' and 'items' in the cases:
// 1. 'elem: Foo' and 'type Foo: array of Baz' and 'type Baz: { ... }'
// or 2. 'elem: Foo' and type Foo: array of Integer|String|...'
else if (node.type) {
// case 2. - in V2 we expand to the underlying base scalar and remove the type property
if (node.items && node.items.type && isBuiltinType(node.items.type)
&& options.toOdata.version === 'v2') delete node.type;
else if (getFinalTypeDef(node.type).items) {
if (!isArtifactInService(node.type, service)) {
let typeId = `${service}.${node.type}`;
let newType = exposeArrayedType(getFinalTypeDef(node.type), typeId);
// When we have in the model something like:
// type Foo: array of Bar; type Bar: { qux: Integer };
// In the type Foo we expand the first level of elements of the items like we have in CDL this:
// type Foo: array of { qux: Integer };
expandFirstLevelOfArrayed(newType);
node.type = typeId;
}
// case 1. - as we keep the type property, the items property is removed
if (node.items) delete node.items;
}
}
function exposeArrayedType(typeDef, typeId) {
let newType = csn.definitions[typeId];
if (newType) {
if (!(typeId in exposedStructTypes)) {
signal(error`Cannot create artificial type "${typeId}" because the name is already used`, newType.$path);
}
return newType;
}
// create empty type
newType = {
kind: 'type',
items: Object.create(null),
}
// copy over the items
newType.items = cloneCsn(typeDef.items);
csn.definitions[typeId] = newType;
exposedStructTypes[typeId] = true;
return newType;
}
}
// (1.2), (2.1) If 'node' exists and has a structured type that is not exposed in 'service', (because the type is
// anonymous or has a definition outside of 'service'), create an equivalent type in 'service', either
// using the type's name or (if anonymous) 'artificialName', and make 'node' use that type instead.
// Complain if there is an error.
function exposeStructTypeOf(node, service, artificialName, parentName) {
if (!node) {
return;
}
if (node.items) {
exposeStructTypeOf(node.items, service, artificialName, parentName);
}
if ((csn.definitions[node.type] || node).elements
&& (!node.type || (!isArtifactInService(node.type, service) && !isBuiltinType(node.type)))) {
let typeDef = node.type ? getCsnDef(node.type) : /* anonymous type */ node;
if (typeDef && isStructured(typeDef) || (node.type && !node.type.startsWith(service))) {
let typeId = node.type ? `${node.type.replace(/\./g, '_')}` : artificialName;
// With the redirection of sub elements, the element which is of named type with an association is now expanded and contains the association
// and the new target. Consequently, we now have both type and elements properties in this case, and the elements should be taken as a priority
// as the correct target is there and no longer in the type definition
let newTypeElements = (node.type && node.elements) ? node.elements : typeDef.elements;
let type = exposeStructType(typeId, newTypeElements, service, parentName);
if (!type) {
// Error already reported
return;
}
if (node.$location) setProp(type, '$location', node.$location);
// Recurse into elements of 'type' (if any)
for (let elemName in type.elements) {
if (structuredOData && node.elements && node.elements[elemName].$location) setProp(type.elements[elemName], '$location', node.elements[elemName].$location);
exposeStructTypeOf(type.elements[elemName], service, `${typeId}_${elemName}`, parentName);
}
typeDef.kind === 'type' ? copyAnnotations(typeDef, type) : copyAnnotations(node, type);
if (structuredOData) delete node.elements;
node.type = `${service}.${typeId}`;
}
}
}
// (1.2), (2.1) Expose an artificial structured type with ID 'typeId' with 'elements' in 'service' (reusing such a type
// if it already exists).
// Return the exposed type. Report any errors
function exposeStructType(typeId, elements, service, parentName) {
let typeName = `${service}.${typeId}`;
// If type already exists, reuse it (complain if not created here)
let type = csn.definitions[typeName];
if (type) {
if (!(typeName in exposedStructTypes)) {
signal(error`Cannot create artificial type "${typeName}" for an action or function "${parentName}" because the name is already used`,
['definitions', parentName]);
return null;
}
return type;
}
// Create type with empty elements
type = {
kind: 'type',
elements: Object.create(null),
};
// Duplicate the type's elements
for (let elemName in elements) {
copyAndAddElement(elements[elemName], type, typeName, elemName);
}
csn.definitions[typeName] = type;
// store typeName in set of exposed struct types
exposedStructTypes[typeName] = true;
return type;
}
// (3.2) Flatten on-conditions in unmanaged associations

@@ -1022,25 +794,1 @@ function processOnCond(def, defName) {

}
// some model utilities => TODO: move them to separate file
function isArtifactInSomeService(artName, services) {
return services.some(serviceName => artName.startsWith(`${serviceName}.`));
}
function isLocalizedArtifactInService(artName, services) {
if (!artName.startsWith('localized.')) return false;
return isArtifactInSomeService(artName.split('.').slice(1).join('.'), services);
}
function getServiceOfArtifact(artName, services) {
return services.find(serviceName => artName.startsWith(`${serviceName}.`));
}
// Check if an artifact with name 'artName' is part of a 'service'
function isArtifactInService(artName, service) {
return artName.startsWith(`${service}.`);
}
function defNameWithoutServiceName(name, service) {
return name.replace(`${service}.`, '');
}

@@ -8,87 +8,98 @@ 'use strict';

const { copyAnnotations } = require('../../model/modelUtils');
const { dfilter } = require('../udict');
const { forEach } = require('../udict');
const { collectAllManagedAssociations } = require('./utils');
const sortByAssociationDependency = require('./sortByAssociationDependency');
function generateForeignKeys(transformers, sortedAssociations) {
sortedAssociations.forEach(item => {
const { definitionName, elementName, element, parent, path } = item;
for (let keyIndex in element.keys) {
let key = element.keys[keyIndex];
let keyPath = path.concat('keys', keyIndex);
let foreignKeyElement = transformers.createForeignKeyElement(element, elementName, key, parent, definitionName, keyPath);
transformers.toFinalBaseType(foreignKeyElement);
copyAnnotations(element, foreignKeyElement, true);
}
});
/**
* Generates foreign keys and returns their names as an array
*/
function generateForeignKeys(transformers, item) {
let arrayOfGeneratedForeignKeyNames = [];
const { definitionName, elementName, element, parent, path } = item;
for (let keyIndex in element.keys) {
let key = element.keys[keyIndex];
let keyPath = path.concat('keys', keyIndex);
let foreignKeyElements = transformers.createForeignKeyElement(element, elementName, key, parent, definitionName, keyPath);
if (!foreignKeyElements) continue;
forEach(foreignKeyElements, (_name, foreignKeyElement) => {
transformers.toFinalBaseType(foreignKeyElements);
copyAnnotations(element, foreignKeyElement, true)
})
arrayOfGeneratedForeignKeyNames.push(...Object.keys(foreignKeyElements))
}
return arrayOfGeneratedForeignKeyNames;
}
// For an array `keys` of foreign key infos, return an array in flattened form
// in one of the two cases:
// (1) replace all foreign keys that are managed associations themselves by
// their respective foreign keys, recursively, with names flattened using
// pathDelimiter between path components.
// (2) replace all foreign keys that are structured with their respective flattened form.
//
// Note: must be done after struct flattening(flattenStructuredElement method),
// otherwise we might encounter already generated foreign key fields in types
// we have already processed.
function flattenForeignKeys(assoc, csnUtils, referenceFlattener) {
let fkSeparator = '_';
let targetArt = csnUtils.getCsnDef(assoc.target);
// get all the elements from the target that have 'key' identifier
let targetKeys = dfilter(targetArt.elements, elem => elem.key === true);
// in case we have explicitly defined FKs
Object.assign(targetKeys, dfilter(targetArt.elements, (elem, elemName) => {
if (elem._flatElementNameWithDots) {
// this is flattened elem -> keys still not flattened, have to check if starts with key ref
// FIXME: review why join('.')? what about join(fkSeparator)?
return assoc.keys.map(key => key.ref.join('.')).some(keyDotName => elemName.startsWith(`${keyDotName}${fkSeparator}`));
} else {
// exact match of the name
return assoc.keys.map(key => key.ref.join('.')).some(keyDotName => keyDotName === elemName);
}
}));
let result = [];
// this iteration assumes the elements in the tartgetArtifact are flattened
/**
* Expand structured keys
* entity A {toB: association to B {stru};} -> CSN: keys:[{ref:['stru']}]
* entity B {stru:{subid:Integer;}}
* after expand -> keys:[{ref:['stru_subid']}]
*/
function expandStructuredKeysForElement(assoc, referenceFlattener) {
let newKeys = [];
for (let key of assoc.keys) {
let fKeyName = key.ref.join(fkSeparator);
// The key is an association - (1)
if (Object.keys(targetKeys).includes(fKeyName) && targetKeys[fKeyName].type
&& targetKeys[fKeyName].type === 'cds.Association' && targetKeys[fKeyName].target
) {
// as there is no assurance that the target of the target has flattened keys already
// has to go through both of the associations
csnUtils.getCsnDef(targetKeys[fKeyName].target); // sanity check if the definition exists
targetKeys[fKeyName].keys.forEach(k => result.push({ ref: [`${fKeyName}${fkSeparator}${k.ref.join(fkSeparator)}`] }));
continue;
let paths = key.$paths;
if (paths) {
let lastPath = paths[paths.length - 1];
let generatedElements = referenceFlattener.getGeneratedElementsForPath(lastPath);
if (generatedElements) {
generatedElements.forEach(elementName => {
newKeys.push({ ref: [elementName], as: elementName });
})
continue;
}
}
let generatedElements = null;
if(referenceFlattener) {
// collect potential flattened keys, which are the counterpart of the current key
let paths=key.$paths;
if(paths) {
generatedElements = referenceFlattener.getGeneratedElementsForPath(paths[paths.length-1]);
newKeys.push(key);
}
if (newKeys.length) {
referenceFlattener.attachPaths(newKeys,assoc.keys.$path)
assoc.keys = newKeys;
}
}
/**
* if a key is an association and it poins to another association,
* the foreign keys of the target association become primary keys
* in the current association
*/
function takeoverForeignKeysOfTargetAssociations(assoc, path, generatedForeignKeyNamesForPath, functions) {
let newResult = [];
for (let keyNumber in assoc.keys) {
let key = assoc.keys[keyNumber]
let keyPath = path.concat('keys', keyNumber);
let resolved = functions.csnUtils.inspectRef(keyPath)
let targetElement = resolved.art;
if (targetElement) {
if (functions.csnUtils.isAssociation(targetElement.type)) {
// association key
expandAssociationKey(key);
} else {
// scalar key
newResult.push(key)
}
}
let flattenedKeys = generatedElements || [];
// The keys is structured element
if (flattenedKeys.length) {
flattenedKeys.forEach(k => result.push({ ref: [k], as: k }));
} else {
// Otherwise simply take as it is
result.push(key);
// target element does not exist, warning is already reported, pass the key anyway
newResult.push(key);
}
}
// If the managed association is NOT NULL, we give it a target min cardinality of 1
// if it didn't already have an explicitly specified min cardinality.
// (No need to check again for min <= max cardinality, because max has already been checked to be > 0)
function expandAssociationKey(key) {
let paths = key.$paths;
if (!paths) return;
let lastPath = paths[paths.length - 1];
let transitionPath = functions.referenceFlattener.getElementTransition(lastPath)
if (transitionPath)
lastPath = transitionPath;
let generatedKeys = generatedForeignKeyNamesForPath[lastPath.join('/')];
if (!generatedKeys) return;
generatedKeys.forEach(fkName => {
newResult.push({ ref: [fkName] });
})
} // expandAssociationKey
assoc.keys = newResult;
}
function fixCardinality(assoc) {
if (assoc.notNull) {

@@ -102,48 +113,58 @@ if (!assoc.cardinality) {

}
assoc.keys = result;
}
/*
function flattenForeignKeysWithRefFlattener(assoc, path, structuredOData, referenceFlattener) {
for (let keyIndex of assoc.keys) {
let key = assoc.keys[keyIndex];
let keyPath = path.concat('keys', keyIndex);
if (!structuredOData) { // flatten the reference using RESOLVED references
let resolvedIsStructured = referenceFlattener.isStructured(keyPath)
if (resolvedIsStructured) {
let ref = key.ref;
let newref = []; // new flattened reference
let previousElementIsStructured = false;
ref.forEach((iref, i) => {
if (previousElementIsStructured == undefined) return; // missing information - skip processing
if (previousElementIsStructured) {
newref[newref.length - 1] = newref[newref.length - 1] + '_' + iref; // prevous element is sructured - concat last with current
} else {
newref.push(iref); // prevous element is not structured - do not flatten, just push it
}
previousElementIsStructured = resolvedIsStructured[i]; // detect structured elements
})
if (key.ref.length > newref.length) { // anything flattened?
key.ref = newref;
}
}
function processSortedForeignKeys(sortedAssociations, functions) {
const { csnUtils, transformers } = functions;
// The map will collect all generated foreign key names for the specific path
let generatedForeignKeyNamesForPath = {}; // map<path,[key-name]>
sortedAssociations.forEach(item => {
const { element, path } = item;
if (csnUtils.isManagedAssociationElement(element) && element.keys) {
takeoverForeignKeysOfTargetAssociations(element, path, generatedForeignKeyNamesForPath, functions);
fixCardinality(element);
}
}
let arrayOfGeneratedForeignKeyNames = generateForeignKeys(transformers, item);
generatedForeignKeyNamesForPath[item.path.join('/')] = arrayOfGeneratedForeignKeyNames;
// If the managed association is NOT NULL, we give it a target min cardinality of 1
// if it didn't already have an explicitly specified min cardinality.
// (No need to check again for min <= max cardinality, because max has already been checked to be > 0)
if (assoc.notNull) {
if (!assoc.cardinality) {
assoc.cardinality = {};
})
}
function expandStructuredKeys(csn, referenceFlattener) {
let managedAssociations = collectAllManagedAssociations(csn);
managedAssociations.forEach(item => {
const { element } = item;
if (element.keys) {
expandStructuredKeysForElement(element, referenceFlattener);
}
if (assoc.cardinality.min === undefined) {
assoc.cardinality.min = 1;
}
}
})
}
*/
module.exports = { generateForeignKeys, flattenForeignKeys };
function processForeignKeys(csn, functions) {
let { referenceFlattener, csnUtils, transformers } = functions;
expandStructuredKeys(csn, referenceFlattener);
// update paths and resolve references
referenceFlattener.attachPaths(csn);
referenceFlattener.resolveAllReferences(csn, csnUtils.inspectRef, csnUtils.isStructured);
// sort all associations by their dependencies
const sortedAssociations = sortByAssociationDependency(csn, referenceFlattener);
// generate foreign keys
processSortedForeignKeys(sortedAssociations, { csnUtils, transformers, referenceFlattener });
}
module.exports = processForeignKeys;

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

* If an association is also a primary key, the additionally created foreign keys become also primary keys in the parent artifact.
* Associations with partial foreign keys force other non-key elements and associations to become virtual primary keys.
* Proper FK generation requires specific order of performing that.
* First PK association should be processed, then the non-PK associations.
* This module provides functionality to sort managed associations depending on if they are marked as primary key or not.
* This module provides functionality to sort managed associations according to their dependencies.
*/

@@ -17,151 +17,90 @@

function buildDependenciesFor(csn) {
const definitionsToProcess = {};
const mapDefinitionsKeyAssociations = {};
const mapUnprocessedKeyAssociationsCount = {};
const {
isAssociationOrComposition
} = require('./utils');
const { forEach } = require('../udict');
function registerKeyAssociation(from, name, target, path, parent, element) {
initIntegerMember(mapUnprocessedKeyAssociationsCount, from);
initMapMember(mapDefinitionsKeyAssociations,from);
mapDefinitionsKeyAssociations[from][name] = { target, path, parent, element };
mapUnprocessedKeyAssociationsCount[from]++;
}
function buildDependenciesFor(csn, referenceFlattener) {
function markDefinitionAsToBeProcessed(definitionName) {
initIntegerMember(mapUnprocessedKeyAssociationsCount, definitionName);
definitionsToProcess[definitionName] = true;
}
function markDefinitionsAsAlreadyProcessed(definitionName) {
definitionsToProcess[definitionName] = false;
}
function setUnprocessedKeyAssocinationCountForDefinition(definitionName, count) {
mapUnprocessedKeyAssociationsCount[definitionName] = count;
}
function getUnprocessedKeyAssociationsCountForDefinition(definitionName) {
return mapUnprocessedKeyAssociationsCount[definitionName];
}
let associationsAlreadyInResult = {}; // map <path,bool> of associations with where added to the result
let associationsNonPK = []; // list of sorted non-primary key associations
// loop over definitions and their elements to find an collect PK and non-PK associations
let dependencies = {};
forEachDefinition(csn, (def, definitionName) => {
let hasAssociations, hasKeyAssociations = false;
let root = [ 'definitions', definitionName ];
forEachMemberRecursively(def, (element, elementName, prop, subpath, parent) => {
let root = ['definitions', definitionName];
forEachMemberRecursively(def, (element, elementName, _prop, subpath, parent) => {
let path = root.concat(subpath);
// go only through managed associations and compositions
if(isAssociationOrComposition(element) && !element.on) {
hasAssociations = true;
markDefinitionAsToBeProcessed(definitionName);
if(element.key) {
registerKeyAssociation(definitionName, elementName, element.target, path, parent, element);
hasKeyAssociations=true;
} else {
if(!associationsAlreadyInResult[path]) {
associationsNonPK.push( { definitionName, elementName, element, parent, path } );
associationsAlreadyInResult[path] = true;
if (isAssociationOrComposition(element) && element.keys && !element.on) { // check association FKs
let elementDependencies = []
element.keys.forEach(iForeignKey => {
let paths = iForeignKey.$paths;
if (!paths) return; // invalid references can not be resolved thus no $paths -> test/odataTransformation/negative/ForeignKeys.cds
let targetElementPath = paths[paths.length - 1];
if (!targetElementPath) return; // TODO check why
let targetElement = getElementForPath(csn, targetElementPath);
if (!targetElement) { // element was moved
targetElementPath = referenceFlattener.getElementTransition(targetElementPath);
if (!targetElementPath) return; // TODO check why
targetElement = getElementForPath(csn, targetElementPath);
}
}
if (targetElement && isAssociationOrComposition(targetElement)) {
elementDependencies.push(targetElementPath);
}
})
dependencies[path.join("/")] = { definitionName, elementName, element, path, parent, dependencies: elementDependencies };
}
}) // forEachMemberRecursively
if(hasAssociations && !hasKeyAssociations) {
//result.push(defName)
markDefinitionsAsAlreadyProcessed(definitionName)
}
}) // forEachDefinition
// Is there anything to process? This would be the list of associations which are PKs
let totalCountOfArtifactsToProcess = Object.keys(definitionsToProcess).filter(definitionName => definitionsToProcess[definitionName]).length;
if(totalCountOfArtifactsToProcess === 0) {
return associationsNonPK; // nothing to do -> return
}
let associationsPK = []; // list of sorted primary key associations
let result = []; // final list of sorted association items
let inResult = {}; // paths of associations which were added in the result
let maxLoops = Object.keys(dependencies).length;
let loops = 0;
do {
// algorithm consistency check: can not process more artifacts than provided (prevent endless loop)
if(loops > totalCountOfArtifactsToProcess)
throw Error(`Error in sortByAssociationDependency: too many loops: ${loops}, expected at most: ${totalCountOfArtifactsToProcess}`);
let countOfProcessed = processArtifacts();
// consistency check: it should process at least one artifact (prevent endless loop)
if(countOfProcessed === 0 && loops > 0)
throw Error('Error in sortByAssociationDependency: processArtifacts did not process any artifacts');
let done = false;
// walk over all dependencies and add to the result if all dependants are already in the result
while (!done) {
if (loops > maxLoops) {
throw Error('Failed to process the association dependencies - max loops reached');
}
done = true;
let toDelete = [];
forEach(dependencies, (name, item) => {
let countOfUnprocessedDependants = 0;
item.dependencies.forEach(path => {
let spath = path.join('/');
if (!inResult[spath]) countOfUnprocessedDependants++;
})
if (countOfUnprocessedDependants === 0) { // all dependants processed?
let spath = item.path.join('/');
if (!inResult[spath]) {
inResult[spath] = true;
result.push(item);
}
toDelete.push(name); // mark association to be removed from the processing list
} else {
done = false;
}
})
// delete already processed associations
toDelete.forEach(name => {
delete dependencies[name];
});
loops++;
} while(prepareArtifactsToProcess());
} // while not done
// return all associations starting with primary key ones as they have priority
return associationsPK.concat(associationsNonPK);
function processArtifacts() {
let countOfProcessedArtifacts = 0;
Object.keys(definitionsToProcess)
.filter(definitionName => definitionsToProcess[definitionName])
.forEach(definitionName => {
if (getUnprocessedKeyAssociationsCountForDefinition(definitionName) === 0) {
let keyAssocs = mapDefinitionsKeyAssociations[definitionName]
for (var elementName in keyAssocs) {
var { path, parent, element } = keyAssocs[elementName];
if (associationsAlreadyInResult[path])
continue;
associationsPK.push({ definitionName, elementName, element, parent, path })
associationsAlreadyInResult[path] = true;
}
markDefinitionsAsAlreadyProcessed(definitionName)
countOfProcessedArtifacts++;
}
})
return countOfProcessedArtifacts;
// check if all dependencies were processed
if (Object.keys(dependencies).length !== 0) {
throw Error('Failed to process the association dependencies - there are more dependencies left');
}
return result;
// if there are artifacts whose targets are still not processed returns true
function prepareArtifactsToProcess() {
let hasMoreToProcess = false;
for(let artifact in mapDefinitionsKeyAssociations) {
let aRed = mapDefinitionsKeyAssociations[artifact];
let stillToProcessCount = 0;
for(let memberName in aRed) {
let target = aRed[memberName].target;
if(definitionsToProcess[target])
stillToProcessCount++;
}
if(stillToProcessCount != getUnprocessedKeyAssociationsCountForDefinition(artifact)) {
hasMoreToProcess = true;
setUnprocessedKeyAssocinationCountForDefinition(artifact, stillToProcessCount);
}
}
return hasMoreToProcess;
function getElementForPath(node, path) {
path.forEach(name => {
if (!node) return;
node = node[name];
})
return node;
}
}
// ------------------------ helper functions ------------------------------------
// Return true if 'artifact' has an association type
function isAssociation(artifact) {
return (artifact.type === 'cds.Association' || artifact.type === 'Association');
}
// Return true if 'artifact' has a composition type
function isComposition(artifact) {
return (artifact.type === 'cds.Composition' || artifact.type === 'Composition')
}
function isAssociationOrComposition(artifact) {
return isAssociation(artifact) || isComposition(artifact);
}
function initMapMember(map,name) {
if(map[name]===undefined) map[name] = {};
}
function initIntegerMember(what,name) {
if(what[name]===undefined) what[name] = 0;
}
module.exports = buildDependenciesFor

@@ -22,6 +22,6 @@ /**

* @param {*} definition definition object to flatten
* @param {*} path path in CSN object
* @param {*} definitionPath path in CSN object
* @param {*} functions utility functions
*/
function flattenDefinition(definition, path, functions) {
function flattenDefinition(definition, definitionPath, functions) {
const { csnUtils, cloneCsn, error, signal, setProp, copyAnnotations } = functions;

@@ -49,4 +49,5 @@ if (definition.kind !== 'entity' && definition.kind !== 'view')

let newElements = {}; // TODO copy $path $paths
flattenStructure(definition, path);
let newElements = {};
flattenStructure(definition, definitionPath);
referenceFlattener.attachPaths(newElements,definitionPath.concat('elements'))
definition.elements = newElements;

@@ -58,5 +59,9 @@

signal(error`Generated element ${elementName} conflicts with other generated element`, path);
return undefined;
} else {
let newElement = createNewElement(element, key, elementNameWithDots, topLevel, propagateAnnotations);
let newPath = definitionPath.concat('elements',elementName)
newElements[elementName] = newElement;
referenceFlattener.registerElementTransition(path,newPath);
return newElement;
}

@@ -104,4 +109,10 @@ } // addNewElement

let elementNameWithDots = elementPathInStructure.concat(elementName).join('.');
addNewElement(element, newElementName, elementNameWithDots, ipath, ikey, isDefinition, propagateAnnotations);
resultingElementNames.push(newElementName);
let newElement = addNewElement(element, newElementName, elementNameWithDots, ipath, ikey, isDefinition, propagateAnnotations);
if(newElement) {
resultingElementNames.push(newElementName);
let movedTo = referenceFlattener.getElementTransition(ipath)
if(movedTo) {
setProp(newElement,'$paths',[movedTo]); // moved always on top-level -> new $paths has only one path element
}
}
}

@@ -108,0 +119,0 @@ }

@@ -389,4 +389,7 @@ 'use strict';

function checkExposedAssoc(artName, assocDef, assocName, service) {
let assocTargetDef = getCsnDef(assocDef.target);
if (!assocDef._ignore && assocDef.target && assocTargetDef && !assocDef.target.startsWith(service)) {
// When we have an aspect and this aspect has element which is a composition of another aspect,
// then the 'assocDef' does not have 'target' property, but only 'targetAspect' property
let assocTarget = assocDef.target || assocDef.targetAspect;
let assocTargetDef = getCsnDef(assocTarget);
if (!assocDef._ignore && assocTarget && assocTargetDef && !assocTarget.startsWith(service)) {
// If we have a 'preserved dotted name' -> a result of flattening -> This scenario is not supported yet

@@ -441,3 +444,3 @@ if (assocDef._flatElementNameWithDots)

if (finalBaseType.elements) {
node.elements = finalBaseType.elements; // copy elements
node.elements = cloneCsn(finalBaseType.elements); // copy elements
delete node.type; // delete the type reference as edm processing does not expect it

@@ -458,3 +461,3 @@ } else {

if (!node.enum && typeDef.enum)
Object.assign(node, { enum: typeDef.enum });
Object.assign(node, { enum: cloneCsn(typeDef.enum) });
if (node.length === undefined && typeDef.length !== undefined)

@@ -768,3 +771,4 @@ Object.assign(node, { length: typeDef.length });

let result = { [elementName]: {} };
let result = Object.create(null);
result[elementName] = {};
for (let prop in elem)

@@ -771,0 +775,0 @@ result[elementName][prop] = elem[prop];

{
"name": "@sap/cds-compiler",
"version": "1.35.0",
"version": "1.39.0",
"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 not supported yet

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

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 too big to display

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

SocketSocket SOC 2 Logo

Product

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

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc