Socket
Socket
Sign inDemoInstall

@sap/cds-compiler

Package Overview
Dependencies
3
Maintainers
3
Versions
99
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.19.2 to 1.23.2

lib/api/main.js

103

bin/cdsc.js

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

// For recursive *.cds expansion, use
// cdsc $(find . -name '*.cds' -type f)
'use strict';

@@ -26,2 +29,4 @@

const { translatePathLocations } = require('../lib/base/messages')
const { translateAssocsToJoins } = require('../lib/transform/translateAssocsToJoins');
const term = require('../lib/utils/term');

@@ -41,7 +46,2 @@ // Note: Instead of throwing ProcessExitError, we would rather just call process.exit(exitCode),

let cmdLine = optionProcessor.processCmdLine(process.argv);
// Deal with '--old-csn' explicitly, overrides newCsn!
if(cmdLine.options.oldCsn) {
cmdLine.options.newCsn = false;
delete cmdLine.options.oldCsn;
}
// Deal with '--version' explicitly

@@ -69,2 +69,6 @@ if (cmdLine.options.version) {

displayUsage(cmdLine.errors, optionProcessor.helpText, 2);
} else if (cmdLine.unknownOptions.length > 0) {
let out = process.stderr;
cmdLine.unknownOptions.forEach(msg => out.write(`cdsc: INFO: ${msg}\n`));
throw new ProcessExitError(2);
}

@@ -81,5 +85,8 @@

}
// Default color omde is 'auto'
term.useColor(cmdLine.options.color || 'auto');
// Do the work for the selected command (default 'toCsn')
executeCommandLine(cmdLine.command || 'toCsn', cmdLine.options, cmdLine.args, cmdLine.unknownOptions);
executeCommandLine(cmdLine.command || 'toCsn', cmdLine.options, cmdLine.args);
} catch (err) {

@@ -112,3 +119,3 @@ // This whole try/catch is only here because process.exit does not work in combination with

// Executes a command line that has been translated to 'command' (what to do), 'options' (how) and 'args' (which files)
function executeCommandLine(command, options, args, unknownOptions = []) {
function executeCommandLine(command, options, args) {
const normalizeFilename = options.testMode && process.platform === 'win32';

@@ -139,31 +146,8 @@ const messageLevels = { Error: 0, Warning: 1, Info: 2, None: 3 };

// special case when we want to call a transformer directly
// without compiling before that (for now only available for toOdata, toHana, toSql)
// works only with combination of --beta-mode in the transformers
if (options.skipCompile && ['toOdata', 'toHana', 'toSql'].includes(command)) {
if (args.length !== 1)
throw new Error(`--skip-compile option works with only one file`);
let csn = '';
try {
csn = JSON.parse(fs.readFileSync(args[0]));
} catch (e) {
throw new Error(`${args[0]} must be a valid json file`);
}
options.betaMode = true;
csn.options = options;
commands[command](csn);
} else {
const fileCache = Object.create(null)
compiler.compile( args.files, undefined, options, fileCache )
.then( commands[command] )
.then( displayMessages, displayErrors )
.catch( catchErrors );
compiler.compile( args, undefined, options )
.then( commands[command] )
.then( displayMessages, displayErrors )
.catch( catchErrors );
}
if (unknownOptions.length) {
let out = process.stdout;
unknownOptions.forEach(msg => out.write(`cdsc: INFO: ${msg}\n`));
}
// Execute the command line option '--to-cdl' and display the results.

@@ -191,8 +175,8 @@ // Return the original model (for chaining)

let hanaResult;
if(options.newTransformers) {
if(options.oldTransformers) {
hanaResult = compiler.toHana(model);
} else {
let csn = compactModel(model, options);
csn.messages = model.messages; // pass messages as compactModel misses that
hanaResult = toHanaWithCsn(csn, options)
} else {
hanaResult = compiler.toHana(model);
}

@@ -210,8 +194,8 @@ for (let name in hanaResult.hdbcds) {

let odataResult;
if(options.newTransformers) {
if(options.oldTransformers) {
odataResult = compiler.toOdata(model)
} else {
let csn = compactModel(model, options);
csn.messages = model.messages; // pass messages as compactModel misses that
odataResult = toOdataWithCsn(csn, options)
} else {
odataResult = compiler.toOdata(model)
odataResult = toOdataWithCsn(csn, options);
}

@@ -243,2 +227,5 @@ translatePathLocations(model.messages, model);

// Return the original model (for chaining)
//
/// THIS MUST SURVIVE IF WE REMOVE THE OLD API
/// DO NOT DELETE THIS TORENAME FUNCTIONALITY!!
function toRename( model ) {

@@ -259,3 +246,3 @@ let renameResult = compiler.toRename(model);

function toSql( model ) {
let sqlResult = options.newTransformers ? toSqlWithCsn(compactModel(model, options), options) : compiler.toSql(model);
let sqlResult = options.oldTransformers ? compiler.toSql(model) : toSqlWithCsn(compactModel(model, options), options);

@@ -268,2 +255,3 @@ ['hdbtabletype', 'hdbtable', 'hdbindex', 'hdbfulltextindex', 'hdbview', 'sql'].forEach(pluginName => {

displayNamedXsnOrCsn(sqlResult._augmentedCsn, sqlResult.csn, 'sql_csn', options);
model.messages = sqlResult.messages;
return model;

@@ -291,3 +279,3 @@ }

if (options.rawOutput)
console.error( util.inspect( reveal( err.model ), false, null ));
console.error( util.inspect( reveal( err.model, options.rawOutput ), false, null ));
else

@@ -315,6 +303,15 @@ displayMessages( err.model, err.errors );

for (let msg of messages) {
if (options.internalMsg)
if (options.internalMsg) {
console.error( util.inspect( msg, { depth: null, maxArrayLength: null} ) );
else if (messageLevels[ msg.severity ] <= options.warning)
console.error( compiler.messageString( msg, normalizeFilename, !options.showMessageId ) );
} else if (messageLevels[ msg.severity ] <= options.warning) {
if (options.noMessageContext) {
console.error(compiler.messageString(msg, normalizeFilename, !options.showMessageId));
} else {
console.error(compiler.messageStringMultiline(msg, normalizeFilename, !options.showMessageId));
const fullfilepath = (msg.location && msg.location.filename )? path.resolve('', msg.location.filename) : undefined;
const sourceLines = fileCache[fullfilepath] ? fileCache[fullfilepath].split('\n') : fileCache;
console.error(compiler.messageContext(sourceLines, msg));
}
}
}

@@ -339,3 +336,9 @@ }

if (options.rawOutput) {
writeToFileOrDisplay(options.out, name + '_raw.txt', util.inspect(reveal(xsn), false, null), true);
if(options.toCsn && options.toCsn.associations === "joins"){
options.forHana = {};
options.forHana.associations = options.toCsn.associations;
writeToFileOrDisplay(options.out, name + '_raw.txt', util.inspect(reveal(translateAssocsToJoins(xsn), options.rawOutput), false, null), true);
} else {
writeToFileOrDisplay(options.out, name + '_raw.txt', util.inspect(reveal(xsn, options.rawOutput), false, null), true);
}
}

@@ -348,3 +351,3 @@ else if (options.internalMsg) {

if (options.enrichCsn)
enrichCsn( csn );
enrichCsn( csn, options );
writeToFileOrDisplay(options.out, name + '.json', csn, true);

@@ -390,3 +393,3 @@ }

function catchErrors (err) {
if (err instanceof Error && err.hasBeenReported)
if (err instanceof Error && err['hasBeenReported'])
return;

@@ -393,0 +396,0 @@ console.error( '' );

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

artref: 'm', paramname: 'b',
Entity: 'D', Enum: 'H', Index: 'J', AnnoDef: 'V', Extend: 'A', Annotate: 'A', Event: 'Y'
Entity: 'D', Enum: 'H', Index: 'J', AnnoDef: 'V', Extend: 'Z', Annotate: 'Z', Event: 'Y'
}

@@ -35,3 +35,3 @@

if (cat !== 'ref' || chars[ tok.start ] !== '$')
chars[ tok.start ] = categoryChars[ cat ] || cat.charAt();
chars[ tok.start ] = categoryChars[ cat ] || cat.charAt(0);
if (tok.stop > tok.start) // stop in ANTLR at last char, not behind

@@ -38,0 +38,0 @@ chars[ tok.start+1 ] = '_';

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

const commands = {
complete, lint
complete, find, lint
}

@@ -23,2 +23,3 @@

const compiler = require('../lib/main');
const { locationString } = require('../lib/base/messages');

@@ -31,2 +32,3 @@ let argv = process.argv;

let frel = path.relative( '', file||'' );
// TODO: proper realname

@@ -41,3 +43,7 @@ if (argv.length > 5 && cmd && line > 0 && column > 0)

console.error( 'ERROR:', err.toString() );
console.error( 'Usage: cdsse complete <line> <col> <file> [-]' );
console.error( 'Usage: cdsse <cmd> <line> <col> <file> [-]' );
console.error( '----------- supported commands <cmd>:' );
console.error( ' complete: syntactic and semantic code completion' );
console.error( ' find: location of definition' );
console.error( ' lint: linter (<line> and <col> are ignored, should be numbers)' );
process.exitCode = 2;

@@ -50,3 +56,3 @@ return false;

return usage( err );
let off = offset( buf );
const off = offset( buf );
if (!off) // outside buffer range

@@ -69,3 +75,3 @@ return usage();

let fname = path.resolve( '', file );
compiler.compile( [file], '', { attachValidNames: true, lintMode: true, betaMode: true } , { [fname]: src } )
compiler.compile( [file], '', { attachValidNames: true, lintMode: true, beta: true } , { [fname]: src } )
.then( ident, ident );

@@ -78,3 +84,3 @@ }

return usage( xsnOrErr );
let vn = messageAt( xsnOrErr, 'validNames', off.prefix ) || Object.create(null);
let vn = messageAt( xsnOrErr, 'validNames', off && off.prefix ) || Object.create(null);
// TODO: if there is no such message, use console.log( 'arbitrary identifier' )

@@ -89,2 +95,65 @@ // if we want to avoid that the editor switches to fuzzy completion match

function find( err, buf ) {
if (err)
return usage( err );
let fname = path.resolve( '', file );
compiler.compile( [file], '', { lintMode: true, beta: true } , { [fname]: buf } )
.then( select, () => true );
return true;
function select( xsn ) {
const [src] = Object.values( xsn.sources );
pathitem( src.usings );
pathitem( src.extensions );
pathitem( src.artifacts );
return true;
}
}
const inspect = {
'_': () => false,
'$': () => false,
id: () => false,
location: () => false,
queries: () => false, // XSN TODO: remove
origin: () => false, // XSN TODO: remove?
elements: (n, p) => (!p || !p.query && !p.columns) && pathitem( n ),
}
function pathitem( node ) {
if (!node || typeof node !== 'object')
return false;
else if (Array.isArray( node ))
return node.some( pathitem );
else if (!Object.getPrototypeOf( node ))
return Object.values( node ).some( pathitem );
const { location } = node;
if (!location || node.$inferred || location.$weak)
return false;
if (location.filename !== frel)
return false;
if (location.start.line > line || location.start.line === line && location.start.column > column)
return 'stop'; // 'stop' means: stop search if in array or dictionary
if (location.end.line < line || location.end.line === line && location.end.column < column) {
if (!node.id)
return false;
}
else if (node.id) {
const art = node._artifact;
// TODO: we could skip over some internal artifacts here
if (art && art.location)
console.log( locationString( art.name.location || art.location ) + ': Definition' );
return true; // true means: stop in array or dictionary or other object
}
let found = false;
for (const prop in node) {
found = (inspect[prop] || inspect[prop.charAt(0)] || pathitem)( node[prop], node ) || found;
if (found === true)
return found;
}
return found;
}
function lint( err, buf ) {

@@ -94,3 +163,3 @@ if (err)

let fname = path.resolve( '', file );
compiler.compile( [file], '', { lintMode: true, betaMode: true } , { [fname]: buf } )
compiler.compile( [file], '', { lintMode: true, beta: true } , { [fname]: buf } )
.then( display, display );

@@ -114,6 +183,6 @@ return true;

if (typeof symbol === 'string') {
if (n.length > 3 && n.charAt() === "'" && n.charAt(1) === symbol)
if (n.length > 3 && n.charAt(0) === "'" && n.charAt(1) === symbol)
console.log( n.slice( 2, -1 ), 'symbolCont' );
}
else if (n.charAt() === "'") {
else if (n.charAt(0) === "'") {
if (symbol)

@@ -135,4 +204,8 @@ console.log( n.slice( 1, -1 ), 'symbol' );

/**
* Returns offsets of current position and start of prefix
* @param {string} buf
* @returns {false | { cursor: number, prefix: number, hasId?: boolean }} Returns false if 'line' is out-of-range.
*/
function offset( buf ) { // for line and column
// Returns offsets of current position and start of prefix
let pos = 0;

@@ -139,0 +212,0 @@ for (let l = line-1; l; --l) {

@@ -15,3 +15,3 @@ # Command Line Migration

Usage is now `cdsc <command> [options] <file...>` instead of `cdsc [options] <file...>`.
Usage is now `cdsc <command> [options] <files...>` instead of `cdsc [options] <file...>`.

@@ -36,10 +36,10 @@ The generation options (`--toHana`, `--toSql`, ...) have been replaced by commands

Commands
H, toHana [options] <file...> Generate HANA CDS source files
O, toOdata [options] <file...> Generate ODATA metadata and annotations
C, toCdl <file...> Generate CDS source files
S, toSwagger [options] <file...> Generate Swagger (OpenAPI) JSON
Q, toSql [options] <file...> Generate SQL DDL statements
toCsn [options] <file...> (default) Generate original model as CSN
toTntSpecificOutput <file...> (internal) Generate TNT-specific post-processed CSN
toRename [options] <file...> (internal) Generate SQL DDL rename statements
H, toHana [options] <files...> Generate HANA CDS source files
O, toOdata [options] <files...> Generate ODATA metadata and annotations
C, toCdl <files...> Generate CDS source files
S, toSwagger [options] <files...> Generate Swagger (OpenAPI) JSON
Q, toSql [options] <files...> Generate SQL DDL statements
toCsn [options] <files...> (default) Generate original model as CSN
toTntSpecificOutput <files...> (internal) Generate TNT-specific post-processed CSN
toRename [options] <files...> (internal) Generate SQL DDL rename statements
```

@@ -46,0 +46,0 @@

@@ -143,1 +143,34 @@ # Error Messages Explained

would provide little benefit, but would add complexity and confusion.
### Redirection issues
The target `OrigTarget` of an existing association can only be redirected to another target `NewTarget`
if the `NewTarget` is a direct or indirect projection of `OrigTarget`
(complex views are questionable and lead to a Warning),
or an entity definition which directly or indirectly includes `OrigTarget`.
```
entity Base {
key i: Integer;
}
entity Proj as projection on Base;
entity NewTarget as projection on Intermediate;
entity Intermediate as projection on Base;
entity Assocs {
base: association to Base;
proj: association to Proj;
}
entity Redirect as projection on Assocs {
base: redirected to NewTarget, // works
proj: redirected to NewTarget // ERROR: does not originate from Proj
}
```
For the above CDS code, you get the following error message:
```
redirect-to-unrelated.cds:16:25-34: Error: The redirected target does not originate from "Proj"
(in entity:"Redirect"/element:"proj")
```

@@ -753,2 +753,3 @@ # Name Resolution in CDS

## Summary
[summary]: #summary

@@ -755,0 +756,0 @@

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

const { transformForHanaWithCsn } = require('./transform/forHanaNew');
const { compact, compactSorted } = require('./json/compactor')
const { compactModel, sortCsn } = require('./json/to-csn')
const { toCdsSource } = require('./render/toCdl');
const { toCdsSource, toCdsSourceCsn } = require('./render/toCdl');
const { toSqlDdl } = require('./render/toSql');

@@ -21,3 +20,2 @@ const { toRenameDdl } = require('./render/toRename');

const { setProp } = require('./base/model');
var { CompilationError, sortMessages } = require('./base/messages');
const { optionProcessor } = require('./optionProcessor')

@@ -97,3 +95,3 @@

result._augmentedCsn = forHanaAugmented;
result.csn = options.newCsn === false ? compactSorted(forHanaAugmented) : compactModel(forHanaAugmented);
result.csn = compactModel(forHanaAugmented);
if(options.testMode)

@@ -126,16 +124,6 @@ result.csn = sortCsn(result.csn);

// Backward compatibility for old naming modes
// FIXME: Remove after a few releases
const { warning, signal } = alerts(csn);
if (options.toHana.names == 'flat') {
signal(warning`Option "{ toHana.names: 'flat' }" is deprecated, use "{ toHana.names: 'plain' }" instead`);
options.toHana.names = 'plain';
}
else if (options.toHana.names == 'deep') {
signal(warning`Option "{ toHana.names: 'deep' }" is deprecated, use "{ toHana.names: 'quoted' }" instead`);
options.toHana.names = 'quoted';
}
const { warning, signal } = alerts(csn, options);
// Verify options
optionProcessor.verifyOptions(options, 'toHana').map(complaint => signal(warning`${complaint}`));
optionProcessor.verifyOptions(options, 'toHana', true).map(complaint => signal(warning`${complaint}`));

@@ -148,4 +136,6 @@ // Special case: For naming variant 'hdbcds' in combination with 'toHana' (and only there!), 'forHana'

options = mergeOptions(options, { forHana: { dialect: 'hana' } }, { forHana : options.toHana });
// Prepare model for HANA (transferring the options to forHana, and setting 'dialect' to 'hana', because 'toHana' is only used for that)
let forHanaCsn = transformForHanaWithCsn(csn, mergeOptions(options, { forHana: { dialect: 'hana' } }, { forHana : options.toHana } ));
let forHanaCsn = transformForHanaWithCsn(csn, options);

@@ -155,6 +145,11 @@ // Assemble result

if (options.toHana.src) {
result.hdbcds = toCdsSource(forHanaCsn, options);
if(options.testMode){
const sorted = sortCsn(forHanaCsn, true);
setProp(sorted, "options", forHanaCsn.options);
result.hdbcds = toCdsSourceCsn(sorted, options);
} else {
result.hdbcds = toCdsSourceCsn(forHanaCsn, options);
}
}
if (options.toHana.csn) {
result._augmentedCsn = options.testMode ? sortCsn(forHanaCsn) : forHanaCsn;
result.csn = options.testMode ? sortCsn(forHanaCsn) : forHanaCsn;

@@ -190,2 +185,3 @@ }

// toOdata.version
// toOdata.odataFormat
// toOdata.xml

@@ -265,5 +261,3 @@ // toOdata.json

if (options.toOdata.csn) {
// TODO: make compactSortedJson the default
result.csn = options.newCsn === false ? (options.testMode ? compactSorted(forOdataAugmented) : compact(forOdataAugmented))
: compactModel(forOdataAugmented);
result.csn = compactModel(forOdataAugmented);
result._augmentedCsn = forOdataAugmented;

@@ -309,4 +303,2 @@ }

function toOdataWithCsn(csn, options) {
const { error, warning, signal } = alerts(csn);
// In case of API usage the options are in the 'options' argument

@@ -329,15 +321,6 @@ // put the OData specific options under the 'options.toOdata' wrapper

// Backward compatibility for old naming modes
// FIXME: Remove after a few releases
if (options.toOdata.names == 'flat') {
signal(warning`Option "{ toOdata.names: 'flat' }" is deprecated, use "{ toOdata.names: 'plain' }" instead`);
options.toOdata.names = 'plain';
}
else if (options.toOdata.names == 'deep') {
signal(warning`Option "{ toOdata.names: 'deep' }" is deprecated, use "{ toOdata.names: 'quoted' }" instead`);
options.toOdata.names = 'deep';
}
const { error, warning, signal } = alerts(csn, options);
// Verify options
optionProcessor.verifyOptions(options, 'toOdata').map(complaint => signal(warning`${complaint}`));
optionProcessor.verifyOptions(options, 'toOdata', true).map(complaint => signal(warning`${complaint}`));

@@ -349,3 +332,3 @@ // Prepare model for ODATA processing

services: Object.create(null),
messages: csn.messages,
messages: forOdataCSN.messages,
}

@@ -396,2 +379,14 @@ if (options.toOdata.csn) {

// Generate edmx for given 'service' based on 'csn' (new-style compact, already prepared for OData)
// using 'options'
function preparedCsnToEdmxAll(csn, options) {
// Merge options with those from model
options = mergeOptions(csn.options, options);
let edmx = csn2edmAll(csn, options);
for(const service in edmx){
edmx[service] = edmx[service].toXML('all');
}
return edmx;
}
// Generate edm-json for given 'service' based on 'csn' (new-style compact, already prepared for OData)

@@ -406,2 +401,14 @@ // using 'options'

// Generate edm-json for given 'service' based on 'csn' (new-style compact, already prepared for OData)
// using 'options'
function preparedCsnToEdmAll(csn, options) {
// Merge options with those from model, override OData version as edm json is always v4
options = mergeOptions(csn.options, options, { toOdata : { version : 'v4' }});
let edmj = csn2edmAll(csn, options);
for(const service in edmj){
edmj[service] = edmj[service].toJSON();
}
return edmj;
}
// ----------- toCdl -----------

@@ -425,4 +432,7 @@

function toCdl(model, options) {
const { warning, signal } = alerts(model);
options = handleToCdlOptions(model, options);
return toCdsSource(model, options);
}
function handleToCdlOptions(model, options, silent=false){
// In case of API usage the options are in the 'options' argument

@@ -437,7 +447,17 @@ // put the OData specific options under the 'options.toCdl' wrapper

options = mergeOptions({ toCdl : true }, model.options, options);
const { warning, signal } = alerts(model, options);
// Verify options
optionProcessor.verifyOptions(options, 'toCdl').map(complaint => signal(warning`${complaint}`));
return toCdsSource(model, options);
optionProcessor.verifyOptions(options, 'toCdl', silent).map(complaint => signal(warning`${complaint}`));
return options;
}
function toCdlWithCsn(csn, options){
options = handleToCdlOptions(csn, options, true);
const result = toCdsSourceCsn(csn, options);
return { result, options };
}
// ----------- toSwagger -----------

@@ -612,2 +632,3 @@

// Assemble result
/** @type {object} */
let result = {};

@@ -619,3 +640,3 @@ if (options.toSql.src) {

result._augmentedCsn = forSqlAugmented;
result.csn = options.newCsn === false ? compactSorted(forSqlAugmented) : compactModel(forSqlAugmented);
result.csn = compactModel(forSqlAugmented);
}

@@ -651,8 +672,6 @@

function toSqlWithCsn(model, options) {
const { warning, error, signal } = alerts(model);
// when toSql is invoked via the CLI - toSql options are under model.options
// ensure the desired format of the user option
if (model.options && model.options.toSql &&(model.options.toSql.user || model.options.toSql.locale)) {
transforUserOption(model.options.toSql);
transformUserOption(model.options.toSql);
}

@@ -670,3 +689,3 @@

if (options && (options.toSql.user || options.toSql.locale)){
transforUserOption(options.toSql);
transformUserOption(options.toSql);
}

@@ -682,15 +701,6 @@

// Backward compatibility for old naming modes
// FIXME: Remove after a few releases
if (options.toSql.names == 'flat') {
signal(warning`Option "{ toSql.names: 'flat' }" is deprecated, use "{ toSql.names: 'plain' }" instead`);
options.toSql.names = 'plain';
}
else if (options.toSql.names == 'deep') {
signal(warning`Option "{ toSql.names: 'deep' }" is deprecated, use "{ toSql.names: 'quoted' }" instead`);
options.toSql.names = 'quoted';
}
const { warning, error, signal } = alerts(model, options);
// Verify options
optionProcessor.verifyOptions(options, 'toSql').map(complaint => signal(warning`${complaint}`));
optionProcessor.verifyOptions(options, 'toSql', true).map(complaint => signal(warning`${complaint}`));

@@ -734,2 +744,3 @@ // FIXME: Currently, '--to-sql' implies transformation for HANA (transferring the options to forHana)

// Assemble result
/** @type {object} */
let result = {};

@@ -740,4 +751,3 @@ if (options.toSql.src) {

if (options.toSql.csn) {
result._augmentedCsn = options.testMode ? sortCsn(forSqlCsn) : forSqlCsn;
result.csn = options.newCsn === false || options.testMode? sortCsn(forSqlCsn) : forSqlCsn;
result.csn = options.testMode ? sortCsn(forSqlCsn) : forSqlCsn;
}

@@ -754,3 +764,3 @@

// "id" and/or "locale" prop(s)
function transforUserOption(options) {
function transformUserOption(options) {
// move the user option value under user.id if specified as a string

@@ -856,4 +866,2 @@ if (options.user && typeof options.user === 'string' || options.user instanceof String) {

// testMode : if true, the result is extra-stable for automated tests (sorted, no 'version')
// newCsn : if false, CSN is returned in the old format '0.1.0'
// (default is the new format '1.0')
// toCsn.flavor : if 'gensrc', the result CSN is only suitable for use as a source, e.g. for combination with

@@ -867,5 +875,3 @@ // additional extend/annotate statements, but not for consumption by clients or backends

function toCsn(model, options) {
const { error, warning, signal } = alerts(model);
// Can't have an optional wrapper here because 'testMode' and 'newCsn' are global options
const { warning, signal } = alerts(model);
// In case of API usage the options are in the 'options' argument

@@ -884,8 +890,3 @@ // put the OData specific options under the 'options.toCsn' wrapper

if (options.toCsn.gensrc && !options.newCsn === false) {
signal(error`CSN in "gensrc" flavor can only be generated as new-style CSN (option "newCsn" required)`);
throw new CompilationError(sortMessages(model.messages), model);
}
return options.newCsn === false ? (options.testMode ? compactSorted(model) : compact(model))
: compactModel(model);
return compactModel(model);
}

@@ -905,2 +906,3 @@

version : 'v4',
odataFormat: 'flat',
},

@@ -939,4 +941,7 @@ toRename: {

preparedCsnToEdmx,
preparedCsnToEdmxAll,
preparedCsnToEdm,
preparedCsnToEdmAll,
toCdl,
toCdlWithCsn,
toSwagger,

@@ -943,0 +948,0 @@ toSql,

@@ -23,3 +23,4 @@ // Implementation of alerts

var locale = process.env.LANG || '';
var language = locale.match(/[a-z]+/);
var languageMatches = locale.match(/[a-z]+/);
var language = (languageMatches && languageMatches.length > 0 && languageMatches[0]) || "";

@@ -50,2 +51,5 @@ // load predefined alerts

signal.ino = info;
let config = options.severities || {};
return {

@@ -83,7 +87,10 @@ info, warning, error, // tag functions for the different alerts

let msg = translation[language] || key;
// Replace the parameters by the corresponding values
/**
* Replace the parameters by the corresponding values
* @type {object} Not a primitive string but a String wrapper object.
*/
let result = new String(msg.replace(/{(\d)}/g, (_, index) => values[Number(index)]));
if (alert.severity === null || typeof alert.severity === 'string')
// in simple cases (no complex object) just use this severity
result._severity = alert.severity;
result._severity = alert.severity; // special property on this string
else if (alert.severity === undefined)

@@ -102,12 +109,12 @@ result._severity = severity;

* exception, the corresponding error is thrown.
*
* @param {any} msg Message text
* @param {any} location Location information
* @param {any} severity severity: Info, Warning, Error
*
* @param {any} msg Message text
* @param {any} [location] Location information
* @param {string} [severity] severity: Info, Warning, Error
* @param {string} [message_id=''] Message ID
* @param {any} context Further context information
* @param {any} [context] Further context information
* @returns {Boolean} true, if no 'Error' alert was handled.
*/
function signal(msg, location, severity, message_id='', context) {
var err = messageBuilder(model, msg, location, severity, message_id, context);
var err = messageBuilder(model, msg, location, message_id in config ? config[message_id] : severity, message_id, context);
if (err.severity)

@@ -114,0 +121,0 @@ // don't collect stuff that doesn't have a severity

@@ -9,3 +9,3 @@ const { CompileMessage } = require('../base/messages');

location = searchForLocation(semanticLocation);
}

@@ -22,3 +22,3 @@ return new CompileMessage(location, msg, severity ? severity : msg._severity, message_id, beautifySemanticLocation(semanticLocation), context);

currentThing = currentThing[step];
if(currentThing && currentThing.$location){

@@ -28,3 +28,3 @@ last_location = currentThing.$location;

}
return last_location;

@@ -38,7 +38,7 @@ }

}
if(semanticLocation[0] !== 'definitions'){
throw new Error('Semantic locations must start with "definitions", found: ' + semanticLocation[0]);
}
if(semanticLocation.length == 1){

@@ -64,3 +64,3 @@ throw new Error('Semantic locations must at least point to an artifact!');

}
function quoted( name ) {

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

@@ -5,2 +5,4 @@ // Functions and classes for syntax messages

const term = require('../utils/term');
// For messageIds, where no severity has been provided via code (central def)

@@ -22,2 +24,8 @@ const standardSeverities = {

const standardTexts = {
'syntax-csn-expected-object': 'Expected object for property $(PROP)',
'syntax-csn-expected-column': 'Expected object or string \'*\' for property $(PROP)',
'syntax-csn-expected-natnum': 'Expected non-negative number for property $(PROP)',
'syntax-csn-expected-cardinality': 'Expected non-negative number or string \'*\' for property $(PROP)',
'syntax-csn-expected-reference': 'Expected non-empty string or object for property $(PROP)',
'syntax-csn-expected-term': 'Expected non-empty string or object for property $(PROP)',
'syntax-anno-after-struct': 'Avoid annotation assignments after structure definitions',

@@ -54,4 +62,4 @@ 'syntax-anno-after-enum': 'Avoid annotation assignments after enum definitions',

'expected-type': 'A type or an element of a type is expected here',
'expected-entity': 'An entity, projection or view is expected here',
'expected-source': 'A query source must be an entity or an association to an entity',
'expected-entity': 'A non-abstract entity, projection or view is expected here',
'expected-source': 'A query source must be a non-abstract entity or an association to an entity',
}

@@ -76,18 +84,43 @@

return loc;
return (!loc.end || loc.$weak)
? `${filename}:${loc.start.line}:${loc.start.column}`
: (loc.start.line == loc.end.line)
? `${filename}:${loc.start.line}:${loc.start.column}-${loc.end.column}`
: `${filename}:${loc.start.line}.${loc.start.column}-${loc.end.line}.${loc.end.column}`;
if (!loc.end || loc.$weak) {
return (loc.start.column)
? `${filename}:${loc.start.line}:${loc.start.column}`
: `${filename}:${loc.start.line}`;
}
else {
return (loc.start.line == loc.end.line)
? `${filename}:${loc.start.line}:${loc.start.column}-${loc.end.column}`
: `${filename}:${loc.start.line}.${loc.start.column}-${loc.end.line}.${loc.end.column}`;
}
}
// Class for combined compiler errors. Additional members:
// `errors`: vector of errors (CompileMessage and errors from peg.js)
// `model`: the CSN model
// TODO: standard param order
/**
* Class for combined compiler errors. Additional members:
* `errors`: vector of errors (CompileMessage and errors from peg.js)
* `model`: the CSN model
* TODO: standard param order
* @class CompilationError
* @extends {Error}
*/
class CompilationError extends Error {
/**
* Creates an instance of CompilationError.
* @param {array} errs vector of errors (CompileMessage and errors from peg.js)
* @param {object} [model] the CSN model
* @param {string} [text] Text of the error
* @param {any} args Any args to pass to the super constructor
*
* @memberOf CompilationError
*/
constructor(errs, model, text, ...args) {
super( text || 'CDS compilation failed\n' + errs.map( m => m.toString() ).join('\n'),
// @ts-ignore Error does not take more arguments according to TypeScript...
...args );
this.errors = errs; // TODO: rename to messages
/** @type {object} model */
this.model;
/** @type {boolean} model */
this.hasBeenReported = false;
Object.defineProperty( this, 'model', { value: model, configurable: true } );

@@ -105,3 +138,3 @@ }

* Class for individual compile errors.
*
*
* @class CompileMessage

@@ -116,6 +149,6 @@ * @extends {Error}

* @param {string} [severity='Error'] Severity: Debug, Info, Warning, Error
* @param {any} id The ID of the message - visible as property messageId
* @param {any} home
* @param {any} context Some further context information, if needed
*
* @param {string} [id] The ID of the message - visible as property messageId
* @param {any} [home]
* @param {any} [context] Some further context information, if needed
*
* @memberOf CompileMessage

@@ -144,6 +177,8 @@ */

return loc;
let location = { filename: loc.file, start: { line: loc.line, column: loc.col } };
if (loc.endLine)
location.end = { line: loc.endLine, column: loc.endCol };
else
let location = {
filename: loc.file,
start: { line: loc.line, column: loc.col || 0 },
end: { line: loc.endLine || loc.line, column: loc.endCol || loc.col || 0 },
};
if (!loc.endLine)
location.$weak = true;

@@ -180,3 +215,3 @@ return location;

// If those do not exist, define a non-enumerable property `messages` in `model`.
function getMessageFunction( model, options = model.options || {} ) {
function getMessageFunction( model, options = model.options || {}, transform ) {
let messages = options.messages || model.messages ||

@@ -196,3 +231,3 @@ Object.defineProperty( model, 'messages',

? params
: messageText( texts || standardTexts[id], params );
: messageText( texts || standardTexts[id], params, transform );
let msg = new CompileMessage( location, text, s, id,

@@ -217,2 +252,7 @@ (typeof home === 'string' ? home : homeName(home)) );

id: quoted,
prop: n => "'" + n + "'",
otherprop: n => "'" + n + "'",
kind: n => '"' + n + '"',
delimited: n => "![" + n + ']',
// msg: m => m,
file: s => "'" + s.replace( /'/g, "''" ) + "'", // sync ;

@@ -222,4 +262,4 @@ };

function transformAnno( anno ) {
return (anno.charAt() === '@') ? quoted( anno ) : quoted( '@' + anno );
// if (anno.charAt() === '@')
return (anno.charAt(0) === '@') ? quoted( anno ) : quoted( '@' + anno );
// if (anno.charAt(0) === '@')
// anno = anno.slice(1);

@@ -277,3 +317,3 @@ // return (!anno || /[^A-Za-z_0-9.]/.test(anno)) ? msgName( '@' + anno ) : '@' + anno;

function messageText( texts, params ) {
function messageText( texts, params, transform ) {
if (typeof texts === 'string')

@@ -283,3 +323,3 @@ texts = { std: texts };

for (let p in params) {
let t = paramsTransform[p];
let t = transform && transform[p] || paramsTransform[p];
args[p] = (t) ? t( params[p], args, params, texts ) : params[p];

@@ -292,3 +332,3 @@ }

function replaceInString( text, params ) {
let pattern = /\$\(([A-Z]+)\)/g;
let pattern = /\$\(([A-Z_]+)\)/g;
let parts = [];

@@ -316,3 +356,6 @@ let start = 0;

// Return message string with location if present
// Return message string with location if present in compact form (i.e. one line)
//
// Example:
// <source>.cds:3:11: Error: cannot find value `nu` in this scope
function messageString( err, normalizeFilename, noMessageId, noHome ) {

@@ -324,4 +367,73 @@ return (err.location ? locationString( err.location, normalizeFilename ) + ': ' : '') +

(!err.home || noHome && err.location && !err.location.$weak ? '' : ' (in ' + err.home + ')');
}
// Return message string with location if present.
//
// Example:
// Error: cannot find value `nu` in this scope
// <source>.cds:3:11
//
function messageStringMultiline( err, normalizeFilename, noMessageId, noHome ) {
const msgId = (err.messageId && !noMessageId ? '[' + err.messageId + ']' : '');
const home = (!err.home || noHome && err.location && !err.location.$weak ? '' : ' (in ' + err.home + ')');
const severity = err.severity || 'Error';
const location = (err.location ? '\n=> ' + locationString( err.location, normalizeFilename ) : '');
return term.asSeverity(severity, severity + msgId) + ': ' + err.message + home + location;
}
/**
* Returns a context (code) string that is human readable (similar to rust's compiler)
*
* Example Output:
* |
* 3 | num * nu
* | ^^
*
* @param {string[]} sourceLines The source code split up into lines, e.g. by `src.split('\n')`
* @param {object} err Error object containing all details like line, message, etc.
*/
function messageContext(sourceLines, err) {
const loc = normalizeLocation(err.location);
if (!loc || !loc.start) {
return "";
}
// Lines are 1-based, we need 0-based ones for arrays
const startLine = loc.start.line - 1;
const endLine = loc.end ? loc.end.line - 1 : startLine;
// check that source lines exists
if (typeof sourceLines[startLine] !== 'string' || typeof sourceLines[endLine] !== 'string') {
return "";
}
const digits = String(endLine + 1).length;
const severity = err.severity || 'Error';
const indent = ' '.repeat(2 + digits);
const endColumn = loc.end ? loc.end.column : loc.start.column + 1;
/** Only print N lines even if the error spans more lines. */
const maxLine = Math.min((startLine + 2), endLine);
let msg = indent + '| ' + '\n';
// print source line(s)
for (let line = startLine; line <= maxLine; line++) {
msg += ' ' + String(line + 1).padStart(digits, ' ') + ' | ' + sourceLines[line].replace(/\t/g, ' ') + '\n';
}
if (startLine === endLine) {
// highlight only for one-line location
msg += indent + '| ' + term.asSeverity(severity, ' '.repeat(Math.max(0, loc.start.column - 1)).padEnd(Math.max(0, endColumn), '^')) + '\n';
} else if (maxLine != endLine) {
// error spans more lines which we don't print
msg += indent + '| ...' + '\n';
} else {
msg += indent + '| ' + '\n';
}
return msg;
}
// Compare two messages `a` and `b`. Return 0 if they are equal, 1 if `a` is

@@ -354,3 +466,3 @@ // larger than `b`, and -1 if `a` is smaller than `b`. Messages without a location

let r = (name.absolute) ? [ quoted( name.absolute ) ] : [];
if (name.query || name.query != null && art.kind !== 'element' || name.$mixin ) // Yes, omit $query.0 for element - TODO: rename to block
if (name.query || name.query != null && art.kind !== 'element' || name.$mixin ) // Yes, omit $query.0 for element - TODO: rename to block
r.push( (art.kind === 'block' ? 'block:' : 'query:') + (name.query + 1) );

@@ -364,2 +476,3 @@ if (name.action && omit !== 'action')

if (name.element && omit !== 'element')
// r.push( `${ art.kind }: ${ quoted( name.element )}` ); or even better element:"assoc"/key:"i" same with enum
r.push( (art.kind === 'enum' ? 'enum:' : 'element:') + quoted( name.element ) );

@@ -424,2 +537,4 @@ return r.join('/');

messageString,
messageStringMultiline,
messageContext,
searchName,

@@ -432,4 +547,5 @@ getMessageFunction,

CompilationError,
normalizeLocation,
translatePathLocations
}

@@ -88,4 +88,7 @@ //

// Like `obj.prop = value`, but not contained in JSON / CSN
// It's important to set enumerable explicitly to false (although 'false' is the default),
// as else, if the property already exists, it keeps the old setting for enumerable.
function setProp (obj, prop, value) {
Object.defineProperty( obj, prop, { value, configurable: true, writable: true } );
let descriptor = { value, configurable: true, writable: true, enumerable: false };
Object.defineProperty( obj, prop, descriptor );
}

@@ -92,0 +95,0 @@

@@ -7,3 +7,3 @@ 'use strict'

// The following definitions should be made
// let optionProcessor = createOptionProcessor();
// const optionProcessor = createOptionProcessor();
// optionProcessor

@@ -17,5 +17,5 @@ // .help(`General help text`);

// .option(' --bar-wiz <w>', ['bla', 'fasel'])
// Options must have a long form, can have at most one <param>, and optionally
// an array of valid param values. Commands and param values must not start
// with '--'. The whole processor and each command may carry a help text.
// Options *must* have a long form, can have at most one <param>, and optionally
// an array of valid param values as strings. Commands and param values must not
// start with '--'. The whole processor and each command may carry a help text.
// To actually parse a command line, use

@@ -25,7 +25,10 @@ // optionProcessor.processCmdLine(process.argv);

function createOptionProcessor() {
let optionProcessor = {
const optionProcessor = {
commands: {},
options: {},
positionalArguments: [],
optionClashes: [],
option,
command,
positionalArgument,
help,

@@ -40,16 +43,15 @@ processCmdLine,

// API: Define a general option
/**
* API: Define a general option.
* @param {string} optString Option string describing the command line option.
* @param {string[]} [validValues] Array of valid values for the options.
*/
function option(optString, validValues) {
let opt = _parseOptionString(optString, validValues);
optionProcessor.options[opt.longName] = opt;
if (opt.shortName) {
if (optionProcessor.options[opt.shortName]) {
throw new Error(`Duplicate assignment for short option ${opt.shortName}`);
}
optionProcessor.options[opt.shortName] = opt;
}
return optionProcessor;
return _addOption(optionProcessor, optString, validValues);
}
// API: Define the main help text (header and general options)
/**
* API: Define the main help text (header and general options)
* @param {string} text Help text describing all options, etc.
*/
function help(text) {

@@ -60,5 +62,8 @@ optionProcessor.helpText = text;

// API: Define a command
/**
* API: Define a command
* @param {string} cmdString Command name, e.g. 'S, toSql'
*/
function command(cmdString) {
let command = {
const command = {
options: {},

@@ -80,11 +85,3 @@ option,

function option(optString, validValues) {
let opt = _parseOptionString(optString, validValues);
command.options[opt.longName] = opt;
if (opt.shortName) {
if (command.options[opt.shortName]) {
throw new Error(`Duplicate assignment for short option ${opt.shortName}`);
}
command.options[opt.shortName] = opt;
}
return command;
return _addOption(command, optString, validValues);
}

@@ -99,2 +96,69 @@

/**
* Adds positional arguments to the command line processor. Instructs the processor
* to either require N positional arguments or a dynamic number (but at least one)
* @param {string} positionalArgumentDefinition Positional arguments, e.g. '<input> <output>' or '<files...>'
*/
function positionalArgument(positionalArgumentDefinition) {
if (optionProcessor.positionalArguments.find((arg) => arg.isDynamic)) {
throw new Error(`Can't add positional arguments after a dynamic one`);
}
const registeredNames = optionProcessor.positionalArguments.map((arg) => arg.name);
const args = positionalArgumentDefinition.split(' ');
for (const arg of args) {
const argName = arg.replace('<', '').replace('>', '').replace('...', '');
if (registeredNames.includes(argName)) {
throw new Error(`Duplicate positional argument ${arg}`);
}
if (!isParam(arg) && !isDynamicPositionalArgument(arg)) {
throw new Error(`Unknown positional argument syntax: ${arg}`)
}
optionProcessor.positionalArguments.push({
name: argName,
isDynamic: isDynamicPositionalArgument(arg),
required: true
});
registeredNames.push(argName);
}
return optionProcessor;
}
/**
* Internal: Define a general or command option.
* Throws if the option is already registered in the given command context.
* or in the given command.
* @private
* @see option()
*/
function _addOption(command, optString, validValues) {
const opt = _parseOptionString(optString, validValues);
if (command.options[opt.longName]) {
throw new Error(`Duplicate assignment for long option ${opt.longName}`);
} else if (optionProcessor.options[opt.longName]) {
// This path is only taken if optString is for commands
optionProcessor.optionClashes.push({
option: opt.longName,
description: `Command '${command.longName}' has option clash with general options for: ${opt.longName}`
});
}
command.options[opt.longName] = opt;
if (opt.shortName) {
if (command.options[opt.shortName]) {
throw new Error(`Duplicate assignment for short option ${opt.shortName}`);
} else if (optionProcessor.options[opt.shortName]) {
// This path is only taken if optString is for commands
optionProcessor.optionClashes.push({
option: opt.shortName,
description: `Command '${command.longName}' has option clash with general options for: ${opt.shortName}`
});
}
command.options[opt.shortName] = opt;
}
return command;
}
// Internal: Parse one command string like "F, toFoo". Return an object like this

@@ -109,3 +173,3 @@ // {

let tokens = cmdString.trim().split(/,? +/);
const tokens = cmdString.trim().split(/, */);
switch (tokens.length) {

@@ -130,3 +194,3 @@ case 1:

// Internal: Parse one option string like "-f, --foo-bar <p>". Return an object like this
// Internal: Parse one option string like "-f, --foo-bar <p>". Returns an object like this
// {

@@ -145,3 +209,4 @@ // longName: '--foo-bar',

let tokens = optString.trim().split(/,? +/);
// split at spaces (with optional preceding comma)
const tokens = optString.trim().split(/,? +/);
switch (tokens.length) {

@@ -155,8 +220,7 @@ case 1:

case 2:
// Could be "--foo <bar>", or "-f -foo"
// Could be "--foo <bar>", or "-f --foo"
if (isLongOption(tokens[0]) && isParam(tokens[1])) {
longName = tokens[0];
param = tokens[1];
}
else if (isShortOption(tokens[0]) && isLongOption(tokens[1])) {
} else if (isShortOption(tokens[0]) && isLongOption(tokens[1])) {
shortName = tokens[0];

@@ -167,3 +231,3 @@ longName = tokens[1];

case 3:
// Must be "-f foo <bar>"
// Must be "-f --foo <bar>"
if (isShortOption(tokens[0]) && isLongOption(tokens[1]) && isParam(tokens[2])) {

@@ -176,6 +240,6 @@ shortName = tokens[0];

default:
throw new Error(`Invalid option description: ${optString}`);
throw new Error(`Invalid option description, too many tokens: ${optString}`);
}
if (!longName) {
throw new Error(`Invalid option description: ${optString}`);
throw new Error(`Invalid option description, missing long name: ${optString}`);
}

@@ -185,2 +249,8 @@ if (!param && validValues) {

}
if (validValues) {
validValues.forEach((value) => {
if (typeof value !== "string")
throw new Error(`Valid values must be of type string: ${optString}`);
});
}
camelName = _camelify(longName);

@@ -196,3 +266,3 @@ return {

// Return a camel-case name "fooBar" for a long option "--foo-bar"
// Return a camelCase name "fooBar" for a long option "--foo-bar"
function _camelify(opt) {

@@ -232,6 +302,7 @@ return opt.substring(2).replace(/-./g, s => s.substring(1).toUpperCase());

// unknownOptions: [],
// args: [
// 'arg1',
// 'arg2',
// ],
// args: {
// length: 4,
// foo: 'value1',
// bar: [ 'value2', 'value3', 'value4' ]
// },
// cmdErrors: [],

@@ -241,7 +312,9 @@ // errors: [],

function processCmdLine(argv) {
let result = {
const result = {
command: undefined,
options: { newCsn: true },
options: { },
unknownOptions: [],
args: [],
args: {
length: 0
},
cmdErrors: [],

@@ -253,8 +326,9 @@ errors: [],

let seenDashDash = false;
// 0: "node", 1: filename
for (let i = 2; i < argv.length; i++) {
let arg = argv[i];
const arg = argv[i];
if (!seenDashDash && arg.startsWith('-') && arg != '--') {
if (result.command) {
// We already have a command
let opt = optionProcessor.commands[result.command].options[arg];
const opt = optionProcessor.commands[result.command].options[arg];
if (opt) {

@@ -265,3 +339,3 @@ // Found as a command option

// No command option, try general options as fallback
let opt = optionProcessor.options[arg];
const opt = optionProcessor.options[arg];
if (opt) {

@@ -275,3 +349,3 @@ i += processOption(i, opt, false);

if (Object.keys(optionProcessor.commands).some(cmd => optionProcessor.commands[cmd].options[arg])) {
let cmd = optionProcessor.commands[
const cmd = optionProcessor.commands[
Object.keys(optionProcessor.commands).find(cmd => optionProcessor.commands[cmd].options[arg])

@@ -290,3 +364,3 @@ ];

// We don't have a command
let opt = optionProcessor.options[arg];
const opt = optionProcessor.options[arg];
if (opt) {

@@ -297,3 +371,3 @@ // Found as a general option

// Not found, complain
result.errors.push(`Unknown option "${arg}"`);
result.unknownOptions.push(`Unknown option "${arg}"`);
}

@@ -314,8 +388,7 @@ }

// Not found as command, take as arg and stop looking for commands
result.args.push(arg);
processPositionalArgument(arg);
result.command = null;
}
} else {
// Arg
result.args.push(arg);
processPositionalArgument(arg);
}

@@ -328,9 +401,28 @@ }

}
// Complain about missing files)
if (result.args.length == 0) {
// FIXME: Might later read from stdin instead
result.errors.push(`Missing <file...>`);
// Complain about first missing positional arguments
const missingArg = optionProcessor.positionalArguments.find((arg) => arg.required && !result.args[arg.name]);
if (missingArg) {
result.errors.push(`Missing positional argument: <${missingArg.name}${missingArg.isDynamic ? '...' : ''}>`)
}
return result;
function processPositionalArgument(argumentValue) {
const inBounds = result.args.length < optionProcessor.positionalArguments.length;
const lastIndex = inBounds ? result.args.length : optionProcessor.positionalArguments.length - 1;
const nextUnsetArgument = optionProcessor.positionalArguments[lastIndex];
if (!inBounds && !nextUnsetArgument.isDynamic) {
result.errors.push(`Too many arguments. Expected ${optionProcessor.positionalArguments.length}`);
return;
}
result.args.length += 1;
if (nextUnsetArgument.isDynamic) {
result.args[nextUnsetArgument.name] = result.args[nextUnsetArgument.name] || [];
result.args[nextUnsetArgument.name].push(argumentValue);
} else {
result.args[nextUnsetArgument.name] = argumentValue;
}
}
// (Note that this works on 'argv' and 'result' from above).

@@ -359,4 +451,4 @@ // Process 'argv[i]' as an option.

// Take the option with the parameter
let value = argv[i + 1];
let shortOption = opt.shortName ? `${opt.shortName}, ` : ''
const value = argv[i + 1];
const shortOption = opt.shortName ? `${opt.shortName}, ` : ''
if (command) {

@@ -397,12 +489,9 @@ // if an unknown option for a command => add it to the array and warn about

// Return an array of complaints (possibly empty)
function verifyOptions(options, command = undefined) {
let result = [];
function verifyOptions(options, command = undefined, silent = false) {
const result = [];
let opts;
if(options.betaMode && !options.testMode) {
if(options.betaMode && !options.testMode && !silent) {
result.push('Option --beta-mode was used. This option should not be used in productive scenarios!')
}
if (options && options.newCsn === false) {
result.push(`Option --old-csn was used. This option is deprecated and should not be used in productive scenarios!`)
}

@@ -420,3 +509,3 @@ if(options) {

if (command) {
let cmd = optionProcessor.commands[command];
const cmd = optionProcessor.commands[command];
if (!cmd) {

@@ -435,12 +524,12 @@ throw new Error(`Expected existing command: "${command}"`);

// Look at each supplied option
for (let camelName in options) {
let opt = opts[_unCamelify(camelName)];
for (const camelName in options) {
const opt = opts[_unCamelify(camelName)];
let error;
if (!opt) {
// Don't report commands in top-level options
if (command || !optionProcessor.commands[camelName]) {
if ((command || !optionProcessor.commands[camelName]) && !silent) {
error = `Unknown option "${command ? command + '.' : ''}${camelName}"`;
}
} else {
let param = options[camelName];
const param = options[camelName];
error = verifyOptionParam(param, opt, command ? command + '.' : '');

@@ -481,3 +570,3 @@ }

if (command && optionProcessor.commands[command]) {
let cmd = optionProcessor.commands[command];
const cmd = optionProcessor.commands[command];
return [... new Set(

@@ -507,2 +596,7 @@ Object.keys(cmd.options).map(optName => cmd.options[optName].camelName)

// Check if 'arg' looks like "<foobar...>"
function isDynamicPositionalArgument(arg) {
return /^<[a-zA-Z]+[.]{3}>$/.test(arg);
}
module.exports = {

@@ -513,2 +607,3 @@ createOptionProcessor,

isParam,
isDynamicPositionalArgument
};

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

}
// Non-structured type - just check for virtual-ness
return construct.virtual;
// Non-structured type - just check for virtual-ness or non-existing elements for entities or views
return construct.virtual || (( construct.kind === 'entity' || construct.kind === 'view' ) && construct.elements === undefined );
}

@@ -29,0 +29,0 @@ }

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

// Check that a structured element ist not casted to a different type
function checkStructureCasting(elem, model) {
const { error, signal } = alerts(model);
if (elem.type && !elem.type.$inferred) {
if (elem._finalType && elem._finalType.elements)
signal(error`Cannot cast to structured element.`, elem.location);
else if (elem.value && elem.value._artifact && elem.value._artifact._finalType && elem.value._artifact._finalType.elements)
signal(error`Structured element cannot be casted to a different type.`, elem.location);
}
}
function checkAssociation(elem, model) {

@@ -217,10 +229,20 @@ const { error, signal } = alerts(model);

}
let parameters = node.type._artifact? node.type._artifact.parameters : [];
let parameters = (node.type._artifact && node.type._artifact.parameters) || [];
let type = node.type._artifact;
let absolute = type && type.name && type.name.absolute;
for (let name in parameters) {
let param = parameters[name];
if (!node[param] && absolute != "cds.hana.ST_POINT" && absolute != "cds.hana.ST_GEOMETRY")
signal(error`Actual value for type parameter '${param}' missing in reference to type '${absolute}'`,
node.type.location );
// does this type has actual type facets?
let hasTypeFacets = !!parameters.reduce((a,p) => {
a |= Number(node[p] !== undefined);
return a;
}, false);
// Are all type factes provided?
if(absolute !== 'cds.Decimal' || hasTypeFacets) {
for (let name in parameters) {
let param = parameters[name];
if (!node[param] && !['cds.hana.ST_POINT', 'cds.hana.ST_GEOMETRY'].includes(absolute))
signal(error`Actual value for type parameter '${param}' missing in reference to type '${absolute}'`,
node.type.location );
}
}

@@ -235,4 +257,6 @@ switch (absolute) {

case "cds.Decimal": {
checkTypeParamValue(node, 'precision', 'positiveInteger', {max: 38});
checkTypeParamValue(node, 'scale', 'positiveInteger', {max: node.precision && node.precision.val});
if(hasTypeFacets) {
checkTypeParamValue(node, 'precision', 'positiveInteger', {max: 38});
checkTypeParamValue(node, 'scale', 'positiveInteger', {max: node.precision && node.precision.val});
}
break;

@@ -338,3 +362,4 @@ }

checkCardinality,
checkLocalizedElement
checkLocalizedElement,
checkStructureCasting
};

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

* Check wether the supplied argument is a virtual element
*
*
* TO CLARIFY: do we want the "no virtual element" check for virtual elements/columns, too?
*
*
* @param {any} arg Argument to check (part of an expression)

@@ -22,6 +22,6 @@ * @returns {Boolean}

* Check a token-stream expression for semantic validity
*
*
* @param {any} xpr The expression to check
* @param {any} model The model
* @returns {undefined}
* @returns {void}
*/

@@ -40,10 +40,10 @@ function checkTokenStreamExpression(xpr, model){

}
}
}
/**
* Check a tree-like expression for semantic validity
*
*
* @param {any} xpr The expression to check
* @param {any} model The model
* @returns {undefined}
* @returns {void}
*/

@@ -78,3 +78,3 @@ function checkTreeLikeExpression(xpr, model){

* Return true if 'xpr' is backlink-like expression (a comparison of "$self" with an assoc)
*
*
* @param {any} xpr The expression to check

@@ -102,7 +102,7 @@ * @returns {Boolean}

/**
* Check an expression (or condition) for semantic validity
*
* Check an expression (or condition) for semantic validity
*
* @param {any} xpr The expression to check
* @param {any} model The model
* @returns {undefined}
* @returns {void}
*/

@@ -109,0 +109,0 @@ function checkExpression(xpr, model) {

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

/**
* @param {object} model XSN Model: only used for messages and options
*/
function getFunctionAndActionChecks(model) {

@@ -12,17 +15,20 @@ const { warning, signal } = alerts(model);

checkActionOrFunction,
checkActionOrFunctionParameter
checkActionOrFunctionParameter,
serviceNameFor
}
function checkActionOrFunction(act, serviceName) {
if (Array.isArray(act)) // duplicated declaration -> will be handle in some of the other steps of the compiler
function checkActionOrFunction(act) {
if (Array.isArray(act)) // duplicated declaration -> will be handled in some of the other steps of the compiler
return;
let location = act.location;
const location = act.location;
const originalAction = act;
if (!act.name.action) { // unbound action or function
let actServiceName = getAbsNameWithoutId(act.name.absolute);
if (actServiceName !== serviceName)
signal(warning`Functions and actions must be declared in a service or bound to an entity`, location);
}
checkIfInService(act);
// Get the service name for the current function/action. We do it at this point because
// if the origin of "act" is set (e.g. through a projection) then we replace "act" with the
// original artefact which may not have a service.
const serviceName = serviceNameFor(act);
if (act.origin) {

@@ -44,11 +50,11 @@ act = act.origin._artifact || /* old csn does not have the _artifact */ act;

if (act.returns.items)
checkReturnTypeArray(act);
checkReturnTypeArray();
// check if return type is entity from the current service
else if (act.returns._finalType.kind === 'entity')
checkReturnEntity(act);
checkReturnEntity();
else if (act.returns._finalType.kind === 'type')
checkReturnUserDefinedType(act);
checkReturnUserDefinedType();
}
function checkReturnTypeArray(act) {
function checkReturnTypeArray() {
if (!act.returns.items)

@@ -66,17 +72,29 @@ return;

if (act.returns.items._finalType.kind === 'entity')
checkReturnEntity(act);
checkReturnEntity();
}
function checkReturnEntity(act) {
function checkReturnEntity() {
// take the full name with the entity name
let returnServiceName = act.returns.items
const absoluteReturnTypeName = act.returns.items
? act.returns.items._finalType.name.absolute
: act.returns._finalType.name.absolute;
// remove the entity name
returnServiceName = getAbsNameWithoutId(returnServiceName);
if (serviceName && returnServiceName !== serviceName)
signal(warning`Entity type must be from the current service '${serviceName}'`, location);
const returnServiceName = getAbsNameWithoutId(absoluteReturnTypeName);
if (serviceName && returnServiceName !== serviceName) {
let loc;
// The action/function may be part of a projection.
// If it is then we should use the original's location and
// not the one of "origin" one because in the latter case
// we would otherwise have invalid or confusing warnings.
if (originalAction.origin) {
loc = originalAction.location;
} else {
const type = (act.returns.items ? act.returns.items.type : act.returns.type);
loc = type ? type.location : null;
}
signal(warning`Entity type must be from the current service '${serviceName}'`, loc || location);
}
}
function checkReturnUserDefinedType(act) {
function checkReturnUserDefinedType() {
if (act.returns._finalType.builtin && options.toOdata && options.toOdata.version === 'v2') {

@@ -99,3 +117,3 @@ signal(warning`Scalar return types are not allowed in OData V2`, location);

let paramTypeArtifact = param.type && param.type._artifact || {};
const paramTypeArtifact = param.type && param.type._artifact || {};
// check if the entity type is from the current service

@@ -110,2 +128,8 @@ if (paramTypeArtifact.kind === 'entity')

}
function checkIfInService(action) {
if (!serviceNameFor(action)) {
signal(warning`Functions and actions must be declared in a service`, action.location);
}
}
}

@@ -117,2 +141,19 @@

/**
* Get the absolute service name for the given action or entity.
* If the action is bound, the service name of its entity is returned.
* @param {object} action An action or entity.
* @returns {string} Absolute service name or an empty string if not service is set.
*/
function serviceNameFor(action) {
if (action._service) {
return action._service.name.absolute;
} else if (action._main && action._main._service) {
return action._main._service.name.absolute;
}
return "";
}
module.exports = getFunctionAndActionChecks;

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

const { checkNotEmptyOrOnlyVirtualElems, checkNoUnmanagedAssocsInGroupByOrderBy } = require('./checkArtifacts');
const { checkPrimaryKeyTypeCompatibility, checkVirtualElement, checkManagedAssoc, checkCardinality, checkLocalizedElement } = require('./checkElements');
const { checkPrimaryKeyTypeCompatibility, checkVirtualElement, checkManagedAssoc, checkCardinality,
checkLocalizedElement, checkStructureCasting } = require('./checkElements');
const { checkExpression } = require('./checkExpressions');

@@ -37,6 +38,4 @@ const { foreachPath } = require('../model/modelUtils');

const { error, info, signal } = alerts(model);
const { checkActionOrFunction, checkActionOrFunctionParameter} = getFunctionAndActionChecks(model);
const { checkActionOrFunction, checkActionOrFunctionParameter, serviceNameFor} = getFunctionAndActionChecks(model);
let currentService = undefined;
// Iterate the model and call generic/specific checkers on each construct

@@ -47,41 +46,46 @@ // (please do not put any actual checks here)

checkGenericArtifact(artifact);
if (artifact.kind == 'context' || artifact.kind == 'service' || artifact.kind == 'namespace' || artifact.kind == 'accesspolicy') {
if (isContainer(artifact)) {
checkGenericContainer(artifact);
}
if (artifact.kind === 'context' || artifact.kind === 'service')
currentService = artifact.name.absolute;
callKindSpecificCheck(artifact, currentService);
callKindSpecificCheck(artifact);
baseModel.forEachMemberRecursively(artifact, member => {
checkGenericConstruct(member);
checkGenericMember(member);
callKindSpecificCheck(member, currentService);
});
callKindSpecificCheck(member);
});
});
function isContainer(artifact) {
return ['context', 'service', 'namespace', 'accesspolicy'].includes(artifact.kind);
}
// Call the appropriate kind-specific check function for 'construct'
function callKindSpecificCheck(construct, serviceName) {
// For each kind, there must be a check function (so that we don't forget one)
function callKindSpecificCheck(construct) {
/**
* For each kind, there must be a check function (so that we don't forget one)
*
* @type {Object.<string, (construct: object) => void>}
*/
const checkFunctions = {
context: nothingToCheckYet,
service: nothingToCheckYet,
namespace: nothingToCheckYet,
accesspolicy: nothingToCheckYet,
entity: checkEntity,
action: checkActionOrFunction,
annotation: nothingToCheckYet,
type: checkType,
aspect: nothingToCheckYet,
const: nothingToCheckYet,
context: nothingToCheckYet,
element: checkElement,
param: checkParam,
entity: checkEntity,
enum: nothingToCheckYet,
const: nothingToCheckYet,
event: nothingToCheckYet,
function: checkActionOrFunction,
key: nothingToCheckYet,
function: checkActionFunction,
action: checkActionFunction,
namespace: nothingToCheckYet,
package: nothingToCheckYet,
param: checkParam,
query: nothingToCheckYet,
role: nothingToCheckYet,
service: nothingToCheckYet,
type: checkType,
view: checkView,
role: nothingToCheckYet,
aspect: nothingToCheckYet,
event: nothingToCheckYet,
package: nothingToCheckYet,
}

@@ -92,3 +96,3 @@ let func = checkFunctions[construct.kind];

}
func(construct, serviceName);
func(construct);
}

@@ -111,9 +115,12 @@

// Called for each main artifact (no need to iterate its members)
function checkGenericArtifact(art) {
/**
* Called for each main artifact (no need to iterate its members).
* @param {object} artifact A "model.definitions" artifact
*/
function checkGenericArtifact(artifact) {
// user defined objects must not live in namespace cds,
// exception: they can live in cds.foundation
// Only an INFO during compile time,
// Only an INFO during compile time,
// reclassified to errors in the backends
checkNotEmptyOrOnlyVirtualElems(art, model);
checkNotEmptyOrOnlyVirtualElems(artifact, model);
}

@@ -146,6 +153,4 @@

function checkEntity(entity) {
if (source(entity)) { // projection
checkProjection(entity, model);
}
function checkEntity(/*entity*/) {
// no-op
}

@@ -157,10 +162,2 @@

function checkProjection(entity) {
// TODO: check too simple (just one source), as most of those in this file
let sourceEntity = source(entity)._artifact;
if(sourceEntity && isAbstractEntity(sourceEntity)) {
signal(error`Projection ${entity.name.absolute} on abstract entity ${sourceEntity.name.absolute}`, source(entity).location);
}
}
function checkView(view) {

@@ -202,2 +199,3 @@ // TODO: check too simple (just one source), as most of those in this file

for (let elemName in query.elements) {
checkStructureCasting(query.elements[elemName], model);
checkExpressionsInPaths(query.elements[elemName].value);

@@ -244,10 +242,5 @@ }

// Actions and functions are almost identical, so we use only one check function
function checkActionFunction(act, serviceName) {
checkActionOrFunction(act, serviceName);
}
function checkParam(param, serviceName) {
function checkParam(param) {
if (param._parent && (param._parent.kind == 'action' || param._parent.kind == 'function')) {
checkActionOrFunctionParameter(param, serviceName);
checkActionOrFunctionParameter(param, serviceNameFor(param._parent));
}

@@ -254,0 +247,0 @@ }

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

},
$weak: { test: isBoolean },
$weak: { test: isBoolean, parser: true },
$notFound: { test: isBoolean },

@@ -167,2 +167,4 @@ },

elements: { kind: true, inherits: 'definitions' },
elements$: { kind: true, enumerable: false, test: TODO },
// TODO: introduce real "specified elements" instead
elements_: { kind: true, parser: true, test: TODO }, // TODO: remove

@@ -175,2 +177,3 @@ _elementsIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove

foreignKeys: { kind: true, inherits: 'definitions' },
$keysNavigation: { kind: true, test: TODO },
foreignKeys_: { kind: true, parser: true, test: TODO }, // TODO: remove

@@ -239,3 +242,3 @@ _foreignKeysIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove

},
orderBy: { test: isArray(), requires: [ 'value' ], optional: [ 'sort', 'nulls', '_$queryNode' ] },
orderBy: { test: isArray(), requires: [ 'value' ], optional: [ 'location', 'sort', 'nulls', '_$queryNode' ] },
sort: { test: locationVal( isString ), enum: [ 'asc', 'desc' ] },

@@ -298,3 +301,3 @@ nulls: { test: locationVal( isString ), enum: [ 'first', 'last' ] },

parser: true,
kind: [ 'entity', 'view' ],
kind: [ 'entity', 'view', 'type' ],
test: isString, // CSN parser should check for 'entity', 'view', 'projection'

@@ -360,3 +363,3 @@ },

optional: [
'path', 'id', 'quoted', // TODO: req path, opt id for main, req id for member
'path', 'id', 'quoted', 'variant', // TODO: req path, opt id for main, req id for member
'$mixin', // TODO: delete, use kind = 'mixin'

@@ -369,2 +372,3 @@ '_artifact', '$inferred',

absolute: { test: isString },
variant: { test: TODO }, // TODO: not set in CDL parser, only in annotationAssignment
element: { test: TODO }, // TODO: { test: isString },

@@ -395,3 +399,3 @@ action: { test: isString },

projection: { kind: true, test: TODO }, // TODO: remove in JSON/CDL parser
technicalConfig: { kind: [ 'entity' ], test: TODO }, // TODO: some spec
technicalConfig: { enumerable: () => true, kind: [ 'entity' ], test: TODO }, // TODO: some spec
sequenceOptions: { kind: true, test: TODO }, // hanaFlavor

@@ -403,3 +407,2 @@ targetElement: { kind: true, inherits: 'type' }, // for foreign keys

blocks: { kind: true, test: TODO }, // TODO: make it $blocks ?
viaTransform: { kind: true, test: TODO }, // TODO remove
length: { kind: true, inherits: 'value' }, // for number is to be checked in resolver

@@ -479,3 +482,3 @@ precision: { kind: true, inherits: 'value' },

function assertProp( node, parent, prop, extraSpec, noPropertyTest ) {
let spec = extraSpec || schema[prop] || schema[prop.charAt()];
let spec = extraSpec || schema[prop] || schema[prop.charAt(0)];
if (!spec)

@@ -486,3 +489,3 @@ throw new Error( `Property '${ prop }' has not been specified`);

if (!noPropertyTest) {
const char = prop.charAt();
const char = prop.charAt(0);
const parser = ('parser' in spec) ? spec.parser : char !== '_' && char !== '$';

@@ -542,3 +545,3 @@ if (stageParser && !parser)

const opt = (optional instanceof Array)
? optional.includes( n ) || optional.includes( n.charAt() )
? optional.includes( n ) || optional.includes( n.charAt(0) )
: optional( n, spec );

@@ -553,3 +556,3 @@ if (!(opt || requires.includes( n ) || n === '$extra'))

function thoseWithKind( prop, spec ) {
const those = spec.schema && spec.schema[prop] || schema[prop] || schema[prop.charAt()];
const those = spec.schema && spec.schema[prop] || schema[prop] || schema[prop.charAt(0)];
return those && those.kind;

@@ -556,0 +559,0 @@ }

@@ -223,2 +223,4 @@ //

function getOrigin( art ) {
if (art._origin)
return art._origin;
if (art.$from && art.$from.length) { // query

@@ -225,0 +227,0 @@ const tabref = art.$from[0]._artifact;

@@ -68,7 +68,10 @@ // Compiler functions and utilities shared across all phases

}
// TODO: delete envFn again and specify some reject for extend/annotate ?
/**
* @param {object} model
* @param {(a, b?, c?) => any} environment
*/
function fns( model, environment = artifactsEnv ) {
const options = model.options || {};
const message = getMessageFunction( model );
// TODO: combine envFn and assoc ?
const specExpected = {

@@ -81,3 +84,4 @@ annotation: { useDefinitions: true, noMessage: true },

type: { reject: rejectNonType }, // TODO: more detailed later (e.g. for enum base type?)
typeOf: { next: '_$next', assoc: false }, // warn
// if we want to disallow assoc nav for TYPE, do not do it her
typeOf: { next: '_$next' },
include: { reject: rejectNonStruct, envFn: artifactsEnv },

@@ -88,5 +92,6 @@ context: { reject: rejectNonContext, envFn: artifactsEnv },

// TODO: dep for (explicit+implicit!) foreign keys
element: { next: '__none_' }, // foreign key element
element: { next: '__none_' }, // TODO: something for technical config - re-think
targetElement: { next: '__none_', assoc: false },
filter: { next: '_$next', lexical: 'main' },
from: { reject: rejectNonSource, assoc: 'from' },
from: { reject: rejectNonSource, assoc: 'from', argsSpec: 'expr' },
const: { next: '_$next', reject: rejectNonConst },

@@ -96,3 +101,13 @@ expr: {

},
unmanaged: { next: '_$next', escape: 'param', noDep: true }, // TODO: special assoc for only on user
on: { // TODO: there will also be a 'from-on'
escape: 'param', // meaning of ':' in front of path? search in 'params'
next: '_$next', // TODO: lexical: ... how to find the (next) lexical environment
rootEnv: 'elements', // the final environment for the path root
noDep: true, // do not set dependency for circular-check
}, // TODO: special assoc for only on user
'mixin-on': {
escape: 'param', // meaning of ':' in front of path? search in 'params'
next: '_$next', // TODO: lexical: ... how to find the (next) lexical environment
noDep: true, // do not set dependency for circular-check
}, // TODO: special assoc for only on user
rewrite: {

@@ -117,2 +132,5 @@ next: '_$next', escape: 'param', noDep: true, rewrite: true,

// TODO: for reject, distinguish between "completely wrong", i.e. assoc
// target is no struct, and "rejected"; the former have _artifact: null, the
// latter the referred one.
function rejectNonConst( art ) {

@@ -144,3 +162,3 @@ return [ 'builtin', 'param', 'const' ].includes( art.kind ) ? undefined : 'expected-const';

function rejectNonEntity( art ) {
return ([ 'view', 'entity' ].includes( art.kind )) // TODO: also not abstract
return ([ 'view', 'entity' ].includes( art.kind ) && !(art.abstract && art.abstract.val))
? undefined

@@ -151,3 +169,6 @@ : 'expected-entity';

function rejectNonTarget( art ) {
return (options.betaMode && art.kind === 'type') ? rejectNonStruct( art ) : rejectNonEntity( art );
return ((options.betaMode || options.beta) && // TODO: delete 'abstract', proper kind: 'aspect'
(art.kind === 'type' || art.kind === 'entity' && art.abstract && art.abstract.val))
? rejectNonStruct( art )
: rejectNonEntity( art );
}

@@ -157,5 +178,6 @@

if ([ 'view', 'entity' ].includes( art.kind ))
return undefined;
const main = [ ...path ].reverse().find( item => !item._artifact._main );
if (![ 'view', 'entity' ].includes( main._artifact.kind ))
return (art.abstract && art.abstract.val) ? 'expected-source' : undefined;
const main = [ ...path ].reverse().find( item => !item._artifact._main )._artifact;
// TODO: better error location if error for main
if (![ 'view', 'entity' ].includes( main.kind ) || main.abstract && main.abstract.val)
return 'expected-source'; // orig: 'A source must start at an entity, projection or view';

@@ -245,3 +267,3 @@ environment( art ); // sets _finalType on art

if (!extDict && !spec.noExt) {
extDict = query && query.$combined ||
extDict = query && spec.rootEnv !== 'elements' && query.$combined ||
environment( user._main ? user._parent : user );

@@ -345,3 +367,3 @@ }

// TODO: store magic variable in lower case (nicer for code completion)
return (name === 'self' || name.charAt() === '$') ? name : name.toUpperCase();
return (name === 'self' || name.charAt(0) === '$') ? name : name.toUpperCase();
}

@@ -474,3 +496,4 @@

let art;
const assoc = spec.assoc && (spec.assoc == null || user);
let nav = spec.assoc !== '$keys' && null; // false for '$keys'
const last = path[path.length - 1];
for (const item of path) {

@@ -485,3 +508,3 @@ if (!item || !item.id) // incomplete AST due to parse error

else {
const env = (spec.envFn || environment)( art, item.location, assoc );
const env = (spec.envFn || environment)( art, item.location, user, spec.assoc );
const sub = setLink( item, env && env[item.id] );

@@ -492,2 +515,26 @@ if (!sub)

return false;
if (nav) { // we have already "pseudo-followed" a managed association
// We currently rely on the check that targetElement references do
// not (pseudo-) follow associations, otherwise potential redirection
// there had to be considered, too. Also, fk refs to sub elements in
// combinations with redirections of the target which directly access
// the potentially renamed sub elements would be really complex.
// With our restriction, no renaming must be considered for item.id.
nav = setTargetReferenceKey( item.id, item );
}
// Now set an _navigation link for managed assocs in ON condition etc
else if (art && art.target && nav != null) {
// Find the original ref for sub and the original foreign key
// definition. This way, we do not need the foreign keys with
// rewritten target element path, which might not be available at
// this point (rewriteKeys in Resolver Phase 5). If we want to
// follow associations in foreign key definitions, rewriteKeys must
// be moved to the on-demand Resolver Phase 2.
let orig; // for the original target element
for (let o = sub; o; o = o.value && o.value._artifact)
orig = o;
nav = (orig._finalType || orig).$keysNavigation;
nav = setTargetReferenceKey( orig.name.id, item );
}
art = sub;

@@ -499,2 +546,22 @@ setXref( art, user, item );

function setTargetReferenceKey( id, item ) {
const node = nav && nav[id];
nav = null;
if (node) {
if (node._artifact) {
// set the original(!) foreign key for the assoc - the "right" ones
// after rewriteKeys() is the one with the same name.id
setLink( item, node._artifact, '_navigation' );
if (item === last)
return;
}
else if (item !== last) {
nav = node.$keysNavigation;
return;
}
}
message( null, item.location, user, {}, 'Error',
'You cannot follow associations other than to elements referred to in a managed association\'s key' );
}
function error( item, env ) {

@@ -533,2 +600,3 @@ if (!spec.next) { // artifact ref

location.$notFound = true;
/** @type {object} */
const err = message( msgId, location, home, ...args );

@@ -730,6 +798,9 @@ // console.log( Object.keys( Object.assign( Object.create(null), ...valid.reverse() ) ) )

/** @type {(a, b) => boolean} */
const testFunctionPlaceholder = () => true;
// Return path step if the path navigates along an association whose final type
// satisfies function `test`; "navigates along" = last path item not considered
// without truthy optional argument `alsoTestLast`.
function withAssociation( ref, test = () => true, alsoTestLast ) {
function withAssociation( ref, test = testFunctionPlaceholder, alsoTestLast ) {
for (const item of ref.path || []) {

@@ -736,0 +807,0 @@ const art = item && item._artifact; // item can be null with parse error

@@ -9,47 +9,95 @@ 'use strict';

/* Vocabulary overview as of January 2020:
var vocabularyDefinitions = {
OASIS: https://github.com/oasis-tcs/odata-vocabularies/tree/master/vocabularies
Aggregation (published)
Authorization (published)
Capabilities (published)
Core (published)
Measures (published)
Repeatability (published)
Temporal (not published, not yet finalized)
Validation (published)
SAP: https://github.com/SAP/odata-vocabularies/tree/master/vocabularies
Analytics (published)
CodeList (published)
Common (pubished)
Communication (published)
Hierarchy (not published, still experimental)
PersonalData (published)
Session (published)
UI (published)
*/
const vocabularyDefinitions = {
'Aggregation': {
'ref': { Uri: "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Aggregation.V1.xml" },
'inc': { Alias: "Aggregation", Namespace: "Org.OData.Aggregation.V1" }
'inc': { Alias: "Aggregation", Namespace: "Org.OData.Aggregation.V1" },
'int': { filename: 'Aggregation.xml' }
},
'Analytics': {
'ref': { Uri: "https://wiki.scn.sap.com/wiki/download/attachments/462030211/Analytics.xml?api=v2" },
'inc': { Alias: "Analytics", Namespace: "com.sap.vocabularies.Analytics.v1" }
'ref': { Uri: "https://sap.github.io/odata-vocabularies/vocabularies/Analytics.xml" },
'inc': { Alias: "Analytics", Namespace: "com.sap.vocabularies.Analytics.v1" },
'int': { filename: 'Analytics.xml' }
},
'Authorization': {
'ref': { Uri: "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Authorization.V1.xml" },
'inc': { Alias: "Authorization", Namespace: "Org.OData.Authorization.V1" }
'inc': { Alias: "Authorization", Namespace: "Org.OData.Authorization.V1" },
'int': { filename: 'Authorization.xml' }
},
'Capabilities': {
'ref': { Uri: "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1.xml" },
'inc': { Alias: "Capabilities", Namespace: "Org.OData.Capabilities.V1" }
'inc': { Alias: "Capabilities", Namespace: "Org.OData.Capabilities.V1" },
'int': { filename: 'Capabilities.xml' }
},
'CodeList': {
'ref': { Uri: "https://sap.github.io/odata-vocabularies/vocabularies/CodeList.xml" },
'inc': { Alias: "CodeList", Namespace: "com.sap.vocabularies.CodeList.v1" },
'int': { filename: 'CodeList.xml' }
},
'Common': {
'ref': { Uri: "https://wiki.scn.sap.com/wiki/download/attachments/448470974/Common.xml?api=v2" },
'inc': { Alias: "Common", Namespace: "com.sap.vocabularies.Common.v1" }
'ref': { Uri: "https://sap.github.io/odata-vocabularies/vocabularies/Common.xml" },
'inc': { Alias: "Common", Namespace: "com.sap.vocabularies.Common.v1" },
'int': { filename: 'Common.xml' }
},
'Communication': {
'ref': { Uri: "https://wiki.scn.sap.com/wiki/download/attachments/448470971/Communication.xml?api=v2" },
'inc': { Alias: "Communication", Namespace: "com.sap.vocabularies.Communication.v1" }
'ref': { Uri: "https://sap.github.io/odata-vocabularies/vocabularies/Communication.xml" },
'inc': { Alias: "Communication", Namespace: "com.sap.vocabularies.Communication.v1" },
'int': { filename: 'Communication.xml' }
},
'Core': {
'ref': { Uri: "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1.xml" },
'inc': { Alias: "Core", Namespace: "Org.OData.Core.V1" }
'inc': { Alias: "Core", Namespace: "Org.OData.Core.V1" },
'int': { filename: 'Core.xml' }
},
'Measures': {
'ref': { Uri: "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Measures.V1.xml" },
'inc': { Alias: "Measures", Namespace: "Org.OData.Measures.V1" }
'inc': { Alias: "Measures", Namespace: "Org.OData.Measures.V1" },
'int': { filename: 'Measures.xml' }
},
'PersonalData': {
'ref': { Uri: "https://wiki.scn.sap.com/wiki/download/attachments/496435637/PersonalData.xml?api=v2" },
'inc': { Alias: "PersonalData", Namespace: "com.sap.vocabularies.PersonalData.v1" }
'ref': { Uri: "https://sap.github.io/odata-vocabularies/vocabularies/PersonalData.xml" },
'inc': { Alias: "PersonalData", Namespace: "com.sap.vocabularies.PersonalData.v1" },
'int': { filename: 'PersonalData.xml' }
},
'Repeatability': {
'ref': { Uri: "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Repeatability.V1.xml" },
'inc': { Alias: "Repeatability", Namespace: "Org.OData.Repeatability.V1" },
'int': { filename: 'Repeatability.xml' }
},
'Session': {
'ref': { Uri: "https://sap.github.io/odata-vocabularies/vocabularies/Session.xml" },
'inc': { Alias: "Session", Namespace: "com.sap.vocabularies.Session.v1" },
'int': { filename: 'Session.xml' }
},
'UI': {
'ref': { Uri: "https://wiki.scn.sap.com/wiki/download/attachments/448470968/UI.xml?api=v2" },
'inc': { Alias: "UI", Namespace: "com.sap.vocabularies.UI.v1" }
'ref': { Uri: "https://sap.github.io/odata-vocabularies/vocabularies/UI.xml" },
'inc': { Alias: "UI", Namespace: "com.sap.vocabularies.UI.v1" },
'int': { filename: 'UI.xml' }
},
'Validation': {
'ref': { Uri: "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Validation.V1.xml" },
'inc': { Alias: "Validation", Namespace: "Org.OData.Validation.V1" }
'inc': { Alias: "Validation", Namespace: "Org.OData.Validation.V1" },
'int': { filename: 'Validation.xml' }
},

@@ -73,3 +121,3 @@ };

const edmlib = require('../edm.js')(options);
const Edm = require('../edm.js')(csn, options);

@@ -159,5 +207,6 @@ // annotation preprocessing

// generate the edmx "frame" around the annotations
let schema = new edmlib.Schema(v, serviceName, serviceName, g_annosArray, false);
let service = new edmlib.DataServices(v, schema);
let edm = new edmlib.Edm(v, service);
let schema = new Edm.Schema(v, serviceName, serviceName, g_annosArray, false);
let service = new Edm.DataServices(v, schema);
/** @type {object} */
let edm = new Edm.Edm(v, service);

@@ -167,4 +216,4 @@ // add references for the used vocabularies

if(vocabularyDefinitions[n].used) {
let r = new edmlib.Reference(v, vocabularyDefinitions[n].ref);
r.append(new edmlib.Include(v, vocabularyDefinitions[n].inc))
let r = new Edm.Reference(v, vocabularyDefinitions[n].ref);
r.append(new Edm.Include(v, vocabularyDefinitions[n].inc))
edm._defaultRefs.push(r);

@@ -228,3 +277,3 @@ }

<Annotations Target="..."> where Target is the full name of the target
There is one exception (Schema), see below
There is one exception (Schema), see below

@@ -349,3 +398,3 @@ carrier = service

if (action.kind === 'function') {
let mapType = (p) => (p.type.startsWith('cds.') && !p.type.startsWith('cds.foundation.')) ?
let mapType = (p) => (p.type.startsWith('cds.') && !p.type.startsWith('cds.foundation.')) ?
edmUtils.mapCdsToEdmType(p, signal, error, false /*is only called for v4*/) : p.type;

@@ -373,3 +422,3 @@ for (let n in action.params) {

if(carrier['@cds.api.ignore']) {
if(!edmUtils.isEdmPropertyRendered(carrier, options)) {
return;

@@ -408,4 +457,4 @@ }

// newAnnosStd: the corresponding <Annotations ...> tag
// for some carriers (service, entity), there can be an alternative target
// altName: name of alternative target
// for some carriers (service, entity), there can be an alternative target (usually the EntitySet)
// alternativeEdmTargetName: name of alternative target
// newAnnosAlt: the corresponding <Annotations ...> tag

@@ -416,11 +465,24 @@ // which one to choose depends on the "AppliesTo" info of the single annotations, so we have

let appliesTest = null; // the condition to decide between std and alt
let testToAlternativeEdmTarget = null; // if true, assign to alternative Edm Target
/** @type {(val?: any) => boolean} */
let testToStandardEdmTarget = ()=>true; // if true, assign to standard Edm Target
let stdName = edmTargetName;
let altName = null;
let alternativeEdmTargetName = null;
let hasAlternativeCarrier = false; // is the alternative annotation target available in the EDM?
if (carrier.kind === 'entity' || carrier.kind === 'view') {
// if annotated object is an entity, annotation goes to the EntityType,
// except if AppliesTo contains EntitySet but not EntityType, then annotation goes to EntitySet
appliesTest = (x => x.match(/EntitySet/) && !x.match(/EntityType/));
// except if AppliesTo contains Singleton/EntitySet but not EntityType, then annotation goes to EntitySet
if(carrier['@odata.singleton.nullable'])
testToAlternativeEdmTarget = (x => x.includes('Singleton') && !x.includes('EntityType'));
else
testToAlternativeEdmTarget = (x => x.includes('EntitySet') && !x.includes('EntityType'));
// if carrier has an alternate 'entitySetName' use this instead of EdmTargetName
// (see edmPreprocessor.initializeParameterizedEntityOrView(), where parameterized artifacts
// are split into *Parameter and *Type entities and their respective EntitySets are eventually
// renamed.
// (which is the definition key in the CSN and usually the name of the EntityType)
// find last . in name and insert "EntityContainer/"
altName = edmTargetName.replace(/\.(?=[^.]*$)/, '.EntityContainer/');
alternativeEdmTargetName = (carrier.entitySetName || edmTargetName).replace(/\.(?=[^.]*$)/, '.EntityContainer/');
hasAlternativeCarrier = carrier.hasEntitySet;
}

@@ -430,14 +492,52 @@ else if (carrier.kind === 'service') {

// except if AppliesTo contains Schema but not EntityContainer, then annotation goes to Schema
appliesTest = (x => x.match(/Schema/) && !x.match(/EntityContainer/));
testToAlternativeEdmTarget = (x => x.includes('Schema') && !x.includes('EntityContainer'));
stdName = edmTargetName + '.EntityContainer';
altName = edmTargetName;
alternativeEdmTargetName = edmTargetName;
hasAlternativeCarrier = true; // EntityContainer is always available
}
//element => decide if navprop or normal property
else if(!carrier.kind) {
// if appliesTo is undefined, return true
if(carrier.target) {
testToStandardEdmTarget = (x=> x ? x.includes('NavigationProperty') : true);
}
else {
// this might be more precise if handleAnnotation would know more about the carrier
testToStandardEdmTarget = (x => x ? ['Parameter', 'Property'].some(y => x.includes(y)): true);
}
}
/* all AppliesTo entries:
"Action",
"ActionImport",
"Annotation",
"Collection",
"ComplexType",
"EntityContainer",
"EntitySet",
"EntityType",
"Function",
"FunctionImport",
"Include",
"NavigationProperty",
"Parameter",
"Property",
"PropertyValue",
"Record",
"Reference",
"ReturnType",
"Schema",
"Singleton",
"Term",
"TypeDefinition"
*/
// result objects that holds all the annotation objects to be created
let newAnnosStd = new edmlib.Annotations(v, stdName); // used in closure
let newAnnosStd = new Edm.Annotations(v, stdName); // used in closure
g_annosArray.push(newAnnosStd);
let newAnnosAlt = null; // create only on demand
let newAnnosAlternative = null; // create only on demand
return function(annotation, appliesTo) {
if (appliesTest && appliesTo && appliesTest(appliesTo)) {
if (testToAlternativeEdmTarget && appliesTo && testToAlternativeEdmTarget(appliesTo)) {
if (carrier.kind === 'service') {

@@ -450,11 +550,12 @@ if (isV2()) {

}
else {
if (!newAnnosAlt) { // only create upon insertion of first anno
newAnnosAlt = new edmlib.Annotations(v, altName);
g_annosArray.push(newAnnosAlt);
// Add <Annotations> to existing carrier only
else if(hasAlternativeCarrier) {
if (!newAnnosAlternative) { // only create upon insertion of first anno
newAnnosAlternative = new Edm.Annotations(v, alternativeEdmTargetName);
g_annosArray.push(newAnnosAlternative);
}
newAnnosAlt.append(annotation);
newAnnosAlternative.append(annotation);
}
}
else {
else if(testToStandardEdmTarget(appliesTo)) {
newAnnosStd.append(annotation);

@@ -486,7 +587,7 @@ }

// @v.t3#y.q2
//
//
// { v : { t1 : ...,
// t2 : { p1 : ...,
// p2 : ... },
// t3#x : { q1 : ...,
// t3#x : { q1 : ...,
// q2 : ... }

@@ -542,4 +643,7 @@ // t3#y : { q1 : ...,

function handleTerm(termName, annoValue, context) {
// create the <Annotation ...> tag
let newAnno = new edmlib.Annotation(v, termName);
/**
* create the <Annotation ...> tag
* @type {object}
* */
let newAnno = new Edm.Annotation(v, termName);

@@ -565,3 +669,3 @@ // termName may contain a qualifier: @UI.FieldGroup#shippingStatus

else {
warningMessage(context, "unknown term " + termNameWithoutQualifiers);
infoMessage(context, "unknown term " + termNameWithoutQualifiers);
}

@@ -652,3 +756,3 @@

if(oTermName == "Core.OperationAvailable" && dTypeName === "Edm.Boolean" && cAnnoValue === null) {
oTarget.append(new edmlib.ValueThing(v, 'Null'));
oTarget.append(new Edm.ValueThing(v, 'Null'));
oTarget._ignoreChildren = true;

@@ -762,3 +866,3 @@ }

typeName = "Decimal";
if (isNaN(val) || isNaN(parseFloat(val))) {
if (isNaN(Number(val)) || isNaN(parseFloat(val))) {
warningMessage(context, "found non-numeric string, but expected type " + dTypeName);

@@ -769,3 +873,3 @@ }

typeName = "Float";
if (isNaN(val) || isNaN(parseFloat(val))) {
if (isNaN(Number(val)) || isNaN(parseFloat(val))) {
warningMessage(context, "found non-numeric string, but expected type " + dTypeName);

@@ -832,3 +936,3 @@ }

dTypeName = 'Edm.Int64';
}
}
else {

@@ -840,4 +944,6 @@ typeName = 'Float';

}
}
else {
}
else if(val === null && dTypeName == null && typeName == 'String') {
dTypeName = 'Edm.String';
} else {
warningMessage(context, "expected simple value, but found value '" + val + "' with type '" + typeof val + "'");

@@ -859,6 +965,7 @@ }

// dTypeName : name of the expected record type according to vocabulary, may be null
//
//
// can be called for a record directly below a term, or at a deeper level
function generateRecord(obj, termName, dTypeName, context) {
let newRecord = new edmlib.Record(v);
/** @type {object} */
let newRecord = new Edm.Record(v);

@@ -942,3 +1049,3 @@ // first determine what is the actual type to be used for the record

let newPropertyValue = new edmlib.PropertyValue(v, i);
let newPropertyValue = new Edm.PropertyValue(v, i);
// property value can be anything, so delegate handling to handleValue

@@ -959,3 +1066,3 @@ handleValue(obj[i], newPropertyValue, termName, dictPropertyTypeName, context);

function generateCollection(annoValue, termName, dTypeName, context) {
let newCollection = new edmlib.Collection(v);
let newCollection = new Edm.Collection(v);

@@ -986,3 +1093,3 @@ let innerTypeName = null;

let res = handleExpression(value["="], innerTypeName, context);
let newPropertyPath = new edmlib.ValueThing(v, res.name, res.value );
let newPropertyPath = new Edm.ValueThing(v, res.name, res.value );
newPropertyPath.setJSON( { [res.name] : res.value } );

@@ -1001,3 +1108,3 @@ newCollection.append(newPropertyPath);

let res = handleSimpleValue(value, innerTypeName, context);
let newThing = new edmlib.ValueThing(v, res.name, value );
let newThing = (value === null) ?new Edm.ValueThing(v, 'Null') : new Edm.ValueThing(v, res.name, value );
newThing.setJSON( { [res.jsonName] : res.value });

@@ -1023,3 +1130,3 @@ newCollection.append(newThing);

let subset = edmUtils.intersect(specialProperties, Object.keys(obj));
if(subset.length > 1) { // doesn't work for three or more...

@@ -1033,3 +1140,3 @@ warningMessage(context, "edmJson code contains more than one special property: " + subset);

let k = Object.keys(obj)[0];
return new edmlib.ValueThing(v, k.slice(1), obj[k] );
return new Edm.ValueThing(v, k.slice(1), obj[k] );
}

@@ -1041,3 +1148,3 @@ warningMessage(context, "edmJson code contains no special property out of: " + specialProperties);

// name of special property determines element kind
let newElem = new edmlib.Thing(v, subset[0].slice(1));
let newElem = new Edm.Thing(v, subset[0].slice(1));
let mainAttribute = null;

@@ -1047,4 +1154,4 @@

// copy all '$' attributes that are not $Apply or $LabeledElement to Thing
if(specialProperties.every(v =>
{ return p != v }))
if(specialProperties.every(v =>
{ return p != v }))
{

@@ -1058,3 +1165,3 @@ if (p.charAt(0) == "$") {

}
}
}
else { // we are either $Apply or $LabeledElement

@@ -1075,3 +1182,3 @@ // handle value of special property

}
newElem.append(new edmlib.ValueThing(v, getTypeName(a), a));
newElem.append(new Edm.ValueThing(v, getTypeName(a), a));
}

@@ -1158,3 +1265,3 @@ }

let type = getDictType(dTypeName);
return type && type["$kind"] == "ComplexType";
return dTypeName == 'Edm.ComplexType' || type && type["$kind"] == "ComplexType";
}

@@ -1188,2 +1295,2 @@

module.exports = { knownVocabularies, csn2annotationEdm };
module.exports = { knownVocabularies, vocabularyDefinitions, csn2annotationEdm };

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

const { getUtils, cloneCsn } = require('../model/csnUtils');
const { checkCSNVersion } = require('../json/csnVersion');
/*

@@ -25,17 +27,18 @@ OData V2 spec 06/01/2017 PDF version is available from here:

function csn2edm(csn, serviceName, _options) {
return csn2edmAll(csn, _options, serviceName)[serviceName];
function csn2edm(_csn, serviceName, _options) {
return csn2edmAll(_csn, _options, serviceName)[serviceName];
}
function csn2edmAll(csn, _options, serviceName=undefined) {
function csn2edmAll(_csn, _options, serviceName=undefined) {
// get us a fresh model copy that we can work with
let csn = cloneCsn(_csn);
const { error, warning, info, signal } = alerts(csn);
const { error, warning, signal } = alerts(csn);
checkCSNVersion(csn, _options);
let rc = Object.create(null);
const { isBuiltinType } = getUtils(csn);
// get us a fresh model copy that we can work with
let model = cloneCsn(csn);
let [services, options] = initializeModel(model, _options, signal, error, warning, info);
const Edm = require('./edm.js')(options, signal, error, warning, info);
let [services, options] = initializeModel(csn, _options);
const Edm = require('./edm.js')(csn, options);

@@ -47,3 +50,2 @@

let rc = Object.create(null);
if(serviceName) {

@@ -81,2 +83,3 @@ let serviceCsn = services.filter(s => s.name == serviceName)[0];

/** @type {object} */
let Schema = new Edm.Schema(v, serviceName, undefined /* unset alias */);

@@ -96,16 +99,16 @@

*/
edmUtils.foreach(model.definitions,
edmUtils.foreach(csn.definitions,
a => edmUtils.isEntityOrView(a) && !a.abstract && a.name.startsWith(serviceName + '.'),
a => createEntityTypeAndSet(a, !(options.isV4() && edmUtils.isContainee(a)) && !a.$proxy)
createEntityTypeAndSet
);
// create unbound actions/functions
edmUtils.foreach(model.definitions, a => edmUtils.isActionOrFunction(a) && a.name.startsWith(serviceName + '.'),
edmUtils.foreach(csn.definitions, a => edmUtils.isActionOrFunction(a) && a.name.startsWith(serviceName + '.'),
(options.isV4()) ? createActionV4 : createActionV2);
// create the complex types
edmUtils.foreach(model.definitions, a => edmUtils.isStructuredType(a) && a.name.startsWith(serviceName + '.'), createComplexType);
edmUtils.foreach(csn.definitions, a => edmUtils.isStructuredType(a) && a.name.startsWith(serviceName + '.'), createComplexType);
if(options.isV4())
{
edmUtils.foreach(model.definitions,
edmUtils.foreach(csn.definitions,
artifact => edmUtils.isDerivedType(artifact) &&

@@ -120,3 +123,3 @@ !edmUtils.isAssociationOrComposition(artifact) &&

if(acc[cur.Name] === undefined) {
acc[cur.Name] = [ cur ];
acc[cur.Name] = [ cur ];
} else {

@@ -146,8 +149,14 @@ acc[cur.Name].push(cur);

}
if(Schema._ec._children.length == 0)
signal(error`EntityContainer must contain at least one EntitySet`, ['definitions',Schema.Namespace]);
// remove EntityContainer if empty
if(Schema._ec._children.length == 0) {
let pos = Schema._children.indexOf(Schema._ec);
Schema._children.splice(pos, 1);
}
if(Schema._children.length == 0) {
signal(warning`Schema is empty`, ['definitions',Schema.Namespace]);
}
return edm
function createEntityTypeAndSet(entityCsn, createEntitySet=true)
function createEntityTypeAndSet(entityCsn)
{

@@ -158,3 +167,3 @@ // EntityType attributes are: Name, BaseType, Abstract, OpenType, HasStream

let EntityTypeName = entityCsn.name.replace(namespace, '');
let EntitySetName = (entityCsn.setName || entityCsn.name).replace(namespace, '');
let EntitySetName = (entityCsn.entitySetName || entityCsn.name).replace(namespace, '');

@@ -179,6 +188,19 @@ let [ properties, hasStream ] = createProperties(entityCsn);

if (createEntitySet)
if (entityCsn.hasEntitySet)
{
let entitySet = new Edm.EntitySet(v, { Name: EntitySetName, EntityType: fullQualified(EntityTypeName) }, entityCsn);
/** @type {object} */
let containerEntry;
let singleton = entityCsn['@odata.singleton'];
let hasNullable = entityCsn['@odata.singleton.nullable'] !== undefined &&
entityCsn['@odata.singleton.nullable'] !== null;
if(singleton || ((singleton === undefined || singleton === null) && hasNullable)) {
containerEntry = new Edm.Singleton(v, { Name: EntitySetName, Type: fullQualified(EntityTypeName) }, entityCsn);
if(entityCsn['@odata.singleton.nullable'])
containerEntry.Nullable= true;
}
else {
containerEntry = new Edm.EntitySet(v, { Name: EntitySetName, EntityType: fullQualified(EntityTypeName) }, entityCsn);
}
// V4: Create NavigationPropertyBinding in EntitySet

@@ -188,8 +210,9 @@ // if NavigationProperty is not a Containment and if the target is not a containee

properties.filter(np =>
np instanceof Edm.NavigationProperty &&
!np.isContainment() && !edmUtils.isContainee(np._targetCsn) && !np._targetCsn.$proxy
np instanceof Edm.NavigationProperty &&
// @ts-ignore TypeScript does not recognize these properties on type NavigationProperty
!np.isContainment() && !edmUtils.isContainee(np._targetCsn) && !np._targetCsn.$proxy
). forEach(np =>
entitySet.append(np.createNavigationPropertyBinding(namespace)));
containerEntry.append(np.createNavigationPropertyBinding(namespace)));
Schema._ec.append(entitySet);
Schema._ec.append(containerEntry);
}

@@ -216,2 +239,3 @@

/** @type {object} */
let actionNode = (iAmAnAction) ? new Edm.Action(v, attributes)

@@ -227,9 +251,12 @@ : new Edm.FunctionDefinition(v, attributes);

actionNode.IsBound = true;
let bpType = actionCsn['@cds.odata.bindingparameter.collection'] ?
'Collection('+fullQualified(entityCsn.name)+')' : fullQualified(entityCsn.name);
let bpType = fullQualified(entityCsn.name);
// Binding Parameter: 'in' at first position in sequence, this is decisive!
actionNode.append(new Edm.Parameter(v, { Name: bpName, Type: bpType }, {} ));
if(actionCsn['@cds.odata.bindingparameter.collection'])
actionNode.append(new Edm.Parameter(v, { Name: bpName, Type: bpType, Collection:true } ));
else
actionNode.append(new Edm.Parameter(v, { Name: bpName, Type: bpType } ));
}
else // unbound => produce Action/FunctionImport
{
/** @type {object} */
let actionImport = iAmAnAction

@@ -242,3 +269,3 @@ ? new Edm.ActionImport(v, { Name: actionName, Action : fullQualified(actionName) })

{
let definition = model.definitions[rt];
let definition = csn.definitions[rt];
if(definition && definition.kind == 'entity' && !definition.abstract)

@@ -271,2 +298,3 @@ {

{
/** @type {object} */
let functionImport = new Edm.FunctionImport(v, { Name: name.replace(namespace, '') } );

@@ -288,3 +316,3 @@

{
let defintion = model.definitions[rt];
let defintion = csn.definitions[rt];
if(defintion && edmUtils.isEntityOrView(defintion))

@@ -324,3 +352,3 @@ {

for (let p in actionCsn)
if (p.match(/^@sap./))
if (p.match(/^@sap\./))
functionImport.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : actionCsn[p] });

@@ -353,5 +381,8 @@

// returns a [ [ Edm Properties ], boolean hasStream ]
// array of Edm Properties
// boolean hasSstream : true if at least one element has @Core.MediaType assignment
/**
* @param {object} parentCsn
* @returns {[object[], boolean]} Returns a [ [ Edm Properties ], boolean hasStream ]:
* array of Edm Properties
* boolean hasStream : true if at least one element has @Core.MediaType assignment
*/
function createProperties(parentCsn)

@@ -372,3 +403,3 @@ {

// This is the V4 edmx:NavigationProperty
// gets rewritten for V2 in addAssocations()
// gets rewritten for V2 in addAssociations()

@@ -389,3 +420,7 @@ // suppress navprop creation only if @odata.navigable:false is not annotated.

}
else if(!elementCsn['@cds.api.ignore'] || elementCsn['@cds.api.ignore'] !== true)
// render ordinary property if element is NOT ...
// 1) ... annotated @cds.api.ignore, @cds.odata.{v2|v4}.ignore (+betaMode)
// 2) ... annotated @odata.foreignKey4 and odataFormat: structured
else if(edmUtils.isEdmPropertyRendered(elementCsn, options))
{

@@ -412,3 +447,3 @@ // CDXCORE-CDXCORE-173

if ( options.isV2() && elementCsn['@Core.MediaType']
|| options.isV4() && isContainerAssoc && !elementCsn.key ) {
|| options.isV4() && isContainerAssoc /*&& !elementCsn.key*/ ) {
hasStream = true;

@@ -426,3 +461,3 @@ // CDXCORE-CDXCORE-177: remove elementCsn from elements dictionary

function createComplexType(structuredTypeCsn)
function createComplexType(structuredTypeCsn)
{

@@ -478,10 +513,10 @@ // V4 attributes: Name, BaseType, Abstract, OpenType

let toRole = navigationProperty.Type.replace(alias, ''); // <= navprops type should be prefixed with alias
let fromEntityType = fromRole;
let toEntityType = toRole;
// The entity set name may not be the same as the type name (parameterized entities have
// The entity set name may not be the same as the type name (parameterized entities have
// differing set names (<T>Parameters => <T>, <T>Type => <T>Set)
let fromEntitySet = ( navigationProperty._csn._parent.setName || fromEntityType).replace(namespace, '');
let toEntitySet = (navigationProperty._targetCsn.setName || toEntityType).replace(namespace, '');
let fromEntitySet = ( navigationProperty._csn._parent.entitySetName || fromEntityType).replace(namespace, '');
let toEntitySet = (navigationProperty._targetCsn.entitySetName || toEntityType).replace(namespace, '');

@@ -537,3 +572,3 @@ // from and to roles must be distinguishable (in case of self association entity E { toE: association to E; ... })

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

@@ -599,3 +634,3 @@ }

// produce a full qualified name replacing the namespace with the alias (if provided)
function fullQualified(name)
function fullQualified(name)
{

@@ -610,3 +645,4 @@ if (name == serviceCsn.name)

{
let annoEdm = translate.csn2annotationEdm(model, serviceName, options);
/** @type {object} */
let annoEdm = translate.csn2annotationEdm(csn, serviceName, options);
for(let i = 0; i < annoEdm.getSchemaCount(); i++)

@@ -620,2 +656,2 @@ {

}
module.exports = { csn2edm, csn2edmAll };
module.exports = { csn2edm, csn2edmAll };

@@ -0,7 +1,21 @@

// @ts-nocheck
'use strict'
const edmUtils = require('./edmUtils.js');
const alerts = require('../base/alerts');
const transformUtils = require('../transform/transformUtilsNew.js');
const { getUtils } = require('../model/csnUtils');
module.exports = function (options, signal, error, warning, info) {
module.exports = function (csn, options) {
const { error, info, signal } = alerts(csn, options);
const pathDelimiter = options.isStructFormat ? '/' : '_';
const { flattenStructuredElement } = transformUtils.getTransformers(csn, options, '/');
const {
getFinalTypeDef,
isStructured,
} = getUtils(csn);
class Node

@@ -108,2 +122,7 @@ {

if(kind=="Parameter" && this.Collection) {
delete this.Collection;
this.Type=`Collection(${this.Type})`;
}
head += this.toXMLattributes();

@@ -276,3 +295,2 @@

}
return json;
}

@@ -430,21 +448,6 @@

class EntitySet extends Node
class Singleton extends Node
{
// virtual
setSapVocabularyAsAttributes(csn)
{
if(csn)
{
for (let p in csn._EntitySetAttributes)
{
if (p.match(/^@sap./))
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn._EntitySetAttributes[p] } );
}
}
}
toJSONattributes(json)
{
// OASIS ODATA-1231 $Collection=true
json['$Collection']=true;
for (let p in this)

@@ -476,4 +479,28 @@ {

class EntitySet extends Singleton
{
// virtual
setSapVocabularyAsAttributes(csn)
{
if(csn)
{
for (let p in csn._EntitySetAttributes)
{
if (p.match(/^@sap\./))
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn._EntitySetAttributes[p] } );
}
}
}
toJSONattributes(json)
{
// OASIS ODATA-1231 $Collection=true
json['$Collection']=true;
return super.toJSONattributes(json);
}
}
class Key extends Node
{
// keys is an array of [name] or [name, alias]
constructor(v, keys)

@@ -484,3 +511,3 @@ {

{
keys.forEach(k => this.append(new PropertyRef(v, k)));
keys.forEach(k => this.append(new PropertyRef(v, ...k)));
}

@@ -637,11 +664,9 @@ }

}
// store undecorated type for JSON
this.set( { _type : this[typeName] });
// decorate for XML (not for Complex/EntityType)
if(this._isCollection)
this[typeName] = `Collection(${this[typeName]})`
}
}
// store undecorated type for JSON
this.set( { _type : this[typeName] });
// decorate for XML (not for Complex/EntityType)
if(this._isCollection)
this[typeName] = `Collection(${this[typeName]})`
}

@@ -653,3 +678,3 @@

// @ property and parameter for performance reasons
if(this._type != 'Edm.String' && this._type) // Edm.String is default)
if(this._type !== 'Edm.String' && this._type) // Edm.String is default)
json['$'+this._typeName] = this._type;

@@ -682,5 +707,18 @@

this.append(...properties);
let keys = properties.filter(c => c.isKey).map(c => c.Name);
if(keys.length > 0)
this.set( { _keys: new Key(v, keys) } );
let keys = properties.filter(c => c.isKey);// .map(c => c.Name);
let keyPaths = [];
// if a key has a complex type, transform the key into an array of paths
keys.forEach(k => {
// TODO: assert !v4?
// why is flattenStructuredElements not working on simple types?
if (isStructured(k._csn.type) || (k._csn.type && getFinalTypeDef(k._csn.type).elements)) {
// flatten the struct paths to (a/b/c) and create an alias __ET_a_b_c for each path
keyPaths.push(...Object.keys(flattenStructuredElement(k._csn, k.Name)).map(k => [k, '__'+this.Name+k.split(pathDelimiter).join('')]));
} else {
keyPaths.push([k.Name]);
}
});
if(keyPaths.length > 0)
this.set( { _keys: new Key(v, keyPaths) } );
}

@@ -817,2 +855,3 @@

// the V2 metadata.xml
// @ts-ignore
Property.SAP_Annotation_Attribute_WhiteList = [

@@ -836,2 +875,3 @@ '@sap.hierarchy.node.for', //-> sap:hierarchy-node-for

{
// @ts-ignore
if (Property.SAP_Annotation_Attribute_WhiteList.includes(p))

@@ -850,3 +890,5 @@ this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn[p] });

{
constructor(v, Name) { super(v, { Name }); }
constructor(v, Name, Alias) {
super(v, (Alias) ? { Name, Alias } : { Name });
}
}

@@ -874,5 +916,4 @@

let json = Object.create(null);
for (let p in this)
json[p[0] == '@' ? p : '$' + p] = this[p]
return json;
json['$Name'] = this.Name;
return this.toJSONattributes(json);
}

@@ -905,11 +946,3 @@ }

delete this.Nullable;
}
if(csn.type === 'cds.Composition')
{
// TODO: to be specified via @sap.on.delete
this.append(new OnDelete(v, { Action: 'Cascade' } ) );
}
let partner = (csn._partnerCsn.length > 0) ? csn._partnerCsn[0] : csn._constraints._originAssocCsn;

@@ -929,4 +962,11 @@ if(partner && partner['@odata.navigable'] !== false) {

*/
if(csn['@odata.contained'] == true || csn.containsTarget)
if(csn['@odata.contained'] == true || csn.containsTarget) {
this.ContainsTarget = true;
}
if(this.ContainsTarget === undefined && csn.type === 'cds.Composition') {
// Delete is redundant in containment
// TODO: to be specified via @sap.on.delete
this.append(new OnDelete(v, { Action: 'Cascade' } ) );
}
}

@@ -1011,3 +1051,3 @@

edmUtils.forAll(this._csn._constraints.constraints,
c => this.append(new ReferentialConstraint(this._v, { Property: c[0], ReferencedProperty: c[1] } ) ) );
c => this.append(new ReferentialConstraint(this._v, { Property: c[0].join(pathDelimiter), ReferencedProperty: c[1].join(pathDelimiter) } ) ) );
}

@@ -1316,4 +1356,4 @@ }

edmUtils.forAll(c, cv => {
node._d.append(new PropertyRef(v, cv[0]));
node._p.append(new PropertyRef(v, cv[1]));
node._d.append(new PropertyRef(v, cv[0].join(pathDelimiter)));
node._p.append(new PropertyRef(v, cv[1].join(pathDelimiter)));
});

@@ -1331,2 +1371,3 @@ return node;

EntitySet,
Singleton,
TypeDefinition,

@@ -1333,0 +1374,0 @@ EnumType,

'use strict';
/* eslint max-statements-per-line:off */
const { setProp } = require('../base/model');
const { getUtils, cloneCsn } = require('../model/csnUtils');
const { forEachDefinition, forEachMemberRecursively, getUtils, cloneCsn } = require('../model/csnUtils');
const alerts = require('../base/alerts');
const edmUtils = require('./edmUtils.js')

@@ -19,2 +21,3 @@ const {

/*

@@ -28,3 +31,3 @@ * edmPreprocessor warms up the model so that it can be converted into an EDM document and

function initializeModel(model, _options, signal, error, warning, info)
function initializeModel(csn, _options)
{

@@ -34,16 +37,20 @@ if(_options == undefined)

const { error, warning, info, signal } = alerts(csn);
const {
isStructured,
isAssocOrComposition,
} = getUtils(csn);
// make sure options are complete
let options = validateOptions(_options);
// are we working with structured OData or not
//const structuredOData = options.betaModeOdata && options.toOdata.version === 'v4';
// First attach names to all definitions in the model
forAll(model.definitions, (a, n) => {
a.name = n;
// First attach names to all definitions in the model
forAll(csn.definitions, (a, n) => {
setProp (a, 'name', n);
});
foreach(model.definitions, isActionOrFunction, a => {
foreach(csn.definitions, isActionOrFunction, a => {
forAll(a.params, (p,n) => {
p.name = n;
setProp (p, 'name', n);
})

@@ -53,4 +60,4 @@ });

// Fetch service objects
let services = Object.keys(model.definitions).reduce((services, artName) => {
let art = model.definitions[artName];
let services = Object.keys(csn.definitions).reduce((services, artName) => {
let art = csn.definitions[artName];
if(art.kind === 'service' && !art.abstract) {

@@ -64,19 +71,113 @@ services.push(art);

// Link association targets and spray @odata.contained over untagged compositions
foreach(model.definitions, isStructuredArtifact, linkAssociationTarget);
foreach(csn.definitions, isStructuredArtifact, linkAssociationTarget);
// Create data structures for containments
foreach(model.definitions, isStructuredArtifact, initializeContainments);
foreach(csn.definitions, isStructuredArtifact, initializeContainments);
// Initialize entities with parameters (add Parameter entity)
foreach(model.definitions, isParameterizedEntityOrView, initializeParameterizedEntityOrView);
foreach(csn.definitions, isParameterizedEntityOrView, initializeParameterizedEntityOrView);
// Initialize structures
foreach(model.definitions, isStructuredArtifact, initializeStructure);
foreach(csn.definitions, isStructuredArtifact, initializeStructure);
// Initialize associations
foreach(model.definitions, isStructuredArtifact, initializeAssociation);
foreach(csn.definitions, isStructuredArtifact, initializeAssociation);
// get constraints for associations
foreach(model.definitions, isStructuredArtifact, initializeConstraints);
foreach(csn.definitions, isStructuredArtifact, initializeConstraints);
// create association target proxies
foreach(model.definitions, isStructuredArtifact, redirectDanglingAssociationsToProxyTargets);
foreach(csn.definitions, isStructuredArtifact, redirectDanglingAssociationsToProxyTargets);
// decide if an entity set needs to be constructed or not
foreach(csn.definitions, isStructuredArtifact, determineEntitySet);
// let all doc props become @Core.Descriptions
forEachDefinition(csn, artifact => {
forEachMemberRecursively(artifact,
member => {
assignAnnotation(member, '@Core.Description', member.doc);
}
);
});
return [services, options];
// link association target to association and add @odata.contained to compositions in V4
function linkAssociationTarget(struct) {
foreach(struct.elements, isAssociationOrComposition, (element, name) => {
if (element._ignore)
return;
if(!element._target) {
let target = csn.definitions[element.target];
if(target) {
setProp(element, '_target', target);
// If target has parameters, xref assoc at target for redirection
if(isParameterizedEntityOrView(target)) {
if(!target.$sources) {
setProp(target, '$sources', Object.create(null));
}
target.$sources[struct.name + '.' + name] = element;
}
}
else {
signal(error`Target ${element.target} cannot be found in the model`, [ 'definitions', struct.name, 'elements', element.name ]);
}
}
// in V4 tag all compositions to be containments
// TOOD: remove betaMode once odataContainment option is finalized
if(options.betaMode &&
options.odataContainment &&
options.isV4() &&
isComposition(element) &&
element['@odata.contained'] === undefined) {
element['@odata.contained'] = true;
}
});
}
// Perform checks and add attributes for "contained" sub-entities:
// - A container is recognized by having an association/composition annotated with '@odata.contained'.
// - All targets of such associations ("containees") are marked with a property
// '_containerEntity: []', having as value an array of container names (i.e. of entities
// that have a '@odata.contained' association pointing to the containee). Note that this
// may be multiple entities, possibly including the container itself.
// - All associations in the containee pointing back to the container are marked with
// a boolean property '_isToContainerEntity : true', except if the association itself
// has the annotation '@odata.contained' (indicating the top-down link in a hierarchy).
// - Rewrite annotations that would be assigned to the containees entity set for the
// non-containment rendering. If containment rendering is active, the containee has no
// entity set. Instead try to rewrite the annotation in such a way that it is effective
// on the containment navigation property.
function initializeContainments(container) {
foreach(container.elements, isAssociationOrComposition, (element, elementName) => {
if (element._ignore)
return;
if(element['@odata.contained']) {
// Let the containee know its container
// (array because the contanee may contained more then once)
let containee = element._target;
if (!element._target._containerEntity) {
setProp(element._target, '_containerEntity', []);
}
// add container only once per containee
if (!containee._containerEntity.includes(container.name)) {
containee._containerEntity.push(container.name);
// Mark associations in the containee pointing to the container (i.e. to this entity)
for (let containeeElementName in containee.elements) {
let containeeElement = containee.elements[containeeElementName];
if (containeeElement._target && containeeElement._target.name) {
// If this is an association that points to a container (but is not by itself contained,
// which would indicate the top role in a hierarchy) mark it with '_isToContainer'
if (containeeElement._target.name == container.name && !containeeElement['@odata.contained']) {
setProp(containeeElement, '_isToContainer', true);
}
}
}
}
rewriteContainmentAnnotations(container, containee, elementName);
}
});
}
// Split an entity with parameters into two entity types with their entity sets,
// one named <name>Parameter and one named <name>Type. Parameter contains Type.
// Containment processing must take place before because it might be that this
// artifact with parameters is already contained. In such a case the existing
// containment chain must be propagated and reused. This requires that the
// containment data structures must be manually added here and rewriteContainmentAnnotations()
// must be called.
function initializeParameterizedEntityOrView(entityCsn, entityName) {

@@ -107,3 +208,3 @@

name: parameterEntityName,
setName: parameterEntitySetName,
entitySetName: parameterEntitySetName,
kind: 'entity',

@@ -140,8 +241,9 @@ isParamEntity:true,

setProp(parameterCsn.elements[parameterToOriginalAssocName], '_target', entityCsn);
setProp(parameterCsn.elements[parameterToOriginalAssocName], '_parameterCsn', []);
model.definitions[parameterCsn.name] = parameterCsn;
csn.definitions[parameterCsn.name] = parameterCsn;
// modify the original parameter entity with backlink and new name
csn.definitions[originalEntityName] = entityCsn;
delete csn.definitions[entityCsn.name];
entityCsn.name = originalEntityName;
entityCsn.setName = originalEntitySetName;
setProp(entityCsn, 'entitySetName', originalEntitySetName);

@@ -154,9 +256,18 @@ // add backlink association

type: 'cds.Association',
_partnerCsn: [],
on: [ { ref: [ 'Parameters', 'Set' ] }, '=', { ref: [ '$self' ] } ]
};
setProp(entityCsn.elements[backlinkAssocName], '_partnerCsn', []);
setProp(entityCsn.elements[backlinkAssocName], '_target', parameterCsn);
setProp(entityCsn.elements[backlinkAssocName], '_parameterCsn', []);
}
// redirect inbound associations/compositions to the parameter entity
/*
TODO: Clearify in Spec meeting if this is correct
Object.keys(entityCsn.$sources || {}).forEach(n => {
entityCsn.$sources[n]._target = parameterCsn;
entityCsn.$sources[n].target = parameterCsn.name;
});
*/
rewriteContainmentAnnotations(parameterCsn, entityCsn, parameterToOriginalAssocName);
}
// Initialize structured artifact (type or entity) 'struct' by doing the

@@ -176,3 +287,4 @@ // following:

// Attach name and Name (Name is used in function ForeignKey4(assoc))
element.name = element.Name = elementName;
element.Name = elementName;
setProp(element, 'name', elementName)
setProp(element, '_parent', struct);

@@ -190,3 +302,2 @@

}
applyAppSpecificLateCsnTransformationOnElement(options, element, struct);

@@ -223,17 +334,13 @@ });

setProp(struct, '_EntitySetAttributes', Object.create(null));
applyAppSpecificLateCsnTransformationOnStructure(options, struct);
struct['$keys'] = keys;
initializeActions(struct.actions);
}
// initialize bound actions and functions
function initializeActions(actions)
{
// Attach name to actions and their parameters
forAll(actions, (a, n) => {
forAll(struct.actions, (a, n) => {
a.name = n;
forAll(a.params, (a, n) => {
a.name = n;
forAll(a.params, (p, n) => {
p.name = n;
});

@@ -243,27 +350,2 @@ });

// link association target to association and add @odata.contained to compositions in V4
function linkAssociationTarget(struct) {
foreach(struct.elements, isAssociationOrComposition, element => {
if (element._ignore)
return;
if(!element._target) {
let target = model.definitions[element.target];
if(target) {
setProp(element, '_target', target);
}
else {
signal(error`Target ${element.target} cannot be found in the model`, [ 'definitions', struct.name, 'elements', element.name ]);
}
}
// TODO: Remove betaMode if finalized
// in V4 tag all compositions to be containments
if(options.betaMode &&
options.isV4() &&
isComposition(element) &&
element['@odata.contained'] === undefined) {
element['@odata.contained'] = true;
}
});
}
// Resolve the association type of 'element' in 'struct' by doing the following:

@@ -283,3 +365,3 @@ // - collect the foreign key elements for the target into attribute 'elements'

//forward annotations from managed association element to its foreign keys
if(element.keys) {
if(element.keys && options.isFlatFormat) {
for(let fk of element.keys) {

@@ -295,39 +377,2 @@ forAll(element, (attr, attrName) => {

// Perform checks and add attributes for "contained" sub-entities:
// - A container is recognized by having an association/composition annotated with '@odata.contained'.
// - All targets of such associations ("containees") are marked with a property
// '_containerEntity: []', having as value an array of container names (i.e. of entities
// that have a '@odata.contained' association pointing to the containee). Note that this
// may be multiple entities, possibly including the container itself.
// - All associations in the containee pointing back to the container are marked with
// a boolean property '_isToContainerEntity : true', except if the association itself
// has the annotation '@odata.contained' (indicating the top-down link in a hierarchy).
function initializeContainments(container) {
foreach(container.elements, isAssociationOrComposition, element => {
if (element._ignore)
return;
if(element['@odata.contained']) {
// Let the containee know its container
// (array because the contanee may contained more then once)
if (!element._target._containerEntity) {
setProp(element._target, '_containerEntity', []);
}
// add container only once per containee
if (!element._target._containerEntity.includes(container.name)) {
element._target._containerEntity.push(container.name);
// Mark associations in the containee pointing to the container (i.e. to this entity)
for (let containeeElementName in element._target.elements) {
let containeeElement = element._target.elements[containeeElementName];
if (containeeElement._target && containeeElement._target.name) {
// If this is an association that points to a container (but is not by itself contained,
// which would indicate the top role in a hierarchy) mark it with '_isToContainer'
if (containeeElement._target.name == container.name && !containeeElement['@odata.contained']) {
setProp(containeeElement, '_isToContainer', true);
}
}
}
}
}
});
}

@@ -337,3 +382,3 @@ function initializeConstraints(struct) {

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

@@ -364,6 +409,2 @@ // only in V2 we must set the target cardinality of the backlink to the forward:

function redirectDanglingAssociationsToProxyTargets(struct) {
const {
isStructured,
isAssocOrComposition,
} = getUtils(model);

@@ -399,11 +440,11 @@ function whatsMyServiceName(n) {

if(myServiceName !== whatsMyServiceName(element._target.name)) {
if(options.betaModeProxy) {
if(options.betaMode && options.isStructFormat && options.isV4() && options.toOdata.odataProxies) {
// search for eventually existing proxy
let proxy = element._target.$proxies.filter(p => p.name.startsWith(myServiceName + '.'))[0];
if(!proxy) {
let name = myServiceName + '.' + element._target.name.split('.').join('_') + '_Proxy_0';
let name = myServiceName + '.' + element._target.name.split('.').join('_') + '_proxy';
proxy = { name, kind: 'entity', $proxy: true, elements: Object.create(null) };
let hasKeys = false;
// TODO: If target has parameters, use them as primary key instead of real PK
foreach(element._target.elements, e => e.key, e => {
// :TODO: getFinalBaseType, resolve structs
// Omit associations (no navigation properties)

@@ -415,11 +456,14 @@ let ignore = false;

if(isStructured(e)) {
ignore = true;
// parameters/elements
exposeStructTypeOf(e, `__${name.replace(/\./g, '_')}_${e.name}`, e.name);
/*
signal(info`Structured types not yet supported as primary keys of proxy entity type "${name}" for unexposed association target "${element._target.name}"`,
['definitions', struct.name, 'elements', element.name]);
*/
}
if(!ignore) {
// clone elements and strip of all annotations
// clone elements and strip off all annotations
proxy.elements[e.name] = cloneCsn(e);
Object.keys(proxy.elements[e.name]).forEach(k => { if(k[0] === '@') delete proxy.elements[e.name][k] } );
Object.keys(proxy.elements[e.name]).forEach(k => {
if(k[0] === '@') delete proxy.elements[e.name][k]
});
hasKeys = true;

@@ -434,6 +478,11 @@ }

}
signal(info`Created proxy entity type "${name}" for unexposed association target "${element._target.name}"`,
// wire up proxy
if(csn.definitions[name] !== undefined)
signal(error`Duplicate proxy entity type "${name}" for unexposed association target "${element._target.name}"`,
['definitions', struct.name, 'elements', element.name]);
// wire up proxy
model.definitions[name] = proxy;
else {
signal(info`Created proxy entity type "${name}" for unexposed association target "${element._target.name}"`,
['definitions', struct.name, 'elements', element.name]);
csn.definitions[name] = proxy;
}
element._target.$proxies.push(proxy);

@@ -447,3 +496,3 @@ }

element._ignore = true;
signal(info`Association is auto-excluded, as target "${element._target.name}" is outside any service`,
signal(warning`No OData navigation property generated for association "${element.name}", as target "${element._target.name}" is outside any service`,
['definitions', struct.name, 'elements', element.name]);

@@ -455,6 +504,131 @@ return;

}
// 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, artificialName, parentName) {
if (!node) {
return;
}
if (node.items) {
exposeStructTypeOf(node.items, artificialName, parentName);
}
let elements = (csn.definitions[node.type] || node).elements;
let isNotInProtNS = node.type && (!node.type.startsWith('cds.') || node.type.startsWith('cds.foundation'));
let isNotInService = node.type && myServiceName !== whatsMyServiceName(node.type);
if (elements && !node.type || (isNotInService && isNotInProtNS)) {
let typeDef = node.type ? csn.definitions[node.type] : /* anonymous type */ node;
if (typeDef && isStructured(typeDef)) {
// if type has been exposed already use this type name
if(typeDef.$odataProxyType) {
node.type = typeDef.$odataProxyType;
return;
} else {
// expose the type
let typeId = node.type ? `${artificialName}_${node.type.replace(/\./g, '_')}` : artificialName;
let type = exposeStructType(typeId, typeDef.elements);
if(type) {
// Recurse into elements of 'type' (if any)
for (let elemName in type.elements) {
exposeStructTypeOf(type.elements[elemName], `${typeId}_${elemName}`, parentName);
}
typeDef.$odataProxyType = node.type = `${myServiceName}.${typeId}`;
}
}
}
}
function exposeStructType(typeId, elements) {
let typeName = `${myServiceName}.${typeId}`;
// If type already exists, reuse it (complain if not created here)
let type = csn.definitions[typeName];
if (type) {
signal(error`Cannot create type "${typeName}" for "${parentName}" because the name is already used`);
return undefined;
}
// Create type with empty elements
type = {
kind: 'type',
name: typeName,
elements: Object.create(null),
};
setProp(type, '$odataProxyType', typeName);
// Duplicate the type's elements
for (let elemName in elements) {
// !!! DO NOT expose associations for proxies!!!
let elem = elements[elemName];
if(!elem.target) {
type.elements[elemName] = Object.create(null);
for (let prop in elem)
type.elements[elemName][prop] = elem[prop];
}
}
csn.definitions[typeName] = type;
return type;
}
}
}
function determineEntitySet(struct) {
// if this is an entity or a view, determine if an entity set is required or not
// 1) must not be a proxy and not a containee in V4
// No annos are rendered for non-existing EntitySet targets.
if(struct.hasEntitySet === undefined) {
let hasEntitySet = ['entity', 'view'].includes(struct.kind) && !(options.isV4() && edmUtils.isContainee(struct)) && !struct.$proxy;
setProp(struct, 'hasEntitySet', hasEntitySet);
}
}
// If containment in V4 is active, annotations that would be assigned to the containees
// entity set are not renderable anymore. In such a case try to reassign the annotations to
// the containment navigation property.
// Today only Capabilities.*Restrictions are known to be remapped as there exists a CDS
// short cut annotation @readonly that gets expanded and can be safely remapped.
function rewriteContainmentAnnotations(container, containee, assocName) {
// rectify Restrictions to NavigationRestrictions
if(options.isV4() && container['@Capabilities.NavigationRestrictions'] === undefined) {
let navRestr = {
RestrictedProperties: [
{
NavigationProperty: assocName
}
]
};
let hasRestrictions = false;
if(containee['@Capabilities.DeleteRestrictions.Deletable'] !== undefined) {
navRestr.RestrictedProperties[0].DeleteRestrictions =
{ 'Deletable': containee['@Capabilities.DeleteRestrictions.Deletable'] };
delete containee['@Capabilities.DeleteRestrictions.Deletable'];
hasRestrictions = true;
}
if(containee['@Capabilities.InsertRestrictions.Insertable'] !== undefined) {
navRestr.RestrictedProperties[0].InsertRestrictions =
{ 'Insertable': containee['@Capabilities.InsertRestrictions.Insertable'] };
delete containee['@Capabilities.InsertRestrictions.Insertable'];
hasRestrictions = true;
}
if(containee['@Capabilities.UpdateRestrictions.Updatable'] !== undefined) {
navRestr.RestrictedProperties[0].UpdateRestrictions =
{ 'Updatable': containee['@Capabilities.UpdateRestrictions.Updatable'] };
delete containee['@Capabilities.UpdateRestrictions.Updatable'];
hasRestrictions = true;
}
//'@Capabilities.ReadRestrictions.Readable'
if(containee['@Capabilities.ReadRestrictions.Readable'] !== undefined) {
navRestr.RestrictedProperties[0].ReadRestrictions =
{ 'Readable': containee['@Capabilities.ReadRestrictions.Readable'] };
delete containee['@Capabilities.ReadRestrictions.Readable'];
hasRestrictions = true;
}
if(hasRestrictions)
container['@Capabilities.NavigationRestrictions'] = navRestr;
}
}
}
/*

@@ -491,3 +665,3 @@ * Late application specific transformations

{
artifact[mapping] = value || artifact[prop]['='] || artifact[prop];
assignAnnotation(artifact, mapping, value || artifact[prop]['='] || artifact[prop]);
}

@@ -523,3 +697,4 @@

// the entity set.
struct['@Core.OptimisticConcurrency'] = (struct['@Core.OptimisticConcurrency'] || []);//.push(element.name);
assignAnnotation(struct, '@Core.OptimisticConcurrency',
(struct['@Core.OptimisticConcurrency'] || [])/*.push(element.name)*/);
}

@@ -558,3 +733,3 @@ }

targets.forEach(tgt => {
struct.elements[tgt]['@sap.attribute-for'] = element.name;
assignAnnotation(struct.elements[tgt], '@sap.attribute-for', element.name);
});

@@ -569,3 +744,3 @@ }

if(ContextDefiningProperties.length > 0)
element['@sap.super-ordinate'] = ContextDefiningProperties[ContextDefiningProperties.length-1];
assignAnnotation(element, '@sap.super-ordinate', ContextDefiningProperties[ContextDefiningProperties.length-1]);
}

@@ -637,4 +812,4 @@

let e = struct.elements[v.Property];
if(e)
e['@sap.filter-restriction'] = stringDict[v.AllowedExpressions];
if(e)
assignAnnotation(e, '@sap.filter-restriction', stringDict[v.AllowedExpressions]);
});

@@ -650,3 +825,3 @@ }

props.forEach(p => {
struct.elements[p]['@sap.required-in-filter'] = true;
assignAnnotation(struct.elements[p], '@sap.required-in-filter', true);
});

@@ -659,3 +834,3 @@ }

if(requiresFilter)
struct._EntitySetAttributes['@sap.requires-filter'] = requiresFilter;
assignAnnotation(struct._EntitySetAttributes, '@sap.requires-filter', requiresFilter);
}

@@ -678,2 +853,11 @@

// Assign but not overwrite annotation
function assignAnnotation(node, name, value) {
if(value !== undefined &&
name !== undefined && name[0] === '@' &&
(node[name] === undefined ||
node[name] && node[name] === null)) {
node[name] = value;
}
}

@@ -680,0 +864,0 @@ module.exports = {

@@ -12,4 +12,10 @@ 'use strict';

const options = Object.assign({ version: 'v4'}, _options);
if (options.toOdata && options.toOdata.version)
options.version = options.toOdata.version;
if (options.toOdata) {
if(options.toOdata.version)
options.version = options.toOdata.version;
if(options.toOdata.odataFormat)
options.odataFormat = options.toOdata.odataFormat;
if(options.toOdata.odataContainment)
options.odataContainment = options.toOdata.odataContainment;
}

@@ -20,2 +26,4 @@ const v2 = options.version.match(/v2/i) != undefined;

options.v = [v2, v4];
options.isStructFormat = options.odataFormat && options.odataFormat === 'structured';
options.isFlatFormat = !options.isStructFormat;

@@ -27,2 +35,3 @@ if(options.v.filter(v=>v).length != 1)

options.isV4 = function() { return this.v[1] == true; }
return options;

@@ -34,2 +43,11 @@ }

// render ordinary property if element is NOT ...
// 1) ... annotated @cds.api.ignore, @cds.odata.{v2|v4}.ignore (+betaMode)
// 2) ... annotated @odata.foreignKey4 and odataFormat: structured
function isEdmPropertyRendered(elementCsn, options) {
return(!elementCsn['@cds.api.ignore']) &&
!(elementCsn['@cds.odata.'+options.version+'.ignore'] && options.betaMode) &&
!(elementCsn['@odata.foreignKey4'] && options.isStructFormat)
}
// returns intersection of two arrays

@@ -46,3 +64,6 @@ function intersect(a,b)

if (dictionary[name] && filter(dictionary[name])) {
func(dictionary[name], name);
if(Array.isArray(func))
func.forEach(f=>f(dictionary[name], name));
else
func(dictionary[name], name);
}

@@ -86,3 +107,3 @@ }

// Return true if the association 'assoc' has cardinality 'to-many'
// Return true if the association 'assoc' has cardinality 'to-many'
function isToMany(assoc) {

@@ -130,3 +151,3 @@ if (!assoc.cardinality) {

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

@@ -144,3 +165,3 @@ let result = { constraints: Object.create(null), selfs: [], termCount: 0 };

/* example for originalTarget:
entity E (with parameters) {
entity E (with parameters) {
... keys and all the stuff ...

@@ -170,6 +191,7 @@ toE: association to E;

if(!assocCsn._target.isParamEntity && originAssocCsn.key) {
if(originAssocCsn.keys) {
if(originAssocCsn.keys && isFlatFormat) {
for(let fk of originAssocCsn.keys) {
let c = [ fk.ref[0], fk.$generatedFieldName ];
result.constraints[c] = c;
const c = [ [ fk.ref[0] ], [ fk.$generatedFieldName ] ];
const key = c.join(',');
result.constraints[key] = c;
}

@@ -227,4 +249,4 @@ }

c => {
let fk = dependentEntity.elements[c[0]];
let pk = principalEntity.$keys[c[1]];
let fk = dependentEntity.elements[c[0][0]];
let pk = principalEntity.$keys[c[1][0]];
return !(pk && fk && !(pk['@cds.api.ignore'] || fk['@cds.api.ignore']));

@@ -243,11 +265,13 @@ },

// FIXME: If path is something structured, perform a path resolution (or use augmented CSN)
if(!assocCsn._target.isParamEntity && assocCsn.keys) {
for(let fk of assocCsn.keys) {
let realFk = assocCsn._parent.elements[fk.$generatedFieldName];
let pk = assocCsn._target.elements[fk.ref[0]];
if(pk && pk.key && !(pk['@cds.api.ignore'] || realFk['@cds.api.ignore']))
{
let c = [ fk.$generatedFieldName, fk.ref[0] ];
result.constraints[c] = c;
if(isFlatFormat) {
let realFk = assocCsn._parent.elements[fk.$generatedFieldName];
let pk = assocCsn._target.elements[fk.ref[0]];
if(pk && pk.key && !(pk['@cds.api.ignore'] || realFk['@cds.api.ignore']))
{
const c = [ [ fk.$generatedFieldName ], [ fk.ref[0] ] ];
const key = c.join(',');
result.constraints[key] = c;
}
}

@@ -302,2 +326,9 @@ }

// if exactly one operand starts with the prefix then this is potentially a constraint
// strip of prefix '$self's
if(lhs[0] === '$self' && lhs.length > 1)
lhs = lhs.slice(1);
if(rhs[0] === '$self' && rhs.length > 1)
rhs = rhs.slice(1);
if((lhs[0] === assocCsn.name && rhs[0] !== assocCsn.name) ||

@@ -308,14 +339,17 @@ (lhs[0] !== assocCsn.name && rhs[0] === assocCsn.name))

//backlink [ self, assocName ]
let c;
if(lhs[0] === assocCsn.name)
c = [rhs[0], lhs[1]];
c = [rhs, lhs.slice(1)];
else
c = [lhs[0], rhs[1]];
c = [lhs, rhs.slice(1)];
// do we have a $self id?
// if so, store partner in selfs array
if(c[0] === '$self')
result.selfs.push(c[1]);
else
result.constraints[c] = c;
if(c[0][0] === '$self' && c[0].length === 1) {
result.selfs.push(c[1][0]);
} else {
const key = c.join(',');
result.constraints[key] = c;
}
}

@@ -451,3 +485,3 @@ }

{
if (edmType == 'Edm.Date')
if (edmType == 'Edm.Date')
edmType = 'Edm.DateTime';

@@ -500,2 +534,3 @@ if (edmType == 'Edm.TimeOfDay')

validateOptions,
isEdmPropertyRendered,
intersect,

@@ -502,0 +537,0 @@ foreach,

@@ -265,4 +265,6 @@ // Transform augmented CSN into compact "official" CSN

}
if (model.version)
newModel.version = model.version;
// hard code the final old style compact CSN version
if (!options.testMode) {
newModel.version = { csn: "0.1.0" };
}

@@ -269,0 +271,0 @@ let PERSIST_COMPACTED_CSN = "PERSIST_COMPACTED_CSN" in process.env;

// csn version functions
// The CSN file format version produced by this compiler
// (Note that all 0.x.x versions are floating targets, i.e. the format is frequently
// changed without notice. The versions only signal certain combinations of "flavors", see below)
// (Note: the SQL name mapping mode is not reflected in the content of the csn, the version only
// signals which default name mapping a backend has to use)
// Historic versions:
// 0.0.1 : Used by HANA CDS for its CSN output (incomplete, not well defined, quite different from CDX ...)
// 0.0.2 : CDX in the initial versions with old-style CSN, default for SQL name mapping is 'quoted'
// 0.0.99 : Like 0.0.2, but with new-style CSN
// Versions that are currently produced by compiler:
// 0.1.0 : Like 0.0.2, default for SQL name mapping is 'plain'
// 0.1.99 : Like 0.1.0, but with new-style CSN
// 0.2 : same as 0.1.99, but with new top-level properties: $version, meta
const newCSNVersions = ["0.1.99","0.2","0.2.0","1.0"];

@@ -8,3 +22,3 @@ // checks if new-csn is requested vie the options of already specified in the CSN

function isNewCSN(csn, options) {
if( (options && options.newCsn === false) ||
if( (options && options.newCsn === false) ||
(csn.version && !newCSNVersions.includes(csn.version.csn)) ||

@@ -14,8 +28,24 @@ (csn.$version && !newCSNVersions.includes(csn.$version)))

return false;
}
}
return true;
}
function checkCSNVersion(csn, options) {
// the new transformer works only with new CSN
const alerts = require('../base/alerts');
const { CompilationError } = require('../base/messages');
const { error, signal } = alerts(csn);
if (!isNewCSN(csn, options)) {
let errStr = 'CSN Version not supported, version tag: "';
errStr += (csn.version && csn.version.csn ? csn.version.csn : (csn.$version ? csn.$version : 'not available')) + '"';
errStr += (options.newCsn !== undefined) ? ', options.newCsn: ' + options.newCsn : '';
signal(error`${errStr}`);
throw new CompilationError(csn.messages, csn);
}
}
module.exports = {
isNewCSN
isNewCSN,
checkCSNVersion
}

@@ -7,9 +7,11 @@ // Transform augmented CSN into compact "official" CSN

const { locationString } = require('../base/messages');
const { assignAll } = require('../model/csnUtils');
const { assignAll } = require('../model/csnUtils'); // TODO: remove if old CSN frontend is gone
const creator = 'CDS Compiler v' + require('../../package.json').version;
const compilerVersion = require('../../package.json').version;
const creator = `CDS Compiler v${ compilerVersion }`;
const csnVersion = '1.0';
var csn_gensrc = true; // good enough here...
var mode_strict = false; // whether to dump with unknown properties (in standard)
/** @type {boolean|string} */
let gensrcFlavor = true; // good enough here...
let strictMode = false; // whether to dump with unknown properties (in standard)

@@ -31,7 +33,9 @@ // IMPORTANT: the order of these properties determine the order of properties

// early expression / query properties -------------------------------------
op: o => (o.val !== 'query') ? o.val : undefined,
op: o => ((o.val !== 'query') ? o.val : undefined),
from: fromOld, // before elements! XSN TODO just one (cross if necessary)
// join done in from()
// func // in expression()
quantifier: ( q, csn ) => { csn[ q.val ] = true; },
quantifier: ( q, csn ) => {
csn[q.val] = true;
},
all: ignore, // XSN TODO use quantifier

@@ -63,4 +67,4 @@ // type properties (without 'elements') ------------------------------------

namedArgs: renameTo( 'args', args ), // XSN TODO - use args
on: (cond) => (typeof cond === 'string' ? undefined : condition( cond )), // also for join
onCond : renameTo( 'on', c => (csn_gensrc && c.$inferred) ? undefined : condition(c) ), // XSN TODO: onCond -> on
on: cond => (typeof cond === 'string' ? undefined : condition( cond )), // also for join
onCond: renameTo( 'on', c => ((gensrcFlavor && c.$inferred) ? undefined : condition(c)) ), // XSN TODO: onCond -> on
// definitions, extensions, members ----------------------------------------

@@ -87,11 +91,12 @@ returns: standard, // storing the return type of actions

name: ignore, // as is provided extra (for select items, in FROM)
viaTransform: b => b, // FIXME: not a standard prop, start with $
generatedFieldName: renameTo( '$generatedFieldName', n => n ), // TODO: XSN name
$syntax: s => s,
_containerEntity: n => n, // FIXME: prop starting with _ is link and non-enumerable
_ignore: a => a, // not yet obsolete - still required by toHana (FIXME: maybe rename to $ignore, or use an annotation instead?)
_ignore: a => a, // FIXME: remove (still required by toHana)
_ignoreMasked: b => b, // FIXME: prop starting with _ is link and non-enumerable
_isToContainer: b => b, // FIXME: prop starting with _ is link and non-enumerable
location, // -> $location
$extra: (e, csn) => { Object.assign( csn, e ); },
$extra: (e, csn) => {
Object.assign( csn, e );
},
// IGNORED -----------------------------------------------------------------

@@ -101,7 +106,7 @@ artifacts: ignore, // well-introduced, hence not $artifacts

blocks: ignore, // FIXME: make it $blocks
builtin: ignore, // XSN: $builtin, "cds" namespace probably exposed by current transformers
builtin: ignore, // XSN: $builtin, check: "cds" namespace exposed by transformers?
queries: ignore, // FIXME: make it $queries (flat)
typeArguments: ignore, // FIXME: make it $typeArgs
calculated: ignore, // TODO remove ($inferred: 'as')
implicitForeignKeys: ignore, // later in assoc: $inferred: { foreignKeys: 'fk' } or $inferred on each fk
implicitForeignKeys: ignore, // XSN TODO: $inferred on each fk instead
indexNo: ignore, // TODO XSN: remove

@@ -113,6 +118,6 @@ origin: ignore, // TODO remove (introduce non-enum _origin link)

viaAll: ignore, // TODO remove, later in elem: $inferred: '*'
'$': ignore,
//'_' not here, as non-enumerable properties are not transformed anyway
$: ignore,
// '_' not here, as non-enumerable properties are not transformed anyway
_typeIsExplicit: ignore,
}
};

@@ -122,33 +127,36 @@ // Dictionary mapping XSN property names to corresponding CSN property names

const csnPropertyNames = {
kind: ['annotate','extend'],
op: ['join','func','xpr'],
quantifier: ['some','any','distinct', 'ref','_links','_art','_scope', 'param', 'val','literal', 'SELECT','SET'], // 'all' explicitly listed
type: ['_type'],
target: ['_target'],
includes: ['_includes'],
foreignKeys: ['keys'],
exclude: ['excluding'],
limit: ['rows'], //'offset',
elements: ['payload', '$elements'],
sourceMax: ['src'],
targetMin: ['min'],
targetMax: ['max'],
name: ['as','cast'],
generatedFieldName: ['$generatedFieldName'],
location: ['$location'],
}
kind: [ 'annotate', 'extend' ],
op: [ 'join', 'func', 'xpr' ], // TODO: 'func','xpr' into 'quantifier'? TODO: 'global'(scope)?
quantifier: [ 'some', 'any', 'distinct', 'ref', '_links', '_art', '_scope', 'param', 'val', 'literal', 'SELECT', 'SET' ], // 'all' explicitly listed
type: [ '_type' ],
target: [ '_target' ],
includes: [ '_includes' ],
foreignKeys: [ 'keys' ],
exclude: [ 'excluding' ],
limit: [ 'rows' ], // 'offset',
elements: [ 'payload', '$elements' ],
sourceMax: [ 'src' ],
targetMin: [ 'min' ],
targetMax: [ 'max' ],
name: [ 'as', 'cast' ],
generatedFieldName: [ '$generatedFieldName' ],
location: [ '$location' ],
};
const propertyOrder = (function () {
let r = {};
const propertyOrder = (function orderPositions() {
const r = {};
let i = 0;
for (let n in transformers) {
for (const n in transformers) {
r[n] = ++i;
for (let c of csnPropertyNames[n] || [])
for (const c of csnPropertyNames[n] || [])
r[c] = ++i;
}
return r;
})();
}());
const typeProperties = [ // just for `cast` in select items
'type', 'length', 'precision', 'scale', 'srid', 'items', 'target', 'elements', 'enum'
// sync with definition in from-csn.js:
const typeProperties = [
'target', 'elements', 'enum', 'items',
'type', 'length', 'precision', 'scale', 'srid', 'localized',
'foreignKeys', 'onCond', // for explicit ON/keys with REDIRECTED
];

@@ -159,45 +167,54 @@

isNot: [ 'is', 'not' ], // TODO XSN: 'is not'
isNull: postfix( ['is', 'null'] ),
isNull: postfix( [ 'is', 'null' ] ),
isNotNull: postfix( [ 'is', 'not', 'null' ] ),
notIn: [ 'not', 'in' ],
between: ternary( ['between'], ['and'] ),
notBetween: ternary( ['not', 'between'], ['and'] ),
like: ternary( ['like'], ['escape'] ),
notLike: ternary( ['not', 'like'], ['escape'] ),
when: (args) => ['when', ...args[0], 'then', ...args[1]],
case: (args) => ['case'].concat( ...args, ['end'] ),
// xpr: (args) => [].concat( ...args ), see below - handled extra
}
between: ternary( [ 'between' ], [ 'and' ] ),
notBetween: ternary( [ 'not', 'between' ], [ 'and' ] ),
like: ternary( [ 'like' ], [ 'escape' ] ),
notLike: ternary( [ 'not', 'like' ], [ 'escape' ] ),
when: exprs => [ 'when', ...exprs[0], 'then', ...exprs[1] ],
case: exprs => [ 'case' ].concat( ...exprs, [ 'end' ] ),
// xpr: (exprs) => [].concat( ...exprs ), see below - handled extra
};
const csnDictionaries = ['params','enum','mixin','elements','actions','payload', 'definitions'];
const csnDirectValues = ['val','messages']; // + all starting with '@'
const csnDictionaries = [ 'args', 'params', 'enum', 'mixin', 'elements', 'actions', 'payload', 'definitions' ];
const csnDirectValues = [ 'val', 'messages' ]; // + all starting with '@'
// Sort property names of CSN according to sequence which is also used by the compactModel function
// Only intended to be used for tests, as no non-enumerable properties are kept.
function sortCsn( csn ) { // only returns enumerable properties
// Only returns enumerable properties, except for certain hidden properties if requested:
// $location, $env, elements.
function sortCsn( csn, keepHidden = false ) {
if (csn instanceof Array)
return csn.map( v => (!v || typeof v !== 'object' ? v : sortCsn(v) ) );
let r = {};
for (let n of Object.keys(csn).sort( compareProperties ) ) {
return csn.map( v => (!v || typeof v !== 'object' ? v : sortCsn(v, keepHidden) ) );
const r = {};
for (const n of Object.keys(csn).sort( compareProperties ) ) {
const val = csn[n];
if (!val || typeof val !== 'object' || n.charAt() === '@' || csnDirectValues.includes(n)) {
if (!val || typeof val !== 'object' || n.charAt(0) === '@' || csnDirectValues.includes(n))
r[n] = val;
}
else if (csnDictionaries.includes(n)) {
r[n] = csnDictionary( val, n === 'definitions' );
}
else {
r[n] = sortCsn(val);
}
else if (csnDictionaries.includes(n))
r[n] = csnDictionary( val, n === 'definitions', keepHidden );
else
r[n] = sortCsn(val, keepHidden);
}
if (keepHidden && typeof csn === 'object') {
if (csn.$location)
setHidden(r, '$location', csn.$location);
if (csn.$env)
setHidden(r, '$env', csn.$env);
if (csn.elements && !r.elements) // non-enumerable 'elements'
setHidden(r, 'elements', csnDictionary( csn.elements, false, keepHidden ) );
}
return r;
}
function csnDictionary( csn, sort ) {
function csnDictionary( csn, sort, keepHidden = false ) {
if (!csn || csn instanceof Array) // null or strange CSN
return csn;
let r = Object.create(null);
for (let n of (sort) ? Object.keys(csn).sort() : Object.keys(csn)) {
r[n] = sortCsn( csn[n] );
}
const r = Object.create(null);
for (const n of (sort) ? Object.keys(csn).sort() : Object.keys(csn))
r[n] = sortCsn( csn[n], keepHidden );
return r;

@@ -207,5 +224,5 @@ }

function compactModel( model, options = model.options || {} ) {
csn_gensrc = options.toCsn && options.toCsn.flavor === 'gensrc';
mode_strict = options.testMode;
let csn = {};
gensrcFlavor = options.toCsn && options.toCsn.flavor === 'gensrc';
strictMode = options.testMode;
const csn = {};
set( 'definitions', csn, model );

@@ -216,4 +233,2 @@ const exts = extensions( model.extensions || [], csn, model );

set( 'messages', csn, model );
if (model.version)
csn.version = model.version; // TODO remove with CSN version 1.1
if (!options.testMode) {

@@ -224,3 +239,3 @@ csn.meta = Object.assign( {}, model.meta, { creator } );

// Use $extra properties of first source as resulting $extra properties
for (let f in model.sources) {
for (const f in model.sources) {
set( '$extra', csn, model.sources[f] );

@@ -233,11 +248,11 @@ break;

function renameTo( csnProp, func ) {
return function( val, csn, node, prop ) {
let sub = func( val, csn, node, prop );
return function renamed( val, csn, node, prop ) {
const sub = func( val, csn, node, prop );
if (sub !== undefined)
csn[csnProp] = sub;
}
};
}
function arrayOf( func ) {
return ( val, ...args ) => val.map( v => func( v, ...args ) );
return ( val, ...nodes ) => val.map( v => func( v, ...nodes ) );
}

@@ -247,14 +262,15 @@

const exts = node.map( standard ).sort( (a, b) => a.annotate.localeCompare( b.annotate ) );
if (!csn_gensrc)
if (!gensrcFlavor)
return exts;
for (let name of Object.keys( model.definitions ).sort()) {
let art = model.definitions[name];
for (const name of Object.keys( model.definitions ).sort()) {
const art = model.definitions[name];
// in definitions (without redef) with potential inferred elements:
if (!(art instanceof Array) && art.elements &&
(art.query || art.includes || art.$inferred)) {
let annos = art.$inferred && annotations( art, true );
let elements = inferred( art.elements, art.$inferred );
let annotate = Object.assign( { annotate: name }, annos );
if (Object.keys( elements ).length)
annotate.elements = elements;
const annos = art.$inferred && annotations( art, true );
const elems = inferred( art.elements, art.$inferred );
/** @type {object} */
const annotate = Object.assign( { annotate: name }, annos );
if (Object.keys( elems ).length)
annotate.elements = elems;
if (Object.keys( annotate ).length > 1)

@@ -267,9 +283,9 @@ exts.push( annotate );

function inferred( elements, inferredParent ) {
let ext = Object.create(null);
for (let name in elements) {
let elem = elements[name];
function inferred( elems, inferredParent ) {
const ext = Object.create(null);
for (const name in elems) {
const elem = elems[name];
if (elem instanceof Array || !inferredParent && !elem.$inferred)
continue;
let csn = annotations( elem, true );
const csn = annotations( elem, true );
if (Object.keys(csn).length)

@@ -284,9 +300,9 @@ ext[name] = csn;

return node.map( standard );
let csn = {};
const csn = {};
// To avoid another object copy, we sort according to the prop names in the
// XSN input node, not the CSN result node. Not really an issue...
let keys = Object.keys( node ).sort( compareProperties );
for (let prop of keys) {
let transformer = transformers[prop] || transformers[prop.charAt(0)] || unexpected;
let sub = transformer( node[prop], csn, node, prop );
const keys = Object.keys( node ).sort( compareProperties );
for (const prop of keys) {
const transformer = transformers[prop] || transformers[prop.charAt(0)] || unexpected;
const sub = transformer( node[prop], csn, node, prop );
if (sub !== undefined)

@@ -299,5 +315,5 @@ csn[prop] = sub;

function unexpected( val, csn, node, prop ) {
if (mode_strict) {
let loc = val && val.location || node.location;
throw new Error( `Unexpected property ${prop} in ${ locationString(loc) }`);
if (strictMode) {
const loc = val && val.location || node.location;
throw new Error( `Unexpected property ${ prop } in ${ locationString(loc) }`);
}

@@ -308,6 +324,6 @@ // otherwise, just ignore the unexpected property

function set( prop, csn, node ) {
let val = node[prop];
const val = node[prop];
if (val === undefined)
return;
let sub = transformers[prop]( node[prop], csn, node, prop );
const sub = transformers[prop]( node[prop], csn, node, prop );
if (sub !== undefined)

@@ -318,3 +334,3 @@ csn[prop] = sub;

function target( val ) {
if (!csn_gensrc)
if (!gensrcFlavor)
// target._artifact is different to _artifact from path with explicit target

@@ -325,8 +341,9 @@ // to model entity with @cds.autoexpose, also different for COMPOSITION OF type/{}

return artifactRef( val, true );
else
return standard( val );
return standard( val );
}
function elements( dict, csn, node ) {
if (csn.from || csn_gensrc && (node.query || node.type)) // with SELECT or inferred elements with gensrc
if (csn.from || gensrcFlavor && (node.query || node.type))
// no 'elements' with SELECT or inferred elements with gensrc;
// hidden 'elements' will be set in query()
return undefined;

@@ -339,13 +356,15 @@ if (node.kind !== 'event')

// for csn_gensrc: return annotations from definition (annotated==false)
// for gensrcFlavor: return annotations from definition (annotated==false)
// or annotations (annotated==true)
function annotations( node, annotated ) {
let csn = {};
let transformer = transformers['@'];
let keys = Object.keys( node ).filter( a => a.charAt(0) === '@' ).sort();
for (let prop of keys) {
let val = node[prop];
if ((val.priority && val.priority !== 'define') == annotated) {
const csn = {};
const transformer = transformers['@'];
const keys = Object.keys( node ).filter( a => a.charAt(0) === '@' ).sort();
for (const prop of keys) {
const val = node[prop];
if ((val.priority && val.priority !== 'define') === annotated) {
// transformer (= value) takes care to exclude $inferred annotation assignments
let sub = transformer( val, csn, node, prop );
const sub = transformer( val );
// As value() just has one value, so we do not provide ( val, csn, node, prop )
// which would be more robust, but makes some JS checks unhappy
if (sub !== undefined)

@@ -360,15 +379,15 @@ csn[prop] = sub;

function messages( value, csn ) {
if (value && value.length )
setHidden( csn, 'messages', value );
function messages( val, csn ) {
if (val && val.length )
setHidden( csn, 'messages', val );
}
function location( loc, csn, xsn ) {
if (xsn.kind && xsn.kind.charAt() !== '$' && xsn.kind !== 'query' &&
if (xsn.kind && xsn.kind.charAt(0) !== '$' && xsn.kind !== 'query' &&
(!xsn.$inferred || !xsn._main)) {
// Also include $location for elements in queries (if not via '*')
let l = xsn.name && xsn.name.location || loc;
// Also include $location for elements in queries (if not via '*')
const l = xsn.name && xsn.name.location || loc;
// csn.$location
let value = { file: l.filename, line: l.start.line, col: l.start.column };
setHidden( csn, '$location', value );
const val = { file: l.filename, line: l.start.line, col: l.start.column };
setHidden( csn, '$location', val );
}

@@ -385,3 +404,3 @@ }

function insertOrderDict( dict ) {
let keys = Object.keys( dict );
const keys = Object.keys( dict );
return dictionary( dict, keys );

@@ -391,3 +410,3 @@ }

function sortedDict( dict ) {
let keys = Object.keys( dict );
const keys = Object.keys( dict );
keys.sort();

@@ -398,3 +417,3 @@ return dictionary( dict, keys );

function nonEmptyDict( dict ) {
let keys = Object.keys( dict );
const keys = Object.keys( dict );
return (keys.length)

@@ -406,5 +425,5 @@ ? dictionary( dict, keys )

function dictionary( dict, keys ) {
let csn = Object.create(null);
for (let name of keys) {
let def = definition( dict[name] );
const csn = Object.create(null);
for (const name of keys) {
const def = definition( dict[name] );
if (def !== undefined)

@@ -417,5 +436,5 @@ csn[name] = def;

function dictAsArray( dict ) {
let csn = [];
for (let n in dict) {
let d = definition( dict[n] );
const csn = [];
for (const n in dict) {
const d = definition( dict[n] );
if (d !== undefined)

@@ -430,17 +449,16 @@ csn.push( d );

return undefined; // TODO: complain with strict
// Do not include namespace definitions or inferred construct (in gensrc):
if (art.kind === 'namespace' || art.$inferred && csn_gensrc)
// Do not include namespace definitions or inferred construct (in gensrc):
if (art.kind === 'namespace' || art.$inferred && gensrcFlavor)
return undefined;
if (art.kind === 'key') { // foreignkey
let key = addExplicitAs( { ref: art.targetElement.path.map( pathItem ) },
art.name, neqPath( art.targetElement ) );
const key = addExplicitAs( { ref: art.targetElement.path.map( pathItem ) },
art.name, neqPath( art.targetElement ) );
set( 'generatedFieldName', key, art );
return extra( key, art );
}
else
return standard( art );
return standard( art );
}
function kind( k, csn, node ) {
if (!node._main && ['annotate', 'extend'].includes( k )) {
if (!node._main && [ 'annotate', 'extend' ].includes( k )) {
// We just use `name.absolute` because it is very likely a "constructed"

@@ -453,3 +471,3 @@ // extensions. The CSN parser must produce name.path like for other refs.

return 'entity';
if (['element', 'key', 'param', 'enum', 'annotate', 'query', '$tableAlias'].includes(k))
if ([ 'element', 'key', 'param', 'enum', 'annotate', 'query', '$tableAlias' ].includes(k))
return undefined;

@@ -460,12 +478,12 @@ return k;

function artifactRef( node, terse ) {
if (node.$inferred && csn_gensrc)
if (node.$inferred && gensrcFlavor)
return undefined;
// Works also on XSN directly coming from parser
let path = node.path;
const { path } = node;
if (!path)
return undefined; // TODO: complain with strict
let length = path.length;
let index = 0;
const { length } = path;
let index = 0;
for (; index < length; ++index) {
let art = path[index]._artifact;
const art = path[index]._artifact;
if (!art || art._main) // stop if at element or not found/compiled

@@ -478,12 +496,14 @@ break;

// to model entity with @cds.autoexpose
let art = path[ index-1 ]._artifact;
const art = path[index - 1]._artifact;
id = (art instanceof Array ? art[0] : art).name.absolute;
}
else if (node.scope === 'typeOf' && path[0]._artifact) { // TYPE OF without ':' in path
let name = path[0]._artifact.name;
const { name } = path[0]._artifact;
return { ref: [ name.absolute, ...name.element.split('.'), ...path.slice(1).map( pathItem ) ] };
}
else if (typeof node.scope === 'number') {
index = node.scope || length; // artifact refs in CDL have scope:0 in XSN
id = (node.scope ? path.slice(0,index) : path).map( id => id.id ).join('.');
index = node.scope || // artifact refs in CDL have scope:0 in XSN
terse === false && path.findIndex( i => i.where || i.namedArgs || i.cardinality) + 1 ||
length;
id = path.slice( 0, index ).map( item => item.id ).join('.');
}

@@ -494,7 +514,7 @@ else { // JSON or i18n input (without compiler links)

}
let main = Object.assign( {}, path[ index-1 ], { id } );
let ref = [main, ...path.slice(index)].map( pathItem );
return (!terse || ref.length != 1 || typeof ref[0] !== 'string')
? { ref }
: ref[0];
const main = Object.assign( {}, path[index - 1], { id } );
const ref = [ main, ...path.slice(index) ].map( pathItem );
return (!terse || ref.length !== 1 || typeof ref[0] !== 'string')
? { ref }
: ref[0];
}

@@ -505,4 +525,3 @@

return item.id;
else
return standard( item );
return standard( item );
}

@@ -513,4 +532,4 @@

return node.map( expression );
let dict = Object.create(null);
for (let param in node)
const dict = Object.create(null);
for (const param in node)
dict[param] = expression( node[param] );

@@ -524,15 +543,15 @@ return dict;

return true; // `@aBool` short for `@aBool: true`
if (node.$inferred && csn_gensrc)
if (node.$inferred && gensrcFlavor)
return undefined;
if (node.path)
return extra( { '=': node.path.map( id => id.id ).join('.') }, node );
if (node.literal == 'enum')
return extra( { "#" : node.symbol.id }, node );
if (node.literal == 'array')
if (node.literal === 'enum')
return extra( { '#': node.symbol.id }, node );
if (node.literal === 'array')
return node.val.map( value );
if (node.literal != 'struct')
if (node.literal !== 'struct')
// no val (undefined) as true only for annotation values (and struct elem values)
return node.name && !('val' in node) || node.val;
let r = Object.create( null );
for (let prop in node.struct)
const r = Object.create( null );
for (const prop in node.struct)
r[prop] = value( node.struct[prop] );

@@ -542,3 +561,3 @@ return r;

function enumValue( v, csn, node ) {
function enumValue( v, csn, node ) {
if (node.kind === 'enum')

@@ -549,8 +568,8 @@ Object.assign( csn, expression(v) );

function condition( node ) {
let expr = expression( node );
return expr.xpr || [expr];
const expr = expression( node );
return expr.xpr || [ expr ];
}
const magicFunctions = // TODO: calculate from compiler/builtins.js (more with HANA?):
['CURRENT_DATE','CURRENT_TIME','CURRENT_TIMESTAMP','CURRENT_USER','SESSION_USER','SYSTEM_USER'];
const magicFunctions // TODO: calculate from compiler/builtins.js (more with HANA?):
= [ 'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP', 'CURRENT_USER', 'SESSION_USER', 'SYSTEM_USER' ];
// TODO: quoted magic names like $now should be complained about in the compiler

@@ -563,7 +582,8 @@

setHidden( ref, '$env', (nav.kind === '$navElement')
? nav.name.alias
: nav.name.query + 1 );
? nav.name.alias
: nav.name.query + 1 );
}
else if ( path[0]._artifact && path[0]._artifact.query )
else if ( path[0]._artifact && path[0]._artifact.query ) {
setHidden( ref, '$env', true );
}
return ref;

@@ -573,3 +593,3 @@ }

function expression( node, withExtra ) {
let en = withExtra != null && node;
const en = withExtra != null && node;
if (typeof node === 'string')

@@ -580,5 +600,5 @@ return node;

if (node instanceof Array) {
let args = node.map( condition );
let rest = args.slice(1).map( a => [',', ...a] );
return { xpr: ['('].concat( args[0], ...rest, [')'] ) };
const exprs = node.map( condition );
const rest = exprs.slice(1).map( a => [ ',', ...a ] );
return { xpr: [ '(' ].concat( exprs[0], ...rest, [ ')' ] ) };
}

@@ -588,4 +608,3 @@ if (node.scope === 'param') {

return extra( { ref: node.path.map( pathItem ), param: true }, en );
else
return extra( { ref: [ node.param.val ], param: true }, en );
return extra( { ref: [ node.param.val ], param: true }, en );
}

@@ -596,8 +615,8 @@ if (node.path) {

return extra( pathRef( node.path ), en );
let item = pathItem( node.path[0] );
const item = pathItem( node.path[0] );
if (typeof item === 'string' && !node.path[0].quoted &&
// TODO: use _artifact if available
magicFunctions.includes( item.toUpperCase() )) {
magicFunctions.includes( item.toUpperCase() ))
return extra( { func: item }, en );
}
return extra( pathRef( node.path ), en );

@@ -609,11 +628,10 @@ }

else if (node.literal === 'enum')
return extra( { "#" : node.symbol.id }, en );
return extra( { '#': node.symbol.id }, en );
else if (node.literal === 'token')
return node.val; // * in COUNT(*)
else // TODO XSN: literal 'hex'->'x'
return extra( { val: node.val, literal: (node.literal==='hex') ? 'x' : node.literal },
en );
return extra( { val: node.val, literal: (node.literal === 'hex') ? 'x' : node.literal },
en );
}
if (node.func) { // TODO XSN: remove op: 'call', func is no path
let call = { func: node.func.path[0].id };
const call = { func: node.func.path[0].id };
if (node.args || node.namedArgs) // no args from CSN input for CURRENT_DATE etc

@@ -623,3 +641,3 @@ call.args = args( node.args || node.namedArgs );

}
if (queryOps[ node.op.val ])
if (queryOps[node.op.val])
return query( node );

@@ -629,4 +647,3 @@ else if (node.op.val === 'xpr')

return extra( { xpr: node.args.map( expression ) }, node );
else // other ops have no $extra
return { xpr: xpr( node ) };
return { xpr: xpr( node ) };
}

@@ -636,18 +653,18 @@

// if (!node.op) console.log(node)
let op = operators[ node.op.val ] || node.op.val.split(' ');
let args = node.args.map( condition );
const op = operators[node.op.val] || node.op.val.split(' ');
const exprs = node.args.map( condition );
if (op instanceof Function)
return op( args );
return op( exprs );
if (node.quantifier)
op.push( node.quantifier.val );
if (args.length < 2)
return [ ...op, ...args[0] || [] ];
return args[0].concat( ...args.slice(1).map( a => [...op, ...a] ) );
if (exprs.length < 2)
return [ ...op, ...exprs[0] || [] ];
return exprs[0].concat( ...exprs.slice(1).map( a => [ ...op, ...a ] ) );
}
function ternary( op1, op2 ) {
return function( args ) {
return (args[2])
? [ ...args[0], ...op1, ...args[1], ...op2, ...args[2] ]
: [ ...args[0], ...op1, ...args[1] ];
return function ternaryOp( exprs ) {
return (exprs[2])
? [ ...exprs[0], ...op1, ...exprs[1], ...op2, ...exprs[2] ]
: [ ...exprs[0], ...op1, ...exprs[1] ];
};

@@ -657,5 +674,5 @@ }

function postfix( op ) {
return function( args ) {
return [ ...args[0], ...op ];
}
return function postfixOp( exprs ) {
return [ ...exprs[0], ...op ];
};
}

@@ -669,24 +686,41 @@

const elems = node.elements;
if (elems && node._main && node._main.$queries && node !== node._main.$queries[0])
setHidden( select, 'elements', elements( elems, select, node ) );
if (elems && node._main && node !== node._main._leadingQuery && gensrcFlavor !== true) {
// Set hidden 'elements' for csnRefs.js. In select-item subqueries,
// gensrcFlavor might have been set to 'column' and must be set to the
// original value 'false' - otherwise no element appears.
const gensrcSaved = gensrcFlavor;
try {
gensrcFlavor = false;
setHidden( select, 'elements', insertOrderDict( elems ) );
}
finally {
gensrcFlavor = gensrcSaved;
}
}
return addLocation( node.location, select );
}
let csn = {};
const csn = {};
// for UNION, ... ----------------------------------------------------------
if (node.op.val !== 'subquery') {
if (node.op.val !== 'unionAll') // XSN TODO: quantifier: 'all'|'distinct'
if (node.op.val !== 'unionAll') { // XSN TODO: quantifier: 'all'|'distinct'
csn.op = node.op.val;
else
csn.op = 'union', csn.all = true;
}
else {
csn.op = 'union';
csn.all = true;
}
}
if (node.args) {
let args = node.args;
// binary -> n-ary - the while loop should be done in parser (toCdl is
let exprs = node.args;
// binary -> n-ary - TODO CDL: the while loop should be done in parser (toCdl is
// currently not prepared)
while (args[0] && args[0].op && args[0].op.val === node.op.val &&
!args[0].all === !node.all && args[0].args)
args = [ ...args[0].args, ...args.slice(1) ]
const block = node._leadingQuery && node._leadingQuery._block;
if (!block || block.$frontend !== 'json') {
while (exprs[0] && exprs[0].op && exprs[0].op.val === node.op.val &&
!exprs[0].all === !node.all && exprs[0].args)
exprs = [ ...exprs[0].args, ...exprs.slice(1) ];
}
if (node.op.val === 'unionAll') // TODO grammar: set DISTINCT - quantifier: 'all'|'distinct'
csn.all = true;
csn.args = args.map( query );
csn.args = exprs.map( query );
}

@@ -700,5 +734,5 @@ set( 'orderBy', csn, node );

function columns( xsnColumns, csn, xsn ) {
let csnColumns = [];
const csnColumns = [];
if (xsnColumns) {
for (let col of xsnColumns) {
for (const col of xsnColumns) {
if (col.val === '*')

@@ -711,3 +745,3 @@ csnColumns.push( '*' );

else { // null = use elements
for (let name in xsn.elements)
for (const name in xsn.elements)
addElementAsColumn( xsn.elements[name], csnColumns );

@@ -722,4 +756,3 @@ }

return from( { join: 'cross', args: node } );
else
return from( node[0] );
return from( node[0] );
}

@@ -738,6 +771,6 @@

// parser (toCdl is currently not prepared)
let args = node.args;
while (node.join === 'cross' && args[0] && args[0].join === node.join && args[0].args)
args = [ ...args[0].args, ...args.slice(1) ]
let join = { join: joinTrans[node.join] || node.join, args: node.args.map( from ) };
let srcs = node.args;
while (node.join === 'cross' && srcs[0] && srcs[0].join === node.join && srcs[0].args)
srcs = [ ...srcs[0].args, ...srcs.slice(1) ];
const join = { join: joinTrans[node.join] || node.join, args: node.args.map( from ) };
set( 'on', join, node );

@@ -750,22 +783,21 @@ return extra( join, node );

else if (!node._artifact || node._artifact._main) { // CQL or follow assoc
return extra( addExplicitAs( artifactRef( node, null ), node.name ), node );
return extra( addExplicitAs( artifactRef( node, false ), node.name ), node );
}
else // if FROM ref is from USING, we might need an AS
return extra( addExplicitAs( artifactRef( node, null ), node.name, function(id) {
let name = node._artifact.name.absolute;
let dot = name.lastIndexOf('.');
return name.substring( dot+1 ) !== id;
}), node );
return extra( addExplicitAs( artifactRef( node, false ), node.name, (id) => {
const name = node._artifact.name.absolute;
const dot = name.lastIndexOf('.');
return name.substring( dot + 1 ) !== id;
}), node );
}
function addElementAsColumn( elem, columns ) {
function addElementAsColumn( elem, cols ) {
if (elem.viaAll) // TODO: elem.$inferred (value '*')
return;
// TODO: 'priority' -> '$priority'
// only list annotations here which a provided directly with definition
let col = (csn_gensrc) ? annotations( elem, false ) : {};
// only list annotations here which are provided directly with definition
const col = (gensrcFlavor) ? annotations( elem, false ) : {};
// with `client` flavor, assignments are available at the element
let saved_gensrc = csn_gensrc;
const gensrcSaved = gensrcFlavor;
try {
csn_gensrc = true;
gensrcFlavor = gensrcFlavor || 'column';
set( 'key', col, elem );

@@ -776,3 +808,3 @@ addExplicitAs( assignAll( col, expression(elem.value) ),

col.cast = {}; // TODO: what about $extra in cast?
for (let prop of typeProperties)
for (const prop of typeProperties)
set( prop, col.cast, elem );

@@ -782,16 +814,17 @@ }

finally {
csn_gensrc = saved_gensrc;
gensrcFlavor = gensrcSaved;
}
// FIXME: Currently toHana requires that an '_ignore' property on the elem is also visible on the column
// Don't ignore virtual columns, let the renderer decide how to render that column
if (!elem.virtual && elem._ignore) {
// FIXME: Currently toHana requires that an '_ignore' property on the elem is
// also visible on the column. Don't ignore virtual columns, let the
// renderer decide how to render that column.
if (!elem.virtual && elem._ignore)
col._ignore = true;
}
if (elem.value && elem.value.location && !elem.$inferred)
location( elem.value.location, col, { kind: 'col' } );
columns.push( extra( col, elem ) );
cols.push( extra( col, elem ) );
}
function orderBy( node ) { // TODO XSN: flatten (no extra 'value'), part of expression
let expr = expression( node.value );
const expr = expression( node.value );
if (node.sort)

@@ -804,4 +837,4 @@ expr.sort = node.sort.val;

function limit( limit, csn, node ) { // XSN TODO: use same structure, $extra
let rows = expression( limit );
function limit( expr, csn, node ) { // XSN TODO: use same structure, $extra
const rows = expression( expr );
return (node.offset)

@@ -813,3 +846,3 @@ ? { rows, offset: expression( node.offset ) }

function $extra( obj, csn ) {
for (let prop of Object.keys( obj ).sort())
for (const prop of Object.keys( obj ).sort())
csn[prop] = obj[prop];

@@ -824,8 +857,10 @@ }

function setHidden( obj, prop, value ) {
Object.defineProperty( obj, prop, { value, configurable: true, writable: true } );
function setHidden( obj, prop, val ) {
Object.defineProperty( obj, prop, {
value: val, configurable: true, writable: true, enumerable: false,
} );
}
function addExplicitAs( node, name, implicit ) {
if (name && (!name.calculated && !name.$inferred || implicit && implicit(name.id) ))
if (name && (!name.calculated && !name.$inferred || !node.ref || implicit && implicit(name.id) ))
node.as = name.id;

@@ -836,6 +871,6 @@ return node;

function neqPath( ref ) {
let path = ref && ref.path;
return path && function( id ) {
let last = path[ path.length-1 ];
return (last && last.id) !== id;
const path = ref && ref.path;
return function test( id ) {
const last = path && path[path.length - 1];
return !last || last.id !== id;
};

@@ -847,4 +882,4 @@ }

return 0;
let oa = propertyOrder[a] || propertyOrder[a.charAt()] || 9999;
let ob = propertyOrder[b] || propertyOrder[b.charAt()] || 9999;
const oa = propertyOrder[a] || propertyOrder[a.charAt(0)] || 9999;
const ob = propertyOrder[b] || propertyOrder[b.charAt(0)] || 9999;
return oa - ob || (a < b ? -1 : 1);

@@ -854,4 +889,4 @@ }

function compactQuery( q ) { // TODO: options
csn_gensrc = true;
mode_strict = false;
gensrcFlavor = true;
strictMode = false;
return q && query( q );

@@ -861,4 +896,4 @@ }

function compactExpr( e ) { // TODO: options
csn_gensrc = true;
mode_strict = false;
gensrcFlavor = true;
strictMode = false;
return e && expression( e );

@@ -869,45 +904,45 @@ }

// TODO: document CSN and XSN for technical configurations
function technicalConfig( tc/*, parentCsn, parentArtifact, prop */) {
let csn = { [tc.backend.val]: { } };
let be = csn[tc.backend.val];
if(tc.backend.calculated)
function technicalConfig( tc/* , parentCsn, parentArtifact, prop */) {
const csn = { [tc.backend.val]: { } };
const be = csn[tc.backend.val];
if (tc.backend.calculated)
be.calculated = true;
if(tc.migration) {
if(!be.tableSuffix)
if (tc.migration) {
if (!be.tableSuffix)
be.tableSuffix = [];
be.tableSuffix.push({ xpr: ['migration', value(tc.migration)] });
be.tableSuffix.push({ xpr: [ 'migration', value(tc.migration) ] });
}
if(tc.storeType) {
if (tc.storeType)
be.storeType = value(tc.storeType);
}
if(tc.extendedStorage) {
if(!be.tableSuffix)
if (tc.extendedStorage) {
if (!be.tableSuffix)
be.tableSuffix = [];
be.tableSuffix.push({ xpr: ['using', 'extended', 'storage'] });
be.tableSuffix.push({ xpr: [ 'using', 'extended', 'storage' ] });
}
if(tc.group) {
if(!be.tableSuffix)
if (tc.group) {
if (!be.tableSuffix)
be.tableSuffix = [];
let group = { xpr: [] };
if(tc.group.name) {
group.xpr.push('group', 'name', { ref: [tc.group.name.id] });
}
if(tc.group.type) {
group.xpr.push('group', 'type', { ref: [tc.group.type.id] });
}
if(tc.group.subType) {
group.xpr.push('group', 'subtype', { ref: [tc.group.subType.id] });
}
const group = { xpr: [] };
if (tc.group.name)
group.xpr.push('group', 'name', { ref: [ tc.group.name.id ] });
if (tc.group.type)
group.xpr.push('group', 'type', { ref: [ tc.group.type.id ] });
if (tc.group.subType)
group.xpr.push('group', 'subtype', { ref: [ tc.group.subType.id ] });
be.tableSuffix.push(group);
}
if(tc.unloadPrio) {
if(!be.tableSuffix)
if (tc.unloadPrio) {
if (!be.tableSuffix)
be.tableSuffix = [];
be.tableSuffix.push({ xpr: ['unload', 'priority', expression(tc.unloadPrio)] });
be.tableSuffix.push({ xpr: [ 'unload', 'priority', expression(tc.unloadPrio) ] });
}
if(tc.autoMerge) {
if(!be.tableSuffix)
if (tc.autoMerge) {
if (!be.tableSuffix)
be.tableSuffix = [];
let autoMerge = { xpr: [] }
if(!tc.autoMerge.val)
const autoMerge = { xpr: [] };
if (!tc.autoMerge.val)
autoMerge.xpr.push('no');

@@ -917,28 +952,26 @@ autoMerge.xpr.push('auto', 'merge');

}
if(tc.partition) {
if(!be.tableSuffix)
if (tc.partition) {
if (!be.tableSuffix)
be.tableSuffix = [];
be.tableSuffix.push({ xpr: [partition(tc.partition)] });
be.tableSuffix.push({ xpr: [ partition(tc.partition) ] });
}
if(tc.fzindexes) {
if(!be.fzindexes)
if (tc.fzindexes) {
if (!be.fzindexes)
be.fzindexes = {};
tc.fzindexes.forEach(i => {
i.columns.filter(c => !c._ignore).forEach(c => {
let stream = [];
tc.fzindexes.forEach((i) => {
i.columns.filter(c => !c._ignore).forEach((c) => {
const stream = [];
stream.push('fuzzy', 'search', 'index', 'on');
if(i.fuzzy) {
if (i.fuzzy) {
stream.push('fuzzy', 'search', 'mode');
if(i.fuzzy.mode) {
if (i.fuzzy.mode)
stream.push(expression(i.fuzzy.mode));
}
}
let name = c.path.map(p=>p.id).join('.');
if(be.fzindexes[name]) {
const name = c.path.map(p => p.id).join('.');
if (be.fzindexes[name])
be.fzindexes[name].push(stream);
}
else {
else
be.fzindexes[name] = [ stream ];
}
});

@@ -948,6 +981,6 @@ });

if(tc.indexes) {
if (tc.indexes) {
be.indexes = {};
for(let idxName in tc.indexes) {
let idx = tc.indexes[idxName];
for (const idxName in tc.indexes) {
const idx = tc.indexes[idxName];
be.indexes[idxName] = (Array.isArray(idx)) ? idx.map(index) : index(idx);

@@ -959,26 +992,27 @@ }

function index(idx) {
let stream = [];
if(idx.kind === 'index') {
if(idx.unique) {
const stream = [];
if (idx.kind === 'index') {
if (idx.unique)
stream.push('unique');
}
stream.push('index', { ref: [idx.name.id] }, 'on', '(');
stream.push('index', { ref: [ idx.name.id ] }, 'on', '(');
columns(idx.columns, stream);
stream.push(')');
if(idx.sort)
if (idx.sort)
stream.push(value(idx.sort));
} else if(idx.kind === 'fulltextindex') {
stream.push('fulltext', 'index', { ref: [idx.name.id] }, 'on', '(');
}
else if (idx.kind === 'fulltextindex') {
stream.push('fulltext', 'index', { ref: [ idx.name.id ] }, 'on', '(');
columns(idx.columns, stream);
stream.push(')');
if(idx.language) {
if(idx.language.column) {
if (idx.language) {
if (idx.language.column) {
stream.push('language', 'column');
stream.push(expression(idx.language.column));
}
if(idx.language.detection) {
if (idx.language.detection) {
stream.push('language', 'detection', '(');
let i = 0;
idx.language.detection.forEach(v => {
if(i > 0)
idx.language.detection.forEach((v) => {
if (i > 0)
stream.push(',');

@@ -991,58 +1025,55 @@ stream.push(expression(v));

}
if(idx.mimeTypeColumn) {
if (idx.mimeTypeColumn)
stream.push('mime', 'type', 'column', expression(idx.mimeTypeColumn));
}
if(idx.fuzzySearchIndex) {
if (idx.fuzzySearchIndex)
stream.push('fuzzy', 'search', 'index', value(idx.fuzzySearchIndex));
}
if(idx.phraseIndexRatio) {
if (idx.phraseIndexRatio)
stream.push('phrase', 'index', 'ratio', expression(idx.phraseIndexRatio));
}
if(idx.configuration) {
if (idx.configuration)
stream.push('configuration', expression(idx.configuration));
}
if(idx.textAnalysis) {
if (idx.textAnalysis)
stream.push('text', 'analysis', value(idx.textAnalysis));
}
if(idx.searchOnly) {
if (idx.searchOnly)
stream.push('search', 'only', value(idx.searchOnly));
}
if(idx.fastPreprocess) {
if (idx.fastPreprocess)
stream.push('fast', 'preprocess', value(idx.fastPreprocess));
}
if(idx.mimeType) {
if (idx.mimeType)
stream.push('mime', 'type', expression(idx.mimeType));
}
if(idx.tokenSeparators) {
if (idx.tokenSeparators)
stream.push('token', 'separators', expression(idx.tokenSeparators));
}
if(idx.textMining) {
if(idx.textMining.state) {
if (idx.textMining) {
if (idx.textMining.state)
stream.push('text', 'mining', value(idx.textMining.state));
}
if(idx.textMining.config) {
if (idx.textMining.config)
stream.push('text', 'mining', 'configuration', expression(idx.textMining.config));
}
if(idx.textMining.overlay) {
if (idx.textMining.overlay)
stream.push('text', 'mining', 'configuration', 'overlay', expression(idx.textMining.overlay));
}
}
if(idx.changeTracking) {
let ct = idx.changeTracking;
if (idx.changeTracking) {
const ct = idx.changeTracking;
stream.push(value(ct.mode));
if(ct.asyncSpec) {
let asp = ct.asyncSpec;
if (ct.asyncSpec) {
const asp = ct.asyncSpec;
stream.push('flush');
if(asp.queue) {
if (asp.queue)
stream.push(value(asp.queue));
}
if(asp.minutes) {
if (asp.minutes) {
stream.push('every', expression(asp.minutes), 'minutes');
if(asp.documents) {
if (asp.documents)
stream.push('or');
}
}
if(asp.documents) {
if (asp.documents)
stream.push('after', expression(asp.documents), 'documents');
}
}

@@ -1055,6 +1086,6 @@ }

function partition(p) {
let stream = [];
const stream = [];
let i = 0;
p.specs.forEach(s => {
if(i == 0)
p.specs.forEach((s) => {
if (i === 0)
stream.push('partition', 'by', ...s.scheme.val.split(' '));

@@ -1066,9 +1097,9 @@ else

});
if(p.wpoac) {
if (p.wpoac)
stream.push('with', 'partitioning', 'on', 'any', 'columns', value(p.wpoac));
}
return stream;
function spec(s) {
if(s.columns) {
if (s.columns) {
stream.push('(');

@@ -1078,15 +1109,15 @@ columns(s.columns, stream);

}
if(s.partitions) {
if (s.partitions)
stream.push('partitions', value(s.partitions));
}
if(s.ranges) {
if (s.ranges) {
stream.push('(');
let oppStore = (s.ranges[0].store === 'default' ? 'extended' : 'default');
let delimiter = false;
s.ranges.forEach(r => {
if(r.store != oppStore) {
if(s.withStorageSpec) {
if(delimiter) {
s.ranges.forEach((r) => {
if (r.store !== oppStore) {
if (s.withStorageSpec) {
if (delimiter)
stream.push(')');
}
stream.push('using', r.store, 'storage', '(');

@@ -1097,28 +1128,28 @@ }

}
if(delimiter) {
if (delimiter)
stream.push(',');
}
stream.push('partition');
if(r.others) {
if (r.others)
stream.push('others');
}
if(r.min && !r.max) {
if (r.min && !r.max)
stream.push('value', '=');
}
if(r.min) {
if (r.min)
stream.push(expression(r.min));
}
if(r.isCurrent) {
if (r.isCurrent)
stream.push('is', 'current');
}
if(r.min && r.max) {
if (r.min && r.max)
stream.push('<=', 'values', '<', expression(r.max));
}
delimiter = true;
});
if(s.withStorageSpec) {
if (s.withStorageSpec)
stream.push(')');
}
stream.push(')');

@@ -1129,12 +1160,13 @@ }

// eslint-disable-next-line no-shadow
function columns(arr, stream) {
let i = 0;
arr.filter(c=>!c._ignore).forEach(c => {
if(i > 0)
arr.filter(c => !c._ignore).forEach((c) => {
if (i > 0)
stream.push(',');
if(c.unit)
stream.push({func: value(c.unit), args: [expression(c)]});
if (c.unit)
stream.push({ func: value(c.unit), args: [ expression(c) ] });
else
stream.push(expression(c));
if(c.sort)
if (c.sort)
stream.push(value(c.sort));

@@ -1146,2 +1178,4 @@ i++;

module.exports = { compactModel, compactQuery, compactExpr, sortCsn };
module.exports = {
compactModel, compactQuery, compactExpr, sortCsn,
};

@@ -99,3 +99,3 @@ /**

* @param {walkWithPathCallback} callback function called for each node: callback(isNode,path,node)
* @param check - optional callback function which returns true if the walk should continue
* @param {(newPath: any, obj: Object) => boolean} [check] - optional callback function which returns true if the walk should continue
*/

@@ -377,3 +377,3 @@ function walkWithPath(node, callback, check) {

* @param {getNextElements} getNextElements callback to obtain the next node to walk, passing the current node as parameter
* @param {build} callback called on each walked node passing that node and returns the resulting product of the operation on this node
* @param {(any) => any} build called on each walked node passing that node and returns the resulting product of the operation on this node
*/

@@ -380,0 +380,0 @@ function walkAndBuild(root, getNextElements, build) {

@@ -81,3 +81,3 @@ // Wrapper around generated ANTLR parser

const rules = {
cdl: { func: 'start', returns: 'source', frontend: 'cdl' },
cdl: { func: 'start', returns: 'source', $frontend: 'cdl' },
query: { func: 'queryEOF', returns: 'query' },

@@ -90,2 +90,3 @@ expr: { func: 'conditionEOF', returns: 'cond' } // yes, condition

var tokenStream = new RewriteTypeTokenStream(lexer);
/** @type {object} */
var parser = new Parser( tokenStream );

@@ -134,4 +135,4 @@ var errorListener = new ErrorListener();

ast.options = options;
if (rulespec.frontend)
ast.$frontend = rulespec.frontend;
if (rulespec.$frontend)
ast.$frontend = rulespec.$frontend;

@@ -138,0 +139,0 @@ if (parser.messages) {

@@ -68,3 +68,2 @@ // Error strategy with special handling for (non-reserved) keywords

antlr4_error.DefaultErrorStrategy.call( this, ...args );
return this;
}

@@ -71,0 +70,0 @@ const super1 = antlr4_error.DefaultErrorStrategy.prototype;

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

combinedLocation,
classifyImplicitName,
identAst,

@@ -51,3 +52,2 @@ numberLiteral,

hanaFlavorOnly,
betaModeOnly,
csnParseOnly,

@@ -119,12 +119,2 @@ noAssignmentInSameLine,

function betaModeOnly( text, ...tokens ) {
if (!text || this.options.betaMode)
return;
if (typeof text !== 'string') {
tokens = [ text, ...tokens ];
text = tokens.map( t => t.text.toUpperCase() ).join(' ') + ' is only supported with --beta-mode';
}
this.message( null, this.tokenLocation( tokens[0], tokens[ tokens.length-1 ] ), text );
}
// Use the following function for language constructs which we (currently) do

@@ -259,9 +249,27 @@ // not really compile, just use to produce a CSN for functions parseToCqn() and

// Return AST for identifier token `token`. Also do validilty check on
// identifer (TODO: check for dots etc/ here).
// Classify token (identifier category) for implicit names,
// to be used in the empty alternative to AS <explitName>.
function classifyImplicitName( category, ref ) {
if (!ref || ref.path) {
const implicit = this._input.LT(-1);
if (implicit.isIdentifier)
implicit.isIdentifier = category;
}
}
// Return AST for identifier token `token`. Also check that identifer is not empty.
function identAst( token, category ) {
token.isIdentifier = category;
var id = token.text;
let id = token.text;
if (token.type !== this.constructor.Identifier && !/^[a-zA-Z]+$/.test( id ))
id = '';
if (token.text[0] === '!') {
id = id.slice( 2, -1 );
if (!id) {
this.message( 'syntax-empty-ident', token, {},
'Error', 'Delimited identifier must contain at least one character' );
}
// XSN TODO: quoted -> $delimited (only use to complain about ![$self] usage!)
return { id, quoted: true, location: this.tokenLocation( token ) };
}
if (token.text[0] !== '"')

@@ -273,4 +281,8 @@ return { id, location: this.tokenLocation( token ) };

this.message( 'syntax-empty-ident', token, {},
'Error', 'Quoted identifier must contain at least one character' );
'Error', 'Delimited identifier must contain at least one character' );
}
else {
this.message( 'syntax-deprecated-ident', token, { delimited: id }, 'Warning',
'Deprecated delimited identifier syntax, use $(DELIMITED) - strings are delimited by single quotes' );
}
return { id, quoted: true, location: this.tokenLocation( token ) };

@@ -424,7 +436,13 @@ }

// Assign all non-empty (undefined, null, {}, []) properties in argument
// `props` and argument `annos` as property `annotationAssignments` to `target`
// and return it. Hack: if argument `annos` is exactly `true`, return
// `Object.assign( target, props )`. ANTLR tokens are replaced by their
// locations.
/** Assign all non-empty (undefined, null, {}, []) properties in argument
* `props` and argument `annos` as property `annotationAssignments` to `target`
* and return it. Hack: if argument `annos` is exactly `true`, return
* `Object.assign( target, props )`. ANTLR tokens are replaced by their
* locations.
*
* @param {any} target
* @param {any[]|true} [annos=[]]
* @param {any} [props]
* @param {any} [location]
*/
function assignProps( target, annos = [], props, location ) {

@@ -490,3 +508,3 @@ if (annos === true)

function handleComposition( cardinality, isComposition) {
function handleComposition( cardinality, isComposition ) {
if (isComposition && !cardinality) {

@@ -493,0 +511,0 @@ const lt1 = this._input.LT(1).type;

@@ -17,3 +17,5 @@ // Main entry point for the Research Vanilla CDS Compiler

const backends = require('./backends');
const { odata, cdl, sql, hdi, hdbcds, edm, edmx } = require('./api/main');
// The compiler version (taken from package.json)

@@ -24,24 +26,3 @@ function version() {

// The CSN file format version produced by this compiler
// (Note that all 0.x.x versions are floating targets, i.e. the format is frequently
// changed without notice. The versions only signal certain combinations of "flavors", see below)
// (Note: the SQL name mapping mode is not reflected in the content of the csn, the version only
// signals which default name mapping a backend has to use)
// Historic versions:
// 0.0.1 : Used by HANA CDS for its CSN output (incomplete, not well defined, quite different from CDX ...)
// 0.0.2 : CDX in the initial versions with old-style CSN, default for SQL name mapping is 'quoted'
// 0.0.99 : Like 0.0.2, but with new-style CSN
// Versions that are currently produced by compiler:
// 0.1.0 : Like 0.0.2, default for SQL name mapping is 'plain'
// 0.1.99 : Like 0.1.0, but with new-style CSN
// 0.2 : same as 0.1.99, but with new top-level properties: $version, meta
// TODO: move csn versioning in to-csn.js
function csnVersion( options ) {
// Merge default options
options = mergeOptions(backends.getDefaultBackendOptions(), options);
// Old-style CSN vs new-style CSN
return options.newCsn === false ? '0.1.0' : '1.0';
}
var { CompilationError, messageString, handleMessages, hasErrors, getMessageFunction }
var { CompilationError, messageString, messageStringMultiline, messageContext, handleMessages, hasErrors, getMessageFunction }
= require('./base/messages');

@@ -61,4 +42,3 @@ var { promiseAllDoNotRejectImmediately } = require('./base/node-helpers');

var fs = require('fs');
const emdx2csn = require('./edm/annotations/edmx2csnNew'); // translate edmx annotations into csn
const { mergeOptions } = require('../lib/model/modelUtils');
const emdx2csn = require('./edm/annotations/edmx2csn'); // translate edmx annotations into csn

@@ -80,6 +60,3 @@ const path = require('path');

else if (['.json', '.csn'].includes(ext)) {
let parseCSNfromJson = require("./json/from-json");
let model = parseCSNfromJson( source, filename, options );
model.$frontend = 'json';
return model;
return require('./json/from-csn').parse( source, filename, options );
} else if (options.fallbackParser || ['.cds', '.hdbcds', '.hdbdd'].includes(ext))

@@ -101,4 +78,10 @@ return parseLanguage( source, filename, options );

function collectSources( filenames, dir ) {
return compile( filenames, dir, { collectSources: true } );
// Collect all sources for the given files in the given base directory.
// This means that all dependency sources (e.g. `using` statements) are
// loaded and stored.
// This function returns a Promise (i.e. calls compile() which returns
// a promise). The fulfullment value is an object with all sources and
// their dependencies.
function collectSources( filenames, basedir ) {
return compile( filenames, basedir, { collectSources: true } );
}

@@ -140,9 +123,9 @@

dir = path.resolve(dir);
var a = processFilenames( filenames, dir );
const a = processFilenames( filenames, dir );
a.fileContentDict = Object.create(null);
let model = { sources: a.sources, options };
const model = { sources: a.sources, options };
const message = getMessageFunction( model );
const parseOptions = optionsWithMessages( options, model );
var all = promiseAllDoNotRejectImmediately( a.files.map(readAndParse) );
let all = promiseAllDoNotRejectImmediately( a.files.map(readAndParse) );

@@ -152,3 +135,3 @@ all = all

// do not reject with PromiseAllError, use InvocationError:
let errs = reason.valuesOrErrors.filter (e => e instanceof Error);
const errs = reason.valuesOrErrors.filter (e => e instanceof Error);
// internal error if no file IO error (has property `path`)

@@ -164,3 +147,3 @@ return Promise.reject( errs.find( e => !e.path ) ||

return collect();
return compileDo( model, a.fileContentDict );
return compileDo( model );
});

@@ -198,9 +181,13 @@

else {
a.fileContentDict[filename] = source;
let ast = parse( source, rel, parseOptions );
a.sources[filename] = ast;
ast.filename = rel;
ast.dirname = path.dirname( filename );
assertConsistency( ast, parseOptions );
fulfill( ast );
try {
a.fileContentDict[filename] = source;
let ast = parse( source, rel, parseOptions );
a.sources[filename] = ast;
ast.filename = rel;
ast.dirname = path.dirname( filename );
assertConsistency( ast, parseOptions );
fulfill( ast );
} catch(e){
reject(e);
}
}

@@ -330,3 +317,11 @@ });

return new Promise( function (fulfill, reject) {
moduleResolve( dep.module, opts, function (err, res) {
// If the global 'cds.home' is set, read modules starting with '@sap/cds/' from there.
// TODO: re-think:
// * what is wrong for a JAVA installation to set a link...
// * preferred to a local installation? Not the node-way...
// * a global? The Umbrella could pass it as an option...
let path = (global['cds'] && global['cds'].home && dep.module.startsWith( '@sap/cds/' ))
? global['cds'].home + dep.module.slice(8) // path.resolve() does not work - huh?
: dep.module;
moduleResolve( path, opts, function (err, res) {
// console.log('RESOLVE', dep, res, err)

@@ -419,6 +414,6 @@ if (err)

// Argument `sourcesDict` is a dictionary (it could actually be a ordinary object)
// mapping filenames to either source texts (string) or an AST-like augmented
// CSNs. It could also be a simple string, which is then considered to be the
// source text of a file named `<stdin>.cds`.
// Argument `sourcesDict` could also be the output of collectSources(), see above.
// mapping filenames to either source texts (string) or XSN objects (AST-like
// augmented CSNs). It could also be a simple string, which is then considered
// to be the source text of a file named `<stdin>.cds`. Argument `sourcesDict`
// could also be the output of collectSources(), see above.
//

@@ -447,4 +442,5 @@ // See function `compile` for the meaning of the argument `options`. If there

}
// else
// sources[filename] = source;
else { // source is a XSN object (CSN/CDL parser output)
sources[filename] = source;
}
}

@@ -480,3 +476,2 @@

if (!options.testMode) {
model.version = versionObject( options );//TODO remove
model.meta = {}; // provide initial central meta object

@@ -508,7 +503,19 @@ }

function processFilenames( filenames, dir ) {
var sources = Object.create(null); // not {} = no [[Prototype]]
var files = [];
var repeated = [];
for (let origname of filenames) {
let name = path.resolve( dir, origname );
const sources = Object.create(null); // not {} = no [[Prototype]]
const files = [];
const repeated = [];
for (const originalName of filenames) {
let name = path.resolve(dir, originalName);
try {
// Resolve possible symbolic link; if the file does not exist
// we just continue using the original name because readFile()
// already handles non-existent files.
name = fs.realpathSync(name);
}
catch (e) {
// Ignore the not-found (ENOENT) error
}
if (sources[name] == null) {

@@ -519,5 +526,4 @@ sources[name] = path.relative( dir, name );

else if (typeof sources[name] === 'string') { // not specified more than twice
let msg = 'Repeated argument: file \'' + sources[name] + '\'';
const msg = 'Repeated argument: file \'' + sources[name] + '\'';
repeated.push( new ArgumentError( name, msg ) );
//sources[name] = true;
}

@@ -528,9 +534,2 @@ }

// Return the 'version' object that should appear in CSNs generated by this compiler.
function versionObject( options ) { // TODO remove
return {
csn: csnVersion( options ),
}
}
// Class for command invocation errors. Additional members:

@@ -542,2 +541,3 @@ // `errors`: vector of errors (file IO or ArgumentError)

this.errors = errs;
this.hasBeenReported = false;
}

@@ -581,2 +581,4 @@ }

messageString,
messageStringMultiline,
messageContext,
InvocationError,

@@ -601,2 +603,4 @@ hasErrors,

parseToExpr,
for: { odata },
to: { cdl, sql, hdi, hdbcds, edm, edmx }
};
// CSN functionality for resolving references
// The functions in this module expect a CSN with valid references. If that is
// not the case, it simply throws an error without any claim for the error
// message to be user-friendly. CSN processors can provide user-friendly error
// messages by calling the Core Compiler in that case. For details, see
// The functions in this module expect a well-formed CSN with valid references.
// If that is not the case, it simply throws an error (which might even be a
// plain TypeError) without any claim for the error message to be
// user-friendly. CSN processors can provide user-friendly error messages by
// calling the Core Compiler in that case. For details, see
// internalDoc/CoreCompiler.md#use-of-the-core-compiler-for-csn-processors.

@@ -31,6 +32,8 @@

const artifactProperties
= ['elements', 'columns', 'keys', 'enum', 'params', 'actions', 'payload', 'definitions', 'extensions'];
= ['elements', 'columns', 'keys', 'mixin', 'enum', 'params', 'actions', 'payload', 'definitions', 'extensions'];
function csnRefs( csn ) {
const views = Object.create(null); // cache for views - OK to add it to CSN?
let refCsnPath = [];
let refLinks = null;
return { effectiveType, artifactRef, inspectRef, queryOrMain };

@@ -97,7 +100,19 @@

function inspectRef( csnPath, whereEntity ) {
const { obj, parent, query, scope } = analyseCsnPath( csnPath, csn );
function whereEntity( csnPath, refCsnPathIndex ) {
if (refCsnPath.length !== refCsnPathIndex ||
refCsnPath.some( ( prop, idx ) => prop !== csnPath[idx] )) {
// safety: ref-where in ref-where -> first store in locals
const path = csnPath.slice( 0, refCsnPathIndex );
const links = inspectRef( path ).links;
refCsnPath = path;
refLinks = links;
}
return refLinks[ csnPath[ refCsnPathIndex + 1 ] ].env;
}
function inspectRef( csnPath ) {
const { obj, parent, query, scope, refCsnPathIndex } = analyseCsnPath( csnPath, csn );
const name = csnPath[1];
const main = csn.definitions[ name ];
const queries = views[ name ] || main.query && allQueries( name, main.query );
const queries = views[ name ] || main.query && allQueries( name, main );

@@ -109,3 +124,3 @@ const path = (typeof obj === 'string') ? [ obj ] : obj.ref;

// 1,2:
// 1,2: with 'param' or 'global' property, in `keys`
if (obj.param)

@@ -115,6 +130,8 @@ return expandRefPath( path, main.params[ head ], 'param' );

return expandRefPath( path, csn.definitions[ head ], scope );
else if (scope === 'keys')
return expandRefPath( path, csn.definitions[ parent.target ].elements[ head ], 'keys' );
// 3:
if (head.charAt() === '$') {
else if (scope === 'keys') {
const target = csn.definitions[ parent.target || parent.cast.target ];
return expandRefPath( path, target.elements[ head ], 'keys' );
}
// 3: $magic
if (head.charAt(0) === '$') {
if (head === '$self' || head === '$projection') {

@@ -128,12 +145,9 @@ let self = query ? queryOrMain( query, main ) : main;

}
// 4 (where inside ref - art for environment must be provided as computing
// 4: where inside ref - art for environment must be provided as computing
// it here would be too expensive)
if (scope === 'ref-where'){
if(!whereEntity){
// Caller SHOULD catch this and then try again with a correct whereEntity
throw new Error("Scope '" + scope + "' but no entity was provided.");
}
return expandRefPath( path, whereEntity.elements[ head ], scope );
if (scope === 'ref-where') {
const { elements } = whereEntity( csnPath, refCsnPathIndex );
return expandRefPath( path, elements[ head ], scope );
}
// 5,6,7:
// 5,6,7: outside queries, in queries where inferred elements are referred to
if (!query)

@@ -153,7 +167,7 @@ return expandRefPath( path, (parent || main).elements[ head ] );

else if (typeof obj.$env === 'string') {
const source = queryOrMain( select._sources[ obj.$env ], main );
const source = select._sources[ obj.$env ];
return expandRefPath( path, source.elements[ head ], 'source' );
}
// 8:
// 8: try to search in MIXIN section (not in ON of JOINs)
if (scope !== 'from-on' && select.mixin) {

@@ -164,3 +178,3 @@ const art = select.mixin[ head ];

}
// 9:
// 9: try to search for table aliases (partially in ON of JOINs)
if (path.length > 1 && (select.$alias || scope !== 'from-on')) {

@@ -171,9 +185,9 @@ const art = select._sources[ head ];

}
// 10:
// 10: search in elements of source entity
// TODO: do not do this if current query has a parent query !!!
if (scope === 'orderBy') {
if (scope === 'on' || scope === 'orderBy') {
return expandRefPath( path, queryOrMain( query, main ).elements[ head ] );
}
if (typeof select.$alias === 'string') { // with unique source
const source = queryOrMain( select._sources[ select.$alias ], main );
const source = select._sources[ select.$alias ];
return expandRefPath( path, source.elements[ head ], 'source' );

@@ -200,5 +214,5 @@ }

function allQueries( name, topQuery ) {
function allQueries( name, main ) {
const all = [];
traverseQuery( topQuery, null, function memorize( query, select ) {
traverseQuery( main.query, null, function memorize( query, select ) {
if (query.ref) { // ref in from

@@ -211,3 +225,3 @@ const as = query.as || implicitAs( query.ref );

const as = query.as;
select._sources[ as ] = query;
select._sources[ as ] = queryOrMain( query, main );
setLink( select, '$alias',

@@ -264,4 +278,7 @@ (select.$alias != null) ? typeof select.$alias === 'string' : as );

callback( from, select );
else if (from.args) // join
else if (from.args) { // join
from.args.forEach( arg => traverseFrom( arg, select, callback ) );
if (from.on) // join
from.on.forEach( arg => traverseQuery( arg, select, callback ) );
}
else

@@ -292,6 +309,6 @@ traverseQuery( from, select, callback ); // sub query in FROM

let isName = false;
let refCsnPathIndex = 0;
for (const prop of csnPath) {
// if (isName || Array.isArray( obj )) { // array item, name/index of artifact/member, (named) argument
if (isName || typeof prop !== 'string') { // array item, name/index of artifact/member, (named) argument
csnPath.forEach( function loop( prop, index ) {
if (isName || Array.isArray( obj )) { // array item, name/index of artifact/member, (named) argument
if (typeof isName === 'string') {

@@ -318,5 +335,11 @@ parent = art;

scope = 'ref-where';
refCsnPathIndex = index - 2;
}
else if (prop === 'on' && scope === 'from') {
scope = 'from-on';
else if (prop === 'on') {
if (scope === 'from')
scope = 'from-on';
else if (scope === 'mixin')
scope = 'mixin-on';
else
scope = 'on';
}

@@ -330,5 +353,5 @@ else if (prop !== 'xpr') {

obj = obj[ prop ];
}
} );
// console.log( 'CPATH:', csnPath, scope, obj );
return { obj, parent, query, scope };
return { obj, parent, query, scope, refCsnPathIndex };
}

@@ -335,0 +358,0 @@

@@ -225,5 +225,5 @@ 'use strict'

/**
* Add an annotation with absolute name 'absoluteName' (including '@') and string value 'theValue' to 'node'
*
* @param {any} absoluteName Name of the annotation, including '@'
* Add an annotation with absolute name 'absoluteName' (including the at-sign) and string value 'theValue' to 'node'
*
* @param {any} absoluteName Name of the annotation, including the at-sign
* @param {any} theValue string value of the annotation

@@ -237,4 +237,7 @@ * @param {any} node Node to add the annotation to

}
// Assemble the annotation
node[absoluteName] = theValue;
// Only overwrite if undefined or null
if(node[absoluteName] === undefined || node[absoluteName] === null) {
// Assemble the annotation
node[absoluteName] = theValue;
}
}

@@ -267,11 +270,11 @@

* instead (this seems to be intended for handling annotations that start with '@' ?)
*
*
* Regardless of their names, transformers are never applied to dictionary elements.
*
*
* The transformer functions are called with the following signature:
* transformer(value, node, resultNode, key)
*
*
* @param {any} node Node to transform
* @param {any} transformers Object defining transformer functions
* @returns
* @returns {object}
*/

@@ -312,3 +315,3 @@ function cloneWithTransformations(node, transformers) {

}
// Resolve to the final type of a type, that means follow type chains, references to other types or

@@ -387,2 +390,4 @@ // elements a.s.o

return (getFinalBaseType(type.type, path, cycleCheck));
else if (type.items)
return getFinalBaseType(type.items, path, cycleCheck);
else

@@ -424,3 +429,3 @@ // TODO: this happens if we don't have a type, e.g. an expression in a select list

// Clone each property
let resultValue = clone(node[key], node, resultNode, key);
let resultValue = clone(node[key]);
if (resultValue !== undefined) {

@@ -430,2 +435,6 @@ resultNode[key] = resultValue;

}
// type exposure in services
if(node.$odataExposedType) {
setProp(resultNode, "$odataExposedType", node.$odataExposedType);
}
if(node.$location){

@@ -437,2 +446,4 @@ setProp(resultNode, "$location", node.$location);

}
if (node.technicalConfig && !resultNode.technicalConfig)
setProp(resultNode, 'technicalConfig', node.technicalConfig);

@@ -442,3 +453,7 @@ if(node.$env && !resultNode.$env){

}
if(node.$path && !resultNode.$path){
setProp(resultNode, '$path', node.$path);
}
return resultNode;

@@ -460,6 +475,11 @@ }

// for details.
function forEachMember( construct, callback, path=[]) {
function forEachMember( construct, callback, path=[], ignoreIgnore=true) {
let obj = construct.returns || construct; // why the extra `returns` for actions?
const obj_path = Array.from(path); // Clone the path
// Allow processing _ignored elements if requested
if(ignoreIgnore && ( construct._ignore || obj._ignore )){
return;
}
if(construct.returns){

@@ -484,8 +504,8 @@ obj_path.push("returns");

// recursively (i.e. also for sub-elements of elements).
function forEachMemberRecursively( construct, callback, path=[] ) {
function forEachMemberRecursively( construct, callback, path=[], ignoreIgnore=true) {
forEachMember( construct, ( member, memberName, prop, subpath ) => {
callback( member, memberName, prop, subpath );
// Descend into nested members, too
forEachMemberRecursively( member, callback, subpath);
}, path);
forEachMemberRecursively( member, callback, subpath, ignoreIgnore);
}, path, ignoreIgnore);
}

@@ -516,5 +536,5 @@

if(node._ignore){
return;
return;
}
if(Array.isArray(node)){

@@ -586,3 +606,3 @@ for (let i = 0; i < node.length; i++) {

}
function traverseFrom( from, callback, path = [] ) {

@@ -595,3 +615,3 @@ if (from.ref) // ignore

}
}
}
else

@@ -605,3 +625,3 @@ traverseQuery( from, callback, path ); // sub query in FROM

* @param artifact - the artifact object
* @param annotationName - the name of the annotation (including the @)
* @param annotationName - the name of the annotation (including the at-sign)
* @param value - the value

@@ -608,0 +628,0 @@ */

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

function enrichCsn( csn ) {
function enrichCsn( csn, options = {} ) {
const transformers = {

@@ -56,3 +56,3 @@ $env: reveal,

function standard( parent, prop, node, whereEntity ) {
function standard( parent, prop, node ) {
if (!node || typeof node !== 'object' || !{}.propertyIsEnumerable.call( parent, prop ))

@@ -63,3 +63,3 @@ return;

if (node instanceof Array) {
node.forEach( (n, i) => standard( node, i, n, whereEntity ) );
node.forEach( (n, i) => standard( node, i, n ) );
}

@@ -69,3 +69,3 @@ else {

const trans = transformers[name] || standard;
trans( node, name, node[name], whereEntity );
trans( node, name, node[name] );
}

@@ -81,3 +81,3 @@ }

}
if (!node.propertyIsEnumerable( prop ))
if (!Object.prototype.propertyIsEnumerable.call( node, prop ))
node['$'+prop] = dict;

@@ -106,11 +106,15 @@ csnPath.pop();

function pathRef( node, prop, path, whereEntity ) {
function pathRef( node, prop, path ) {
const { links, art, scope } = (() => {
try {
return inspectRef( csnPath, whereEntity );
if (options.testMode)
return inspectRef( csnPath );
else {
try {
return inspectRef( csnPath );
}
catch (e) {
return { scope: e.toString() };
}
}
catch (e) {
return { scope: e.toString() };
}
})();
} )();
if (links)

@@ -129,3 +133,3 @@ node._links = links.map( l => refLocation( l.art ) );

if (s.where)
standard( s, 'where', s.where, links[i].env );
standard( s, 'where', s.where );
csnPath.pop();

@@ -132,0 +136,0 @@ }

@@ -142,12 +142,28 @@ 'use strict'

}
// Assemble the annotation
node[absoluteName] = {
name: {
absolute: absoluteName.substring(1),
},
val: theValue,
literal: 'string',
};
if(isAnnotationAssignable(node, absoluteName)) {
// Assemble the annotation
node[absoluteName] = {
name: {
absolute: absoluteName.substring(1),
},
val: theValue,
literal: 'string',
};
}
}
/**
* Check wether an annotation can be assigned or not
* (must be either undefined or null value)
* @param {object} node Assignee
* @param {string} name Annotation name
* @returns {boolean}
*/
function isAnnotationAssignable(node, name) {
if (!name.startsWith('@')) {
throw Error('Annotation name should start with "@": ' + name);
}
return (node[name] === undefined || node[name] && node[name].val === null);
}
// Add an annotation with absolute name 'absoluteName' (including '@') and string value 'theValue' to 'node'

@@ -160,13 +176,34 @@ function addBoolAnnotationTo(absoluteName, theValue, node) {

// Assemble the annotation
node[absoluteName] = {
name: {
absolute: absoluteName.substring(1),
if(isAnnotationAssignable(node, absoluteName)) {
node[absoluteName] = {
name: {
absolute: absoluteName.substring(1),
location: node.location, // inherit location from main element
},
val: theValue,
literal: 'boolean',
location: node.location, // inherit location from main element
},
val: theValue,
literal: 'boolean',
location: node.location, // inherit location from main element
};
};
}
}
// Add an annotation with absolute name 'absoluteName' (including '@') and enum value 'theValue' to 'node'
function addEnumAnnotationTo(absoluteName, theValue, node) {
// Sanity check
if (!absoluteName.startsWith('@')) {
throw Error('Annotation name should start with "@": ' + absoluteName);
}
if(isAnnotationAssignable(node, absoluteName)) {
// Assemble the annotation
node[absoluteName] = {
name: {
absolute: absoluteName.substring(1),
location: node.location, // inherit location from main element
},
symbol: { id: theValue },
literal: 'enum',
location: node.location, // inherit location from main element
};
}
}
// Add an annotation with absolute name 'absoluteName' (including '@') and path ref 'theValue' to 'node'

@@ -179,11 +216,12 @@ function addRefAnnotationTo(absoluteName, theValue, node) {

// Assemble the annotation
if(isAnnotationAssignable(node, absoluteName)) {
//member['@'+vlAnno] = { name: { path: vlAnno.split('.') }, path: [ { id: member.name.id } ] };
//member['@'+vlAnno] = { name: { path: vlAnno.split('.') }, path: [ { id: member.name.id } ] };
node[absoluteName] = {
name: {
absolute: absoluteName.substring(1),
},
path: theValue
};
node[absoluteName] = {
name: {
absolute: absoluteName.substring(1),
},
path: theValue
};
}
}

@@ -204,7 +242,9 @@

}
// FIXME: We leave the path as it was here and only adapt the absolute name - does that make sense?
annotation.name.absolute = toName.substring(1);
delete node[fromName];
node[toName] = annotation;
// FIXME: Should we try to resolve the annotation (but currently it does not seem to have a _artifact ?)
if(isAnnotationAssignable(node, toName)) {
// FIXME: We leave the path as it was here and only adapt the absolute name - does that make sense?
annotation.name.absolute = toName.substring(1);
delete node[fromName];
node[toName] = annotation;
// FIXME: Should we try to resolve the annotation (but currently it does not seem to have a _artifact ?)
}
}

@@ -458,2 +498,3 @@

addBoolAnnotationTo,
addEnumAnnotationTo,
addRefAnnotationTo,

@@ -460,0 +501,0 @@ renameAnnotation,

@@ -20,3 +20,3 @@ // Make internal properties of the XSN / augmented CSN visible

function revealInternalProperties( model ) {
function revealInternalProperties( model, name ) {
var unique_id = 0;

@@ -42,2 +42,3 @@

$navigation: dictionary,
$keysNavigation: dictionary,
$combined: artifactDictionary,

@@ -81,3 +82,3 @@ $dictOrderBy: artifactDictionary,

}
return reveal( model );
return reveal( name && name !== '+' ? { definitions: model.definitions[name] } : model );

@@ -185,3 +186,3 @@ function artifactIdentifier( node, parent ) {

function dictionary( node ) {
return reveal( node, '__proto__' );
return reveal( node, '__proto__' );
}

@@ -194,3 +195,3 @@

function revealQuery( node ) {
return reveal( node, undefined, undefined, true );
return reveal( node, undefined, undefined, '__neverEqualKind' );
}

@@ -227,3 +228,3 @@

(proto === null || protoProp === null ? reveal : transformers[prop]) ||
(node.propertyIsEnumerable( prop ) ? reveal : primOrString);
(Object.prototype.propertyIsEnumerable.call( node, prop ) ? reveal : primOrString);
r[prop] = func( item, node );

@@ -230,0 +231,0 @@ }

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

.option('-v, --version')
.option('-w, --warning <level>', ['0', '1', '2'])
.option('-w, --warning <level>', ['0', '1', '2', '3'])
.option(' --show-message-id')
.option(' --no-message-context')
.option(' --color <mode>', ['auto', 'always', 'never'])
.option('-o, --out <dir>')

@@ -22,8 +24,8 @@ .option(' --lint-mode')

.option('-E, --enrich-csn')
.option('-R, --raw-output')
.option('-R, --raw-output <name>')
.option(' --internal-msg')
.option(' --beta-mode')
.option(' --new-transformers')
.option(' --new-csn')
.option(' --old-csn')
.option(' --old-transformers')
.option(' --std-json-parser')
.option(' --old-csn-frontend')
.option(' --long-autoexposed')

@@ -33,9 +35,8 @@ .option(' --hana-flavor')

.option(' --test-mode')
.option('--precision <prec>')
.option('--scale <scale>')
.option('--length <length>')
.positionalArgument('<files...>')
.help(`
Usage: cdsc <command> [options] <file...>
Usage: cdsc <command> [options] <files...>
Compile a CDS model given from input <file...>s and generate results according to <command>.
Compile a CDS model given from input <files...>s and generate results according to <command>.
Input files may be CDS source files (.cds), CSN model files (.json) or pre-processed ODATA

@@ -56,2 +57,9 @@ annotation XML files (.xml). Output depends on <command>, see below. If no command is given,

--show-message-id Show message ID in error, warning and info messages
--no-message-context Print messages as single lines without code context (useful for
redirecting output to other processes). Default is to print human
readable text similar to Rust's compiler with a code excerpt.
--color <mode> Use colors for warnings. Modes are:
auto: (default) Detect color support of the tty.
always:
never:
-o, --out <dir> Place generated files in directory <dir>, default is "-" for <stdout>

@@ -63,4 +71,2 @@ --lint-mode Generate nothing, just produce messages if any (for use by editors)

Type options
--precision <prec> Default precision for 'cds.Decimal'
--scale <scale> Default scale for 'cds.Decimal'
--length <length> Default 'length' for 'cds.String'

@@ -75,8 +81,9 @@

-E, --enrich-csn Show non-enumerable CSN properties and locations of references
-R, --raw-output Write raw augmented CSN and error output to <stdout>, long!
-R, --raw-output <name> Write XSN for definition "name" and error output to <stdout>,
with name = "+", write complete XSN, long!
--internal-msg Write raw messages with call stack to <stdout>/<stderr>
--beta-mode Enable unsupported, incomplete (beta) features
--new-transformers Use the new transformers that work on CSN instead of XSN
--new-csn Produce CSN version 1.0 format (default)
--old-csn Produce CSN version 0.1 format (deprecated, overrides --new-csn)
--old-transformers Use the old transformers that work on XSN instead of CSN
--old-csn-frontend Use old CSN frontend
--std-json-parser Use standard JSON parser for CSN parsing with old CSN frontend
--hana-flavor Compile with backward compatibility for HANA CDS (incomplete)

@@ -91,9 +98,9 @@ --parse-only Stop compilation after parsing and write result to <stdout>

Commands
H, toHana [options] <file...> Generate HANA CDS source files
O, toOdata [options] <file...> Generate ODATA metadata and annotations
C, toCdl <file...> Generate CDS source files
S, toSwagger [options] <file...> Generate Swagger (OpenAPI) JSON
Q, toSql [options] <file...> Generate SQL DDL statements
toCsn [options] <file...> (default) Generate original model as CSN
toRename [options] <file...> (internal) Generate SQL DDL rename statements
H, toHana [options] <files...> Generate HANA CDS source files
O, toOdata [options] <files...> Generate ODATA metadata and annotations
C, toCdl <files...> Generate CDS source files
S, toSwagger [options] <files...> Generate Swagger (OpenAPI) JSON
Q, toSql [options] <files...> Generate SQL DDL statements
toCsn [options] <files...> (default) Generate original model as CSN
toRename [options] <files...> (internal) Generate SQL DDL rename statements
`);

@@ -106,6 +113,7 @@

.option('-a, --associations <proc>', ['assocs', 'joins'])
.option('-u, --user <user>')
.option('-s, --src')
.option('-c, --csn')
.help(`
Usage: cdsc toHana [options] <file...>
Usage: cdsc toHana [options] <files...>

@@ -130,2 +138,3 @@ Generate HANA CDS source files, or CSN.

joins : Transform associations to joins
-u, --user <user> Value for the "$user" variable
-s, --src (default) Generate HANA CDS source files "<artifact>.hdbcds"

@@ -142,6 +151,9 @@ -c, --csn Generate "hana_csn.json" with HANA-preprocessed model

.option(' --combined')
.option(' --odata-containment')
.option(' --odata-proxies')
.option('-c, --csn')
.option('-f, --odata-format <format>', ['flat', 'structured'])
.option('-n, --names <style>', ['plain', 'quoted', 'hdbcds'])
.help(`
Usage: cdsc toOdata [options] <file...>
Usage: cdsc toOdata [options] <files...>

@@ -160,2 +172,8 @@ Generate ODATA metadata and annotations, or CSN.

-c, --csn Generate "odata_csn.json" with ODATA-preprocessed model
-f, --odata-format <format> Set the format of the identifier rendering
(ignored by '--old-transformers')
flat : (default) Flat type and property names
structured : (V4 only) Render structured metadata
--odata-containment Generate Containment Navigation Properties for compositions (V4 only)
--odata-proxies (highly experimental) Generate Proxies for out-of-service navigation targets.
-n, --names <style> Annotate artifacts and elements with "@cds.persistence.name", which is

@@ -173,3 +191,3 @@ the corresponding database name (see "--names" for "toHana or "toSql")

.help(`
Usage: cdsc toCdl [options] <file...>
Usage: cdsc toCdl [options] <files...>

@@ -187,3 +205,3 @@ Generate CDS source files "<artifact>.cds".

.help(`
Usage: cdsc toSwagger [options] <file...>
Usage: cdsc toSwagger [options] <files...>

@@ -208,3 +226,3 @@ Generate Swagger (OpenAPI) JSON, or CSN

.help(`
Usage: cdsc toSql [options] <file...>
Usage: cdsc toSql [options] <files...>

@@ -236,3 +254,3 @@ Generate SQL DDL statements to create tables and views, or CSN

sqlite : (default) Common SQL for sqlite
-u, --user <user> Value for the "$user" variable in "sqlite" dialect
-u, --user <user> Value for the "$user" variable
-l, --locale <locale> Value for the "$user.locale" variable in "sqlite" dialect

@@ -251,3 +269,3 @@ -s, --src <style> Generate SQL source files as <artifact>.<suffix>

.help(`
Usage: cdsc toRename [options] <file...>
Usage: cdsc toRename [options] <files...>

@@ -273,3 +291,3 @@ (internal, subject to change): Generate SQL stored procedure containing DDL statements to

.help(`
Usage: cdsc toCsn [options] <file...>
Usage: cdsc toCsn [options] <files...>

@@ -276,0 +294,0 @@ Generate original model as CSN to "csn.json"

@@ -10,3 +10,2 @@ /**

const walker = require("../json/walker")
const { transformLocation } = require('./renderUtil');

@@ -22,3 +21,3 @@ // database name - uppercase if not quoted

* Check for duplicate artifacts or elements
*
*
* @class DuplicateChecker

@@ -36,3 +35,3 @@ */

* Initialize the state of the checker.
*
*
* @memberOf DuplicateChecker

@@ -47,11 +46,11 @@ */

* Add an artifact to the "seen"-list
*
* @param {any} name
* @param {any} location
*
*
* @param {any} name
* @param {any} location
*
* @memberOf DuplicateChecker
*/
addArtifact( name, location ){
addArtifact( name, location, modelName ){
const dbname = asDBName(name);
this.currentArtifact = { name, location, elements: {} };
this.currentArtifact = { name, location, elements: {}, modelName };
if(!this.seenArtifacts[dbname])

@@ -65,10 +64,10 @@ this.seenArtifacts[dbname] = [this.currentArtifact];

* Add an element to the "seen"-list
*
* @param {any} name
* @param {any} location
* @returns
*
*
* @param {any} name
* @param {any} location
* @returns
*
* @memberOf DuplicateChecker
*/
addElement(name,location){
addElement(name,location, modelName){
if(!this.currentArtifact.elements)

@@ -78,3 +77,3 @@ return;

let currentElements = this.currentArtifact.elements;
const element = {name,location};
const element = {name,location, modelName};
if(!currentElements[dbname])

@@ -88,3 +87,3 @@ currentElements[dbname] = [element];

* No more artifacts need to be processed, check for duplicates and re-init the object.
*
*
* @memberOf DuplicateChecker

@@ -96,3 +95,3 @@ */

artifacts.forEach(artifact => { // report all colliding artifacts
signal(error`Duplicated artifact '${artifactName}', origin: '${artifact.name}'`, transformLocation(artifact.location));
signal(error`Duplicated artifact '${artifactName}', origin: '${artifact.name}'`, ['definitions', artifact.modelName] );
});

@@ -104,3 +103,3 @@ }

elements.forEach(element => { // report all colliding elements
signal(error`Duplicated element '${element.name}' in artifact '${artifact.name}'`, transformLocation(element.location));
signal(error`Duplicated element '${element.name}' in artifact '${artifact.name}'`, ['definitions', artifact.modelName,'elements', element.modelName]);
})

@@ -107,0 +106,0 @@ }

@@ -33,24 +33,5 @@ // Common render functions for toCdl.js and toSql.js

/**
* Get the $location from an object and make it look like the XSN location - delete - not necessary
*
* @param {any} location
*/
function transformLocation(location){
return {
start: {
line: location.line,
column: location.col
},
end: {
line: location.line,
column: location.col
},
filename: location.file
}
}
module.exports = {
renderFunc,
transformLocation
renderFunc
}
"use strict";
const { CompilationError, hasErrors, sortMessages } = require('../base/messages');
const { mergeOptions, getTopLevelArtifactNameOf, getParentNameOf, getLastPartOf,
getLastPartOfRef, getParentNamesOf } = require('../model/modelUtils');
const { compactModel } = require('../json/to-csn');
const { compactModel, sortCsn } = require('../json/to-csn');
const keywords = require('../base/keywords');
const version = require('../../package.json').version;
const alerts = require('../base/alerts');
const { transformLocation, renderFunc } = require('./renderUtil'); // TODO: transformLocation should not be necessary
const { renderFunc } = require('./renderUtil');
const DuplicateChecker = require('./DuplicateChecker');
const { setProp } = require('../base/model');
const { checkCSNVersion } = require('../json/csnVersion');
const { handleMessages } = require('../base/messages');
function toCdsSource(model, options){
options = mergeOptions(model.options, options);
let csn = compactModel(model);
if (options.testMode)
csn = sortCsn(csn, true);
if(model.messages)
setProp(csn, "messages", model.messages);
return toCdsSourceCsn(csn, options);
}
// Render the CSN model 'model' to CDS source text. One source is created per

@@ -22,14 +35,15 @@ // top-level artifact. Return a dictionary of top-level artifacts

// FIXME: This comment no longer tells the whole truth
function toCdsSource(model, options) {
function toCdsSourceCsn(csn, options) {
// Merge options (arguments first, then model options)
options = mergeOptions(model.options, options);
options = mergeOptions(csn.options, options);
let plainNames = options.forHana && options.forHana.names == 'plain';
let hdbcdsNames = options.forHana && options.forHana.names == 'hdbcds';
// Skip compactModel if already using CSN
const csn = (options.newTransformers) ? model : compactModel(model);
//const csn = cloneCsn(model);
const { signal, warning, error } = alerts(csn);
const { signal, warning, error } = alerts(csn, options);
checkCSNVersion(csn, options);
let result = Object.create(null);

@@ -57,3 +71,3 @@

// This environment is passed down the call hierarchy, for dealing with
// indentation and name resolution issues
// indentation and name resolution issues
let env = createEnv();

@@ -69,3 +83,3 @@ let sourceStr = renderArtifact(artifactName, csn.definitions[artifactName], env); // Must come first because it populates 'env.topLevelAliases'

globalDuplicateChecker && globalDuplicateChecker.check(signal, error); // perform duplicates check
// If there are unapplied 'extend' and 'annotate' statements, render them separately

@@ -80,5 +94,3 @@ // FIXME: Clarify if we should also do this for HANA (probably not?)

// Throw exception in case of errors
if (hasErrors(csn.messages)) {
throw new CompilationError(sortMessages(csn.messages), csn);
}
handleMessages(csn, options);
return result;

@@ -143,2 +155,4 @@

function renderArtifact(artifactName, art, env) {
// FIXME: Correctly build the paths during runtime to give better locations
env.path = ['definitions', artifactName];
// Ignore whole artifacts if toHana says so

@@ -275,3 +289,3 @@ if (art._ignore) {

let normalizedArtifactName = renderArtifactName(artifactName, env);
globalDuplicateChecker && globalDuplicateChecker.addArtifact(normalizedArtifactName, art && art.$location);
globalDuplicateChecker && globalDuplicateChecker.addArtifact(normalizedArtifactName, art && art.$location, artifactName);
result += env.indent + (art.abstract ? 'abstract ' : '') + 'entity ' + normalizedArtifactName;

@@ -286,3 +300,3 @@ let parameters = Object.keys(art.params || []).map(name => renderParameter(name, art.params[name], childEnv)).join(',\n');

let duplicateChecker = new DuplicateChecker(); // registry for all artifact names and element names
duplicateChecker.addArtifact(artifactName, art && art.$location)
duplicateChecker.addArtifact(artifactName, art && art.$location, artifactName)
for (let name in art.elements) {

@@ -386,4 +400,4 @@ result += renderElement(name, art.elements[name], childEnv, duplicateChecker);

let result = renderAnnotationAssignments(elm, env);
duplicateChecker && elm && duplicateChecker.addElement(quoteOrUppercaseId(elementName), elm && elm.$location);
result += env.indent + (elm.virtual ? 'virtual ' : '')
duplicateChecker && elm && duplicateChecker.addElement(quoteOrUppercaseId(elementName), elm && elm.$location, elementName);
result += env.indent + (elm.virtual ? 'virtual ' : '')
+ (elm.key ? 'key ' : '')

@@ -579,5 +593,5 @@ + ((elm.masked && !elm._ignoreMasked)? 'masked ' : '')

} else {
// If key is explicitly set in a not-first query of a UNION, issue an error.
// If key is explicitly set in a non-leading query, issue an error.
if(col.key && env.skipKeys){
signal(error`KEY must only be added in the first query of a UNION`, transformLocation(col.$location));
signal(error`KEY must only be added in the leading query`, env.path);
}

@@ -634,3 +648,3 @@ const key = (!env.skipKeys && (col.key || (options.forHana && leaf && env._artifact.elements[leaf] && env._artifact.elements[leaf].key)) ? 'key ' : '');

env._artifact = art;
result += renderQuery(art.query, true, syntax, env);
result += renderQuery(art.query, true, syntax, env, ['definitions',artifactName,'query']);
result += ';\n';

@@ -646,20 +660,15 @@ result += renderQueryElementAnnotations(artifactName, art, env);

// or 'entity')
function renderQuery(query, isLeadingQuery, syntax, env) {
function renderQuery(query, isLeadingQuery, syntax, env, path=[]) {
let result = '';
env.skipKeys = !isLeadingQuery;
// Set operator, like UNION, INTERSECT, ...
if (query.SET) {
// First arg may be leading query
result += `(${renderQuery(query.SET.args[0], isLeadingQuery, 'view', env)}`
result += `(${renderQuery(query.SET.args[0], isLeadingQuery, 'view', env, path.concat(['SET','args',0]))}`
// FIXME: Clarify if set operators can be n-ary (assuming binary here)
if (query.SET.op) {
if(query.SET.op === 'union'){
// For UNION, don't render "key X as X" in the second SELECT
env.skipKeys = true;
}
// Loop over all other arguments, i.e. for A UNION B UNION C UNION D ...
for(let i = 1; i < query.SET.args.length; i++){
result += `\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} ${renderQuery(query.SET.args[i], false, 'view', env)}`;
result += `\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} ${renderQuery(query.SET.args[i], false, 'view', env, path.concat(['SET','args',i]))}`;
}
// Reset afterwards
env.skipKeys = false;
}

@@ -690,3 +699,3 @@ result += ')';

} else {
throw new Error(`Unknown query syntax: ${syntax}`);
throw new Error(`Unknown query syntax: ${syntax}`);
}

@@ -707,3 +716,3 @@ if (select.mixin) {

result += ' {\n' +
(select.columns||['*']).map(col => renderViewColumn(col, childEnv))
(select.columns||['*']).map((col) => renderViewColumn(col, childEnv))
.filter(s => s != '')

@@ -898,5 +907,3 @@ .join(',\n') + '\n';

if (isBuiltinType(elm.type)) {
// cds.Integer => render as Integer (no quotes)
result += elm.type.replace(/^cds\./, '');
result += renderTypeParameters(elm);
result += renderBuiltinType(elm);
} else {

@@ -913,2 +920,11 @@ // Simple absolute name

function renderBuiltinType(elm) {
// cds.Integer => render as Integer (no quotes)
// Map Decimal (w/o Prec/Scale) to cds.DecimalFloat for HANA CDS
if(options.toHana && elm.type === 'cds.Decimal' && elm.scale === undefined && elm.precision === undefined)
return 'DecimalFloat';
else {
return elm.type.replace(/^cds\./, '') + renderTypeParameters(elm);
}
}
// Render the 'enum { ... } part of a type declaration

@@ -1004,4 +1020,12 @@ function renderEnum(enumPart, env) {

// FIXME: this is all not enough: we might need an explicit select item alias
if (x.ref[1] === 'id')
return "SESSION_CONTEXT('XS_APPLICATIONUSER')";
if (x.ref[1] === 'id') {
if (options.toHana.user && typeof options.toHana.user === 'string' || options.toHana.user instanceof String) {
return `'${options.toHana.user}'`;
}
else if ((options.toHana.user && options.toHana.user.id) && (typeof options.toHana.user.id === 'string' || options.toHana.user.id instanceof String)) {
return `'${options.toHana.user.id}'`;
} else {
return "SESSION_CONTEXT('APPLICATIONUSER')";
}
}
else if (x.ref[1] === 'locale')

@@ -1157,3 +1181,3 @@ return "SESSION_CONTEXT('LOCALE')";

// Render (primitive) type parameters of element 'elm', i.e.
// Render (primitive) type parameters of element 'elm', i.e.
// length, precision and scale (even if incomplete), plus any other unknown ones.

@@ -1259,3 +1283,3 @@ function renderTypeParameters(elm /*, env */) {

// fact that 'absName' is used in 'env', so that an appropriate USING can be constructed
// if necessary.
// if necessary.
function renderAbsoluteNamePlain(absName, env) {

@@ -1323,3 +1347,3 @@ // Add using declaration

}
// Depending on the naming style, render the namespace declaration for a top-level artifact 'name'

@@ -1498,2 +1522,2 @@ // if it has a namespace parent. Assume that this is only called for top-level artifacts.

module.exports = { toCdsSource };
module.exports = { toCdsSource, toCdsSourceCsn };

@@ -24,3 +24,3 @@

// FIXME: Currently requires 'options.forHana', because it requires the 'forHana' transformations
// FIXME: Currently requires 'options.forHana', because it requires the 'forHana' transformations
if (!options.forHana) {

@@ -27,0 +27,0 @@ throw new Error('toRenameDdl can currently only be used with HANA preprocessing');

"use strict";
const { CompilationError, hasErrors, sortMessages } = require('../base/messages');
const { getTopLevelArtifactNameOf, getParentNameOf, getParentNamesOf, getLastPartOf, getLastPartOfRef } = require('../model/modelUtils');

@@ -9,6 +8,7 @@ const keywords = require('../base/keywords');

const version = require('../../package.json').version;
const { renderFunc } = require('./renderUtil'); // TODO: transformLocation should not be necessary
const { renderFunc } = require('./renderUtil');
const DuplicateChecker = require("./DuplicateChecker");
const { checkCSNVersion } = require('../json/csnVersion');
const { handleMessages } = require('../base/messages');
// Type mapping from cds type names to DB type names:

@@ -77,3 +77,3 @@ // (in the future, we would introduce an option for the mapping table)

function toSqlDdl(csn, options = csn.options) {
const { error, signal, warning, info } = alerts(csn);
const { error, signal, warning, info } = alerts(csn, options);

@@ -85,2 +85,5 @@ // FIXME: Currently requires 'options.forHana', because it can only render HANA-ish SQL dialect

checkCSNVersion(csn, options);
// Create artificial namespace objects, so that each artifact has parents up to top-level.

@@ -125,5 +128,3 @@ // FIXME: This is actually only necessary to make 'getParentNameOf' work - should be reworked

// Throw exception in case of errors
if (hasErrors(csn.messages)) {
throw new CompilationError(sortMessages(csn.messages), csn);
}
handleMessages(csn, options);

@@ -205,3 +206,3 @@ // Transfer results from hdb-specific dictionaries into 'sql' dictionary in proper order if toSql.src == 'sql'

let result = '';
// Only HANA has row/column tables
// Only HANA has row/column tables
if (options.toSql.dialect == 'hana') {

@@ -217,3 +218,3 @@ if (hanaTc && hanaTc.storeType) {

let tableName = quoteSqlId(absoluteCdsName(artifactName));
duplicateChecker.addArtifact(tableName, art && art.$location)
duplicateChecker.addArtifact(tableName, art && art.$location, artifactName)
result += 'TABLE ' + tableName;

@@ -258,3 +259,3 @@ result += ' (\n';

// Only HANA has indices
// FIXME: Really? We should provide a DB-agnostic way to specify that
// FIXME: Really? We should provide a DB-agnostic way to specify that
if (options.toSql.dialect === 'hana') {

@@ -295,3 +296,3 @@ renderIndexesInto(art.technicalConfig && art.technicalConfig.hana.indexes, artifactName, resultObj, env);

let quotedElementName = quoteSqlId(elementName);
duplicateChecker.addElement(quotedElementName, elm && elm.$location);
duplicateChecker.addElement(quotedElementName, elm && elm.$location, elementName);
let result = env.indent + quotedElementName + ' '

@@ -422,3 +423,3 @@ + renderTypeReference(artifactName, elementName, elm)

// FIXME: Misleading name, should be something like 'renderQueryFrom'. All the query
// parts should probably also be rearranged.
// parts should probably also be rearranged.
// Returns the source as a string.

@@ -540,10 +541,12 @@ function renderViewSource(artifactName, source, env) {

let result = '';
let leaf = col.as || col.ref && col.ref[col.ref.length-1];
let leaf = col.as || col.ref && col.ref[col.ref.length-1] || col.func;
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);
} else if (col.cast) {
result = env.indent + 'CAST(' + renderExpr(col, env, true) + ' AS ';
result += renderBuiltinType(col.cast.type) + renderTypeParameters(col.cast);
result += ') ' + quoteSqlId(leaf);
} else {
// FIXME: We may want to wrap a cast around 'col' if it has an explicit type?
result = env.indent + renderExpr(col, env, true);
// Explicit or implicit alias?
if (col.as) {

@@ -560,3 +563,3 @@ result += ' AS ' + quoteSqlId(col.as);

let viewName = quoteSqlId(absoluteCdsName(artifactName));
duplicateChecker.addArtifact(viewName, art && art.$location)
duplicateChecker.addArtifact(viewName, art && art.$location, artifactName)
let result = 'VIEW ' + viewName;

@@ -693,3 +696,3 @@ result += renderParameterDefinitions(artifactName, art.params);

let typeName = quoteSqlId(absoluteCdsName(artifactName));
duplicateChecker.addArtifact(typeName, art && art.$location)
duplicateChecker.addArtifact(typeName, art && art.$location, artifactName)
let result = 'TYPE ' + quoteSqlId(absoluteCdsName(artifactName)) + ' AS TABLE (\n';

@@ -768,3 +771,3 @@ let childEnv = increaseIndent(env);

// Render the nullability of an element or parameter (can be unset, true, or false)
// Render the nullability of an element or parameter (can be unset, true, or false)
function renderNullability(obj, treatKeyAsNotNull = false) {

@@ -778,3 +781,3 @@ if (obj.notNull === undefined && !(obj.key && treatKeyAsNotNull)) {

// Render (primitive) type parameters of element 'elm', i.e.
// Render (primitive) type parameters of element 'elm', i.e.
// length, precision and scale (even if incomplete), plus any other unknown ones.

@@ -877,16 +880,15 @@ function renderTypeParameters(elm) {

if (x.ref[1] === 'id') {
if (options.forHana.dialect === 'sqlite') {
if (options.toSql.user && typeof options.toSql.user === 'string' || options.toSql.user instanceof String) {
return `'${options.toSql.user}'`;
}
else if ((options.toSql.user && options.toSql.user.id) && (typeof options.toSql.user.id === 'string' || options.toSql.user.id instanceof String)) {
return `'${options.toSql.user.id}'`;
} else {
if (options.toSql.user && typeof options.toSql.user === 'string' || options.toSql.user instanceof String) {
return `'${options.toSql.user}'`;
}
else if ((options.toSql.user && options.toSql.user.id) && (typeof options.toSql.user.id === 'string' || options.toSql.user.id instanceof String)) {
return `'${options.toSql.user.id}'`;
} else {
if(options.forHana.dialect === 'sqlite'){
signal(warning`The "$user" variable is not supported by SQLite. Use the "toSql.user" option to set a value for "$user.id"`);
return `'$user.id'`;
} else {
return "SESSION_CONTEXT('APPLICATIONUSER')";
}
}
else {
return "SESSION_CONTEXT('XS_APPLICATIONUSER')";
}
}

@@ -903,3 +905,3 @@ else if (x.ref[1] === 'locale') {

if(options.forHana.dialect === 'sqlite') {
return "current_time";
return "current_timestamp";
}

@@ -960,5 +962,6 @@ else if(options.forHana.dialect === 'hana') {

if (typeof(s) == 'string') {
// TODO: When is this actually executed and not handled already in renderExpr?
const magicForHana = {
'$now': 'CURRENT_TIMESTAMP',
'$user.id': "SESSION_CONTEXT('XS_APPLICATIONUSER')",
'$user.id': "SESSION_CONTEXT('APPLICATIONUSER')",
'$user.locale': "SESSION_CONTEXT('LOCALE')",

@@ -965,0 +968,0 @@ }

@@ -87,3 +87,3 @@ const schemaObjects = require('./swaggerSchemaObjects');

// if the user wants some specific order or more complicated path => should specify it with the @Swagger.path annotation
// and have to take care of correct parameters names
// and have to take care of correct parameters names
if (!action['@Swagger.path'] && action.params)

@@ -90,0 +90,0 @@ Object.keys(action.params).forEach(pName => {

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

const { isManagedAssociationElement, isStructuredElement, isAssociation, isAssocOrComposition, isElementWithType,
renameAnnotation, addBoolAnnotationTo, addStringAnnotationTo, addRefAnnotationTo, copyAnnotations,
renameAnnotation, addBoolAnnotationTo, addStringAnnotationTo, addEnumAnnotationTo, addRefAnnotationTo, copyAnnotations,
foreachPath, hasBoolAnnotation, getElementDatabaseNameOf, getArtifactDatabaseNameOf } = require('../model/modelUtils');

@@ -22,3 +22,3 @@ const transformUtils = require('./transformUtils');

// '@odata.foreignKey4'). Propagate along projections accordingly. Names are built using
// <assoc>_<key>, conflicts are checked.
// <assoc>_<key>, conflicts are checked.
// - Complete the 'foreignKeys' property for all managed associations, so that there

@@ -35,3 +35,3 @@ // is always a 'generatedFieldName' for the corresponding generated foreign key field.

function transform4odata(inputModel, options) {
const { error, warning, signal } = alerts(inputModel);
const { error, warning, info, signal } = alerts(inputModel);
let model;

@@ -53,3 +53,3 @@

extractValidFromToKeyElement, checkAssignment, checkMultipleAssignments } = transformUtils.getTransformers(model, '_');
// semantic checks before flattening

@@ -64,2 +64,4 @@ forEachDefinition(model, artifact => {

validKey.push(...k);
});

@@ -169,2 +171,5 @@ // Check that @cds.valid.from/to/key is only in valid places

// key fields in types we have already processed.
// Check that each service has max one draft root
// This must be done before draft processing, since all composition targets
// are annotated with @odata.draft.enabled in that step
forEachDefinition(model, (artifact) => {

@@ -200,2 +205,12 @@ forEachMemberRecursively(artifact, (member) => {

});
if (artifact._service && (artifact.kind == 'entity' || artifact.kind == 'view') && hasBoolAnnotation(artifact, '@odata.draft.enabled')) {
let location = artifact['@odata.draft.enabled'].name && artifact['@odata.draft.enabled'].name.location;
if(artifact._service.$draftCount)
artifact._service.$draftCount++;
else
setProp(artifact._service, '$draftCount', 1);
if(artifact._service.$draftCount > 1)
signal(info`Service "${artifact._service.name.absolute}" should not have more then one draft root artifact`, location);
}
});

@@ -213,4 +228,4 @@

// Ignore if not part of a service
let location = artifact['@odata.draft.enabled'].name && artifact['@odata.draft.enabled'].name.location;
if (!artifact._service) {
let location = artifact['@odata.draft.enabled'].name && artifact['@odata.draft.enabled'].name.location;
signal(warning`Ignoring annotation "@odata.draft.enabled" - artifact "${artifact.name.absolute}" is not part of a service`, location);

@@ -255,4 +270,14 @@ }

// CDXCORE-458
else if (member.kind == 'element' && member.items && options.toOdata.version == 'v2') {
signal(error`"${artifact.name.absolute}.${memberName}": Element must not be an "array of" for OData V2`, member.location);
else if (member.kind == 'element' && member.items) {
if(options.toOdata.version == 'v2') {
signal(error`"${artifact.name.absolute}.${memberName}": Element must not be an "array of" for OData V2`, member.location);
}
else if(['entity', 'view'].includes(artifact.kind)) {
if(member.items.elements && !member.items.type) {
signal(error`"${artifact.name.absolute}.${memberName}": Element must not be an "array of anonymous type"`, member.location);
}
if(!member.items.elements && member.items.type && !member.items.type._artifact.builtin && member.items.type._service != artifact._service) {
signal(error`${artifact.name.absolute}.${memberName}": Array type "${member.items.type._artifact.name.absolute}" is not member of service`, member.location);
}
}
}

@@ -273,3 +298,3 @@

signal(
warning`'@Analytics.Measure' expects '@Aggregation.default' to be assigned as well in element '${member.name.absolute}.${member.name.id}'`,
info`'@Analytics.Measure' expects '@Aggregation.default' to be assigned as well in element '${member.name.absolute}.${member.name.id}'`,
member.location

@@ -368,3 +393,3 @@ );

* Check if any warnings were raised in earlier steps that need to be reclassified - i.e. as errors
*
*
* @param {any} model The model

@@ -391,2 +416,70 @@ * @returns {Array} Reclassified messages-Array

// Rename shorthand annotations within artifact or element 'node' according to a builtin
// list.
function renameShorthandAnnotations(node) {
// FIXME: Verify this list - are they all still required? Do we need any more?
const mappings = {
'@label': '@Common.Label',
'@title': '@Common.Label',
'@description': '@Core.Description',
'@ValueList.entity': '@Common.ValueList.entity',
'@ValueList.type': '@Common.ValueList.type',
'@Capabilities.Deletable': '@Capabilities.DeleteRestrictions.Deletable',
'@Capabilities.Insertable': '@Capabilities.InsertRestrictions.Insertable',
'@Capabilities.Updatable': '@Capabilities.UpdateRestrictions.Updatable',
}
let rewriteCapabilities = true;
if(hasBoolAnnotation(node, '@readonly') && hasBoolAnnotation(node, '@insertonly') && ['entity', 'view'].includes(node.kind)) {
rewriteCapabilities = false;
signal(warning`"@readonly" and "@insertonly" cannot be assigned in combination`, node.location);
}
for (let name in node) {
// Rename according to map above
if (mappings[name] != undefined) {
renameAnnotation(node, name, mappings[name]);
}
// Special case: '@important: [true|false]' becomes '@UI.Importance: [#High|#Low]'
if (name == '@important') {
renameAnnotation(node, name, '@UI.Importance');
let annotation = node['@UI.Importance'];
annotation.literal = 'enum';
annotation.symbol = {
// Note that an original '@important' without ': true' shows up as undefined value here!!
id: (annotation.val == undefined || annotation.val == true) ? 'High' : 'Low',
};
// FIXME: Strangely, enum-valued annotations have no value
delete annotation.val;
}
// Special case: '@readonly' becomes a triplet of capability restrictions for entities,
// but '@Core.Immutable' for everything else.
if(rewriteCapabilities) {
if (name == '@readonly') {
if (node.kind == 'entity' || node.kind == 'view') {
addBoolAnnotationTo('@Capabilities.DeleteRestrictions.Deletable', false, node);
addBoolAnnotationTo('@Capabilities.InsertRestrictions.Insertable', false, node);
addBoolAnnotationTo('@Capabilities.UpdateRestrictions.Updatable', false, node);
}
else {
renameAnnotation(node, name, '@Core.Computed');
}
}
// @insertonly is effective on entities/queries only
else if (name == '@insertonly') {
if (node.kind == 'entity' || node.kind == 'view') {
addBoolAnnotationTo('@Capabilities.DeleteRestrictions.Deletable', false, node);
addBoolAnnotationTo('@Capabilities.ReadRestrictions.Readable', false, node);
addBoolAnnotationTo('@Capabilities.UpdateRestrictions.Updatable', false, node);
}
}
}
// Only on element level: translate @mandatory
if(name === '@mandatory' && node.kind === 'element' && node['@Common.FieldControl'] === undefined) {
addEnumAnnotationTo('@Common.FieldControl', 'Mandatory', node);
}
}
}
// Apply checks to all annotations in the model

@@ -409,2 +502,7 @@ // node: artifact/element/action/function/parameter/... that carries the annotations

// 'rootArtifact' is the root artifact where composition traversal started.
// Constraints
// Draft Root: Exactly one PK of type UUID
// Draft Node: One PK of type UUID + 0..1 PK of another type
// Draft Node: Must not be reachable from multiple draft roots
function generateDraftForOdata(artifact, rootArtifact, visitedArtifacts) {

@@ -422,19 +520,35 @@ // Sanity check

// FIXME: Current restriction: Must only have exactly one key, which is of type UUID
let keyNames = Object.keys(artifact.elements).filter(elemName => {
return artifact.elements[elemName].key && artifact.elements[elemName].key.val;
});
if (keyNames.length != 1) {
signal(warning`"${artifact.name.absolute}": Ignoring annotation "@odata.draft.enabled" - currently only supported for artifacts with exactly one key of type "UUID"`, artifact.location);
return;
if(artifact === rootArtifact) {
// draft root
if(keyNames.length != 1) {
signal(info`"${artifact.name.absolute}": "@odata.draft.enabled" - Draft root should have exactly one key element`, artifact.location);
} else {
let keyElem = artifact.elements[keyNames[0]];
if (keyElem._finalType.name.absolute != 'cds.UUID' && keyElem._finalType.name.$renamed != 'cds.UUID') {
signal(info`"${artifact.name.absolute}": "@odata.draft.enabled" - Draft root key element should be of type "cds.UUID"`, keyElem.location);
}
}
} else {
// draft node
if(keyNames.length < 1) {
signal(info`"${artifact.name.absolute}": "@odata.draft.enabled" - Draft node should have at least one key element`, artifact.location);
} else if(keyNames.length > 2) {
signal(info`"${artifact.name.absolute}": "@odata.draft.enabled" - Draft node should not have more than two key elements`, artifact.location);
} else {
let uuidCount = 0;
keyNames.forEach(kn => {
let keyElem = artifact.elements[kn];
if (keyElem._finalType.name.absolute == 'cds.UUID' || keyElem._finalType.name.$renamed == 'cds.UUID') {
uuidCount++;
}
});
if(uuidCount != 1) {
signal(info`"${artifact.name.absolute}": "@odata.draft.enabled" - Draft node should have one key element of type "cds.UUID"`, artifact.location);
}
}
}
let keyElem = artifact.elements[keyNames[0]];
// Sanity check
if (!keyElem._finalType) {
throw new Error('Expecting artifact to have final type: ' + JSON.stringify(keyElem));
}
if (keyElem._finalType.name.absolute != 'cds.UUID' && keyElem._finalType.name.$renamed != 'cds.UUID') {
signal(warning`"${artifact.name.absolute}": Ignoring annotation "@odata.draft.enabled" - currently only supported for key of type "UUID"`, keyElem.location);
return;
}

@@ -461,14 +575,25 @@ // Generate the DraftAdministrativeData projection into the service, unless there is already one

for (let elemName in artifact.elements) {
let elem = artifact.elements[elemName];
// Make all non-key elements nullable
if (elem.notNull && elem.notNull.val===true && !(elem.key && elem.key.val)) {
delete elem.notNull;
}
}
// Generate the additional elements into the draft-enabled artifact
// key IsActiveEntity : Boolean default true
let isActiveEntity = createScalarElement('IsActiveEntity', 'cds.Boolean', true, true);
let isActiveEntity = createScalarElement('IsActiveEntity', 'cds.Boolean', true, true, false);
addBoolAnnotationTo('@UI.Hidden', true, isActiveEntity);
addElement(isActiveEntity, artifact);
// HasActiveEntity : Boolean default false
let hasActiveEntity = createScalarElement('HasActiveEntity', 'cds.Boolean', false, false);
let hasActiveEntity = createScalarElement('HasActiveEntity', 'cds.Boolean', false, false, true);
addBoolAnnotationTo('@UI.Hidden', true, hasActiveEntity);
addElement(hasActiveEntity, artifact);
// HasDraftEntity : Boolean default false;
let hasDraftEntity = createScalarElement('HasDraftEntity', 'cds.Boolean', false, false);
let hasDraftEntity = createScalarElement('HasDraftEntity', 'cds.Boolean', false, false, true);
addBoolAnnotationTo('@UI.Hidden', true, hasDraftEntity);
addElement(hasDraftEntity, artifact);

@@ -486,2 +611,3 @@

addBoolAnnotationTo('@odata.contained', true, draftAdministrativeData);
addBoolAnnotationTo('@UI.Hidden', true, draftAdministrativeData);
addElement(draftAdministrativeData, artifact);

@@ -494,2 +620,3 @@ // Note that we need to do the ODATA transformation steps for managed associations

addBoolAnnotationTo('@odata.contained', true, foreignKeyElement);
addBoolAnnotationTo('@UI.Hidden', true, foreignKeyElement);
}

@@ -525,7 +652,2 @@ // SiblingEntity : Association to one <artifact> on (... IsActiveEntity unequal, all other key fields equal ...)

// Make all non-key elements nullable
if (elem.notNull && elem.notNull.val===true && !(elem.key && elem.key.val)) {
elem.notNull.val = false;
}
// Draft-enable the targets of composition elements (draft nodes), too

@@ -662,5 +784,5 @@ if (elem.target && elem._finalType.type && elem._finalType.type._artifact.name.absolute === 'cds.Composition') {

* Mark elements that are annotated with @odata.on.insert/update with the annotation @Core.Computed.
*
*
* Implements: CDXCORE-62
*
*
* @param {any} node The node to check

@@ -678,3 +800,3 @@ */

//
//
/**

@@ -686,3 +808,3 @@ * Mark association whose target has @cds.odata.valuelist with @Common.ValueList.viaAssociation

*
* Do this only if the association is navigable and the enclosing artifact is
* Do this only if the association is navigable and the enclosing artifact is
* a service member (don't pollute the CSN with unnecessary annotations).

@@ -692,4 +814,2 @@ *

*
* @param {any} artifact The parent artifact
* @param {any} node The node to check
*/

@@ -706,51 +826,2 @@ function addCommonValueListviaAssociation(member) {

// Rename shorthand annotations within artifact or element 'node' according to a builtin
// list.
function renameShorthandAnnotations(node) {
// FIXME: Verify this list - are they all still required? Do we need any more?
const mappings = {
'@label': '@Common.Label',
'@title': '@Common.Label',
'@description': '@Core.Description',
'@ValueList.entity': '@Common.ValueList.entity',
'@ValueList.type': '@Common.ValueList.type',
'@Capabilities.Deletable': '@Capabilities.DeleteRestrictions.Deletable',
'@Capabilities.Insertable': '@Capabilities.InsertRestrictions.Insertable',
'@Capabilities.Updatable': '@Capabilities.UpdateRestrictions.Updatable',
}
for (let name in node) {
// Rename according to map above
if (mappings[name] != undefined) {
renameAnnotation(node, name, mappings[name]);
}
// Special case: '@important: [true|false]' becomes '@UI.Importance: [#High|#Low]'
if (name == '@important') {
renameAnnotation(node, name, '@UI.Importance');
let annotation = node['@UI.Importance'];
annotation.literal = 'enum';
annotation.symbol = {
// Note that an original '@important' without ': true' shows up as undefined value here!!
id: (annotation.val == undefined || annotation.val == true) ? 'High' : 'Low',
};
// FIXME: Strangely, enum-valued annotations have no value
delete annotation.val;
}
// Special case: '@readonly' becomes a triplet of capability restrictions for entities,
// but '@Core.Immutable' for everything else.
if (name == '@readonly') {
if (node.kind == 'entity' || node.kind == 'view') {
addBoolAnnotationTo('@Capabilities.DeleteRestrictions.Deletable', false, node);
addBoolAnnotationTo('@Capabilities.InsertRestrictions.Insertable', false, node);
addBoolAnnotationTo('@Capabilities.UpdateRestrictions.Updatable', false, node);
}
else {
renameAnnotation(node, name, '@Core.Computed');
}
}
}
}
// Return an array of non-abstract service names contained in (augmented or compacted) 'model''

@@ -757,0 +828,0 @@ function getServiceNames(model) {

'use strict';
const alerts = require('../base/alerts');
const { CompilationError, hasErrors } = require('../base/messages');
const { handleMessages } = require('../base/messages');
const { setProp } = require('../base/model');
const transformUtils = require('./transformUtilsNew');
const { mergeOptions, copyAnnotations, getElementDatabaseNameOf } = require('../model/modelUtils');
const { getUtils, cloneCsn, forEachDefinition, forEachMemberRecursively } = require('../model/csnUtils');
const { isNewCSN } = require('../json/csnVersion');
const { getUtils, cloneCsn, forEachDefinition, forEachMemberRecursively, forEachRef } = require('../model/csnUtils');
const { checkCSNVersion } = require('../json/csnVersion');
// Transformation for ODATA. Expects a CSN 'inputModel', processes it for ODATA.

@@ -21,2 +20,3 @@ // The twin of forOdata.js

// TODO: probably only for OData V2?
// (1.1.a) prepare for flattening of foreign keys - pre-resolve foreign key references
// (1.2) Expose (named or anonymous) structured types used in structured types

@@ -36,3 +36,4 @@ // (1.3) Unravel derived types for elements, actions, action parameters, types and

// (3.1) Generate foreign key fields for managed associations, depends on (1.1) in V4 or flatten case
// (3.2) Flatten on-conditions in unmanaged associations
// (3.2) Flatten/normalize on-conditions in unmanaged associations
// (3.3) Check that each service has max one draft root
// Step (3.1) is in the third walk of the model

@@ -61,10 +62,3 @@ //

let csn = cloneCsn(inputModel);
const { error, warning, signal } = alerts(csn);
// the new transformer works only with new CSN
if (!isNewCSN(inputModel, options)) {
signal(error`OData transformer does not support the provided CSN version.`);
throw new CompilationError(csn.messages, csn);
}

@@ -74,2 +68,7 @@ options = mergeOptions(inputModel.options, options);

const { error, warning, info, signal } = alerts(csn, options);
// the new transformer works only with new CSN
checkCSNVersion(csn, options);
const {

@@ -87,3 +86,5 @@ flattenForeignKeys, createForeignKeyElement,

recurseElements,
} = transformUtils.getTransformers(csn, '_');
setAnnotation,
renameAnnotation
} = transformUtils.getTransformers(csn, options, '_');

@@ -97,12 +98,13 @@ const {

getServiceName,
isBuiltinType,
hasBoolAnnotation,
isAssocOrComposition,
isAssociation,
isBuiltinType,
isManagedAssociationElement,
isStructured,
hasBoolAnnotation,
isManagedAssociationElement,
inspectRef
} = getUtils(csn);
// are we working with structured OData or not
const structuredOData = options.betaModeOdata && options.toOdata.version === 'v4';
const structuredOData = options.toOdata.odataFormat === 'structured' && options.toOdata.version === 'v4';

@@ -117,3 +119,3 @@ // exposed struct types (see function exposeStructType)

// (0) Semantic checks before flattening regarding temporal data
// (0) Semantic checks before flattening regarding temporal data and array of
forEachDefinition(csn, (artifact, artifactName, propertyName, path) => {

@@ -129,3 +131,3 @@ // Gather all element names with @cds.valid.from/to/key

// Check that @cds.valid.from/to/key is only in valid places
validFrom.forEach(obj => checkAssignment('@cds.valid.from', obj.element, obj.path, artifact, artifactName));
validFrom.forEach(obj => checkAssignment('@cds.valid.from', obj.element, obj.path, artifact));
validTo.forEach(obj => checkAssignment('@cds.valid.to', obj.element, obj.path, artifact));

@@ -141,16 +143,98 @@ validKey.forEach(obj => checkAssignment('@cds.valid.key', obj.element, obj.path, artifact));

// First walk:
// (1.1) Flatten structs
// (1.2) Expose (named or anonymous) structured types used in structured types
// (1.3) Unravel derived type chains,
// (1.4) Mark fields with @odata.on.insert/update as @Core.Computed
// (1.5) Resolve annotation shorthands,
// (1.6) Check @cds... annotations
// (1.1) Unravel derived type chains to final one for elements, actions, action parameters (propagating annotations)
forEachDefinition(csn, (def, defName) => {
forEachMemberRecursively(def, (member) => {
if (!member.elements && !member.items) // structured does not have final base type and arrays are covered on the next line
toFinalBaseType(member);
if(member.type && member.type.ref && member.elements) // type of was already expanded and we can delete the reference to the type
delete member.type;
toFinalBaseType(member.items);
toFinalBaseType(member.returns);
toFinalBaseType(member.returns && member.returns.items);
}, ['definitions', defName]);
});
// (1.1) Unravel derived type chains for types and annotations (propagating annotations)
forEachDefinition(csn, (def) => {
if (def.kind != 'entity') {
if (!isStructured(getFinalTypeDef(def)))
toFinalBaseType(def);
toFinalBaseType(def.items);
toFinalBaseType(def.returns);
toFinalBaseType(def.returns && def.returns.items);
}
// If the definition('def' variable) is a type definition and the assigned type of this very same definition('def' variable)
// is structured type, e.g.:
//
// type Struct1 {
// a : Integer;
// b : Integer;
// };
// type Struct2: Struct1;
// after compilation the csn looks like this:
// ...
// "S.Struct1": {
// "kind": "type",
// "elements": {
// "a": { "type": "cds.Integer" },
// "b": { "type": "cds.Integer" }
// } },
// "S.Struct2": {
// "kind": "type",
// "type": "S.Struct1",
// "elements": {
// "a": { "type": "cds.Integer" },
// "b": { "type": "cds.Integer" }
// } } ...
//
// "S.Struct2" should looks just like "S.Struct1" => the "type": "S.Struct1" property has to be removed
if (def.kind === 'type' && def.type && !isBuiltinType(def.type) && !def.type.ref) {
// elements are already there -> do not show the type
delete def.type;
}
});
// (1.1.a) resolve all foreign keys - used later in the flattening
let refMapIsStructured = {}; // map of references containing boolean flag for each reference-item if structured of not
if(!structuredOData) {
forEachDefinition(csn, (def, defName) => {
forEachMemberRecursively(def, (member, memberName, propertyName, path) => {
if(member.keys) {
for(let k in member.keys) {
let ipath = path.concat('keys',k)
let resolved = inspectRef(ipath);
let structured = resolved.links.map(link => link.art?(isStructured(link.art)):undefined)
refMapIsStructured[ipath.join('/')] = structured;
}
}
}, ['definitions', defName])
})
}
// (1.2) Mark fields with @odata.on.insert/update as @Core.Computed
// (1.3) Resolve annotation shorthands,
// (1.4) Check @cds... annotations
// (1.5) Flatten structs
// (1.6) Expose (named or anonymous) structured types used in structured types
// TODO: only for V2 or via special option???
forEachDefinition(csn, (def, defName, propertyName, path) => {
// (1.1) Flatten structs - for entities and views only (might result in new elements)
forEachMemberRecursively(def, (member, memberName, propertyName, path) => {
// (1.2) Mark fields with @odata.on.insert/update as @Core.Computed
annotateCoreComputed(member);
// (1.3) Resolve annotation shorthands for elements, actions, action parameters
renameShorthandAnnotations(member, path);
// (1.4) check annotations
checkAnnotations(member, path);
}, ['definitions', defName]);
// (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, csn) || (elem.type && getFinalTypeDef(elem.type, csn).elements)) {
if (isStructured(elem)) {
if (structuredOData) {

@@ -176,4 +260,4 @@ if (!isArtifactInSomeService(defName, services)) return;

}
// (1.2) Expose (named or anonymous) structured types used in structured types
else if (isArtifactInSomeService(defName, services) && def.kind == 'type') {
// (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) {

@@ -186,42 +270,4 @@ let elem = def.elements[elemName];

forEachMemberRecursively(def, (member, memberName, propertyName, path) => {
// (1.3) Unravel derived type chains for elements, actions, action parameters (propagating annotations)
toFinalBaseType(member);
toFinalBaseType(member.items);
toFinalBaseType(member.returns);
toFinalBaseType(member.returns && member.returns.items);
// (1.4) Mark fields with @odata.on.insert/update as @Core.Computed
annotateCoreComputed(member);
// (1.5) Resolve annotation shorthands for elements, actions, action parameters
renameShorthandAnnotations(member);
// (1.6) check annotations
checkAnnotations(member, path);
}, ['definitions', defName]);
// (1.3) Unravel derived type chains for types and annotations (propagating annotations)
if (def.kind != 'entity') {
toFinalBaseType(def);
toFinalBaseType(def.items);
toFinalBaseType(def.returns);
toFinalBaseType(def.returns && def.returns.items);
}
// If the artifact is a derived structured type, unravel that as well
// "S.Struct2": { "kind": "type", "type": "S.Struct1",
// "elements":
// { "a": { "type": "cds.Integer" },
// "b": { "type": "cds.Integer" } }
// } where S.Struct1 is also struct type def and the elements come from there
if (def.kind == 'type' && def.type &&
!isBuiltinType(def.type) && getCsnDef(def.type).elements) {
// elements are already there -> do not show the type
delete def.type;
}
// (1.5) Resolve annotation shorthands for entities, types, annotations, ...
renameShorthandAnnotations(def);
renameShorthandAnnotations(def, path);
// (1.6) check annotations

@@ -252,5 +298,17 @@ checkAnnotations(def, ['definitions', defName]);

// (3.2) Flatten on-conditions in unmanaged associations
// (3.3) Check that each service has max one draft root
// This must be done before 4.1, since all composition targets are annotated with @odata.draft.enabled in this step
let flattenedKeyArts = Object.create(null);
forEachDefinition(csn, (def, defName) => {
flattenForeignKeysForArt(def, defName, flattenedKeyArts);
if (isArtifactInSomeService(defName, services) && ['entity', 'view'].includes(def.kind) && def['@odata.draft.enabled']) {
let serviceName = getServiceOfArtifact(defName, services);
let service = getCsnDef(serviceName);
if (service.$draftCount)
service.$draftCount++;
else
setProp(service, '$draftCount', 1);
if (service.$draftCount > 1)
signal(info`Service "${serviceName}" should not have more then one draft root artifact`, ['definitions', defName]);
}
});

@@ -287,3 +345,3 @@

if (isAssocOrComposition(elem.type)) {
checkForeignKeys(elem, elemName, defName);
checkForeignKeys(elem, elemName, defName, options);
}

@@ -304,6 +362,6 @@

forEachMemberRecursively(def, (member, memberName, propertyName, path) => {
if (isArtifactInSomeService(defName, services)) {
if (isArtifactInSomeService(defName, services) || isLocalizedArtifactInService(defName, services)) {
let service = getServiceOfArtifact(defName, services);
// (4.2) Check associations
if (isAssocOrComposition(member.type, csn)) {
if (isAssocOrComposition(member.type)) {
// Check that exposed associations do not point to non-exposed targets

@@ -314,11 +372,23 @@ checkExposedAssoc(defName, member, memberName, service);

if (def.kind === 'type' && options.toOdata.version == 'v2') {
signal(warning`"${defName}.${memberName}": Structured types must not contain associations for OData V2`);
signal(warning`"${defName}.${memberName}": Structured types must not contain associations for OData V2`, path);
}
}
// (4.3) CDXCORE-458
else if (propertyName === 'elements' && member.items && options.toOdata.version == 'v2') {
signal(error`"${defName}.${memberName}": Element must not be an "array of" for OData V2`, path);
else if (propertyName === 'elements' && member.items) {
if (options.toOdata.version == 'v2') {
signal(error`"${defName}.${memberName}": Element must not be an "array of" for OData V2`, path);
}
else if (['entity', 'view'].includes(def.kind)) {
// array of <anonymous type> is not allowed
// array of T is allowed, if T is in defining service or in namespace 'cds'
if (member.items.elements && !member.items.type) {
signal(error`"${defName}.${memberName}": Element must not be an "array of anonymous type"`, path);
}
if (member.items.type && !member.items.elements &&
!isArtifactInSomeService(member.items.type, ['cds', getServiceName(defName)])) {
signal(error`${defName}.${memberName}": Array type "${member.items.type}" is not member of service`, path);
}
}
}
// (4.4) If the member is an association and the target is annotated with @cds.odata.valuelist,

@@ -335,3 +405,3 @@ // annotate the association with @Common.ValueList.viaAssociation (but only for service member artifacts

signal(
warning`'@Analytics.Measure' expects '@Aggregation.default' to be assigned as well in element '${defName}.${memberName}'`,
info`'@Analytics.Measure' expects '@Aggregation.default' to be assigned as well in element '${defName}.${memberName}'`,
// ['definitions', defName, 'elements', memberName]

@@ -432,8 +502,8 @@ path

/**
* Check if any warnings were raised in earlier steps that need to be reclassified - i.e. as errors
*
* @param {any} csn The csn
* @returns {Array} Reclassified messages-Array
*/
/**
* Check if any warnings were raised in earlier steps that need to be reclassified - i.e. as errors
*
* @param {any} csn The csn
* @returns {Array} Reclassified messages-Array
*/
function reclassifyWarnings(csn) {

@@ -450,7 +520,9 @@ return csn.messages.map(message => {

if (options.messages) reclassifyWarnings(options);
else if (csn.messages) reclassifyWarnings(csn);
// Throw exception in case of errors
if (hasErrors(csn.messages)) {
csn.messages = reclassifyWarnings(csn);
throw new CompilationError(csn.messages, csn);
}
handleMessages(csn, options);
if (options.messages) setProp(csn, 'messages', options.messages);
return csn;

@@ -471,3 +543,3 @@

// list.
function renameShorthandAnnotations(node) {
function renameShorthandAnnotations(node, path) {
// FIXME: Verify this list - are they all still required? Do we need any more?

@@ -485,2 +557,7 @@ const mappings = {

let rewriteCapabilities = true;
if(node['@readonly'] && node['@insertonly'] && ['entity', 'view'].includes(node.kind)) {
rewriteCapabilities = false;
signal(warning`"@readonly" and "@insertonly" cannot be assigned in combination`, path);
}
for (let name in node) {

@@ -500,11 +577,25 @@ // Rename according to map above

// but '@Core.Immutable' for everything else.
if (name == '@readonly') {
if (node.kind == 'entity' || node.kind == 'view') {
node['@Capabilities.DeleteRestrictions.Deletable'] = false;
node['@Capabilities.InsertRestrictions.Insertable'] = false;
node['@Capabilities.UpdateRestrictions.Updatable'] = false;
} else {
renameAnnotation(node, name, '@Core.Computed');
if(rewriteCapabilities) {
if (name == '@readonly') {
if (node.kind == 'entity' || node.kind == 'view') {
setAnnotation(node, '@Capabilities.DeleteRestrictions.Deletable', false);
setAnnotation(node, '@Capabilities.InsertRestrictions.Insertable', false);
setAnnotation(node, '@Capabilities.UpdateRestrictions.Updatable', false);
} else {
renameAnnotation(node, name, '@Core.Computed');
}
}
// @insertonly is effective on entities/queries only
else if (name == '@insertonly') {
if (node.kind == 'entity' || node.kind == 'view') {
setAnnotation(node, '@Capabilities.DeleteRestrictions.Deletable', false);
setAnnotation(node, '@Capabilities.ReadRestrictions.Readable', false);
setAnnotation(node, '@Capabilities.UpdateRestrictions.Updatable', false);
}
}
}
// Only on element level: translate @mandatory
if (name === '@mandatory' && node.kind === undefined && node['@Common.FieldControl'] === undefined) {
setAnnotation(node, '@Common.FieldControl', { '#': 'Mandatory' });
}
}

@@ -606,3 +697,4 @@ }

flattenedKeyArts[defName] = true;
forEachMemberRecursively(def, (member, memberName) => {
let rootPath =['definitions',defName]
forEachMemberRecursively(def, (member, memberName, prop, subpath) => {
// Generate foreign key elements for managed associations

@@ -613,4 +705,27 @@ if (isManagedAssociationElement(member) && !member._ignore) {

// Generate foreign key elements
for (let key of member.keys) {
let foreignKeyElement = createForeignKeyElement(member, memberName, key, def, defName);
for (let keyIndex in member.keys) {
let key = member.keys[keyIndex];
let keyPath = rootPath.concat(subpath).concat('keys', keyIndex);
{ // flatten the reference using RESOLVED references
let resolvedIsStructured = refMapIsStructured[keyPath.join('/')];
if(resolvedIsStructured) {
let ref = key.ref;
let newref = []; // new flattened reference
let previousElementIsStructured=false;
ref.forEach((iref,i) => {
if(newref == undefined) return; // already failed - skip processing
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(newref != undefined) { // flattening succeeded?
key.ref = newref;
}
}
}
let foreignKeyElement = createForeignKeyElement(member, memberName, key, def, defName, rootPath.concat(subpath).concat('keys', keyIndex));
toFinalBaseType(foreignKeyElement);

@@ -634,5 +749,8 @@ // Propagate the association's annotations to the foreign key element

// (3.2) Flatten on-conditions in unmanaged associations
if (member.type && isAssocOrComposition(member.type, csn) && member.on) {
flattenOnCond(member, memberName, def.elements);
// (3.2) Flatten/normalize on-conditions in unmanaged associations
if (member.type && isAssocOrComposition(member.type) && member.on) {
if(!structuredOData) // flat-mode
flattenOnCond(member, memberName, def.elements, defName);
else // structured-mode
normalizeOnCondForStructuredMode(member)
}

@@ -642,5 +760,21 @@ });

// The function performs normalization for on-conditions in 'structured'-mode (for odata) as follows:
// 1. removes leading $self in references
function normalizeOnCondForStructuredMode(assoc) {
if (!assoc.on) return; // nothing to do
forEachRef(assoc, (ref, node) => {
// remove leading $self when at the begining of a ref
if (ref.length > 1 && ref[0] === '$self')
node.ref.splice(0, 1);
});
}
// (4.1) Generate all that is required in ODATA for draft enablement of 'artifact' into the artifact,
// into its transitively reachable composition targets, and into the model.
// 'rootArtifact' is the root artifact where composition traversal started.
// Constraints
// Draft Root: Exactly one PK of type UUID
// Draft Node: One PK of type UUID + 0..1 PK of another type
// Draft Node: Must not be reachable from multiple draft roots
function generateDraftForOdata(artifact, artifactName, rootArtifact, visitedArtifacts) {

@@ -651,3 +785,2 @@ // Sanity check

}
// Nothing to do if already draft-enabled (composition traversal may have circles)

@@ -659,15 +792,35 @@ if ((artifact['@Common.DraftRoot.PreparationAction'] || artifact['@Common.DraftNode.PreparationAction'])

// FIXME: Current restriction: Must only have exactly one key, which is of type UUID
let keyNames = Object.keys(artifact.elements).filter(elemName => {
return artifact.elements[elemName].key && artifact.elements[elemName].key === true;
});
if (keyNames.length != 1) {
signal(warning`"${artifactName}": Ignoring annotation "@odata.draft.enabled" - currently only supported for artifacts with exactly one key of type "UUID"`, ['definitions', artifactName]);
return;
if (artifact === rootArtifact) {
// draft root
if (keyNames.length != 1) {
signal(info`"${artifactName}": "@odata.draft.enabled" - Draft root should have exactly one key element`, ['definitions', artifactName]);
} else {
let keyElem = artifact.elements[keyNames[0]];
if (keyElem.type != 'cds.UUID' /* && keyElem._finalType.name.$renamed != 'cds.UUID' */) {
signal(info`"${artifactName}": "@odata.draft.enabled" - Draft root key element should be of type "cds.UUID"`, ['definitions', artifactName, 'elements', keyNames[0]]);
}
}
} else {
// draft node
if (keyNames.length < 1) {
signal(info`"${artifactName}": "@odata.draft.enabled" - Draft node should have at least one key element`, ['definitions', artifactName]);
} else if (keyNames.length > 2) {
signal(info`"${artifactName}": "@odata.draft.enabled" - Draft node should not have more than two key elements`, ['definitions', artifactName]);
} else {
let uuidCount = 0;
keyNames.forEach(kn => {
let keyElem = artifact.elements[kn];
if (keyElem.type == 'cds.UUID' /* && keyElem._finalType.name.$renamed != 'cds.UUID' */) {
uuidCount++;
}
});
if (uuidCount != 1) {
signal(info`"${artifactName}": "@odata.draft.enabled" - Draft node should have one key element of type "cds.UUID"`, ['definitions', artifactName]);
}
}
}
let keyElem = artifact.elements[keyNames[0]];
if (keyElem.type != 'cds.UUID' /* && keyElem._finalType.name.$renamed != 'cds.UUID' */) {
signal(warning`"${artifactName}": Ignoring annotation "@odata.draft.enabled" - currently only supported for key of type "UUID"`);
return;
}

@@ -694,14 +847,24 @@ // Generate the DraftAdministrativeData projection into the service, unless there is already one

for (let elemName in artifact.elements) {
let elem = artifact.elements[elemName];
// Make all non-key elements nullable
if (elem.notNull && elem.key !== true) {
delete elem.notNull;
}
}
// Generate the additional elements into the draft-enabled artifact
// key IsActiveEntity : Boolean default true
let isActiveEntity = createScalarElement('IsActiveEntity', 'cds.Boolean', true, true);
let isActiveEntity = createScalarElement('IsActiveEntity', 'cds.Boolean', true, true, false);
isActiveEntity.IsActiveEntity['@UI.Hidden'] = true;
addElement(isActiveEntity, artifact);
// HasActiveEntity : Boolean default false
let hasActiveEntity = createScalarElement('HasActiveEntity', 'cds.Boolean', false, false);
let hasActiveEntity = createScalarElement('HasActiveEntity', 'cds.Boolean', false, false, true);
hasActiveEntity.HasActiveEntity['@UI.Hidden'] = true;
addElement(hasActiveEntity, artifact);
// HasDraftEntity : Boolean default false;
let hasDraftEntity = createScalarElement('HasDraftEntity', 'cds.Boolean', false, false);
let hasDraftEntity = createScalarElement('HasDraftEntity', 'cds.Boolean', false, false, true);
hasDraftEntity.HasDraftEntity['@UI.Hidden'] = true;
addElement(hasDraftEntity, artifact);

@@ -714,2 +877,3 @@

draftAdministrativeData.DraftAdministrativeData['@odata.contained'] = true;
draftAdministrativeData.DraftAdministrativeData['@UI.Hidden'] = true;
addElement(draftAdministrativeData, artifact);

@@ -723,4 +887,4 @@

uuidDraftKey = uuidDraftKey[0]; // filter returns an array, but it has only one element
let foreignKeyElement = createForeignKeyElement(draftAdministrativeData.DraftAdministrativeData, 'DraftAdministrativeData', uuidDraftKey, artifact, artifactName);
foreignKeyElement['@odata.contained'] = true;
let path = ['definitions', artifactName, 'elements', 'DraftAdministrativeData', 'keys', 0];
createForeignKeyElement(draftAdministrativeData.DraftAdministrativeData, 'DraftAdministrativeData', uuidDraftKey, artifact, artifactName, path);
}

@@ -746,7 +910,2 @@ // SiblingEntity : Association to one <artifact> on (... IsActiveEntity unequal, all other key fields equal ...)

// Make all non-key elements nullable
if (elem.notNull && elem.key !== true) {
elem.notNull = false;
}
// Draft-enable the targets of composition elements (draft nodes), too

@@ -756,2 +915,3 @@ // TODO rewrite

let draftNode = csn.definitions[elem.target];
// Ignore if that is our own draft root

@@ -802,3 +962,3 @@ if (draftNode != rootArtifact) {

// Common.ValueList in the final EDM.
// Do this only if the association is navigable and the enclosing artifact is
// Do this only if the association is navigable and the enclosing artifact is
// a service member (don't pollute the CSN with unnecessary annotations).

@@ -808,4 +968,4 @@ // TODO: test???

let vlAnno = '@Common.ValueList.viaAssociation';
if (isAssociation(member.type, csn)) {
let navigable = member['@odata.navigable']!==false; // navigable disabled only if explicitly set to false
if (isAssociation(member.type)) {
let navigable = member['@odata.navigable'] !== false; // navigable disabled only if explicitly set to false
let targetDef = getCsnDef(member.target);

@@ -830,19 +990,2 @@ if (navigable && targetDef['@cds.odata.valuelist'] && !member[vlAnno]) {

// Rename annotation 'fromName' in 'node' to 'toName' (both names including '@')
function renameAnnotation(node, fromName, toName) {
let annotation = node && node[fromName];
// Sanity checks
if (!fromName.startsWith('@')) {
throw Error('Annotation name should start with "@": ' + fromName);
}
if (!toName.startsWith('@')) {
throw Error('Annotation name should start with "@": ' + toName);
}
if (annotation == undefined) {
throw Error('Annotation ' + fromName + ' not found in ' + JSON.stringify(node));
}
delete node[fromName];
node[toName] = annotation;
}
// some model utilities => TODO: move them to separate file

@@ -853,2 +996,7 @@ function isArtifactInSomeService(artName, services) {

function isLocalizedArtifactInService(artName, services) {
if (!artName.startsWith('localized.')) return false;
return isArtifactInSomeService(artName.split('.').slice(1).join('.'), services);
}
function getServiceOfArtifact(artName, services) {

@@ -855,0 +1003,0 @@ return services.find(serviceName => artName.startsWith(`${serviceName}.`));

@@ -70,3 +70,3 @@ const { setProp, forEachDefinition, forEachGeneric, forEachMemberRecursively } = require('../base/model');

// then check its elements, if some of the elements:
// -> is an association - set a '_swaggerTarget' to the association with a value of the corresponding
// -> is an association - set a '_swaggerTarget' to the association with a value of the corresponding
// projection(on the target of the assoc in the underlying context) from the current service

@@ -73,0 +73,0 @@ // or throw an error if the target is not exposed in the current service

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

}
if(element.type._artifact.name.absolute == 'cds.Decimal' && element.precision === undefined && model.options.precision) {
element.precision = { literal: 'number', val: model.options.precision }
}
if(element.type._artifact.name.absolute == 'cds.Decimal' && element.scale === undefined && model.options.scale) {
element.scale = { literal: 'number', val: model.options.scale }
}
}

@@ -95,3 +89,3 @@ }

$inferred: 'flatten',
viaTransform: true
$viaTransform: true
},

@@ -125,3 +119,3 @@ kind: 'key',

element: flatElemName,
viaTransform: true
$viaTransform: true
},

@@ -149,3 +143,3 @@ kind: 'key',

// Note that this must happen after struct flattening (because it assumes that the
// element names it encounters are relative to 'artifact').
// element names it encounters are relative to 'artifact').
// Return the newly generated foreign key element.

@@ -180,3 +174,3 @@ function createForeignKeyElement(assoc, foreignKey, artifact) {

$inferred: 'flatten',
viaTransform: true
$viaTransform: true
},

@@ -223,3 +217,3 @@ kind: 'element',

element: valueForeignKeyElementName,
viaTransform: true, // FIXME: Do we still need this?
$viaTransform: true, // FIXME: Do we still need this?
}

@@ -293,3 +287,4 @@ // FIXME: Remove once the compactor no longer renders 'origin'

// type properties 'key', 'notNull', 'virtual', 'masked' to the flattened elements.
function flattenStructuredElement(elem) {
function flattenStructuredElement(elem, ipath=[]) {
let path=ipath.concat(elem.name.id)
// Sanity check

@@ -305,3 +300,3 @@ if (!isStructuredElement(elem)) {

// Descend recursively into structured children
let grandChildElems = flattenStructuredElement(childElem);
let grandChildElems = flattenStructuredElement(childElem, path);
for (let grandChildName in grandChildElems) {

@@ -316,3 +311,2 @@ let flatElemName = elem.name.id + pathDelimiter + grandChildName;

// Preserve the generated element name as it would have been with 'hdbcds' names
setProp(flatElem, '_flatElementNameWithDots', elem.name.id + '.' + grandChildName);
result[flatElemName] = flatElem;

@@ -326,3 +320,3 @@ }

flatElem.name.$inferred = 'flatten';
flatElem.viaTransform = true; // FIXME: This name is not ideal but used elsewhere, too)
flatElem.$viaTransform = true; // FIXME: This name is not ideal but used elsewhere, too)
setProp(flatElem, '_finalType', childElem._finalType);

@@ -336,3 +330,3 @@ setProp(flatElem, '_main', elem._main);

// Preserve the generated element name as it would have been with 'hdbcds' names
setProp(flatElem, '_flatElementNameWithDots', elem.name.id + '.' + childName);
setProp(flatElem, '_flatElementNameWithDots', path.concat(childName).join("."));
result[flatElemName] = flatElem;

@@ -368,3 +362,3 @@ }

element : flatElemName,
viaTransform: true,
$viaTransform: true,
}

@@ -385,3 +379,3 @@ // Just extend 'elem's path by one step, leaving all IDs as they are (will be fixed later by flattenStructStepsInPath)

// fused together into one step, using '_' (so that the path fits again for flattened
// structs), e.g.
// structs), e.g.
// [ (Entity), (struct1), (struct2), (assoc), (elem) ] should result in

@@ -426,3 +420,3 @@ // [ (Entity), (struct1_struct2_assoc), (elem) ]

result = result.slice(1);
}
}
return result;

@@ -466,3 +460,3 @@ }

location: node.type.location,
viaTransform: true,
$viaTransform: true,
};

@@ -704,3 +698,3 @@ // Make sure all propagated type properties are taken as non-inferred

// Add a default value 'defaultVal' if supplied
function createScalarElement(elemName, typeName, isKey, defaultVal) {
function createScalarElement(elemName, typeName, isKey, defaultVal, notNull=false) {
let type = model.definitions[typeName];

@@ -736,2 +730,5 @@ if (!type) {

}
if(notNull) {
elem.notNull = { val: true };
}
return elem;

@@ -981,5 +978,4 @@ }

* If the element has annotation @cds.valid.from or @cds.valid.to, return it.
*
*
* @param {any} element Element to check
* @param {boolean} [to=true] Wether to check for @cds.valid to or not
* @returns {Array[]} Array of arrays, first filed has an array with the element if it has @cds.valid.from, second field if it has @cds.valid.to. Default value is [] for each field.

@@ -1006,3 +1002,3 @@ */

* - The artifact is not a view
*
*
* Signals an error, if:

@@ -1012,3 +1008,3 @@ * - The element is structured

* - Has an element as _parent.kind
*
*
* @param {any} annoName Annotation name

@@ -1031,3 +1027,3 @@ * @param {any} element Element to check

* Signals an error/warning if an annotation has been assigned more than once
*
*
* @param {any} array Array of elements that have the annotation

@@ -1034,0 +1030,0 @@ * @param {any} annoName Name of the annotation

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

const { dfilter } = require('./udict');
const { getUtils, cloneCsn } = require('../model/csnUtils');
const { cloneCsn, forEachRef, getUtils } = require('../model/csnUtils');

@@ -21,11 +21,11 @@ // Return the public functions of this module, with 'model' captured in a closure (for definitions, options etc).

// TODO: check the situation with assocs with values. In compacted CSN such elements have only "@Core.Computed": true
function getTransformers(model, pathDelimiter = '_') {
const { error, warning, signal } = alerts(model);
function getTransformers(model, options, pathDelimiter = '_') {
const { error, warning, signal } = alerts(model, options);
const {
getCsnDef,
getFinalBaseType,
hasBoolAnnotation,
inspectRef,
isStructured,
isBuiltinType,
hasBoolAnnotation,
getFinalBaseType,
inspectRef
isBuiltinType
} = getUtils(model);

@@ -43,2 +43,3 @@

toFinalBaseType,
copyTypeProperties,
isAssociationOperand,

@@ -62,2 +63,4 @@ isDollarSelfOperand,

recurseElements,
renameAnnotation,
setAnnotation,
};

@@ -69,3 +72,3 @@

function addDefaultTypeFacets(element) {
if(!element || !element.type)
if (!element || !element.type)
return;

@@ -75,7 +78,9 @@

element.length = model.options && model.options.length ? model.options.length : 5000;
if (!(model.options && model.options.length))
setProp(element, "$default", true);
}
if(element.type === 'cds.Decimal' && element.precision === undefined && model.options.precision) {
if (element.type === 'cds.Decimal' && element.precision === undefined && model.options.precision) {
element.precision = model.options.precision;
}
if(element.type === 'cds.Decimal' && element.scale === undefined && model.options.scale) {
if (element.type === 'cds.Decimal' && element.scale === undefined && model.options.scale) {
element.scale = model.options.scale;

@@ -89,3 +94,3 @@ }

// (1) replace all foreign keys that are managed associations themselves by
// their respective foreign keys, recursively, with names flattened using
// their respective foreign keys, recursively, with names flattened using
// pathDelimiter between path components.

@@ -111,3 +116,4 @@ // (2) replace all foreign keys that are structured with their respective flattened form.

// this is flattened elem -> keys still not flattened, have to check if starts with key ref
return assoc.keys.map(key => key.ref.join('.')).some(keyDotName => elemName.startsWith(keyDotName));
// FIXME: review why join('.')? what about join(fkSeparator)?
return assoc.keys.map(key => key.ref.join('.')).some(keyDotName => elemName.startsWith(`${keyDotName}${fkSeparator}`));
} else {

@@ -140,3 +146,3 @@ // exact match of the name

for (let keyName in targetKeys) {
if (targetKeys[keyName].viaTransform && keyName.startsWith(fKeyName))
if (targetKeys[keyName].$viaTransform && keyName.startsWith(`${fKeyName}${fkSeparator}`))
flattenedKeys.push(keyName);

@@ -172,16 +178,6 @@ }

// Assemble artificial foreign key element
let assocTargetDef = getCsnDef(assoc.target);
let fkArtifact = inspectRef(path).art;
let fkArtifact;
if(!path){
fkArtifact = assocTargetDef.elements[foreignKey.ref.join(pathDelimiter)]; // foreignKey.as ???
} else {
const {art} = inspectRef(path);
fkArtifact = art;
}
// In case of compiler errors the foreign key might be missing
if(!fkArtifact && hasErrors((model.options && model.options.messages) || model.messages)) {
if (!fkArtifact && hasErrors((model.options && model.options.messages) || model.messages)) {
return null;

@@ -273,5 +269,6 @@ }

// Sanity checks
if (foreignKey.ref.length > 1) {
throw Error('Expecting foreign key ' + foreignKey.$generatedFieldName + ' to be flattened');
}
if(options && options.toOdata && options.toOdata.odataFormat != 'structured')
if (foreignKey.ref.length > 1) {
throw Error('Expecting foreign key ' + foreignKey.$generatedFieldName + ' to be flattened');
}
if (!target) {

@@ -298,5 +295,5 @@ throw Error('Expecting target of association ' + assocName + ' to be resolved');

// @foo: true,
// elements:
// elements:
// { a: { type: 'cds.Integer' } },
// { b: {
// { b: {
// elements:

@@ -315,10 +312,12 @@ // { b1: type: 'cds.String', length: 42 } } },

// type: 'cds.String',
// length: 42 },
// length: 42 },
// }
function flattenStructuredElement(elem, elemName) {
function flattenStructuredElement(elem, elemName, ipath=[]) {
let path=ipath.concat(elemName)
// in case the element is of user defined type => take the definition of the type
// for type of 'x' -> elem.type is an object, not a string -> use directly
let elemType = getFinalBaseType(elem.type);
let elemType;
if(!elem.elements) // structures does not have final base type
elemType = getFinalBaseType(elem.type);
// if no elements => check if the element is of user defined structured type

@@ -340,3 +339,3 @@ if (!elem.elements && elem.type && !isBuiltinType(elem.type)) {

// Descend recursively into structured children
let grandChildElems = flattenStructuredElement(childElem, childName);
let grandChildElems = flattenStructuredElement(childElem, childName, path);
for (let grandChildName in grandChildElems) {

@@ -352,3 +351,2 @@ let flatElemName = elemName + pathDelimiter + grandChildName;

// Preserve the generated element name as it would have been with 'hdbcds' names
setProp(flatElem, '_flatElementNameWithDots', elemName + '.' + grandChildName);
result[flatElemName] = flatElem;

@@ -360,4 +358,4 @@ }

let flatElem = cloneCsn(childElem);
flatElem.viaTransform = true; // FIXME: This name is not ideal but used elsewhere, too)
setProp(flatElem, '_flatElementNameWithDots', elemName + '.' + childName);
setProp(flatElem, '$viaTransform', true); // FIXME: This name is not ideal but used elsewhere, too)
setProp(flatElem, '_flatElementNameWithDots', path.concat(childName).join("."));
result[flatElemName] = flatElem;

@@ -389,16 +387,11 @@ }

// Refs of length 1 cannot contain steps - no need to check
if(ref.length < 2){
return ref;
if (ref.length < 2) {
return ref;
}
try{
try {
return flatten(ref, path);
} catch(e){
if(e.message && e.message == "Scope 'ref-where' but no entity was provided."){
const main = path.slice(0, path.lastIndexOf("ref"));
const { links } = inspectRef(main);
const whereEntity = links[path[path.lastIndexOf("ref")+1]].art;
return flatten(ref, path, whereEntity.target ? getCsnDef(whereEntity.target) : whereEntity );
} catch (e) {
if (e.message && e.message == "Scope 'ref-where' but no entity was provided.") {
return flatten(ref, path);
} else {

@@ -409,12 +402,12 @@ throw e;

function flatten(ref, path, whereEntity){
function flatten(ref, path) {
let result = [];
//let stack = []; // IDs of path steps not yet processed or part of a struct traversal
const { links, scope } = inspectRef( path, whereEntity );
//let stack = []; // IDs of path steps not yet processed or part of a struct traversal
const { links, scope } = inspectRef(path);
if (scope === "$magic")
return ref;
let flattenStep = false;
links.forEach( (value, idx) => {
links.forEach((value, idx) => {
if (flattenStep)
result[result.length-1] += pathDelimiter + ref[idx];
result[result.length - 1] += pathDelimiter + ref[idx];
else {

@@ -425,4 +418,4 @@ result.push(ref[idx]);

});
// If the path starts with '$self', this is now redundant (because of flattening) and can be omitted,
// making life easier for consumers
// If the path starts with '$self', this is now redundant (because of flattening) and can be omitted,
// making life easier for consumers
if (result[0] == '$self' && result.length > 1) {

@@ -434,95 +427,53 @@ result = result.slice(1);

}
// After flattening of elements we need to flatten the on-conditions of
// unmanaged associations using those newly created elements
//
// Examples:
// Input as: assoc.on: [{ ref: ['a1', 'x'] }, '=', { ref: ['$self', 'x'] }]
// ^^^ ^^^ ^^^ ^^^
// (1) (2) (3) (4)
// (1) is the current association which is a sub element of struct elem 's' ( ... elements: { s: { a1: association to ... }})
// (2) is non-structured element in the target
// (3) is '$self' ... clearly
// (4) is non-structured element in the current scope
// The flatten on-condition is: [{ ref: ['s_a1', 'x'] }, '=', { ref: ['x'] }]
//
// Input as: assoc.on: [{ ref: ['a2', 'x'] }, '=', { ref: ['$self', 's', 'y'] }]
// ^^^ ^^^ ^^^ ^^^^^^^
// (1) (2) (3) (4)
// (1) is the current association which is a sub element of struct elem 's' ( ... elements: { s: { a2: association to ... }})
// (2) is non-structured element in the target
// (3) is '$self' ... clearly
// (4) is structured element in the current scope
// The flatten on-condition is: [{ ref: ['s_a2', 'x'] }, '=', { ref: ['s_y'] }]
//
// Input as: assoc.on: [{ ref: [{ ref: ['a3', 'x'] }, '=', { ref: ['y'] }]
// ^^^ ^^^ ^^^
// (1) (2) (3)
// (1) is the current association which is a sub element of struct elem 's' ( ... elements: { s: { a3: association to ... }})
// (2) is non-structured element in the target
// (3) element from the current nameresolution scope ( .. elements: { s : { y: ... ; a3: association to ... })
// The flatten on-condition is: [{ ref: ['s_a3', 'x'] }, '=', { ref: ['s_y'] }]
//
// Input as: assoc.on: [{ ref: ['a4', 'struct', 'k1'] }, '=', { ref: ['$self', 's', 'struct', 'k2'] }]
// ^^^ ^^^^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^
// (1) (2) (3) (4)
// (1) is the current association which is a sub element of struct elem 's' ( ... elements: { s: { a4: association to ... }})
// (2) is structured element in the target
// (3) is '$self' ... clearly
// (4) is structured element in the current scope
// The flatten on-condition is: [{ ref: ['s_a4', 'struct_k1'] }, '=', { ref: ['s_struct_k2'] }]
function flattenOnCond(assoc, assocName, defElements) {
// Flatten on conditions of unmanaged associations. This method assumes that
// other flattening of the elements was already performed and there are
// no left over structure elements marked with some ignore flag. Also, uses
// the non-enumerable property '_flatElementNameWithDots'.
function flattenOnCond(assoc, assocName, defElements, defName) {
if (!assoc.on) return; // nothing to do
let allRefs = assoc.on.filter(elem => typeof elem === 'object' && elem.ref);
let targetDef = getCsnDef(assoc.target);
for (let refObj of allRefs) {
// if reference is just '$self'
if (refObj.ref.length === 1 && refObj.ref[0] === '$self')
continue;
// remove '$self' when first elem in ref and flatten the rest if needed
if (refObj.ref[0] === '$self') {
refObj.ref.shift();
// the rest of the reference should point to an element from the current definition
let potentialFlattenElem = refObj.ref.join(pathDelimiter);
if (defElements && defElements[potentialFlattenElem] && defElements[potentialFlattenElem].viaTransform) {
refObj.ref = [potentialFlattenElem];
forEachRef(assoc, (ref, node, path) => {
// assoc itself is inside a struct -> all the first refs need to be flattened;
if (assoc.$viaTransform) {
// when the first ref id is the same association
if (assoc._flatElementNameWithDots.endsWith(`.${ref[0]}`))
ref.splice(0, 1, assocName);
// elem from the curr name resolution scope
// but not a $self, will deal with this one later
else if (ref[0] !== '$self') {
// .splice(-1, 1, ref[0]) does not work here, why???
let currStructFlatName = `${assoc._flatElementNameWithDots.split('.').slice(0, -1).join('_')}`
node.ref.splice(0, 1, `${currStructFlatName}_${ref[0]}`);
}
continue;
}
// assoc element itself was flatten
// -> every first step where current assoc is specified needs to be replaced
if (assoc.viaTransform && assocName.endsWith(`${pathDelimiter}${refObj.ref[0]}`)) {
refObj.ref.splice(0, 1, assocName);
// the rest of the reference should point to an element from the target
let potentialFlattenElem = refObj.ref.slice(1).join(pathDelimiter);
if (targetDef.elements && targetDef.elements[potentialFlattenElem] && targetDef.elements[potentialFlattenElem].viaTransform) {
refObj.ref.splice(1, refObj.ref.length - 1, potentialFlattenElem);
}
}
let flatRef = [];
let needToFlat = false;
// reference to flattened element in the target
let flattenRefName = refObj.ref.join(pathDelimiter);
if (targetDef.elements) {
// exact match of flatten element name
if (targetDef.elements[flattenRefName] && targetDef.elements[flattenRefName].viaTransform) {
refObj.ref = [flattenRefName];
continue;
}
// when element is defined in the current name resolution scope, like
// entity E {
// key x: Integer;
// s : {
// y : Integer;
// a3 : association to E on a3.x = y;
// }
// }
let potentialFlattenElem = Object.keys(targetDef.elements)
.find(elemName => elemName.endsWith(`${pathDelimiter}${flattenRefName}`));
if (potentialFlattenElem && targetDef.elements[potentialFlattenElem].viaTransform) {
refObj.ref = [potentialFlattenElem];
continue;
}
}
}
// $user.locale must not be flatten and they are not understand by inspectRef
if (ref.join('.') === '$user.locale')
return;
ref.slice().forEach(refId => {
if (needToFlat) {
let flatLastElem = `${flatRef[flatRef.length - 1]}${pathDelimiter}${refId}`;
flatRef.splice(-1, 1, flatLastElem);
needToFlat = false;
} else
flatRef.push(refId);
node.ref = flatRef;
let { art } = inspectRef(path);
// if not resolved to an art, then this element was flattened
if (!art)
needToFlat = true;
});
// remove leading $self when at the begining of a ref
if (ref.length > 1 && ref[0] === '$self')
node.ref.splice(0, 1);
}, ['definitions', defName, 'elements', assocName]);
}

@@ -536,6 +487,37 @@

if (assocDef._flatElementNameWithDots)
signal(error`Redirection for sub elements not supported yet - association "${artName}.${assocName}"`);
signal(error`Redirection for sub elements not supported yet - association "${artName}.${assocName}"`, ['definitions', artName, 'elements', assocName]);
}
}
/**
* Copy properties of the referenced type, but don't resolve to the final base type.
*
* @param {any} node Node to copy to
* @returns {void}
*/
function copyTypeProperties(node) {
// Nothing to do if no type (or if array/struct type)
if (!node || !node.type) return;
// .. or if it is a ref
if (node.type && node.type.ref) return;
// .. or builtin already
if (node.type && typeof node.type === 'string' && node.type.startsWith('cds.') && !node.type.startsWith('cds.foundation.')) return;
// The type might already be a full fledged type def (array of)
const typeDef = typeof node.type === 'string' ? getCsnDef(node.type) : node.type;
// Nothing to do if type is an array or a struct type
if (typeDef.items || typeDef.elements) return;
// if the declared element is an enum, these values are with priority
if (!node.enum && typeDef.enum)
Object.assign(node, { enum: typeDef.enum });
if (!node.length && typeDef.length && !typeDef.$default)
Object.assign(node, { length: typeDef.length });
if (!node.precision && typeDef.precision)
Object.assign(node, { precision: typeDef.precision });
if (!node.scale && typeDef.scale)
Object.assign(node, { scale: typeDef.scale });
if (!node.srid && typeDef.srid)
Object.assign(node, { srid: typeDef.srid });
}
// Replace the type of 'node' with its final base type (in contrast to the compiler,

@@ -548,3 +530,3 @@ // also unravel derived enum types, i.e. take the final base type of the enum's base type.

// In case of a ref -> Follow the ref
if(node.type && node.type.ref) {
if (node.type && node.type.ref) {
node.type = getFinalBaseType(node.type);

@@ -565,4 +547,2 @@ return;

Object.assign(node, { length: typeDef.length });
if (!node.length && typeDef.length)
Object.assign(node, { length: typeDef.length });
if (!node.precision && typeDef.precision)

@@ -698,6 +678,6 @@ Object.assign(node, { precision: typeDef.precision });

// Add a default value 'defaultVal' if supplied
// example result: { foo: { type: 'cds.Integer', key: true, default: { val: 6 } } }
// ^^^ ^^^^^^^^^ ^^^^ ^^
// elemName typeName isKey defaultVal
function createScalarElement(elemName, typeName, isKey = false, defaultVal = undefined) {
// example result: { foo: { type: 'cds.Integer', key: true, default: { val: 6 }, notNull: true } }
// ^^^ ^^^^^^^^^ ^^^^ ^^ ^^
// elemName typeName isKey defaultVal notNull
function createScalarElement(elemName, typeName, isKey = false, defaultVal = undefined, notNull=false) {
if (!isBuiltinType(typeName) && !model.definitions[typeName]) {

@@ -712,3 +692,3 @@ throw new Error('Expecting valid type name: ' + typeName);

if (isKey) {
result[elemName].key = true
result[elemName].key = true;
}

@@ -720,2 +700,5 @@ if (defaultVal !== undefined) {

}
if(notNull) {
result[elemName].notNull = true;
}
return result;

@@ -732,8 +715,8 @@ }

if (!arg.ref) {
// Not a path, hence not an association (literal, expression, function, whatever ...)
// Not a path, hence not an association (literal, expression, function, whatever ...)
return false;
}
const {art} = inspectRef(path);
const { art } = inspectRef(path);
// If it has a target, it is an association or composition
return art.target !== undefined;
return art && art.target !== undefined;
}

@@ -783,3 +766,4 @@

// TODO: check the usage of this function's param 'keyElem' ?
function createForeignKey(keyElemName/*, keyElem */) {
function createForeignKey(keyElemName, keyElem = undefined) { /* eslint-disable-line no-unused-vars */
return {

@@ -813,6 +797,6 @@ ref: [keyElemName]

* Add element 'elem' to 'artifact'
*
*
* @param {any} elem is in form: { b: { type: 'cds.String' } }
* @param {any} artifact is: { kind: 'entity', elements: { a: { type: 'cds.Integer' } ... } }
* @returns {undefined}
* @param {any} artifact is: { kind: 'entity', elements: { a: { type: 'cds.Integer' } ... } }
* @returns {void}
*/

@@ -912,3 +896,3 @@ function addElement(elem, artifact) {

* If the element has annotation @cds.valid.from or @cds.valid.to, return it.
*
*
* @param {any} element Element to check

@@ -920,10 +904,10 @@ * @param {Array} path path in CSN for error messages

let validFroms = [], validTos = [], validKeys = [];
if(hasBoolAnnotation(element, '@cds.valid.from')) {
validFroms.push({element, path: [...path]});
if (hasBoolAnnotation(element, '@cds.valid.from')) {
validFroms.push({ element, path: [...path] });
}
if(hasBoolAnnotation(element, '@cds.valid.to')) {
validTos.push({element, path: [...path]});
if (hasBoolAnnotation(element, '@cds.valid.to')) {
validTos.push({ element, path: [...path] });
}
if(hasBoolAnnotation(element, '@cds.valid.key')) {
validKeys.push({element, path: [...path]});
if (hasBoolAnnotation(element, '@cds.valid.key')) {
validKeys.push({ element, path: [...path] });
}

@@ -938,3 +922,3 @@ return [validFroms, validTos, validKeys];

* - The artifact is not a view
*
*
* Signals an error, if:

@@ -944,5 +928,6 @@ * - The element is structured

* - Has an element as _parent.kind
*
*
* @param {any} annoName Annotation name
* @param {any} elementName Name of the element to be checked
* @param {any} element Element to be checked
* @param {any[]} path
* @param {any} artifact Artifact

@@ -952,5 +937,5 @@ * @returns {Boolean} True if no errors

function checkAssignment(annoName, element, path, artifact) {
if(artifact.kind !== 'type' && !artifact.query) {
if (artifact.kind !== 'type' && !artifact.query) {
// path.length > 4 to check for structured elements
if(element.elements || element.target || path.length > 4) {
if (element.elements || element.target || path.length > 4) {
signal(error`Element cannot be annotated with "${annoName}"`, path);

@@ -965,3 +950,3 @@ return false;

* Signals an error/warning if an annotation has been assigned more than once
*
*
* @param {any} array Array of elements that have the annotation

@@ -973,5 +958,5 @@ * @param {any} annoName Name of the annotation

*/
function checkMultipleAssignments(array, annoName, artifact, artifactName, err=true) {
if(array.length > 1) {
if(err == true) {
function checkMultipleAssignments(array, annoName, artifact, artifactName, err = true) {
if (array.length > 1) {
if (err == true) {
signal(error`"${annoName}" must be assigned only once`, ['definitions', artifactName]);

@@ -986,3 +971,3 @@ } else {

* Calls `callback` for each element in `elements` property of `artifact` recursively.
*
*
* @param artifact the artifact

@@ -1006,2 +991,42 @@ * @param path path to get to `artifact` (mainly used for error messages)

}
// Rename annotation 'fromName' in 'node' to 'toName' (both names including '@')
function renameAnnotation(node, fromName, toName) {
let annotation = node && node[fromName];
// Sanity checks
if (!fromName.startsWith('@')) {
throw Error('Annotation name should start with "@": ' + fromName);
}
if (!toName.startsWith('@')) {
throw Error('Annotation name should start with "@": ' + toName);
}
if (annotation == undefined) {
throw Error('Annotation ' + fromName + ' not found in ' + JSON.stringify(node));
}
if(node[toName] === undefined || node[toName] === null) {
delete node[fromName];
node[toName] = annotation;
}
}
/**
* Assign annotation to a node but do not overwrite already existing annotation assignment
* that is (assignment is either undefined or has null value)
*
* @param {object} node Assignee
* @param {string} name Annotation name
* @param {any} value Annotation value
* @returns {void}
*/
function setAnnotation(node, name, value) {
if (!name.startsWith('@')) {
throw Error('Annotation name should start with "@": ' + name);
}
if (value === undefined) {
throw Error('Annotation value must not be undefined');
}
if(node[name] === undefined || node[name] === null)
node[name] = value;
}
}

@@ -1049,2 +1074,3 @@

module.exports = {

@@ -1051,0 +1077,0 @@ // This function retrieves the actual exports

'use strict'
const { setProp, forEachGeneric, forEachDefinition } = require('../base/model');
var { handleMessages } = require('../base/messages');
const modelUtils = require('../model/modelUtils.js');

@@ -9,3 +10,3 @@ const compactor = require('../json/compactor');

const {compactModel} = require('../json/to-csn');
const { messageString } = require('../base/messages');
// Paths that start with an artifact of protected kind are special

@@ -16,4 +17,13 @@ // either ignore them in QAT building or in path rewriting

function translateAssocsToJoinsCSN(csn, options){
let main = require('../main');
const model = main.compileSources({ '<stdin>.json' : JSON.stringify(csn, null, 2) }, options);
let { augment } = require("../json/from-csn");
let xsn = augment(csn);
let compileSources = require('../main').compileSources;
// Do not re-complain about localized
const severities = Object.assign(Object.create(options.severities || null), {'recalculated-localized': 'debug'});
const opt = Object.assign({}, options, { severities });
const model = compileSources( { '<stdin>.csn': xsn }, opt );
translateAssocsToJoins(model, options);
// Expand the * and keep the information of other columns
// as we need to remember casts etc.
forEachDefinition(model, art => {

@@ -23,25 +33,64 @@ if (art.$queries) {

if (query.columns) {
for (let col of query.columns) {
if (col.val === "*") {
// If column contains a "*" set columns to null, which forces columns to be computed from elements
query.columns = Object.values(query.elements);
break;
if(query.columns.some((col) => col.val == '*')){
const columnMap = {};
for(const eltName of Object.keys(query.elements)){
columnMap[eltName] = query.elements[eltName];
}
for(let col of query.columns){
if(col.val === '*'){
// ignore, already handled above
} else {
// Save the column to keep casts etc.
columnMap[col.name.id] = col;
}
}
query.columns = Object.values(columnMap);
}
}
// else {
// // FIXME: is this really correct?
// query.columns = Object.values(query.elements);
// }
for (let elemName in query.elements) {
delete query.elements[elemName].viaAll;
}
}
}
});
return compactModel(translateAssocsToJoins(model));
/**
* Removes duplicate messages from the given messages array without destroying
* references to the array.
* @param {object[]} messages Unsorted messages array.
*/
function removeDuplicates(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;
});
messages.length = 0;
for (let msg of uniqueMessages) {
messages.push(msg);
}
}
// Make sure that we don't complain twice about the same things
removeDuplicates( options.messages || model.messages );
// If A2J reports error - end! Continuing with a broken CSN makes no sense
handleMessages(model, options);
// FIXME: Move this somewhere more appropriate
const compact = compactModel(model);
setProp(compact, "options", csn.options);
setProp(compact, "messages", model.messages);
return compact;
}
function translateAssocsToJoins(model)
function translateAssocsToJoins(model, inputOptions = {})
{
const { error, signal } = alerts(model);
const { error, warning, signal } = alerts(model, inputOptions);
var options = model.options || {};
var options = model.options || inputOptions;

@@ -52,3 +101,3 @@ const fullJoinOption = options.forHana && options.forHana.associations == 'joins';

const pathDelimiter = (options.forHana && options.forHana.names == 'hdbcds') ? '.' : '_';
const { compactNode } = compactor.getCompactors(options);

@@ -175,3 +224,2 @@

// 2) Walk over each from table path, transform it into a join tree

@@ -224,3 +272,3 @@ env.walkover = { from:true, onCondFrom:false, select: false, filter: false };

function createLeftOuterJoins(query, env)
{
{
if(query.op.val === 'query')

@@ -332,3 +380,3 @@ {

// be rendered verbatim
if((env.location === 'OrderBy' && !pathNode.path[0]._navigation))
if((env.location === 'OrderBy' && !pathNode.path[0]._navigation))
return;

@@ -343,15 +391,3 @@

}
// iterate over the path and search for first QA, this is the table alias
let QA = undefined;
let pl = path.length-1;
let ps = path[pl--];
if(ps._navigation && ps._navigation._parent)
QA = ps._navigation._parent.$QA;
while(!QA && pl >= 0)
{
ps = path[pl--];
if(ps._navigation && ps._navigation._parent)
QA = ps._navigation._parent.$QA;
}
let [QA, ps] = rightMostQA(path);
if(QA) {

@@ -374,2 +410,18 @@ // if the found QA is the mixin QA and if the path length is one,

}
function rightMostQA(path) {
// iterate over the path and search for first QA, this is the table alias
let QA = undefined;
let pl = path.length-1;
let ps = path[pl--];
if(ps._navigation && ps._navigation._parent)
QA = ps._navigation._parent.$QA;
while(!QA && pl >= 0)
{
ps = path[pl--];
if(ps._navigation && ps._navigation._parent)
QA = ps._navigation._parent.$QA;
}
return [QA, ps];
}
}

@@ -556,4 +608,16 @@

else {
env.firstAssocPathStep = assoc.name.id;
return cloneOnCondition(assoc.onCond || assoc.on);
if(env.assocStack === undefined) {
env.assocStack = [];
env.assocStack.head = function() {
return this[this.length-1];
}
env.assocStack.id = function() {
return (this.head() && this.head().name.id);
}
}
env.assocStack.push(assoc);
let onCond = cloneOnCondition(assoc.onCond || assoc.on);
env.assocStack.pop();
return onCond;
}

@@ -586,4 +650,12 @@

{
env.firstAssocPathStep = fwdAssoc.name.id;
result.args.push(createOnCondition(fwdAssoc, tgtAlias, srcAlias));
//env.assocStack.includes(fwdAssoc) => recursion
if(env.assocStack.length == 2) {
// reuse (ugly) error message from forHana
signal(error`An association that uses "$self" in its ON-condition cannot be compared to "$self"`, env.assocStack[0].location);
// don't check these paths again
args[i].$check = args[i+2].$check = false;
}
else {
result.args.push(createOnCondition(fwdAssoc, ...swapTableAliasesForFwdAssoc(fwdAssoc, srcAlias, tgtAlias)));
}
i += 2; // skip next two tokens and continue with loop

@@ -603,33 +675,16 @@ continue;

// If this is a backlink condition, produce the
// If this is a backlink condition, produce the
// ON cond of the forward assoc with swapped src/tgt aliases
let fwdAssoc = getForwardAssociationExpr(expr);
if(fwdAssoc) {
env.firstAssocPathStep = fwdAssoc.name.id;
let newSrcAlias = tgtAlias;
let newTgtAlias = {};
// first try to identify table alias for complex views or
// redirected associations
if(fwdAssoc._redirected) {
newTgtAlias.id = fwdAssoc._redirected[fwdAssoc._redirected.length-1].$QA.name.id;
newTgtAlias._artifact = fwdAssoc._redirected[fwdAssoc._redirected.length-1]._finalType;
newTgtAlias._navigation = fwdAssoc._redirected[fwdAssoc._redirected.length-1].$QA._navigation;
if(env.assocStack.length == 2) {
// reuse (ugly) error message from forHana
signal(error`An association that uses "$self" in its ON-condition cannot be compared to "$self"`, expr.location);
// don't check these paths again
expr.args.forEach(x => x.$check = false );
return expr;
}
else {
// if target was used as part of complex query (join in FROM clause) use
// ta.QA
let ta = env.lead.$tableAliases[fwdAssoc.target._artifact.name.id];
if(ta) {
let srcQA = ta.$QA;
newTgtAlias.id = srcQA.name.id;
newTgtAlias._artifact = srcQA._artifact;
newTgtAlias._navigation = srcQA._navigation;
}
else {
// last resort, use last original srcAlias
newTgtAlias = Object.assign(newTgtAlias, srcAlias);
}
return createOnCondition(fwdAssoc, ...swapTableAliasesForFwdAssoc(fwdAssoc, srcAlias, tgtAlias));
}
let oc = createOnCondition(fwdAssoc, newSrcAlias, newTgtAlias);
return oc;
}

@@ -649,2 +704,35 @@

// The src/tgtAliases need to be swapped for ON Condition of the forward assoc.
// The correct table alias is the QA of the original target. If this target
// has been redirected, use the QA of the redirected target.
// As last resort use the source alias information.
function swapTableAliasesForFwdAssoc(fwdAssoc, srcAlias, tgtAlias) {
let newSrcAlias = tgtAlias;
let newTgtAlias = {};
// first try to identify table alias for complex views or
// redirected associations
if(fwdAssoc._redirected && fwdAssoc._redirected.length) {
newTgtAlias.id = fwdAssoc._redirected[fwdAssoc._redirected.length-1].$QA.name.id;
newTgtAlias._artifact = fwdAssoc._redirected[fwdAssoc._redirected.length-1]._finalType;
newTgtAlias._navigation = fwdAssoc._redirected[fwdAssoc._redirected.length-1].$QA._navigation;
}
else {
// if target was used as part of complex query (join in FROM clause) use
// ta.QA
let ta = env.lead.$tableAliases[fwdAssoc.target._artifact.name.id];
if(ta) {
let srcQA = ta.$QA;
newTgtAlias.id = srcQA.name.id;
newTgtAlias._artifact = srcQA._artifact;
// sometimes _navigation can be found directly in the QA object, sometimes in the first path step
newTgtAlias._navigation = srcQA._navigation || (srcQA.path && srcQA.path[0]._navigation);
}
else {
// last resort, use last original srcAlias
newTgtAlias = Object.assign(newTgtAlias, srcAlias);
}
}
return [newSrcAlias, newTgtAlias];
}
function rewritePathNode(pathNode)

@@ -658,4 +746,3 @@ {

let [head, ...tail] = path;
if(rhs.mixin)
env.firstAssocPathStep = assocQAT.name.id;
if(!checkPathDictionary(pathNode, env)) {

@@ -670,4 +757,14 @@ return pathNode;

{
if(['$projection', '$self'].includes(head.id) && tail.length)
path = env.lead.elements[tail[0].id].value.path;
if (['$projection', '$self'].includes(head.id) && tail.length) {
let value = env.lead.elements[tail[0].id].value;
/*
If the value is an expression in the select block, return the unmodified
expression. rewriteGenericPaths will check and rewrite these paths later
to the correct ON condition expression.
*/
if(!value.path)
return value;
else
path = value.path;
}

@@ -705,3 +802,3 @@ /*

/* if the $projection path has association path steps make sure to address the
/* if the $projection path has association path steps make sure to address the
element by its last table alias name. Search from the end upwards

@@ -714,3 +811,83 @@ to the top for the first association path step and cut off path here.

path = path.slice(i+1, path.length);
[ tableAlias, path ] = constructTableAliasAndTailPath(path);
/*
If the mixin is a backlink to some forward association, the forward ON condition
paths have no _navigation link to the table aliases. The challenge is to find the
correct QAs for the paths of the forward ON condition.
Example:
entity A { key id: Integer; }
entity B { key id: Integer; toV: association to V on id = toV.id; elt: String; }
view V as select from A
mixin {
toB: association to B on $self = toB.toV; // first use of 'id = toV.id'
}
into {
A.id
toB.elt
};
view V1 as select from A
mixin {
toB: association to B on $self = toB.toV; // second use of 'id = toV.id'
}
into {
A.id
toB.elt
};
Information we have:
* name of the forward association (env.assocStack)
* head has no _navigation
* the forward association's target side is this view
=> For all paths on the target side, we have to find the appropriate $tableAlias
path._artifact is reference into view.elements, the value of the select item
is the path in the select list. The first path step is linked into $tableAliases via
_navigation
* the forward association's source side is the target of the mixin (the assocQAT.QA)
=> easy: assocQAT is _navigation
* If a $self is used multiple times, the forward ON cond paths are resolved to
the original target (in the example above against V). However, we cannot lookup
the _navigation link by following the _artifact.value.path[0] as this would always
lead to V.query[0].$tableAliases.$A. Instead we need to lookup the element in the
combined list of elements made available by the from clause.
*/
let _navigation = undefined; // don't modify original path
if(head._navigation === undefined) {
if(head.id === env.assocStack.id()) {
// source side from view point of view (target side from forward point of view)
path = tail; // pop assoc step
let elt = env.lead.$combined[path[0].id];
let err = 'Element "' + path[0].id +
'" referred in association "' + assoc.name.id +'" of Artifact "' + assoc.name.absolute +'"';
if(elt) {
if(Array.isArray(elt)) {
err += ' is available from multiple query sources ' +
elt.map(e => '"' + e.origin._artifact.name.absolute + '"').join(', ');
signal(error`${err}`, assocQAT.origin._artifact.location);
return pathNode.path;
} else {
// check if element has same origin on both ends
if(elt.origin._artifact._main !== path[0]._artifact.origin._artifact._main) {
err += ' originates from "' +
path[0]._artifact.origin._artifact._main.name.absolute+'" and from "' +
elt.origin._artifact._main.name.absolute +
'" in "' + elt._main.name.absolute + '"';
signal(warning`${err}`, assocQAT.origin._artifact.location);
}
_navigation = elt._parent;
}
} else {
err += ' has not been found';
signal(error`${err}`, assocQAT.origin._artifact.location);
return pathNode.path;
}
} else {
// target side from view point of view (source side from forward point of view)
//if(assocQAT.$QA._artifact === path[0]._artifact._parent)
_navigation = assocQAT;
}
}
[ tableAlias, path ] = constructTableAliasAndTailPath(path, _navigation);
}

@@ -882,4 +1059,4 @@ else // ON condition of non-mixin association

}
return paths.map(p => {
return { id: (prefix ? prefix + pathDelimiter : '' ) + p.id, _artifact: p._artifact }
return paths.map(p => {
return { id: (prefix ? prefix + pathDelimiter : '' ) + p.id, _artifact: p._artifact }
} );

@@ -892,9 +1069,12 @@ }

*/
function constructTableAliasAndTailPath(path)
function constructTableAliasAndTailPath(path, navigation=undefined)
{
let [head, ...tail] = path;
let QA = head._navigation.$QA || head._navigation._parent.$QA;
if(navigation === undefined)
navigation = head._navigation;
let QA = navigation.$QA || navigation._parent.$QA;
// First path step is table alias, use and pop it off
if(head._navigation.$QA)
if(navigation.$QA && tail.length > 0)
path = tail;

@@ -955,2 +1135,7 @@

/**
* @param {any} assocStep
* @param {any[]} path
* @param {string} [pathStr='']
*/
function substituteFKAliasForPath(assocStep, path, pathStr='')

@@ -960,2 +1145,3 @@ {

let ppt = assocStep._artifact.$fkPathPrefixTree.children;
/** @type any */
let fk = undefined; // last found FK

@@ -1002,2 +1188,3 @@ let fkPs = undefined; // last path step that found FK

3) Managed associations are only allowed to access a foreign key element
4) $self, $projection without suffix should not appear here anymore
Returns true on success, false otherwise

@@ -1009,3 +1196,3 @@ */

return pathDict.$check;
pathDict.$check = true;

@@ -1026,4 +1213,4 @@ // all leaf types must be scalar in a query

}
let headIsAssoc = head.id == env.firstAssocPathStep;
// assocStack eventually undefined => head is not Assoc
let headIsAssoc = head.id == (env.assocStack && env.assocStack.id());
let lead = env.art || env.lead;

@@ -1074,3 +1261,3 @@ path.forEach((ps, idx) => {

}
/*

@@ -1365,7 +1552,7 @@ Create path prefix trees and merge paths into the trees depending on the path location.

* The final _artifact ref is set as _artifact ref to the path
*
* @param {Objects[]} pathSteps Array of [ 'pathStep id', _artifact reference, namedArgs (optional) ]
* @param {any} alias Alias to set as the name property -> { id: <alias> }
*
* @param {Object[]} pathSteps Array of [ 'pathStep id', _artifact reference, namedArgs (optional) ]
* @param {any} [alias] Alias to set as the name property -> { id: <alias> }
* @param {boolean} [rewritten=true] If true, mark the objects with $rewritten
* @returns {Object} CSN path
* @returns {object} CSN path
*/

@@ -1489,3 +1676,3 @@ function constructPathNode(pathSteps, alias, rewritten=true)

// Ask for Array before typeof object (which would also be true for Array)
if(Array.isArray(node))
if(Array.isArray(node))
node.map(n => walk(n, env));

@@ -1522,3 +1709,3 @@ // instanceof Object doesn't respect dictionaries...

If the filter becomes JOIN relevant, default FILTERS (part of the
If the filter becomes JOIN relevant, default FILTERS (part of the
association definition) MUST be CLONED to each assoc path step

@@ -1525,0 +1712,0 @@ BEFORE resolution.

@@ -96,4 +96,4 @@ /**

* in obj.
* @param {Array} obj
* @param {function} callback
* @param {Array} obj
* @param {(value: string, index: number, array: string[]) => boolean} callback
*/

@@ -176,2 +176,2 @@ function dsome(obj, callback) {

forEachProp
}
}
{
"name": "@sap/cds-compiler",
"version": "1.19.2",
"lockfileVersion": 1,
"requires": true,
"version": "1.23.2",
"dependencies": {
"antlr4": {
"version": "4.7.1",
"resolved": "http://nexus.wdf.sap.corp:8081/nexus/repository/build.releases.npm/antlr4/-/antlr4-4.7.1.tgz",
"integrity": "sha512-haHyTW7Y9joE5MVs37P2lNYfU2RWBLfcRDD8OWldcdZm5TiCE91B5Xl1oWSwiDUSd4rlExpt2pu1fksYQjRBYQ=="
"version": "4.7.1"
},
"resolve": {
"version": "1.8.1",
"resolved": "http://nexus.wdf.sap.corp:8081/nexus/repository/build.releases.npm/resolve/-/resolve-1.8.1.tgz",
"integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
"requires": {
"path-parse": "1.0.5"
},
"dependencies": {
"path-parse": {
"version": "1.0.5",
"resolved": "http://nexus.wdf.sap.corp:8081/nexus/repository/build.releases.npm/path-parse/-/path-parse-1.0.5.tgz",
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME="
"version": "1.0.5"
}

@@ -28,7 +17,5 @@ }

"sax": {
"version": "1.2.4",
"resolved": "http://nexus.wdf.sap.corp:8081/nexus/repository/build.releases.npm/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
"version": "1.2.4"
}
}
}

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

{"bin":{"cdsc":"bin/cdsc.js","cdshi":"bin/cdshi.js","cdsse":"bin/cdsse.js"},"bundleDependencies":false,"dependencies":{"antlr4":"4.7.1","resolve":"1.8.1","sax":"1.2.4"},"deprecated":false,"description":"CDS (Core Data Services) compiler and backends","keywords":["CDS"],"main":"lib/main.js","name":"@sap/cds-compiler","version":"1.19.2","license":"SEE LICENSE IN developer-license-3.1.txt"}
{"bin":{"cdsc":"bin/cdsc.js","cdshi":"bin/cdshi.js","cdsse":"bin/cdsse.js"},"bundleDependencies":false,"dependencies":{"antlr4":"4.7.1","resolve":"1.8.1","sax":"1.2.4"},"deprecated":false,"description":"CDS (Core Data Services) compiler and backends","keywords":["CDS"],"main":"lib/main.js","name":"@sap/cds-compiler","version":"1.23.2","license":"SEE LICENSE IN developer-license-3.1.txt"}

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is 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 not supported yet

SocketSocket SOC 2 Logo

Product

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

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc