Socket
Socket
Sign inDemoInstall

@sap/cds-compiler

Package Overview
Dependencies
Maintainers
1
Versions
106
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sap/cds-compiler - npm Package Compare versions

Comparing version 1.50.2 to 2.1.4

bin/cdsv2m.js

3

bin/cds_update_identifiers.js

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

// Users should use the compiler to get all messages.
const errors = options.messages.filter(msg => (msg.severity === 'Error'));
const errors = options.messages
.filter(msg => (msg.severity === 'Error' && msg.messageId !== 'syntax-deprecated-ident'));
if (errors.length > 0) {

@@ -63,0 +64,0 @@ errors.forEach((msg) => {

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

const compiler = require('../lib/main');
const compiler = require('../lib/compiler');
const main = require('../lib/main');
const { compactModel } = require('../lib/json/to-csn');
const { toOdataWithCsn, toHanaWithCsn, toSqlWithCsn, toCdlWithCsn } = require('../lib/backends');
const { toOdataWithCsn, toHanaWithCsn, toSqlWithCsn, toCdlWithCsn, toRenameWithCsn, alterConstraintsWithCsn } = require('../lib/backends');
var util = require('util');
var fs = require('fs');
var path = require('path');
var reveal = require('../lib/model/revealInternalProperties');
var { reveal } = require('../lib/model/revealInternalProperties');
const enrichCsn = require('../lib/model/enrichCsn');
const { optionProcessor } = require('../lib/optionProcessor');
const { translatePathLocations, explainMessage, hasMessageExplanation } = require('../lib/base/messages')
const { translateAssocsToJoins } = require('../lib/transform/translateAssocsToJoins');
const { explainMessage, hasMessageExplanation, sortMessages } = require('../lib/base/messages')
const term = require('../lib/utils/term');
const nodeHelper = require('../lib/base/node-helpers');
const { splitLines } = require('../lib/utils/file');
const { addLocalizationViews } = require('../lib/transform/localized');

@@ -49,3 +50,3 @@ // Note: Instead of throwing ProcessExitError, we would rather just call process.exit(exitCode),

if (cmdLine.options.version) {
process.stdout.write(compiler.version() + '\n');
process.stdout.write(main.version() + '\n');
throw new ProcessExitError(0);

@@ -94,2 +95,5 @@ }

if (cmdLine.options.rawOutput)
cmdLine.options.attachValidNames = true;
// Internally, parseCdl is an option so we map the command to it.

@@ -118,19 +122,16 @@ if (cmdLine.command === 'parseCdl') {

cmdLine.options.beta = {
'toSwagger': true,
'foreignKeyConstraints': true,
'toRename': true,
'subElemRedirections': true,
'addTextsLanguageAssoc': true,
'technicalConfig': true,
'keyRefError': true,
'odataProxies': true,
'assocsWithParams': true,
'cleanCsn': false,
'hanaAssocRealCardinality': true,
'originalKeysForTemporal': true,
'odataDefaultValues': true,
'mapAssocToJoinCardinality': true,
'dontRenderVirtualElements': true,
'ignoreAssocPublishingInUnion': true
'ignoreAssocPublishingInUnion': true,
}
}
if (cmdLine.options.deprecated) {
const features = cmdLine.options.deprecated.split(',');
cmdLine.options.deprecated = {};
features.forEach((val) => cmdLine.options.deprecated[val] = true);
}
// Do the work for the selected command

@@ -159,3 +160,3 @@ executeCommandLine(cmdLine.command, cmdLine.options, cmdLine.args);

if (!['toCdl', 'toOdata', 'toHana', 'toCsn', 'toSql'].includes(command)) {
displayUsage(`Option '--direct-backend' cannot be used with command '${command}'`,
displayUsage(`Option '--direct-backend' can't be used with command '${command}'`,
optionProcessor.helpText, 2);

@@ -209,4 +210,4 @@ }

toRename,
manageConstraints,
toSql,
toSwagger
};

@@ -226,6 +227,8 @@ const commandsWithoutCompilation = {

options.messages = [];
const fileCache = Object.create(null)
const compiled = options.directBackend ?
nodeHelper.readFile( args.files[0] ).then((str) => JSON.parse( str )) :
compiler.compile( args.files, undefined, options, fileCache );
compiler.compileX( args.files, undefined, options, fileCache );

@@ -241,10 +244,4 @@ compiled.then( commands[command] )

function toCdl( model ) {
let cdlResult;
if(options.oldTransformers){
cdlResult = compiler.toCdl(model);
} else {
const csn = options.directBackend ? model : compactModel(model, options);
csn.messages = model.messages; // pass messages as compactModel misses that
cdlResult = toCdlWithCsn(csn, options).result
}
const csn = options.directBackend ? model : compactModel(model, options);
const cdlResult = toCdlWithCsn(csn, options).result;
for (const name in cdlResult) {

@@ -256,3 +253,3 @@ writeToFileOrDisplay(options.out, name + '.cds', cdlResult[name]);

// Execute the command line option '--to-csn' and display the results.
// Execute the command line option 'toCsn' and display the results.
// Return the original model (for chaining)

@@ -272,15 +269,9 @@ function toCsn( model ) {

function toHana( model ) {
let hanaResult;
if(options.oldTransformers) {
hanaResult = compiler.toHana(model);
} else {
let csn = options.directBackend ? model : compactModel(model, options);
csn.messages = model.messages; // pass messages as compactModel misses that
hanaResult = toHanaWithCsn(csn, options)
const csn = options.directBackend ? model : compactModel(model, options);
const hanaResult = toHanaWithCsn(csn, options);
for (const name in hanaResult.hdbcds) {
const fileName = `${name}.hdbcds`;
writeToFileOrDisplay(options.out, fileName, hanaResult.hdbcds[name]);
}
for (let name in hanaResult.hdbcds) {
writeToFileOrDisplay(options.out, name + '.hdbcds', hanaResult.hdbcds[name]);
}
displayNamedXsnOrCsn(hanaResult._augmentedCsn, hanaResult.csn, 'hana_csn', options);
model.messages = hanaResult.messages;
displayNamedCsn(hanaResult.csn, 'hana_csn', options);
return model;

@@ -292,4 +283,3 @@ }

function toOdata( model ) {
let odataResult;
if(options.toOdata &&
if(options.toOdata &&
options.toOdata.version === 'v4x') {

@@ -300,10 +290,4 @@ options.toOdata.version = 'v4';

}
if(options.oldTransformers) {
odataResult = compiler.toOdata(model)
} else {
let csn = options.directBackend ? model : compactModel(model, options);
csn.messages = model.messages; // pass messages as compactModel misses that
odataResult = toOdataWithCsn(csn, options);
}
translatePathLocations(model.messages, model);
let csn = options.directBackend ? model : compactModel(model, options);
const odataResult = toOdataWithCsn(csn, options);
for (let serviceName in odataResult.services) {

@@ -327,3 +311,3 @@ // <service>_metadata.xml (metadata)

}
displayNamedXsnOrCsn(odataResult._augmentedCsn, odataResult.csn, 'odata_csn', options);
displayNamedCsn(odataResult.csn, 'odata_csn', options);
return model;

@@ -338,4 +322,5 @@ }

function toRename( model ) {
let renameResult = compiler.toRename(model);
let storedProcedure = 'PROCEDURE RENAME_' + model.options.toRename.names.toUpperCase() + '_TO_PLAIN LANGUAGE SQLSCRIPT AS BEGIN\n';
let csn = options.directBackend ? model : compactModel(model, options);
let renameResult = toRenameWithCsn(csn, options);
let storedProcedure = 'PROCEDURE RENAME_' + renameResult.options.toRename.names.toUpperCase() + '_TO_PLAIN LANGUAGE SQLSCRIPT AS BEGIN\n';
for (let name in renameResult.rename) {

@@ -346,18 +331,27 @@ storedProcedure += ' --\n -- ' + name + '\n --\n';

storedProcedure += "END;\n";
writeToFileOrDisplay(options.out, 'storedProcedure_' + model.options.toRename.names + '_to_plain.sql', storedProcedure, true);
writeToFileOrDisplay(options.out, 'storedProcedure_' + renameResult.options.toRename.names + '_to_plain.sql', storedProcedure, true);
return model;
}
// Execute the command line option 'manageConstraints' and display the results.
function manageConstraints( model ) {
const csn = options.directBackend ? model : compactModel(model, options);
const alterConstraintsResult = alterConstraintsWithCsn(csn, options);
const { src } = options.manageConstraints || {};
Object.keys(alterConstraintsResult).forEach((id) => {
const renderedConstraintStatement = alterConstraintsResult[id];
if(src === 'hdi')
writeToFileOrDisplay(options.out, `${id}.hdbconstraint`, renderedConstraintStatement);
else
writeToFileOrDisplay(options.out, `${id}.sql`, renderedConstraintStatement);
})
}
// Execute the command line option '--to-sql' and display the results.
// Return the original model (for chaining)
function toSql( model ) {
let sqlResult;
if (options.oldTransformers) {
sqlResult = compiler.toSql(model) ;
} else {
const csn = options.directBackend ? model : compactModel(model, options);
sqlResult = toSqlWithCsn(csn, options);
}
const csn = options.directBackend ? model : compactModel(model, options);
const sqlResult = toSqlWithCsn(csn, options);
['hdbtabletype', 'hdbtable', 'hdbindex', 'hdbfulltextindex', 'hdbview', 'sql'].forEach(pluginName => {
['hdbtabletype', 'hdbtable', 'hdbindex', 'hdbfulltextindex', 'hdbview', 'hdbconstraint', 'sql'].forEach(pluginName => {
for(let name in sqlResult[pluginName]) {

@@ -367,20 +361,6 @@ writeToFileOrDisplay(options.out, name + '.' + pluginName, sqlResult[pluginName][name] + '\n', true);

});
displayNamedXsnOrCsn(sqlResult._augmentedCsn, sqlResult.csn, 'sql_csn', options);
model.messages = sqlResult.messages;
displayNamedCsn(sqlResult.csn, 'sql_csn', options);
return model;
}
// Execute the command line option '--to-swagger' and display the results.
// Return the original model (for chaining)
function toSwagger( model ) {
let swaggerResult = compiler.toSwagger(model);
for (let serviceName in swaggerResult.services || {}) {
writeToFileOrDisplay(options.out, `${serviceName}_swagger.json`, swaggerResult.services[serviceName], false);
}
if (swaggerResult._augmentedCsn) {
displayNamedXsn(swaggerResult._augmentedCsn, 'swagger_csn', options);
}
return model;
}
function explain() {

@@ -401,3 +381,3 @@ if (args.length !== 1)

function displayErrors (err) {
if (err instanceof compiler.CompilationError) {
if (err instanceof main.CompilationError) {
if (options.rawOutput)

@@ -423,3 +403,9 @@ console.error( util.inspect( reveal( err.model, options.rawOutput ), false, null ));

function displayMessages( model, messages = model.messages ) {
/**
* Print the model's messages to stderr in a human readable way.
*
* @param {CSN.Model | XSN.Model} model
* @param {CSN.Message[]} messages
*/
function displayMessages( model, messages = options.messages ) {
if (!Array.isArray(messages))

@@ -430,2 +416,4 @@ return model;

sortMessages(messages);
if (options.internalMsg) {

@@ -437,3 +425,3 @@ messages.map(msg => util.inspect( msg, { depth: null, maxArrayLength: null} ) )

messages.filter(msg => (messageLevels[ msg.severity ] <= options.warning))
.forEach(msg => log(compiler.messageString(msg, normalizeFilename, !options.showMessageId)));
.forEach(msg => log(main.messageString(msg, normalizeFilename, !options.showMessageId)));
}

@@ -450,7 +438,9 @@ else {

messages.filter(msg => messageLevels[ msg.severity ] <= options.warning).forEach(msg => {
hasAtLeastOneExplanation = hasAtLeastOneExplanation || compiler.hasMessageExplanation(msg.messageId)
const name = msg.location && msg.location.filename;
hasAtLeastOneExplanation = hasAtLeastOneExplanation || main.hasMessageExplanation(msg.messageId)
const name = msg.location && msg.location.file;
const fullFilePath = name ? path.resolve('', name) : undefined;
log(compiler.messageStringMultiline(msg, normalizeFilename, !options.showMessageId));
log(compiler.messageContext(sourceLines(fullFilePath), msg));
const context = fullFilePath && sourceLines(fullFilePath);
log(main.messageStringMultiline(msg, { normalizeFilename, noMessageId: !options.showMessageId, withLineSpacer: !!context, hintExplanation: true }));
if (context)
log(main.messageContext(context, msg));
log() // newline

@@ -465,16 +455,2 @@ });

/**
* @param {XSN.Model} xsn
* @param {CSN.Model} csn
* @param {string} name
* @param {CSN.Options} options
*/
function displayNamedXsnOrCsn(xsn, csn, name, options) {
if(xsn && options.rawOutput) {
displayNamedXsn(xsn, name, options);
} else if (csn) {
displayNamedCsn(csn, name, options);
}
}
// Write the model 'model' to file '<name>.{json|raw.txt}' in directory 'options.out',

@@ -486,9 +462,3 @@ // or display it to stdout if 'options.out' is '-'.

if (options.rawOutput) {
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);
}
writeToFileOrDisplay(options.out, name + '_raw.txt', util.inspect(reveal(xsn, options.rawOutput), false, null), true);
}

@@ -499,3 +469,5 @@ else if (options.internalMsg) {

else if (!options.lintMode) {
let csn = compiler.toCsn(xsn, options);
let csn = compactModel(xsn, options);
if (command === 'toCsn' && options.toCsn && options.toCsn.withLocalized)
addLocalizationViews(csn, options);
if (options.enrichCsn)

@@ -513,4 +485,6 @@ enrichCsn( csn, options );

function displayNamedCsn(csn, name, options) {
if (!csn) // only print CSN if it is set.
return;
if (options.internalMsg) {
writeToFileOrDisplay(options.out, name + '_raw.txt', csn.messages, true);
writeToFileOrDisplay(options.out, name + '_raw.txt', options.messages, true);
}

@@ -527,6 +501,11 @@ else if (!options.lintMode && !options.internalMsg) {

// For filenames, illegal characters (slash, backslash, colon) are replaced by '_'.
function writeToFileOrDisplay(dir, filename, content, omitHeadline = false) {
function writeToFileOrDisplay(dir, fileName, content, omitHeadline = false) {
if (options.lintMode && !options.rawOutput || options.internalMsg)
return;
filename = filename.replace(/[:/\\]/g, '_');
fileName = fileName.replace(/[:/\\]/g, '_');
// replace all dots with underscore to get deployable .hdbcds sources (except the one before the file extension)
if(options.toHana)
fileName = fileName.replace(/\.(?=.*?\.)/g, '_')
if (!(content instanceof String || typeof content === 'string')) {

@@ -537,3 +516,3 @@ content = JSON.stringify(content, null, 2);

if (!omitHeadline) {
process.stdout.write(`// ------------------- ${filename} -------------------\n`);
process.stdout.write(`// ------------------- ${fileName} -------------------\n`);
}

@@ -546,3 +525,3 @@ process.stdout.write(`${content}\n`);

// TODO: We might consider using async file-system API ...
fs.writeFileSync(path.join(dir, filename), content);
fs.writeFileSync(path.join(dir, fileName), content);
}

@@ -549,0 +528,0 @@ }

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

const compiler = require('../lib/main');
const compiler = require('../lib/compiler');
const fs = require('fs');

@@ -29,3 +29,3 @@ fs.readFile( '/dev/stdin', 'utf8', highlight );

}
let ts = compiler.parse( buf, 'hi.cds', { attachTokens: true } ).tokenStream;
let ts = compiler.parseX( buf, 'hi.cds', { attachTokens: true, messages: [] } ).tokenStream;
if (!buf.length || !ts.tokens || !ts.tokens.length)

@@ -32,0 +32,0 @@ return;

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

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

@@ -42,3 +43,3 @@

if (err)
console.error( 'ERROR:', err.toString() );
console.error( 'ERROR:', err );
console.error( 'Usage: cdsse <cmd> <line> <col> <file> [-]' );

@@ -59,5 +60,6 @@ console.error( '----------- supported commands <cmd>:' );

return usage();
let hasId = false;
if (off.prefix !== off.cursor) { // with keyword/name prefix
// tokensAt( buf, off.cursor, false ); // list symbolAtCursor
off.hasId = tokensAt( buf, off.prefix, false );
hasId = tokensAt( buf, off.prefix, off.col, false );
}

@@ -69,9 +71,9 @@ else {

// gen/languageParser, calculate "symbol continuation"
tokensAt( buf, off.prefix-1, charBefore );
off.hasId = tokensAt( buf, off.prefix, true );
tokensAt( buf, off.prefix-1, off.col-1, charBefore );
hasId = tokensAt( buf, off.prefix, off.col, true );
}
if (off.hasId) {
if (hasId) {
let src = buf.substring( 0, off.prefix ) + '__NO_SUCH_ID__' + buf.substring( off.cursor );
let fname = path.resolve( '', file );
compiler.compile( [file], '', { attachValidNames: true, lintMode: true, beta: true } , { [fname]: src } )
compiler.compileX( [file], '', { attachValidNames: true, lintMode: true, beta: true, messages: [] } , { [fname]: src } )
.then( ident, ident );

@@ -82,5 +84,5 @@ }

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

@@ -99,3 +101,3 @@ // if we want to avoid that the editor switches to fuzzy completion match

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

@@ -134,11 +136,12 @@ return true;

/** @type {XSN.WithLocation} */
const { location } = node;
if (!location || node.$inferred || location.$weak)
if (!location || node.$inferred || !location.endLine) // weak location
return false;
if (location.filename !== frel)
if (location.file !== frel)
return false;
if (location.start.line > line || location.start.line === line && location.start.column > column)
if (location.line > line || location.line === line && location.col > 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 (location.endLine < line || location.endLine === line && location.endCol < column) {
if (!node.id)

@@ -167,3 +170,3 @@ return false;

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

@@ -173,7 +176,7 @@ return true;

function display( xsnOrErr ) {
let messages = xsnOrErr.messages || xsnOrErr.errors;
let messages = xsnOrErr.messages || xsnOrErr.options && xsnOrErr.options.messages;
if (!messages)
return usage( xsnOrErr );
for (let msg of messages)
console.log( compiler.messageString( msg ) );
console.log( main.messageString( msg ) );
return true;

@@ -183,5 +186,5 @@ }

function tokensAt( buf, offset, symbol ) {
function tokensAt( buf, offset, col, symbol ) {
let src = buf.substring( 0, offset ) + '≠' + buf.substring( offset );
let et = messageAt( compiler.parse( src, frel ), 'expectedTokens', offset ) || [];
let et = messageAt( compiler.parseX( src, frel, { messages: [] } ), 'expectedTokens', col ) || [];
for (let n of et) {

@@ -196,5 +199,7 @@ if (typeof symbol === 'string') {

}
else if (/^[A-Z]+$/.test( n )) {
else if (/^[A-Z_]+$/.test( n )) {
console.log( n.toLowerCase(), 'keyword' );
}
else
console.log( n, 'unknown' );
}

@@ -204,5 +209,5 @@ return et.includes( 'Identifier' );

function messageAt( model, prop, offset ) {
let msg = (model.messages||model.errors).find(
m => m[prop] && m.location.start.offset === offset && m.location.filename === frel );
function messageAt( model, prop, col ) {
let msg = (model.messages || model.options && model.options.messages).find(
m => m[prop] && m.location.line === line && m.location.col === col && m.location.file === frel );
return msg && msg[prop];

@@ -214,3 +219,3 @@ }

* @param {string} buf
* @returns {false | { cursor: number, prefix: number, hasId?: boolean }} Returns false if 'line' is out-of-range.
* @returns {false | { cursor: number, prefix: number, col: number }} Returns false if 'line' is out-of-range.
*/

@@ -224,5 +229,5 @@ function offset( buf ) { // for line and column

}
let cursor = pos + column - 1;
let endsId = /[a-z_0-9$]*$/i.exec( buf.substring( pos, cursor ) );
return { cursor, prefix: endsId.index + pos };
const cursor = pos + column - 1;
const prefix = /[a-z_0-9$]*$/i.exec( buf.substring( pos, cursor ) ).index + pos;
return { cursor, prefix, col: column + prefix - cursor };
}

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

# ChangeLog for cdx compiler and backends
# ChangeLog for cds compiler and backends

@@ -9,2 +9,226 @@ <!-- markdownlint-disable MD024 -->

## Version 2.1.4 - 2021-03-31
### Fixed
- The postinstall step now never fails with an exit code != 0. As the postinstall step is optional, it should not break any `npm install` steps.
## Version 2.1.2 - 2021-03-29
### Fixed
- ensure `postinstall` script is part of the shipped `package.json`
## Version 2.1.0 - 2021-03-26
### Added
- Inferred sub elements of a referred structure type can be individually annotated.
- All primitive types except for binary are now allowed as enum value types.
- Allow users to define `A.B` even if there is a definition `A` which is not a context or service.
- You can now provide almost all annotation assignments without delimited identifiers:
the use of `.`, `@` and `#` is fine for annotation names,
property names of structures, and in references used as annotation values.
- for.odata:
+ All the artifacts that have localized fields get a `$localized: true` property.
+ Allow the user to define draft actions for annotation purposes
* `draftPrepare(SideEffectsQualifier: String) returns <ET>`,
* `draftActivate() returns <ET>`,
* `draftEdit(PreserveChanges: Boolean) returns <ET>`
- to.edm(x):
+ Warn about non-applicable annotations.
+ Render property default values (only OData V4).
+ Option `odataProxies` exposes association targets outside of the current service.
These `EntityType`s do only expose their primary keys have no accompanying `EntitySet`.
The added navigation targets are exposed under their namespace or if not available under namespace `root`.
`odataProxies` is only available with `--format=structured`.
+ Option `odataXServiceRefs` renders an `edm:Reference` to the service for those navigation targets
that are defined in another service. `odataXServiceRefs` is only available with `--format=structured`.
+ Duplicate EntityContainer entries with same name will raise an error.
+ `array of` elements are now allowed for OData V2, too.
- to.sql/hdi/hdbcds: Explicitly render the implicit alias for functions without arguments, e.g. `current_date`.
- to.sql:
+ Sort the SQL statements according to the deployment order.
+ New sql dialect `plain`, which now is the default.
synchronously.
- API:
+ `compileSync()` is now compatible to `compile()`:
the function can also receive a file cache and will resolve all `using`s
+ New API functions `parse.cql` (prefer it to deprecated `parseToCqn`) and
`parse.expr` (prefer it to deprecated `parseToExpr`)
+ function `getArtifactCdsPersistenceName` now accepts a CSN as a third parameter (used to be a namespace). With a CSN provided,
the name can be correctly constructed for naming modes `quoted` and `hdbcds`. Without a CSN, the name is possibly wrong
if it contains dots. If the CSN is not provided or the third parameter is not a CSN, the old, deprecated, implementation is used.
- `cdsc` and other client tools:
+ Added `--with-localized` to the command `toCsn` which adds convenience views for localized entities to the output.
+ A script `bin/cds_update_identifiers.js` was added. You can use it to update the delimited identifier style in your CDS sources.
+ A script `bin/cdscv2m.js` was added.
It's command `ria` adds `@cds.redirection.target: false` annotate statements
for all ambiguous redirection errors.
- Added `deprecated` options; setting any of them disables all `beta` options.
### Changed
- CSN representation:
+ CSN Version is set to `2.0`
+ CSN `definitions` are not sorted anymore
+ `$syntax` is non-enumerable
+ increase the use of JS numbers in the CSN for numbers in CDL, especially noticable in annotation values
+ Annotation definitions are to be found in the top-level property `vocabularies`.
+ Introduce `kind: 'aspect'` to replace `kind: 'type', $syntax: 'aspect'` and
`kind: 'entity', abstract: true` (the deprecated variants are still accepted as input).
+ Projections are rendered via `projection` instead of `query.SELECT`.
+ Parentheses are represented structurally and unnecessary parentheses are omitted.
+ Use `.` instead of `_` for the name suffix of generated texts entities and the calculated entity for managed compositions.
+ The CSN returned by `compile()` does not include localized convenience views anymore.
- Core engine (function `compile`):
+ An assignment `@Foo.Bar` is always `@Foo.Bar`, we do not try to search anymore
for a local definition of `Foo` probably having a different full name.
+ Localized convenience views are no longer generated by the core compiler but added by the `for.odata`
and `to.sql/hdi/hdbcds` processing on demand.
+ Minimize name clashes when calculating names for autoexposed entities,
extends the v1 option `dependentAutoexposed` to sub artifacts of entites (see “Added”).
+ Ambiguities when redirecting associations now always lead to compile errors;
you might want to use the new annotation `@cds.redirection.target` to solve them.
+ The association `up_` in the calculated entity for managed compositions is now managed.
_Limitation_: Nested managed compositions are not activatable via `to.hdbcds --names=hdbcds`.
+ Bound actions and functions are no longer propagated from the main query source to the resulting view or projection.
+ Remove annotation `@cds.autoexpose` from generated `.texts` entity
+ Require `order by` references to start with a table alias when referring to source elements.
+ Infer the type of a `select` item from the type of a top-level `cast`.
- Localized convenience views now also contain `masked` elements of the original artifact.
- for.odata:
+ Even with `--format: structured`, (flat) foreign keys for managed associations are generated.
+ An `entity` or an `aspect` defined outside the current service cannot be used as action parameter or return types.
+ Structured elements are expanded in-place.
+ Foreign keys for managed associations are created in-place.
- to.edm(x):
+ An `Edm.TypeDefinition` is rendered for a derived scalar type and used as type reference instead of
rendering the final scalar type, including the `array of`/`many` predicates.
+ `enum` type definition as service member is rendered as `edm:TypeDefinition` instead of `edm:EnumType`.
+ Set default source cardinality of compositions to exact one. This is observable in V2 EDM only.
+ Key must not be `nullable=true`, this includes all sub elements of used structured types.
+ Default values are no longer propagated from the principal to the generated foreign key element.
+ `array of array` is rejected, nested Collections `Collection(Collection(...))` are illegal.
+ Temporal rendering:
* `@cds.valid.from` is not `Edm.KeyRef` anymore.
* `@cds.valid.key` is rendered as `@Core.AlternateKeys`.
+ Downgrade message "`<Term>` is not applied" from warning to info.
+ Update Vocabularies 'Aggregation', 'Capabilities', 'Core', 'Validation'.
- to.sql/to.hdi/to.hdbcds:
+ Reject using associations or compositions in query elements starting with `$self` or `$projection`.
+ Virtual elements are not rendered.
+ Structured elements are expanded in-place.
+ Foreign keys for managed associations are created in-place.
+ Implicit/CDL-style casts are not rendered as SQL CASTs.
+ All association usages in queries are always translated into JOIN expressions
(except for to.hdbcds `--names=hdbcds`).
- to.sql/to.hdi:
+ Downgrade message `to-many-no-on` from error to warning.
+ Default values are no longer propagated from the principal to the generated foreign key element.
- to.sql:
+ Changed type mappings for `--dialect=sqlite`:
* `cds.Date` -> `DATE_TEXT`
* `cds.Time` -> `TIME_TEXT`
* `cds.Timestamp` -> `TIMESTAMP_TEXT`
* `cds.DateTime` -> `TIMESTAMP_TEXT`
* `cds.Binary` -> `BINARY_BLOB`
* `cds.hana.Binary` -> `BINARY_BLOB`
+ Don't check missing type facets.
- to.hdbcds:
+ References to derived, primitive types are replaced by their final type.
The derived type definitions are not rendered anymore for hdbcds naming mode.
+ Don't check missing type facets in views.
- to.cdl:
+ Render maximum cardinality as 'to one' or 'to many'.
+ Return at most two files. The first one (named `model.cds`) contains all definitions, simply rendered in order,
without namespaces or usings. Contexts and services are NOT nested. The second file (named `<namespace>.cds`)
represents the CSN `namespace` property, simply defining such a namespace and requiring the first file.
- API changes:
+ The API functions `compile()` and `compileSync()` return a CSN and not an XSN,
`compactModel()` returns the first argument.
+ If `options` does not provide a `messages` property, all messages are printed to standard error.
+ The `options.messages` is kept throughout the compiler and contains all messages from the compiler and all backends.
+ Messages are not sorted anymore; use the API function `sortMessages` to have it sorted.
### Removed
- Core engine (function `compile`):
+ Referential integrity issues now always lead to compile errors.
+ The `type of` operator (without `:` in the reference) cannot be used
for parameters and inside queries anymore.
+ Using `"…"` for delimited identifiers leads to a compile error.
+ Issue an error for “smart artifact references”, i.e.
when using `Definition.elem` instead of `Definition:elem`
+ The definition of annotations is no longer allowed in `context`s and `service`s.
+ Providing an alias name without `as` leads to a compile error or warning.
+ Providing unexpected kind of definitions for `type` or other references leads to a compile error.
+ The ancient CSN 0.1.0 format generation has been removed.
+ The compiler does no longer look for modules whose file extension is `.csn.json`,
both `.csn` and `.json` is still checked.
- for.odata:
+ With `--format: structured`, the property `$generatedFieldName` in keys of
managed associations has been removed.
+ Artificially exposed types that are required to make a service self contained are
removed from the OData processed CSN.
+ Localized convenience views are no longer part of the OData CSN.
- API changes:
+ The deprecated XSN based transformers `forHana`, `forOdata`, `toSwagger`, `toSql`, `toCsn`, `toCdl`
have now been removed from the code base.
+ Remove `collectSources()` as well as `options.collectSources`.
+ A `CompilationError` usually does not have the property `model` anymore,
to avoid potential memory issues.
+ CSN compiler messages no longer have a `location` property. Use `$location` instead.
- The following `cdsc` options have been removed:
+ `--old-transformers`.
+ `--hana-flavor` with all corresponding rudimentarily implemented language constructs.
+ `--new-resolve` (the new resolver is now the default).
### Fixed
- Core engine (function `compile`):
+ Managed composition in sub elements are now properly redirected,
even if the sub structure comes from a referred type.
+ Do not dump with sub queries in the `on` condition of `join`s.
+ Properly report that managed aspect composition inside types and as sub elements
are not supported yet.
+ Make sure that including elements with managed aspect compositions only
use the provided target aspect, but not the generated target entity.
+ Properly handle the extra keywords in the third argument of the HANA SQL function `round`.
- to.edm(x):
+ Return all warnings to the user.
+ Don't render references and annotations for unexposed associations.
+ Rendering of `@Validation.AllowedValue` for elements of type enum annotated with `@assert.range`:
* Add `@Core.Description`, if the enum symbol has a `@Core.Description`, `@description` or document comments.
+ Primary key aliases are now the path basenames, colliding aliases are numbered.
+ Fix a bug in constraint calculation if principal has no primary keys.
+ Illegal OData identifiers which are not exposed in the generated edmx schema are not causing errors anymore.
+ Improve non-enum value handling on term definitions based on an enum type by raising a warning and rendering
the value with appropriate scalar EDM type.
+ Render annotion qualifier in JSON format.
- to.sql/hdi/hdbcds:
+ Reject structured view parameters for HANA.
+ Types are not rendered anymore for HANA in quoted mode.
+ Structured elements in subqueries are now properly expanded.
+ Actions, functions, annotations and events do not have DB specific checks run on them, as
they will not be part of the resulting artifacts anyways
+ With `--names=quoted` or `hdbcds`, some `.` in artifact names are turned into `_`.
In general, this happens when part of the name prefix is "shadowed" by a non-context/service;
any `.` after that point is turned into `_`. This change also affects the filenames and the
`@cds.persistence.name` annotation in the CSN returned by `to.hdi.migration` and `for.odata`.
- to.sql/hdi:
+ Fixed a bug which led to an exception if elements were referenced as types.
+ For the SQLite dialect, date, time and timestamp are rendered as simple string literals instead of function calls.
+ For naming mode "plain", date, time and timestamps are rendered as SQL-compliant literals.
- to.sql/hdbcds: Fix issue which led to wrong ON conditions for naming mode `hdbcds`.
- to.sql:
+ SRID of SAP HANA spatial type (`ST_POINT` & `ST_GEOMETRY`) is not rendered as the length of `CHAR`
for SQL-dialects other than `hana`. The resulting `CHAR` has a default length of 2000.
- to.hdbcds:
+ Nullability constraints on view parameters are not rendered anymore.
+ CDS and HANA CDS types inside cast expressions are mapped to their SQL-counterparts, as the CDS types can't be used in a cast.
- to.cdl: Correctly render `event` typed as `projection`.
- to.hdi.migration: Don't generate `ALTER` for type change from association to composition or vice versa (if the rest stays the same),
as the resulting SQL is identical.
## Version 1.50.2 - 2021-03-19

@@ -108,3 +332,3 @@

## Version 1.46.4 - 2020-11-26
## Version 1.46.4 - 2020-11-28

@@ -111,0 +335,0 @@ ### Fixed

@@ -10,2 +10,28 @@ # ChangeLog of Beta Features for cdx compiler and backends

## Version 2.0.8
### Added `foreignKeyConstraints`
to.sql/to.hdi: If the beta option `foreignKeyConstraints` is supplied, referential constraints are generated for compliant associations and compositions.
## Version 2.0.2
### Removed `dontRenderVirtualElements`
Virtual elements are no longer rendered in views as `null as <id>` or added to potentially generated
draft tables. This behavior can be turned off with deprecated option `renderVirtualElements` for backward compatibility.
### Removed `originalKeysForTemporal`
### Removed `odataDefaultValues`
OData: Default values for EntityType properties are rendered always.
### Removed `subElemRedirections`
This option is now enabled by default.
### Removed `keyRefError`
## Version 1.44.0

@@ -12,0 +38,0 @@

@@ -28,3 +28,8 @@ {

}]
},
"settings": {
"jsdoc": {
"mode": "typescript"
}
}
}

@@ -8,7 +8,4 @@ /** @module API */

const { setProp, isBetaEnabled } = require('../base/model');
const alerts = require('../base/alerts');
const { emptyLocation } = require('../base/location');
const {
sortMessages, messageString, CompilationError,
} = require('../base/messages');
const { CompilationError, makeMessageFunction, deduplicateMessages } = require('../base/messages');
const { compactModel } = require('../json/to-csn');

@@ -18,3 +15,17 @@ const { transform4odataWithCsn } = require('../transform/forOdataNew.js');

const { compareModels } = require('../modelCompare/compare');
const sortViews = require('../model/sortViews');
const { getResultingName } = require('../model/csnUtils');
/**
* Return the artifact name for use for the hdbresult object
* So that it stays compatible with v1 .texts
*
* @param {string} artifactName Name to map
* @param {CSN.Model} csn SQL transformed model
* @returns {string} Name with . replaced as _ in some places
*/
function getFileName(artifactName, csn) {
return getResultingName(csn, 'quoted', artifactName);
}
const propertyToCheck = {

@@ -66,11 +77,2 @@ odata: 'toOdata',

/**
* Sort and log the given messages to stderr.
*
* @param {string[]} messages Messages to display
*/
function displayMessages(messages = []) {
sortMessages(messages).forEach(m => process.stderr.write(`${ messageString(m, false, true) }\n`));
}
/**
* Check the characteristics of the provided, already transformed CSN

@@ -83,5 +85,6 @@ * Report an error if they do not match with the currently requested options

* @param {string[]} relevantOptionNames Option names that are defining characteristics
* @param {string[]} [warnAboutMismatch=[]] Option names to warn about, but not error on
* @param {string[]} warnAboutMismatch Option names to warn about, but not error on
* @param {string} module Name of the module that calls this function, e.g. `for.odata`
*/
function checkPreTransformedCsn(csn, options, relevantOptionNames, warnAboutMismatch) {
function checkPreTransformedCsn(csn, options, relevantOptionNames, warnAboutMismatch, module) {
if (!csn.meta) {

@@ -91,16 +94,12 @@ // Not able to check

}
const { error, warning, throwWithError } = alerts.makeMessageFunction(csn, options);
const { error, warning, throwWithError } = makeMessageFunction(csn, options, module);
for (const name of relevantOptionNames ) {
if (options[name] !== csn.meta.options[name]) {
error('wrong-pretransformed-csn', null,
`Expected pre-processed CSN to have option "${ name }" set to "${ options[name] }". Found: "${ csn.meta.options[name] }".`);
}
if (options[name] !== csn.meta.options[name])
error('wrong-pretransformed-csn', null, `Expected pre-processed CSN to have option "${ name }" set to "${ options[name] }". Found: "${ csn.meta.options[name] }"`);
}
for (const name of warnAboutMismatch ) {
if (options[name] !== csn.meta.options[name]) {
warning('options-mismatch-pretransformed-csn', null,
`Expected pre-processed CSN to have option "${ name }" set to "${ options[name] }". Found: "${ csn.meta.options[name] }".`);
}
if (options[name] !== csn.meta.options[name])
warning('options-mismatch-pretransformed-csn', null, `Expected pre-processed CSN to have option "${ name }" set to "${ options[name] }". Found: "${ csn.meta.options[name] }"`);
}

@@ -134,9 +133,2 @@

attachTransformerCharacteristics(oDataCsn, 'odata', internalOptions, relevantOdataOptions, warnAboutMismatchOdata);
// FIXME: forOdataNew should correctly pass messages to options
internalOptions.messages = oDataCsn.messages;
// Cleanup the CSN
delete oDataCsn.messages;
delete oDataCsn.options;
return oDataCsn;

@@ -154,8 +146,3 @@ }

const internalOptions = prepareOptions.for.odata(options);
cloneCsnMessages(csn, options, internalOptions);
const result = odataInternal(csn, internalOptions);
processMessages(internalOptions, options);
return result;
return odataInternal(csn, internalOptions);
}

@@ -172,7 +159,3 @@

const internalOptions = prepareOptions.to.cdl(externalOptions);
cloneCsnMessages(csn, externalOptions, internalOptions);
const { result, options } = backends.toCdlWithCsn(cloneCsn(csn), internalOptions);
processMessages(options, externalOptions);
const { result } = backends.toCdlWithCsn(cloneCsn(csn, internalOptions), internalOptions);
return result;

@@ -189,9 +172,14 @@ }

const internalOptions = prepareOptions.to.sql(options);
cloneCsnMessages(csn, options, internalOptions);
// we need the CSN for view sorting
internalOptions.toSql.csn = true;
let intermediateResult;
if (isBetaEnabled(options, 'pretransformedCSN') && isPreTransformed(csn, 'sql')) {
checkPreTransformedCsn(csn, internalOptions, relevantSqlOptions, warnAboutMismatchOdata);
if (isBetaEnabled(internalOptions, 'pretransformedCSN') && isPreTransformed(csn, 'sql')) {
internalOptions.noRecompile = true; // pre-transformed cannot be recompiled!
checkPreTransformedCsn(csn, internalOptions, relevantSqlOptions, warnAboutMismatchOdata, 'to.sql');
intermediateResult = backends.renderSqlWithCsn(csn, internalOptions);
// attach CSN for view sorting
intermediateResult.csn = cloneCsn(csn, internalOptions);
}

@@ -202,5 +190,5 @@ else {

processMessages(intermediateResult, options);
const result = sortViews(intermediateResult);
return Object.values(flattenResultStructure(intermediateResult));
return result.map(obj => obj.sql).filter(create => create);
}

@@ -217,9 +205,88 @@

const internalOptions = prepareOptions.to.hdi(options);
cloneCsnMessages(csn, options, internalOptions);
// we need the CSN for view sorting
internalOptions.toSql.csn = true;
const intermediateResult = backends.toSqlWithCsn(csn, internalOptions);
processMessages(intermediateResult, options);
return flattenResultStructure(intermediateResult);
const sqlCSN = intermediateResult.csn;
delete intermediateResult.csn;
if (internalOptions.testMode) {
// All this mapping is needed because sortViews crossmatches
// passed in SQLs with the CSN artifact name
// But we also need to return it with the correct file ending in the end
// so remember and do lot's of mapping here.
const flat = flattenResultStructure(intermediateResult);
const nameMapping = Object.create(null);
const sqlsWithCSNNamesToSort = Object.create(null);
const sqlsNotToSort = Object.create(null);
Object.keys(flat).forEach((key) => {
const artifactNameLikeInCsn = key.replace(/\.[^/.]+$/, '');
nameMapping[artifactNameLikeInCsn] = key;
if (key.endsWith('.hdbtable') || key.endsWith('.hdbview'))
sqlsWithCSNNamesToSort[artifactNameLikeInCsn] = flat[key];
else
sqlsNotToSort[key] = flat[key];
});
const sorted = sortViews({ sql: sqlsWithCSNNamesToSort, csn: sqlCSN })
.filter(obj => obj.sql)
.reduce((previous, current) => {
const hdiArtifactName = remapName(nameMapping[current.name], sqlCSN);
previous[hdiArtifactName] = current.sql;
return previous;
}, Object.create(null));
// now add the not-sorted stuff, like indizes
Object.keys(sqlsNotToSort).forEach((key) => {
sorted[remapName(key, sqlCSN)] = sqlsNotToSort[key];
});
return sorted;
}
return remapNames(flattenResultStructure(intermediateResult), sqlCSN);
}
/**
* Remap names so that they stay consistent between v1 and v2
*
* Mainly important for _texts -> .texts
*
* @param {object} dict Result dictionary by toSql
* @param {CSN.Model} csn SQL transformed CSN
* @returns {object} New result structure
*/
function remapNames(dict, csn) {
const result = Object.create(null);
for (const [ key, value ] of Object.entries(dict)) {
const name = remapName(key, csn);
result[name] = value;
}
return result;
}
/**
* Remap names so that it stays consistent between v1 and v2
*
* Mainly important for _texts -> .texts
*
* @param {string} key Filename
* @param {CSN.Model} csn SQL transformed CSN
* @returns {string} Remapped filename
*/
function remapName(key, csn) {
const lastDot = key.lastIndexOf('.');
const prefix = key.slice(0, lastDot);
const suffix = key.slice(lastDot);
const remappedName = getFileName(prefix, csn);
return remappedName + suffix;
}
/**
* Return all changes in artifacts between two given models.

@@ -274,3 +341,4 @@ * Note: Only supports changes in entities (not views etc.) compiled/rendered as HANA-CSN/SQL.

// Prepare after-image.
cloneCsnMessages(csn, options, internalOptions);
// FIXME: Is this needed?
// cloneCsnMessages(csn, options, internalOptions);
const afterImage = backends.toSqlWithCsn(csn, internalOptions).csn;

@@ -302,3 +370,3 @@

for (const [ name, sqlStatement ] of Object.entries(artifacts))
result.push({ name, suffix, sql: sqlStatement });
result.push({ name: getFileName(name, afterImage), suffix, sql: sqlStatement });
}

@@ -315,3 +383,3 @@ return result;

for (const [ name ] of Object.entries(deletions))
result.push({ name, suffix: '.hdbtable' });
result.push({ name: getFileName(name, beforeImage), suffix: '.hdbtable' });

@@ -328,3 +396,3 @@ return result;

for (const [ name, changeset ] of Object.entries(migrations))
result.push({ name, suffix: '.hdbmigrationtable', changeset });
result.push({ name: getFileName(name, afterImage), suffix: '.hdbmigrationtable', changeset });

@@ -346,6 +414,4 @@ return result;

const internalOptions = prepareOptions.to.hdbcds(options);
cloneCsnMessages(csn, options, internalOptions);
const intermediateResult = backends.toHanaWithCsn(csn, internalOptions);
processMessages(intermediateResult, options);
return flattenResultStructure(intermediateResult);

@@ -366,18 +432,15 @@ }

);
cloneCsnMessages(csn, options, internalOptions);
const { service } = options;
let servicesAndMessages;
let servicesEdmj;
if (isPreTransformed(csn, 'odata')) {
checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata);
servicesAndMessages = backends.preparedCsnToEdm(csn, service, internalOptions);
checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata, 'for.odata');
servicesEdmj = backends.preparedCsnToEdm(csn, service, internalOptions);
}
else {
const oDataCsn = odataInternal(csn, internalOptions);
servicesAndMessages = backends.preparedCsnToEdm(oDataCsn, service, internalOptions);
servicesEdmj = backends.preparedCsnToEdm(oDataCsn, service, internalOptions);
}
internalOptions.messages = servicesAndMessages.messages;
processMessages(internalOptions, options);
return servicesAndMessages.edmj;
return servicesEdmj.edmj;
}

@@ -396,23 +459,20 @@

const internalOptions = prepareOptions.to.edm(options);
cloneCsnMessages(csn, options, internalOptions);
const { error, signal } = alerts(csn, internalOptions);
const { error } = makeMessageFunction(csn, internalOptions, 'for.odata');
if (internalOptions.version === 'v2')
signal(error`ODATA JSON output is not available for ODATA V2`);
error(null, null, 'OData JSON output is not available for OData V2');
let servicesAll;
let servicesAndMessages;
const result = {};
if (isPreTransformed(csn, 'odata')) {
checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata);
servicesAndMessages = backends.preparedCsnToEdmAll(csn, internalOptions);
checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata, 'for.odata');
servicesAll = backends.preparedCsnToEdmAll(csn, internalOptions);
}
else {
const oDataCsn = odataInternal(csn, internalOptions);
servicesAndMessages = backends.preparedCsnToEdmAll(oDataCsn, internalOptions);
servicesAll = backends.preparedCsnToEdmAll(oDataCsn, internalOptions);
}
const { messages } = servicesAndMessages;
const services = servicesAndMessages.edmj;
const services = servicesAll.edmj;
for (const serviceName in services) {

@@ -424,4 +484,2 @@ const lEdm = services[serviceName];

}
internalOptions.messages = messages;
processMessages(internalOptions, options);
return result;

@@ -442,18 +500,16 @@ }

);
cloneCsnMessages(csn, options, internalOptions);
const { service } = options;
let servicesAndMessages;
let services;
if (isPreTransformed(csn, 'odata')) {
checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata);
servicesAndMessages = backends.preparedCsnToEdmx(csn, service, internalOptions);
checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata, 'for.odata');
services = backends.preparedCsnToEdmx(csn, service, internalOptions);
}
else {
const oDataCsn = odataInternal(csn, internalOptions);
servicesAndMessages = backends.preparedCsnToEdmx(oDataCsn, service, internalOptions);
services = backends.preparedCsnToEdmx(oDataCsn, service, internalOptions);
}
processMessages(internalOptions, options);
return servicesAndMessages.edmx;
return services.edmx;
}

@@ -472,17 +528,14 @@

const internalOptions = prepareOptions.to.edmx(options);
cloneCsnMessages(csn, options, internalOptions);
let servicesAndMessages;
const result = {};
let oDataCsn = csn;
if (isPreTransformed(csn, 'odata')) {
checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata);
servicesAndMessages = backends.preparedCsnToEdmxAll(csn, internalOptions);
}
else {
const oDataCsn = odataInternal(csn, internalOptions);
servicesAndMessages = backends.preparedCsnToEdmxAll(oDataCsn, internalOptions);
}
const { messages } = servicesAndMessages;
const services = servicesAndMessages.edmx;
if (isPreTransformed(csn, 'odata'))
checkPreTransformedCsn(csn, internalOptions, relevantOdataOptions, warnAboutMismatchOdata, 'for.odata');
else
oDataCsn = odataInternal(csn, internalOptions);
const servicesEdmx = backends.preparedCsnToEdmxAll(oDataCsn, internalOptions);
const services = servicesEdmx.edmx;
// Create annotations and metadata once per service

@@ -494,4 +547,2 @@ for (const serviceName in services) {

internalOptions.messages = messages;
processMessages(internalOptions, options);
return result;

@@ -501,37 +552,2 @@ }

/**
* Either display the internally collected messages
* or return them to the external collector
*
* @param {any} internal Object with a messages property
* @param {any} external Object with a messages property
*/
function processMessages(internal, external) {
if (!external.messages) {
displayMessages(internal.messages);
}
else if (internal.messages) {
// Copy all messages, without duplicates and without killing the reference
external.messages.length = 0;
sortMessages(internal.messages).forEach(m => external.messages.push(m));
}
}
/**
* In case of recompilation, `csn.messages` may contain messages whereas
* `internalOptions.messages` may not.
* This function copies the messages so that they can be reclassified in
* the backends. Messages are only copied if user's `options.messages`
* does not exist. If it does then all messages should be there to
* begin with.
*
* @param {CSN.Model} csn User's CSN model
* @param {CSN.Model} externalOptions User's options.
* @param {CSN.Options} internalOptions Internal options, already cloned from user's options.
*/
function cloneCsnMessages(csn, externalOptions, internalOptions) {
if (!externalOptions.messages && csn.messages)
internalOptions.messages = [ ...internalOptions.messages, ...csn.messages ];
}
/**
* Flatten the result structure to a flat map.

@@ -583,3 +599,2 @@ *

cloneCsnMessages(csn, options, internalOptions);

@@ -590,7 +605,3 @@ // Add flag so it thinks we ran through forHanaNew

const sqlResult = toSqlDdl(csn, internalOptions);
processMessages(internalOptions, options);
const result = Object.values(flattenResultStructure(sqlResult));
return result;
return Object.values(flattenResultStructure(sqlResult));
}

@@ -617,2 +628,5 @@

* @returns {object} XSN
*
* TODO: move to lib/compiler/, consider new $recompile option, probaby issue
* message api-recompiled-csn there.
*/

@@ -626,3 +640,3 @@ function recompile(csn, options) {

// the option and is only intended for CDL sources.
options.parseCdl = false;
options.parseCdl = false; // TODO: delete this option
// Explicitly delete all toCsn options

@@ -632,7 +646,11 @@ delete options.toCsn;

const { augment } = require('../json/from-csn');
const { compileSources } = require('../main');
const { compileSourcesX } = require('../compiler');
/* eslint-enable global-require */
const file = csn.$location && csn.$location.file || '<stdin>.csn';
const xsn = augment(csn, file); // in-place
return compileSources( { '<stdin>.csn': xsn }, options );
const file = csn.$location && csn.$location.file &&
csn.$location.file.replace(/[.]cds$/, '.cds.csn') || '<recompile>.csn';
const xsn = augment(csn, file, options); // in-place
const compiled = compileSourcesX( { [file]: xsn }, { ...options, $recompile: true } );
if (options.messages)
deduplicateMessages(options.messages);
return compiled;
}

@@ -642,6 +660,6 @@

* @param {any} processor CSN processor
* @param {string} name Name of the processor
* @param {string} _name Name of the processor
* @returns {any} Function that calls the processor and recompiles in case of internal errors
*/
function publishCsnProcessor( processor, name = '' ) {
function publishCsnProcessor( processor, _name ) {
api.internal = processor;

@@ -677,32 +695,10 @@

const { info } = alerts.makeMessageFunction( csn, options );
info( 'api-recompiled-csn', emptyLocation('csn.json'), 'CSN input had to be recompiled' );
const { info } = makeMessageFunction( csn, options, 'compile' );
info( 'api-recompiled-csn', emptyLocation('csn.json'), {}, 'CSN input had to be recompiled' );
// next line to be replaced by CSN parser call which reads the CSN object
const xsn = getXsn(csn, options);
return processor( compactModel(xsn), options, ...args );
const xsn = recompile(csn, options);
const recompiledCsn = compactModel(xsn);
return processor( recompiledCsn, options, ...args );
}
}
/**
* Return an XSN, with some special options depending on the transformation run
*
* @param {any} csn CSN to augment
* @param {any} options Options to use
* @returns {object} XSN
*/
function getXsn(csn, options) {
if ([ 'to.sql', 'to.hdi', 'to.hdbcds' ].includes(name)) {
const optionsClone = Object.assign({}, options);
optionsClone.messages = options.messages;
if (!optionsClone.severities)
optionsClone.severities = {};
optionsClone.severities['rewrite-not-supported'] = 'Error';
optionsClone.severities['query-undefined-element'] = 'Error';
return recompile(csn, optionsClone);
}
return recompile(csn, options );
}
}

@@ -730,11 +726,11 @@

/**
* Available types of association handling
* Available naming modes
*
* @typedef {'assocs' | 'joins' } AssociationMode
* @typedef {'plain' | 'quoted' | 'hdbcds' } NamingMode
*/
/**
* Available naming modes
* Available oData versions
*
* @typedef {'plain' | 'quoted' | 'hdbcds' } NamingMode
* @typedef {'v2' | 'v4' } oDataVersion
*/

@@ -745,3 +741,3 @@

*
* @typedef {'v2' | 'v4' } oDataVersion
* @typedef { 'structured' | 'flat' } oDataFormat
*/

@@ -753,7 +749,7 @@

* @typedef {object} Options
* @property {boolean} [beta=false] Enable experimental features - not for productive use!
* @property {object} [beta] Enable experimental features - not for productive use!
* @property {boolean} [dependentAutoexposed=false] For dependent autoexposed entities (managed compositions, texts entity), follow name of base entity
* @property {boolean} [longAutoexposed=false] Deprecated: Produce long names (with underscores) for autoexposed entities
* @property {Map<string, number>} [severities={}] Map of message-id and severity that allows setting the severity for the given message
* @property {Array} [messages] Allows collecting all messages in the options instead of as part of the CSN
* @property {Array} [messages] Allows collecting all messages in the options instead of printing them to stderr.
*/

@@ -765,7 +761,8 @@

* @typedef {object} oDataOptions
* @property {boolean} [beta=false] Enable experimental features - not for productive use!
* @property {object} [beta] Enable experimental features - not for productive use!
* @property {boolean} [longAutoexposed=false] Deprecated: Produce long names (with underscores) for autoexposed entities
* @property {Map<string, number>} [severities={}] Map of message-id and severity that allows setting the severity for the given message
* @property {Array} [messages] Allows collecting all messages in the options instead of as part of the CSN
* @property {Array} [messages] Allows collecting all messages in the options instead of printing them to stderr.
* @property {oDataVersion} [odataVersion='v4'] Odata version to use
* @property {oDataFormat} [odataFormat='flat'] Wether to generate oData as flat or as structured. Structured only with v4.
* @property {NamingMode} [sqlMapping='plain'] Naming mode to use

@@ -780,7 +777,6 @@ * @property {string} [service] If a single service is to be rendered

* @property {NamingMode} [sqlMapping='plain'] Naming mode to use
* @property {SQLDialect} [sqlDialect='hana'] SQL dialect to use - hana, hanaJoins, hanaAssocs
* @property {boolean} [beta=false] Enable experimental features - not for productive use!
* @property {object} [beta] Enable experimental features - not for productive use!
* @property {boolean} [longAutoexposed=false] Deprecated: Produce long names (with underscores) for autoexposed entities
* @property {Map<string, number>} [severities={}] Map of message-id and severity that allows setting the severity for the given message
* @property {Array} [messages] Allows collecting all messages in the options instead of as part of the CSN
* @property {Array} [messages] Allows collecting all messages in the options instead of printing them to stderr.
*/

@@ -793,7 +789,6 @@

* @property {NamingMode} [sqlMapping='plain'] Naming mode to use
* @property {SQLDialect} [sqlDialect='hana'] SQL dialect to use - hana, hanaJoins, hanaAssocs
* @property {boolean} [beta=false] Enable experimental features - not for productive use!
* @property {object} [beta] Enable experimental features - not for productive use!
* @property {boolean} [longAutoexposed=false] Deprecated: Produce long names (with underscores) for autoexposed entities
* @property {Map<string, number>} [severities={}] Map of message-id and severity that allows setting the severity for the given message
* @property {Array} [messages] Allows collecting all messages in the options instead of as part of the CSN
* @property {Array} [messages] Allows collecting all messages in the options instead of printing them to stderr.
*/

@@ -807,9 +802,9 @@

* @property {SQLDialect} [sqlDialect='sqlite'] SQL dialect to use
* @property {object} [magicVars] Value for the "$user.locale" variable in "sqlite" dialect
* @property {object} [magicVars] Object containing values for magic variables like "$user"
* @property {string} [magicVars.locale] Value for the "$user.locale" in "sqlite" dialect
* @property {string} [magicVars.user] Value for the "$user" variable in "sqlite" dialect
* @property {boolean} [beta=false] Enable experimental features - not for productive use!
* @property {object} [beta] Enable experimental features - not for productive use!
* @property {boolean} [longAutoexposed=false] Deprecated: Produce long names (with underscores) for autoexposed entities
* @property {Map<string, number>} [severities={}] Map of message-id and severity that allows setting the severity for the given message
* @property {Array} [messages] Allows collecting all messages in the options instead of as part of the CSN
* @property {Array} [messages] Allows collecting all messages in the options instead of printing them to stderr.
*/

@@ -848,5 +843,5 @@

/**
* An array of SQL statements, first CREATE TABLE, then CREATE VIEW.
* A SQL statement - CREATE TABLE, CREATE VIEW etc.
*
* @typedef {string[]} SQL
* @typedef {string} SQL
*/

@@ -853,0 +848,0 @@

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

// TODO: there should be just one place where the options are defined with
// their types (not also in validate.js or whatever).
// Options that are advertised and documented to users

@@ -10,7 +13,7 @@ const publicOptionsNewAPI = [

'beta',
'dependentAutoexposed',
'longAutoexposed',
'deprecated',
'localizedLanguageFallback', // why can't I define the option type here?
'severities',
'messages',
'withLocations',
// DB

@@ -29,4 +32,6 @@ 'sqlDialect',

'odataXServiceRefs',
'odataDefaultValues',
'service',
'serviceNames',
//
'dictionaryPrototype',
];

@@ -36,3 +41,2 @@

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

@@ -43,8 +47,8 @@ 'fuzzyCsnError',

'traceParserAmb',
'hanaFlavor',
'withLocations',
'testMode',
'noRecompile',
'internalMsg',
'localizedWithoutCoalesce', // experiment version of 'localizedLanguageFallback',
'dependentAutoexposed', // deprecated, no effect - TODO: safe to remove?
'longAutoexposed', // deprecated, no effect - TODO: safe to remove?
'localizedWithoutCoalesce', // deprecated version of 'localizedLanguageFallback',
];

@@ -68,3 +72,3 @@

customValidators, combinationValidators) {
const options = Object.assign({ messages: [] }, defaults);
const options = Object.assign({}, defaults);
const inputOptionNames = Object.keys(input);

@@ -79,2 +83,7 @@ for (const name of overallOptions) {

}
// use original messages object, i.e. keep the reference!
if (input.messages)
options.messages = input.messages;
// Validate the filtered input options

@@ -114,17 +123,3 @@ // only "new-style" options are here

case 'sqlDialect':
if (optionValue === 'hana') {
options.dialect = optionValue;
}
else if (optionValue === 'sqlite') {
options.dialect = optionValue;
options.associations = 'joins';
}
else if (optionValue === 'hanaJoins') {
options.dialect = 'hana';
options.associations = 'joins';
}
else if (optionValue === 'hanaAssocs') {
options.dialect = 'hana';
options.associations = 'assocs';
}
options.dialect = optionValue;
break;

@@ -152,3 +147,3 @@ case 'sqlMapping':

const hardOptions = { src: 'sql' };
const defaultOptions = { sqlMapping: 'plain', sqlDialect: 'sqlite' };
const defaultOptions = { sqlMapping: 'plain', sqlDialect: 'plain' };
const processed = translateOptions(options, defaultOptions, hardOptions, undefined, [ 'sql-dialect-and-naming' ]);

@@ -163,4 +158,5 @@

const hardOptions = { src: 'hdi' };
const defaultOptions = { sqlMapping: 'plain', sqlDialect: 'hanaJoins' };
const processed = translateOptions(options, defaultOptions, hardOptions, { sqlDialect: generateStringValidator([ 'hana', 'hanaJoins', 'hanaAssocs' ]) });
const defaultOptions = { sqlMapping: 'plain', sqlDialect: 'hana' };
const processed = translateOptions(options, defaultOptions, hardOptions, { sqlDialect: generateStringValidator([ 'hana' ]) });
const result = Object.assign({}, processed);

@@ -172,4 +168,4 @@ result.toSql = Object.assign({}, processed);

hdbcds: (options) => {
const defaultOptions = { sqlMapping: 'plain' };
const processed = translateOptions(options, defaultOptions, {}, { sqlDialect: generateStringValidator([ 'hana', 'hanaJoins', 'hanaAssocs' ]) });
const defaultOptions = { sqlMapping: 'plain', sqlDialect: 'hana' };
const processed = translateOptions(options, defaultOptions, {}, { sqlDialect: generateStringValidator([ 'hana' ]) });

@@ -179,3 +175,2 @@ const result = Object.assign({}, processed);

return result;

@@ -191,3 +186,2 @@ },

return result;

@@ -221,3 +215,3 @@ },

hana: (options) => {
const defaultOptions = { sqlMapping: 'plain', sqlDialect: 'hanaAssocs' };
const defaultOptions = { sqlMapping: 'plain', sqlDialect: 'hana' };
const processed = translateOptions(options, defaultOptions);

@@ -224,0 +218,0 @@

@@ -52,2 +52,9 @@ 'use strict';

},
deprecated: {
validate: val => val !== null && typeof val === 'object' && !Array.isArray(val),
expected: () => 'type object',
found: (val) => {
return val === null ? val : `type ${ typeof val }`;
},
},
severities: {

@@ -72,3 +79,3 @@ validate: val => val !== null && typeof val === 'object' && !Array.isArray(val),

},
sqlDialect: generateStringValidator([ 'sqlite', 'hana', 'hanaAssocs', 'hanaJoins' ]),
sqlDialect: generateStringValidator([ 'sqlite', 'hana', 'plain' ]),
sqlMapping: generateStringValidator([ 'plain', 'quoted', 'hdbcds' ]),

@@ -82,2 +89,10 @@ odataVersion: generateStringValidator([ 'v2', 'v4' ]),

},
serviceNames: {
validate: val => Array.isArray(val) && !val.some(y => (typeof y !== 'string')),
expected: () => 'type array of string',
found: val => `type ${ typeof val }`,
},
dictionaryPrototype: {
validate: () => true,
},
};

@@ -89,8 +104,8 @@

severity: 'error',
message: () => 'Structured OData is only supported with OData version v4.',
getMessage: () => 'Structured OData is only supported with OData version v4',
},
'sql-dialect-and-naming': {
validate: options => options.sqlDialect && options.sqlMapping && ![ 'hana', 'hanaJoins', 'hanaAssocs' ].includes(options.sqlDialect) && [ 'quoted', 'hdbcds' ].includes(options.sqlMapping),
validate: options => options.sqlDialect && options.sqlMapping && ![ 'hana' ].includes(options.sqlDialect) && [ 'quoted', 'hdbcds' ].includes(options.sqlMapping),
severity: 'error',
message: options => `sqlDialect '${ options.sqlDialect }' cannot be combined with sqlMapping '${ options.sqlMapping }'`,
getMessage: options => `sqlDialect '${ options.sqlDialect }' can't be combined with sqlMapping '${ options.sqlMapping }'`,
},

@@ -100,3 +115,3 @@ 'beta-no-test': {

severity: 'warning',
message: () => 'Option "beta" was used. This option should not be used in productive scenarios!',
getMessage: () => 'Option "beta" was used. This option should not be used in productive scenarios!',
},

@@ -118,3 +133,3 @@ };

const messageCollector = { messages: [] };
const { error } = makeMessageFunction(messageCollector);
const { error, throwWithError } = makeMessageFunction(null, messageCollector);

@@ -128,6 +143,6 @@ for (const optionName of Object.keys(options)) {

}
handleMessages(messageCollector);
throwWithError();
}
const message = makeMessageFunction(options);
const message = makeMessageFunction(null, options);

@@ -137,5 +152,8 @@ for (const combinationValidatorName of combinationValidators.concat([ 'beta-no-test' ])) {

if (combinationValidator.validate(options))
message[combinationValidator.severity]('invalid-option-combination', null, {}, combinationValidator.message(options));
message[combinationValidator.severity]('invalid-option-combination', null, {}, combinationValidator.getMessage(options));
}
// TODO: Replace with message.throwWithError():
// But be aware that it only throws with non-configurable errors and that this
// will lead to issues in test3. See #6037
handleMessages(undefined, options);

@@ -142,0 +160,0 @@ }

@@ -6,40 +6,23 @@ 'use strict';

const csnToSwagger = require('./render/toSwagger');
const { transformForHana } = require('./transform/forHana');
const { transformForHanaWithCsn } = require('./transform/forHanaNew');
const { compactModel, sortCsn } = require('./json/to-csn')
const { toCdsSource, toCdsSourceCsn } = require('./render/toCdl');
const { toCdsSourceCsn } = require('./render/toCdl');
const { toHdbcdsSource } = require('./render/toHdbcds');
const { toSqlDdl } = require('./render/toSql');
const { toRenameDdl } = require('./render/toRename');
const { transform4odata } = require('./transform/forOdata');
const { manageConstraints } = require('./render/manageConstraints');
const { transform4odataWithCsn } = require('./transform/forOdataNew');
const { csn2edm, csn2edmAll } = require('./edm/csn2edm');
const { mergeOptions } = require('./model/modelUtils');
const alerts = require('./base/alerts');
const { handleMessages } = require('./base/messages');
const { setProp, isBetaEnabled } = require('./base/model');
const { mergeOptions } = require('./model/csnUtils');
const { isBetaEnabled } = require('./base/model');
const { optionProcessor } = require('./optionProcessor')
const timetrace = require('./utils/timetrace');
const { makeMessageFunction } = require('./base/messages');
const { forEachDefinition } = require('./model/csnUtils');
function deprecated( model, options, signal, backend ) {
if (options.beta || options.betaMode) { // by intention, no test for individual beta features
signal(signal.error`Deprecated backend '${backend}' cannot be used with beta features`);
handleMessages( model, options );
}
else if (model.$newfeatures) {
signal(signal.error`Deprecated backend '${backend}' cannot be used with most new features like ${model.$newfeatures}`);
handleMessages( model, options );
}
else {
signal(signal.warning`Deprecated backend '${backend}' - do not disable feature 'snapi'`);
}
}
/**
* Transform an augmented CSN 'model' into HANA-compatible CDS source.
* Transform a CSN into HANA-compatible CDS source.
* The following options control what is actually generated (see help above):
* options : {
* toHana.names
* toHana.associations
* toHana.src

@@ -50,3 +33,2 @@ * toHana.csn

* If 'toHana.names' is not provided, 'quoted' is used.
* If 'toHana.associations' is not provided, 'assocs' is used.
* If neither 'toHana.src' nor 'toHana.csn' are provided, the default is to generate only HANA CDS

@@ -58,3 +40,2 @@ * source files.

* csn : the (compact) transformed CSN model
* _augmentedCsn : (subject to change): the augmented CSN model
* hdbcds : a dictionary of top-level artifact names, containing for each name 'X':

@@ -65,70 +46,5 @@ * <X> : the HANA CDS source string of the artifact 'X'. Please note that the

* X reflects the naming policy set by toHana.names
* messages : an array of strings with warnings (if any)
* }
* Throws a CompilationError on errors.
*
* @param {XSN.Model} model
* @param {CSN.Options} [options]
*/
function toHana(model, options) {
// In case of API usage the options are in the 'options' argument
// put the OData specific options under the 'options.toHana' wrapper
// and leave the rest under 'options'
if (options && !options.toHana) {
_wrapRelevantOptionsForCmd(options, 'toHana');
}
// Provide defaults and merge options with those from model
options = mergeOptions({ toHana : getDefaultBackendOptions().toHana }, model.options, options);
// Provide something to generate if nothing else was given (conditional default)
if (!options.toHana.src && !options.toHana.csn) {
options.toHana.src = true;
}
const { warning, signal } = alerts(model);
deprecated( model, options, signal, 'toHana' ); // -> to.hdbcds
// Backward compatibility for old naming modes
// FIXME: Remove after a few releases
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';
}
// Verify options
optionProcessor.verifyOptions(options, 'toHana').map(complaint => signal(warning`${complaint}`));
// Special case: For naming variant 'hdbcds' in combination with 'toHana' (and only there!), 'forHana'
// must leave namespaces, structs and associations alone.
if (options.toHana.names === 'hdbcds') {
options = mergeOptions(options, { forHana : { keepNamespaces: true, keepStructsAssocs: true } });
}
// Prepare model for HANA (transferring the options to forHana, and setting 'dialect' to 'hana', because 'toHana' is only used for that)
let forHanaAugmented = transformForHana(model, mergeOptions(options, { forHana: { dialect: 'hana' } }, { forHana : options.toHana } ));
// Assemble result
let result = {};
if (options.toHana.src) {
result.hdbcds = toCdsSource(forHanaAugmented);
}
if (options.toHana.csn) {
result._augmentedCsn = forHanaAugmented;
result.csn = compactModel(forHanaAugmented);
if(options.testMode)
result.csn = sortCsn(result.csn);
}
// Transfer warnings (errors would have resulted in an exception before we come here)
if (forHanaAugmented.messages && forHanaAugmented.messages.length > 0) {
result.messages = forHanaAugmented.messages;
}
return result;
}
/**
* The twin of the toHana function but using CSN as a model *
* @param {CSN.Model} csn

@@ -146,3 +62,3 @@ * @param {CSN.Options} [options]

// Provide defaults and merge options with those from model
options = mergeOptions({ toHana : getDefaultBackendOptions().toHana }, csn.options, options);
options = mergeOptions({ toHana : getDefaultBackendOptions().toHana }, options);

@@ -154,3 +70,3 @@ // Provide something to generate if nothing else was given (conditional default)

const { warning } = makeMessageFunction(csn, options);
const { warning } = makeMessageFunction(csn, options, 'to.hana');

@@ -172,21 +88,10 @@ // Verify options

// Assemble result
let result = {
messages: []
};
let result = {};
// Transfer warnings (errors would have resulted in an exception before we come here)
if (forHanaCsn.messages && forHanaCsn.messages.length > 0) {
result.messages = forHanaCsn.messages;
}
if (options.toHana.src) {
if(options.testMode){
const sorted = sortCsn(forHanaCsn, true);
setProp(sorted, "options", forHanaCsn.options);
setProp(sorted, "messages", forHanaCsn.messages);
options.messages = result.messages;
result.hdbcds = toCdsSourceCsn(sorted, options);
result.hdbcds = toHdbcdsSource(sorted, options);
} else {
options.messages = result.messages;
result.hdbcds = toCdsSourceCsn(forHanaCsn, options);
result.hdbcds = toHdbcdsSource(forHanaCsn, options);
}

@@ -203,136 +108,2 @@ }

// ----------- toOdata -----------
// Generate ODATA for augmented CSN `model` using `options`.
// Before anything is generated, the following transformations are applied to 'model':
// FIXME: Verify that this is still correct
// - Flatten structured elements (and foreign keys of managed associations pointing to
// keys that are themselves managed associations).
// - Generate foreign key fields for entities with managed associations (annotated with
// '@odata.foreignKey4'). Propagate along projections accordingly. Names are built using
// <assoc>_<key>, conflicts are checked.
// - Complete the 'foreignKeys' property for all managed associations, so that there
// is always a 'generatedFieldName' for the corresponding generated foreign key field.
// - Implicitly redirect associations based on exposure
// - Check that exposed associations do not point to non-exposed targets
// - Unravel derived type chains, propagating annotations upwards.
// - Rename annotations according to a fixed list of short-hands
// The following options control what is actually generated (see help above):
// options : {
// toOdata.version
// toOdata.odataFormat
// toOdata.xml
// toOdata.json
// toOdata.separate
// toOdata.combined
// toOdata.csn
// toOdata.names
// }
// Options provided here are merged with (and take precedence over) options from 'model'.
// If 'toOdata.version' is not provided, 'v4' is used.
// If neither 'toOdata.xml' nor 'toOdata.json' nor 'toOdata.csn' are provided, the default is
// to generate only XML output. If neither 'toOdata.separate' nor 'toOdata.combined' are provided,
// the default is to generate only combined XML output.
// If all provided options are part of 'toOdata', the 'toOdata' wrapper can be omitted.
//
// The result object contains the generation results as follows (as enabled in 'options'):
// result : {
// csn : the (compact) transformed CSN model including all services
// _augmentedCsn : (subject to change): the augmented CSN model including all services
// services : a dictionary of service names, containing for each name:
// <servicename> : {
// annotations : an XML string with EDMX annotations for service 'svc'
// metadata : an XML string with EDMX metadata for service 'svc'
// combined : an XML string with both EDMX metadata and annotations for service 'svc'
// metadata_json : a JSON object (not a string!) with EDM metadata for service 'svc'
// }
// messages : an array of strings with warnings (if any)
// }
// If 'model' does not contain any services, 'csn' will still contain the transformed model, but
// 'services' will be an empty dictionary.
//
// Throws a CompilationError on errors.
function toOdata(model, options) {
const { error, warning, signal } = alerts(model);
// In case of API usage the options are in the 'options' argument
// put the OData specific options under the 'options.toOdata' wrapper
// and leave the rest under 'options'
if (options && !options.toOdata) {
_wrapRelevantOptionsForCmd(options, 'toOdata');
}
// Provide defaults and merge options with those from model
options = mergeOptions({ toOdata : getDefaultBackendOptions().toOdata }, model.options, options);
deprecated( model, options, signal, 'toOdata' ); // -> for.odata / to.edm / to.edmx
// Provide something to generate if nothing else was given (conditional default)
if (!options.toOdata.xml && !options.toOdata.json && !options.toOdata.csn) {
options.toOdata.xml = true;
}
if (!options.toOdata.separate && !options.toOdata.combined) {
options.toOdata.combined = true;
}
// 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 = 'quoted';
}
// Verify options
optionProcessor.verifyOptions(options, 'toOdata').map(complaint => signal(warning`${complaint}`));
// Prepare model for ODATA processing
let forOdataAugmented = transform4odata(model, options);
// Assemble result object
let result = {
services: Object.create(null),
messages: model.messages,
}
if (options.toOdata.csn) {
result.csn = compactModel(forOdataAugmented);
result._augmentedCsn = forOdataAugmented;
}
// Create annotations and metadata once per service
if (options.toOdata.xml || options.toOdata.json) {
// Compact the model
let compactedModel = compactModel(forOdataAugmented);
setProp(compactedModel, 'messages', forOdataAugmented.messages);
let allServices = csn2edmAll(compactedModel, options);
for(let serviceName in allServices) {
let l_edm = allServices[serviceName];
result.services[serviceName] = {};
if (options.toOdata.xml) {
if (options.toOdata.separate) {
result.services[serviceName].annotations = l_edm.toXML('annotations');
result.services[serviceName].metadata = l_edm.toXML('metadata');
}
if (options.toOdata.combined) {
result.services[serviceName].combined = l_edm.toXML('all');
}
}
if (options.toOdata.json) {
// JSON output is not available for ODATA V2
if (options.toOdata.version === 'v2') {
signal(error`ODATA JSON output is not available for ODATA V2`);
}
// FIXME: Why only metadata_json - isn't this rather a 'combined_json' ? If so, rename it!
result.services[serviceName].metadata_json = l_edm.toJSON();
}
}
}
return result;
}
/**

@@ -353,3 +124,3 @@ * Generate ODATA for `csn` using `options`.

// Provide defaults and merge options with those from csn
options = mergeOptions({ toOdata : getDefaultBackendOptions().toOdata }, csn.options, options);
options = mergeOptions({ toOdata : getDefaultBackendOptions().toOdata }, options);

@@ -364,3 +135,3 @@ // Provide something to generate if nothing else was given (conditional default)

const { error, warning } = makeMessageFunction(csn, options);
const { error, warning } = makeMessageFunction(csn, options, 'for.odata');

@@ -375,3 +146,2 @@ // Verify options

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

@@ -402,3 +172,3 @@ if (options.toOdata.csn) {

if (options.toOdata.version === 'v2') {
error(null, null, `ODATA JSON output is not available for ODATA V2`);
error(null, null, `OData JSON output is not available for OData V2`);
}

@@ -417,8 +187,5 @@ // FIXME: Why only metadata_json - isn't this rather a 'combined_json' ? If so, rename it!

function preparedCsnToEdmx(csn, service, options) {
// Merge options with those from model
options = mergeOptions(csn.options, options);
let edmx = csn2edm(csn, service, options).toXML('all');
return {
edmx,
messages: options.messages || csn.messages,
};

@@ -430,4 +197,2 @@ }

function preparedCsnToEdmxAll(csn, options) {
// Merge options with those from model
options = mergeOptions(csn.options, options);
let edmx = csn2edmAll(csn, options);

@@ -439,3 +204,2 @@ for(const service in edmx){

edmx,
messages: options.messages || csn.messages,
};

@@ -447,8 +211,7 @@ }

function preparedCsnToEdm(csn, service, 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 = csn2edm(csn, service, options).toJSON();
// Merge options; override OData version as edm json is always v4
options = mergeOptions(options, { toOdata : { version : 'v4' }});
const edmj = csn2edm(csn, service, options).toJSON();
return {
edmj,
messages: options.messages || csn.messages,
};

@@ -460,4 +223,4 @@ }

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' }});
// Merge options; override OData version as edm json is always v4
options = mergeOptions(options, { toOdata : { version : 'v4' }});
let edmj = csn2edmAll(csn, options);

@@ -469,3 +232,2 @@ for(const service in edmj){

edmj,
messages: options.messages || csn.messages,
};

@@ -477,29 +239,2 @@ }

/**
* Generate CDS source text for augmented CSN model 'model'.
* The following options control what is actually generated:
* options : {
* FIXME: This option should be removed and something like 'toCdl.dialect: 'hana' be
* used instead.
* toHana : if true, HANA-specific source dialect is generated (affects e.g. the
* translation of '$self.foo' in paths and ::-ish namespace declarations)
* }
* One source is created per top-level artifact.
* Return a dictionary of top-level artifacts
* by their names, like this:
* { "foo" : "using XY; context foo {...};",
* "bar::wiz" : "namespace bar::; entity wiz {...};"
* }
* Throws a CompilationError on errors.
*
* @param {XSN.Model} model
* @param {CSN.Options} [options]
*/
function toCdl(model, options) {
const { signal } = alerts(model);
options = handleToCdlOptions(model, options);
deprecated( model, options, signal, 'toCdl' ); // -> to.cdl/to.hdbcds
return toCdsSource(model, options);
}
/**
* @param {XSN.Model | CSN.Model} model

@@ -517,6 +252,6 @@ * @param {CSN.Options} options

// Merge options with those from model
// Merge options with those from XSN model
options = mergeOptions({ toCdl : true }, model.options, options);
const { warning } = makeMessageFunction(model, options);
const { warning } = makeMessageFunction(model, options, 'to.cdl');

@@ -530,2 +265,15 @@ // Verify options

/**
* Generate CDS source text for CSN model.
* One source is created per top-level artifact.
* Returns an object with a `result` dictionary of top-level artifacts
* by their names, like this:
*
* {
* "foo" : "using XY; context foo {...};",
* "bar.wiz" : "namespace bar; entity wiz {...};"
* }
*
* Throws a CompilationError on errors.
*
* @param {CSN.Model} csn

@@ -540,209 +288,28 @@ * @param {CSN.Options} options

// ----------- toSwagger -----------
// Generate OpenAPI JSON version 3 for the augmented CSN 'model'.
// The following options control what is actually generated:
// options : {
// toSwagger.json : if true, generate OpenAPI JSON output
// toSwagger.csn : if true, generate the transformed CSN model
// }
// Options provided here are merged with (and take precedence over) options from 'model'.
// If neither 'toSwagger.json' nor 'toSwagger.csn' are provided, the default is to generate
// only OpenAPI JSON.
// If all provided options are part of 'toSwagger', the 'toSwagger' wrapper can be omitted.
// One OpenAPI JSON object is created per service.
// The result object contains the generation results as follows (as enabled in 'options'):
// result: {
// csn : the (compact) transformed CSN model including all services
// _augmentedCsn : (subject to change): the augmented CSN model including all services
// services: {
// <servicename>: {
// openapi: '3.0.0',
// info: { ... },
// paths: { ...},
// components: {
// schemas: { ... }
// }
// }
// }
// }
//
// Throws a CompilationError on errors.
function toSwagger(model, options) {
const { warning, error, signal } = alerts(model);
// In case of API usage the options are in the 'options' argument
// put the OData specific options under the 'options.toSwagger' wrapper
// and leave the rest under 'options'
if (options && !options.toSwagger) {
_wrapRelevantOptionsForCmd(options, 'toSwagger');
}
// Merge options with those from model
options = mergeOptions(model.options, options);
// hide to swagger behind betaMode
if (isBetaEnabled(options, 'toSwagger')) {
signal(warning`The to swagger backend is experimental`);
} else {
signal(error`The to swagger backend is only available in beta-mode and is experimental`);
}
// If neither 'json' nor 'csn' is specified as output option, produce 'json'
if (!options.toSwagger || (!options.toSwagger.json && !options.toSwagger.csn)) {
options = mergeOptions({ toSwagger: { json: true } }, options);
}
// Verify options
optionProcessor.verifyOptions(options, 'toSwagger').map(complaint => signal(warning`${complaint}`));
// Actual implementation
return csnToSwagger(model, options);
}
// ----------- toSql -----------
// Generate SQL DDL statements for augmented CSN 'model'.
// The following options control what is actually generated (see help above):
// options : {
// toSql.names
// toSql.associations
// toSql.dialect
// toSql.user.id
// toSql.user.locale
// toSql.src
// toSql.csn
// }
// Options provided here are merged with (and take precedence over) options from 'model'.
// If neither 'toSql.src' nor 'toSql.csn' are provided, the default is to generate only SQL DDL
// source files.
// If all provided options are part of 'toSql', the 'toSql' wrapper can be omitted.
// The result object contains the generation results as follows (as enabled in 'options'):
// result : {
// csn : the (compact) transformed CSN model
// _augmentedCsn : (subject to change): the augmented CSN model
// sql : a dictionary of top-level artifact names, containing for each name 'X':
// <X> : a string with SQL DDL statements for artifact 'X', terminated with ';'.
// Please note that the name of 'X' may contain characters that are not
// legal for filenames on all operating systems (e.g. ':', '\' or '/').
// messages : an array of strings with warnings (if any)
// }
// Throws a CompilationError on errors.
function toSql(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)) {
transformUserOption(model.options.toSql);
}
// In case of API usage the options are in the 'options' argument
// put the OData specific options under the 'options.toSql' wrapper
// and leave the rest under 'options'
if (options && !options.toSql) {
_wrapRelevantOptionsForCmd(options, 'toSql');
}
// when the API funtion is used directly - toSql options are in options
// ensure the desired format of the user option
if (options && (options.toSql.user || options.toSql.locale)){
transformUserOption(options.toSql);
}
// Provide defaults and merge options with those from model
options = mergeOptions({ toSql : getDefaultBackendOptions().toSql }, model.options, options);
deprecated( model, options, signal, 'toSql' ); // -> to.sql, to.hdi
// Provide something to generate if nothing else was given (conditional default)
if (!options.toSql.src && !options.toSql.csn) {
options.toSql.src = 'sql';
}
// 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';
}
// Verify options
optionProcessor.verifyOptions(options, 'toSql').map(complaint => signal(warning`${complaint}`));
// FIXME: Currently, '--to-sql' implies transformation for HANA (transferring the options to forHana)
let forHanaOptions = options.toSql;
// Special case: For naming variant 'hdbcds' in combination with 'toSql', 'forHana' must leave
// namespaces alone (but must still flatten structs because we need the leaf element names).
if (options.toSql.names === 'hdbcds') {
forHanaOptions.keepNamespaces = true;
}
// It doesn't make much sense to use 'sqlite' dialect with associations
if (options.toSql.dialect === 'sqlite' && options.toSql.associations !== 'joins') {
signal(warning`Option "{ toSql.dialect: 'sqlite' }" should always be combined with "{ toSql.assocs: 'joins' }"`);
}
if(options.toSql.dialect !== 'hana') {
// CDXCORE-465, 'quoted' and 'hdbcds' are to be used in combination with dialect 'hana' only
if(['quoted', 'hdbcds'].includes(options.toSql.names)) {
signal(error`Option "{ toSql.dialect: '${options.toSql.dialect}' }" cannot be combined with "{ toSql.names: '${options.toSql.names}' }"`);
}
// No non-HANA SQL for HDI
if(options.toSql.src === 'hdi') {
signal(error`Option "{ toSql.dialect: '${options.toSql.dialect}' }" cannot be combined with "{ toSql.src: '${options.toSql.src}' }"`);
}
}
// Because (even HANA) SQL cannot deal with associations in mixins that are published in the same view,
// the association processing must at least be 'mixin', even if callers specified 'assocs'
if (forHanaOptions.associations === 'assocs') {
forHanaOptions.associations = 'mixin';
}
// FIXME: Should not be necessary
forHanaOptions.alwaysResolveDerivedTypes = true;
let forSqlAugmented = transformForHana(model, mergeOptions(options, { forHana : forHanaOptions } ));
// Assemble result
/** @type {object} */
let result = {};
if (options.toSql.src) {
result = toSqlDdl(compactModel(forSqlAugmented), forSqlAugmented.options);
}
if (options.toSql.csn) {
result._augmentedCsn = forSqlAugmented;
result.csn = compactModel(forSqlAugmented);
}
// Transfer warnings (errors would have resulted in an exception before we come here)
if (forSqlAugmented.messages && forSqlAugmented.messages.length > 0) {
result.messages = forSqlAugmented.messages;
}
return result;
// If among the options user, user.id or user.locale are specified via the CLI or
// via the API, then ensure that at the end there is a user option, which is an object and has(have)
// "id" and/or "locale" prop(s)
function transformUserOption(options) {
// move the user option value under user.id if specified as a string
if (options.user && typeof options.user === 'string' || options.user instanceof String) {
options.user = { id: options.user };
}
// move the locale option(if provided) under user.locale
if (options.locale) {
options.user
? Object.assign(options.user, { locale: options.locale })
: options.user = { locale: options.locale };
delete options.locale;
}
}
}
/**
* The twin of the toSql function but using CSN as a model
* Generate SQL DDL statements for augmented CSN 'model'.
* The following options control what is actually generated (see help above):
* options : {
* toSql.names
* toSql.dialect
* toSql.user.id
* toSql.user.locale
* toSql.src
* toSql.csn
* }
* Options provided here are merged with (and take precedence over) options from 'model'.
* If neither 'toSql.src' nor 'toSql.csn' are provided, the default is to generate only SQL DDL
* source files.
* If all provided options are part of 'toSql', the 'toSql' wrapper can be omitted.
* The result object contains the generation results as follows (as enabled in 'options'):
* result : {
* csn : the (compact) transformed CSN model
* sql : a dictionary of top-level artifact names, containing for each name 'X':
* <X> : a string with SQL DDL statements for artifact 'X', terminated with ';'.
* Please note that the name of 'X' may contain characters that are not
* legal for filenames on all operating systems (e.g. ':', '\' or '/').
* }
* Throws a CompilationError on errors.
*

@@ -754,21 +321,17 @@ * @param {CSN.Model} model

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

@@ -778,3 +341,3 @@ return result;

function transformSQLOptions(model, options){
function transformSQLOptions(model, options) {
// when toSql is invoked via the CLI - toSql options are under model.options

@@ -793,3 +356,3 @@ // ensure the desired format of the user option

// when the API funtion is used directly - toSql options are in options
// when the API function is used directly - toSql options are in options
// ensure the desired format of the user option

@@ -808,3 +371,3 @@ if (options && (options.toSql.user || options.toSql.locale)){

const { warning, error } = makeMessageFunction(model, options);
const { warning, error } = makeMessageFunction(model, options, 'to.sql');

@@ -823,24 +386,13 @@ // Verify options

// It doesn't make much sense to use 'sqlite' dialect with associations
if (options.toSql.dialect === 'sqlite' && options.toSql.associations !== 'joins') {
warning(null, null, `Option "{ toSql.dialect: 'sqlite' }" should always be combined with "{ toSql.assocs: 'joins' }"`);
}
if(options.toSql.dialect !== 'hana') {
// CDXCORE-465, 'quoted' and 'hdbcds' are to be used in combination with dialect 'hana' only
if(['quoted', 'hdbcds'].includes(options.toSql.names)) {
error(null, null, `Option "{ toSql.dialect: '${options.toSql.dialect}' }" cannot be combined with "{ toSql.names: '${options.toSql.names}' }"`);
error(null, null, `Option "{ toSql.dialect: '${options.toSql.dialect}' }" can't be combined with "{ toSql.names: '${options.toSql.names}' }"`);
}
// No non-HANA SQL for HDI
if(options.toSql.src === 'hdi') {
error(null, null, `Option "{ toSql.dialect: '${options.toSql.dialect}' }" cannot be combined with "{ toSql.src: '${options.toSql.src}' }"`);
error(null, null, `Option "{ toSql.dialect: '${options.toSql.dialect}' }" can't be combined with "{ toSql.src: '${options.toSql.src}' }"`);
}
}
// Because (even HANA) SQL cannot deal with associations in mixins that are published in the same view,
// the association processing must at least be 'mixin', even if callers specified 'assocs'
if (forHanaOptions.associations === 'assocs') {
forHanaOptions.associations = 'mixin';
}
// FIXME: Should not be necessary

@@ -880,3 +432,3 @@ forHanaOptions.alwaysResolveDerivedTypes = true;

}
// ----------- toRename -----------
// ----------- toRenameWithCsn -----------

@@ -904,7 +456,6 @@ // FIXME: Not yet supported, only in beta mode

// legal for filenames on all operating systems (e.g. ':', '\' or '/').
// messages : an array of strings with warnings (if any)
// }
// Throws a CompilationError on errors.
function toRename(model, options) {
const { error, warning } = makeMessageFunction(model, options);
function toRenameWithCsn(csn, options) {
const { error, warning } = makeMessageFunction(csn, options, 'to.rename');

@@ -918,4 +469,4 @@ // In case of API usage the options are in the 'options' argument

// Provide defaults and merge options with those from model
options = mergeOptions({ toRename : getDefaultBackendOptions().toRename }, model.options, options);
// Provide defaults and merge options
options = mergeOptions({ toRename : getDefaultBackendOptions().toRename }, options);

@@ -947,18 +498,58 @@ // Backward compatibility for old naming modes

// FIXME: Currently, '--to-rename' implies transformation for HANA (transferring the options to forHana)
let forHanaAugmented = transformForHana(model, mergeOptions(options, { forHana : options.toRename } ));
// FIXME: Currently, 'toRename' implies transformation for HANA (transferring the options to forHana)
let forHanaCsn = transformForHanaWithCsn(csn, mergeOptions(options, { forHana : options.toRename } ));
// forHanaCsn looses empty contexts and services, add them again so that toRename can calculate the namespaces
forEachDefinition(csn, (artifact, artifactName) => {
if(['context', 'service'].includes(artifact.kind) && forHanaCsn.definitions[artifactName] === undefined) {
forHanaCsn.definitions[artifactName] = artifact;
}
});
// Assemble result
let result = {
rename : toRenameDdl(forHanaAugmented),
rename : toRenameDdl(forHanaCsn, options),
options
};
// Transfer warnings (errors would have resulted in an exception before we come here)
if (forHanaAugmented.messages && forHanaAugmented.messages.length > 0) {
result.messages = forHanaAugmented.messages;
}
return result;
}
function alterConstraintsWithCsn(csn, options) {
const { error } = makeMessageFunction(csn, options, 'manageConstraints');
// Requires beta mode
if (!isBetaEnabled(options, 'foreignKeyConstraints'))
error(null, null, 'ALTER TABLE statements for adding/modifying referential constraints are only available in beta mode');
const {
drop, alter, names, src
} = options.manageConstraints || {};
if(drop && alter)
error(null, null, 'Option “--drop” can\'t be combined with “--alter”');
options.toSql = {
dialect: 'hana',
names: names || 'plain'
}
const transformedOptions = transformSQLOptions(csn, options);
const mergedOptions = mergeOptions(transformedOptions.options, { forHana : transformedOptions.forHanaOptions });
const forSqlCsn = transformForHanaWithCsn(csn, mergedOptions);
const intermediateResult = manageConstraints(forSqlCsn, mergedOptions);
if(options.testMode !== true)
return intermediateResult;
// if in testmode, return a string containing all the artifacts
let resultString = '';
const extension = src && src === 'hdi' ? 'hdbconstraint' : 'sql';
for(const id in intermediateResult){
const initialComment = `--$ --- ${id}.${extension} ---\n\n`;
resultString += initialComment;
resultString += intermediateResult[id];
resultString += '\n\n'
}
return resultString;
}
// ----------- toCsn -----------
// TODO: delete

@@ -977,3 +568,3 @@ // Generate compact CSN for augmented CSN 'model'

function toCsn(model, options) {
const { warning } = makeMessageFunction(model);
const { warning } = makeMessageFunction(model, options, 'to.csn');
// In case of API usage the options are in the 'options' argument

@@ -986,3 +577,3 @@ // put the OData specific options under the 'options.toCsn' wrapper

// Merge options with those from model
// Merge options with those from XSN model
options = mergeOptions({ toCsn : {} }, model.options, options);

@@ -993,3 +584,3 @@

return compactModel(model);
return compactModel(model, options);
}

@@ -1008,16 +599,14 @@

toHana: {
names : 'plain',
associations: 'assocs',
names : 'plain'
},
toOdata: {
version : 'v4',
odataFormat: 'flat',
odataFormat: 'flat'
},
toRename: {
names: 'hdbcds',
names: 'hdbcds'
},
toSql: {
names : 'plain',
associations: 'joins',
dialect: 'sqlite',
dialect: 'plain'
},

@@ -1043,5 +632,3 @@ };

module.exports = {
toHana,
toHanaWithCsn,
toOdata,
toOdataWithCsn,

@@ -1052,6 +639,3 @@ preparedCsnToEdmx,

preparedCsnToEdmAll,
toCdl,
toCdlWithCsn,
toSwagger,
toSql,
toSqlWithCsn,

@@ -1061,3 +645,4 @@ renderSqlWithCsn,

getDefaultBackendOptions,
toRename,
toRenameWithCsn,
alterConstraintsWithCsn
}
// Functions for dictionaries (Objects without prototype)
//
// Warning: this is Core-compiler only stuff
'use strict';
// New-style duplicate representation - use the style below for artifacts and
// _combined only
function dictAdd( dict, name, entry, duplicateCallback ) {
const found = dict[name];
if (!found) {
dict[name] = entry;
return entry;
}
if (!found.$duplicates) { // not already with duplicates
found.$duplicates = [];
// Redefinitions from second source -> also complain in first source
if (duplicateCallback && name) // do not complain with empty name ''
duplicateCallback( name, found.name.location, found );
}
found.$duplicates.push( entry );
if (entry.$duplicates instanceof Array)
found.$duplicates.push( ...entry.$duplicates )
else if (duplicateCallback && name) // do not complain with empty name ''
duplicateCallback( name, entry.name.location, entry );
entry.$duplicates = true;
return found;
}
function dictForEach( dict, callback ) {
for (const name in dict) {
const entry = dict[name];
if (entry instanceof Array) {
entry.forEach( callback );
}
else {
callback( entry );
if (entry.$duplicates instanceof Array)
entry.$duplicates.forEach( callback );
}
}
}
// Add entry `entry` with key `name` to the dictionary `dict`. If an entry

@@ -10,6 +49,5 @@ // (called `found`) with the same name is already defined, call

// `filename`s are different, call the callback again on `found.name.location`.
function addToDict( dict, name, entry, messageCallback ) {
function dictAddArray( dict, name, entry, messageCallback ) {
var found = dict[name];
if (!found || found.builtin) { // do not replace a builtin definition
// XSN TODO: store duplicate definitions in prop $duplicates, do not use array (except $combined?)
dict[name] = entry; // also ok if array (redefined)

@@ -27,4 +65,2 @@ return entry;

messageCallback( name, found.name.location, found );
if (messageCallback !== null)
found.$duplicate = true;
}

@@ -42,9 +78,5 @@ }

messageCallback( name, found.name.location, found );
if (messageCallback !== null)
found.$duplicate = true;
}
if (messageCallback && name)
messageCallback( name, entry.name.location, entry );
if (messageCallback !== null)
entry.$duplicate = true;
}

@@ -54,21 +86,2 @@ return entry;

// TODO: either use dict[lengthSymbol] to store length or just use (after perf
// test) Object.keys(dict).length
// var lengthSymbol = typeof Symbol !== 'undefined' && Symbol.for && Symbol.for('dictLength')
const orderedDictionaries = {
elements: '_elementsIndexNo',
foreignKeys: '_foreignKeysIndexNo',
params: '_paramsIndexNo'
};
function addToDictWithIndexNo( parent, env, name, entry, messageCallback ) {
addToDict( parent[env], name, entry, messageCallback );
let ordered = orderedDictionaries[env];
if (!ordered)
return;
if (!(ordered in parent))
Object.defineProperty( parent, ordered, { value: 0, configurable: true, writable: true } );
entry.indexNo = ++parent[ordered];
}
function clearDict( parent, env, inPlace ) {

@@ -83,6 +96,2 @@ if (!inPlace || !parent[env])

}
let ordered = orderedDictionaries[env];
if (ordered)
delete parent[ordered];
return ordered;
}

@@ -106,4 +115,4 @@

module.exports = {
addToDict,
addToDictWithIndexNo,
dictAdd, dictForEach,
dictAddArray,
clearDict,

@@ -110,0 +119,0 @@ pushToDict,

@@ -44,2 +44,3 @@ module.exports = {

'SESSION_USER',
'SYSTEM_USER',
'SYSUUID',

@@ -46,0 +47,0 @@ ],

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

const { analyseCsnPath, traverseQuery } = require('../model/csnRefs');
const { copyPropIfExist } = require('../utils/objectUtils');
/**
* Create a location with location properties `filename` and `start` from
* argument `start`, and location property `end` from argument `end`.
* Create a location with properties `file`, `line` and `col` from argument
* `start`, and properties `endLine` and `endCol` from argument `end`.
*
* @param {XSN.WithLocation} start
* @param {XSN.WithLocation} end
* @returns {CSN.Location}
*/

@@ -18,7 +23,10 @@ function combinedLocation( start, end ) {

return start.location;
return {
filename: start.location.filename,
start: start.location.start,
end: end && end.location && end.location.end,
const loc = {
file: start.location.file,
line: start.location.line,
col: start.location.col,
};
copyPropIfExist(loc, 'endLine', end.location);
copyPropIfExist(loc, 'endCol', end.location);
return loc;
}

@@ -30,9 +38,11 @@

* @param {string} filename
* @returns {XSN.Location}
* @returns {CSN.Location}
*/
function emptyLocation(filename) {
return {
filename,
start: { offset: 0, line: 1, column: 1 },
end: { offset: 0, line: 1, column: 1 },
file: filename,
line: 1,
col: 1,
endLine: 1,
endCol: 1,
};

@@ -43,12 +53,12 @@ }

* Create an empty location object with the given file name.
* The end line/column is not set and the location is therefore $weak.
* The end line/column is not set and therefore the location is weak.
*
* @param {string} filename
* @returns {XSN.Location}
* @returns {CSN.Location}
*/
function emptyWeakLocation(filename) {
return {
filename,
start: { offset: 0, line: 1, column: 1 },
$weak: true,
file: filename,
line: 1,
col: 1,
};

@@ -60,3 +70,3 @@ }

*
* @returns {XSN.Location}
* @returns {CSN.Location}
*/

@@ -68,20 +78,7 @@ function builtinLocation() {

/**
* Normalize location: to old-style at the moment, TODO: should switch to new-style
*
* @param {object} loc CSN style location
* @returns {XSN.Location}
* @returns {CSN.Location}
*/
function normalizeLocation( loc ) {
// `file` may be undefined, though it should not.
// TODO: `loc` may also be a string from $location
if (!loc || typeof loc !== 'object' || !('file' in loc))
return loc;
const location = {
filename: loc.file,
start: { line: loc.line || 0, column: loc.col || 0 },
end: { line: loc.endLine || loc.line || 0, column: loc.endCol || loc.col || 0 },
};
if (!loc.endLine)
location.$weak = true;
return location;
// TODO: remove function
return loc;
}

@@ -93,7 +90,10 @@

* ASSUMPTION: all entries in the dictionary have a property `location` and
* `location.filename` has always the same value.
* `location.file` has always the same value.
*
* TODO: remove this function - if we really want to have dictionary locations,
* set them in the CDL parser, e.g. via a symbol.
*
* @param {object} dict
* @param {XSN.Location} [extraLocation]
* @returns {XSN.Location}
* @param {CSN.Location} [extraLocation]
* @returns {CSN.Location}
*/

@@ -107,2 +107,3 @@ function dictLocation( dict, extraLocation ) {

/** @type {CSN.Location[]} */
const locations = [].concat( ...dict.map( _objLocations ) );

@@ -112,6 +113,20 @@ if (extraLocation)

const min = locations.reduce( (a, b) => (a.start.offset < b.start.offset ? a : b) );
const max = locations.reduce( (a, b) => ((a.end || a.start).offset > (b.end || b.start).offset ? a : b) );
return { filename: min.filename, start: min.start, end: max.end };
const min = locations.reduce( (a, b) => (a.line < b.line || (a.line === b.line && a.col < b.col) ? a : b) );
const max = locations.reduce( (a, b) => {
const lineA = (a.endLine || a.line);
const lineB = (b.endLine || b.line);
return (lineA > lineB || (lineA === lineB && (a.endCol || a.col) > (b.endCol || b.col)) ? a : b);
});
return {
file: min.file,
line: min.line,
col: min.col,
endLine: max.endLine,
endCol: max.endCol,
};
}
dictLocation.end = (dict) => {
const loc = dictLocation( dict );
return loc && { file: loc.file, line: loc.endLine, col: loc.endCol };
};

@@ -124,2 +139,4 @@ function _objLocations( obj ) {

function constructSemanticLocationFromCsnPath(csnPath, model) {
if (!model)
return null;
// Copy because this function shift()s from the path.

@@ -126,0 +143,0 @@ csnPath = [ ...csnPath ];

@@ -7,80 +7,11 @@ // Functions and classes for syntax messages

const { normalizeLocation } = require('./location');
const { isDeprecatedEnabled } = require('./model');
const { centralMessages, centralMessageTexts } = require('./message-registry');
const { constructSemanticLocationFromCsnPath } = require('./location');
const { copyPropIfExist } = require('../utils/objectUtils');
const _messageIdsWithExplanation = require('../../share/messages/message-explanations.json').messages;
const fs = require('fs');
const path = require('path');
// For messageIds, where no severity has been provided via code (central def)
const standardSeverities = {
'syntax-anno-after-struct': ['Error'],
'syntax-anno-after-enum': ['Error'],
'syntax-anno-after-params': ['Error'],
'ref-undefined-def': 'Error',
'ref-undefined-art': 'Error',
'ref-rejected-on': ['Error'], // TODO: 'Error'
'anno-undefined-def': 'Info', // for annotate statement (for CSN or CDL path cont)
'anno-undefined-art': 'Info', // for annotate statement (for CDL path root)
'anno-undefined-element': 'Info',
'anno-undefined-action': 'Info',
'anno-undefined-param': 'Info',
}
// For messageIds, where no text has been provided via code (central def)
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',
'syntax-anno-after-enum': 'Avoid annotation assignments after enum definitions',
'syntax-anno-after-params': 'Avoid annotation assignments after parameters',
'ref-undefined-def': {
std: 'Artifact $(ART) has not been found',
element: 'Artifact $(ART) has no element $(MEMBER)'
},
'ref-undefined-art': 'No artifact has been found with name $(NAME)',
'ref-undefined-element': {
std: 'Element $(ART) has not been found',
element: 'Artifact $(ART) has no element $(MEMBER)'
},
'ref-rejected-on': {
mixin: 'Do not refer to a mixin like $(ID) in the explicit ON of a redirection',
alias: 'Do not refer to a source element (via table alias $(ID)) in the explicit ON of a redirection',
},
'anno-undefined-def': 'Artifact $(ART) has not been found',
'anno-undefined-art': 'No artifact has been found with name $(NAME)',
'anno-undefined-element': {
std: 'Element $(ART) has not been found',
element: 'Artifact $(ART) has no element $(MEMBER)',
enum: 'Artifact $(ART) has no enum $(MEMBER)'
},
'anno-undefined-action': {
std: 'Action $(ART) has not been found',
action: 'Artifact $(ART) has no action $(MEMBER)'
},
'anno-undefined-param': {
std: 'Parameter $(ART) has not been found',
param: 'Artifact $(ART) has no parameter $(MEMBER)'
},
'expected-const': 'A constant value is expected here',
'expected-struct': 'An aspect or a non-query entity without parameters is expected here',
'expected-context': 'A context or service is expected here',
'expected-type': 'A type or an element of a type is expected here',
'expected-no-query': 'A type or an element of a type is expected here',
'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',
'i18n-different-value': 'Different translation for key $(PROP) of language $(OTHERPROP) in unrelated layers'
}
const availableMessageExplanations = {
'check-proper-type': true,
'check-proper-type-of': true,
'extend-repeated-intralayer': true,
'extend-unrelated-layer': true,
'redirected-to-ambiguous': true,
'redirected-to-unrelated': true,
'rewrite-not-supported': true,
};
/**

@@ -96,2 +27,35 @@ * Returns true if at least one of the given messages is of severity "Error"

/**
* Returns true if at least one of the given messages is of severity "Error"
* and *cannot* be reclassified to a warning.
*
* @param {CSN.Message[]} messages
* @param {string} moduleName
* @returns {boolean}
*/
function hasNonDowngradableErrors( messages, moduleName ) {
return messages && messages.some( m => m.severity === 'Error' &&
(!m.messageId || !isDowngradable( m.messageId, moduleName )));
}
/**
* Returns true if the given message id exist in the central message register and is
* downgradable, i.e. an error can be reclassified to a warning or lower.
* Returns false if the messages is an errorFor the given moduleName.
*
* @param {string} messageId
* @param {string} moduleName
* @returns {boolean}
*/
function isDowngradable( messageId, moduleName ) {
if (!centralMessages[messageId])
return false;
const msg = centralMessages[messageId];
return (!msg.errorFor || !msg.errorFor.includes(moduleName)) &&
(msg.severity !== 'Error' ||
msg.configurableFor === true || // useful with error for syntax variants
msg.configurableFor && msg.configurableFor.includes( moduleName ));
}
/**
* Return gnu-style error string for location `loc`:

@@ -102,3 +66,3 @@ * - 'File:Line:Col' without `loc.end`

*
* @param {CSN.Location|XSN.Location} location
* @param {CSN.Location|CSN.Location} location
* @param {boolean} [normalizeFilename]

@@ -110,19 +74,19 @@ */

const loc = normalizeLocation( location );
let filename = (loc.filename && normalizeFilename)
? loc.filename.replace( /\\/g, '/' )
: loc.filename;
if (!(loc instanceof Object) || !loc.start)
let filename = (loc.file && normalizeFilename)
? loc.file.replace( /\\/g, '/' )
: loc.file;
if (!(loc instanceof Object))
return loc;
if (!loc.start || !loc.start.line) {
if (!loc.line) {
return filename;
}
else if (!loc.end || loc.$weak) {
return (loc.start.column)
? `${filename}:${loc.start.line}:${loc.start.column}`
: `${filename}:${loc.start.line}`;
else if (!loc.endLine) {
return (loc.col)
? `${filename}:${loc.line}:${loc.col}`
: `${filename}:${loc.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}`;
return (loc.line === loc.endLine)
? `${filename}:${loc.line}:${loc.col}-${loc.endCol}`
: `${filename}:${loc.line}.${loc.col}-${loc.endLine}.${loc.endCol}`;
}

@@ -142,4 +106,4 @@ }

* Creates an instance of CompilationError.
* @param {array} messages vector of errors (CompileMessage and errors from peg.js)
* @param {object} [model] the CSN model
* @param {array} messages vector of errors
* @param {XSN.Model} [model] the XSN model, only to be set with options.attachValidNames
* @param {string} [text] Text of the error

@@ -154,11 +118,10 @@ * @param {any} args Any args to pass to the super constructor

...args );
this.errors = messages; // TODO: remove
// this.messages = messages; // use this instead
this.messages = messages;
/** @type {object} model */
this.model;
this.model; // TODO: what is this for?
/** @type {boolean} model */
this.hasBeenReported = false; // TODO: remove this bin/cdsc.js specifics
// TODO: remove property `model`
Object.defineProperty( this, 'model', { value: model, configurable: true } );
Object.defineProperty( this, 'model', { value: model || undefined, configurable: true } );
}

@@ -170,2 +133,5 @@ toString() { // does not really help -> set message

}
get errors() {
return this.messages;
}
}

@@ -177,3 +143,2 @@

* @class CompileMessage
* @extends {Error}
*/

@@ -191,3 +156,3 @@ class CompileMessage {

*/
constructor(location, msg, severity = 'Error', id = null, home = null) {
constructor(location, msg, severity = 'Error', id = null, home = null, moduleName = null) {
this.message = msg;

@@ -198,3 +163,3 @@ this.location = normalizeLocation( location );

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

@@ -204,3 +169,6 @@ if (id)

// this.messageId = id; // ids not yet finalized
if (moduleName)
Object.defineProperty( this, '$module', { value: moduleName } );
}
toString() { // should have no argument...

@@ -210,66 +178,42 @@ return messageString( this, undefined, true ); // no message-id before finalization!

}
/**
* Class for individual compile message.
*
* @class CompileMessage
* @extends {Error}
*/
class DebugCompileMessage extends Error {
/**
* Creates an instance of DebugCompileMessage, used with option `internalMsg`
* @param {any} location Location of the message
* @param {string} msg The message text
* @param {CSN.MessageSeverity} [severity='Error'] Severity: Debug, Info, Warning, Error
* @param {string} [id] The ID of the message - visible as property messageId
* @param {any} [home]
*
* @memberOf CompileMessage
*/
constructor(location, msg, severity = 'Error', id = null, home = null) {
super(msg);
this.location = normalizeLocation( location );
this.$location = dollarLocation( this.location );
this.validNames = null;
if (home) // semantic location, e.g. 'entity:"E"/element:"x"'
this.home = home;
this.severity = severity;
if (id)
Object.defineProperty( this, 'messageId', { value: id } );
// this.messageId = id; // ids not yet finalized
}
toString() { // should have no argument...
return messageString( this, undefined, true ); // no message-id before finalization!
}
}
/**
* Temporary v1 function to convert an "old-style" location to "new-style".
*
* @param {CSN.Location} location
* @return {CSN.Location}
* @todo Remove
*/
function dollarLocation( location ) {
const file = location && location.filename || undefined;
const start = file && location.start || { line: undefined, column: undefined };
const end = file && !location.$weak && location.end || { line: undefined, column: undefined };
return {
const file = location && location.file || undefined;
if (!file)
return {};
const loc = {
file,
line: start.line,
col: start.column,
endLine: end.line,
endCol: end.column,
line: location.line,
col: location.col,
address: undefined,
};
copyPropIfExist(loc, 'endLine', location);
copyPropIfExist(loc, 'endCol', location);
// TODO:
// return {
// ...location,
// address: undefined,
// };
return loc;
}
/**
* Handle compiler messages, i.e. throw a compiler exception if there are
* errors, otherwise sort the messages (see compareMessage()).
* Handle compiler messages, i.e. throw a compiler exception if there are errors.
*
* @param {object} model
* @param {object} model CSN or XSN
* @param {CSN.Options} [options]
* @deprecated Use throwWithError() from makeMessageFunction instead.
*/
function handleMessages( model, options = model.options || {} ) {
const messages = options.messages || model.messages;
function handleMessages( model, options = {} ) {
const messages = options.messages;
if (messages && messages.length) {
messages.sort( compareMessage );
if (hasErrors( messages ))
throw new CompilationError( messages, model );
throw new CompilationError( messages, options.attachValidNames && model );
}

@@ -287,15 +231,72 @@ return model;

/**
* @param {string} severity
* @deprecated
* Reclassify the given message's severity using:
*
* 1. The specified severity: either centrally provided or via the input severity
* - when generally specified as 'Error', immediately return 'Error'
* if message is not specified as configurable (for the given module name)
* - when generally specified otherwise, immediately return 'Error'
* if message is specified as being an error for the given module name
* 2. User severity wishes in option `severities`: when provided and no 'Error' has
* been returned according to 1, return the severity according to the user wishes.
* 3. Otherwise, use the specified severity.
*
* @param {string} id
* @param {CSN.MessageSeverity} severity
* @param {object} severities
* @param {string} moduleName
* @returns {CSN.MessageSeverity}
*
* TODO: we should pass options as usual
*/
function reclassifiedSeverity( id, severity, severities, moduleName, deprecatedDowngradable ) {
// The following is a bug workaround: the input severity (at least 'Error')
// must be as provided centrally for that module - TODO: test with testMode,
// probably also that error() is only used if message is always `Error` in that module
if (severity === 'Error')
return 'Error';
const spec = centralMessages[id] || { severity };
if (spec.severity === 'Error') {
const { configurableFor } = spec;
if (!(Array.isArray( configurableFor )
? configurableFor.includes( moduleName )
: configurableFor && (configurableFor !== 'deprecated' || deprecatedDowngradable)))
return 'Error';
}
else {
const { errorFor } = spec;
if (Array.isArray( errorFor ) && errorFor.includes( moduleName ))
return 'Error';
}
return normalizedSeverity( severities[id] ) || spec.severity;
}
function normalizedSeverity( severity ) {
if (typeof severity !== 'string')
return (severity === null) ? 'Debug' : 'Error';
const s = severitySpecs[severity.toLowerCase()];
return s && s.name || 'Error';
return (severity == null) ? null : 'Error';
let s = severitySpecs[ severity.toLowerCase() ];
return s ? s.name : 'Error';
}
/**
* Reclassifies all messages according to the current module.
* This is required because if throwWithError() throws and the message's
* severities has `errorFor` set, then the message may still appear to be a warning.
*
* TODO: this actually likely needs to be called by the backend module at the beginning!
*
* @param {CSN.Message[]} messages
* @param {object} severities
* @param {string} moduleName
*/
function reclassifyMessagesForModule(messages, severities, moduleName, deprecatedDowngradable) {
for (const msg of messages) {
if (msg.messageId && msg.severity !== 'Error')
msg.severity = reclassifiedSeverity(msg.messageId, msg.severity, severities, moduleName, deprecatedDowngradable);
}
}
/**
* Compare two severities. Returns 0 if they are the same, and <0 if
* `a` has a lower `level` than `b` according to {@link severitySpecs}.
* `a` has a lower `level` than `b` according to {@link severitySpecs},
* where "lower" means: comes first when sorted.
*

@@ -312,97 +313,342 @@ * compareSeverities('Error', 'Info') => Error < Info => -1

const bSpec = severitySpecs[b.toLowerCase()] || { level: 10 };
// Levels are "inverted" in severitySpecs, so revert order here:
return aSpec.level - bSpec.level;
}
// Return message function to issue errors, warnings, info and debug messages.
// Messages are put into the `messages` property of argument `options` or `model`.
// If those do not exist, define a non-enumerable property `messages` in `model`.
function getMessageFunction( model, options = model.options || {}, transform = null ) {
let messages = options.messages || model.messages ||
Object.defineProperty( model, 'messages',
{ value: [], configurable: true, writable: true } )
.messages;
let config = options.severities || {};
/**
* @todo This was copied from somewhere just to make CSN paths work.
* @param {CSN.Model} model
* @param {CSN.Path} path
*/
function searchForLocation( model, path ) {
if (!model)
return null;
// Don't display a location if we cannot find one!
let lastLocation = null;
/** @type {object} */
let currentStep = model;
for (const step of path) {
if (!currentStep)
return lastLocation;
currentStep = currentStep[step];
if (currentStep && currentStep.$location)
lastLocation = currentStep.$location;
}
return function message( id, location, home, params = {}, severity = undefined, texts = undefined ) {
if (!severity) // TODO: check that they are always eq per messageId
severity = standardSeverities[id];
let s = normalizedSeverity( severity );
if ((s !== 'Error' || severity instanceof Array) && id && id in config )
s = normalizedSeverity( config[id] );
let text = (typeof params === 'string')
? params
: messageText( texts || standardTexts[id], params, transform );
const homename = (typeof home === 'string') ? home : homeName(home);
let msg = (options.internalMsg)
? new DebugCompileMessage( location, text, s, id, homename )
: new CompileMessage( location, text, s, id, homename );
messages.push( msg );
const definition = typeof home !== 'string' && homeName( home, true );
if (definition)
msg.$location.address = { definition };
return msg;
}
return lastLocation;
}
/**
* Wrapper function around the old getMessageFunction().
* Used to make backporting from v2 easier.
* Create the `message` function to emit messages.
*
* Differences to v2: Last argument "severity" was added because no central message registry exists.
*
* @param {XSN.Model} model
* @example
* ```
* const { makeMessageFunction } = require(‘../base/messages’);
* function module( …, options ) {
* const { message, info, throwWithError } = makeMessageFunction( model, options, moduleName );
* // [...]
* message( 'message-id', <location>, <text-arguments>, <severity>, <text> );
* info( 'message-id', <location>, [<text-arguments>,] <text> );
* // [...]
* throwWithError();
* }
* ```
* @param {object} model
* @param {CSN.Options} [options]
* @param {any} [transform]
* @param {string} [moduleName]
*/
function makeMessageFunction( model, options = model.options || {}, transform = null ) {
const message = getMessageFunction(model, options, transform);
function makeMessageFunction( model, options = model.options || {}, moduleName = null ) {
const hasMessageArray = !!options.messages;
const severities = options.severities || {};
const deprecatedDowngradable = isDeprecatedEnabled( options, 'downgradableErrors' );
/**
* Array of collected compiler messages. Only use it for debugging. Will not
* contain the messages created during a `callTransparently` call.
*
* @type {CSN.Message[]}
*/
let messages = options && options.messages || [];
function makeWrapper(defaultSeverity) {
return (id, loc, params, texts, severity = defaultSeverity) => {
const isTuple = Array.isArray(loc); // [ location, user ]
if (isTuple)
return message(id, loc[0], loc[1], params, severity, texts);
return message(id, loc, null, params, severity, texts);
};
return {
message, error, warning, info, debug,
messages, throwWithError, callTransparently
};
function _message(id, location, textOrArguments, severity, texts = null) {
_validateFunctionArguments(id, location, textOrArguments, severity, texts);
// Special case for _info, etc.: textOrArguments may be a string.
if (typeof textOrArguments === 'string') {
texts = { std: textOrArguments };
textOrArguments = {};
}
if (id) {
_checkId(id, severity);
severity = reclassifiedSeverity( id, severity, severities, moduleName, deprecatedDowngradable );
}
const [ fileLocation, semanticLocation, definition ] = _normalizeMessageLocation(location);
const text = messageText( texts || centralMessageTexts[id], textOrArguments );
/** @type {CSN.Message} */
const msg = new CompileMessage( fileLocation, text, severity, id, semanticLocation, moduleName );
if (options.internalMsg)
msg.error = new Error( 'stack' );
if (definition)
msg.$location.address = { definition };
messages.push( msg );
if (!hasMessageArray)
console.error( messageString( msg ) );
return msg;
}
return {
message: makeWrapper(null),
error: makeWrapper('Error'),
warning: makeWrapper('Warning'),
info: makeWrapper('Info'),
debug: makeWrapper('Debug'),
messages: options.messages || model.messages,
throwWithError: () => handleMessages(model, options),
};
/**
* Validate the arguments for the message() function. This is needed during the transition
* to the new makeMessageFunction().
*/
function _validateFunctionArguments(id, location, textArguments, severity, texts) {
if (!options.testMode)
return;
if (id !== null && typeof id !== 'string')
_expectedType('id', id, 'string')
if (location !== null && location !== undefined && !Array.isArray(location) && typeof location !== 'object')
_expectedType('location', location, 'XSN/CSN location, CSN path')
if (severity != null && typeof severity !== 'string')
_expectedType('severity', severity, 'string')
const isShortSignature = (typeof textArguments === 'string') // textArguments => texts
if (isShortSignature) {
if (texts)
throw new Error('No "texts" argument expected because text was already provided as third argument.');
} else {
if (textArguments !== undefined && typeof textArguments !== 'object')
_expectedType('textArguments', textArguments, 'object')
if (texts !== undefined && typeof texts !== 'object' && typeof texts !== 'string')
_expectedType('texts', texts, 'object or string')
}
function _expectedType(field, value, type) {
throw new Error(`Invalid argument type for ${ field }! Expected ${ type } but got ${ typeof value }. Do you use the old function signature?`);
}
}
/**
* Normalize the given location. Location may be a CSN path, XSN/CSN location or an
* array of the form `[CSN.Location, user, suffix]`.
*
* @param {any} location
* @returns {[CSN.Location, string, string]} Location, semantic location and definition.
*/
function _normalizeMessageLocation(location) {
if (!location)
// e.g. for general messages unrelated to code
return [ null, null, null ]
if (typeof location === 'object' && !Array.isArray(location))
// CSN/CSN.Location (with line/endLine, col/endCol)
return [ normalizeLocation(location), location.home || null, null ]
const isCsnPath = (typeof location[0] === 'string');
if (isCsnPath) {
return [
searchForLocation( model, location ),
constructSemanticLocationFromCsnPath( location, model ),
location[1] // location[0] is 'definitions'
];
}
let semanticLocation = location[1] ? homeName( location[1], false ) : null;
if (location[2]) // optional suffix
semanticLocation += '/' + location[2]
const definition = location[1] ? homeName( location[1], true ) : null;
// If no XSN location is given, check if we can use the one of the artifact
let fileLocation = location[0];
if (!fileLocation && location[1])
fileLocation = location[1].location || location[1].$location || null;
return [ fileLocation, semanticLocation, definition ];
}
/**
* Check that the central message severity matches the given one.
*
* @param {string} id
* @param {CSN.MessageSeverity} severity
*/
function _checkId(id, severity) {
if (!options.testMode || !severity)
return;
if (!centralMessages[id]) {
centralMessages[id] = { severity, throughMessageCall: true };
}
else if (centralMessages[id].severity !== severity) {
// TODO: Enable if getMessageFunction() is removed because that function
// does message reclassification before calling _message();
//
// if (centralMessages[id].throughMessageCall) {
// throw new Error(`Mismatch for message '${ id }' between provided severity '${ severity }' and previous call with '${ centralMessages[id].severity }'`)
// } else {
// throw new Error(`Mismatch for message '${ id }' between provided severity '${ severity }' and central one '${ centralMessages[id].severity }'`)
// }
}
}
/**
* Create a compiler message for model developers.
*
* @param {string} id Message ID
* @param {[CSN.Location, XSN.Artifact]|CSN.Path|CSN.Location|CSN.Location} location
* Either a (XSN/CSN-style) location, a tuple of file location
* and "user" (address) or a CSN path a.k.a semantic location path.
* @param {object} [textArguments] Text parameters that are replaced in the texts.
* @param {string|object} [texts]
*/
function message(id, location, textArguments = null, texts = null) {
if (!id)
throw new Error('A message id is missing!');
if (!centralMessages[id])
throw new Error(`Message id '${ id }' is missing an entry in the central message register!`);
return _message(id, location, textArguments, null, texts);
}
/**
* Create a compiler error message.
* @see message()
*/
function error(id, location, textOrArguments = null, texts = null) {
return _message(id, location, textOrArguments, 'Error', texts);
}
/**
* Create a compiler warning message.
* @see message()
*/
function warning(id, location, textOrArguments = null, texts = null) {
return _message(id, location, textOrArguments, 'Warning', texts);
}
/**
* Create a compiler info message.
* @see message()
*/
function info(id, location, textOrArguments = null, texts = null) {
return _message(id, location, textOrArguments, 'Info', texts);
}
/**
* Create a compiler debug message (usually not shown).
* @see message()
*/
function debug(id, location, textOrArguments = null, texts = null) {
return _message(id, location, textOrArguments, 'Debug', texts);
}
/**
* Throws a CompilationError exception if there is at least one error message
* in the model's messages after reclassifying existing messages according to
* the module name.
* If `--test-mode` is enabled, this function will only throw if the
* error *cannot* be downgraded to a warning. This is done to ensure that
* developers do not rely on certain errors leading to an exception.
*
* @param {CSN.Message[]} [msgs] Which messages to check for. Default: The ones of
* this makeMessageFunction() scope.
*/
function throwWithError(msgs = messages) {
if (!msgs || !msgs.length)
return;
reclassifyMessagesForModule(msgs, severities, moduleName); // TODO: no, at the beginning of the module
const hasError = options.testMode ? hasNonDowngradableErrors : hasErrors;
if (hasError( msgs, moduleName ))
throw new CompilationError( msgs, options.attachValidNames && model );
}
/**
* Collects all messages during the call of the callback function instead of
* storing them in the model. Returns the collected messages.
*
* @param {Function} callback
* @param {...any} args
* @returns {CSN.Message[]}
*/
function callTransparently(callback, ...args) {
const backup = messages;
messages = [];
callback(...args);
const collected = messages;
messages = backup;
return collected;
}
}
const quote = { // could be an option in the future
name: n => `“${ n }”`,
prop: p => `‘${ p }’`,
file: f => `‘${ f }’`,
code: c => `«${ c }»`,
meta: m => `‹${ m }›`,
// TODO: probably use keyword as function name, but its name would not have length 4 :-(
word: w => w.toUpperCase(), // keyword
}
const paramsTransform = {
// simple convenience:
name: quoted,
id: quoted,
alias: quoted,
anno: transformAnno,
anno: a => (a.charAt(0) === '@' ? quote.name( a ) : quote.name( '@' + a )),
delimited: n => '![' + n + ']',
file: quote.file,
prop: quote.prop,
otherprop: quote.prop,
code: quote.code,
newcode: quote.code,
kind: quote.meta,
keyword: quote.word,
// more complex convenience:
names: transformManyWith( quoted ),
art: transformArg,
target: transformArg,
type: transformArg,
token: t => t.match( /^[a-zA-Z]+$/ ) ? t.toUpperCase() : "'" + t + "'",
code: n => '`' + n + '`',
newcode: n => '`' + n + '`',
name: quoted,
names: transformManyWith( quoted ),
id: quoted,
prop: n => "'" + n + "'",
otherprop: n => "'" + n + "'",
kind: n => '"' + n + '"',
delimited: n => '![' + n + ']',
offending: tokenSymbol,
expecting: transformManyWith( tokenSymbol ),
// msg: m => m,
file: s => "'" + s.replace( /'/g, "''" ) + "'", // sync ;
};
function transformAnno( anno ) {
return (anno.charAt(0) === '@') ? quoted( anno ) : quoted( '@' + anno );
// if (anno.charAt(0) === '@')
// anno = anno.slice(1);
// return (!anno || /[^A-Za-z_0-9.]/.test(anno)) ? msgName( '@' + anno ) : '@' + anno;
function transformManyWith( t ) {
return function transformMany( many, r, args, texts ) {
const prop = ['none','one'][ many.length ];
if (!prop || !texts[prop] || args['#'] )
return many.map(t).join(', ');
r['#'] = prop; // text variant
return many.length && t( many[0] );
};
}
function quoted( name ) {
return (name) ? quote.name( name ) : '<?>'; // TODO: failure in --test-mode, then remove
}
function tokenSymbol( token ) {
if (token.match( /^[A-Z][A-Z]/ )) // keyword
return quote.word( token );
else if (token.match( /^[A-Z][a-z]/ )) // Number, Identifier, ...
return quote.meta( token );
if (token.startsWith("'") && token.endsWith("'")) // operator token symbol
return quote.prop( token.slice( 1, -1 ));
else if (token === '<EOF>')
return quote.meta( token.slice( 1, -1 ) );
else
return quote.code( token ); // should not happen
}
function transformArg( arg, r, args, texts ) {

@@ -412,3 +658,3 @@ if (!arg || typeof arg !== 'object')

if (args['#'] || args.member )
return artName( arg );
return shortArtName( arg );
if (arg._artifact)

@@ -423,3 +669,3 @@ arg = arg._artifact;

if (!prop || !texts[prop] )
return artName( arg );
return shortArtName( arg );
r['#'] = texts[ name.$variant ] && name.$variant || prop; // text variant (set by searchName)

@@ -430,12 +676,2 @@ r.member = quoted( name[prop] );

function transformManyWith( t ) {
return function transformMany( many, r, args, texts ) {
let prop = ['none','one'][ many.length ];
if (!prop || !texts[prop] || args['#'] )
return many.map(t).join(', ');
r['#'] = prop; // text variant
return many.length && t( many[0] );
};
}
const nameProp = {

@@ -449,5 +685,12 @@ enum: 'element',

if (!variant) {
let type = art._finalType && art._finalType.kind !== 'undefined' ? art._finalType : art;
art = type.target && type.target._artifact || type;
variant = ['context','service','namespace'].includes(art.kind) ? 'absolute' : 'element';
// used to mention the "effective" type in the message, not the
// originally provided one (TODO: mention that in the message text)
let type = art._effectiveType && art._effectiveType.kind !== 'undefined' ? art._effectiveType : art;
if (type.elements) { // only mentioned elements
art = type.target && type.target._artifact || type;
variant = 'element';
}
else {
variant = 'absolute';
}
}

@@ -491,6 +734,9 @@ let prop = nameProp[variant] || variant;

/** @param {XSN.Location} loc */
/**
* @param {CSN.Location} loc
* @returns {CSN.Location}
*/
function weakLocation( loc ) {
// use return { ...location, $weak: true } if that is JS standard
return { filename: loc.filename, start: loc.start, end: loc.end, $weak: true };
// no endLine/endCol
return { file: loc.file, line: loc.line, col: loc.col };
}

@@ -513,5 +759,7 @@

(err.severity||'Error') +
// TODO: use [message-id]
(err.messageId && !noMessageId ? ' ' + err.messageId + ': ' : ': ') +
err.message +
(!err.home || noHome && err.location && !err.location.$weak ? '' : ' (in ' + err.home + ')');
// even with noHome, print err.home if the location is weak
(!err.home || noHome && err.location && err.location.endLine ? '' : ' (in ' + err.home + ')');
}

@@ -540,18 +788,32 @@

* Error: cannot find value `nu` in this scope
* <source>.cds:3:11
* <source>.cds:3:11, at entity:“E”
*
* @param {CSN.Message} err
* @param {boolean} [normalizeFilename]
* @param {boolean} [noMessageId]
* @param {boolean} [noHome]
* @param {object} [config = {}]
* @param {boolean} [config.normalizeFilename] Replace windows `\` with forward slashes `/`.
* @param {boolean} [config.noMessageId]
* @param {boolean} [config.hintExplanation] If true, messages with explanations will get a "…" marker.
* @param {boolean} [config.withLineSpacer] If true, an additional line (with `|`) will be inserted between message and location.
* @returns {string}
*/
function messageStringMultiline( err, normalizeFilename, noMessageId, noHome ) {
const explainHelp = hasMessageExplanation(err.messageId) ? '…' : '';
const msgId = (err.messageId && !noMessageId) ? `[${ err.messageId }${ explainHelp }]` : '';
const home = (!err.home || (noHome && err.location && !err.location.$weak) ? '' : ' (in ' + err.home + ')');
function messageStringMultiline( err, config = {} ) {
const explainHelp = (config.hintExplanation && hasMessageExplanation(err.messageId)) ? '…' : '';
const msgId = (err.messageId && !config.noMessageId) ? `[${ err.messageId }${ explainHelp }]` : '';
const home = !err.home ? '' : ('at ' + 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;
let location = '';
if (err.location) {
location += locationString( err.location, config.normalizeFilename )
if (home)
location += ', '
}
let lineSpacer = '';
if (config.withLineSpacer) {
let additionalIndent = err.location ? `${ err.location.endLine || err.location.line || 1 }`.length : 1;
lineSpacer = `\n ${ ' '.repeat( additionalIndent ) }|`;
}
return term.asSeverity(severity, severity + msgId) + ' ' + err.message + lineSpacer + '\n ' + location + home;
}

@@ -574,3 +836,3 @@

const loc = normalizeLocation(err.location);
if (!loc || !loc.start) {
if (!loc || !loc.line) {
return '';

@@ -580,4 +842,4 @@ }

// 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;
const startLine = loc.line - 1;
const endLine = loc.endLine ? loc.endLine - 1 : startLine;

@@ -594,3 +856,3 @@ // check that source lines exists

// e.g. for single character locations it is "start + 1"
let endColumn = loc.end ? loc.end.column - 1 : loc.start.column;
let endColumn = loc.endCol ? loc.endCol - 1 : loc.col;
/** Only print N lines even if the error spans more lines. */

@@ -612,4 +874,4 @@ const maxLine = Math.min((startLine + 2), endLine);

// highlight only for one-line location; at least one character is highlighted
const highlighter = ' '.repeat(Math.max(0, loc.start.column - 1))
.padEnd(Math.max(loc.start.column, endColumn), '^');
const highlighter = ' '.repeat(Math.max(0, loc.col - 1))
.padEnd(Math.max(loc.col, endColumn), '^');
msg += indent + '| ' + term.asSeverity(severity, highlighter);

@@ -636,9 +898,9 @@ } else if (maxLine !== endLine) {

if (a.location && b.location) {
let aend = !a.location.$weak && a.location.end || { line: Number.MAX_SAFE_INTEGER, column: 0 };
let bend = !b.location.$weak && b.location.end || { line: Number.MAX_SAFE_INTEGER, column: 0 };
return ( c( a.location.filename, b.location.filename ) ||
c( a.location.start.line, b.location.start.line ) ||
c( a.location.start.column, b.location.start.column ) ||
c( aend.line, bend.line ) ||
c( aend.column, bend.column ) ||
const aEnd = a.location.endLine && a.location.endCol && a.location || { endLine: Number.MAX_SAFE_INTEGER, endCol: Number.MAX_SAFE_INTEGER };
const bEnd = b.location.endLine && b.location.endCol && b.location || { endLine: Number.MAX_SAFE_INTEGER, endCol: Number.MAX_SAFE_INTEGER };
return ( c( a.location.file, b.location.file ) ||
c( a.location.line, b.location.line ) ||
c( a.location.col, b.location.col ) ||
c( aEnd.endLine, bEnd.endLine ) ||
c( aEnd.endCol, bEnd.endCol ) ||
c( homeSortName( a ), homeSortName( b ) ) ||

@@ -671,9 +933,13 @@ c( a.message, b.message ) );

// Return sort-relevant part of semantic location (after the ':').
// Messages without semantic locations are considered smaller (for syntax errors)
// and (currently - should not happen in v2) larger for other messages.
/**
* Return sort-relevant part of semantic location (after the ':').
* Messages without semantic locations are considered smaller (for syntax errors)
* and (currently - should not happen in v2) larger for other messages.
*
* @param {CSN.Message} msg
*/
function homeSortName( { home, messageId } ) {
return (!home)
? (messageId && messageId.startsWith( 'syntax-' ) ? '' : '~')
: home.substring( home.indexOf( ':' ) + 1 ); // i.e. starting with the \"
: home.substring( home.indexOf(':') ); // i.e. starting with the ':', is always there
}

@@ -698,9 +964,15 @@

const hash = messageHash(msg);
const present = seen.has(hash);
if(!present){
if (!seen.has(hash)) {
seen.set(hash, msg);
} else if( msg.location && msg.location.end ) {
// message already present, replace only if current msg is the more precise one
if( msg.location.end.column > msg.location.start.column )
} else if (msg.location) {
const existing = seen.get(hash);
// If this messages has an end but the existing does not, then the new message is more precise.
// If both messages do (or don't) have an endLine, then compare them based on their location.
// Assume that a message is more precise if it comes later (i.e. may be included in the other).
if (msg.location.endLine && !existing.location.endLine ||
(!msg.location.endLine === !existing.location.endLine && compareMessage(msg, existing) > 0)) {
seen.set(hash, msg);
}
}

@@ -713,13 +985,21 @@ }

function shortArtName( art ) {
const { name } = art;
if ([ 'select', 'action', 'alias', 'param' ].every( n => name[n] == null ) &&
!name.absolute.includes(':'))
return quote.name( name.element ? `${ name.absolute }:${ name.element }` : name.absolute );
return artName( art );
}
function artName( art, omit ) {
let name = art.name;
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
r.push( (art.kind === 'block' ? 'block:' : 'query:') + (name.query + 1) );
if (name.select && name.select > 1 || name.select != null && art.kind !== 'element') // Yes, omit select:1 for element - TODO: re-check
r.push( (art.kind === 'extend' ? 'block:' : 'query:') + name.select ); // TODO: rename to 'select:1' and consider whether there are more selects
if (name.action && omit !== 'action')
r.push( memberActionName(art) + ':' + quoted( name.action ) );
if (name.param && omit !== 'param') // TODO: also use for alias/mixin
r.push( 'param:' + quoted( name.param ) );
else if (name.alias) // TODO: use 'param'
r.push( (name.$mixin ? 'mixin:' : 'alias:') + quoted( name.alias ) )
if (name.alias)
r.push( (art.kind === 'mixin' ? 'mixin:' : 'alias:') + quoted( name.alias ) )
if (name.param != null && omit !== 'param')
r.push( name.param ? 'param:' + quoted( name.param ) : 'returns' ); // TODO: join
if (name.element && omit !== 'element')

@@ -793,29 +1073,3 @@ // r.push( `${ art.kind }: ${ quoted( name.element )}` ); or even better element:"assoc"/key:"i" same with enum

function quoted( name ) {
return (name) ? '"' + name.replace( /"/g, '""' ) + '"' : '<?>'; // sync ";
}
/**
* The function converts the path-locations of the messages to file-locations.
* It navigates the provided xsn to find the path and consume the corresponding file-location found there.
*/
function translatePathLocations(messages,xsn) {
messages.forEach(msg => { // for all messages
let location = msg.location;
if(!(location instanceof Array))
return; // not a path-location
let element = xsn;
location.forEach(name => { // deep dive
if(element === undefined)
return; // do not translate invalid paths
element=element[name];
});
if(element && element.location)
msg.location = element.location; // replace path-location with -file-location
else
delete msg.location; // delete invalid array locations as message handling doesn't support them
});
}
/**
* Get the explanation string for the given message-id.

@@ -840,3 +1094,3 @@ *

function hasMessageExplanation(messageId) {
return !!availableMessageExplanations[messageId];
return messageId && _messageIdsWithExplanation.includes(messageId);
}

@@ -846,8 +1100,5 @@

* Returns an array of message IDs that have an explanation text.
* Only used for testing.
*
* @private
*/
function messageIdsWithExplanation() {
return Object.keys(availableMessageExplanations);
return _messageIdsWithExplanation;
}

@@ -863,3 +1114,2 @@

searchName,
getMessageFunction,
makeMessageFunction,

@@ -872,5 +1122,3 @@ artName,

CompileMessage,
DebugCompileMessage,
CompilationError,
translatePathLocations,
explainMessage,

@@ -877,0 +1125,0 @@ hasMessageExplanation,

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

union: 'union',
unionAll: 'union',
intersect: 'union',
except: 'union',
minus: 'union',
subquery: 'union', // for (subquery) with ORDER BY or LIMIT/OFFSET

@@ -17,2 +17,4 @@ }

*
* Beta features cannot be used when options.deprecated is set.
*
* A feature always needs to be provided - otherwise false will be returned.

@@ -28,5 +30,20 @@ *

const beta = options.beta || options.betaMode;
return beta && typeof beta === 'object' && feature && beta[feature];
return beta && typeof beta === 'object' && !options.deprecated && feature && beta[feature];
}
/**
* Test for deprecated feature, stored in option `deprecated`.
* With that, the value of `deprecated` is a dictionary of feature=>Boolean.
*
* Please do not move this function to the "option processor" code.
*
* @param {object} options Options
* @param {string} feature Feature to check for
* @returns {boolean}
*/
function isDeprecatedEnabled( options, feature ) {
const { deprecated } = options;
return deprecated && typeof deprecated === 'object' && deprecated[feature];
}
// Apply function `callback` to all artifacts in dictionary

@@ -44,4 +61,5 @@ // `model.definitions`. See function `forEachGeneric` for details.

function forEachMember( construct, callback, target ) {
let obj = construct.returns || construct; // why the extra `returns` for actions?
obj = obj.items || obj;
let obj = construct;
while (obj.items)
obj = obj.items;
forEachGeneric( target || obj, 'elements', callback );

@@ -52,2 +70,5 @@ forEachGeneric( obj, 'enum', callback );

forEachGeneric( construct, 'params', callback );
if (construct.returns)
callback( construct.returns, '', 'params' );
}

@@ -120,6 +141,5 @@

let obj = dict[name];
if (obj instanceof Array) // redefinitions
obj.forEach( (o, i, a) => callback( o, name, prop, i, a ) )
else
callback( obj, name, prop );
callback( obj, name, prop );
if (obj.$duplicates instanceof Array) // redefinitions
obj.$duplicates.forEach( o => callback( o, name, prop ) )
}

@@ -130,23 +150,2 @@ }

// Call function `callback` with arguments `target` and `source` where `source`
// is calculated by `sourceRef(target)._artifact`, make sure that `callback`
// had been called with `source` as `target` before, and so on. To do so, we
// set property `_status` of the objects involved to value `status`.
function applyLinearly( status, target, sourceRef, callback ) {
if (!target)
return;
let chain = [];
let obj;
while ((obj = sourceRef(target)) && obj._status !== status && obj._artifact) {
setProp( obj, '_status', status );
chain.push( target );
target = obj._artifact;
}
let source = target;
for (target of chain.reverse()) {
callback( target, source );
source = target;
}
}
/**

@@ -164,4 +163,7 @@ * Like `obj.prop = value`, but not contained in JSON / CSN

Object.defineProperty( obj, prop, descriptor );
return value;
}
// FIXME: this function is only used by old tests -- REMOVE!
//
// Clone 'node', transforming nodes therein recursively. Object 'transformers' is expected

@@ -225,2 +227,3 @@ // to contain a mapping of property 'key' names to transformer functions. The node's properties

isBetaEnabled,
isDeprecatedEnabled,
queryOps,

@@ -234,5 +237,4 @@ forEachDefinition,

forEachInOrder,
applyLinearly,
setProp,
cloneWithTransformations,
};

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

// a `type` property looks the same in objects for definitions or in the object
// which is the value of the `items` property. TODO: check `technicalConfig`.
// which is the value of the `items` property.

@@ -71,3 +71,2 @@ // The model is described by a schema which specifies the type for all property

const { locationString, hasErrors } = require('../base/messages');
const { queryOps } = require('../base/model');

@@ -77,3 +76,3 @@

const typeProperties = [
'type', 'typeArguments', 'length', 'precision', 'scale', 'srid',
'type', '$typeArgs', 'length', 'precision', 'scale', 'srid',
];

@@ -91,6 +90,6 @@

optional: [
'vocabularies',
'messages',
'extensions',
'i18n',
'version', '$version', // version without --test-mode
'meta',

@@ -103,2 +102,3 @@ '$magicVariables',

'_entities', '$entity',
'$blocks',
'$newfeatures',

@@ -110,6 +110,6 @@ ],

optional: [
'messages', 'options', 'definitions',
'messages', 'options', 'definitions', 'vocabularies',
'extensions', 'i18n',
'artifacts', 'artifacts_', 'namespace', 'usings', // CDL parser
'filename', 'dirname', // TODO: move filename into a normal location? Only in model.sources
'location', 'dirname',
'dependencies', // for USING..FROM

@@ -124,24 +124,10 @@ 'kind', // TODO: remove from parser

isRequired: parent => noSyntaxErrors() || Object.keys( parent ).length,
requires: [ 'filename', 'start' ],
optional: [ 'end', '$weak', '$notFound' ],
kind: true,
requires: [ 'file' ], // line is optional in top-level location
optional: [ 'line', 'col', 'endLine', 'endCol', '$notFound' ],
schema: {
start: {
requires: [ 'line', 'column' ],
optional: [ 'offset' ],
schema: {
line: { test: isNumber },
column: { test: isNumber },
offset: { test: isNumber },
},
},
end: {
requires: [ 'line', 'column' ],
optional: [ 'offset' ],
schema: {
line: { test: isNumber },
column: { test: isNumber },
offset: { test: isNumber },
},
},
$weak: { test: isBoolean, parser: true },
line: { test: isNumber },
col: { test: isNumber },
endLine: { test: isNumber },
endCol: { test: isNumber },
$notFound: { test: isBoolean },

@@ -151,5 +137,5 @@ },

sources: { test: isDictionary( isObject ) },
filename: { test: isString },
dirname: { test: isString },
realname: { test: isString },
file: { test: isString },
dirname: { test: isString }, // TODO: really necessary?
realname: { test: isString }, // TODO: really necessary?
dependencies: {

@@ -159,3 +145,3 @@ test: isArray(),

},
fileDep: { test: TODO },
fileDep: { test: TODO }, // in usings
$frontend: { parser: true, test: isString, enum: [ 'cdl', 'json', 'xml' ] },

@@ -173,2 +159,7 @@ $newfeatures: { test: TODO }, // if new features have been used which break the old backends

},
vocabularies: {
test: isDictionary( definition ),
requires: [ 'kind', 'name' ],
optional: thoseWithKind,
},
extensions: {

@@ -190,3 +181,2 @@ kind: [ 'context' ], // syntax error (as opposed to HANA CDS), but still there

},
_localized: { kind: true, test: TODO }, // true or artifact
_assocSources: { kind: true, test: TODO }, // just null: isArray( inDefinitions ) during resolve

@@ -204,3 +194,3 @@ $magicVariables: {

requires: [ 'kind', 'name' ],
optional: [ 'elements', '$autoElement', '$uncheckedElements', '_finalType', '_deps' ],
optional: [ 'elements', '$autoElement', '$uncheckedElements', '_effectiveType', '_deps' ],
schema: {

@@ -218,2 +208,3 @@ kind: { test: isString, enum: [ 'builtin' ] },

$builtins: { test: TODO },
$blocks: { test: TODO },
builtin: { kind: true, test: builtin },

@@ -227,4 +218,2 @@ $internal: {

},
version: { test: TODO }, // TODO: describe - better: 'header'
$version: { test: isString },
meta: { test: TODO }, // never tested due to --test-mode

@@ -235,3 +224,3 @@ namespace: {

requires: [ 'location' ],
optional: [ 'path', 'dcPath' ], // dcPath with --hana-flavor
optional: [ 'path' ],
},

@@ -241,27 +230,17 @@ usings: {

requires: [ 'kind', 'location' ],
optional: [ 'name', 'extern', 'usings', 'annotationAssignments', 'fileDep' ],
// TODO: get rid of annotationAssignments: []
optional: [ 'name', 'extern', 'usings', '$annotations', 'fileDep' ],
// TODO: get rid of $annotations: []
},
extern: {
requires: [ 'location', 'path' ],
optional: [ 'dcPath' ],
schema: { path: { inherits: 'path', optional: [ 'quoted' ] } },
schema: { path: { inherits: 'path', optional: [ '$delimited' ] } },
},
dcPath: { inherits: 'path', optional: [ 'quoted' ] },
elements: { kind: true, inherits: 'definitions' },
elements: { kind: true, inherits: 'definitions', also: [ 0 ] }, // 0 for cyclic expansions
// specified elements in query entities (TODO: introduce real "specified elements" instead):
elements$: { kind: true, enumerable: false, test: TODO },
// TODO: introduce real "specified elements" instead
elements_: { kind: true, parser: true, test: TODO }, // TODO: remove
_elementsIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove
actions: { kind: true, inherits: 'definitions' },
actions_: { kind: true, parser: true, test: TODO }, // TODO: remove
enum: { kind: true, inherits: 'definitions' },
enum_: { 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
_foreignKeysIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove
params: { kind: true, inherits: 'definitions' },
params_: { kind: true, parser: true, test: TODO }, // TODO: remove
_paramsIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove
mixin: { inherits: 'definitions' },

@@ -275,4 +254,4 @@ query: {

optional: [
'name', 'quantifier', 'orderBy', 'limit', 'offset', '_leadingQuery',
'name', 'kind', '_parent', '_main', '_finalType', '$navigation', // in FROM
'quantifier', 'orderBy', 'limit', '_leadingQuery',
'name', '$parens', 'kind', '_parent', '_main', '_effectiveType', // in FROM
],

@@ -283,8 +262,6 @@ },

optional: [
'_tableAlias', // for sub query in FROM
'name', 'quantifier', 'mixin', 'exclude', 'columns', 'elements', '_deps',
'where', 'groupBy', 'having', 'orderBy', '$orderBy', 'limit', 'offset',
'_elementsIndexNo', '_projections', '_block', '_parent', '_main', '_finalType',
'$tableAliases', 'kind', '_firstAliasInFrom', 'queries', '_$next', '$combined',
'$dictOrderBy',
'name', '$parens', 'quantifier', 'mixin', 'excludingDict', 'columns', 'elements', '_deps',
'where', 'groupBy', 'having', 'orderBy', '$orderBy', 'limit',
'_projections', '_block', '_parent', '_main', '_effectiveType',
'$tableAliases', 'kind', '_$next', '_combined',
],

@@ -295,20 +272,32 @@ },

from: {
inherits: 'query',
test: isArray( query ), // TODO: not array
op: { // join
schema: { args: { inherits: 'from', test: isArray( query ) } },
test: from,
join: { // join
schema: { args: { inherits: 'from', test: isArray( from ) } },
requires: [ 'op', 'location', 'args', 'join' ],
optional: [
'on', 'kind', 'name', 'cardinality',
'$tableAliases', 'queries', '$combined',
'_block', '_parent', '_main', '_leadingQuery', '_$next', '_deps',
'on', '$parens', 'cardinality',
'kind', 'name', '_block', '_parent', '_main',
'$tableAliases', '_combined', '_joinParent', '$joinArgsIndex',
'_leadingQuery', '_$next', '_deps',
],
},
path: {
ref: {
requires: [ 'location', 'path' ],
optional: [
'name', '_tableAlias', '_status', // TODO: only in from
'scope', '_artifact', '$inferred', // TODO: remove the rest
'kind', 'name', '$syntax', '_block', '_parent', '_main',
'elements', '_origin', '_joinParent', '$joinArgsIndex', '$syntax',
'$parens', '_status', // TODO: only in from
'scope', '_artifact', '$inferred', 'kind',
'_effectiveType', // TODO:check this
],
},
query: {
requires: [ 'query', 'location' ],
optional: [
'$parens',
'kind', 'name', '_block', '_parent', '_main',
'_effectiveType', 'elements', '_origin', '_joinParent', '$joinArgsIndex',
],
},
none: { optional: () => true }, // parse error
},

@@ -323,27 +312,16 @@ columns: {

},
exclude: { // TODO: -> excluding, re-think structure
excludingDict: {
test: isDictionary( definition ), // definition since redef
requires: [ 'location', 'name' ],
optional: [ 'annotationAssignments' ], // TODO: get rid of annos: []
optional: [ '$annotations' ], // TODO: get rid of annos: []
},
orderBy: {
test: isArray(),
requires: [ 'value' ],
optional: [ 'location', 'sort', 'nulls', '_$queryNode' ],
},
orderBy: { inherits: 'value', test: isArray( expression ) },
sort: { test: locationVal( isString ), enum: [ 'asc', 'desc' ] },
nulls: { test: locationVal( isString ), enum: [ 'first', 'last' ] },
$orderBy: { inherits: 'orderBy' },
_$queryNode: { test: TODO }, // TODO: remove
$dictOrderBy: { test: TODO },
groupBy: { inherits: 'value', test: isArray( expression ) },
limit: { inherits: 'value' },
limit: { requires: [ 'rows' ], optional: [ 'offset', 'location' ] },
rows: { inherits: 'value' },
offset: { inherits: 'value' },
$combined: { test: TODO },
_typeIsExplicit: {
kind: true,
enumerable: true,
parser: true,
test: TODO,
}, // TODO: remove
_combined: { test: TODO },
type: {

@@ -353,4 +331,3 @@ kind: true,

optional: [
'scope', '_artifact', '$inferred', // TODO: remove the rest
'calculated', // TODO: remove calculated
'scope', '_artifact', '$inferred', '$parens',
],

@@ -362,5 +339,5 @@ },

optional: [
'path', 'elements', '_elementsIndexNo', '_outer',
'scope', '_artifact', '$inferred', // TODO: remove the rest
'calculated', // TODO: remove calculated
'path', 'elements', '_outer',
'scope', '_artifact', '$inferred',
'_effectiveType', // by propagation
],

@@ -372,5 +349,4 @@ },

optional: [
'path', 'elements', '_elementsIndexNo', '_outer',
'scope', '_artifact', '$inferred', // TODO: remove the rest
'calculated', // TODO: remove calculated
'path', 'elements', '_outer',
'scope', '_artifact', '$inferred',
],

@@ -381,6 +357,5 @@ },

requires: [ 'location', 'id' ], // TODO: it can be `func` instead of `id` later
// TODO: rename namedArgs to args
optional: [
'quoted', // TODO remove?
'args', 'namedArgs',
'$delimited', // TODO remove?
'args', '$syntax',
'where', 'cardinality',

@@ -392,5 +367,5 @@ '_artifact', '_navigation',

id: { test: isString },
quoted: { test: isBoolean },
scope: { test: isScope }, // TODO: clarify where scope can occur
func: { test: TODO }, // TODO: change structure
$delimited: { parser: true, test: isBoolean },
scope: { test: isScope },
func: { test: TODO },
kind: {

@@ -401,20 +376,27 @@ isRequired: !stageParser && (() => true),

enum: [
'context', 'service', 'view', 'entity', 'type', 'const', 'annotation',
'context', 'service', 'entity', 'type', 'aspect', 'const', 'annotation',
'element', 'enum', 'action', 'function', 'param', 'key', 'event',
'annotate', 'extend',
'query', 'mixin',
'select', '$join', 'mixin',
'source', 'namespace', 'using',
'$tableAlias',
'$tableAlias', '$navElement',
],
},
// locations of parentheses pairs around expression:
$parens: { parser: true, test: TODO },
$syntax: {
parser: true,
kind: [ 'entity', 'view', 'type' ],
kind: [ 'entity', 'view', 'type', 'aspect' ],
test: isString, // CSN parser should check for 'entity', 'view', 'projection'
},
value: {
optional: [ 'location', '$inferred', 'sort', 'nulls' ],
kind: true,
test: expression, // properties below are "sub specifications"
ref: { inherits: 'type' },
none: { optional: [ 'location' ] },
ref: {
requires: [ 'location', 'path' ],
optional: [ 'scope', 'variant', '_artifact', '$inferred', '$parens', 'sort', 'nulls' ],
},
none: { optional: [ 'location', '$parens' ] },
// TODO: why optional / enough in name?

@@ -424,18 +406,24 @@ // TODO: "yes" instead "none": val: true, optional literal/location

requires: [ 'literal', 'location' ],
// TODO: remove augmented, rename symbol to sym
// TODO: struct only for annotation assignments
optional: [ 'val', 'symbol', 'name', '$inferred', 'augmented' ],
// TODO: rename symbol to sym
// TODO: struct and variant only for annotation assignments
optional: [
'val', 'sym', 'name', '$inferred', '$parens',
'struct', 'variant', 'sort', 'nulls',
],
},
op: {
schema: { args: { inherits: 'args', args: 'positional' } },
schema: { args: { inherits: 'args' } },
requires: [ 'op', 'location' ],
optional: [
'args', 'namedArgs',
'args',
'func',
'quantifier',
'$inferred', 'augmented', // TODO: remove augmented
'$inferred',
'$parens',
'_artifact', // _artifact with "localized data"s 'coalesce'
'sort', 'nulls', // if used in GROUP BY
...typeProperties, // for CAST
],
},
query: { inherits: 'query' },
query: { requires: [ 'query', 'location' ] },
},

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

enum: [
'string', 'number', 'boolean', 'hex',
'string', 'number', 'boolean', 'x',
'time', 'date', 'timestamp',

@@ -451,7 +439,7 @@ 'struct', 'array', 'enum', 'null', 'token',

},
symbol: { requires: [ 'location', 'id' ], optional: [ 'quoted', 'augmented' ] },
sym: { requires: [ 'location', 'id' ], optional: [ '$delimited' ] },
val: {
test: isVal, // the following for array/struct value
requires: [ 'location' ],
optional: [ 'literal', 'val', 'symbol', 'struct', 'path', 'name', 'augmented', '$duplicate' ],
optional: [ 'literal', 'val', 'sym', 'struct', 'variant', 'path', 'name', '$duplicate' ],
// TODO: restrict path to #simplePath

@@ -462,15 +450,11 @@ },

inherits: 'value',
optional: [ 'name', '$duplicate' ],
test: args,
optional: [ '_typeIsExplicit', ...typeProperties ], // for cast() in expressions
},
namedArgs: { inherits: 'value', optional: [ 'name', '$duplicate' ], test: args },
onCond: { kind: true, inherits: 'value' },
on: { kind: true, inherits: 'value', test: expressionOrString },
// TODO: rename 'onCond' to 'on', remove 'on'
on: { kind: true, inherits: 'value', test: expression },
where: { inherits: 'value' },
having: { inherits: 'value' },
op: { test: locationVal( isString ) },
join: { test: isString },
join: { test: locationVal( isString ) },
quantifier: { test: locationVal( isString ) },
augmented: { enumerable: false, test: TODO }, // FIXME: remove
// preliminary -----------------------------------------------------------

@@ -481,7 +465,7 @@ doc: { kind: true, test: locationVal( isStringOrNull ) }, // doc comment

inherits: 'value',
optional: [ 'name', '_block', 'priority', '$duplicate', '$inferred' ],
optional: [ 'name', '_block', '$priority', '$duplicate', '$inferred', '$duplicates' ],
// TODO: name requires if not in parser?
},
priority: { test: TODO }, // TODO: rename to $priority
annotationAssignments: { kind: true, test: TODO }, // TODO: rename to $assignments
$priority: { test: TODO }, // TODO: rename to $priority
$annotations: { parser: true, kind: true, test: TODO },
name: {

@@ -491,16 +475,13 @@ isRequired: stageParser && (() => false), // not required in parser

schema: {
query: { test: TODO },
$mixin: { test: TODO },
}, // TODO: rename query prop in name, delete $mixin
select: { test: TODO },
}, // TODO: rename query prop in name
requires: [ 'location' ],
optional: [
'path', 'id', 'quoted', 'variant', // TODO: req path, opt id for main, req id for member
'$mixin', // TODO: delete, use kind = 'mixin'
'path', 'id', '$delimited', 'variant', // TODO: req path, opt id for main, req id for member
'_artifact', '$inferred',
'calculated', // TODO: remove calculated
'absolute', 'element', 'alias', 'query', 'action', 'param',
'absolute', 'select', 'alias', 'element', 'action', 'param',
],
},
absolute: { test: isString },
variant: { test: TODO }, // TODO: not set in CDL parser, only in annotationAssignment
variant: { test: TODO }, // TODO: not set in CDL parser, only in $annotations
element: { test: TODO }, // TODO: { test: isString },

@@ -511,3 +492,2 @@ action: { test: isString },

expectedKind: { kind: [ 'extend' ], inherits: 'kind' },
abstract: { kind: true, test: locationVal() },
virtual: { kind: true, test: locationVal() },

@@ -520,23 +500,21 @@ key: { kind: true, test: locationVal(), also: [ null, undefined ] },

kind: [ 'action', 'function' ],
requires: [ 'kind', 'location' ],
optional: thoseWithKind,
},
items: {
kind: true,
requires: [ 'location' ],
optional: [
'enum',
'elements', 'cardinality', 'target', 'on', 'onCond', 'foreignKeys', 'items',
'_outer', '_finalType', 'notNull',
'origin', '_block', '$inferred', '_deps',
'elements_', '_elementsIndexNo', '$syntax', // TODO: remove
'_foreignKeysIndexNo', '_status',
'elements', 'cardinality', 'target', 'on', 'foreignKeys', 'items',
'_outer', '_effectiveType', 'notNull',
'_origin', '_block', '$inferred', '$expand', '_deps',
'$syntax',
'_status', '_redirected',
...typeProperties,
],
},
items: { kind: true, inherits: 'returns' }, // yes, also optional 'items'
dbType: { kind: true, test: locationVal() },
source: { kind: true, test: TODO }, // TODO: remove in JSON/CDL parser - only in old
projection: { kind: true, test: TODO }, // TODO: remove in JSON/CDL parser
technicalConfig: { enumerable: () => true, kind: [ 'entity' ], test: TODO }, // TODO: some spec
sequenceOptions: { kind: true, test: TODO }, // hanaFlavor
}, // yes, also optional 'items'
targetElement: { kind: true, inherits: 'type' }, // for foreign keys
indexNo: { kind: true, test: TODO }, // TODO: remove
artifacts: { kind: true, inherits: 'definitions', test: isDictionary( inDefinitions ) },
artifacts_: { kind: true, parser: true, test: TODO }, // TODO: remove
_subArtifacts: { kind: true, inherits: 'definitions', test: isDictionary( inDefinitions ) },
blocks: { kind: true, test: TODO }, // TODO: make it $blocks ?

@@ -558,3 +536,3 @@ length: { kind: true, inherits: 'value' }, // for number is to be checked in resolver

default: { kind: true, inherits: 'value' },
typeArguments: { kind: true, test: TODO }, // TODO $typeArgs: only in CDL parser or w errors
$typeArgs: { parser: true, kind: true, test: TODO },
$tableAliases: { kind: true, test: TODO }, // containing $self outside queries

@@ -568,9 +546,8 @@ _block: { kind: true, test: TODO },

_navigation: { test: TODO },
_finalType: { kind: true, test: TODO },
_tableAlias: { test: TODO },
_firstAliasInFrom: { test: TODO },
_effectiveType: { kind: true, test: TODO },
_joinParent: { test: TODO },
$joinArgsIndex: { test: isNumber },
_outer: { test: TODO }, // for returns/items
queries: { kind: true, test: TODO }, // TODO: $queries with other structure
$queries: {
kind: [ 'entity', 'view' ],
kind: [ 'entity', 'event' ],
test: isArray(),

@@ -582,22 +559,20 @@ requires: [

'op', 'from', 'elements',
'$combined',
'$tableAliases', '_firstAliasInFrom',
'queries', // TODO: deprecated?
'_combined',
'$tableAliases',
],
optional: [
'_finalType',
'_effectiveType', '$parens',
'_deps',
// query specific
'where', 'columns', 'mixin', 'quantifier', 'offset',
'orderBy', '$orderBy', 'groupBy', 'exclude', 'having',
'limit', '_tableAlias', '_elementsIndexNo',
'orderBy', '$orderBy', 'groupBy', 'excludingDict', 'having',
'limit',
],
},
_leadingQuery: { kind: true, test: TODO },
$navigation: { kind: true, test: TODO },
$replacement: { kind: true, test: TODO }, // for smart * in queries
origin: { kind: true, test: TODO }, // TODO: define some _origin
_origin: { kind: [ 'entity' ], test: TODO }, // origin composition aspect for entity
$from: { kind: true, test: TODO }, // all table refs necessary to compute elements
_redirected: { kind: true, test: TODO }, // for REDIRECTED TO:
_origin: { kind: [ 'entity' ], test: TODO },
_from: { kind: true, test: TODO }, // all table refs necessary to compute elements
// array of $tableAlias (or includes) for explicit and implicit redirection:
_redirected: { kind: true, test: TODO },
// ...array of table aliases for targets from orig to new

@@ -609,3 +584,2 @@ _$next: { kind: true, test: TODO }, // next lexical search environment for values

_deps: { kind: true, test: TODO }, // for cyclic calculation
_xref: { kind: true, test: TODO },
_scc: { kind: true, test: TODO }, // for cyclic calculation

@@ -619,14 +593,18 @@ _sccCaller: { kind: true, test: TODO }, // for cyclic calculation

$lateExtensions: { test: TODO },
_upperAspects: { kind: [ 'type', 'entity', 'view' ], test: isArray( TODO ) },
_ancestors: { kind: [ 'type', 'entity', 'view' ], test: isArray( TODO ) },
_descendants: { kind: [ 'entity', 'view' ], test: isDictionary( isArray( TODO ) ) },
$duplicate: { parser: true, kind: true, test: isBoolean },
_upperAspects: { kind: [ 'type', 'entity' ], test: isArray( TODO ) },
// for implicit redirection - direct and indirect query sources of simple
// projections/views without @(cds.redirection.target: false):
_ancestors: { kind: [ 'type', 'entity' ], test: isArray( TODO ) },
// for implicit redirection - maps service name to simple projections/views
// in that service which have the current artifact in _ancestors
// (it can contain the artifact itself with no/failed autoexposure):
_descendants: { kind: [ 'entity' ], test: isDictionary( isArray( TODO ) ) },
$duplicates: { parser: true, kind: true, test: TODO }, // array of arts or true
$extension: { kind: true, test: TODO }, // TODO: introduce $applied instead or $status
$inferred: { parser: true, kind: true, test: isString },
$expand: { kind: true, test: isString },
$autoexpose: { kind: [ 'entity' ], test: isBoolean, also: [ null, 'Composition' ] },
_ignore: { kind: true, enumerable: true, test: TODO }, // TODO: rename to $ignore/$delete
calculated: { kind: true, test: TODO }, // TODO remove
viaAll: { kind: true, test: TODO }, // TODO remove
implicitForeignKeys: { kind: true, test: TODO }, // TODO: do it with $inferred with object value
redirected: { kind: true, test: TODO }, // TODO: do it with not-$inferred
$a2j: { kind: true, enumerable: true, test: TODO },
$extra: { parser: true, test: TODO }, // for unexpected properties in CSN

@@ -641,3 +619,3 @@ $withLocalized: { test: isBoolean },

if (_noSyntaxErrors == null)
_noSyntaxErrors = !hasErrors( options.messages || model.messages ); // TODO: check messageId?
_noSyntaxErrors = !hasErrors( options.messages ); // TODO: check messageId?
return _noSyntaxErrors;

@@ -736,8 +714,18 @@ }

function query( node, parent, prop, spec, idx ) {
while (node instanceof Array) {
if (node.length !== 1)
throw new Error( `Unexpected length ${ node.length } for query expression${ at( [ node, parent ], prop, idx ) }` );
// TODO: also check getOwnPropertyNames(node)
[ node ] = node;
// select from <EOF> produces from: [null]
if (node !== null || noSyntaxErrors()) {
isObject( node, parent, prop, spec, idx );
// eslint-disable-next-line no-nested-ternary
const choice = (node.from !== undefined || node.columns)
? 'select'
: (node.op) ? 'union' : 'none'; // from: null from parse error
if (spec[choice])
assertProp( node, parent, prop, spec[choice], choice );
else
throw new Error( `No specification for computed variant '${ choice }'${ at( [ node, parent ], prop, idx ) }` );
}
}
function from( node, parent, prop, spec, idx ) {
// select from <EOF> produces from: [null]

@@ -748,9 +736,8 @@ if (node !== null || noSyntaxErrors()) {

// eslint-disable-next-line no-nested-ternary
const op = node.op && node.op.val // NOSONAR (isObject ensures that node.op does not throw)
? (queryOps[node.op.val] || 'op')
: (node.path) ? 'path' : 'none';
if (spec[op])
assertProp( node, parent, prop, spec[op], op );
const choice = (node.path) ? 'ref' : (node.join) ? 'join'
: (node.query) ? 'query' : 'none';
if (spec[choice])
assertProp( node, parent, prop, spec[choice], choice );
else
throw new Error( `No specification for computed variant '${ op }'${ at( [ node, parent ], prop, idx ) }` );
throw new Error( `No specification for computed variant '${ choice }'${ at( [ node, parent ], prop, idx ) }` );
}

@@ -771,7 +758,2 @@ }

function expressionOrString( node, ...rest ) { // TODO: remove with onCond
if (typeof node !== 'string')
expression( node, ...rest );
}
function expression( node, parent, prop, spec, idx ) {

@@ -808,6 +790,6 @@ // TODO CSN parser?: { val: <token>, literal: 'token' } for keywords

return 'val';
else if (node.query)
return 'query';
else if (!node.op)
return 'none';
else if (typeof node.op.val === 'string' && queryOps[node.op.val])
return 'query';
return 'op';

@@ -822,5 +804,2 @@ }

}
else if (spec.args === 'positional') {
throw new Error( `Expected array${ at( [ null, parent ], prop ) }` );
}
else if (node && typeof node === 'object' && !Object.getPrototypeOf( node )) {

@@ -836,6 +815,7 @@ for (const n in node)

function at( nodes, prop, name ) {
const n = name && (typeof name === 'number' ? ` for index ${ name }` : ` for "${ name }"`);
// eslint-disable-next-line no-nested-ternary
const n = name ? (typeof name === 'number' ? ` for index ${ name }` : ` for "${ name }"`) : '';
const loc = nodes.find( o => o && typeof o === 'object' && (o.location || o.start) );
const f = (prop) ? `${ n || '' } in property '${ prop }'` : n;
const l = loc && locationString( loc.location || loc ) || model.filename;
const f = (prop) ? `${ n } in property '${ prop }'` : n;
const l = locationString( loc && loc.location || loc || model.location );
return (!l) ? f : `${ f } at ${ l }`;

@@ -846,2 +826,4 @@ }

return function dictionary( node, parent, prop, spec ) {
if (spec.also && spec.also.includes( node ))
return;
// if (!node || typeof node !== 'object' || Object.getPrototypeOf( node ))

@@ -868,3 +850,3 @@ // console.log(node,prop,model.$frontend)

const requires = [ 'val', 'location' ];
const optional = [ 'literal', '$inferred', 'augmented' ]; // TODO: remove augmented
const optional = [ 'literal', '$inferred' ];
standard( node, parent, prop, { schema: valSchema, requires, optional }, name );

@@ -921,3 +903,5 @@ };

}
else if (!art.name.absolute || !model.definitions[art.name.absolute]) {
else if (!art.name.absolute ||
!model.definitions[art.name.absolute] &&
!(model.vocabularies && model.vocabularies[art.name.absolute])) {
// TODO: sign ignored artifacts with $inferred = 'IGNORED'

@@ -924,0 +908,0 @@ if (parent.kind === 'source' ||

@@ -56,13 +56,30 @@ // The builtin artifacts of CDS

/**
* Functions without parentheses in CDL (common standard SQL-92 functions)
* (do not add more - make it part of the SQL renderer to remove parentheses for
* other funny SQL functions like CURRENT_UTCTIMESTAMP).
*/
const functionsWithoutParens = [
'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP',
'CURRENT_USER', 'SESSION_USER', 'SYSTEM_USER',
];
const specialFunctions = {
ROUND: [
null, null, { // 3rd argument: rounding mode
ROUND_HALF_UP: 'argFull',
ROUND_HALF_DOWN: 'argFull',
ROUND_HALF_EVEN: 'argFull',
ROUND_UP: 'argFull',
ROUND_DOWN: 'argFull',
ROUND_CEILING: 'argFull',
ROUND_FLOOR: 'argFull',
},
],
};
/**
* Variables that have special meaning in CDL/CSN.
*/
const magicVariables = { // in SQL-92
CURRENT_DATE: {},
CURRENT_TIME: {},
CURRENT_TIMESTAMP: {},
CURRENT_USER: {},
SESSION_USER: {},
SYSTEM_USER: {}, // not in HANA
// SQL-92: USER - intentionally omitted (useful element name), most DB have USER()
// SQL-92: VALUE - intentionally omitted (useful element name), which DB supports this?
const magicVariables = {
$user: {

@@ -169,3 +186,3 @@ elements: { id: {}, locale: {} },

model.$builtins.hana = hana;
cds.artifacts.hana = hana;
cds._subArtifacts.hana = hana;
env( coreHana, 'cds.hana.', hana );

@@ -179,3 +196,3 @@ // namespace:"localized" stores localized convenience views ---

function createNamespace( name, builtin ) {
return {
const art = {
kind: 'namespace',

@@ -185,10 +202,11 @@ // builtin namespaces don't have a cds file, so no location available

blocks: [],
artifacts: Object.create(null),
builtin,
location: builtinLocation(),
};
setProp( art, '_subArtifacts', Object.create(null) );
return art;
}
/**
* Insert the builtins into the parent's `artifacts` dictionary without the
* Insert the builtins into the parent's `_subArtifacts` dictionary without the
* prefix and into the model's `definitions` dictionary prefixed.

@@ -210,4 +228,4 @@ *

if (parent)
parent.artifacts[name] = art;
setProp( art, '_finalType', art );
parent._subArtifacts[name] = art;
setProp( art, '_effectiveType', art );
setProp( art, '_deps', [] );

@@ -235,3 +253,3 @@ Object.assign( art, builtins[name] );

art.$uncheckedElements = magic.$uncheckedElements;
// setProp( art, '_finalType', art );
// setProp( art, '_effectiveType', art );
}

@@ -247,3 +265,3 @@ model.$magicVariables = { kind: '$magicVariables', artifacts };

setProp( magic, '_parent', parent );
// setProp( magic, '_finalType', magic );
// setProp( magic, '_effectiveType', magic );
return magic;

@@ -254,2 +272,4 @@ }

module.exports = {
functionsWithoutParens,
specialFunctions,
initBuiltins,

@@ -256,0 +276,0 @@ isIntegerTypeName,

@@ -115,3 +115,3 @@ // Detect cycles in the dependencies between nodes (artifacts and elements)

if (dep.art._scc.lowlink === w._scc.lowlink) // in same SCC
reportCycle( dep.art, dep.location, w );
reportCycle( w, dep.art, dep.location );
}

@@ -118,0 +118,0 @@ return r;

@@ -5,4 +5,8 @@ //

const { forEachDefinition, forEachMember, setProp }
= require( '../base/model');
const {
forEachDefinition,
forEachMember,
setProp,
isDeprecatedEnabled,
} = require( '../base/model');
const { linkToOrigin, withAssociation } = require('./shared');

@@ -12,3 +16,2 @@ // const { refString } = require( '../base/messages')

function propagate( model ) {
const { options } = model;
const props = {

@@ -45,3 +48,2 @@ '@com.sap.gtt.core.CoreModel.Indexable': never,

cardinality: always,
onCond: always, // is expensive, but often rewritten - TODO: on
on: ( prop, target, source ) => {

@@ -57,2 +59,4 @@ target[prop] = source[prop];

};
const { options } = model;
const enableExpandElements = !isDeprecatedEnabled( options, 'noElementsExpansion' );

@@ -165,3 +169,3 @@ forEachDefinition( model, run );

function availableAtType( prop, target, source ) {
if (target.kind === 'type' && !options.hanaFlavor)
if (target.kind === 'type')
return false;

@@ -188,2 +192,4 @@ const ref = target.type || source.type;

return;
if (prop === 'elements' && source.$expand === 'origin' && source.type)
return;
const location = target.type && !target.type.$inferred && target.type.location ||

@@ -197,3 +203,3 @@ target.location ||

member.$inferred = 'proxy';
setFinalType(member, dict[name]);
setEffectiveType(member, dict[name]);
}

@@ -209,3 +215,3 @@ }

if (target.kind) { // not in 'returns' and 'items'
const from = target.$from && target.$from[0].path;
const from = target._from && target._from[0].path;
if (!(from ? from[from.length - 1]._artifact : source)._main)

@@ -222,4 +228,4 @@ always( prop, target, source );

function withKind( prop, target, source ) {
if (target.kind) // not in 'returns' and 'items'
always( prop, target, source );
if (target.kind && (!target._parent || target._parent.returns !== target))
always( prop, target, source ); // not in 'returns' and 'items'
}

@@ -233,8 +239,6 @@

function returns( prop, target, source, ok ) {
// TODO: remove returns in XSN
if (ok || target.$inferred === 'proxy' || target.$inferred === 'include' ) {
const origin = {};
target[prop] = { $inferred: 'proxy', origin };
setFinalType( target[prop], source[prop] );
setProp( origin, '_artifact', source[prop] );
target[prop] = { $inferred: 'proxy' };
setEffectiveType( target[prop], source[prop] );
setProp( target[prop], '_origin', source[prop] );
setProp( target[prop], '_outer', target._outer || target ); // for setMemberParent

@@ -246,8 +250,8 @@ }

// usually considered expensive, except:
// - array of Entity, array of String(3), array of DerivedScalar
// - array of Entity (smooth upgrade: array of String(3), array of DerivedScalar)
const line = availableAtType( prop, target, source );
if (!line ||
line.type && line.type._artifact && line.type._artifact.kind === 'entity' ||
!line.elements && !line.enum && !line.items )
returns( prop, target, source, !options.hanaFlavor );
!line.elements && !line.enum && !line.items && !enableExpandElements)
returns( prop, target, source, true );
}

@@ -264,6 +268,7 @@ }

return art._origin;
if (art.$from && art.$from.length) { // query
const tabref = art.$from[0]._artifact;
if (art._from && art._from.length) { // query
const tabref = art._from[0]._artifact;
return (tabref && tabref.kind === 'element')
? tabref._finalType && tabref._finalType.target && tabref._finalType.target._artifact
? tabref._effectiveType && tabref._effectiveType.target &&
tabref._effectiveType.target._artifact
: tabref;

@@ -274,3 +279,3 @@ }

? art.type._artifact
: art.origin && art.origin._artifact;
: art._origin;
}

@@ -285,5 +290,5 @@

function setFinalType( target, source ) {
if ('_finalType' in source)
setProp( target, '_finalType', source._finalType);
function setEffectiveType( target, source ) {
if ('_effectiveType' in source)
setProp( target, '_effectiveType', source._effectiveType);
}

@@ -290,0 +295,0 @@

@@ -6,4 +6,4 @@ // Compiler functions and utilities shared across all phases

const { searchName, getMessageFunction } = require('../base/messages');
const { addToDict, addToDictWithIndexNo, pushToDict } = require('../base/dictionaries');
const { searchName, makeMessageFunction } = require('../base/messages');
const { dictAdd, dictAddArray, pushToDict } = require('../base/dictionaries');
const { setProp } = require('../base/model');

@@ -26,12 +26,14 @@

entity: { elements: true, actions: true, params: () => false },
view: { elements: true, actions: true, params: () => false },
query: { elements: true },
$tableAlias: { normalized: 'alias', $navigation: true }, // table alias in select
$navElement: { normalized: 'element', $navigation: true },
select: { normalized: 'select', elements: true },
$join: { normalized: 'select' },
$tableAlias: { normalized: 'alias' }, // table alias in select
$self: { normalized: 'alias' }, // table alias in select
$navElement: { normalized: 'element' },
event: { elements: true },
type: { elements: propExists, enum: propExists },
aspect: { elements: propExists },
annotation: { elements: propExists, enum: propExists },
const: {},
enum: { normalized: 'element' },
element: { elements: propExists, enum: propExists, dict: 'elements' },
mixin: { normalized: 'alias' },
action: {

@@ -45,4 +47,3 @@ params: () => false, elements: () => false, enum: () => false, dict: 'actions',

param: { elements: () => false, enum: () => false, dict: 'params' },
source: { artifacts: true },
block: { artifacts: true },
source: { artifacts: true }, // TODO -> $source
using: {},

@@ -68,3 +69,3 @@ extend: {

function artifactsEnv( art ) {
return art.artifacts || Object.create(null);
return art._subArtifacts || Object.create(null);
}

@@ -85,10 +86,20 @@

const options = model.options || {};
const message = getMessageFunction( model );
const {
info, warning, error, message,
} = makeMessageFunction( model, model.options, 'compile' );
// TODO: combine envFn and assoc ?
const specExpected = {
annotation: { useDefinitions: true, noMessage: true },
extend: { useDefinitions: true, envFn: artifactsEnv }, // ref in top-level EXTEND
// TODO: re-check --------------------------------------------------------
annotation: { useDefinitions: true, noMessage: true, global: 'vocabularies' },
// TODO: artifact references ---------------------------------------------
extend: {
useDefinitions: true,
envFn: artifactsEnv,
artItemsCount: Number.MAX_SAFE_INTEGER,
},
// ref in top-level EXTEND
annotate: {
useDefinitions: true,
envFn: artifactsEnv,
artItemsCount: Number.MAX_SAFE_INTEGER,
undefinedDef: 'anno-undefined-def',

@@ -98,22 +109,44 @@ undefinedArt: 'anno-undefined-art',

type: { // TODO: more detailed later (e.g. for enum base type?)
reject: rejectNonType,
envFn: artifactsEnv,
check: checkTypeRef,
expectedMsgId: 'expected-type',
sloppyMsgId: 'ref-sloppy-type',
deprecateSmart: true,
warn: warnAboutElementWithNonType,
},
// if we want to disallow assoc nav for TYPE, do not do it her
typeOf: { next: '_$next' },
include: { reject: rejectNonStruct, envFn: artifactsEnv },
context: { reject: rejectNonContext, envFn: artifactsEnv },
target: { reject: rejectNonEntity, noDep: true, envFn: artifactsEnv },
actionParamType: {
envFn: artifactsEnv,
check: checkActionParamTypeRef,
expectedMsgId: 'expected-actionparam-type',
sloppyMsgId: 'ref-sloppy-actionparam-type',
deprecateSmart: true,
},
eventType: {
envFn: artifactsEnv,
check: checkEventTypeRef,
expectedMsgId: 'expected-event-type',
sloppyMsgId: 'ref-sloppy-event-type',
deprecateSmart: true,
},
include: {
check: checkIncludesRef,
expectedMsgId: 'expected-struct',
envFn: artifactsEnv,
},
target: {
check: checkEntityRef,
expectedMsgId: 'expected-entity',
noDep: true,
envFn: artifactsEnv,
},
compositionTarget: {
reject: rejectNonTarget,
check: checkTargetRef,
expectedMsgId: 'expected-target',
sloppyMsgId: 'ref-sloppy-target',
noDep: 'only-entity',
envFn: artifactsEnv,
},
// TODO: dep for (explicit+implicit!) foreign keys
element: { next: '__none_' }, // TODO: something for technical config - re-think
targetElement: { next: '__none_', assoc: false },
filter: { next: '_$next', lexical: 'main' },
from: {
reject: rejectNonSource,
envFn: artifactsEnv,
check: checkSourceRef,
expectedMsgId: 'expected-source',
assoc: 'from',

@@ -123,3 +156,14 @@ argsSpec: 'expr',

},
const: { next: '_$next', reject: rejectNonConst }, // DEFAULT
// element references ----------------------------------------------------
// if we want to disallow assoc nav for TYPE, do not do it here
typeOf: { next: '_$next', dollar: true }, // TODO: disallow in var
// TODO: dep for (explicit+implicit!) foreign keys
targetElement: { next: '__none_', assoc: false, dollar: false },
filter: { next: '_$next', lexical: 'main', dollar: 'none' },
default: {
next: '_$next',
dollar: true,
check: checkConstRef,
expectedMsgId: 'expected-const',
},
expr: { // in: from-on,

@@ -147,4 +191,12 @@ next: '_$next', escape: 'param', assoc: 'nav',

// expr TODO: write dependency, but care for $self
param: { reject: rejectNonConst },
global: { useDefinitions: true, global: true }, // for using declaration
param: {
check: checkConstRef,
expectedMsgId: 'expected-const',
},
global: { // for using declaration
envFn: artifactsEnv,
artItemsCount: Number.MAX_SAFE_INTEGER,
useDefinitions: true,
global: 'definitions',
},
};

@@ -159,72 +211,49 @@

// 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 ) {
return [ 'builtin', 'param', 'const' ].includes( art.kind ) ? undefined : 'expected-const';
function checkConstRef( art ) {
return ![ 'builtin', 'param' ].includes( art.kind );
}
function rejectNonStruct( art ) {
// TODO - better
// - always Error: with params, with query, no elements
// - in aspect: Warning for not aspect
// - in type: Warning for not aspect or type
// - in entity: Warning for not aspect or entity
// - in event: Warning for not aspect or event (or entity?)
return ([ 'type', 'entity', 'event' ].includes( art.kind ) &&
art.elements && !art.query && !art.params)
? undefined
: 'expected-struct';
function checkIncludesRef( art ) {
// We currently disallow using
// - derived structure types: would have to follow type in extend/include;
// - entities with params: clarify inheritance, use of param in ON/DEFAULT;
// - query entities/events: difficult sequence of resolve steps
return !(art.elements && !art.query && !art.type && !art.params);
}
function rejectNonContext( art ) {
return ([ 'context', 'service' ].includes( art.kind ))
? undefined
: 'expected-context';
function checkTypeRef( art ) {
if (art.kind === 'type' || art.kind === 'element')
return false;
return ![ 'entity', 'aspect', 'event' ].includes( art.kind ) || 'sloppy';
}
function rejectNonType( art ) {
return ([ 'type', 'entity', 'view', 'event' ].includes( art.kind ) ||
// art.kind === 'type' || // too strong for positive/BoundFunctions
// art._main && art._main.kind === 'type') // currently too strong
art._main && [ 'type', 'entity', 'view', 'event' ].includes( art._main.kind ))
? undefined
: 'expected-type';
function checkActionParamTypeRef( art ) {
return !(art.kind === 'entity' && art._service) && checkTypeRef( art );
}
/**
* Warns about artifacts that cannot be used as an element type.
*
* @param {XSN.Artifact} art
* @param {XSN.Artifact} user
*/
function warnAboutElementWithNonType( art, user ) {
return (user.kind === 'element' && art.query)
? 'expected-no-query'
: undefined;
function checkEventTypeRef( art ) {
return art.kind !== 'event' && checkActionParamTypeRef( art );
}
function rejectNonEntity( art ) {
return ([ 'view', 'entity' ].includes( art.kind ) && !(art.abstract && art.abstract.val))
? undefined
: 'expected-entity';
function checkEntityRef( art ) {
return art.kind !== 'entity';
}
function rejectNonTarget( art ) {
return (art.$syntax === 'aspect' || art.kind === 'entity' && art.abstract && art.abstract.val)
? rejectNonStruct( art )
: rejectNonEntity( art );
function checkTargetRef( art ) {
if (art.kind === 'entity' || art.kind === 'aspect')
return false;
return art.kind !== 'type' || 'sloppy';
}
function rejectNonSource( art, path ) {
if ([ 'view', 'entity' ].includes( art.kind ))
return (art.abstract && art.abstract.val) ? 'expected-source' : undefined;
const main = [ ...path ].reverse().find( item => !item._artifact._main )._artifact;
function checkSourceRef( art, path ) { // for FROM
if (art.kind === 'entity' )
return false;
if (art.kind !== 'element')
return true;
const elem = path.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';
environment( art ); // sets _finalType on art
return (!art._finalType || art._finalType.target)
? undefined
: 'expected-source'; // orig: 'The path must end with an association'
if (elem._main.kind !== 'entity' )
return true; // elem not starting at entity
environment( art ); // sets _effectiveType on art
return !(art._effectiveType || art).target;
}

@@ -243,3 +272,3 @@

let art = (ref.scope === 'global' || spec.global)
? getPathRoot( ref.path, spec, user, {}, model.definitions )
? getPathRoot( ref.path, spec, user, {}, model[spec.global || 'definitions'] )
: getPathRoot( ref.path, spec, user, user._block, null, true );

@@ -280,4 +309,4 @@ if (art === false) // redefinitions

if (!spec.escape) {
message( 'ref-unexpected-scope', ref.location, user, {},
'Error', 'Unexpected parameter reference' );
error( 'ref-unexpected-scope', [ ref.location, user ], {},
'Unexpected parameter reference' );
return setLink( ref, null );

@@ -301,9 +330,13 @@ }

? user._main // in path filter, just $magic (and $parameters)
: (user.kind === 'query')
: (user.kind === 'select' || user.kind === '$join')
? user
: user._parent && user._parent.kind === 'query' && user._parent;
: user._parent && user._parent.kind === 'select' && user._parent;
env = (spec.lexical === 'from') ? query._parent : query || user._main || user;
// queries: first tabaliases, then $magic - value refs: first $self, then $magic
if (!extDict && !spec.noExt) {
extDict = query && spec.rootEnv !== 'elements' && query.$combined ||
// TODO: change to name restriction for $joins, not own environments
extDict = query && spec.rootEnv !== 'elements' &&
// first step: only use _combined of real query - TODO:
// reject if not visible, but not allow more (!)
(query._combined || query._parent._combined) ||
environment( user._main ? user._parent : user );

@@ -315,3 +348,3 @@ }

let art = (ref.scope === 'global' || spec.global)
? getPathRoot( path, spec, user, {}, model.definitions )
? getPathRoot( path, spec, user, {}, model[spec.global || 'definitions'] )
: getPathRoot( path, spec, user, env, extDict, msgArt || 0 );

@@ -325,9 +358,10 @@ if (!art) {

return setLink( ref, art );
else if (art instanceof Array) // redefined art referenced by using proxy
else if (art.$duplicates) // redefined art referenced by using proxy
return setLink( ref, false );
setLink( head, art ); // we do not want to see the using
}
else if (art.name.$mixin) { // TODO: art.kind === 'mixin'
else if (art.kind === 'mixin') {
if (spec.noAliasOrMixin) {
signalNotFound( 'ref-rejected-on', head.location, user, extDict && [ extDict ],
// TODO: good enough for now - change later to not search for table aliases at all
signalNotFound( 'ref-rejected-on', [ head.location, user ], extDict && [ extDict ],
{ '#': 'mixin', id: head.id } );

@@ -341,10 +375,10 @@ // also set link on head?

else if (art.kind === '$navElement') {
// console.log(message( null, art.location, art, {}, 'Info','NE').toString())
setLink( head, art, '_navigation' );
setLink( head, art.origin._artifact );
setXref( head._artifact, user, head );
setLink( head, art._origin );
// TODO: set art?
}
else if (art.kind === '$tableAlias') {
if (spec.noAliasOrMixin && !art.self) { // TODO: extra kind $self?
signalNotFound( 'ref-rejected-on', head.location, user, extDict && [ extDict ],
else if (art.kind === '$tableAlias' || art.kind === '$self') {
if (spec.noAliasOrMixin && art.kind !== '$self') { // TODO: extra kind $self?
// TODO: good enough for now - change later to not search for table aliases at all
signalNotFound( 'ref-rejected-on', [ head.location, user ], extDict && [ extDict ],
{ '#': 'alias', id: head.id } );

@@ -355,18 +389,14 @@ // also set link on head?

setLink( head, art, '_navigation' );
// console.log( message( null, art.location, art,
// {type: art.type, target: art._finalType && art._finalType.target},
// 'Info','NAV').toString())
// TODO: probable better set the _artifact link of FROM.assoc to target!
if (art.type) { // FROM reference
const assoc = art._finalType && art._finalType.target;
art = setLink( head, assoc ? assoc._artifact : art.type._artifact );
if (!art)
return setLink( ref, art );
}
else { // FROM subquery, $projection, $self
setLink( head, art._finalType ); // the query (sub or self)
}
setLink( head, art._origin ); // query source or leading query in FROM
// require('../model/revealInternalProperties').log(model, 'foo.bar.S.V1a')
if (!art._origin)
return setLink( ref, art._origin );
}
art = getPathItem( path, spec, user );
// how many path items are for artifacts (rest: elements)
const artItemsCount = (typeof ref.scope === 'number')
? ref.scope || Number.MAX_SAFE_INTEGER
: spec.artItemsCount || 1; // 0 or 1 does not matter (first item resolved by getPathRoot)
// console.log(expected, ref.path.map(a=>a.id),artItemsCount)
art = getPathItem( path, spec, user, artItemsCount );
if (!art)

@@ -382,8 +412,12 @@ return setLink( ref, art );

}
if (spec.reject) {
const msg = spec.reject( art, path );
if (msg) {
signalNotFound( msg, ref.location, user );
if (spec.check) {
const fail = spec.check( art, path );
if (fail === true) {
signalNotFound( spec.expectedMsgId, [ ref.location, user ], null );
return setLink( ref, false );
}
else if (fail) {
signalNotFound( spec.sloppyMsgId, [ ref.location, user ], null );
// no return!
}
}

@@ -393,15 +427,13 @@ if (spec.warn) {

if (msgId)
message( msgId, ref.location, user, {}, 'Warning' );
warning( msgId, [ ref.location, user ] );
}
if (user && (!spec.noDep ||
spec.noDep === 'only-entity' && art.kind !== 'entity' && art.kind !== 'view')) {
spec.noDep === 'only-entity' && art.kind !== 'entity')) {
const { location } = ref; // || combinedLocation( head, path[tail.length] );
// TODO: location of last path item if not main artifact
if (!user._deps)
setProp( user, '_deps', [] );
if (!art._main || spec.assoc !== 'from') {
user._deps.push( { art, location } );
dependsOn( user, art, location );
}
else {
user._deps.push( { art: art._main, location } );
dependsOn( user, art._main, location );
environment( art, location, user );

@@ -417,7 +449,8 @@ // Without on-demand resolve, we can simply signal 'undefined "x"'

deprecateSmart( ref, art, user );
// TODO: follow FROM here, see csnRef - fromRef
return setLink( ref, art );
}
// Issue warnings for deprecates "smart" element-in-artifact references
// without a colon; and warnings for misplaced colons in references.
// Issue errors for "smart" element-in-artifact references
// without a colon; and errors for misplaced colons in references.
// This function likely disappears again in cds-compiler v2.x.

@@ -431,4 +464,4 @@ function deprecateSmart( ref, art, user ) {

const item = path[ref.scope];
message( 'ref-unexpected-colon', item.location, user, { id: item.id },
'Warning', 'Replace the colon before $(ID) by a dot' );
error( 'ref-unexpected-colon', [ item.location, user ], { id: item.id },
'Replace the colon before $(ID) by a dot' );
ref.scope = 0; // correct (otherwise CSN refs are wrong)

@@ -438,4 +471,4 @@ }

const item = path[scope];
message( 'ref-missing-colon', item.location, user, { id: item.id },
'Warning', 'Replace the dot before $(ID) by a colon' );
error( 'ref-missing-colon', [ item.location, user ], { id: item.id },
'Replace the dot before $(ID) by a colon' );
ref.scope = scope; // no need to recalculate in to-csn.js

@@ -451,7 +484,7 @@ }

// For each property name `<prop>` in `typeArtifact.parameters`, we move a number
// in art.typeArguments (a vector of numbers with locations) to `artifact.<prop>`.
// in art.$typeArgs (a vector of numbers with locations) to `artifact.<prop>`.
// TODO: error if no parameters applicable
// TODO: also check for number
function resolveTypeArguments(artifact, typeArtifact, user) {
const args = artifact.typeArguments || [];
const args = artifact.$typeArgs || [];
const parameters = typeArtifact.parameters || [];

@@ -464,38 +497,15 @@ const parLength = parameters.length;

par = { name: par };
if (!artifact[par.name] && (i < args.length || par.literal)) {
const { location } = artifact.type;
artifact[par.name] = args[i] || {
literal: par.literal, val: par.val, location, $inferred: 'type-param',
};
}
if (!artifact[par.name] && i < args.length)
artifact[par.name] = args[i];
}
if (args.length > parLength) {
artifact.typeArguments = artifact.typeArguments.slice(parLength);
message( 'unexpected-type-arg', artifact.typeArguments[0].location,
user, { art: typeArtifact },
'Warning', 'Too many arguments for type $(ART)' );
artifact.$typeArgs = artifact.$typeArgs.slice(parLength);
warning( 'unexpected-type-arg', [ artifact.$typeArgs[0].location, user ],
{ art: typeArtifact }, 'Too many arguments for type $(ART)' );
}
else if (artifact.typeArguments) {
delete artifact.typeArguments;
else if (artifact.$typeArgs) {
delete artifact.$typeArgs;
}
}
// Set a cross-reference from the 'user' in artifact 'art'
// 'user' is a navigatable node, while 'where' gives a closer hint (e.g. an item in a path)
// For example in 'a.b.c as d' the definition of b gets a xref object with user d and where = b
function setXref(art, user, where = user) {
if (!user)
return;
if (!art._xref)
setProp( art, '_xref', [] );
// if (!art._xref.includes(user))
// art._xref.push(user);
art._xref.push( { user, where } );
}
function transformMagicName( name ) {
// TODO: store magic variable in lower case (nicer for code completion)
return (name === 'self' || name.charAt(0) === '$') ? name : name.toUpperCase();
}
// Return artifact or element referred by name `head`. The first environment

@@ -509,2 +519,3 @@ // we search in is `env`. If `unchecked` is equal to `true`, do not report an error

return undefined; // parse error
// if (head.id === 'k') {console.log(Object.keys(user));throw Error(JSON.stringify(user.name))}
// if head._artifact is set or is null then it was already computed once

@@ -525,6 +536,3 @@ if ('_artifact' in head)

const e = art.artifacts || art.$tableAliases || Object.create(null);
const r = (art.kind !== '$magicVariables')
? e[head.id]
// do not find magic variables if quoted:
: (!head.quoted) && e[transformMagicName( head.id )];
const r = e[head.id];
if (r) {

@@ -535,10 +543,9 @@ if (r instanceof Array) { // redefinitions

}
if (r.kind === 'block') {
return setLink( head, r.name._artifact );
}
else if (r.kind === '$parameters') {
if (!head.quoted && path.length > 1) {
message( 'ref-obsolete-parameters', head.location, user,
// if (head.$delimited && r.kind !== '$tableAlias' && r.kind !== 'mixin')
// TODO: warning for delimited special - or directly in parser
if (r.kind === '$parameters') {
if (!head.$delimited && path.length > 1) {
message( 'ref-obsolete-parameters', [ head.location, user ],
{ code: `$parameters.${ path[1].id }`, newcode: `:${ path[1].id }` },
[ 'Error' ], 'Obsolete $(CODE) - replace by $(NEWCODE)' );
'Obsolete $(CODE) - replace by $(NEWCODE)' );
// TODO: replace it in to-csn correspondingly

@@ -548,19 +555,23 @@ return setLink( head, r );

}
else if (r.kind !== '$tableAlias' ||
(r.self ? !head.quoted : path.length > 1)) {
// except $self if quoted, or "real" table aliases (not $self) with path len 1
// TODO: $projection only if not quoted _and_ length > 1
else if (r.kind === '$self') {
// TODO: handle $delimited differently
// TODO: $projection only if not delimited _and_ length > 1
return setLink( head, r );
}
else if (r.kind !== '$tableAlias' || path.length > 1) {
// except "real" table aliases (not $self) with path len 1
// TODO: $projection only if not delimited _and_ length > 1
return setLink( head, r );
}
}
}
if (extDict) {
if (extDict && (!spec.dollar || head.id[0] !== '$')) {
const r = extDict[head.id];
if (r instanceof Array) {
if (r[0].kind === '$navElement') {
const names = r.filter( e => !e.$duplicate)
const names = r.filter( e => !e.$duplicates)
.map( e => `${ e.name.alias }.${ e.name.element }` );
if (names.length) {
message( 'ref-ambiguous', head.location, user, { id: head.id, names },
'Error', 'Ambiguous $(ID), replace by $(NAMES)' );
error( 'ref-ambiguous', [ head.location, user ], { id: head.id, names },
'Ambiguous $(ID), replace by $(NAMES)' );
}

@@ -596,3 +607,3 @@ }

for (const name in extDict) {
if (!name.includes('.'))
if (!name.includes('.') && (!spec.dollar || name[0] !== '$'))
e[name] = extDict[name];

@@ -608,18 +619,20 @@ }

if (msgArt) {
signalNotFound( 'ref-undefined-element', head.location, user, valid,
// TODO: we might mention both the "direct" and the "effective" type and
// always just mentioned one identifier as not found
signalNotFound( 'ref-undefined-element', [ head.location, user ], valid,
{ art: searchName( msgArt, head.id, 'element' ) } );
}
else {
signalNotFound( 'ref-undefined-var', head.location, user, valid, { id: head.id },
'Error', 'Element or variable $(ID) has not been found' );
signalNotFound( 'ref-undefined-var', [ head.location, user ], valid, { id: head.id },
'Element or variable $(ID) has not been found' );
}
}
else if (env.$frontend && env.$frontend !== 'cdl' || spec.global) {
// IDE can inspect <model>.definitions - provide null for valid
signalNotFound( spec.undefinedDef || 'ref-undefined-def', head.location, user, valid,
{ art: head.id } );
// IDE can inspect <model>.definitions - provide null for valid
signalNotFound( spec.undefinedDef || 'ref-undefined-def', [ head.location, user ],
valid, { art: head.id } );
}
else {
signalNotFound( spec.undefinedArt || 'ref-undefined-art', head.location, user, valid,
{ name: head.id } );
signalNotFound( spec.undefinedArt || 'ref-undefined-art', [ head.location, user ],
valid, { name: head.id } );
}

@@ -633,3 +646,3 @@ return setLink( head, null );

// element item in the path)
function getPathItem( path, spec, user ) {
function getPathItem( path, spec, user, artItemsCount ) {
let art;

@@ -639,2 +652,3 @@ let nav = spec.assoc !== '$keys' && null; // false for '$keys'

for (const item of path) {
--artItemsCount;
if (!item || !item.id) // incomplete AST due to parse error

@@ -652,6 +666,7 @@ return undefined;

else {
const env = (spec.envFn || environment)( art, item.location, user, spec.assoc );
const fn = (spec.envFn && artItemsCount >= 0) ? spec.envFn : environment;
const env = fn( art, item.location, user, spec.assoc );
const sub = setLink( item, env && env[item.id] );
if (!sub)
return (sub === 0) ? 0 : error( item, env );
return (sub === 0) ? 0 : errorNotFound( item, env );
else if (sub instanceof Array) // redefinitions

@@ -678,9 +693,8 @@ return false;

let orig; // for the original target element
for (let o = sub; o; o = o.value && o.value._artifact)
for (let o = sub; o; o = o.value && o.value._artifact) // TODO: or use _origin?
orig = o;
nav = (orig._finalType || orig).$keysNavigation;
nav = (orig._effectiveType || orig).$keysNavigation;
nav = setTargetReferenceKey( orig.name.id, item );
}
art = sub;
setXref( art, user, item );
}

@@ -706,14 +720,15 @@ }

}
message( null, item.location, user, {}, 'Error',
// eslint-disable-next-line max-len
'You cannot follow associations other than to elements referred to in a managed association\'s key' );
error( null, [ item.location, user ], {},
// eslint-disable-next-line max-len
'You can\'t follow associations other than to elements referred to in a managed association\'s key' );
}
function error( item, env ) {
function errorNotFound( item, env ) {
if (!spec.next) { // artifact ref
// TODO: better for TYPE OF, FROM e.Assoc (even disallow for other refs)
signalNotFound( spec.undefinedDef || 'ref-undefined-def', item.location, user,
[ env ], { art: searchName( art, item.id, spec.envFn && 'absolute' ) } );
const a = searchName( art, item.id, (spec.envFn || art._subArtifacts) && 'absolute' );
signalNotFound( spec.undefinedDef || 'ref-undefined-def', [ item.location, user ],
[ env ], { art: a } );
}
else if (art.name.query) {
else if (art.name.select && art.name.select > 1) {
// TODO: 'The current query has no element $(MEMBER)' with $self.MEMBER

@@ -723,14 +738,15 @@ // and 'The sub query for alias $(ALIAS) has no element $(MEMBER)'

// TODO: views elements are proxies to query-0 elements, not the same
signalNotFound( 'query-undefined-element', item.location, user,
[ env ], { id: item.id }, 'Error',
// TODO: better message text
signalNotFound( 'query-undefined-element', [ item.location, user ],
[ env ], { id: item.id },
'Element $(ID) has not been found in the elements of the query' );
}
else if (art.kind === '$parameters') {
signalNotFound( 'ref-undefined-param', item.location, user,
signalNotFound( 'ref-undefined-param', [ item.location, user ],
[ env ], { art: searchName( art._main, item.id, 'param' ) },
'Error', { param: 'Entity $(ART) has no parameter $(MEMBER)' } );
{ param: 'Entity $(ART) has no parameter $(MEMBER)' } );
}
else {
signalNotFound( 'ref-undefined-element', item.location, user,
[ env ], { art: searchName( art, item.id ) } );
signalNotFound( 'ref-undefined-element', [ item.location, user ],
[ env ], { art: searchName( art, item.id, 'element' ) } );
}

@@ -741,4 +757,13 @@ return null;

function signalNotFound( msgId, location, home, valid, ...args ) {
// if (!location) console.log(msgId, valid, ...args)
/**
* Make a "not found" error and optionally attach valid names.
*
* @param {string} msgId
* @param {any} location
* @param {any} valid
* @param {object} [textParams]
* @param {any} [texts]
*/
function signalNotFound(msgId, location, valid, textParams, texts ) {
// if (!location) console.log(msgId, valid, textParams, texts)
if (location.$notFound)

@@ -748,3 +773,3 @@ return;

/** @type {object} */
const err = message( msgId, location, home, ...args );
const err = message( msgId, location, textParams, texts );
// console.log( Object.keys( Object.assign( Object.create(null), ...valid.reverse() ) ) )

@@ -756,5 +781,5 @@ if (valid && (options.attachValidNames || options.testMode))

const names = Object.keys( err.validNames );
message( null, location, null,
names.length ? `Valid: ${ names.sort().join(', ') }` : 'No valid names',
'Info' );
info( null, location[0] || location, // no semantic location
{ '#': !names.length ? 'zero' : 'std' },
{ std: `Valid: ${ names.sort().join(', ') }`, zero: 'No valid names' });
}

@@ -776,5 +801,6 @@ }

// namespaces which would lead to false positives.
// TODO: should this really be different to annotate-unknown?
if (art.kind === 'namespace') {
message( 'anno-namespace', construct.name.location, construct, {}, 'Info',
'Namespaces cannot be annotated' );
info( 'anno-namespace', [ construct.name.location, construct ], {},
'Namespaces can\'t be annotated' );
}

@@ -784,13 +810,13 @@ // Builtin annotations would also get lost. Same as for namespaces:

else if (art.builtin === true) {
message( 'anno-builtin', construct.name.location, construct, {}, 'Info',
'Builtin types should not be annotated. Use custom type instead.' );
info( 'anno-builtin', [ construct.name.location, construct ], {},
'Builtin types should not be annotated. Use custom type instead' );
}
}
// TODO: block should be construct._block
if (construct.annotationAssignments && construct.annotationAssignments.doc )
art.doc = construct.annotationAssignments.doc;
if (!construct.annotationAssignments || !construct.annotationAssignments.length) {
if (construct.$annotations && construct.$annotations.doc )
art.doc = construct.$annotations.doc;
if (!construct.$annotations || !construct.$annotations.length) {
if (construct === art)
return;
// annotationAssignments is set if parsed from CDL but may not be set if
// $annotations is set if parsed from CDL but may not be set if
// the input is CSN so we extract them.

@@ -810,3 +836,3 @@ for (const annoProp in construct) {

}
for (const anno of construct.annotationAssignments) {
for (const anno of construct.$annotations) {
const ref = anno.name;

@@ -831,5 +857,5 @@ const name = resolveUncheckedPath( ref, 'annotation', { _block: block } );

if (iHaveVariant) {
message( 'anno-duplicate-variant', item.name.variant.location, construct,
{}, // TODO: params
'Error', 'Annotation variant has been already provided' );
error( 'anno-duplicate-variant', [ item.name.variant.location, construct ],
{}, // TODO: params
'Annotation variant has been already provided' );
}

@@ -861,4 +887,4 @@ prop = `${ prop }#${ item.name.variant.id }`; // TODO: check for double variants

function setAnnotation( art, annoProp, anno, priority = 'define') {
anno.priority = priority;
addToDict( art, annoProp, anno );
anno.$priority = priority;
dictAddArray( art, annoProp, anno );
}

@@ -873,3 +899,3 @@ }

// The link (_artifact,_finalType,...) usually has the artifact as value.
// The link (_artifact,_effectiveType,...) usually has the artifact as value.
// Falsy values are:

@@ -879,3 +905,3 @@ // - undefined: not computed yet, parse error, no ref

// - false (only complete ref): multiple definitions, rejected
// - 0 (for _finalType only): circular reference
// - 0 (for _effectiveType only): circular reference
function setLink( obj, value = null, prop = '_artifact' ) {

@@ -886,7 +912,6 @@ Object.defineProperty( obj, prop, { value, configurable: true, writable: true } );

function linkToOrigin( origin, name, parent, prop, location ) {
function linkToOrigin( origin, name, parent, prop, location, silentDep ) {
const elem = {
name: { location: location || origin.name.location, id: name },
kind: origin.kind,
origin: { location },
location: location || origin.location,

@@ -898,5 +923,9 @@ };

setMemberParent( elem, name, parent, prop ); // TODO: redef in template
setProp( elem.origin, '_artifact', origin );
// TODO: make this just elem._origin, remove elem.origin
setProp( elem, '_deps', [ { art: origin, location } ] );
setProp( elem, '_origin', origin );
// TODO: should we use silent dependencies also for other things, like
// included elements? (Currently for $inferred: 'expand-element' only)
if (silentDep)
dependsOnSilent( elem, origin );
else
dependsOn( elem, origin, location );
return elem;

@@ -910,3 +939,3 @@ }

parent[prop] = Object.create(null);
addToDictWithIndexNo( parent, prop, name, elem );
dictAdd( parent[prop], name, elem );
}

@@ -921,5 +950,5 @@ if (parent._outer)

const normalized = kindProperties[elem.kind].normalized || elem.kind;
[ 'element', 'alias', 'query', 'param', 'action' ].forEach( ( kind ) => {
[ 'element', 'alias', 'select', 'param', 'action' ].forEach( ( kind ) => {
if (normalized === kind)
elem.name[kind] = (parent.name[kind] != null && kind !== 'query') ? `${ parent.name[kind] }.${ name }` : name;
elem.name[kind] = (parent.name[kind] != null && kind !== 'select' && kind !== 'alias') ? `${ parent.name[kind] }.${ name }` : name;

@@ -935,2 +964,28 @@ else if (parent.name[kind] != null)

/**
* Adds a dependency user -> art with the given location.
*
* @param {XSN.Artifact} user
* @param {XSN.Artifact} art
* @param {XSN.Location} location
*/
function dependsOn( user, art, location ) {
if (!user._deps)
setProp( user, '_deps', [] );
user._deps.push( { art, location } );
}
/**
* Same as "dependsOn" but the dependency from user -> art is silent,
* i.e. not reported to the user.
*
* @param {XSN.Artifact} user
* @param {XSN.Artifact} art
*/
function dependsOnSilent( user, art ) {
if (!user._deps)
setProp( user, '_deps', [] );
user._deps.push( { art } );
}
function storeExtension( elem, name, prop, parent, block ) {

@@ -957,3 +1012,3 @@ if (prop === 'enum')

const art = item && item._artifact; // item can be null with parse error
if (art && art._finalType && art._finalType.target && test( art._finalType, item ))
if (art && art._effectiveType && art._effectiveType.target && test( art._effectiveType, item ))
return (alsoTestLast || item !== ref.path[ref.path.length - 1]) && item;

@@ -970,2 +1025,4 @@ }

linkToOrigin,
dependsOn,
dependsOnSilent,
setMemberParent,

@@ -972,0 +1029,0 @@ storeExtension,

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

const oDataDictionary = require('../../gen/Dictionary.json');
const alerts = require('../../base/alerts');
const { makeMessageFunction } = require('../../base/messages');
const { forEachDefinition } = require('../../model/csnUtils');
/* Vocabulary overview as of January 2020:

@@ -138,3 +140,3 @@

const messageFunctions = alerts.makeMessageFunction(csn, options);
const messageFunctions = makeMessageFunction(csn, options);
const { info, warning, error } = messageFunctions;

@@ -207,5 +209,4 @@

// looks like <service name, can contain dots>.<id>
for (let objName in csn.definitions) {
forEachDefinition(csn, (object, objName) => {
if(objName == serviceName || objName.startsWith(serviceName + '.')) {
let object = csn.definitions[objName];
if (object.kind === 'action' || object.kind === 'function') {

@@ -223,3 +224,3 @@ handleAction(objName, object, null);

}
}
});

@@ -273,3 +274,3 @@ // filter out empty <Annotations...> elements

// this function is called in the translation code to issue an info/warning/error message
// messages are reported via the alerts attribute of csn
// messages are reported via the severity function
// context contains "semantic location"

@@ -333,4 +334,3 @@ function message(severity, context, message) {

if (!object.elements) return;
for (let elemName in object.elements) {
let element = object.elements[elemName];
Object.entries(object.elements).forEach(([elemName, element]) => {
// determine the name of the target in the resulting edm

@@ -345,3 +345,3 @@ // for non-assoc element, this simply is "<objectName>/<elementName>"

}
}
});
}

@@ -352,7 +352,6 @@

function handleNestedElements(objname, baseElemName, elementsObj) {
for (let elemName in elementsObj) {
let element = elementsObj[elemName];
if(!elementsObj) return;
Object.entries(elementsObj).forEach(([elemName, element]) => {
if (Object.keys(element).filter( x => x.substr(0,1) === '@' ).filter(filterKnownVocabularies).length > 0) {
message(warning, null, 'annotations at nested elements are not yet supported, object ' + objname + ', element ' + baseElemName + '.' + elemName);
message(warning, null, `annotations at nested elements are not yet supported, object ${objname}, element ${baseElemName}.${elemName}`);
}

@@ -363,3 +362,3 @@

}
}
});
}

@@ -380,2 +379,3 @@

function handleBoundActions(cObjectname, cObject) {
if(!cObject.actions) return;
// get service name: remove last part of the object name

@@ -387,7 +387,6 @@ // only works if all objects ly flat in the service

for (let n in cObject.actions) {
let action = cObject.actions[n];
Object.entries(cObject.actions).forEach(([n, action]) => {
let actionName = serviceName + '.' + (isV2() ? entityName + '_' : '') + n;
handleAction(actionName, action, cObjectname);
}
});
}

@@ -402,4 +401,4 @@

let actionName = cActionName;
if (isV2()) { // insert before last "."
actionName = actionName.replace(/\.(?=[^.]*$)/, '.EntityContainer/')
if (isV2()) { // Replace up to last dot with <serviceName>.EntityContainer
actionName = actionName.replace(/.*\.(?=[^.]*$)/, serviceName+'.EntityContainer/')
}

@@ -411,5 +410,7 @@ else { // add parameter type list

handleAnnotations(actionName, cAction);
for (let n in cAction.params) { // handle parameters
let edmTargetName = actionName + '/' + n;
handleAnnotations(edmTargetName, cAction.params[n]);
if(cAction.params) {
Object.entries(cAction.params).forEach(([n, p]) => {
let edmTargetName = actionName + '/' + n;
handleAnnotations(edmTargetName, p);
});
}

@@ -427,6 +428,7 @@ }

edmUtils.mapCdsToEdmType(p, messageFunctions, false /*is only called for v4*/) : p.type;
for (let n in action.params) {
let p = action.params[n];
let isArrayType = !p.type && p.items && p.items.type;
params.push(isArrayType ? 'Collection(' + mapType(p.items) + ')' : mapType(p));
if(action.params) {
action.params && Object.values(action.params).forEach(p => {
let isArrayType = !p.type && p.items && p.items.type;
params.push(isArrayType ? 'Collection(' + mapType(p.items) + ')' : mapType(p));
});
}

@@ -452,4 +454,5 @@ }

// do nothing
if(!isEdmPropertyRendered(carrier, options) ||
(carrier.kind === 'type' && !edmUtils.isStructuredType(carrier) && options.isV2())) {
(edmUtils.isDerivedType(carrier) && options.isV2())) {
return;

@@ -511,4 +514,4 @@ }

// (which is the definition key in the CSN and usually the name of the EntityType)
// find last . in name and insert "EntityContainer/"
alternativeEdmTargetName = (carrier.$entitySetName || edmTargetName).replace(/\.(?=[^.]*$)/, '.EntityContainer/');
// Replace up to last dot with <serviceName>.EntityContainer/
alternativeEdmTargetName = (carrier.$entitySetName || edmTargetName).replace(/.*\.(?=[^.]*$)/, serviceName+'.EntityContainer/');
hasAlternativeCarrier = carrier.$hasEntitySet;

@@ -522,3 +525,3 @@ }

// either only AppliesTo=[EntityContainer]
(!x.includes('Schema') && x.includes('EntityContainer')) ||
(!x.includes('Schema') && x.includes('EntityContainer')) ||
// or AppliesTo=[Schema, EntityContainer]

@@ -668,3 +671,3 @@ (x.includes('Schema') && x.includes('EntityContainer')))

if(dictTerm && dictTerm.AppliesTo) {
message(info, context, 'Term "' + fullTermName + '" is not applied (AppliesTo="' + dictTerm.AppliesTo.join(' ') + '")');
message(info, context, `Term "${ fullTermName }" is not applied (AppliesTo="${ dictTerm.AppliesTo.join(' ') }")`);
}

@@ -694,3 +697,3 @@ }

message(error, context,
`OData annotation term "${identifier}" must consist of one or more dot separated simple identifiers (each starting with a letter or underscore, followed by at most 127 letters)`)
`OData annotation term "${ identifier }" must consist of one or more dot separated simple identifiers (each starting with a letter or underscore, followed by at most 127 letters)`);
}

@@ -708,3 +711,3 @@ })

message(error, context,
`OData annotation qualifier "${p[1]}" must start with a letter or underscore, followed by at most 127 letters, underscores or digits`);
`OData annotation qualifier "${ p[1] }" must start with a letter or underscore, followed by at most 127 letters, underscores or digits`);
}

@@ -715,3 +718,3 @@ newAnno.Term = termNameWithoutQualifiers;

if (p.length>2) {
message(warning, context, 'multiple qualifiers (' + p[1] + ',' + p[2] + (p.length>3?',...':'') + ')')
message(warning, context, `multiple qualifiers (${ p[1] },${ p[2] }${ p.length > 3 ? ',...' : '' })`);
}

@@ -726,3 +729,3 @@

else {
message(info, context, 'unknown term ' + termNameWithoutQualifiers);
message(info, context, `Unknown term “${ termNameWithoutQualifiers }”`);
}

@@ -829,12 +832,12 @@

if (!expectedType && !isPrimitiveType(dTypeName)) {
message(warning, context, "internal error: dictionary inconsistency: type '" + dTypeName + "' not found");
message(warning, context, `internal error: dictionary inconsistency: type '${ dTypeName }' not found`);
}
else if (isComplexType(dTypeName)) {
message(warning, context, 'found enum value, but expected complex type ' + dTypeName);
message(warning, context, `found enum value, but expected complex type ${ dTypeName }`);
}
else if (isPrimitiveType(dTypeName) || expectedType['$kind'] !== 'EnumType') {
message(warning, context, 'found enum value, but expected non-enum type ' + dTypeName);
message(warning, context, `found enum value, but expected non-enum type ${ dTypeName }`);
}
else if (!expectedType['Members'].includes(enumValue)) {
message(warning, context, 'enumeration type ' + dTypeName + ' has no value ' + enumValue);
message(warning, context, `enumeration type ${ dTypeName } has no value ${ enumValue }`);
}

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

if (!type || type['IsFlags'] !== 'true') {
message(warning, context, "enum type '" + dTypeName + "' doesn't allow multiple values");
message(warning, context, `enum type '${ dTypeName }' doesn't allow multiple values`);
}

@@ -924,3 +927,3 @@

if (!['true','false'].includes(val)) {
message(warning, context, 'found String, but expected type ' + dTypeName);
message(warning, context, `found String, but expected type ${ dTypeName }`);
}

@@ -931,3 +934,3 @@ }

if (isNaN(Number(val)) || isNaN(parseFloat(val))) {
message(warning, context, 'found non-numeric string, but expected type ' + dTypeName);
message(warning, context, `found non-numeric string, but expected type ${ dTypeName }`);
}

@@ -938,10 +941,10 @@ }

if (isNaN(Number(val)) || isNaN(parseFloat(val))) {
message(warning, context, 'found non-numeric string, but expected type ' + dTypeName);
message(warning, context, `found non-numeric string, but expected type ${ dTypeName }`);
}
}
else if (isComplexType(dTypeName)) {
message(warning, context, 'found String, but expected complex type ' + dTypeName);
message(warning, context, `found String, but expected complex type ${ dTypeName }`);
}
else if (isEnumType(dTypeName)) {
message(warning, context, 'found String, but expected enum type ' + dTypeName);
message(warning, context, `found String, but expected enum type ${ dTypeName }`);
typeName = 'EnumMember';

@@ -972,3 +975,3 @@ }

else {
message(warning, context, 'found Boolean, but expected type ' + dTypeName);
message(warning, context, `found Boolean, but expected type ${ dTypeName }`);
}

@@ -978,3 +981,3 @@ }

if (isComplexType(dTypeName)) {
message(warning, context, 'found number, but expected complex type ' + dTypeName);
message(warning, context, `found number, but expected complex type ${ dTypeName }`);
}

@@ -985,6 +988,6 @@ else if (dTypeName === 'Edm.String') {

else if (dTypeName === 'Edm.PropertyPath') {
message(warning, context, 'found number, but expected type ' + dTypeName);
message(warning, context, `found number, but expected type ${ dTypeName }`);
}
else if (dTypeName === 'Edm.Boolean') {
message(warning, context, 'found number, but expected type ' + dTypeName);
message(warning, context, `found number, but expected type ${ dTypeName }`);
}

@@ -1011,7 +1014,8 @@ else if (dTypeName === 'Edm.Decimal') {

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

@@ -1040,5 +1044,5 @@ if( ['Edm.AnnotationPath', 'Edm.ModelElementPath', 'Edm.NavigationPropertyPath', 'Edm.PropertyPath', 'Edm.Path' ].includes(dTypeName) )

if (!getDictType(dTypeName) && !isPrimitiveType(dTypeName) && !isCollection(dTypeName))
message(warning, context, "internal error: dictionary inconsistency: type '" + dTypeName + "' not found");
message(warning, context, `internal error: dictionary inconsistency: type '${ dTypeName }' not found`);
else
message(warning, context, "found complex type, but expected type '" + dTypeName + "'");
message(warning, context, `found complex type, but expected type '${ dTypeName }'`);
return newRecord;

@@ -1052,8 +1056,8 @@ }

// this type doesn't exist
message(warning, context, "explicitly specified type '" + actualTypeName + "' not found in vocabulary");
message(warning, context, `explicitly specified type '${ actualTypeName }' not found in vocabulary`);
}
else if (dTypeName && !isDerivedFrom(actualTypeName, dTypeName)) {
// this type doesn't fit the expected one
message(warning, context, "explicitly specified type '" + actualTypeName
+ "' is not derived from expected type '" + dTypeName + "'");
message(warning, context, `explicitly specified type '${ actualTypeName
}' is not derived from expected type '${ dTypeName }'`);
actualTypeName = dTypeName;

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

// this type is abstract
message(warning, context, "explicitly specified type '" + actualTypeName + "' is abstract, specify a concrete type");
message(warning, context, `explicitly specified type '${ actualTypeName }' is abstract, specify a concrete type`);
actualTypeName = dTypeName;

@@ -1082,6 +1086,7 @@ }

if (isAbstractType(actualTypeName)) {
message(warning, context, "type '" + dTypeName + "' is abstract, use '$Type' to specify a concrete type");
}
if (isAbstractType(actualTypeName))
message(warning, context, `type '${ dTypeName }' is abstract, use '$Type' to specify a concrete type`);
newRecord.Type = actualTypeName;

@@ -1097,3 +1102,3 @@ }

// loop over elements
for (let i in obj) {
for (let i of Object.keys(obj)) {
context.stack.push('.' + i);

@@ -1115,3 +1120,3 @@

if (!dictPropertyTypeName){
message(warning, context, "record type '" + actualTypeName + "' doesn't have a property '" + i + "'");
message(warning, context, `record type '${ actualTypeName }' doesn't have a property '${ i }'`);
}

@@ -1145,3 +1150,3 @@ }

else {
message(warning, context, 'found collection value, but expected non-collection type ' + dTypeName);
message(warning, context, `found collection value, but expected non-collection type ${ dTypeName }`);
}

@@ -1200,3 +1205,3 @@ }

if(subset.length > 1) { // doesn't work for three or more...
message(warning, context, 'edmJson code contains more than one special property: ' + subset);
message(warning, context, `EDM JSON code contains more than one special property: ${ subset }`);
return null;

@@ -1210,3 +1215,3 @@ }

}
message(warning, context, 'edmJson code contains no special property out of: ' + specialProperties);
message(warning, context, `EDM JSON code contains no special property out of: ${ specialProperties }`);
return null;

@@ -1219,7 +1224,5 @@ }

for (let p in obj) {
for (let p of Object.keys(obj)) {
// copy all '$' attributes that are not $Apply or $LabeledElement to Thing
if(specialProperties.every(v =>
{ return p != v }))
{
if (specialProperties.every(v => p != v)) {
if (p.charAt(0) === '$') {

@@ -1230,3 +1233,3 @@ // simple attribute

else {
message(warning, context, 'unexpected element without $: ' + p);
message(warning, context, `unexpected element without $: ${ p }`);
}

@@ -1233,0 +1236,0 @@ }

'use strict';
const edmUtils = require('../edmUtils.js');
const alerts = require('../../base/alerts');
const { makeMessageFunction } = require('../../base/messages.js');

@@ -18,3 +18,3 @@

function preprocessAnnotations(csn, serviceName, options) {
const { warning, signal } = alerts(csn);
const { warning } = makeMessageFunction(csn, options);
let fkSeparator = '_';

@@ -44,6 +44,6 @@

keyNames.push['MISSING'];
signal(warning`in annotation preprocessing: target ${targetName} has no key`);
warning(null, null, `in annotation preprocessing: target ${targetName} has no key`);
}
else if (keyNames.length > 1)
signal(warning`in annotation preprocessing: target ${targetName} has multiple key elements`);
warning(null, null, `in annotation preprocessing: target ${targetName} has multiple key elements`);

@@ -141,3 +141,3 @@ // TODO: what happens if key of target is itself a managed association?

if (carrier.kind === 'entity' || carrier.kind === 'view') {
signal(warning`annotation preprocessing/${aNameWithoutQualifier}: annotation must not be used for an entity, ${ctx}`);
warning(null, null, `annotation preprocessing/${aNameWithoutQualifier}: annotation must not be used for an entity, ${ctx}`);
throw 'leave';

@@ -156,3 +156,3 @@ }

if (!assocName) {
signal(warning`in annotation preprocessing/${aNameWithoutQualifier}: value of 'viaAssociation' must be a path, ${ctx}`);
warning(null, null, `in annotation preprocessing/${aNameWithoutQualifier}: value of 'viaAssociation' must be a path, ${ctx}`);
throw 'leave';

@@ -162,3 +162,3 @@ }

if (!assoc || !(assoc.type === 'cds.Association' || assoc.type === 'cds.Composition')) {
signal(warning`in annotation preprocessing/${aNameWithoutQualifier}: there is no association "${assocName}", ${ctx}`);
warning(null, null, `in annotation preprocessing/${aNameWithoutQualifier}: there is no association "${assocName}", ${ctx}`);
throw 'leave';

@@ -173,3 +173,3 @@ }

if (annoNames.map(x=>x.split('#')[0]).find(x=>(x=='@Common.ValueList.viaAssociation'))) {
signal(warning`in annotation preprocessing/@Common.ValueList: 'entity' is ignored, as 'viaAssociation' is present, ${ctx}`);
warning(null, null, `in annotation preprocessing/@Common.ValueList: 'entity' is ignored, as 'viaAssociation' is present, ${ctx}`);
throw 'leave';

@@ -180,3 +180,3 @@ }

if (annoVal['=']) {
signal(warning`in annotation preprocessing/${aNameWithoutQualifier}: annotation value must be a string, ${ctx}`);
warning(null, null, `in annotation preprocessing/${aNameWithoutQualifier}: annotation value must be a string, ${ctx}`);
}

@@ -192,3 +192,3 @@

if (!vlEntity) {
signal(warning`in annotation preprocessing/${aNameWithoutQualifier}: entity "${enameFull}" does not exist, ${ctx}`);
warning(null, null, `in annotation preprocessing/${aNameWithoutQualifier}: entity "${enameFull}" does not exist, ${ctx}`);
throw 'leave';

@@ -224,7 +224,7 @@ }

if (keys.length === 0) {
signal(warning`in annotation preprocessing/value help shortcut: entity "${enameFull}" has no key, ${ctx}`);
warning(null, null, `in annotation preprocessing/value help shortcut: entity "${enameFull}" has no key, ${ctx}`);
throw 'leave';
}
else if (keys.length > 1)
signal(warning`in annotation preprocessing/value help shortcut: entity "${enameFull}" has more than one key, ${ctx}`);
warning(null, null, `in annotation preprocessing/value help shortcut: entity "${enameFull}" has more than one key, ${ctx}`);
valueListProp = keys[0];

@@ -268,3 +268,3 @@

let newObj = Object.create( Object.getPrototypeOf(carrier) );
for (let e in carrier) {
Object.keys(carrier).forEach( e => {
if (e === '@Common.ValueList.entity' || e === '@Common.ValueList.viaAssociation') {

@@ -284,3 +284,3 @@ newObj['@Common.ValueList.Label'] = label;

delete carrier[e];
}
});
Object.assign(carrier, newObj);

@@ -302,3 +302,3 @@ }

if (!textAnno) {
signal(warning`in annotation preprocessing: TextArrangement shortcut without Text annotation, ${ctx}`);
warning(null, null, `in annotation preprocessing: TextArrangement shortcut without Text annotation, ${ctx}`);
}

@@ -305,0 +305,0 @@

@@ -12,8 +12,6 @@ 'use strict';

const translate = require('./annotations/genericTranslation.js');
const alerts = require('../base/alerts');
const { setProp } = require('../base/model');
const { cloneCsn, isEdmPropertyRendered } = require('../model/csnUtils');
const { checkCSNVersion } = require('../json/csnVersion');
const builtins = require('../compiler/builtins');
const { handleMessages } = require('../base/messages');
const { makeMessageFunction } = require('../base/messages');

@@ -25,17 +23,13 @@ /*

/* invocation:
metadata: csn2edm(forOdata, { version:'v2' })
*/
function csn2edm(_csn, serviceName, _options) {
return csn2edmAll(_csn, _options, serviceName)[serviceName];
return csn2edmAll(_csn, _options, [ serviceName ])[ serviceName ];
}
function csn2edmAll(_csn, _options, serviceName=undefined) {
function csn2edmAll(_csn, _options, serviceNames=undefined) {
// get us a fresh model copy that we can work with
let csn = cloneCsn(_csn);
let csn = cloneCsn(_csn, _options);
// use original options for messages; cloned CSN for semantic location
const messageFunctions = alerts.makeMessageFunction(csn, _options);
const { info, warning, error } = messageFunctions;
const messageFunctions = makeMessageFunction(csn, _options, 'to.edmx');
const { info, warning, error, throwWithError } = messageFunctions;
checkCSNVersion(csn, _options);

@@ -45,7 +39,7 @@

let [ services, options ] = initializeModel(csn, _options);
let [ allServices, allSchemas, whatsMyServiceRootName, options ] = initializeModel(csn, _options);
const Edm = require('./edm.js')(options);
let v = options.v;
if(services.length === 0) {
if(Object.keys(allServices).length === 0) {
info(null, null, `No Services in model`);

@@ -55,17 +49,21 @@ return rc;

if(serviceName) {
let serviceCsn = services.filter(s => s.name == serviceName)[0];
if(serviceCsn == undefined) {
warning(null, null, `No service definition with name "${serviceName}" found in the model`);
}
else {
rc[serviceName] = createEdm(serviceCsn);
}
if(serviceNames === undefined)
serviceNames = options.serviceNames;
if(serviceNames) {
serviceNames.forEach(name => {
let serviceCsn = allServices[name];
if(serviceCsn == undefined) {
warning(null, null, { name }, 'No service definition with name $(NAME) found in the model');
}
else {
rc[name] = createEdm(serviceCsn);
}
});
}
else {
rc = services.reduce((services, serviceCsn) => {
rc = Object.values(allServices).reduce((services, serviceCsn) => {
services[serviceCsn.name] = createEdm(serviceCsn);
return services; }, rc);
}
handleMessages(csn, options);
throwWithError();
return rc;

@@ -86,2 +84,3 @@

const service = new Edm.DataServices(v);
/** @type {object} */
const edm = new Edm.Edm(v, service);

@@ -123,3 +122,19 @@

-----------------------------------------------*/
let LeadSchema;
const fqSchemaXRef = [serviceCsn.name];
const whatsMySchemaName = function(n) {
return fqSchemaXRef.reduce((rc, sn) => !rc && n && n.startsWith(sn + '.') ? rc = sn : rc, undefined);
}
// create schema containers
const subSchemaDictionary = {
[serviceCsn.name]: {
name: serviceCsn.name,
fqName: serviceCsn.name,
_csn: serviceCsn,
container: true,
definitions: Object.create(null)
}
};
if(options.isV4()) {

@@ -129,25 +144,13 @@ // tunnel schema xref and servicename in options to edm.Typebase to rectify

options.serviceName = serviceCsn.name;
const fqSchemaXRef = [serviceCsn.name];
options.whatsMySchemaName = function(n) {
return fqSchemaXRef.reduce((rc, sn) => !rc && n && n.startsWith(sn + '.') ? rc = sn : rc, undefined);
}
// List of all schema names in this service, including the service itself
options.whatsMySchemaName = whatsMySchemaName;
const schemas = {
[serviceCsn.name]: {
name: serviceCsn.name,
fqName: serviceCsn.name,
_csn: serviceCsn,
container: true,
definitions: Object.create(null)
}
};
const fqNameToSchema = { [serviceCsn.name]: schemas[serviceCsn.name] };
Object.entries(csn.definitions).reduce((schemas, [fqName, art]) => {
// add sub schemas
if(fqName.startsWith(serviceCsn.name + '.') && art.kind === 'context') {
// Add additional schema containers as sub contexts to the service
Object.entries(allSchemas).reduce((subSchemaDictionary, [fqName, art]) => {
if(serviceCsn.name === whatsMyServiceRootName(fqName) &&
fqName.startsWith(serviceCsn.name + '.') && art.kind === 'schema') {
fqSchemaXRef.push(fqName);
// strip the toplevel service schema name
// Strip the toplevel service schema name (see comment above)
const name = fqName.replace(serviceCsn.name + '.', '');
schemas[name] = {
subSchemaDictionary[name] = {
name,

@@ -159,30 +162,102 @@ fqName,

};
fqNameToSchema[fqName] = schemas[name];
}
return schemas;
}, schemas);
return subSchemaDictionary;
}, subSchemaDictionary);
// sort schemas in reverse order to allow longest match
// Sort schema names in reverse order to allow longest match
fqSchemaXRef.sort((a,b) => b.length-a.length);
// fill the schemas, unfortunately this can't be done in one step
// as all possible prefix combinations must be known in fqSchemaXRef
Object.entries(csn.definitions).reduce((schemas, [name, art]) => {
// Fill the schemas and references, fqSchemaXRef must be complete
populateSchemas(subSchemaDictionary);
const xServiceRefs = populateXserviceRefs();
/* TODO:
const references = Object.entries(allSchemas).reduce((references, [fqName, art]) => {
// add references
if(fqName.startsWith(serviceCsn.name + '.') && art.kind === 'reference') {
fqSchemaXRef.push(fqName);
references.push(art);
}
return references;
}, []);
*/
// Add xrefs to full schema cross ref list for further usage
fqSchemaXRef.push(...xServiceRefs);
fqSchemaXRef.sort((a,b) => b.length-a.length);
// Bring the schemas in alphabetical order, service first, root last
const sortedSchemaNames = Object.keys(subSchemaDictionary).filter(n => n !== 'root' && n !== serviceCsn.name).sort();
if(subSchemaDictionary.root)
sortedSchemaNames.push('root');
// Finally create the schemas and register them in the service.
LeadSchema = createSchema(subSchemaDictionary[serviceCsn.name]);
service.registerSchema(serviceCsn.name, LeadSchema);
sortedSchemaNames.forEach(name => {
const schema = subSchemaDictionary[name];
service.registerSchema(schema.fqName, createSchema(schema));
});
// Add cross service references to the EDM
xServiceRefs.forEach(ref => {
let r = new Edm.Reference(v, ref.ref);
r.append(new Edm.Include(v, ref.inc))
edm._defaultRefs.push(r);
});
}
else {
populateSchemas(subSchemaDictionary);
LeadSchema = createSchema(subSchemaDictionary[serviceCsn.name]);
service.registerSchema(serviceCsn.name, LeadSchema);
}
/*
EntityContainer duplicate check
*/
service._children.forEach(c => {
c._ec && Object.entries(c._ec._registry).forEach((([setName, arr]) => {
if(arr.length > 1) {
error(null, null, { name: c.Namespace, id: setName },
`Namespace $(NAME): Duplicate entries in EntityContainer with Name=$(ID) for ${arr.map(a =>a.getDuplicateMessage()).join(', ')} `);
}
}));
});
// Create annotations and distribute into Schemas
translate.csn2annotationEdm(csn, edm, serviceCsn.name, options);
return edm
// Sort definitions into their schema container
function populateSchemas(schemas) {
Object.entries(csn.definitions).reduce((schemas, [fqName, art]) => {
// Identify service members by their definition name only, this allows
// to let the internal object.name have the sub-schema name.
let schemaName = options.whatsMySchemaName(name);
// With nested services we must do a longest path match and check wether
// the current definition belongs to the current toplevel service definition.
if(schemaName && art.kind !== 'context') {
// strip the toplevel serviceName from object.name except if the schema name is the service name itself
// proxy names are not prefixed, as they need to be reused.
if(schemaName !== serviceCsn.name) {
name = art.name = name.replace(serviceCsn.name + '.', '');
schemaName = schemaName.replace(serviceCsn.name + '.', '');
// Definition is to be considered if
// its name has a schema prefix and it's not a schema defining context
// and its service root is the current service being generated
let mySchemaName = whatsMySchemaName(fqName);
// Add this definition to a (sub) schema, if it is not
// a container (context, service) and
// not marked to be ignored as schema member
if(mySchemaName &&
serviceCsn.name === whatsMyServiceRootName(fqName, false) &&
![ 'context', 'service' ].includes(art.kind)) {
// Strip the toplevel serviceName from object.name
// except if the schema name is the service name itself.
// Proxy names are not prefixed, as they need to be reused.
if(mySchemaName !== serviceCsn.name) {
fqName = art.name = fqName.replace(serviceCsn.name + '.', '');
mySchemaName = mySchemaName.replace(serviceCsn.name + '.', '');
}
schemas[schemaName].definitions[name] = art;
schemas[mySchemaName].definitions[fqName] = art;
}
return schemas;
}, schemas);
}
// Fill xServiceRefs for Edm.Reference
function populateXserviceRefs() {
/*

@@ -208,7 +283,8 @@ References into other Schemas

};
*/
*/
const references = Object.entries(csn.definitions).reduce((references, [fqName, art]) => {
return Object.entries(allSchemas).reduce((references, [fqName, art]) => {
// add references
if(fqName.startsWith(serviceCsn.name + '.') && art.kind === 'reference') {
let mySchemaName = whatsMySchemaName(fqName);
if(art.kind === 'reference' && mySchemaName && serviceCsn.name === whatsMyServiceRootName(fqName, false)) {
fqSchemaXRef.push(fqName);

@@ -219,44 +295,9 @@ references.push(art);

}, []);
fqSchemaXRef.sort((a,b) => b.length-a.length);
// bring the schemas in alphabetical order, service first, root last
const sortedSchemaNames = Object.keys(schemas).filter(n => n !== 'root' && n !== serviceCsn.name).sort();
sortedSchemaNames.splice(0,0, serviceCsn.name);
if(schemas.root)
sortedSchemaNames.push('root');
// finally create the schemas and register them in the service.
sortedSchemaNames.forEach(name => {
const schema = schemas[name];
service.registerSchema(schema.fqName, createSchema(schema));
});
references.forEach(ref => {
let r = new Edm.Reference(v, ref.ref);
r.append(new Edm.Include(v, ref.inc))
edm._defaultRefs.push(r);
});
}
else {
const schema = {
name: serviceCsn.name,
fqName: serviceCsn.name,
_csn: serviceCsn,
container: true,
definitions: csn.definitions
};
const LeadSchema = createSchema(schema);
service.registerSchema(schema.fqName, LeadSchema);
}
// Create annotations and distribute into Schemas
translate.csn2annotationEdm(csn, edm, serviceCsn.name, options);
return edm
// Main schema creator function
function createSchema(schema) {
/** @type {object} */
const Schema = new Edm.Schema(v, schema.name, undefined /* unset alias */, schema._csn, /* annotations */ [], schema.container);
const EntityContainer = Schema._ec || (LeadSchema && LeadSchema._ec);
// now namespace and alias are used to create the fullQualified(name)

@@ -274,3 +315,3 @@ const schemaNamePrefix = schema.name + '.'

edmUtils.foreach(schemaCsn.definitions,
a => edmUtils.isEntityOrView(a) && !a.abstract && a.name.startsWith(schemaNamePrefix),
a => edmUtils.isEntity(a) && !a.abstract && a.name.startsWith(schemaNamePrefix),
createEntityTypeAndSet

@@ -282,4 +323,6 @@ );

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

@@ -322,11 +365,13 @@ if(options.isV4())

// FIXME: Location for sub schemas?
warning(null, ['definitions', Schema.Namespace], `Schema "${Schema.Namespace}" is empty`);
warning(null, ['definitions', Schema.Namespace], { name: Schema.Namespace }, 'Schema $(NAME) is empty');
}
for(let name in NamesInSchemaXRef) {
if(NamesInSchemaXRef[name].length > 1) {
Object.entries(NamesInSchemaXRef).forEach(([name, refs]) => {
if(refs.length > 1) {
let artifactName = `${Schema.Namespace}.${name}`;
error(null, ['definitions', artifactName], `Duplicate name "${name}" in Schema "${Schema.Namespace}"`);
error(null, ['definitions', artifactName], { name: Schema.Namespace },
'Duplicate name in Schema $(NAME)');
}
}
});
return Schema;

@@ -337,10 +382,12 @@

let EntityTypeName = entityCsn.name.replace(schemaNamePrefix, '');
let EntitySetName = (entityCsn.$entitySetName || entityCsn.name).replace(schemaNamePrefix, '');
let EntitySetName = edmUtils.getBaseName(entityCsn.$entitySetName || entityCsn.name);
let [ properties, hasStream ] = createProperties(entityCsn);
const loc = ['definitions', entityCsn.name];
const type = `${schema.name}.${EntityTypeName}`;
if(properties.length === 0) {
warning(null, ['definitions', entityCsn.name], `EntityType "${schema.name}.${EntityTypeName}" has no properties`);
warning(null, loc, { type }, 'EDM EntityType $(TYPE) has no properties');
} else if(entityCsn.$edmKeyPaths.length === 0) {
warning(null, ['definitions', entityCsn.name], `EntityType "${schema.name}.${EntityTypeName}" has no primary key`);
warning(null, loc, { type }, 'EDM EntityType $(TYPE) has no primary key');
}

@@ -357,3 +404,3 @@

if (Schema._ec && entityCsn.$hasEntitySet)
if (EntityContainer && entityCsn.$hasEntitySet)
{

@@ -385,3 +432,3 @@ /** @type {object} */

Schema._ec.append(containerEntry);
EntityContainer.register(containerEntry);
}

@@ -401,3 +448,3 @@

let actionName = actionCsn.name.replace(schemaNamePrefix, '');
let actionName = edmUtils.getBaseName(actionCsn.name);

@@ -427,3 +474,3 @@ let attributes = { Name: actionName, IsBound : false };

}
else if(Schema._ec)// unbound => produce Action/FunctionImport
else if(EntityContainer)// unbound => produce Action/FunctionImport
{

@@ -441,6 +488,6 @@ /** @type {object} */

{
actionImport.EntitySet = rt.replace(schemaNamePrefix, '');
actionImport.EntitySet = edmUtils.getBaseName(rt);
}
}
Schema._ec.append(actionImport);
EntityContainer.register(actionImport);
}

@@ -485,3 +532,3 @@

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

@@ -517,6 +564,6 @@ functionImport.EntitySet = rt.replace(schemaNamePrefix, '');

// is this still required?
for (let p in actionCsn)
edmUtils.forAll(actionCsn, (v, p) => {
if (p.match(/^@sap\./))
functionImport.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : actionCsn[p] });
functionImport.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : v });
});
// then append all other parameters

@@ -530,4 +577,4 @@ // V2 XML: Parameters that are not explicitly marked as Nullable or NotNullable in the CSN must become Nullable=true

if(Schema._ec)
Schema._ec.append(functionImport);
if(EntityContainer)
EntityContainer.register(functionImport);

@@ -542,3 +589,3 @@ function getReturnType(action)

if(type && action.returns.items)
if(action.returns._isCollection)
type = `Collection(${type})`

@@ -600,4 +647,4 @@

hasStream = true;
info(null, ['definitions', parentCsn.name],
`"${parentCsn.name}: Property "${elementName}" annotated with '@Core.MediaType' is removed from EDM in Odata V2`);
info(null, ['definitions', parentCsn.name], { name: parentCsn.name, id: elementName, anno: '@Core.MediaType' },
'$(NAME): Property $(ID) annotated with $(ANNO) is removed from EDM in OData V2');

@@ -623,3 +670,4 @@ } else

if(properties.length === 0) {
warning(null, structuredTypeCsn.$path, `ComplexType "${structuredTypeCsn.name}" has no properties`);
warning(null, ['definitions', structuredTypeCsn.name], { name: structuredTypeCsn.name },
'EDM ComplexType $(NAME) has no properties');
}

@@ -637,2 +685,3 @@ complexType.append(...(properties));

let props = { Name: typeCsn.name.replace(schemaNamePrefix, '') };
/* Do not render enum types any more, code remains here for future reference
if((typeCsn.items && typeCsn.items.enum) || typeCsn.enum) {

@@ -644,4 +693,5 @@ if (!builtins.isIntegerTypeName(typeCsn.type)) {

} else {
typeDef = new Edm.TypeDefinition(v, props, typeCsn );
}
*/
typeDef = new Edm.TypeDefinition(v, props, typeCsn );
// }
Schema.append(typeDef);

@@ -777,3 +827,3 @@ }

// A managed composition is treated as association
if(navigationProperty._csn.type === 'cds.Composition' && (navigationProperty._csn.on || navigationProperty._csn.onCond)) {
if(navigationProperty._csn.type === 'cds.Composition' && navigationProperty._csn.on) {
navigationProperty._edmAssociation.append(Edm.ReferentialConstraint.createV2(v,

@@ -789,3 +839,3 @@ toRole, fromRole, constraints.constraints));

Schema.append(navigationProperty._edmAssociation);
if(Schema._ec && !navigationProperty._targetCsn.$proxy) {
if(EntityContainer && !navigationProperty._targetCsn.$proxy) {
let assocSet = new Edm.AssociationSet(v, { Name: assocName, Association: fullQualified(assocName) },

@@ -795,3 +845,3 @@ fromRole, toRole, fromEntitySet, toEntitySet);

assocSet.setSapVocabularyAsAttributes(navigationProperty._csn._SetAttributes);
Schema._ec.append(assocSet);
EntityContainer.register(assocSet);
}

@@ -798,0 +848,0 @@ }

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

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

@@ -41,8 +40,10 @@ module.exports = function (options) {

let newAttributes = Object.create(null);
for (let p in attributes) newAttributes[p] = {
value: attributes[p],
configurable: true,
enumerable: false,
writable: true
}
edmUtils.forAll(attributes, (value, p) => {
newAttributes[p] = {
value,
configurable: true,
enumerable: false,
writable: true
}
});
return Object.defineProperties(this, newAttributes)

@@ -91,7 +92,6 @@ }

{
for (let p in this)
{
edmUtils.forAll(this, (v,p) => {
if (p !== 'Name')
json[p[0] === '@' ? p : '$' + p] = this[p];
}
json[p[0] === '@' ? p : '$' + p] = v;
});
return json;

@@ -137,11 +137,10 @@ }

let tmpStr = '';
for (let p in this) {
edmUtils.forAll(this, (v, p) => {
if (typeof this[p] !== 'object')
tmpStr += ' ' + p + '="' + edmUtils.escapeString(this[p]) + '"'
}
for (let p in this._xmlOnlyAttributes)
{
if (typeof this._xmlOnlyAttributes[p] !== 'object')
tmpStr += ' ' + p + '="' + edmUtils.escapeString(this._xmlOnlyAttributes[p]) + '"'
}
tmpStr += ' ' + p + '="' + edmUtils.escapeString(v) + '"'
});
edmUtils.forAll(this._xmlOnlyAttributes, (v,p) => {
if (typeof v !== 'object')
tmpStr += ' ' + p + '="' + edmUtils.escapeString(v) + '"'
});
return tmpStr;

@@ -166,5 +165,6 @@ }

const attr = (useSetAttributes ? csn._SetAttributes : csn);
for (let p in attr)
edmUtils.forAll(attr, (v, p) => {
if (p.match(/^@sap./))
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : attr[p] } );
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : v } );
});
}

@@ -216,3 +216,3 @@ }

super(v, props);
this.set( { _annotations: annotations, _actions: {} } );
this.set( { _annotations: annotations, _actions: Object.create(null) } );
this.setXml( { xmlns: (this.v2) ? 'http://schemas.microsoft.com/ado/2008/09/edm' : 'http://docs.oasis-open.org/odata/ns/edm' } );

@@ -275,7 +275,6 @@

{
for (let p in this)
{
edmUtils.forAll(this, (v,p) => {
if (p !== 'Name' && p !== 'Namespace')
json[p[0] === '@' ? p : '$' + p] = this[p];
}
json[p[0] === '@' ? p : '$' + p] = v;
});
}

@@ -417,3 +416,5 @@

{
return '<?xml version="1.0" encoding="utf-8"?>\n' + super.toXML('', what);
let rc = '<?xml version="1.0" encoding="utf-8"?>\n';
rc += `${super.toXML('', what)}`;
return rc;
}

@@ -435,2 +436,6 @@

{
constructor() {
super(...arguments);
this.set( { _registry: Object.create(null) } );
}
// use the _SetAttributes

@@ -441,2 +446,9 @@ setSapVocabularyAsAttributes(csn)

}
register(entry) {
if(!this._registry[entry.Name])
this._registry[entry.Name] = [entry];
else
this._registry[entry.Name].push(entry);
super.append(entry);
}
}

@@ -450,12 +462,11 @@

{
for (let p in this)
{
edmUtils.forAll(this, (v,p) => {
if (p !== 'Name')
{
if(p === 'EntityType') // it's $Type in json
json['$Type'] = this[p];
json['$Type'] = v;
else
json[p[0] === '@' ? p : '$' + p] = this[p];
json[p[0] === '@' ? p : '$' + p] = v;
}
}
});
return json;

@@ -473,2 +484,6 @@ }

}
getDuplicateMessage() {
return `EntityType "${this.EntityType}"`
}
}

@@ -562,4 +577,12 @@

*/
class FunctionImport extends Node {} //ActionFunctionBase {}
class ActionImport extends Node {}
class FunctionImport extends Node {
getDuplicateMessage() {
return `Function "${this.Name}"`
}
} //ActionFunctionBase {}
class ActionImport extends Node {
getDuplicateMessage() {
return `Action "${this.Name}"`
}
}

@@ -643,11 +666,10 @@ class TypeBase extends Node

for (let p in this)
{
edmUtils.forAll(this, (v,p) => {
if (p !== 'Name' && p != this._typeName
// remove this line if Nullable=true becomes default
&& !(p === 'Nullable' && this[p] == false))
&& !(p === 'Nullable' && v == false))
{
json[p[0] === '@' ? p : '$' + p] = this[p]
json[p[0] === '@' ? p : '$' + p] = v;
}
}
});

@@ -875,8 +897,7 @@ if(this._isCollection)

// translate the following @sap annos as xml attributes to the Property
for (let p in csn)
{
edmUtils.forAll(csn, (v, p) => {
// @ts-ignore
if (Property.SAP_Annotation_Attribute_WhiteList.includes(p))
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn[p] });
}
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : v });
});
}

@@ -887,3 +908,3 @@

// added a @Core.ComputedDefaultValue for complex defaults
if (csn.default && !csn['@Core.ComputedDefaultValue'] && isBetaEnabled(options, 'odataDefaultValues')) {
if (csn.default && !csn['@Core.ComputedDefaultValue']) {

@@ -906,3 +927,8 @@ let def = csn.default;

if(defVal !== undefined) {
this[`Default${this.v4 ? 'Value' : ''}`] = ['cds.Boolean', 'cds.Binary', 'cds.LargeBinary', 'cds.Integer64', 'cds.Integer'].includes(csn.type)
/* No Default Value rendering in V2 (or only with future flag).
Reason: Fiori UI5 expects 'Default' under extension namespace 'sap:'
Additionally: The attribute is named 'Default' in V2 and 'DefaultValue' in V4
*/
if(this.v4)
this[`Default${this.v4 ? 'Value' : ''}`] = ['cds.Boolean', 'cds.Binary', 'cds.LargeBinary', 'cds.Integer64', 'cds.Integer'].includes(csn.type)
? defVal

@@ -1180,5 +1206,6 @@ : edmUtils.escapeString(defVal);

{
for (let p in this)
edmUtils.forAll(this, (v,p) => {
if (p !== 'Target')
json[p[0] === '@' ? p : '$' + p] = this[p]
json[p[0] === '@' ? p : '$' + p] = v;
});
return json;

@@ -1366,3 +1393,6 @@ }

}
}
getDuplicateMessage() {
return `Association "${this.Association}"`
}
}

@@ -1369,0 +1399,0 @@ class Dependent extends Node {}

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

function foreach(dictionary, filter, func) {
if (dictionary != undefined) {
for (let name in dictionary) {
if (dictionary[name] && filter(dictionary[name])) {
if(Array.isArray(func))
func.forEach(f=>f(dictionary[name], name));
else
func(dictionary[name], name);
}
dictionary && Object.entries(dictionary).forEach(([name, value]) => {
if (filter(value)) {
if(Array.isArray(func))
func.forEach(f=>f(value, name));
else
func(value, name);
}
}
});
}

@@ -102,3 +100,3 @@

{
return isAssociation(artifact) && artifact.onCond == undefined && artifact.on == undefined;
return isAssociation(artifact) && artifact.on == undefined;
}

@@ -119,16 +117,11 @@

function isEntityOrView(artifact)
function isEntity(artifact)
{
return artifact.kind === 'entity' || artifact.kind === 'view';
return ['entity'].includes(artifact.kind);
}
function isParameterizedEntityOrView(artifact) {
return isEntityOrView(artifact) && artifact.params;
function isParameterizedEntity(artifact) {
return isEntity(artifact) && artifact.params;
}
// Return true if 'artifact' is a real structured type (not an entity)
function isStructuredType(artifact) {
return isStructuredArtifact(artifact) && !isEntityOrView(artifact);
}
// Return true if 'artifact' is structured (i.e. has elements, like a structured type or an entity)

@@ -140,12 +133,15 @@ function isStructuredArtifact(artifact) {

function isDerivedType(artifact)
{
return artifact.kind === 'type' && !isStructuredType(artifact);
// Return true if 'artifact' is a real structured type (not an entity)
function isStructuredType(artifact) {
return ['type'].includes(artifact.kind) && isStructuredArtifact(artifact);
}
function isActionOrFunction(artifact)
{
return artifact.kind === 'action' || artifact.kind === 'function';
function isDerivedType(artifact) {
return ['type'].includes(artifact.kind) && !isStructuredArtifact(artifact);
}
function isActionOrFunction(artifact) {
return ['action', 'function'].includes(artifact.kind);
}
function resolveOnConditionAndPrepareConstraints(assocCsn, messageFunctions) {

@@ -212,3 +208,5 @@ if(!assocCsn._constraints)

{
warning(null, ['definitions', parentArtifactName], `Cannot resolve backlink to ${assocCsn._target.name}/${partner}" from "${parentArtifactName}/${assocCsn.name}"`);
warning(null, ['definitions', parentArtifactName],
{ partner: `${assocCsn._target.name}/${partner}`, name: `${parentArtifactName}/${assocCsn.name}` },
'Can\'t resolve backlink to $(PARTNER) from $(NAME)');
}

@@ -358,3 +356,3 @@ });

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

@@ -404,6 +402,7 @@ let realFk = assocCsn._parent.items ? assocCsn._parent.items.elements[fk.$generatedFieldName] : assocCsn._parent.elements[fk.$generatedFieldName];

------------
undef => '*' // CDS default mapping for associations
1 => 0..1 // CDS default mapping for compositions
undef => '*' // CDS default mapping for associations
undef => 1 // CDS default mapping for compositions
1 => 0..1 // Association
1 => 1 // Composition
n => '*'
n/a => 1 // not expressable
* => *

@@ -430,27 +429,14 @@

const isAssoc = csn.type === 'cds.Association';
if(!csn.cardinality)
csn.cardinality = Object.create(null);
// set missing defaults
// A managed composition is treated as an ordinary association
/*
if(csn.type === 'cds.Composition' && csn.on || csn.onCond) {
if(!csn.cardinality.src)
csn.cardinality.src = '1';
if(!csn.cardinality.min)
csn.cardinality.min = 0;
if(!csn.cardinality.max)
csn.cardinality.max = '*';
}
else */
{
if(!csn.cardinality.src)
csn.cardinality.src = '*';
if(!csn.cardinality.min)
csn.cardinality.min = 0;
if(!csn.cardinality.max)
csn.cardinality.max = 1;
}
if(!csn.cardinality.src)
csn.cardinality.src = isAssoc ? '*' : '1';
if(!csn.cardinality.min)
csn.cardinality.min = 0;
if(!csn.cardinality.max)
csn.cardinality.max = 1;
let srcCardinality = (csn.cardinality.src == 1) ? '0..1' : '*';
let srcCardinality = (csn.cardinality.src == 1) ? (isAssoc ? '0..1' : '1') : '*';
let tgtCardinality = (csn.cardinality.max > 1 || csn.cardinality.max === '*') ? '*' :

@@ -522,3 +508,3 @@ (csn.cardinality.min == 1) ? '1' : '0..1';

if (edmType == undefined) {
error(null, csn.$location, `No Edm type available for "${cdsType}"`);
error(null, csn.$path, { type: cdsType }, `No EDM type available for $(TYPE)`);
}

@@ -532,6 +518,6 @@ if(isV2)

if(['cds.hana.ST_POINT', 'cds.hana.ST_GEOMETRY'].includes(cdsType)) {
error(null, csn.$location, `"OData V2 does not support Geometry data types, ${cdsType}" cannot be mapped`);
error(null, csn.$path, { type: cdsType }, `OData V2 does not support Geometry data types, $(TYPE) can't be mapped`);
}
if(cdsType === 'cds.DecimalFloat' || cdsType === 'cds.hana.SMALLDECIMAL')
warning(null, csn.$location, `"OData V2 does not support ${cdsType}"`);
warning(null, csn.$path, { type: cdsType }, `OData V2 does not support $(TYPE)`);
}

@@ -616,2 +602,14 @@ else // isV4

// return the path prefix of a given name or if no prefix available 'root'
function getSchemaPrefix(name) {
const lastDotIdx = name.lastIndexOf('.');
return (lastDotIdx > 0 ) ? name.substring(0, lastDotIdx) : 'root';
}
// get artifacts base name
function getBaseName(name) {
const lastDotIdx = name.lastIndexOf('.');
return (lastDotIdx > 0 ) ? name.substring(lastDotIdx+1, name.length) : name;
}
module.exports = {

@@ -628,6 +626,6 @@ validateOptions,

isToMany,
isEntityOrView,
isEntity,
isStructuredType,
isStructuredArtifact,
isParameterizedEntityOrView,
isParameterizedEntity,
isDerivedType,

@@ -642,2 +640,4 @@ isActionOrFunction,

escapeString,
getSchemaPrefix,
getBaseName
}

@@ -18,4 +18,6 @@ // csn version functions

const newCSNVersions = ["0.1.99","0.2","0.2.0","1.0"];
// checks if new-csn is requested vie the options of already specified in the CSN
// Use literal version constants intentionally and not number intervals to
// record all published version strings of the core compiler.
const newCSNVersions = ["0.1.99","0.2","0.2.0","1.0","2.0"];
// checks if new-csn is requested via the options of already specified in the CSN
// default: old-style

@@ -33,13 +35,13 @@ function isNewCSN(csn, options) {

function checkCSNVersion(csn, options) {
// the new transformer works only with new CSN
const alerts = require('../base/alerts');
const { CompilationError } = require('../base/messages');
if (!isNewCSN(csn, options)) {
// the new transformer works only with new CSN
const { makeMessageFunction } = require('../base/messages');
const { error, throwWithError } = makeMessageFunction(csn, options);
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);
error(null, null, errStr);
throwWithError();
}

@@ -46,0 +48,0 @@ }

@@ -91,6 +91,7 @@ // CSN frontend - transform CSN into XSN

const { normalizeLocation } = require('../base/location');
const { getMessageFunction } = require('../base/messages');
const { addToDict } = require('../base/dictionaries');
const { makeMessageFunction } = require('../base/messages');
const { dictAdd } = require('../base/dictionaries');
let inExtensions = null;
let vocabInDefinitions = null;

@@ -109,4 +110,4 @@ // CSN property names reserved for CAP

// do not include CSN v0.1.0 properties here:
'ref', 'xpr', 'val', '#', 'func', 'SELECT', 'SET', // Core Compiler checks SELECT/SET
'param', 'global', 'literal', 'args', 'cast', // only with 'ref'/'ref'/'val'/'func'
'ref', 'xpr', 'list', 'val', '#', 'func', 'SELECT', 'SET', // Core Compiler checks SELECT/SET
'param', 'global', 'literal', 'args', 'cast', // only with 'ref'/'ref'/'val'/'func'
];

@@ -119,6 +120,6 @@

':expr': [
'ref', 'xpr', 'val', '#', 'func', 'SELECT', 'SET',
'ref', 'xpr', 'list', 'val', '#', 'func', 'SELECT', 'SET',
'=', 'path', 'value', 'op', // '='/'path' is CSN v0.1.0 here
],
':ext': [ 'annotate', 'extend' ],
':ext': [ 'annotate', 'extend' ], // TODO: better msg for test/negative/UnexpectedProperties.csn
':assoc': [ 'on', 'keys', 'foreignKeys', 'onCond' ], // 'foreignKeys'/'onCond' is CSN v0.1.0

@@ -131,2 +132,6 @@ ':join': [ 'join', 'as' ],

// Functions reading properties which do no count for the message
// 'Object in $(PROP) must have at least one property'
const functionsOfIrrelevantProps = [ ignore, extra, explicitName ];
const schemaClasses = {

@@ -137,2 +142,3 @@ condition: {

msgId: 'syntax-csn-expected-term',
// TODO: also specify requires here, and adapt onlyWith()
optional: exprProperties,

@@ -164,6 +170,11 @@ },

validKinds: [
'entity', 'type', 'action', 'function', 'context', 'service', 'event', 'annotation',
'entity', 'type', 'aspect', 'action', 'function', 'context', 'service', 'event', 'annotation',
],
// requires: { entity: ['elements', 'query', 'includes'] } - not, make it work w/o elements
},
vocabularies: {
dictionaryOf: definition,
defaultKind: 'annotation',
validKinds: [],
},
extensions: {

@@ -185,5 +196,15 @@ arrayOf: definition,

validKinds: [ 'element' ],
inKind: [ 'element', 'type', 'entity', 'param', 'annotation', 'event', 'annotate', 'extend' ],
inKind: [
'element',
'type',
'aspect',
'entity',
'param',
'annotation',
'event',
'annotate',
'extend',
],
},
payload: { // keep it for a while, TODO: remove with v2
payload: { // keep it for a while, TODO: remove with v2 - at least warning
dictionaryOf: definition, // duplicate of line below only for better error message

@@ -209,4 +230,4 @@ type: renameTo( 'elements', dictionaryOf( definition ) ),

dictionaryOf: definition,
defaultKind: 'element',
validKinds: [], // XSN TODO: kind 'mixin' by parser
defaultKind: 'mixin',
validKinds: [],
},

@@ -218,4 +239,12 @@ columns: {

validKinds: [], // pseudo kind '$column'
requires: [ 'ref', 'xpr', 'val', '#', 'func', 'SELECT', 'SET' ], // requires one of...
requires: [ 'ref', 'xpr', 'val', '#', 'func', 'list', 'SELECT', 'SET' ], // requires one of...
inKind: [ 'extend' ], // only valid in extend and SELECT
schema: {
xpr: {
class: 'condition',
type: xprInValue,
inKind: [ '$column' ],
inValue: true,
},
},
},

@@ -252,4 +281,4 @@ keys: {

as: {
// remark: 'as' does not count as "proper" property in standard check that
// an object has >0 props (hard-coded)
// remark: 'as' does not count as "relevant" property in standard check that
// an object has >0 props, see const functionsOfIrrelevantProps.
type: explicitName,

@@ -374,4 +403,8 @@ inKind: [ '$column', 'key' ],

type: xpr,
inKind: [ '$column' ],
// special treatment in $column
},
list: {
class: 'condition',
type: list,
},
val: {

@@ -404,9 +437,19 @@ type: value,

query: {
type: object,
type: embed,
optional: [ 'SELECT', 'SET' ],
inKind: [ 'entity', 'event' ],
},
projection: {
type: queryTerm,
xsnOp: 'SELECT',
requires: 'from',
optional: [
'from', 'all', 'distinct', 'columns', 'excluding', // no 'mixin'
'where', 'groupBy', 'having', 'orderBy', 'limit',
],
inKind: [ 'entity', 'event' ],
},
SELECT: {
type: queryTerm,
xsnOp: 'query', // TODO: 'SELECT'
xsnOp: 'SELECT',
requires: 'from',

@@ -421,3 +464,3 @@ optional: [

type: queryTerm,
xsnOp: 'subquery', // might be overwritten by 'op'
xsnOp: '$query', // might be overwritten by 'op'
requires: 'args',

@@ -427,3 +470,3 @@ optional: [ 'op', 'all', 'distinct', 'args', 'orderBy', 'limit' ],

args: {
arrayOf: object,
arrayOf: embed, // like query
type: queryArgs,

@@ -446,3 +489,3 @@ minLength: 1,

from: {
type: from, // XSN TODO: no array anymore, then type: object
type: object,
optional: [ 'ref', 'global', 'join', 'cardinality', 'args', 'on', 'SELECT', 'SET', 'as' ],

@@ -462,6 +505,2 @@ schema: {

},
on: { // remove if XSN TODO onCond -> on has been done
class: 'condition', // no renaming to onCond in XSN
onlyWith: [ 'join' ],
},
},

@@ -472,11 +511,10 @@ },

distinct: { type: asQuantifier },
all: { type: asQuantifier }, // XSN TODO: use quantifier also for UNION etc
all: { type: asQuantifier },
// further query properties: -----------------------------------------------
excluding: {
arrayOf: string,
type: excluding, // XSN TODO: exclude->excluding
type: excluding,
},
on: {
class: 'condition',
type: onCondition, // XSN TODO: 'onCond'->'on', then remove schema 'on' in 'from'
onlyWith: [ 'target', 'join' ],

@@ -488,3 +526,3 @@ inKind: [ 'element', 'mixin' ],

inKind: [],
type: expr,
type: renameTo( 'on', expr ),
optional: exprProperties,

@@ -501,4 +539,4 @@ },

},
orderBy: { // TODO XSN: make `sort` and `nulls` sibling properties
arrayOf: orderBy, optional: [ 'sort', 'nulls', ...exprProperties ],
orderBy: {
arrayOf: expr, optional: [ 'sort', 'nulls', ...exprProperties ],
},

@@ -511,8 +549,7 @@ sort: {

},
limit: { // XSN TODO: structure like CSN, then type: object
type: embed, requires: 'rows', optional: [ 'rows', 'offset' ],
limit: {
type: object, requires: 'rows', optional: [ 'rows', 'offset' ],
},
rows: {
class: 'expression',
type: renameTo( 'limit', expr ),
},

@@ -532,7 +569,6 @@ offset: {

},
abstract: {
type: boolOrNull,
inKind: [ 'entity', 'service' ],
abstract: { // v1: with 'abstract', an entity becomes an aspect
type: ( val, spec ) => boolOrNull( val, spec ) && undefined,
inKind: [ 'entity', 'aspect' ], // 'aspect' because 'entity' is replaced by 'aspect' early
},
// dbType: boolOrNull, // TODO: currently with --hana-flavor only
key: {

@@ -550,3 +586,2 @@ type: boolOrNull,

},
// unique: boolOrNull, // TECHNICAL CONFIGURATION only
virtual: {

@@ -557,3 +592,3 @@ type: boolOrNull,

cast: {
type: cast,
type: embed,
// cast can be:

@@ -569,14 +604,15 @@ // 1. Inside "columns" => not in value

class: 'expression',
inKind: [ 'element', 'param' ],
inKind: [ 'element', 'param', 'type' ],
},
includes: {
arrayOf: stringRef,
inKind: [ 'entity', 'type', 'event', 'extend' ],
inKind: [ 'entity', 'type', 'aspect', 'event', 'extend' ],
},
returns: {
type: object,
optional: [ ...typeProperties ],
type: definition,
defaultKind: 'param',
validKinds: [ 'param' ],
inKind: [ 'action', 'function' ],
},
technicalConfig: { // TODO: spec, re-check
technicalConfig: { // treat it like external_property
type: extra,

@@ -588,3 +624,3 @@ inKind: [ 'entity' ],

ignore: true,
inKind: [ 'entity', 'type' ],
inKind: [ 'entity', 'type', 'aspect' ],
},

@@ -636,3 +672,3 @@ origin: { // old-style CSN

optional: [
'requires', 'definitions', 'extensions', 'i18n',
'requires', 'definitions', 'vocabularies', 'extensions', 'i18n',
'namespace', 'version', 'messages', 'meta', 'options', '@', '$location',

@@ -655,4 +691,10 @@ ],

/** @type {(id, location, home, params?: object, severity?: any, texts?: any ) => any} */
/** @type {(id, location, textOrArguments, texts?) => void} */
let message = () => undefined;
/** @type {(id, location, textOrArguments, texts?) => void} */
let error = () => undefined;
/** @type {(id, location, textOrArguments, texts?) => void} */
let warning = () => undefined;
/** @type {(id, location, textOrArguments, texts?) => void} */
let info = () => undefined;

@@ -662,3 +704,3 @@ let csnVersionZero = false;

let virtualLine = 1;
/** @type {XSN.Location[]} */
/** @type {CSN.Location[]} */
let dollarLocations = [];

@@ -695,3 +737,3 @@

else
throw new Error( `Missing type specification for property "${ p }` );
throw new Error( `Missing type specification for property "${ p }"` );
}

@@ -732,8 +774,8 @@ }

if (minLength > val.length) {
message( 'syntax-csn-expected-length', location(true), null,
{ prop: spec.prop, n: minLength, '#': minLength === 1 ? 'one' : 'std' },
'Error', {
std: 'Expected array in $(PROP) to have at least $(N) items',
one: 'Expected array in $(PROP) to have at least one item',
} );
error( 'syntax-csn-expected-length', location(true),
{ prop: spec.prop, n: minLength, '#': minLength === 1 ? 'one' : 'std' },
{
std: 'Expected array in $(PROP) to have at least $(N) items',
one: 'Expected array in $(PROP) to have at least one item',
} );
}

@@ -764,8 +806,2 @@ if (val.length)

function cast( obj, spec, xsn, csn ) {
// XSN TODO: make sure that $inferred is enough
xsn[csn.cast.target ? 'redirected' : '_typeIsExplicit'] = true;
// embed all other properties, e.g. "type" and type properties
embed( obj, spec, xsn );
}
function extra( node, spec, xsn ) {

@@ -777,2 +813,11 @@ if (!xsn.$extra)

function eventualCast( obj, spec, xsn ) {
if (!obj.cast || spec.optional && !spec.optional.includes('cast'))
return xsn;
xsn.op = { val: 'cast', location: xsn.location };
const r = { location: xsn.location };
xsn.args = [ r ];
return r;
}
function object( obj, spec ) {

@@ -785,2 +830,4 @@ if (!isObject( obj, spec ))

const csnProps = Object.keys( obj );
const o = eventualCast( obj, spec, r ); // do s/th special for CAST
let relevantProps = 0;
if (csnProps.length) {

@@ -791,5 +838,9 @@ ++virtualLine;

const s = getSpec( spec, obj, p, xor, expected );
const val = s.type( obj[p], s, r, obj, p );
// TODO: count illegal properties with Error msg as relevant to avoid 2nd error
if (!functionsOfIrrelevantProps.includes( s.type ))
++relevantProps;
const v = (s.inValue) ? o : r;
const val = s.type( obj[p], s, v, obj, p );
if (val !== undefined)
r[p] = val;
v[p] = val;
++virtualLine;

@@ -801,9 +852,17 @@ }

// console.log(csnProps,JSON.stringify(spec))
if (!csnProps.length || csnProps.length === 1 && csnProps[0] === 'as') {
message( 'syntax-csn-required-subproperty', location(true), null,
{ prop: spec.msgProp, '#': csnProps.length ? 'as' : 'std' },
'Error', {
std: 'Object in $(PROP) must have at least one property',
as: 'Object in $(PROP) must have at least one property other than \'as\'',
} );
if (!relevantProps) {
error( 'syntax-csn-required-subproperty', location(true),
{
prop: spec.msgProp,
'#': (
// eslint-disable-next-line no-nested-ternary
!csnProps.length ? 'std'
: csnProps.length === 1 && csnProps[0] === 'as' ? 'as'
: 'relevant'),
},
{
std: 'Object in $(PROP) must have at least one property',
as: 'Object in $(PROP) must have at least one property other than \'as\'',
relevant: 'Object in $(PROP) must have at least one relevant property',
} );
}

@@ -821,4 +880,4 @@ }

if (!csnVersionZero) {
message( 'syntax-csn-zero-delete', location(true), null, { prop: spec.msgProp },
'Warning', 'Delete/inline CSN v0.1.0 property $(PROP)' );
warning( 'syntax-csn-zero-delete', location(true), { prop: spec.msgProp },
'Delete/inline CSN v0.1.0 property $(PROP)' );
}

@@ -867,3 +926,3 @@ string( o, spec );

// TODO the following 'if' (if necessary) should be part of the core compiler
if (prop === 'definitions') {
if (prop === 'definitions' || prop === 'vocabularies') { // as spec property
// xsn.name.path = name.split('.').map( id => ({ id, location: location() }) );

@@ -883,3 +942,8 @@ r.name = {

popLocation( def );
return r;
if (kind !== 'annotation' || prop === 'vocabularies')
return r;
if (!vocabInDefinitions)
vocabInDefinitions = Object.create(null);
vocabInDefinitions[name] = r; // deprecated: anno def in 'definitions'
return undefined;

@@ -899,4 +963,4 @@ function expected( p, s ) {

if (!dict || typeof dict !== 'object' || Array.isArray( dict )) {
message( 'syntax-csn-expected-object', location(true), null,
{ prop: spec.prop }, 'Error' ); // spec.prop, not spec.msgProp!
error( 'syntax-csn-expected-object', location(true),
{ prop: spec.prop }); // spec.prop, not spec.msgProp!
return ignore( dict );

@@ -911,4 +975,4 @@ }

if (!name) {
message( 'syntax-csn-empty-name', location(true), null,
{ prop: spec.prop }, 'Warning', // TODO: Error
warning( 'syntax-csn-empty-name', location(true),
{ prop: spec.prop }, // TODO: Error
'Property names in dictionary $(PROP) must not be empty' );

@@ -934,3 +998,3 @@ }

// definer will complain about repeated names
addToDict( r, name, definition( def, spec, r, array, name ) );
dictAdd( r, name, definition( def, spec, r, array, name ) );
++virtualLine;

@@ -967,8 +1031,16 @@ }

if (val === 'view' && xsn.kind === 'entity') {
message( 'syntax-csn-zero-value', location(true), null, { prop: spec.msgProp },
'Warning', 'Replace CSN v0.1.0 value in $(PROP) by something specified' );
warning( 'syntax-csn-zero-value', location(true), { prop: spec.msgProp },
'Replace CSN v0.1.0 value in $(PROP) by something specified' );
}
else if ((val === 'entity' || val === 'type') && xsn.kind === 'aspect') {
info( 'syntax-csn-aspect', location(true), { kind: 'aspect', '#': val },
{
std: 'Use the dedicated kind $(KIND) for aspect definitions',
// eslint-disable-next-line max-len
entity: 'Abstract entity definitions are deprecated; use aspect definitions (having kind $(KIND)) instead',
} );
}
else {
message( 'syntax-csn-expected-valid', location(true), null, { prop: spec.msgProp },
'Error', 'Expected valid string for property $(PROP)' );
error( 'syntax-csn-expected-valid', location(true), { prop: spec.msgProp },
'Expected valid string for property $(PROP)' );
}

@@ -1014,4 +1086,4 @@ return ignore( val );

if (!path.every( id => id)) {
message( 'syntax-csn-expected-name', location(true), null, { prop: spec.msgProp },
'Warning', 'Expected correct name for property $(PROP)' );
warning( 'syntax-csn-expected-name', location(true), { prop: spec.msgProp },
'Expected correct name for property $(PROP)' );
}

@@ -1026,4 +1098,4 @@ xsn.path = path.map( id => ({ id, location: location() }) );

return { val, location: location() };
message( 'syntax-csn-expected-boolean', location(true), null, { prop: spec.msgProp },
'Warning', 'Expected boolean or null for property $(PROP)' );
warning( 'syntax-csn-expected-boolean', location(true), { prop: spec.msgProp },
'Expected boolean or null for property $(PROP)' );
ignore( val );

@@ -1037,4 +1109,4 @@ return { val: !!val, location: location() };

return val;
message( 'syntax-csn-expected-string', location(true), null, { prop: spec.msgProp },
'Error', 'Expected non-empty string for property $(PROP)' );
error( 'syntax-csn-expected-string', location(true), { prop: spec.msgProp },
'Expected non-empty string for property $(PROP)' );
return ignore( val );

@@ -1047,4 +1119,4 @@ }

return { val, literal: 'string', location: location() };
message( 'syntax-csn-expected-string', location(true), null, { prop: spec.msgProp },
'Error', 'Expected non-empty string for property $(PROP)' );
error( 'syntax-csn-expected-string', location(true), { prop: spec.msgProp },
'Expected non-empty string for property $(PROP)' );
return ignore( val );

@@ -1057,4 +1129,4 @@ }

return { val, literal: 'number', location: location() };
message( spec.msgId || 'syntax-csn-expected-natnum', location(true), null,
{ prop: spec.msgProp }, 'Error' );
error( spec.msgId || 'syntax-csn-expected-natnum', location(true),
{ prop: spec.msgProp } );
return ignore( val );

@@ -1074,3 +1146,3 @@ }

xsn.literal = 'enum'; // CSN cannot have both '#' and 'literal'
xsn.symbol = { id, location: location() };
xsn.sym = { id, location: location() };
}

@@ -1095,3 +1167,3 @@

return {
symbol: { id: val['#'], location: location() },
sym: { id: val['#'], location: location() },
literal: 'enum',

@@ -1136,4 +1208,4 @@ location: location(),

return val;
message( 'syntax-csn-expected-scalar', location(true), null, { prop: spec.msgProp },
'Error', 'Only scalar values are supported for property $(PROP)' );
error( 'syntax-csn-expected-scalar', location(true), { prop: spec.msgProp },
'Only scalar values are supported for property $(PROP)' );
return ignore( val );

@@ -1148,5 +1220,5 @@ }

if (typeof val === 'string' && validLiteralsExtra[val] === type)
return (val === 'x' ? 'hex' : val); // XSN TODO: 'hex'->'x'
message( 'syntax-csn-expected-valid', location(true), null, { prop: spec.msgProp },
'Error', 'Expected valid string for property $(PROP)' );
return val;
error( 'syntax-csn-expected-valid', location(true), { prop: spec.msgProp },
'Expected valid string for property $(PROP)' );
return ignore( val );

@@ -1167,3 +1239,20 @@ }

function args( exprs, spec, xsn ) {
function list( exprs, spec, xsn ) {
xsn.op = { val: ',', location: location() };
xsn.args = arrayOf( exprOrString )( exprs, spec, xsn );
}
function xprInValue( exprs, spec, xsn ) {
// if the top-level xpr is just for a cast:
if (exprs.length === 1 && exprs[0].cast) {
const x = {};
xpr( exprs, spec, x );
Object.assign( xsn, x.args[0] );
}
else {
xpr( exprs, spec, xsn );
}
}
function args( exprs, spec ) {
if (Array.isArray( exprs )) {

@@ -1173,5 +1262,5 @@ return arrayOf( exprOrString )( exprs, spec );

else if (!exprs || typeof exprs !== 'object') {
message( 'syntax-csn-expected-args', location(true), null,
{ prop: spec.prop }, 'Error', // spec.prop, not spec.msgProp!
'Expected array or object for property $(PROP)' );
error( 'syntax-csn-expected-args', location(true),
{ prop: spec.prop }, // spec.prop, not spec.msgProp!
'Expected array or object for property $(PROP)' );
return ignore( exprs );

@@ -1190,4 +1279,3 @@ }

}
xsn.namedArgs = r; // XSN TODO: 'namedArgs'->'args'
return undefined;
return r;
}

@@ -1224,17 +1312,12 @@

function onCondition( cond, spec, xsn ) {
xsn.onCond = condition( cond, spec );
return undefined;
}
function vZeroValue( obj, spec, xsn ) {
if (xsn.value) {
// TODO: also "sign" xsn.value created by inValue to complain about both 'value' and 'ref' etc
message( 'syntax-csn-unexpected-property', location(true), null, { prop: spec.msgProp },
'Warning', 'Unexpected CSN property $(PROP)' );
warning( 'syntax-csn-unexpected-property', location(true), { prop: spec.msgProp },
'Unexpected CSN property $(PROP)' );
return undefined;
}
if (!csnVersionZero) {
message( 'syntax-csn-zero-delete', location(true), null, { prop: spec.msgProp },
'Warning', 'Delete/inline CSN v0.1.0 property $(PROP)' );
warning( 'syntax-csn-zero-delete', location(true), { prop: spec.msgProp },
'Delete/inline CSN v0.1.0 property $(PROP)' );
}

@@ -1247,15 +1330,19 @@ return expr( obj, spec );

function queryTerm( term, spec, xsn ) { // for CSN properties 'SELECT' and 'SET'
if (!isObject( term, spec ))
// TODO: re-check $location: pushLocation( term ) / popLocation( term )
xsn.query = object( term, spec );
if (!xsn.query)
return;
pushLocation( term );
if (!xsn.op)
xsn.op = { val: spec.xsnOp, location: location() };
Object.assign( xsn, object( term, spec ) );
popLocation( term );
// XSN TODO: remove op query and subquery?
if (!xsn.query.op) {
xsn.query.op = {
val: (spec.prop !== 'SET' ? 'SELECT' : '$query'),
location: location(), // XSN TODO: work without location everywhere
};
}
if (spec.prop !== 'SET' && !xsn.query.from)
xsn.query.from = null; // make it clear that SELECT is used with parse error
if (spec.prop === 'projection')
xsn.$syntax = 'projection';
}
function from( val, spec ) {
return [ object( val, spec ) ]; // XSN TODO: no array
}
function asQuantifier( quantifier, spec, xsn ) {

@@ -1274,24 +1361,25 @@ if (quantifier)

const id = string( ex, spec ) || '';
addToDict( r, id, { name: { id, location: location() }, location: location() },
// eslint-disable-next-line no-loop-func
( name, loc ) => {
message( 'duplicate-excluding', loc, null, { name },
'Error', 'Duplicate EXCLUDING for source element $(NAME)' );
} );
dictAdd( r, id, { name: { id, location: location() }, location: location() },
duplicateExcluding );
++virtualLine;
}
xsn.exclude = r; // XSN TODO: exclude -> excluding
xsn.excludingDict = r;
}
function setOp( val, spec, xsn, csn ) { // UNION, ...
// similar to string(), but without literal, and with csn.all hack
return string( val, spec ) &&
{ val: (csn.all && val === 'union' ? 'unionAll' : val), location: location() };
function duplicateExcluding( name, loc ) {
error( 'duplicate-excluding', loc, { name, keyword: 'excluding' },
'Duplicate $(NAME) in the $(KEYWORD) clause' );
}
function setOp( val, spec ) { // UNION, ...
// similar to string(), but without literal
return string( val, spec ) && { val, location: location() };
}
function join( val, spec, xsn ) {
if (!string( val, spec ))
return undefined;
xsn.op = { val: 'join', location: location() };
return val;
const loc = location();
xsn.op = { val: 'join', location: loc };
return { val, location: loc };
}

@@ -1301,26 +1389,10 @@

if (Array.isArray( val ) && val.length > 1 && !csn.op) {
message( 'syntax-csn-expected-property', location(true), null,
{ prop: 'args', otherprop: 'op' }, 'Warning',
warning( 'syntax-csn-expected-property', location(true),
{ prop: 'args', otherprop: 'op' },
'CSN property $(PROP) expects property $(OTHERPROP) to be specified' );
xsn.op = { val: 'union', location: location() };
}
return arrayOf( object )( val, spec );
return arrayOf( object )( val, spec ).map( q => q.query );
}
function orderBy( node, spec ) {
const val = expr( node, spec ); // TODO: allow number for select item number?
if (!val)
return undefined;
const r = { value: val, location: val.location };
if (val.sort) {
r.sort = val.sort;
delete val.sort; // XSN TODO
}
if (val.nulls) {
r.nulls = val.nulls;
delete val.nulls;
}
return r;
}
// i18n ------------------------------

@@ -1337,5 +1409,5 @@

return { val: keyVal, literal: 'string', location: location() };
message( 'syntax-csn-expected-translation', location(true), null,
{ prop: textKey, otherprop: spec.prop }, 'Error',
'Expected string for text key $(PROP) of language $(OTHERPROP)' );
error( 'syntax-csn-expected-translation', location(true),
{ prop: textKey, otherprop: spec.prop },
'Expected string for text key $(PROP) of language $(OTHERPROP)' );
return ignore( keyVal );

@@ -1352,4 +1424,4 @@ }

// TODO v2: Warning only with --sloppy
message( 'syntax-csn-unknown-property', location(true), null, { prop },
'Warning', 'Unknown CSN property $(PROP)' );
warning( 'syntax-csn-unknown-property', location(true), { prop },
'Unknown CSN property $(PROP)' );
}

@@ -1364,4 +1436,4 @@ else { // TODO v2: always (i.e. also with message) add to $extra

if (s.vZeroIgnore && s.vZeroIgnore === csn[prop]) { // for "op": "call"
message( 'syntax-csn-zero-delete', location(true), null, { prop },
'Warning', 'Delete/inline CSN v0.1.0 property $(PROP)' );
warning( 'syntax-csn-zero-delete', location(true), { prop },
'Delete/inline CSN v0.1.0 property $(PROP)' );
return { type: ignore };

@@ -1384,7 +1456,7 @@ }

: (parentSpec.msgProp ? 'std' : 'top');
message( 'syntax-csn-unexpected-property', location(true), null,
message( 'syntax-csn-unexpected-property', location(true),
{
prop, otherprop: parentSpec.msgProp, kind, '#': variant,
},
[ 'Error' ], {
{
std: 'CSN property $(PROP) is not expected in $(OTHERPROP)',

@@ -1419,3 +1491,7 @@ top: 'CSN property $(PROP) is not expected top-level',

inExtensions = null;
return (spec.validKinds.includes( kind )) ? kind : spec.defaultKind;
if (!spec.validKinds.includes( kind ))
return spec.defaultKind;
return (def.abstract || def.$syntax === 'aspect')
? 'aspect' // deprecated abstract entity or kind:type for aspects
: kind;
}

@@ -1440,16 +1516,16 @@

if (prop) {
message( 'syntax-csn-dependent-property', location(true), null,
{ prop, otherprop: need }, 'Error',
'CSN property $(PROP) can only be used in combination with $(OTHERPROP)');
error( 'syntax-csn-dependent-property', location(true),
{ prop, otherprop: need },
'CSN property $(PROP) can only be used in combination with $(OTHERPROP)');
xor['no:req'] = prop;
}
else if (!xor['no:req']) {
message( 'syntax-csn-required-property', location(true), null,
{ prop: need, otherprop: spec.msgProp, '#': spec.prop },
'Error', { // TODO $(PARENT), TODO: do not use prop===0 hack
std: 'Object in $(OTHERPROP) must have the property $(PROP)',
columns: 'Object in $(OTHERPROP) must have an expression property like $(PROP)',
// eslint-disable-next-line max-len
extensions: 'Object in $(OTHERPROP) must have the property \'annotate\' or \'extend\'',
} );
error( 'syntax-csn-required-property', location(true),
{ prop: need, otherprop: spec.msgProp, '#': spec.prop },
{ // TODO $(PARENT), TODO: do not use prop===0 hack
std: 'Object in $(OTHERPROP) must have the property $(PROP)',
columns: 'Object in $(OTHERPROP) must have an expression property like $(PROP)',
// eslint-disable-next-line max-len
extensions: 'Object in $(OTHERPROP) must have the property \'annotate\' or \'extend\'',
} );
}

@@ -1466,5 +1542,5 @@ return spec;

}
message( 'syntax-csn-excluded-property', location(true), null,
{ prop, otherprop: xor[group] }, 'Error',
'CSN property $(PROP) can only be used alternatively to $(OTHERPROP)');
error( 'syntax-csn-excluded-property', location(true),
{ prop, otherprop: xor[group] },
'CSN property $(PROP) can only be used alternatively to $(OTHERPROP)');
return false;

@@ -1482,4 +1558,4 @@ }

return;
message( 'syntax-csn-zero-prop', location(true), null, { prop, otherprop },
'Warning', 'Replace CSN v0.1.0 property $(OTHERPROP) by $(PROP)' );
warning( 'syntax-csn-zero-prop', location(true), { prop, otherprop },
'Replace CSN v0.1.0 property $(OTHERPROP) by $(PROP)' );
}

@@ -1492,4 +1568,4 @@

return array;
message( 'syntax-csn-expected-array', location(true), null, { prop: spec.prop },
'Error', 'Expected array for property $(PROP)' );
error( 'syntax-csn-expected-array', location(true), { prop: spec.prop },
'Expected array for property $(PROP)' );
return ignore( array );

@@ -1501,4 +1577,4 @@ }

return obj;
message( spec.msgId || 'syntax-csn-expected-object', location(true), null,
{ prop: spec.msgProp }, 'Error' );
error( spec.msgId || 'syntax-csn-expected-object', location(true),
{ prop: spec.msgProp });
return ignore( obj );

@@ -1510,4 +1586,4 @@ }

if (!path.every( id => id)) {
message( 'syntax-csn-expected-name', location(true), null, { prop },
'Warning', 'Expected correct name for property $(PROP)' );
warning( 'syntax-csn-expected-name', location(true), { prop },
'Expected correct name for property $(PROP)' );
}

@@ -1519,14 +1595,17 @@ return { path: path.map( id => ({ id, location: location() }) ), location: location() };

if (!csnVersionZero && spec.vZeroFor == null) { // but 0 does not match!
message( 'syntax-csn-zero-value', location(true), null, { prop: spec.msgProp },
'Warning', 'Replace CSN v0.1.0 value in $(PROP) by something specified' );
warning( 'syntax-csn-zero-value', location(true), { prop: spec.msgProp },
'Replace CSN v0.1.0 value in $(PROP) by something specified' );
}
}
/**
* @param {boolean} [enforceJsonPos]
* @returns {CSN.Location}
*/
function location( enforceJsonPos ) {
return !enforceJsonPos && dollarLocations.length &&
dollarLocations[dollarLocations.length - 1] || {
filename: csnFilename,
start: { line: virtualLine, column: 0 },
end: { line: virtualLine, column: 0 }, // TODO: $location in XSN
$weak: true,
file: csnFilename,
line: virtualLine,
col: 0,
};

@@ -1547,4 +1626,3 @@ }

dollarLocations.push( null ); // must match with popLocation()
message( 'syntax-csn-expected-object', location(true), null, { prop: '$location' },
'Error' );
error( 'syntax-csn-expected-object', location(true), { prop: '$location' } );
}

@@ -1560,6 +1638,5 @@ // hidden feature: string $location

dollarLocations.push( {
filename: loc.substring( 0, m.index ),
start: { line, column },
end: { line, column },
$weak: true,
file: loc.substring( 0, m.index ),
line,
col: column,
} );

@@ -1581,3 +1658,3 @@ }

* @param {string} filename
* @param {CSN.Options} [options]
* @param {CSN.Options} options
* @returns {object} Augmented CSN (a.k.a XSN)

@@ -1591,13 +1668,32 @@ */

inExtensions = null;
vocabInDefinitions = null;
const xsn = { $frontend: 'json' };
message = getMessageFunction( xsn, options );
const msgFcts = makeMessageFunction( xsn, options, 'parse' );
message = msgFcts.message;
error = msgFcts.error;
warning = msgFcts.warning;
info = msgFcts.info;
if (csnVersionZero) {
message( 'syntax-csn-zero-version', location(true), null, {},
'Warning', 'Parsing CSN version 0.1.0' );
warning( 'syntax-csn-zero-version', location(true),
'Parsing CSN version 0.1.0' );
}
const r = object( csn, topLevelSpec );
delete r.location; // TODO XSN: use location instead extra filename XSN prop
if (vocabInDefinitions)
attachVocabInDefinitions( r );
return Object.assign( xsn, r );
}
// For v1 CSNs with annotation definitions
function attachVocabInDefinitions( csn ) {
if (!csn.vocabularies) {
csn.vocabularies = vocabInDefinitions;
}
else {
for (const name in vocabInDefinitions)
dictAdd( csn.vocabularies, name, vocabInDefinitions[name] );
}
}
function parse( source, filename = 'csn.json', options = {} ) {

@@ -1611,4 +1707,2 @@ try {

const xsn = {};
// eslint-disable-next-line no-shadow
const message = getMessageFunction( xsn, options );
const msg = e.message;

@@ -1630,8 +1724,10 @@ const p = /in JSON at position ([0-9]+)/.exec( msg );

}
/** @type {XSN.Location} */
/** @type {CSN.Location} */
const loc = {
filename, start: { line, column }, end: { line, column }, $weak: true,
file: filename,
line,
col: column,
};
message( 'syntax-csn-illegal-json', loc, null, { msg },
'Error', 'Illegal JSON: $(MSG)' );
const msgs = makeMessageFunction( xsn, options, 'parse' );
msgs.error( 'syntax-csn-illegal-json', loc, { msg }, 'Illegal JSON: $(MSG)' );
return xsn;

@@ -1638,0 +1734,0 @@ }

@@ -1,12 +0,20 @@

// Transform augmented CSN into compact "official" CSN
// Transform XSN (augmented CSN) into CSN
// The transformation works as follows: we transform a value in the XSN
// according to the following rules:
//
// - if it is a non-object, return it directly
// - if it is an array, return it with all items transformed recursively
// - if it is another object, return it with all property values transformed
// according to function `transformers.<prop>` or (if it does not exist)
// recursively to the rule; properties with value `undefined` are deleted
'use strict';
const { queryOps } = require('../base/model');
const { locationString } = require('../base/messages');
const { forEachGeneric } = require('../base/model');
const { isDeprecatedEnabled } = require('../base/model');
const compilerVersion = require('../../package.json').version;
const creator = `CDS Compiler v${ compilerVersion }`;
const csnVersion = '1.0';
const csnVersion = '2.0';

@@ -16,3 +24,6 @@ /** @type {boolean|string} */

let strictMode = false; // whether to dump with unknown properties (in standard)
let parensAsStrings = false;
let projectionAsQuery = false;
let withLocations = false;
let dictionaryPrototype = null;

@@ -22,3 +33,3 @@ // IMPORTANT: the order of these properties determine the order of properties

const transformers = {
// early and modifiers (without null / not null) -------------------------------------
// early and modifiers (without null / not null) ---------------------------
kind,

@@ -28,4 +39,2 @@ id: n => n, // in path item

'@': value,
abstract: value,
dbType: value, // TODO: currently with --hana-flavor only
virtual: value,

@@ -37,4 +46,4 @@ key: value,

// early expression / query properties -------------------------------------
op: o => ((o.val !== 'query') ? o.val : undefined),
from: fromOld, // before elements! XSN TODO just one (cross if necessary)
op: o => ((o.val !== 'SELECT' && o.val !== '$query') ? o.val : undefined),
from, // before elements!
// join done in from()

@@ -58,3 +67,3 @@ // func // in expression()

enum: insertOrderDict,
items: standard,
items,
includes: arrayOf( artifactRef ), // also entities

@@ -64,3 +73,3 @@ // late expressions / query properties -------------------------------------

columns,
exclude: renameTo( 'excluding', Object.keys ), // XSN TODO: exclude->excluding
excludingDict: renameTo( 'excluding', Object.keys ),
groupBy: arrayOf( expression ),

@@ -71,8 +80,8 @@ where: condition, // also pathItem after 'cardinality' before 'args'

orderBy: arrayOf( orderBy ), // TODO XSN: make `sort` and `nulls` sibling properties
limit, // TODO XSN: include offset
offset: ignore, // TODO XSN: move into `limit` - see limit
namedArgs: renameTo( 'args', args ), // XSN TODO - use args
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
sort: value,
nulls: value,
limit: standard,
rows: expression,
offset: expression,
on: c => ((gensrcFlavor && c.$inferred) ? undefined : condition(c)),
// definitions, extensions, members ----------------------------------------

@@ -86,13 +95,13 @@ returns: standard, // storing the return type of actions

elements,
sequenceOptions: ignore, // TODO: currently not in the JSON by HANA
actions: nonEmptyDict,
technicalConfig, // TODO: spec, re-check
actions: nonEmptyDict, // TODO: just normal dictionary
// special: top-level, cardinality -----------------------------------------
sources: ignore,
sources,
definitions: sortedDict,
vocabularies: sortedDict,
extensions, // is array
messages, // consider compactQuery / compactExpr
i18n,
messages: ignore,
options: ignore,
sourceMin: renameTo( 'srcmin', value ),
sourceMax: renameTo( 'src', value ), // TODO XSN: rename?
sourceMax: renameTo( 'src', value ),
targetMin: renameTo( 'min', value ),

@@ -102,9 +111,9 @@ targetMax: renameTo( 'max', value ),

name: ignore, // as is provided extra (for select items, in FROM)
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, // 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
$syntax: dollarSyntax,
// location is not renamed to $location as the name is well established in
// XSN and too many places (also outside the compiler) had to be adapted
location, // non-enumerable $location in CSN
$a2j: (e, csn) => { // on artifact level
Object.assign( csn, e );
},
$extra: (e, csn) => {

@@ -115,18 +124,9 @@ Object.assign( csn, e );

artifacts: ignore, // well-introduced, hence not $artifacts
annotationAssignments: ignore, // FIXME: make it $annotations
blocks: ignore, // FIXME: make it $blocks
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, // XSN TODO: $inferred on each fk instead
indexNo: ignore, // TODO XSN: remove
origin: ignore, // TODO remove (introduce non-enum _origin link)
projection: ignore, // TODO remove
redirected: ignore, // TODO remove: no need for this
source: ignore, // TODO remove
viaAll: ignore, // TODO remove, later in elem: $inferred: '*'
// $inferred is not renamed to $generated (likely name of a future CSN
// property) as too many places (also outside the compiler) had to be adapted
$: ignore,
// '_' not here, as non-enumerable properties are not transformed anyway
_typeIsExplicit: ignore,
expectedKind: ignore, // TODO: may be set in extensions but is unused

@@ -138,2 +138,3 @@ };

const csnPropertyNames = {
virtual: [ 'abstract' ], // abstract is compiler v1 CSN property
kind: [ 'annotate', 'extend' ],

@@ -147,6 +148,7 @@ op: [ 'join', 'func', 'xpr' ], // TODO: 'func','xpr' into 'quantifier'? TODO: 'global'(scope)?

foreignKeys: [ 'keys' ],
exclude: [ 'excluding' ],
excludingDict: [ 'excluding' ],
limit: [ 'rows' ], // 'offset',
query: [ 'projection' ],
elements: [ '$elements' ], // $elements for --enrich-csn
sources: [ 'namespace' ],
sources: [ 'namespace', '$sources' ],
sourceMin: [ 'srcmin' ],

@@ -157,5 +159,4 @@ sourceMax: [ 'src' ],

name: [ 'as', 'cast' ],
generatedFieldName: [ '$generatedFieldName' ],
location: [ '$env', '$location' ], // --enrich-csn
_typeIsExplicit: [
expectedKind: [
'_type', '_targetAspect', '_target', '_includes', '_links', '_art', '_scope',

@@ -180,3 +181,3 @@ ], // --enrich-csn

'type', 'length', 'precision', 'scale', 'srid', 'localized',
'foreignKeys', 'onCond', // for explicit ON/keys with REDIRECTED
'foreignKeys', 'on', // for explicit ON/keys with REDIRECTED
];

@@ -202,11 +203,19 @@

];
const csnDirectValues = [ 'val', 'messages' ]; // + all starting with '@'
const csnDirectValues = [ 'val' ]; // + all starting with '@' - TODO: still relevant
// Sort property names of CSN according to sequence which is also used by the compactModel function
// Only intended to be used for tests, as no non-enumerable properties are kept.
// Only returns enumerable properties, except for certain hidden properties if requested:
// $location, $env, elements.
function sortCsn( csn, keepHidden = false ) {
/**
* 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.
* Only returns enumerable properties, except for certain hidden properties if requested:
* $location, $env, elements.
*
* @param {object} csn
* @param {CSN.Options|false} cloneOptions
*/
function sortCsn( csn, cloneOptions = false ) {
if (typeof cloneOptions === 'object')
initModuleVars( cloneOptions );
if (csn instanceof Array)
return csn.map( v => (!v || typeof v !== 'object' ? v : sortCsn(v, keepHidden) ) );
return csn.map( v => (!v || typeof v !== 'object' ? v : sortCsn(v, cloneOptions) ) );
const r = {};

@@ -220,8 +229,10 @@ for (const n of Object.keys(csn).sort( compareProperties ) ) {

// Array check for property `args` which may either be a dictionary or an array.
r[n] = csnDictionary( val, n === 'definitions', keepHidden );
r[n] = csnDictionary( val, n === 'definitions', cloneOptions );
else
r[n] = sortCsn(val, keepHidden);
r[n] = sortCsn(val, cloneOptions);
}
if (keepHidden && typeof csn === 'object') {
if (cloneOptions && typeof csn === 'object') {
if (csn.$sources && !r.$sources)
setHidden(r, '$sources', csn.$sources);
if (csn.$location && !r.$location)

@@ -236,3 +247,5 @@ setHidden(r, '$location', csn.$location);

if (csn.elements && !r.elements) // non-enumerable 'elements'
setHidden(r, 'elements', csnDictionary( csn.elements, false, keepHidden ) );
setHidden(r, 'elements', csnDictionary( csn.elements, false, cloneOptions ) );
if (csn.$tableConstraints && !r.$tableConstraints)
setHidden(r, '$tableConstraints', csn.$tableConstraints);
}

@@ -242,8 +255,14 @@ return r;

function csnDictionary( csn, sort, keepHidden = false ) {
function csnDictionary( csn, sort, cloneOptions = false ) {
if (!csn || csn instanceof Array) // null or strange CSN
return csn;
const r = Object.create(null);
const proto = cloneOptions && (typeof cloneOptions === 'object') &&
cloneOptions.dictionaryPrototype;
// eslint-disable-next-line no-nested-ternary
const dictProto = (typeof proto === 'object') // including null
? proto
: (proto) ? Object.prototype : null;
const r = Object.create( dictProto );
for (const n of (sort) ? Object.keys(csn).sort() : Object.keys(csn))
r[n] = sortCsn( csn[n], keepHidden );
r[n] = sortCsn( csn[n], cloneOptions );

@@ -261,9 +280,7 @@ return r;

function compactModel( model, options = model.options || {} ) {
gensrcFlavor = options.parseCdl || options.toCsn && options.toCsn.flavor === 'gensrc';
strictMode = options.testMode;
withLocations = options.withLocations;
initModuleVars( options );
const csn = {};
const sources = model.sources || Object.create(null);
if (options.parseCdl) {
const using = usings( sources );
const srcDict = model.sources || Object.create( null ); // not dictionaryPrototype!
if (options.parseCdl) { // TODO: make it a csnFlavor?
const using = usings( srcDict );
if (using.length)

@@ -273,4 +290,4 @@ csn.requires = using;

// 'namespace' for complete model is 'namespace' of first source
for (const first in sources) {
const { namespace } = sources[first];
for (const first in srcDict) {
const { namespace } = srcDict[first];
if (namespace && namespace.path)

@@ -281,15 +298,20 @@ csn.namespace = namespace.path.map( i => i.id ).join('.');

set( 'definitions', csn, model );
set( 'vocabularies', csn, model );
const exts = extensions( model.extensions || [], csn, model );
if (exts.length)
if (exts && exts.length)
csn.extensions = exts;
if (model.i18n)
csn.i18n = i18n( model.i18n );
set( 'messages', csn, model );
const [ src ] = Object.keys( model.sources );
const file = src && model.sources[src].filename;
if (file) {
Object.defineProperty( csn, '$location', {
value: { file }, configurable: true, writable: true, enumerable: withLocations,
} );
set( 'i18n', csn, model );
set( 'sources', csn, model );
// Set $location, use $extra properties of first source as resulting $extra properties
for (const first in srcDict) {
const loc = srcDict[first].location;
if (loc && loc.file) {
Object.defineProperty( csn, '$location', {
value: { file: loc.file }, configurable: true, writable: true, enumerable: withLocations,
} );
}
set( '$extra', csn, srcDict[first] );
break;
}
if (!options.testMode) {

@@ -299,7 +321,2 @@ csn.meta = Object.assign( {}, model.meta, { creator } );

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

@@ -323,6 +340,6 @@ }

*
* @param {object} sources Dictionary of source files to their AST/XSN.
* @param {object} srcDict Dictionary of source files to their AST/XSN.
*/
function usings( sources ) {
const sourceNames = Object.keys(sources);
function usings( srcDict ) {
const sourceNames = Object.keys(srcDict);
if (sourceNames.length === 0)

@@ -332,3 +349,3 @@ return [];

// Take the first file as parseCdl should only receive one file.
const source = sources[sourceNames[0]];
const source = srcDict[sourceNames[0]];
const requires = [];

@@ -348,7 +365,8 @@ if (source && source.dependencies)

function extensions( node, csn, model ) {
const exts = node.map( standard ).sort(
(a, b) => (a.annotate || a.extend).localeCompare( b.annotate || b.extend )
);
if (model.kind && model.kind !== 'source')
return undefined;
const exts = node.map( standard );
for (const name of Object.keys( model.definitions || {} ).sort()) {
// builtins are non-enumerable for smaller display
for (const name of Object.getOwnPropertyNames( model.definitions || {} ).sort()) {
const art = model.definitions[name];

@@ -360,6 +378,4 @@

// In parseCdl mode extensions were already put into "extensions".
if (!model.options.parseCdl && art.kind === 'namespace') {
if (!model.options.parseCdl && (art.kind === 'namespace' || art.builtin)) {
extractAnnotationsToExtension( art );
if (art.builtin === 'reserved')
forEachGeneric( art, 'artifacts', extractAnnotationsToExtension);
}

@@ -382,3 +398,5 @@ else if (gensrcFlavor) {

return exts;
return exts.sort(
(a, b) => (a.annotate || a.extend).localeCompare( b.annotate || b.extend )
);

@@ -418,7 +436,7 @@ // extract namespace/builtin annotations

function i18n( i18nNode ) {
const csn = Object.create( null );
const csn = Object.create( dictionaryPrototype );
for (const langKey in i18nNode) {
const langDict = i18nNode[langKey];
if (!csn[langKey])
csn[langKey] = Object.create( null );
csn[langKey] = Object.create( dictionaryPrototype );
for (const textKey in langDict)

@@ -430,4 +448,16 @@ csn[langKey][textKey] = langDict[textKey].val;

function sources( srcDict, csn ) {
// TODO: sort according to some layering order, see #6368
const names = Object.keys( srcDict);
setHidden( csn, '$sources', (!strictMode) ? names : names.map( relativeName ) );
return undefined;
function relativeName( name ) {
const loc = srcDict[name].location;
return loc && loc.file || name;
}
}
function inferred( elems, inferredParent ) {
const ext = Object.create(null);
const ext = Object.create( dictionaryPrototype );
for (const name in elems) {

@@ -465,3 +495,3 @@ const elem = elems[name];

const loc = val && val.location || node.location;
throw new Error( `Unexpected property ${ prop } in ${ locationString(loc) }`);
throw new Error( `Unexpected property ${ prop } in ${ locationString(loc) }` );
}

@@ -498,4 +528,14 @@ // otherwise, just ignore the unexpected property

function items( obj, csn, node ) {
if (node.$expand === 'origin' && node.type && node.kind !== 'type')
// no 'elements' with SELECT or inferred elements with gensrc;
// hidden 'elements' will be set in query()
return undefined;
return standard( obj );
}
function elements( dict, csn, node ) {
if (csn.from || gensrcFlavor && (node.query || node.type))
if (csn.from ||
gensrcFlavor && (node.query || node.type) ||
node.$expand === 'origin' && node.type && node.kind !== 'type')
// no 'elements' with SELECT or inferred elements with gensrc;

@@ -516,5 +556,5 @@ // hidden 'elements' will be set in query()

const val = node[prop];
// val.priority isn't set for computed annotations like @Core.Computed
// val.$priority isn't set for computed annotations like @Core.Computed
// and @odata.containment.ignore
if (val.priority && (val.priority !== 'define') === annotated) {
if (val.$priority && (val.$priority !== 'define') === annotated) {
// transformer (= value) takes care to exclude $inferred annotation assignments

@@ -531,16 +571,41 @@ const sub = transformer( val );

function ignore() {}
const specialDollarValues = {
':': undefined,
udf: 'udf',
calcview: 'calcview',
};
function messages( val, csn ) {
if (val && val.length )
setHidden( csn, 'messages', val );
function dollarSyntax( node, csn ) {
// eslint-disable-next-line no-prototype-builtins
if (specialDollarValues.hasOwnProperty( node ))
return specialDollarValues[node];
if (projectionAsQuery)
return node;
setHidden( csn, '$syntax', node );
return undefined;
}
function ignore() {}
function location( loc, csn, xsn ) {
if (xsn.kind && xsn.kind.charAt(0) !== '$' && xsn.kind !== 'query' &&
(!xsn.$inferred || !xsn._main)) {
if (xsn.kind && xsn.kind.charAt(0) !== '$' && xsn.kind !== 'select' &&
(!xsn.$inferred || !xsn._main)) { // TODO: also for 'select'
// Also include $location for elements in queries (if not via '*')
const l = xsn.name && xsn.name.location || loc;
// csn.$location
const val = { file: l.filename, line: l.start.line, col: l.start.column };
addLocation( xsn.name && xsn.name.location || loc, csn );
}
}
/**
* Adds the given location to the CSN.
*
* @param {CSN.Location} loc
* @param {object} csn
*/
function addLocation( loc, csn ) {
if (loc) {
// Remove endLine/endCol:
// Reasoning: $location is mostly attached to definitions/members but the name
// is often not the reason for an error or warning. So we gain little benefit for
// two more properties.
const val = { file: loc.file, line: loc.line, col: loc.col };
Object.defineProperty( csn, '$location', {

@@ -550,8 +615,2 @@ value: val, configurable: true, writable: true, enumerable: withLocations,

}
}
// Add location to SELECT
function addLocation( loc, csn ) {
if (loc)
location( loc, csn, { kind: 'entity' } );
return csn;

@@ -567,3 +626,4 @@ }

const keys = Object.keys( dict );
keys.sort();
if (strictMode)
keys.sort();
return dictionary( dict, keys );

@@ -580,3 +640,3 @@ }

function dictionary( dict, keys ) {
const csn = Object.create(null);
const csn = Object.create( dictionaryPrototype );
for (const name of keys) {

@@ -610,3 +670,2 @@ const def = definition( dict[name] );

addLocation( art.targetElement.location, key );
set( 'generatedFieldName', key, art );
return extra( key, art );

@@ -624,5 +683,6 @@ }

}
if (k === 'view') // XSN TODO: kind: 'entity'
return 'entity';
if ([ 'element', 'key', 'param', 'enum', 'annotate', 'query', '$tableAlias' ].includes(k))
if ([
'element', 'key', 'param', 'enum', 'annotate', 'select', '$join',
'$tableAlias', 'annotation', 'mixin',
].includes(k))
return undefined;

@@ -641,2 +701,4 @@ return k;

return undefined; // TODO: complain with strict
else if (!path.length)
return [];

@@ -663,3 +725,3 @@ const link = path[0]._artifact; // XSN TODO: store double definitions differently

// a fake element with just a correct absolute name and _parent/_main links.
if (!root._main || root.kind === 'query') { // $self/$projection
if (!root._main || root.kind === 'select') { // $self/$projection
// in query, only correct for leading query ->

@@ -693,3 +755,3 @@ // TODO: forbid TYPE OF elem / TYPE OF $self.elem in queries

// try to find ':' position syntactically for FROM
scope = !terse && path.findIndex( i => i.where || i.namedArgs || i.cardinality) + 1 ||
scope = !terse && path.findIndex( i => i.where || i.args || i.cardinality) + 1 ||
path.length;

@@ -713,3 +775,2 @@ }

if (!item.args &&
!item.namedArgs &&
!item.where &&

@@ -726,5 +787,5 @@ !item.cardinality &&

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

@@ -739,6 +800,8 @@ }

return undefined;
if (node.path)
return extra( { '=': node.path.map( id => id.id ).join('.') }, node );
if (node.path) {
const ref = node.path.map( id => id.id ).join('.');
return extra( { '=': node.variant ? `${ ref }#${ node.variant.id }` : ref }, node );
}
if (node.literal === 'enum')
return extra( { '#': node.symbol.id }, node );
return extra( { '#': node.sym.id }, node );
if (node.literal === 'array')

@@ -749,3 +812,3 @@ return node.val.map( value );

return node.name && !('val' in node) || node.val;
const r = Object.create( null );
const r = Object.create( dictionaryPrototype );
for (const prop in node.struct)

@@ -760,3 +823,3 @@ r[prop] = value( node.struct[prop] );

if (node.kind === 'enum' || node._parent && node._parent.kind === 'extend')
Object.assign( csn, expression(v) );
Object.assign( csn, expression( v, true ) );
}

@@ -766,10 +829,5 @@

const expr = expression( node );
return expr.xpr || [ expr ];
return !expr.cast && expr.xpr || [ expr ];
}
// TODO: calculate from compiler/builtins.js (more with HANA?):
const magicFunctions = [
'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

@@ -780,6 +838,6 @@

const nav = path[0]._navigation;
if (nav && !nav.self && nav.name.query != null) {
if (nav && nav.kind !== '$self' && nav.name.select != null) {
setHidden( ref, '$env', (nav.kind === '$navElement')
? nav.name.alias
: nav.name.query + 1 );
: nav.name.select );
}

@@ -792,4 +850,4 @@ else if ( path[0]._artifact && path[0]._artifact.query ) {

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

@@ -799,42 +857,23 @@ return node;

return {};
if (node instanceof Array) {
const exprs = node.map( condition );
const rest = exprs.slice(1).map( a => [ ',', ...a ] );
return { xpr: [ '(' ].concat( exprs[0], ...rest, [ ')' ] ) };
}
if (node.scope === 'param') {
if (node.path)
return extra( typeCast({ ref: node.path.map( pathItem ), param: true }, en), en );
return extra( typeCast({ ref: [ node.param.val ], param: true }, en), en );
return extra( { ref: node.path.map( pathItem ), param: true }, dollarExtraNode );
return { ref: [ node.param.val ], param: true }; // CDL rule for runtimes
}
if (node.path) {
// TODO: global
if (node.path.length !== 1)
return extra( typeCast( pathRef( node.path ), en ), en );
const item = pathItem( node.path[0] );
if (typeof item === 'string' && !node.path[0].quoted &&
// TODO: use _artifact if available
magicFunctions.includes( item.toUpperCase() ))
return extra( typeCast( { func: item }, en ), en );
return extra( typeCast( pathRef( node.path ), en), en );
return extra( pathRef( node.path ), dollarExtraNode );
}
if (node.literal) {
if (typeof node.val === node.literal || node.val === null)
return extra(typeCast( { val: node.val }, en ), en );
return extra( { val: node.val }, dollarExtraNode );
else if (node.literal === 'enum')
return extra(typeCast( { '#': node.symbol.id }, en ), en );
return extra( { '#': node.sym.id }, dollarExtraNode );
else if (node.literal === 'token')
return node.val; // * in COUNT(*)
return extra(
typeCast( { val: node.val, literal: (node.literal === 'hex') ? 'x' : node.literal }, en ),
en
);
return extra( { val: node.val, literal: node.literal }, dollarExtraNode );
}
if (node.func) { // TODO XSN: remove op: 'call', func is no path
const call = { func: node.func.path[0].id };
if (node.namedArgs) {
call.args = args( node.namedArgs );
}
else if (node.args) { // no args from CSN input for CURRENT_DATE etc
if (node.args) { // no args from CSN input for CURRENT_DATE etc
call.args = args( node.args );

@@ -850,28 +889,33 @@ const arg0 = call.args[0];

}
return extra( call, en );
return extra( call, dollarExtraNode );
}
if (queryOps[node.op.val])
return query( node );
if (node.query)
return query( node.query );
if (!node.op) // parse error
return { xpr: [] };
else if (node.op.val === 'xpr')
// do not use xpr() for xpr, as it would flatten inner xpr's (semantically ok)
return extra( typeCast({ xpr: node.args.map( expression ) }, node ), node );
return typeCast({ xpr: xpr( node ) }, node);
return extra({ xpr: node.args.map( expression ) }, node );
else if (node.op.val === 'cast')
return cast( expression( node.args[0] ), node);
// from here on: CDL input (no $extra possible)
else if (node.op.val !== ',')
return { xpr: xpr( node ) };
return (parensAsStrings)
? { xpr: [ '(', ...xpr( node ), ')' ] }
: { list: node.args.map( expression ) };
}
function assignExpression( node, expr ) {
const e = expression( expr );
const r = Object.assign( node, e );
if (e.$env)
setHidden( r, '$env', e.$env );
if (e.elements) {
// possibly set hidden by backends
setHidden( r, 'elements', e.elements );
}
return r;
}
function xpr( node ) {
// if (!node.op) console.log(node)
const op = operators[node.op.val] || node.op.val.split(' ');
const exprs = node.args.map( condition );
const exprs = node.args.map( ( sub ) => {
const expr = expression( sub );
// return !sub.$parens && !expr.cast && expr.xpr || [ expr ]; if parensAsStrings is gone
if (expr.cast || !expr.xpr || sub.$parens && !parensAsStrings)
return [ expr ];
else if (sub.$parens && sub.op.val !== ',')
return [ '(', ...expr.xpr, ')' ];
return expr.xpr;
} );
if (op instanceof Function)

@@ -900,6 +944,11 @@ return op( exprs );

function query( node ) {
function query( node, csn, xsn ) {
while (node instanceof Array) // in parentheses -> remove
node = node[0];
if (node.op.val === 'query') {
if (node.op.val === 'SELECT') {
if (xsn && xsn.query === node && xsn.$syntax === 'projection' &&
node.from && node.from.path && !projectionAsQuery) {
csn.projection = standard( node );
return undefined;
}
const select = { SELECT: standard( node ) };

@@ -914,2 +963,3 @@ const elems = node.elements;

gensrcFlavor = false;
// TODO: or set inside SELECT?
setHidden( select, 'elements', insertOrderDict( elems ) );

@@ -923,31 +973,12 @@ }

}
const csn = {};
const union = {};
// for UNION, ... ----------------------------------------------------------
if (node.op.val !== 'subquery') {
if (node.op.val !== 'unionAll') { // XSN TODO: quantifier: 'all'|'distinct'
csn.op = node.op.val;
}
else {
csn.op = 'union';
csn.all = true;
}
}
if (node.args) {
let exprs = node.args;
// binary -> n-ary - TODO CDL: the while loop should be done in parser (toCdl is
// currently not prepared)
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 = exprs.map( query );
}
set( 'orderBy', csn, node );
set( 'limit', csn, node ); // TODO XSN: also offset
set( '$extra', csn, node );
return addLocation( node.location, { SET: csn } );
set( 'op', union, node );
set( 'quantifier', union, node );
// set( 'args', union, node ):
union.args = node.args.map( query );
set( 'orderBy', union, node );
set( 'limit', union, node );
set( '$extra', union, node );
return addLocation( node.location, { SET: union } );
}

@@ -972,24 +1003,9 @@

function fromOld( node ) {
// TODO: currently an array in XSN:
if (node.length > 1)
return from( { join: 'cross', args: node } );
return from( node[0] );
}
// XSN TODO: remove '…Outer'
const joinTrans = { leftOuter: 'left', rightOuter: 'right', fullOuter: 'full' };
function from( node ) {
while (node instanceof Array) // in parentheses
while (node instanceof Array) // TODO: old-style parentheses - keep tmp for A2J
node = node[0];
// TODO: CSN: FROM ((SELECT...)) as -> also add 'subquery' op? - Together
// with []-elimination in FROM... -> normal standard()
if (node.join) { // XSN TODO: remove '…Outer'
// binary (without additions) -> n-ary - the while loop should be done in
// parser (toCdl is currently not prepared)
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 };
if (node.join) {
const join = { join: node.join.val };
set( 'cardinality', join, node );

@@ -1000,4 +1016,4 @@ join.args = node.args.map( from );

}
else if (!node.path) {
return addExplicitAs( query( node ), node.name ); // $extra inside SELECT/SET
else if (node.query) {
return addExplicitAs( query( node.query ), node.name ); // $extra inside SELECT/SET
}

@@ -1015,5 +1031,4 @@ else if (!node._artifact || node._artifact._main) { // CQL or follow assoc

function addElementAsColumn( elem, cols ) {
if (elem.viaAll) // TODO: elem.$inferred (value '*')
if (elem.$inferred === '*')
return;
// TODO: 'priority' -> '$priority'
// only list annotations here which are provided directly with definition

@@ -1028,5 +1043,15 @@ const col = (gensrcFlavor) ? annotations( elem, false ) : {};

set( 'key', col, elem );
addExplicitAs( assignExpression( col, elem.value ),
const expr = expression( elem.value, true );
// console.log(Object.keys(expr))
addExplicitAs( Object.assign( col, (expr.cast ? { xpr: [ expr ] } : expr) ),
elem.name, neqPath( elem.value ) );
typeCast(col, elem);
// $env and elements (sub queries) in expr are hidden (not set via Object.assign):
if (!expr.cast) {
if (expr.$env)
setHidden( col, '$env', expr.$env );
if (expr.elements)
setHidden( col, 'elements', expr.elements );
}
if (elem.type && !elem.type.$inferred || elem.target && !elem.target.$inferred)
cast( col, elem );
}

@@ -1042,9 +1067,11 @@ finally {

if (elem.value && elem.value.location && !elem.$inferred)
location( elem.value.location, col, { kind: 'col' } );
if (elem.value && !elem.$inferred) {
const parens = elem.value.$parens;
addLocation( (parens ? parens[parens.length - 1] : elem.value.location), col );
}
cols.push( extra( col, elem ) );
}
function orderBy( node ) { // TODO XSN: flatten (no extra 'value'), part of expression
const expr = expression( node.value );
function orderBy( node ) {
const expr = expression( node, 'ignoreExtra' );
if (node.sort)

@@ -1054,12 +1081,5 @@ expr.sort = node.sort.val;

expr.nulls = node.nulls.val;
return extra( expr, node );
return extra( expr, node ); // extra properties after sort/nulls
}
function limit( expr, csn, node ) { // XSN TODO: use same structure, $extra
const rows = expression( expr );
return (node.offset)
? { rows, offset: expression( node.offset ) }
: { rows };
}
function extra( csn, node ) {

@@ -1071,9 +1091,11 @@ if (node && node.$extra)

function typeCast( csn, node ) {
if (node._typeIsExplicit || node.redirected) { // TODO: XSN: introduce $inferred
csn.cast = {}; // TODO: what about $extra in cast?
for (const prop of typeProperties)
set( prop, csn.cast, node );
}
return csn;
function cast( csn, node ) {
let r = csn;
if (csn.cast)
r = { xpr: [ csn ], cast: {} };
else
r.cast = {}; // TODO: what about $extra in cast?
for (const prop of typeProperties)
set( prop, r.cast, node );
return r;
}

@@ -1089,3 +1111,3 @@

if (name && name.id &&
(!name.calculated && !name.$inferred || !node.ref || implicit && implicit(name.id) ))
(!name.$inferred || !node.ref && !node.func || implicit && implicit(name.id) ))
node.as = name.id;

@@ -1096,3 +1118,3 @@ return node;

function neqPath( ref ) {
const path = ref && ref.path;
const path = ref && (ref.path || !ref.args && ref.func && ref.func.path);
return function test( id ) {

@@ -1113,5 +1135,3 @@ const last = path && path[path.length - 1];

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

@@ -1121,277 +1141,22 @@ }

function compactExpr( e ) { // TODO: options
gensrcFlavor = true;
strictMode = false;
withLocations = false;
return e && expression( e );
initModuleVars();
return e && expression( e, true );
}
// TODO: use style as in rest of this file: property transformators, coding style
// TODO: document CSN and XSN for technical configurations
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)
be.tableSuffix = [];
be.tableSuffix.push({ xpr: [ 'migration', value(tc.migration) ] });
}
if (tc.storeType)
be.storeType = value(tc.storeType);
if (tc.extendedStorage) {
if (!be.tableSuffix)
be.tableSuffix = [];
be.tableSuffix.push({ xpr: [ 'using', 'extended', 'storage' ] });
}
if (tc.group) {
if (!be.tableSuffix)
be.tableSuffix = [];
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)
be.tableSuffix = [];
be.tableSuffix.push({ xpr: [ 'unload', 'priority', expression(tc.unloadPrio) ] });
}
if (tc.autoMerge) {
if (!be.tableSuffix)
be.tableSuffix = [];
const autoMerge = { xpr: [] };
if (!tc.autoMerge.val)
autoMerge.xpr.push('no');
autoMerge.xpr.push('auto', 'merge');
be.tableSuffix.push(autoMerge);
}
if (tc.partition) {
if (!be.tableSuffix)
be.tableSuffix = [];
be.tableSuffix.push({ xpr: [ partition(tc.partition) ] });
}
if (tc.fzindexes) {
if (!be.fzindexes)
be.fzindexes = {};
tc.fzindexes.forEach((i) => {
i.columns.filter(c => !c._ignore).forEach((c) => {
const stream = [];
stream.push('fuzzy', 'search', 'index', 'on');
if (i.fuzzy) {
stream.push('fuzzy', 'search', 'mode');
if (i.fuzzy.mode)
stream.push(expression(i.fuzzy.mode));
}
const name = c.path.map(p => p.id).join('.');
if (be.fzindexes[name])
be.fzindexes[name].push(stream);
else
be.fzindexes[name] = [ stream ];
});
});
}
if (tc.indexes) {
be.indexes = {};
for (const idxName in tc.indexes) {
const idx = tc.indexes[idxName];
be.indexes[idxName] = (Array.isArray(idx)) ? idx.map(index) : index(idx);
}
}
return csn;
function index(idx) {
const stream = [];
if (idx.kind === 'index') {
if (idx.unique)
stream.push('unique');
stream.push('index', { ref: [ idx.name.id ] }, 'on', '(');
columns(idx.columns, stream);
stream.push(')');
if (idx.sort)
stream.push(value(idx.sort));
}
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) {
stream.push('language', 'column');
stream.push(expression(idx.language.column));
}
if (idx.language.detection) {
stream.push('language', 'detection', '(');
let i = 0;
idx.language.detection.forEach((v) => {
if (i > 0)
stream.push(',');
stream.push(expression(v));
i++;
});
stream.push(')');
}
}
if (idx.mimeTypeColumn)
stream.push('mime', 'type', 'column', expression(idx.mimeTypeColumn));
if (idx.fuzzySearchIndex)
stream.push('fuzzy', 'search', 'index', value(idx.fuzzySearchIndex));
if (idx.phraseIndexRatio)
stream.push('phrase', 'index', 'ratio', expression(idx.phraseIndexRatio));
if (idx.configuration)
stream.push('configuration', expression(idx.configuration));
if (idx.textAnalysis)
stream.push('text', 'analysis', value(idx.textAnalysis));
if (idx.searchOnly)
stream.push('search', 'only', value(idx.searchOnly));
if (idx.fastPreprocess)
stream.push('fast', 'preprocess', value(idx.fastPreprocess));
if (idx.mimeType)
stream.push('mime', 'type', expression(idx.mimeType));
if (idx.tokenSeparators)
stream.push('token', 'separators', expression(idx.tokenSeparators));
if (idx.textMining) {
if (idx.textMining.state)
stream.push('text', 'mining', value(idx.textMining.state));
if (idx.textMining.config)
stream.push('text', 'mining', 'configuration', expression(idx.textMining.config));
if (idx.textMining.overlay) {
stream.push('text', 'mining', 'configuration', 'overlay',
expression(idx.textMining.overlay));
}
}
if (idx.changeTracking) {
const ct = idx.changeTracking;
stream.push(value(ct.mode));
if (ct.asyncSpec) {
const asp = ct.asyncSpec;
stream.push('flush');
if (asp.queue)
stream.push(value(asp.queue));
if (asp.minutes) {
stream.push('every', expression(asp.minutes), 'minutes');
if (asp.documents)
stream.push('or');
}
if (asp.documents)
stream.push('after', expression(asp.documents), 'documents');
}
}
}
return stream;
}
function partition(p) {
const stream = [];
let i = 0;
p.specs.forEach((s) => {
if (i === 0)
stream.push('partition', 'by', ...s.scheme.val.split(' '));
else
stream.push(',', ...s.scheme.val.split(' '));
spec(s);
i++;
});
if (p.wpoac)
stream.push('with', 'partitioning', 'on', 'any', 'columns', value(p.wpoac));
return stream;
function spec(s) {
if (s.columns) {
stream.push('(');
columns(s.columns, stream);
stream.push(')');
}
if (s.partitions)
stream.push('partitions', value(s.partitions));
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)
stream.push(')');
stream.push('using', r.store, 'storage', '(');
}
delimiter = false;
oppStore = r.store;
}
if (delimiter)
stream.push(',');
stream.push('partition');
if (r.others)
stream.push('others');
if (r.min && !r.max)
stream.push('value', '=');
if (r.min)
stream.push(expression(r.min));
if (r.isCurrent)
stream.push('is', 'current');
if (r.min && r.max)
stream.push('<=', 'values', '<', expression(r.max));
delimiter = true;
});
if (s.withStorageSpec)
stream.push(')');
stream.push(')');
}
}
}
// eslint-disable-next-line no-shadow
function columns(arr, stream) {
let 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) ] });
else
stream.push(expression(c));
if (c.sort)
stream.push(value(c.sort));
i++;
});
}
function initModuleVars( options = { csnFlavor: 'gensrc' } ) {
gensrcFlavor = options.parseCdl || options.csnFlavor === 'gensrc' ||
options.toCsn && options.toCsn.flavor === 'gensrc';
strictMode = options.testMode;
const proto = options.dictionaryPrototype;
// eslint-disable-next-line no-nested-ternary
dictionaryPrototype = (typeof proto === 'object') // including null
? proto
: (proto) ? Object.prototype : null;
withLocations = options.withLocations;
parensAsStrings = isDeprecatedEnabled( options, 'parensAsStrings' );
projectionAsQuery = isDeprecatedEnabled( options, 'projectionAsQuery' );
}
module.exports = {
cloneCsnDictionary: csnDictionary,
compactModel,

@@ -1398,0 +1163,0 @@ compactQuery,

@@ -16,11 +16,2 @@ /**

/**
* Checks if the provided parameter is an array
* @param obj the variable to check
* @returns true if the parameter is an array otherwise it returns false
*/
function isArray(obj) {
return Array.isArray(obj);
}
/**
* Callback of the forEach function called for each node it walks

@@ -73,3 +64,3 @@ * @callback forEachCallback

let allKeys = Object.getOwnPropertyNames(objs); // consider non-enumerable properties also
let ignoreLength = isArray(objs);
let ignoreLength = Array.isArray(objs);
for(var key of allKeys) {

@@ -76,0 +67,0 @@ let visible = visibleKeys.includes(key);

@@ -0,1 +1,3 @@

// @ts-nocheck : Issues with Tokens on `this`, e.g. `this.DOT`.
// Wrapper around generated ANTLR parser

@@ -11,3 +13,3 @@

const { getMessageFunction, CompileMessage, DebugCompileMessage } = require('../base/messages');
const { makeMessageFunction, CompileMessage } = require('../base/messages');
const errorStrategy = require('./errorStrategy');

@@ -26,4 +28,4 @@

syntaxError( recognizer, offendingSymbol, line, column, msg, e ) {
if (!(e instanceof CompileMessage || e instanceof DebugCompileMessage)) // not already reported
recognizer.message( null, offendingSymbol, msg );
if (!(e instanceof CompileMessage)) // not already reported
recognizer.error( null, offendingSymbol, msg );
}

@@ -42,6 +44,15 @@ }

if (t.type === this.DOT) {
const n = super.LT(k + 1);
if (n && n.type === this.BRACE)
const n = super.LT(k + 1) || { type: this.Identifier };
// after a '.', there is no keyword -> no word is reserved:
if (n.type < this.Identifier && /^[a-z]+$/i.test( n.text ))
n.type = this.Identifier;
else if (n.type === this.BRACE)
t.type = this.DOTbeforeBRACE;
}
else if (t.type === this.AT) {
const n = super.LT(k + 1) || { type: this.Identifier };
// after a '@', there is no keyword -> no word is reserved:
if (n.type < this.Identifier && /^[a-z]+$/i.test( n.text ))
n.type = this.Identifier;
}
else if (t.type === this.NEW) {

@@ -77,2 +88,3 @@ const n = super.LT(k + 1);

ts.DOT = ts.DOTbeforeBRACE && ts.BRACE && tokenTypeOf( recognizer, "'.'" );
ts.AT = tokenTypeOf( recognizer, "'@'" );
ts.NEW = Parser.NEW;

@@ -115,3 +127,3 @@ ts.Identifier = Parser.Identifier;

parser.options = options;
parser.$message = getMessageFunction( parser, options ); // sets parser.messages
parser.$message = makeMessageFunction( parser, options, 'parse' ); // sets parser.messages

@@ -118,0 +130,0 @@ initTokenRewrite( parser, tokenStream );

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

const keywordRegexp = /^[a-zA-Z]+$/;
const keywordRegexp = /^[a-zA-Z]+$/; // we don't have keywords with underscore

@@ -53,11 +53,11 @@ let SEMI = null;

const reserved = this.getCurrentToken();
if (reserved.type !== identType && !keywordRegexp.test( reserved.text ))
const token = this.getCurrentToken();
if (token.type === identType || !keywordRegexp.test( token.text ))
return antlr4.Parser.prototype.match.call( this, ttype );
// eventually issue a warning (if reserved.type !== identType)
// 'use "RESERVED" for a name, as RESERVED is a reserved keyword'
this.message( 'syntax-fragile-ident', token, { id: token.text, delimited: token.text },
'$(ID) is a reserved name here - write $(DELIMITED) instead if you want to use it' );
this._errHandler.reportMatch(this);
this.consume();
return reserved;
return token;
}

@@ -191,10 +191,10 @@

if (expecting && expecting.length) {
err = recognizer.message( 'syntax-mismatched-token', e.offendingToken,
{ offending, expecting: expecting.join(', ') },
'Error', 'Mismatched $(OFFENDING), expecting $(EXPECTING)' );
err = recognizer.error( 'syntax-mismatched-token', e.offendingToken,
{ offending, expecting },
'Mismatched $(OFFENDING), expecting $(EXPECTING)' );
err.expectedTokens = expecting;
}
else { // should not really happen anymore... -> no messageId !
err = recognizer.message( null, e.offendingToken, { offending },
'Error', 'Mismatched $(OFFENDING)' );
err = recognizer.error( null, e.offendingToken, { offending },
'Mismatched $(OFFENDING)' );
}

@@ -214,5 +214,5 @@ if (!recognizer.avoidErrorListeners) // with --trace-parser or --trace-parser-ambig

const offending = this.getTokenDisplay( token, recognizer );
const err = recognizer.message( 'syntax-extraneous-token', token,
{ offending, expecting: expecting.join(', ') },
'Error', 'Extraneous $(OFFENDING), expecting $(EXPECTING)' );
const err = recognizer.error( 'syntax-extraneous-token', token,
{ offending, expecting },
'Extraneous $(OFFENDING), expecting $(EXPECTING)' );
err.expectedTokens = expecting; // TODO: remove next token?

@@ -233,5 +233,5 @@ if (!recognizer.avoidErrorListeners) // with --trace-parser or --trace-parser-ambig

// TODO: if non-reserved keyword will not been parsed as keyword, use Identifier for offending
const err = recognizer.message( 'syntax-missing-token', token,
{ offending, expecting: expecting.join(', ') },
'Error', 'Missing $(EXPECTING) before $(OFFENDING)' );
const err = recognizer.error( 'syntax-missing-token', token,
{ offending, expecting },
'Missing $(EXPECTING) before $(OFFENDING)' );
err.expectedTokens = expecting;

@@ -246,5 +246,5 @@ if (!recognizer.avoidErrorListeners) // with --trace-parser or --trace-parser-ambig

const expecting = this.getExpectedTokensForMessage( recognizer, t );
const m = recognizer.message( 'syntax-ignored-with', t,
{ offending: "';'", expecting: expecting.join(', ') },
'Warning', 'Unexpected $(OFFENDING), expecting $(EXPECTING) - ignored previous WITH' );
const m = recognizer.warning( 'syntax-ignored-with', t,
{ offending: "';'", expecting },
'Unexpected $(OFFENDING), expecting $(EXPECTING) - ignored previous WITH' );
m.expectedTokens = expecting;

@@ -303,11 +303,11 @@ }

const reserved = recognizer.getCurrentToken();
if (!keywordRegexp.test( reserved.text ))
const token = recognizer.getCurrentToken();
if (!keywordRegexp.test( token.text ))
return super1.recoverInline.call( this, recognizer );
// eventually issue a warning
// 'use "RESERVED" for a name, as RESERVED is a reserved keyword'
recognizer.message( 'syntax-fragile-ident', token, { id: token.text, delimited: token.text },
'$(ID) is a reserved name here - write $(DELIMITED) instead if you want to use it' );
this.reportMatch(recognizer); // we know current token is correct
recognizer.consume();
return reserved;
return token;
}

@@ -331,6 +331,16 @@

let names = [];
const pc = recognizer.constructor;
for (const v of expected.intervals) {
for (let j = v.start; j < v.stop; j++)
names.push( expected.elementName(recognizer.literalNames, recognizer.symbolicNames, j ) );
for (let j = v.start; j < v.stop; j++) {
// a generic keyword as such does not appear in messages, only its replacements,
// which are function name and argument position dependent:
if (j === pc.GenericArgFull)
names.push( ...recognizer.$genericKeywords.argFull );
// other expected tokens usually appear in messages, except the helper tokens
// which are used to solve ambiguities via the parser method setLocalToken():
else if (j !== pc.HelperToken1 && j !== pc.HelperToken2)
names.push( expected.elementName(recognizer.literalNames, recognizer.symbolicNames, j ) );
}
}
// The parser method excludeExpected() additionally removes some tokens from the message:
if (recognizer.$adaptExpectedToken && recognizer.$nextTokensToken === recognizer.$adaptExpectedToken) {

@@ -337,0 +347,0 @@ const excludes = (excludesForNextToken && recognizer.$adaptExpectedExcludes[0] instanceof Array)

@@ -11,5 +11,6 @@ // Generic ANTLR parser class with AST-building functions

const { ATNState } = require('antlr4/atn/ATNState');
const { addToDictWithIndexNo } = require('../base/dictionaries');
const { dictAdd, dictAddArray } = require('../base/dictionaries');
const locUtils = require('../base/location');
const { parseDocComment } = require('./docCommentParser');
const { functionsWithoutParens, specialFunctions } = require('../compiler/builtins');

@@ -37,3 +38,6 @@ // Class which is to be used as grammar option with

Object.create( antlr4.Parser.prototype ), {
message,
message: function(...args) { return message.call(this, 'message', ...args); },
error: function(...args) { return message.call(this, 'error', ...args); },
warning: function(...args) { return message.call(this, 'warning', ...args); },
info: function(...args) { return message.call(this, 'info', ...args); },
attachLocation,

@@ -43,5 +47,11 @@ startLocation,

combinedLocation,
surroundByParens,
unaryOpForParens,
leftAssocBinaryOp,
classifyImplicitName,
fragileAlias,
identAst,
functionAst,
valuePathAst,
signedExpression,
numberLiteral,

@@ -59,9 +69,11 @@ quotedLiteral,

handleComposition,
hanaFlavorOnly,
notSupportedYet,
csnParseOnly,
noAssignmentInSameLine,
noSemicolonHere,
setLocalKeyword,
setLocalToken,
excludeExpected,
isStraightBefore,
meltKeywordToIdentifier,
prepareGenericKeywords,
constructor: GenericAntlrParser, // keep this last

@@ -89,3 +101,2 @@ }

unexpected_char: /[^0-9a-f]/i,
literal: 'hex',
},

@@ -108,6 +119,6 @@ time: {

// Push message `msg` with location `loc` to array of errors:
function message( id, loc, ...args ) {
return this.$message( id, // function $message is set in antlrParser.js
(loc instanceof antlr4.CommonToken) ? this.tokenLocation(loc) : loc,
null, ...args );
function message( severity, id, loc, ...args ) {
const msg = this.$message[severity];
return msg( id, // function $message is set in antlrParser.js
(loc instanceof antlr4.CommonToken) ? this.tokenLocation(loc) : loc, ...args );
}

@@ -121,4 +132,5 @@

// TODO: this is not completely done this way
function hanaFlavorOnly( text, ...tokens ) {
if (!text || this.options.hanaFlavor)
function notSupportedYet( text, ...tokens ) {
if (!text)
return;

@@ -129,3 +141,3 @@ if (typeof text !== 'string') {

}
this.message( null, this.tokenLocation( tokens[0], tokens[tokens.length - 1] ), text );
this.error( null, this.tokenLocation( tokens[0], tokens[tokens.length - 1] ), text );
}

@@ -143,5 +155,6 @@

}
this.message( null, this.tokenLocation( tokens[0], tokens[tokens.length - 1] ), text );
this.error( null, this.tokenLocation( tokens[0], tokens[tokens.length - 1] ), text );
}
/** @this {object} */
function noSemicolonHere() {

@@ -164,2 +177,3 @@ const handler = this._errHandler;

if (excludes) {
// @ts-ignore
const t = this.getCurrentToken();

@@ -173,10 +187,8 @@ this.$adaptExpectedToken = t;

function setLocalKeyword( string, notBefore ) {
function setLocalToken( string, tokenName, notBefore, inSameLine ) {
const ll1 = this.getCurrentToken();
if (ll1.type === this.constructor.Identifier &&
ll1.text.toUpperCase() === string) {
// console.log('LT2:',this._input.LT(2).text.toUpperCase() , !notBefore.includes( this._input.LT(2).text.toUpperCase() ))
if (!notBefore || !notBefore.includes( this._input.LT(2).text.toUpperCase() ))
ll1.type = this.constructor[string];
}
if (ll1.text.toUpperCase() === string &&
(!inSameLine || this._input.LT(-1).line === ll1.line) &&
(!notBefore || !notBefore.test( this._input.LT(2).text )))
ll1.type = this.constructor[tokenName];
}

@@ -202,4 +214,4 @@

if (t.text === '@' && t.line <= this._input.LT(-1).line) {
this.message( 'syntax-anno-same-line', t, {},
'Warning', 'Annotation assignment belongs to next statement' );
this.warning( 'syntax-anno-same-line', t, {},
'Annotation assignment belongs to next statement' );
}

@@ -218,2 +230,26 @@ }

function meltKeywordToIdentifier() {
const { Identifier } = this.constructor;
const token = this.getCurrentToken() || { type: Identifier };
if (token.type < Identifier && /^[a-z]+$/i.test( token.text ))
token.type = Identifier;
}
function prepareGenericKeywords( pathItem ) {
this.$genericKeywords = { argFull: [] };
if (!pathItem)
return;
const func = pathItem.id && specialFunctions[pathItem.id.toUpperCase()];
const spec = func && func[pathItem.args.length];
if (!spec)
return;
// currently, we only have 'argFull', i.e. a keyword which is alternative to expression
// TODO: If not just at the beginning, we need a stack for $genericKeywords,
// as we can have nested special functions
this.$genericKeywords.argFull = Object.keys( spec );
const token = this.getCurrentToken() || { text: '' };
if (spec[token.text.toUpperCase()] === 'argFull')
token.type = this.constructor.GenericArgFull;
}
// Attach location matched by current rule to node `art`. If a location is

@@ -224,3 +260,3 @@ // already provided, only set the end location. Use this function only

function attachLocation( art ) {
if (!art)
if (!art || art.$parens)
return art;

@@ -230,7 +266,4 @@ if (!art.location)

const { stop } = this._ctx;
art.location.end = {
offset: stop.stop + 1, // after the last char (special for EOF?)
line: stop.line,
column: stop.stop - stop.start + stop.column + 2,
};
art.location.endLine = stop.line;
art.location.endCol = stop.stop - stop.start + stop.column + 2; // after the last char (special for EOF?)
return art;

@@ -243,24 +276,33 @@ }

*
* @returns {XSN.Location}
* @returns {CSN.Location}
*/
function startLocation( token = this._ctx.start ) {
return {
filename: this.filename,
start: { offset: token.start, line: token.line, column: token.column + 1 },
file: this.filename,
line: token.line,
col: token.column + 1,
};
}
// Return location of `token`. If `endToken` is provided, use its end
// location as end location in the result.
function tokenLocation( token, endToken = token, val ) {
/**
* Return location of `token`. If `endToken` is provided, use its end
* location as end location in the result.
*
* @param {object} token
* @param {object} endToken
* @param {any} val
*/
function tokenLocation( token, endToken, val ) {
if (!token)
return undefined;
if (!endToken) // including null
endToken = token;
/** @type {CSN.Location} */
const r = {
filename: this.filename,
start: { offset: token.start, line: token.line, column: token.column + 1 },
end: { // we only have single-line tokens
offset: endToken.stop + 1, // after the last char (special for EOF?)
line: endToken.line,
column: endToken.stop - endToken.start + endToken.column + 2,
},
file: this.filename,
line: token.line,
col: token.column + 1,
// we only have single-line tokens
endLine: endToken.line,
endCol: endToken.stop - endToken.start + endToken.column + 2, // after the last char (special for EOF?)
};

@@ -280,5 +322,27 @@ if (val !== undefined)

function surroundByParens( expr, open, close ) {
const location = this.tokenLocation( open, close );
if (expr.$parens)
expr.$parens.push( location );
else
expr.$parens = [ location ];
return expr;
}
function unaryOpForParens( query, val ) {
const parens = query.$parens;
if (!parens)
return query;
const location = parens[parens.length - 1];
return { op: { val, location }, location, args: [query] };
}
// If the token before the current one is a doc comment (ignoring other tokens
// on the hidden channel), put its "cleaned-up" text as value of property `doc`
// of arg `node` (which could be an array). Complain if `doc` is already set.
//
// The doc comment token is not a non-hidden token for the following reasons:
// - misplaced doc comments would lead to a parse error (incompatible),
// - would influence the prediction, probably even induce adaptivePredict() calls,
// - is only slightly "more declarative" in the grammar.
function docComment( node ) {

@@ -291,4 +355,4 @@ if (!this.options.docComment)

if (node.doc) {
this.message( 'syntax-duplicate-doc-comment', token, {},
'Warning', 'Repeated doc comment - previous doc is replaced' );
this.warning( 'syntax-duplicate-doc-comment', token, {},
'Repeated doc comment - previous doc is replaced' );
}

@@ -308,2 +372,11 @@ node.doc = this.tokenLocation( token, token, parseDocComment( token.text ) );

function fragileAlias( ast, safe = false ) {
if (safe || ast.$delimited || !/^[a-zA-Z][a-zA-Z_]+$/.test( ast.id ))
this.warning( 'syntax-sloppy-alias', ast.location, { keyword: 'as' },
'Please add the keyword $(KEYWORD) in front of the alias name' );
else // configurable error
this.message( 'syntax-fragile-alias', ast.location, { keyword: 'as' },
'Please add the keyword $(KEYWORD) in front of the alias name' );
}
// Return AST for identifier token `token`. Also check that identifer is not empty.

@@ -318,21 +391,22 @@ function identAst( token, category ) {

if (!id) {
this.message( 'syntax-empty-ident', token, {},
'Error', 'Delimited identifier must contain at least one character' );
this.error( 'syntax-empty-ident', token, {},
'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 ) };
// $delimited is used to complain about ![$self] and other magic vars usage;
// we might complain about that already here via @arg{category}
return { id, $delimited: true, location: this.tokenLocation( token ) };
}
if (token.text[0] !== '"')
return { id, location: this.tokenLocation( token ) };
// quoted:
// delimited:
id = id.slice( 1, -1 ).replace( /""/g, '"' );
if (!id) {
this.message( 'syntax-empty-ident', token, {},
'Error', 'Delimited identifier must contain at least one character' );
this.error( 'syntax-empty-ident', token, {},
'Delimited identifier must contain at least one character' );
}
else {
this.message( 'syntax-deprecated-ident', token, { delimited: id }, 'Warning',
this.message( 'syntax-deprecated-ident', token, { delimited: id },
'Deprecated delimited identifier syntax, use $(DELIMITED) - strings are delimited by single quotes' );
}
return { id, quoted: true, location: this.tokenLocation( token ) };
return { id, $delimited: true, location: this.tokenLocation( token ) };
}

@@ -354,2 +428,47 @@

function valuePathAst( ref ) {
// TODO: XSN representation of functions is a bit strange - rework if methods
// are introduced
const { path } = ref;
if (!path || path.broken)
return ref;
if (path.length !== 1) {
const item = path.find( i => i.args && i.$syntax !== ':' );
if (!item)
return ref;
this.error( 'syntax-not-suported', item.location,
'Methods in expressions are not supported yet' );
path.broken = true;
path.length = 1;
}
const { args, id, location } = path[0];
if (args) {
if (path[0].$syntax !== ':')
return { op: { location, val: 'call' }, func: ref, location: ref.location, args };
}
else if (!path[0].$delimited && functionsWithoutParens.includes( id.toUpperCase() )) {
return { op: { location, val: 'call' }, func: ref, location: ref.location };
}
return ref;
}
// If a '-' is directly before an unsigned number, consider it part of the number;
// otherwise (including for '+'), represent it as extra unary prefix operator.
function signedExpression( signToken, expr ) {
const sign = this.tokenLocation( signToken, undefined, signToken.text );
const nval =
(signToken.text === '-' &&
expr.literal === 'number' &&
sign.location.endLine === expr.location.line &&
sign.location.endCol === expr.location.col &&
(typeof expr.val === 'number'
? expr.val >= 0 && -expr.val
: !expr.val.startsWith('-') && `-${ expr.val }`)) || false;
if (nval === false)
return { op: sign, args: [ expr ] };
expr.val = nval;
--expr.location.col;
return expr;
}
// Return AST for number token `token` with optional token `sign`. Represent

@@ -363,5 +482,6 @@ // the number as number in property `val` if the number can safely be

// TODO: warning for space in between
const { end } = location;
const { endLine, endCol } = location;
location = this.startLocation( sign );
location.end = end;
location.endLine = endLine;
location.endCol = endCol;
text = sign.text + text;

@@ -371,6 +491,9 @@ }

if (!Number.isSafeInteger(num)) {
if (sign != null)
if (sign == null) {
this.error( 'syntax-no-integer', token, {},
'An integer number is expected here' );
}
else if (text !== `${num}`) {
return { literal: 'number', val: text, location };
this.message( 'syntax-no-integer', token, {},
'Error', 'An integer number is expected here' );
}
}

@@ -384,2 +507,3 @@ return { literal: 'number', val: num, location };

function quotedLiteral( token, literal ) {
/** @type {CSN.Location} */
const location = this.tokenLocation( token );

@@ -396,3 +520,3 @@ const pos = token.text.search( '\'' ) + 1; // pos of char after quote

!this.options.parseOnly)
this.message( null, location, p.test_msg ); // TODO: message id
this.error( null, location, p.test_msg ); // TODO: message id

@@ -402,6 +526,8 @@ if (p.unexpected_char) {

if (~idx) {
this.message( null, { // TODO: message id
filename: location.filename,
start: atChar( idx ),
end: atChar( idx + (val[idx] === '\'' ? 2 : 1) ),
this.error( null, { // TODO: message id
file: location.file,
line: location.line,
endLine: location.line,
col: atChar(idx),
endCol: atChar( idx + (val[idx] === '\'' ? 2 : 1) ),
}, p.unexpected_msg );

@@ -417,7 +543,3 @@ }

function atChar(i) {
return {
line: location.start.line,
column: location.start.column + pos + i,
offset: location.start.offset + pos + i,
};
return location.col + pos + i;
}

@@ -438,14 +560,17 @@ }

else {
const { start, end } = this.tokenLocation( prefix );
if (end.line !== ident.location.start.line || // end.offset will disappear
end.column !== ident.location.start.column) {
const tokenLoc = this.tokenLocation( prefix );
if (tokenLoc.endLine !== ident.location.line ||
tokenLoc.endCol !== ident.location.col) {
const wsLocation = {
filename: ident.location.filename,
start: end, // !
end: { ...ident.location.start },
file: ident.location.file,
line: tokenLoc.endLine, // !
col: tokenLoc.endCol, // !
endLine: ident.location.line,
endCol: ident.location.col,
};
this.message( 'syntax-anno-space', wsLocation, {}, 'Error', // TODO: really Error?
'Expected identifier after \'@\' but found whitespace' );
this.error( 'syntax-anno-space', wsLocation, {}, // TODO: really Error?
'Expected identifier after \'@\' but found whitespace' );
}
ident.location.start = start;
ident.location.line = tokenLoc.line;
ident.location.col = tokenLoc.col;
ident.id = prefix.text + ident.id;

@@ -470,6 +595,7 @@ path.push( ident );

if (name instanceof Array) {
// XSN TODO: clearly say: definitions have name.path, members have name.id
const last = name.length && name[name.length - 1];
if (last && last.id) { // // A.B.C -> 'C'
name = {
id: last.id, location: last.location, calculated: true, $inferred: 'as',
id: last.id, location: last.location, $inferred: 'as',
};

@@ -488,25 +614,26 @@ }

// no id was parsed, but with error recovery: no further error
env += '_'; // could be tested in name search
if (!parent[env])
parent[env] = [ art ];
else
parent[env].push(art);
// TODO: add to parent[env]['']
// which could be tested in name search (then no undefined-ref error)
return art;
}
else if (env === 'artifacts') {
dictAddArray( parent[env], art.name.id, art );
}
else if (kind || this.options.parseOnly) {
addToDictWithIndexNo( parent, env, art.name.id, art );
dictAdd( parent[env], art.name.id, art );
}
else {
addToDictWithIndexNo( parent, env, art.name.id, art, ( name, loc ) => {
dictAdd( parent[env], art.name.id, art, ( name, loc ) => {
// do not use function(), otherwise `this` is wrong:
if (kind === 0) {
this.message( 'duplicate-argument', loc, { name },
'Error', 'Duplicate value for parameter $(NAME)' );
this.error( 'duplicate-argument', loc, { name },
'Duplicate value for parameter $(NAME)' );
}
else if (kind === '') {
this.message( 'duplicate-excluding', loc, { name },
'Error', 'Duplicate EXCLUDING for source element $(NAME)' );
this.error( 'duplicate-excluding', loc, { name, keyword: 'excluding' },
'Duplicate $(NAME) in the $(KEYWORD) clause' );
}
else {
this.message( 'duplicate-prop', loc, { name },
'Error', 'Duplicate value for structure property $(NAME)' );
this.error( 'duplicate-prop', loc, { name },
'Duplicate value for structure property $(NAME)' );
}

@@ -541,3 +668,3 @@ } );

/** Assign all non-empty (undefined, null, {}, []) properties in argument
* `props` and argument `annos` as property `annotationAssignments` to `target`
* `props` and argument `annos` as property `$annotations` to `target`
* and return it. Hack: if argument `annos` is exactly `true`, return

@@ -553,10 +680,2 @@ * `Object.assign( target, props )`, for rule `namedValue`. ANTLR tokens are

function assignProps( target, annos = [], props, location ) {
while (Array.isArray( props )) {
// XSN TODO: change representation of parentheses around expressions
// Then this check can be removed
// @ts-ignore `location` may exist on props even though it's an array.
this.message( null, props.location || location || this.startLocation( this._ctx.start ), {},
'Error', 'Remove the parentheses around the expression' );
props = props[0];
}
if (annos === true)

@@ -578,3 +697,3 @@ return Object.assign( target, props );

if (annos)
target.annotationAssignments = annos;
target.$annotations = annos;
return target;

@@ -589,2 +708,22 @@ }

// Create AST node for binary operator `op` and arguments `args`
function leftAssocBinaryOp( left, opToken, eToken, right, extraProp = 'quantifier' ) {
const op = this.tokenLocation( opToken, undefined, opToken.text.toLowerCase() );
const extra = eToken
? this.tokenLocation( eToken, undefined, eToken.text.toLowerCase() )
: undefined;
if (!left.$parens &&
(left.op && left.op.val) === (op && op.val) &&
(left[extraProp] && left[extraProp].val) === (extra && extra.val)) {
left.args.push( right );
return left;
}
else if (extra) {
return { op, [extraProp]: extra, args: [ left, right ], location: left.location };
}
else {
return { op, args: [ left, right ], location: left.location };
}
}
// Set property `prop` of `target` to value `value`. Issue error if that

@@ -597,4 +736,4 @@ // property has been set before, while mentioning the keywords previously

if (prev) {
this.message( 'syntax-repeated-option', loc, { option: prev.option },
'Error', 'Option $(OPTION) has already been specified' );
this.error( 'syntax-repeated-option', loc, { option: prev.option },
'Option $(OPTION) has already been specified' );
}

@@ -618,4 +757,4 @@ if (typeof value === 'boolean') {

else if (!inferred) {
this.message( 'syntax-repeated-cardinality', location, { token: token.text },
'Warning', 'The target cardinality has already been specified - ignored $(TOKEN)' );
this.warning( 'syntax-repeated-cardinality', location, { keyword: token.text },
'The target cardinality has already been specified - ignored $(KEYWORD)' );
}

@@ -622,0 +761,0 @@ }

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

// Main entry point for the Research Vanilla CDS Compiler
// Main entry point for the CDS Compiler
//

@@ -18,6 +18,8 @@ // File for external usage = which is read in other modules with

const { odata, cdl, sql, hdi, hdbcds, edm, edmx } = require('./api/main');
const { emptyWeakLocation } = require('./base/location');
const { getArtifactDatabaseNameOf, getElementDatabaseNameOf } = require('./model/csnUtils');
const { sortMessages, sortMessagesSeverityAware, deduplicateMessages } = require('./base/messages');
const parseLanguage = require('./language/antlrParser');
const { parseX, compileX, compileSyncX, compileSourcesX, InvocationError } = require('./compiler');
// The compiler version (taken from package.json)

@@ -35,556 +37,17 @@ function version() {

hasErrors,
makeMessageFunction,
explainMessage,
hasMessageExplanation
} = require('./base/messages');
const { promiseAllDoNotRejectImmediately } = require('./base/node-helpers');
const assertConsistency = require('./compiler/assert-consistency');
const parseLanguage = require('./language/antlrParser');
const moduleLayers = require('./compiler/moduleLayers');
const { define } = require('./compiler/definer');
const resolve = require('./compiler/resolver');
const propagator = require('./compiler/propagator');
const semanticChecks = require('./checks/semanticChecks');
const compactJson = require('./json/compactor').compact;
const compactSortedJson = require('./json/compactor').compactSorted;
const { compactModel, compactQuery, compactExpr } = require('./json/to-csn')
const fs = require('fs');
const edmx2csn = require('./edm/annotations/edmx2csn'); // translate edmx annotations into csn
const path = require('path');
const moduleResolve = require('resolve');
const { resolveCDS, isLocalFile, extensions, packageFilter } = require('./utils/resolve');
/**
* Parse the given source with the correct parser based on the file name's
* extension. For example use edm2csn for `.xml` files and the CDL parser
* for `.cds` files.
*
* @param {string} source Source code of the file.
* @param {string} filename Filename including its extension, e.g. "file.cds"
* @param {object} options Compile options
*/
function parse( source, filename, options = {} ) {
const ext = path.extname( filename ).toLowerCase();
if (ext === '.xml') {
return edmx2csn( source, filename, options );
}
else if (['.json', '.csn'].includes(ext)) {
return require('./json/from-csn').parse( source, filename, options );
} else if (options.fallbackParser || ['.cds', '.hdbcds', '.hdbdd'].includes(ext))
return parseLanguage( source, filename, options );
else {
const model = {};
const { error } = makeMessageFunction( model, options );
error( 'file-unknown-ext', emptyWeakLocation(filename),
{ file: ext && ext.slice(1), '#': !ext && 'none' }, {
std: 'Unknown file extension $(FILE)',
none: 'No file extension'
} );
return model;
}
}
// 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 } );
}
// Main function: Compile the sources from the files given by the array of
// `filenames`. As usual with the `fs` library, relative file names are
// relative to the working directory `process.cwd()`. With argument `dir`, the
// file names are relative to `process.cwd()+dir`. Options can have the
// following properties:
// - Truthy `parseOnly`: stop compilation after parsing.
// - Truthy `lintMode`: do not do checks and propagation
// - many others - TODO
// This function returns a Promise. See ../bin/cdsv.js for an example usage.
// See function `compileSync` or `compileSources` for alternative compile
// functions.
//
// The promise is fulfilled if all files could be read and processed without
// errors. The fulfillment value is an augmented CSN (see
// ./compiler/definer.js).
//
// If there are errors, the promise is rejected. If there was an invocation
// error (repeated filenames or if the file could not be read), the rejection
// value is an InvocationError. Otherwise, the rejection value is a
// CompilationError containing a vector of individual errors.
//
// `fileCache` is a dictionary of absolute file names to the file content
// - false: the file does not exist
// - true: file exists (fstat), no further knowledge yet - i.e. value will change!
// - 'string' or instanceof Buffer: the file content
// - { realname: fs.realpath(filename) }: if filename is not canonicalized
//
function compile( filenames, dir='', options = {}, fileCache = Object.create(null) ) {
// A non-proper dictionary (i.e. with prototype) is safe if the keys are
// absolute file names - they start with `/` or `\` or similar
// if (Object.getPrototypeOf( fileCache ))
// fileCache = Object.assign( Object.create(null), fileCache );
dir = path.resolve(dir);
const a = processFilenames( filenames, dir );
a.fileContentDict = Object.create(null);
// For the transition phase, have the option to use the new OR old
// resolve functionality.
const resolveFunction = options.newResolve ? resolveCDS : moduleResolve;
const model = { sources: a.sources, options };
const { error } = makeMessageFunction( model );
const parseOptions = optionsWithMessages( options, model );
let all = promiseAllDoNotRejectImmediately( a.files.map(readAndParse) );
all = all
.then( testInvocation, function (reason) {
// do not reject with PromiseAllError, use InvocationError:
const errs = reason.valuesOrErrors.filter (e => e instanceof Error);
// internal error if no file IO error (has property `path`)
return Promise.reject( errs.find( e => !e.path ) ||
new InvocationError([...a.repeated, ...errs]) )
});
if (!options.parseOnly && !options.parseCdl)
all = all.then( readDependencies );
return all.then( function() {
moduleLayers.setLayers( a.sources );
if (options.collectSources)
return collect();
return compileDo( model );
});
function collect() {
handleMessages( model );
let dependencies = Object.create(null);
for (let name in a.sources) {
let deps = a.sources[name].dependencies;
if (deps && deps.length) {
let mod = Object.create(null);
deps.forEach( d => mod[ d.val ] = d.realname );
dependencies[name] = mod;
}
}
return { sources: a.fileContentDict, dependencies, files: a.files };
}
// Read file `filename` and parse its content, return messages
function readAndParse( filename ) {
if ( filename === false ) // module which has not been found
return [];
let rel = a.sources[filename] || path.relative( dir, filename );
if (typeof rel === 'object') // already parsed
return []; // no further dependency processing
// no parallel readAndParse with same resolved filename should read the file,
// also ensure deterministic sequence in a.sources:
a.sources[filename] = { filename: rel };
return new Promise( function (fulfill, reject) {
readFile( filename, 'utf8', function (err, source) {
if (err)
reject(err);
else {
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);
}
}
});
});
}
function readFile( filename, enc, cb ) {
if (typeof enc === 'function') // moduleResolve uses old-style API
cb = enc, enc = null;
let body = fileCache[ filename ];
if (body && typeof body === 'object' && body.realname) {
filename = body.realname; // use fs.realpath name
body = fileCache[ filename ];
}
if (body !== undefined && body !== true) { // true: we just know it is there
if (body === false) {
body = new Error( `ENOENT: no such file or directory, open '${filename}'`);
body.code = 'ENOENT', body.errno = -2, body.syscall = 'open';
body.path = filename;
}
if (body instanceof Error) {
traceFS( 'READFILE:cache-error:', filename, body.message );
cb( body ) // no need for process.nextTick( cb, body ) with moduleResolve
}
else {
traceFS( 'READFILE:cache:', filename, body );
cb( null, body );
}
}
else {
traceFS( 'READFILE:start:', filename );
// TODO: set cache directly to some "delay" - store error differently?
// e.g. an error of callback functions!
fs.readFile( filename, enc, function( err, data ) {
fileCache[ filename ] = err || data;
traceFS( 'READFILE:data:', filename, err || data );
cb( err, data );
});
}
}
function isFile( filename, cb ) {
let body = fileCache[ filename ];
if (body !== undefined) {
traceFS( 'ISFILE:cache:', filename, body );
if (body instanceof Error)
cb( body ) // no need for process.nextTick( cb, body ) with moduleResolve
else
cb( null, !!body );
}
else {
traceFS( 'ISFILE:start:', filename, body );
// in the future (if we do module resolve ourself with just readFile),
// we avoid parallel readFile by storing having an array of `cb`s in
// fileCache[ filename ] before starting fs.readFile().
fs.stat( filename, function( err, stat ) {
if (err)
body = (err.code === 'ENOENT' || err.code === 'ENOTDIR') ? false : err;
else
body = !!(stat.isFile() || stat.isFIFO());
if (fileCache[ filename ] === undefined) // parallel readFile() has been processed
fileCache[ filename ] = body;
traceFS( 'ISFILE:data:', filename, body );
if (body instanceof Error)
cb( err );
else
cb( null, body );
});
}
}
function traceFS( intro, filename, data ) {
if (options.traceFs)
// eslint-disable-next-line no-console
console.log( intro, filename,
(typeof data === 'string' || data instanceof Buffer)
? typeof data
: (data === undefined) ? '?' : '' + data );
}
// Combine the parse results (if there are not file IO errors)
function testInvocation( values ) {
if (a.repeated.length)
// repeated file names in invocation => just report these
return Promise.reject( new InvocationError(a.repeated) );
return values;
}
function readDependencies( astArray ) {
let promises = [];
for (let ast of astArray) {
// console.log( 'READ-DEP:',ast.filename, ast.dependencies && ast.dependencies.length )
if (!ast.dependencies || !ast.dependencies.length)
continue;
let dependencies = Object.create( null );
for (let d of ast.dependencies) {
let module = d.val;
let dep = dependencies[module];
if (dep)
dep.usingFroms.push( d );
else
dependencies[module] = { module, basedir: ast.dirname, usingFroms: [d] };
}
// create promises after all usingFroms have been collected, as the
// Promise executor is called immediately with `new`:
for (let module in dependencies)
promises.push( resolveModule( dependencies[module] ) );
}
if (!promises.length)
return [];
// read files (important part: adding filename to a.sources) after having
// resolved the module names to ensure deterministic sequence in a.sources
return Promise.all( promises )
.then( fileNames => Promise.all( fileNames.map( readAndParse ) ) )
.then( readDependencies );
}
function resolveModule( dep ) {
// let opts = { extensions, packageFilter, basedir: dep.basedir, preserveSymlinks: false };
// `preserveSymlinks` option does not really work -> provide workaround anyway...
// Hm, the resolve package also does not follow the node recommendation:
// "Using fs.stat() to check for the existence of a file before calling
// fs.open(), fs.readFile() or fs.writeFile() is not recommended"
let opts = { extensions, packageFilter, basedir: dep.basedir, isFile, readFile };
return new Promise( function (fulfill, reject) {
// 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;
resolveFunction( path, opts, function (err, res) {
// console.log('RESOLVE', dep, res, err)
if (err)
reject(err);
else {
let body = fileCache[ res ];
if (body === undefined || body === true) { // use fs if no or just temp entry
dep.absname = res;
fs.realpath( res, cb );
}
else if (body && typeof body === 'object' && body.realname) {
//dep.absname = body.realname;
cb( null, body.realname ); // use fs.realpath name
}
else {
//dep.absname = res;
cb( null, res );
}
}
});
function cb( err, res ) {
if (err)
reject(err);
else {
if (dep.absname)
fileCache[ dep.absname ] = (dep.absname === res) || { realname: res };
dep.resolved = res; // store in dep that module resolve was successful
for (let from of dep.usingFroms)
from.realname = res;
fulfill(res);
}
}
})
.catch( function() { // (err) TODO: check for expected exceptions
if (dep.resolved) {
let resolved = path.relative( dep.basedir, dep.resolved );
if (options.testMode)
resolved = resolved.replace( /\\/g, '/' );
for (let from of dep.usingFroms)
error( 'file-not-readable', from.location, { file: resolved },
'Cannot read file $(FILE)' );
}
else if (isLocalFile( dep.module ) ) {
for (let from of dep.usingFroms)
error( 'file-unknown-local', from.location, { file: dep.module },
'Cannot find local module $(FILE)' );
}
else {
let internal = /[\\/]/.test( dep.module ) && 'internal';
for (let from of dep.usingFroms)
error( 'file-unknown-package', from.location,
{ file: dep.module, '#': internal }, {
std: 'Cannot find package $(FILE)',
internal: 'Cannot find package module $(FILE)'
} );
}
return false;
});
}
}
/**
* Synchronous version of function `compile` with limited functionality:
* - option `--follow-deps` is not supported,
* - an invocation error ends the compilation immediately.
*
* @param {string[]} filenames Files to compile.
* @param {string} [dir=""] Base directory. All files are resolved relatively
* to this directory
* @param {object} [options={}] Compilation options.
* @returns {XSN.Model} Augmented CSN
*/
function compileSync( filenames, dir = '', options = {} ) {
dir = path.resolve(dir);
const processedFiles = processFilenames(filenames, dir);
const sources = Object.create(null);
if (processedFiles.repeated.length)
throw new InvocationError(processedFiles.repeated);
try {
for (const filename of processedFiles.files) {
const source = fs.readFileSync( filename, 'utf8' );
sources[ processedFiles.sources[filename] ] = source;
}
}
catch (e) {
throw new InvocationError( [e] );
}
return compileSources( sources, options );
}
/**
* Promise-less main functions: compile the given sources.
*
* Argument `sourcesDict` is a dictionary (it could actually be a ordinary object)
* 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.
*
* See function `compile` for the meaning of the argument `options`. If there
* are parse or other compilation errors, throw an exception CompilationError
* containing a vector of individual errors.
*
* @param {string|object} sourcesDict Files to compile.
* @param {object} [options={}] Compilation options.
* @returns {XSN.Model} Augmented CSN
*/
function compileSources( sourcesDict, options = {} ) {
const content = sourcesDict.sources ? sourcesDict.sources :
(typeof sourcesDict === 'string') ? { '<stdin>.cds': sourcesDict } : sourcesDict;
const sources = Object.create(null);
const model = { sources, options };
makeMessageFunction( model ); // make sure that we have a "central" messages array
const parseOptions = optionsWithMessages( options, model );
for (const filename in content) {
const source = content[filename];
if (typeof source === 'string') {
const ast = parse( source, filename, parseOptions );
sources[filename] = ast;
ast.filename = filename;
assertConsistency( ast, parseOptions );
}
else { // source is a XSN object (CSN/CDL parser output)
sources[filename] = source;
}
}
// add dependencies to AST
for (const filename in sourcesDict.dependencies) {
const dependency = sourcesDict.dependencies[ filename ];
for (const val in dependency) {
const dep = { literal: 'string', val, realname: dependency[val] /*, location: ???*/};
const arr = sources[filename].dependencies;
if (arr)
arr.push( dep );
else
sources[filename].dependencies = [ dep ];
}
}
moduleLayers.setLayers( sources );
return compileDo( model );
}
// Make sure to use a "central" messages array during parsing:
function optionsWithMessages( options, model ) {
return (options.messages)
? options
// : node 8.3 / elint-5+: { messages: model.messages, ...options };
: Object.assign( { messages: model.messages }, options );
}
/**
* On the given model (AST like CSN) run the definer, resolver as well as semantic checks.
* Creates an augmented CSN (XSN) and returns it.
*
* @param {object} model AST like CSN generated e.g. by `parseLanguage()`
* @returns {XSN.Model} Augmented CSN (XSN)
*/
function compileDo( model ) {
const options = model.options;
if (!options.testMode) {
model.meta = {}; // provide initial central meta object
}
if (options.parseOnly)
return handleMessages( model );
define( model );
// do not run the resolver in parse-cdl mode or we get duplicate annotations, etc.
if (options.parseCdl)
return handleMessages( model );
resolve( model );
assertConsistency( model );
handleMessages( model ); // stop compilation with errors
if (options.lintMode)
return model;
semanticChecks(model);
handleMessages( model );
return propagator.propagate( model );
}
// Process an array of `filenames`. Returns an object with properties:
// - `sources`: dictionary which has a filename as key (value is irrelevant)
// - `files`: the argument array without repeating the same name
// - `repeated`: array of filenames which have been repeatedly listed
// (listed only once here even if listed thrice)
//
// Note: there is nothing file-specific about the filenames, the filenames are
// not normalized - any strings work
function processFilenames( filenames, dir ) {
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]) {
sources[name] = path.relative( dir, name );
files.push(name);
}
else if (typeof sources[name] === 'string') { // not specified more than twice
const msg = 'Repeated argument: file \'' + sources[name] + '\'';
repeated.push( new ArgumentError( name, msg ) );
}
}
return { sources, files, repeated };
}
// Class for command invocation errors. Additional members:
// `errors`: vector of errors (file IO or ArgumentError)
class InvocationError extends Error {
constructor(errs,...args) {
super(...args);
this.errors = errs;
this.hasBeenReported = false;
}
}
// Class for argument errors. Additional members:
// `argument`: the command argument (repeated file names)
class ArgumentError extends Error {
constructor(arg,...args) {
super(...args);
this.argument = arg;
}
}
function parseToCqn( cdl, filename = '<query>.cds', options = {} ) {
function parseCql( cdl, filename = '<query>.cds', options = {} ) {
let xsn = parseLanguage( cdl, filename, Object.assign( {parseOnly:true}, options ), 'query' );
handleMessages( xsn );
handleMessages( xsn, options );
return compactQuery( xsn );
}
function parseToExpr( cdl, filename = '<expr>.cds', options = {} ) {
function parseExpr( cdl, filename = '<expr>.cds', options = {} ) {
let xsn = parseLanguage( cdl, filename, Object.assign( {parseOnly:true}, options ), 'expr' );
handleMessages( xsn );
handleMessages( xsn, options );
return compactExpr( xsn );

@@ -597,10 +60,6 @@ }

version,
parse,
collectSources,
compile, // main function
compileSync, // main function
compileSources, // main function
compactJson, // should rather use toCsn
compactSortedJson, // should rather use toCsn
compactModel,
compile: (...args) => compileX(...args).then( compactModel ), // main function
compileSync: (...args) => compactModel( compileSyncX(...args) ), // main function
compileSources: (...args) => compactModel( compileSourcesX(...args) ), // main function
compactModel: csn => csn, // for easy v2 migration
CompilationError,

@@ -615,8 +74,6 @@ sortMessages,

hasMessageExplanation,
InvocationError,
InvocationError, // TODO: make it no error if same file name is provided twice
hasErrors,
// Backends
toHana : backends.toHana,
toOdata : backends.toOdata,
// TODO: Expose when transformers are switched to CSN

@@ -626,11 +83,10 @@ // toOdataWithCsn: backends.toOdataWithCsn,

preparedCsnToEdm : (csn, service, options) => { return backends.preparedCsnToEdm(csn, service, options).edmj},
toCdl : backends.toCdl,
toSwagger : backends.toSwagger,
toSql : backends.toSql,
toCsn : backends.toCsn,
toRename : backends.toRename, // Tentative, subject to change
// additional API:
parseToCqn,
parseToExpr,
parse: { cql: parseCql, expr: parseExpr }, // preferred names
/**
* @deprecated Use parse.cql instead
*/
parseToCqn: parseCql,
parseToExpr: parseExpr, // deprecated name
// SNAPI

@@ -641,3 +97,8 @@ for: { odata },

getArtifactCdsPersistenceName: getArtifactDatabaseNameOf,
getElementCdsPersistenceName: getElementDatabaseNameOf
getElementCdsPersistenceName: getElementDatabaseNameOf,
// INTERNAL functions for the cds-lsp package and friends - before you use
// it, you MUST talk with us - there can be potential incompatibilities with
// new releases (even having the same major version):
$lsp: { parse: parseX, compile: compileX },
};

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

// TODO: think of using the term `query` for the thing inside SELECT/SET.
'use strict';

@@ -157,3 +159,3 @@

const main = csn.definitions[name];
const queries = views[name] || main.query && allQueries( name, main );
const queries = views[name] || allQueries( name, main );

@@ -192,4 +194,4 @@ const path = (typeof obj === 'string') ? [ obj ] : obj.ref;

if (!query)
return expandRefPath( path, (parent || main).elements[head] );
const select = query.SELECT;
return expandRefPath( path, (parent || main).elements[head], scope );
const select = query.SELECT || query.projection;
if (!select || obj.$env === true)

@@ -201,3 +203,3 @@ // TODO: do not do this if current query has a parent query (except with obj.$env)

if (typeof obj.$env === 'number') { // head is mixin or table alias name
const s = (obj.$env) ? queries[obj.$env - 1].SELECT : select;
const s = (obj.$env) ? queries[obj.$env - 1] : select;
const m = s.mixin && s.mixin[head];

@@ -231,3 +233,3 @@ return expandRefPath( path, m || s._sources[head], (m ? 'mixin' : 'alias') );

if (scope === 'on' || scope === 'orderBy')
return expandRefPath( path, queryOrMain( query, main ).elements[head] );
return expandRefPath( path, queryOrMain( query, main ).elements[head], scope );

@@ -273,3 +275,6 @@ if (typeof select.$alias === 'string') { // with unique source

const all = [];
traverseQuery( main.query, null, function memorize( query, select ) {
const projection = main.query || main.projection && main;
if (!projection)
return all;
traverseQuery( projection, null, function memorize( query, select ) {
if (query.ref) { // ref in from

@@ -286,6 +291,7 @@ const as = query.as || implicitAs( query.ref );

}
if (query.SELECT) { // every SELECT query -- TODO: remember number?
setLink( query.SELECT, '_sources', Object.create(null) );
setLink( query.SELECT, '$alias', null );
all.push( query );
const proj = query.SELECT || query.projection;
if (proj) { // every SELECT query -- TODO: remember number?
setLink( proj, '_sources', Object.create(null) );
setLink( proj, '$alias', null );
all.push( proj );
}

@@ -322,5 +328,5 @@ } );

function traverseQuery( query, select, callback ) {
if (query.SELECT) {
if (query.SELECT || query.projection) {
callback( query, select );
query = query.SELECT;
query = query.SELECT || query.projection;
traverseFrom( query.from, query, callback );

@@ -388,3 +394,4 @@ }

csnPath.forEach( ( prop, index ) => {
for (let index = 0; index < csnPath.length; index++) {
const prop = csnPath[index];
// array item, name/index of artifact/member, (named) argument

@@ -410,3 +417,3 @@ if (isName || Array.isArray( obj )) {

}
else if (prop === 'SELECT' || prop === 'SET') {
else if (prop === 'SELECT' || prop === 'SET' || prop === 'projection') {
query = obj;

@@ -433,4 +440,9 @@ scope = prop;

}
if (!obj[prop])
// Path does not match the CSN. Break out of loop and use current object as best guess.
break;
obj = obj[prop];
} );
}
// console.log( 'CPATH:', csnPath, scope, obj, parent.$location );

@@ -437,0 +449,0 @@ return {

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

const { sortCsn } = require('../json/to-csn');
const version = require('../../package.json').version;

@@ -63,2 +64,4 @@ // Low-level utility functions to work with compact CSN.

/**

@@ -69,3 +72,3 @@ * Create an object to track visited objects identified by a unique string.

function createVisited(id) {
let visited = {};
let visited = Object.create(null);
check(id);

@@ -373,3 +376,3 @@ return { check };

if (typeof(type) === 'string') {
if (type.startsWith('cds.') && type.indexOf('.') === type.lastIndexOf('.')) // built-in type
if (isBuiltinType(type)) // built-in type
return type;

@@ -407,3 +410,3 @@ if (cycleCheck) {

else if (type.items)
return getFinalBaseType(type.items, path, cycleCheck);
return type;
else

@@ -431,12 +434,8 @@ // TODO: this happens if we don't have a type, e.g. an expression in a select list

* Deeply clone the given CSN model and return it.
*
* @param {object} csn
* @param {CSN.Options} options CSN Options, only used for options.dictionaryPrototype
*/
function cloneCsn(csn){
// cannot require this earlier because to-csn has a dependency on this file
const clonedCsn = sortCsn(csn, true);
if (csn.messages){
setProp(clonedCsn, 'messages', csn.messages);
}
return clonedCsn;
function cloneCsn(csn, options) {
return sortCsn(csn, options || true); // TODO: Remove `true` when all cloneCsn calls pass options.
}

@@ -526,12 +525,11 @@

function forEachGeneric( obj, prop, callback, path = []) {
let dict = obj[prop];
for (let name in dict) {
let dictObj = dict[name];
if (dictObj instanceof Array) // redefinitions - not in CSN!
dictObj.forEach( o => cb( o, name ) );
else
cb( dictObj, name );
const dict = obj[prop];
for (const name in dict) {
if (!Object.prototype.hasOwnProperty.call(dict, name))
continue;
const dictObj = dict[name];
cb( dictObj, name );
}
function cb(o, name ) {
if(callback instanceof Array)
if (callback instanceof Array)
callback.forEach(cb => cb( o, name, prop, path.concat([prop, name])));

@@ -567,2 +565,4 @@ else

for (let name in node) {
if (!Object.hasOwnProperty.call( node, name ))
continue;
// If ref found within a non-dictionary, call callback

@@ -609,6 +609,8 @@ if (name === 'ref' && Object.getPrototypeOf(node)) {

if (q.SELECT) {
p.push('SELECT');
// The projection is turned into a normalized query - there
// is no real SELECT, it is fake
if(!(path.length === 3 && path[2] === 'projection'))
p.push('SELECT');
cb( q, p );
q = q.SELECT;
traverseFrom( q.from, callback, p.concat(['from']) );
}

@@ -620,2 +622,6 @@ else if (q.SET) {

}
if (q.from)
traverseFrom( q.from, callback, p.concat(['from']) );
for (const prop of ['args', 'xpr', 'columns', 'where', 'having']) {

@@ -662,2 +668,20 @@ // all properties which could have sub queries (directly or indirectly)

function forAllElements(artifact, artifactName, cb){
if(artifact.elements) {
cb(artifact, artifact.elements, ['definitions', artifactName, 'elements']);
}
if(artifact.query) {
forAllQueries(artifact.query, (q, p) => {
if(q.SELECT) {
if(q.elements) {
cb(q, q.elements, [...p.slice(0,-1), 'elements']);
} else if(q.$elements) {
cb(q, q.$elements, [...p.slice(0,-1), '$elements']);
}
}
}, ['definitions', artifactName, 'query'])
}
}
/**

@@ -683,3 +707,3 @@ * Returns true if the element has a specific annotation set to the given value.

* @param {CSN.Element} elementCsn
* @param {CSN.Options & CSN.OdataOptions} options EDM specific options
* @param {CSN.Options & CSN.ODataOptions} options EDM specific options
*/

@@ -711,27 +735,111 @@ function isEdmPropertyRendered(elementCsn, options) {

* - For the 'hdbcds' naming convention, this means converting '.' to '::' on
* the border between namespace and top-level artifact.
* the border between namespace and top-level artifact and correctly replacing some '.' with '_'.
* - For the 'plain' naming convention, it means converting all '.' to '_' and uppercasing.
* - For the 'quoted' naming convention, this is just 'artifactName'.
* - For the 'quoted' naming convention, this means correctly replacing some '.' with '_'.
*
* If the old function signature is used - with a namespace as the third argument - the result might be wrong,
* since the '.' -> '_' conversion for quoted/hdbcds is missing.
*
* @param {string} artifactName The name of the artifact
* @param {('plain'|'quoted'|'hdbcds')} namingConvention The naming convention to use
* @param {string} [namespace] The namespace of the artifact
* @param {CSN.Model|string|undefined} csn
* @returns {string} The resulting database name for (absolute) 'artifactName', depending on the current naming convention.
*/
function getArtifactDatabaseNameOf(artifactName, namingConvention, namespace = undefined) {
if (namingConvention === 'hdbcds') {
if (namespace) {
return `${namespace}::${artifactName.substring(namespace.length + 1)}`;
function getArtifactDatabaseNameOf(artifactName, namingConvention, csn) {
if(csn && typeof csn === 'object' && csn.definitions)
if (namingConvention === 'quoted' || namingConvention === 'hdbcds') {
return getResultingName(csn, namingConvention, artifactName);
}
return artifactName;
else if (namingConvention === 'plain') {
return artifactName.replace(/\./g, '_').toUpperCase();
} else {
throw new Error('Unknown naming convention: ' + namingConvention);
}
else {
const namespace = csn;
console.error(`This invocation of "getArtifactCdsPersistenceName" is deprecated, as it doesn't produce correct output with definition names containing dots - please provide a CSN as the third parameter.`);
if (namingConvention === 'hdbcds') {
if (namespace) {
return `${namespace}::${artifactName.substring(namespace.length + 1)}`;
}
return artifactName;
}
else if (namingConvention === 'plain') {
return artifactName.replace(/\./g, '_').toUpperCase();
}
else if (namingConvention === 'quoted') {
return artifactName;
}
else {
throw new Error('Unknown naming convention: ' + namingConvention);
}
}
else if (namingConvention === 'plain') {
return artifactName.replace(/\./g, '_').toUpperCase();
}
else if (namingConvention === 'quoted') {
}
/**
* Get the name that the artifact definition has been rendered as - except for plain, there we just return the name as-is.
* Without quoting/escaping stuff.
*
* Example: namespace.context.entity.with.dot
* - plain: namespace.context.entity.with.dot
* - quoted: namespace.context.entity_with_dot
* - hdbcds: namespace::context.entity_with_dot
*
* @param {CSN.Model} csn CSN model
* @param {string} namingMode Naming mode to use
* @param {string} artifactName Artifact name to use
* @returns {string} The resulting name
*/
function getResultingName(csn, namingMode, artifactName) {
if (namingMode === 'plain' || artifactName.indexOf('.') === -1)
return artifactName;
const namespace = getNamespace(csn, artifactName);
// Walk from front to back until we find a non-namespace/context
// and join everything we've seen until that point with ., the rest
// with _ (and the namespace with :: for hdbcds naming)
const stopIndex = namespace ? namespace.split('.').length : 0;
const parts = artifactName.split('.');
const realParts = getUnderscoredName(stopIndex, parts, csn);
const name = realParts ? realParts.join('.') : artifactName;
return (namespace && namingMode === 'hdbcds') ? `${namespace}::${name.slice(namespace.length + 1)}` : name;
}
/**
* Get the suffix and prefix part - with '.' join for prefix, '_' for suffix.
* We determine when to start using '_' by walking from front to back until we find
* the first shadowing definition that is not a namespace, context or service.
*
* Anything following is joined by '_'.
*
*
* @param {number} startIndex Index to start looking at the parts - used to skip the namespace
* @param {string[]} parts Parts of the name, split at .
* @param {CSN.Model} csn
* @returns {string[]|null} Array of at most 2 strings: if both: [prefix, suffix], otherwise just one - or null
*/
function getUnderscoredName(startIndex, parts, csn) {
for (let i = startIndex; i < parts.length; i++) {
const namePart = parts.slice(0, i).join('.');
const art = csn.definitions[namePart];
if (art && !(art.kind === 'namespace' || art.kind === 'context' || art.kind === 'service')) {
const prefix = parts.slice(0, i - 1).join('.');
const suffix = parts.slice(i - 1).join('_');
const result = [];
if (prefix)
result.push(prefix);
if (suffix)
result.push(suffix);
return result;
}
}
else {
throw new Error('Unknown naming convention: ' + namingConvention);
}
return null;
}

@@ -862,2 +970,430 @@

const _dependencies = Symbol('_dependencies');
const _dependents = Symbol('_dependents');
/**
* Calculate the hard dependencies between artifacts (as needed to ensure the correct view order).
* Only works on A2Jed HANA CSN!
*
* _dependents: All artifacts that depend on this artifact (because they have a ref that points to it)
* _dependencies: All artifacts this artifact depends on (because it has a ref to it)
*
* @param {object} csn A CSN to enrich in-place
* @returns {object} CSN with _dependents/_dependencies set, "cleanup" function, _dependents/_dependencies Symbol used
*/
function setDependencies( csn ) {
const cleanup = [];
const { artifactRef } = csnRefs(csn);
forEachDefinition(csn, (artifact, artifactName) => {
if(getNormalizedQuery(artifact).query) {
initDependencies(artifact);
forAllQueries(getNormalizedQuery(artifact).query, (query) => {
if(query.SELECT && query.SELECT.from) {
if(query.SELECT.from.args) {
handleArgs(artifact, artifactName, query.SELECT.from.args);
} else {
if(typeof query.SELECT.from === 'string' || query.SELECT.from.ref )
handleDependency(artifactRef(query.SELECT.from), artifact, artifactName);
}
}
}, ['definitions', artifactName, (artifact.projection ? 'projection' : 'query')])
}
})
return {cleanup, csn, _dependents, _dependencies};
function handleArgs(artifact, artifactName, args){
for(let arg of args){
if (arg.args) {
handleArgs(artifact, artifactName, arg.args);
} else if (arg.ref) {
handleDependency(artifactRef(arg), artifact, artifactName)
}
}
}
function handleDependency(dependency, dependant, dependantName) {
dependant[_dependencies].add(dependency);
initDependents(dependency);
dependency[_dependents][dependantName] = dependant;
}
function initDependents(obj){
if(!obj[_dependents]) {
obj[_dependents] = Object.create(null);
cleanup.push(() => delete obj[_dependents]);
}
}
function initDependencies(obj){
if(!obj[_dependencies]) {
obj[_dependencies] = new Set();
cleanup.push(() => delete obj[_dependencies]);
}
}
}
/**
* If the artifact is either abstract or assigned '@cds.persistence.skip' it
* never reaches the Database layer.
*
* @param {CSN.Artifact} art
* @returns {boolean}
*/
function isPersistedOnDatabase(art) {
return !([ 'entity', 'view' ].includes(art.kind) && (art.abstract || hasBoolAnnotation(art, '@cds.persistence.skip')));
}
/**
* Central generated by cds-compiler string generator function without further decoration
* for unified tagging of generated content
*
* @returns {string} String containing compiler version that was used to generate content
*/
function generatedByCompilerVersion() {
return `generated by cds-compiler version ${version}`;
}
/**
* Return the projection to look like a query.
*
* @param {CSN.Artifact} art Artifact with a query or a projection
* @returns {object} Object with a query property.
*/
function getNormalizedQuery(art) {
if (art.projection) {
return { query: { SELECT: art.projection } };
}
return art;
}
/**
* Merge multiple 'options' objects (from right to left, i.e. rightmost wins). Structured option values are
* merged deeply. Structured option value from the right may override corresponding bool options on the left,
* but no other combination of struct/scalar values is allowed. Array options are not merged, i.e. their
* content is treated like scalars.
* Returns a new options object.
*
* @param {...CSN.Options} optionsObjects
* @return {CSN.Options}
*/
function mergeOptions(...optionsObjects) {
let result = {};
for (const options of optionsObjects) {
if (options)
result = mergeTwo(result, options, 'options');
}
// Use original "messages"
const msgOptions = optionsObjects.reverse().find(opt => opt && Array.isArray(opt.messages));
if (msgOptions) {
result.messages = msgOptions.messages;
}
return result;
// Recursively used for scalars, too
function mergeTwo(left, right, name) {
let result;
// Copy left as far as required
if (Array.isArray(left)) {
// Shallow-copy left array
result = left.slice();
} else if (isObject(left)) {
// Deep-copy left object (unless empty)
result = Object.keys(left).length ? mergeTwo({}, left, name) : {};
} else {
// Just use left scalar
result = left;
}
// Check against improper overwriting
if (isObject(left) && !Array.isArray(left) && (Array.isArray(right) || isScalar(right))) {
throw new Error(`Cannot overwrite structured option "${name}" with array or scalar value`);
}
if ((isScalar(left) && typeof left !== 'boolean' || Array.isArray(left)) && isObject(right) && !Array.isArray(right)) {
throw new Error(`Cannot overwrite non-boolean scalar or array option "${name}" with structured value`);
}
// Copy or overwrite properties from right to left
if (Array.isArray(right)) {
// Shallow-copy right array
result = right.slice();
} else if (isObject(right)) {
// Object overwrites undefined, scalars and arrays
if (result === undefined || isScalar(result) || Array.isArray(result)) {
result = {};
}
// Deep-copy right object into result
for (let key of Object.keys(right)) {
result[key] = mergeTwo(result[key], right[key], `${name}.${key}`);
}
} else {
// Right scalar wins (unless undefined)
result = (right !== undefined) ? right : result;
}
return result;
}
// Return true if 'o' is a non-null object or array
function isObject(o) {
return typeof o === 'object' && o !== null
}
// Return true if 'o' is a non-undefined scalar
function isScalar(o) {
return o !== undefined && !isObject(o);
}
}
// Return the name of the top-level artifact surrounding the artifact 'name'
// in 'model'.
// We define "top-level artifact" to be an artifact that has either no parent or only
// ancestors of kind 'namespace'. Note that it is possible for a non-top-level artifact
// to have a namespace as parent and e.g. a context as grandparent (weird but true).
// Will return the artifact 'name' if it is a top-level artifact itself, and 'undefined'
// if there is no artifact surrounding 'name' in the model
// TODO: to be checked by author: still intended behaviour with 'cds' prefix?
// FIXME: This only works with namespace-hacking, i.e. adding them as artifacts...
function getTopLevelArtifactNameOf(name, model) {
let dotIdx = name.indexOf('.');
if (dotIdx == -1) {
// No '.' in the name, i.e. no parent - this is a top-level artifact (if it exists)
return model.definitions[name] ? name : undefined;
}
// If the first name part is not in the model, there is nothing to find
if (!model.definitions[name.substring(0, dotIdx)]) {
return undefined;
}
// Skip forward through '.'s until finding a non-namespace
while (dotIdx != -1 && (!model.definitions[name.substring(0, dotIdx)] || model.definitions[name.substring(0, dotIdx)].kind === 'namespace')) {
dotIdx = name.indexOf('.', dotIdx + 1);
}
if (dotIdx == -1) {
// This is a top-level artifact
return name;
}
// The skipped part of 'name' is the top-level artifact name
return name.substring(0, dotIdx);
}
/**
* If the artifact with the name given is part of a context (or multiple), return the top-most context.
* Else, return the artifact itself. Namespaces are not of concern here.
*
* @param {string} artifactName Name of the artifact
* @param {CSN.Model} csn
* @returns {string} Name of the root
*/
function getRootArtifactName(artifactName, csn) {
const parts = artifactName.split('.');
if (parts.length === 1)
return artifactName;
let seen = getNamespace(csn, artifactName) || '';
const startIndex = (seen === '') ? 0 : seen.split('.').length;
for (let i = startIndex; i < parts.length; i++) {
if (seen === '')
seen = parts[i];
else
seen = `${seen}.${parts[i]}`;
const art = csn.definitions[seen];
// Our artifact seems to be contained in this context
if (art && art.kind === 'context')
return seen;
}
// Our artifact is a root artifact itself
return seen;
}
// Return the last part of 'name'.
// Examples:
// 'foo.bar.wiz' => 'wiz'
// 'foo' => 'foo';
// 'foo::bar' => 'bar'
function getLastPartOf(name) {
return name.substring(name.search(/[^.:]+$/));
}
// Return the last part of reference array 'ref'
// Examples:
// ['foo.bar', 'wiz'] => 'wiz'
// ['foo.bar.wiz'] => 'wiz'
// ['foo'] => 'foo';
// ['foo::bar'] => 'bar'
function getLastPartOfRef(ref) {
let lastPathStep = ref[ref.length - 1];
return getLastPartOf(lastPathStep.id || lastPathStep);
}
// Return the name of the parent artifact of the artifact 'name' or
// '' if there is no parent.
function getParentNameOf(name) {
return name.substring(0, name.lastIndexOf('.'));
}
// Return an array of parent names of 'name' (recursing into grand-parents)
// Examples:
// 'foo.bar.wiz' => [ 'foo.bar', 'foo' ]
// 'foo' => []
// 'foo::bar.wiz' => 'foo::bar'
// 'foo::bar' => []
function getParentNamesOf(name) {
let remainder = name.slice(0, -getLastPartOf(name).length);
if (remainder.endsWith('.')) {
let parentName = remainder.slice(0, -1);
return [parentName, ...getParentNamesOf(parentName)];
} else {
return [];
}
}
// Copy all annotations from 'fromNode' to 'toNode'. Overwrite existing ones only if 'overwrite' is true
function copyAnnotations(fromNode, toNode, overwrite=false) {
// Ignore if no toNode (in case of errors)
if (!toNode) {
return;
}
for (let prop in fromNode) {
if (!Object.hasOwnProperty.call( fromNode, prop ))
continue;
if (prop.startsWith('@')) {
if (toNode[prop] == undefined || overwrite) {
toNode[prop] = fromNode[prop];
}
}
}
}
// Return true if 'type' is a composition type
function isComposition(type) {
if (!type)
return type;
if (type._artifact)
type = type._artifact.name;
return type.absolute === 'cds.Composition';
}
function isAspect(node) {
return node && node.kind === 'aspect';
}
// For each property named 'path' in 'node' (recursively), call callback(path, node)
function forEachPath(node, callback) {
if (node === null || typeof node !== 'object') {
// Primitive node
return;
}
for (let name in node) {
if (!Object.hasOwnProperty.call( node, name ))
continue;
// If path found within a non-dictionary, call callback
if (name === 'path' && Object.getPrototypeOf(node)) {
callback(node.path, node);
}
// Descend recursively
forEachPath(node[name], callback);
}
}
// Add an annotation with absolute name 'absoluteName' (including '@') and string value 'theValue' to 'node'
function addBoolAnnotationTo(absoluteName, theValue, node) {
// Sanity check
if (!absoluteName.startsWith('@')) {
throw Error('Annotation name should start with "@": ' + absoluteName);
}
// Assemble the annotation
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
};
}
}
// Add an annotation with absolute name 'absoluteName' (including '@') and array 'theValue' to 'node'
function addArrayAnnotationTo(absoluteName, theValue, node) {
// Sanity check
if (!absoluteName.startsWith('@')) {
throw Error(`Annotation name should start with "@": ${ absoluteName}`);
}
// Assemble the annotation
if(isAnnotationAssignable(node, absoluteName)) {
node[absoluteName] = {
name: {
absolute: absoluteName.substring(1),
location: node.location, // inherit location from main element
},
val: theValue,
literal: 'array',
location: node.location, // inherit location from main element
};
}
}
/**
* 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);
}
/**
* Return true if the artifact has a valid, truthy persistence.exists/skip annotation
*
* @param {CSN.Artifact} artifact
* @returns {boolean}
*/
function hasValidSkipOrExists(artifact) {
return (artifact.kind === 'entity' || artifact.kind === 'view') &&
(hasBoolAnnotation(artifact, '@cds.persistence.exists', true) || hasBoolAnnotation(artifact, '@cds.persistence.skip', true))
}
/**
* Get the namespace part of the artifact name - not the whole prefix, just the part caused by namespaces.
*
* @param {CSN.Model} csn CSN model
* @param {string} artifactName artifact name to get the namespace for
* @returns {string | null} The namespace name
*/
function getNamespace(csn, artifactName) {
const parts = artifactName.split('.');
let seen = parts[0];
const art = csn.definitions[seen];
// First step is not a namespace (we faked those in the CSN)
// No subsequent step can be a namespace then
if (art && art.kind !== 'namespace')
return null;
for (let i = 1; i < parts.length; i++) {
// This was definitely a namespace so far
const previousArtifactName = seen;
seen = `${seen}.${parts[i]}`;
// This might not be - if it isn't, return the result.
const currentArtifact = csn.definitions[seen];
if (currentArtifact && currentArtifact.kind !== 'namespace')
return previousArtifactName;
}
// We came till here - so the full artifactName is a namespace
return artifactName;
}
module.exports = {

@@ -874,7 +1410,29 @@ getUtils,

forAllQueries,
forAllElements,
hasBoolAnnotation,
isEdmPropertyRendered,
getArtifactDatabaseNameOf,
getResultingName,
getUnderscoredName,
getElementDatabaseNameOf,
applyTransformations
applyTransformations,
setDependencies,
isPersistedOnDatabase,
generatedByCompilerVersion,
getNormalizedQuery,
mergeOptions,
getTopLevelArtifactNameOf,
getRootArtifactName,
getLastPartOfRef,
getParentNamesOf,
getParentNameOf,
getLastPartOf,
copyAnnotations,
isComposition,
isAspect,
forEachPath,
addBoolAnnotationTo,
addArrayAnnotationTo,
hasValidSkipOrExists,
getNamespace
};

@@ -16,18 +16,42 @@ // Make internal properties of the XSN / augmented CSN visible

class NOT_A_DICTIONARY {} // used for consol.log display
function locationString( loc ) {
return (typeof loc === 'object' && loc && loc.start)
if (loc instanceof Array)
return loc.map( locationString );
if (loc == null)
return '';
return (typeof loc === 'object' && loc.file)
? msg.locationString(loc)
: (loc === null ? 'null' : typeof loc) + ':' + msg.locationString(loc);
: (typeof loc) + ':' + msg.locationString(loc);
}
var unique_id = 0;
// some (internal) kinds are normally represented as links
const kindsRepresentedAsLinks = {
// represent SELECTs in query / SET-args property as link:
select: (art, parent) => art._main && parent !== art._main.$queries,
// represent table alias in from / join-args property as link:
$tableAlias: (art, parent) => art._parent && parent !== art._parent.$tableAliases,
// represent table alias in JOIN node as link:
$navElement: (art, parent) => art._parent && parent !== art._parent.elements,
// represent mixin in $tableAliases as link:
mixin: (art, parent) => art._parent && parent !== art._parent.mixin,
// represent $projection as link, as it is just another search name for $self:
$self: (_a, _p, name) => name !== '$self',
}
function revealInternalProperties( model, name ) {
var unique_id = 0;
var transformers = {
const transformers = {
messages: m => m,
name: shortenName,
location: locationString,
$parens: locationString, // array
options: revealOptions,
messages: reveal,
sources: dictionary,
artifacts: artifactDictionary,
definitions: artifactDictionary,
elements: (e,n) => (n.self ? artifactDictionary(e) : dictionary(e)),
vocabularies: dictionary,
elements,
columns,

@@ -38,11 +62,8 @@ actions: dictionary,

foreignKeys: dictionary,
exclude: dictionary,
excludingDict: dictionary,
struct: dictionary,
queries: () => '<deprecated>',
$queries: n => n.map( revealQuery ),
$tableAliases: tableAliases,
$navigation: dictionary,
$keysNavigation: dictionary,
$combined: artifactDictionary,
$dictOrderBy: artifactDictionary,
mixin: dictionary,
args: dictionary,
$tableAliases: dictionary,
_combined: artifactDictionary,
$layerNumber: n => n,

@@ -52,36 +73,10 @@ $extra: e => e,

_layerExtends: layerExtends,
_parent: artifactIdentifier,
_outer: artifactIdentifier,
_main: artifactIdentifier,
_block: artifactIdentifier,
_artifact: artifactIdentifier,
_navigation: artifactIdentifier,
_origTarget: artifactIdentifier,
_tableAlias: artifactIdentifier,
_projections: artifactIdentifier, // array
_redirected: artifactIdentifier, // array
_entities: artifactIdentifier, // array
_localized: artifactIdentifier, // or true
_assocSources: artifactIdentifier, // array
_oldAssocSources: artifactIdentifier, // array
$compositionTargets: d => d, // dictionary( boolean )
_upperAspects: artifactIdentifier, // array
_ancestors: artifactIdentifier, // array
_descendants: artifactDictionary, // dict of array
_$queryNode: n => (n.location && { location: locationString( n.location ) }),
_$next: artifactIdentifier,
_finalType: artifactIdentifier,
_leadingQuery: artifactIdentifier,
_source: artifactIdentifier,
_extend: (a) => reveal( a, null ),
_annotate: (a) => reveal( a, null ),
_extend: reveal,
_annotate: reveal,
_deps: dependencyInfo,
_xref: xrefInfo,
_incomplete: primOrString,
_shadowed: (a) => reveal( a, null ),
_status: primOrString, // is a string anyway
_service: artifactIdentifier,
_firstAliasInFrom: artifactIdentifier,
}
return reveal( parseXsnPath(name, model) );
unique_id = 1;
return revealXsnPath(name, model);

@@ -110,7 +105,12 @@ // Returns the desired artifact/dictionary in the XSN.

// `name.space/S/E/elements/a/type/scope/`
function parseXsnPath(path, xsn) {
function revealXsnPath(path, xsn) {
if (!path || path === '+')
return xsn;
return reveal( xsn );
path = path.split('/');
if (path.length === 1) {
return reveal( xsn.definitions[path] );
}
// with the code below, we might miss the right transformer function
path.unshift('definitions');

@@ -121,3 +121,3 @@

xsn = xsn[segment]
else if (segment)
else if (segment) // huh, this should be a call error
throw new Error(`Raw Output: Path segment "${ segment }" could not be found. Path: ${ JSON.stringify(path) }!"`)

@@ -128,73 +128,19 @@ }

obj[propName] = xsn;
return obj;
return reveal( obj );
}
function artifactIdentifier( node, parent ) {
if (node instanceof Array)
return node.map( artifactIdentifier );
if (!(node instanceof Object))
return primOrString( node );
if (node.__unique_id__ == null)
Object.defineProperty( node, '__unique_id__', { value: ++unique_id } );
let outer = '##' + node.__unique_id__;
if (node._outer) {
outer = (node._outer.items === node) ? '/items'
: (node._outer.returns === node) ? '/returns' : '/returns/items';
node = node._outer;
function shortenName( node, parent ) {
const name = reveal( node, parent );
if (name && typeof name === 'object' && name.absolute) {
const text = artifactIdentifier( parent );
delete name.absolute;
delete name.select;
delete name.action;
delete name.parameter;
delete name.alias;
delete name.element;
name['-->'] = text;
}
if (node === parent)
return 'this';
if (node.kind === 'source')
return 'source:' + quoted( node.filename );
if (node.kind === '$magicVariables')
return '$magicVariables';
if (!node.name) {
try{
return JSON.stringify(node);
}
catch (e) {
return e.toString();
}
}
switch (node.kind) {
case undefined: // TODO: remove this `returns` property for actions
return (node._artifact && node._artifact.kind)
? artifactIdentifier( node._artifact )
: JSON.stringify(node.name);
case 'builtin':
return '$magicVariables/' + msg.artName(node);
case 'source':
case 'using':
return 'source:' + quoted( node.location && node.location.filename ) +
'/using:' + quoted( node.name.id )
default: {
let kind = (node._main)
? node._main.kind
: (node.kind === 'block')
? node._parent && node._parent.kind
: node.kind;
return (kind || '<kind>') + ':' + msg.artName( node ) + outer +
// todo: get rid of $renamed / do the oData renaming late on the CSN
(node.name.$renamed ? '-original:' + quoted( node.name.$renamed ) : '');
}
}
return name;
}
function quoted( name, undef = '<undefined>' ) {
return (typeof name === 'number')
? name
: name ? '"' + name.replace( /"/g, '""' ) + '"' : undef;
}
function artifactDictionary( node ) {
if (node == null || typeof node !== 'object' || Object.getPrototypeOf(node)
|| !model.definitions || node === model.definitions )
return dictionary( node ); // no dictionary or no definitions section
let dict = Object.create(null);
for (let name in node) {
dict[name] = artifactIdentifier( node[name] );
}
return dict;
}
function dependencyInfo( deps ) {

@@ -208,11 +154,6 @@ if (!(deps instanceof Array))

function xrefInfo( xref ) {
return xref.map( d => artifactIdentifier( d.user ) );
}
function layerExtends( dict ) {
let r = Object.create(null);
// let proto = Object.getPrototypeOf(dict);
// if (proto)
// r.__proto__ = proto.realname || proto;
const r = Object.create( Object.getPrototypeOf(dict)
? NOT_A_DICTIONARY.prototype
: Object.prototype );
for (let name in dict)

@@ -223,39 +164,59 @@ r[name] = true;

function primOrString( node ) {
if (node === null || typeof node !== 'object')
// node instanceof Object would be false for dict
return node
if (node instanceof Array)
return node.map( primOrString );
if (node instanceof Object)
return '' + node;
else
return '<dict>';
function columns( nodes, query ) {
// If we will have specified elements, we need another test to see columns in --parse-cdl
return nodes && nodes.map( c => (c._parent && c._parent.elements)
? artifactIdentifier( c, query )
: reveal( c, nodes ) );
}
function columns( nodes ) {
// If we will have specified elements, we need another test to see columns in --parse-cdl
return nodes && nodes.map( c => (c._parent && c._parent.elements) ? artifactIdentifier( c ) : reveal( c ) );
function elements( dict, parent ) {
// do not display elements of leading query as they are the same as the view elements:
return (parent._main && parent._main._leadingQuery === parent)
? '{ ... }'
: dictionary( dict, parent );
}
function dictionary( node ) {
return reveal( node, '__proto__' );
function revealOptions( node, parent ) {
return (parent === model || node !== model.options) ? reveal( node, parent ) : '{ ... }';
}
function tableAliases( dict, query ) {
return query.join ? artifactDictionary( dict ) : dictionary( dict );
function artifactDictionary( node, parent ) {
if (!node || typeof node !== 'object' || !model.definitions || parent === model )
return dictionary( node ); // no dictionary or no definitions section
const dict = Object.create( Object.getPrototypeOf(node)
? NOT_A_DICTIONARY.prototype
: Object.prototype );
for (let name in node) {
const art = node[name];
dict[name] = (art.kind !== 'using')
? artifactIdentifier( art )
: reveal( art, parent );
}
return dict;
}
function revealQuery( node ) {
return reveal( node, undefined, undefined, '__neverEqualKind' );
function dictionary( node ) {
if (!node || typeof node !== 'object')
return primOrString( node );
if (node instanceof Array) // with args
return node.map( n => reveal( n, node ) );
// Make unexpected prototype visible with node-10+:
const r = Object.create( Object.getPrototypeOf(node)
? NOT_A_DICTIONARY.prototype
: Object.prototype );
for (let prop of Object.getOwnPropertyNames( node )) { // also non-enumerable
r[prop] = reveal( node[prop], node, prop );
}
return r;
}
function revealOptions( node ) {
return reveal( node.messages
? Object.assign( {}, node, { messages: node.messages.length } )
: node );
function revealNonEnum( node, parent ) {
if (!(node && node instanceof Object))
return primOrString( node );
if (Object.getPrototypeOf( node ))
return artifactIdentifier( node, parent );
return artifactDictionary( node, parent );
}
function reveal( node, protoProp, _array, identifierKind = 'query' ) {
// warning: 'protoProp' bound via map()
function reveal( node, parent, prop ) {
if (node == null || typeof node !== 'object' )

@@ -265,24 +226,18 @@ // node instanceof Object would be false for dict

if (node instanceof Array)
return node.map( reveal );
return node.map( n => reveal( n, node ) );
let proto = Object.getPrototypeOf(node);
if (proto !== null && proto !== Object.prototype)
return node;
if (proto && node.kind === identifierKind && !node.join)
return artifactIdentifier( node );
const asLinkTest = kindsRepresentedAsLinks[ node.kind ];
if (asLinkTest && asLinkTest( node, parent, prop ))
return artifactIdentifier( node, parent );
let r = Object.create( proto );
let r = Object.create( Object.getPrototypeOf( node ) );
// property to recognize === objects
if (proto && node.kind && node.__unique_id__ == null)
if (node.kind && node.__unique_id__ == null)
Object.defineProperty( node, '__unique_id__', { value: ++unique_id } );
for (let prop of Object.getOwnPropertyNames( node )) { // also non-enumerable
let item = node[prop];
let func =
(proto === null || protoProp === null ? reveal : transformers[prop]) ||
(Object.prototype.propertyIsEnumerable.call( node, prop ) ? reveal : primOrString);
r[prop] = func( item, node );
const func = transformers[prop] ||
({}.propertyIsEnumerable.call( node, prop ) ? reveal : revealNonEnum);
r[prop] = func( node[prop], node );
}
if (proto && protoProp === '__proto__')
Object.defineProperty( r, protoProp, { enumerable: true, value: '' + proto } );
return r;

@@ -292,2 +247,76 @@ }

module.exports = revealInternalProperties;
function artifactIdentifier( node, parent ) {
if (node instanceof Array)
return node.map( a => artifactIdentifier( a, node ) );
if (unique_id && node.__unique_id__ == null)
Object.defineProperty( node, '__unique_id__', { value: ++unique_id } );
let outer = unique_id ? '##' + node.__unique_id__ : '';
if (node._outer) {
outer = (node._outer.items === node) ? '/items'
: (node._outer.returns === node) ? '/returns' : '/returns/items';
node = node._outer;
}
if (node === parent)
return 'this';
if (node.kind === 'source')
return 'source:' + quoted( node.location.file );
if (node.kind === '$magicVariables')
return '$magicVariables';
if (!node.name) {
try {
return (locationString( node.location ) || '') + '##' + node.__unique_id__;
// return JSON.stringify(node);
}
catch (e) {
return e.toString();
}
}
switch (node.kind) {
case undefined:
return (node._artifact && node._artifact.kind)
? artifactIdentifier( node._artifact )
: JSON.stringify(node.name);
case 'builtin':
return '$magicVariables/' + msg.artName(node);
case 'source':
case 'using':
return 'source:' + quoted( node.location && node.location.file ) +
'/using:' + quoted( node.name.id )
default: {
return ((node._main || node).kind || '<kind>') + ':' + msg.artName( node ) + outer;
}
}
}
function primOrString( node ) {
if (node === null || typeof node !== 'object')
// node instanceof Object would be false for dict
return node
if (node instanceof Array)
return node.map( primOrString );
if (node instanceof Object)
return '' + node;
else
return '<dict>';
}
function quoted( name, undef = '‹undefined›' ) {
return (typeof name === 'number')
? name
: name ? '“' + name + '”' : undef;
}
// To be used for tracing, e.g. by
// require('../model/revealInternalProperties').log(model, 'E_purposes')
function logXsnModel( model, name ) {
console.log( require('util').inspect( revealInternalProperties( model, name ), false, null ) );
}
// To be used for tracing, e.g. by
// console.log(require('../model/revealInternalProperties').ref(type._artifact))
function xsnRef( node ) {
unique_id = 0;
return artifactIdentifier( node );
}
module.exports = { reveal: revealInternalProperties, log: logXsnModel, ref: xsnRef };

@@ -36,2 +36,32 @@ 'use strict';

return function compareArtifacts(artifact, name) { // (, topKey, path) topKey == 'definitions'
function addElements() {
const elements = {};
forEachMember(artifact, getElementComparator(otherArtifact, elements));
if (Object.keys(elements).length > 0) {
elementAdditions.push({
extend: name,
elements: elements
});
}
}
function removeOrChangeElements() {
const removedElements = {};
const changedElements = {};
const modification = { migrate: name };
forEachMember(otherArtifact, getElementComparator(artifact, removedElements));
if (Object.keys(removedElements).length > 0) {
modification.remove = removedElements;
}
forEachMember(artifact, getElementComparator(otherArtifact, null, changedElements));
if (Object.keys(changedElements).length > 0) {
modification.change = changedElements;
}
if (modification.remove || modification.change) {
elementChanges.push(modification);
}
}
const otherArtifact = otherModel.definitions[name];

@@ -73,33 +103,2 @@ const isPersisted = isPersistedAsTable(artifact);

}
function addElements() {
const elements = {};
forEachMember(artifact, getElementComparator(otherArtifact, elements));
if (Object.keys(elements).length > 0) {
elementAdditions.push({
extend: name,
elements: elements
});
}
}
function removeOrChangeElements() {
const removedElements = {};
const changedElements = {};
const modification = { migrate: name };
forEachMember(otherArtifact, getElementComparator(artifact, removedElements));
if (Object.keys(removedElements).length > 0) {
modification.remove = removedElements;
}
forEachMember(artifact, getElementComparator(otherArtifact, null, changedElements));
if (Object.keys(changedElements).length > 0) {
modification.change = changedElements;
}
if (modification.remove || modification.change) {
elementChanges.push(modification);
}
}
};

@@ -112,3 +111,3 @@ }

&& !artifact.abstract
&& (!artifact.query || hasBoolAnnotation(artifact, '@cds.persistence.table'))
&& (!artifact.query && !artifact.projection || hasBoolAnnotation(artifact, '@cds.persistence.table'))
&& !hasBoolAnnotation(artifact, '@cds.persistence.skip')

@@ -130,3 +129,3 @@ && !hasBoolAnnotation(artifact, '@cds.persistence.exists');

}
if (otherElement.type !== element.type || JSON.stringify(otherElement) !== JSON.stringify(element)) {
if (relevantTypeChange(element.type, otherElement.type) || typeParametersChanged(element, otherElement)) {
// Type or parameters, e.g. association target, changed.

@@ -145,2 +144,46 @@ changedElements[name] = changedElement(element, otherElement);

function relevantTypeChange(type, otherType) {
return otherType !== type && ![type, otherType].every(t => ['cds.Association', 'cds.Composition'].includes(t));
}
/**
* Returns whether two things are deeply equal.
* Function-type things are compared in terms of identity,
* object-type things in terms of deep equality of all of their properties,
* all other things in terms of strict equality (===).
*
* @param a {any} first thing
* @param b {any} second thing
* @param include {function} function of a key and a depth, returning true if and only if the given key at the given depth is to be included in comparison
* @param depth {number} the current depth in property hierarchy below each of the original arguments (positive, counting from 0; don't set)
* @returns {boolean}
*/
function deepEqual(a, b, include = () => true, depth = 0) {
function isObject(x) {
return x !== null && typeof x === 'object';
}
function samePropertyCount() {
return Object.keys(a).length === Object.keys(b).length;
}
function allPropertiesEqual() {
return Object.keys(a).reduce((prev, key) => prev && (!include(key, depth) || deepEqual(a[key], b[key], include, depth + 1)), true);
}
return isObject(a)
? isObject(b)
? samePropertyCount() && allPropertiesEqual()
: false
: a === b;
}
/**
* Returns whether any type parameters differ between two given elements. Ignores whether types themselves differ (`type` property).
* @param element {object} an element
* @param otherElement {object} another element
* @returns {boolean}
*/
function typeParametersChanged(element, otherElement) {
return !deepEqual(element, otherElement, (key, depth) => !(depth === 0 && key === 'type'));
}
function changedElement(element, otherElement) {

@@ -154,3 +197,4 @@ return {

module.exports = {
compareModels
compareModels,
deepEqual
};

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

// into an options object) and by the API functions (to verify options)
let optionProcessor = createOptionProcessor();
const optionProcessor = createOptionProcessor();

@@ -30,6 +30,3 @@ // General options

.option(' --beta <list>')
.option(' --old-transformers')
.option(' --new-resolve')
.option(' --dependent-autoexposed')
.option(' --long-autoexposed')
.option(' --deprecated <list>')
.option(' --hana-flavor')

@@ -41,3 +38,3 @@ .option(' --direct-backend')

.option(' --localized-without-coalesce')
.option('--length <length>')
.option(' --length <length>')
.positionalArgument('<files...>')

@@ -71,7 +68,2 @@ .help(`

-o, --out <dir> Place generated files in directory <dir>, default is "-" for <stdout>
--dependent-autoexposed If the root entity of dependent autoexposed entities
(for managed compositions, texts entity), is explicitly exposed
with a certain name in a service, use that name as prefix
for the name of the dependent autoexposed entities
(recommended option, but incompatible to switch it on by default).
--lint-mode Generate nothing, just produce messages if any (for use by editors)

@@ -97,13 +89,16 @@ --fuzzy-csn-error Report free-style CSN properties as errors

Valid values are:
foreignKeyConstraints
addTextsLanguageAssoc
subElemRedirections
keyRefError
odataProxies
hanaAssocRealCardinality
originalKeysForTemporal
odataDefaultValues
mapAssocToJoinCardinality
dontRenderVirtualElements
--old-transformers Use the old transformers that work on XSN instead of CSN
--new-resolve Use new module resolver. Will become the default in the future.
ignoreAssocPublishingInUnion
--deprecated <list> Comma separated list of deprecated options.
Valid values are:
noElementsExpansion
v1KeysForTemporal
parensAsStrings
projectionAsQuery
renderVirtualElements
unmanagedUpInComponent
createLocalizedViews
--hana-flavor Compile with backward compatibility for HANA CDS (incomplete)

@@ -120,15 +115,13 @@ --parse-only Stop compilation after parsing and write result to <stdout>

Backward compatibility options (deprecated, do not use)
--long-autoexposed Produce long names (with underscores) for autoexposed entities
Commands
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
parseCdl [options] <file> Generate a CSN that is close to the CDL source.
explain <message-id> Explain a compiler message.
toRename [options] <files...> (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
Q, toSql [options] <files...> Generate SQL DDL statements
toCsn [options] <files...> (default) Generate original model as CSN
parseCdl [options] <file> Generate a CSN that is close to the CDL source.
explain <message-id> Explain a compiler message.
toRename [options] <files...> (internal) Generate SQL DDL rename statements
manageConstraints [options] <files...> (internal) Generate ALTER TABLE statements to
add / modify referential constraints.
`);

@@ -140,3 +133,2 @@

.option('-n, --names <style>', ['plain', 'quoted', 'hdbcds'])
.option('-a, --associations <proc>', ['assocs', 'joins'])
.option(' --render-virtual')

@@ -165,5 +157,2 @@ .option(' --joinfk')

using element names with dots).
-a, --associations <proc> Processing of associations:
assocs : (default) Keep associations in HANA CDS as far as possible
joins : Transform associations to joins
--render-virtual Render virtual elements in views and draft tables

@@ -207,3 +196,2 @@ --joinfk Create JOINs for foreign key accesses

-f, --odata-format <format> Set the format of the identifier rendering
(ignored by '--old-transformers')
flat : (default) Flat type and property names

@@ -236,24 +224,8 @@ structured : (V4 only) Render structured metadata

optionProcessor.command('S, toSwagger')
.option('-h, --help')
.option('-j, --json')
.option('-c, --csn')
.help(`
Usage: cdsc toSwagger [options] <files...>
Generate Swagger (OpenAPI) JSON, or CSN
Options
-h, --help Show this help text
-j, --json (default) Generate OpenAPI JSON output for each service as "<svc>_swagger.json
-c, --csn Generate "swagger_csn.json" with Swagger-preprocessed model
`);
optionProcessor.command('Q, toSql')
.option('-h, --help')
.option('-n, --names <style>', ['plain', 'quoted', 'hdbcds'])
.option('-a, --associations <proc>', ['assocs', 'joins'])
.option(' --render-virtual')
.option(' --joinfk')
.option('-d, --dialect <dialect>', ['hana', 'sqlite'])
.option('-d, --dialect <dialect>', ['hana', 'sqlite', 'plain'])
.option('-u, --user <user>')

@@ -281,16 +253,10 @@ .option('-l, --locale <locale>')

combination with "hana" dialect.
-a, --associations <proc> Processing of associations:
assocs : Keep associations as far as possible. Note that some
associations (e.g. those defined in a mixin and used in
the same view) must always be replaced by joins because of
SQL limitations, and that "assocs" should only be used
with "hana" dialect.
joins : (default) Transform associations to joins
--render-virtual Render virtual elements in views and draft tables
--joinfk Create JOINs for foreign key accesses
-d, --dialect <dialect> SQL dialect to be generated:
plain : (default) Common SQL - no assumptions about DB restrictions
hana : SQL with HANA specific language features
sqlite : (default) Common SQL for sqlite
sqlite : Common SQL for sqlite
-u, --user <user> Value for the "$user" variable
-l, --locale <locale> Value for the "$user.locale" variable in "sqlite" dialect
-l, --locale <locale> Value for the "$user.locale" variable in "sqlite"/"plain" dialect
-s, --src <style> Generate SQL source files as <artifact>.<suffix>

@@ -325,5 +291,45 @@ sql : (default) <suffix> is "sql"

optionProcessor.command('manageConstraints')
.option('-h, --help')
.option('-n, --names <style>', ['plain', 'quoted', 'hdbcds'])
.option('-v, --validated <bool>', ['true', 'false'])
.option('-e, --enforced <bool>', ['true', 'false'])
.option('-s, --src <style>', ['sql', 'hdi'])
.option(' --drop')
.option(' --alter')
.help(`
Usage: cdsc manageConstraints [options] <files...>
(internal, subject to change): Generate SQL DDL ALTER TABLE statements to add / modify
referential constraints on an existing model.
Options
-h, --help Display this help text
-n, --names <style> Assume existing tables were generated with "--names <style>":
plain : (default) Assume SQL tables were flattened and dots were
replaced by underscores
quoted : Assume existing SQL tables and views were named in original
case as in CDL (with dots), but column names were flattened
with underscores
hdbcds : Assume existing SQL tables and column names were produced
as HANA CDS would have generated them from the same CDS source
(like "quoted", but using element names with dots).
-v, --validated <bool> Whether existing records are checked for referential integrity upon constraint activation:
true : (default) VALIDATED
false : NOT VALIDATED
-e, --enforced <bool> Whether future operations on constrained fields are checked for referential integrity:
true : (default) ENFORCED
false : NOT ENFORCED
-s, --src <style> Generate SQL source files as <artifact>.<suffix>
sql : (default) <suffix> is "sql"
hdi : constraint will be generated with <suffix> "hdbconstraint"
--drop Generate "ALTER TABLE <table> DROP CONSTRAINT <constraint>" statements
--alter Generate "ALTER TABLE <table> ALTER CONSTRAINT <constraint>" statements
`);
optionProcessor.command('toCsn')
.option('-h, --help')
.option('-f, --flavor <flavor>', ['client', 'gensrc'])
.option(' --with-localized')
.help(`

@@ -342,2 +348,5 @@ Usage: cdsc toCsn [options] <files...>

backends
Internal options (for testing only, may be changed/removed at any time)
--with-localized Add localized convenience views to the CSN output.
`);

@@ -344,0 +353,0 @@

@@ -11,9 +11,15 @@ /**

const walker = require('../json/walker')
const walker = require('../json/walker');
// database name - uppercase if not quoted
/**
* database name - uppercase if not quoted
*
* @param {string} name Artifact name
*
* @returns {string} Name as it is present on the database
*/
function asDBName(name) {
return (name[0]=='"')
? name
: name.toUpperCase();
return (name[0] === '"')
? name
: name.toUpperCase();
}

@@ -26,7 +32,4 @@

*/
class DuplicateChecker{
constructor(){
this.seenArtifacts;
this.currentArtifact;
class DuplicateChecker {
constructor() {
this.init();

@@ -37,6 +40,4 @@ }

* Initialize the state of the checker.
*
* @memberOf DuplicateChecker
*/
init(){
init() {
this.seenArtifacts = {};

@@ -49,14 +50,15 @@ this.currentArtifact = {};

*
* @param {any} name
* @param {any} location
*
* @memberOf DuplicateChecker
* @param {string} name Rendered artifact name
* @param {CSN.Location} location CSN location of the artifact
* @param {string} modelName CSN artifact name
*/
addArtifact( name, location, modelName ){
const dbname = asDBName(name);
this.currentArtifact = { name, location, elements: {}, modelName };
if(!this.seenArtifacts[dbname])
this.seenArtifacts[dbname] = [this.currentArtifact];
addArtifact( name, location, modelName ) {
const dbName = asDBName(name);
this.currentArtifact = {
name, location, elements: {}, modelName,
};
if (!this.seenArtifacts[dbName])
this.seenArtifacts[dbName] = [ this.currentArtifact ];
else
this.seenArtifacts[dbname].push(this.currentArtifact);
this.seenArtifacts[dbName].push(this.currentArtifact);
}

@@ -67,18 +69,17 @@

*
* @param {any} name
* @param {any} location
* @returns
* @param {string} name Rendered element name
* @param {CSN.Location} location
* @param {string} modelName CSN element name
*
* @memberOf DuplicateChecker
*/
addElement(name,location, modelName){
if(!this.currentArtifact.elements)
addElement(name, location, modelName) {
if (!this.currentArtifact.elements)
return;
const dbname = asDBName(name);
let currentElements = this.currentArtifact.elements;
const element = {name,location, modelName};
if(!currentElements[dbname])
currentElements[dbname] = [element];
const dbName = asDBName(name);
const currentElements = this.currentArtifact.elements;
const element = { name, location, modelName };
if (!currentElements[dbName])
currentElements[dbName] = [ element ];
else
currentElements[dbname].push(element);
currentElements[dbName].push(element);
}

@@ -89,17 +90,28 @@

*
* @memberOf DuplicateChecker
* @param {Function} error Function of makeMessageFunction()
* @param {CSN.Options} options Options used for the compilation
*/
check(signal, error){
check(error, options = null) {
walker.forEach(this.seenArtifacts, (artifactName, artifacts) => {
if(artifacts.length>1) {
artifacts.forEach(artifact => { // report all colliding artifacts
signal(error`Duplicated artifact '${artifactName}', origin: '${artifact.name}'`, ['definitions', artifact.modelName] );
if (artifacts.length > 1) {
artifacts.forEach((artifact) => { // report all colliding artifacts
const collidesWith = this.seenArtifacts[artifactName].find( art => art !== artifact );
let namingMode;
if (options)
namingMode = options.toHana ? options.toHana.names : options.toSql.names;
else
namingMode = 'plain';
error(null, [ 'definitions', artifact.modelName ],
{ name: collidesWith.modelName, prop: namingMode },
'Artifact name containing dots can\'t be mapped to a SQL compliant identifier in naming mode $(PROP) because it conflicts with existing definition $(NAME)');
});
}
artifacts.forEach(artifact => {
artifacts.forEach((artifact) => {
walker.forEach(artifact.elements, (elementName, elements) => {
if(elements.length>1) {
elements.forEach(element => { // report all colliding elements
signal(error`Duplicated element '${element.name}' in artifact '${artifact.name}'`, ['definitions', artifact.modelName,'elements', element.modelName]);
})
if (elements.length > 1) {
elements.forEach((element) => { // report all colliding elements
error(null, [ 'definitions', artifact.modelName, 'elements', element.modelName ],
`Duplicated element '${element.name}' in artifact '${artifact.name}'`);
});
}

@@ -109,3 +121,3 @@ });

});
//clean internal structures
// clean internal structures
this.init();

@@ -112,0 +124,0 @@ }

'use strict';
const { CompilationError, hasErrors, sortMessages } = require('../base/messages');
const { mergeOptions, getTopLevelArtifactNameOf, getParentNameOf, getParentNamesOf } = require('../model/modelUtils');
const { compactModel } = require('../json/to-csn');
const { CompilationError, hasErrors } = require('../base/messages');
const { checkCSNVersion } = require('../json/csnVersion');
const { getUtils } = require('../model/csnUtils');
// Render the augmented CSN 'model' to SQL DDL statements renaming existing tables and their
// columns so that they match the result of "toHana" or "toSql" with the 'plain' option for names.
// Expects the naming convention of the existing tables to be either 'quoted' or 'hdbcds' (default).
// The following options control what is actually generated:
// options : {
// toRename.names : existing names, either 'quoted' or 'hdbcds' (default)
// }
// Return a dictionary of top-level artifacts by their names, like this:
// { "foo" : "RENAME TABLE \"foo\" ...",
// "bar::wiz" : "RENAME VIEW \"bar::wiz\" ..."
// }
function toRenameDdl(model, options) {
// FIXME: This is not up-to-date in regards to the changes to hdbcds/sql quoting etc.
/**
*
* Render the augmented CSN 'model' to SQL DDL statements renaming existing tables and their
* columns so that they match the result of "toHana" or "toSql" with the 'plain' option for names.
* Expects the naming convention of the existing tables to be either 'quoted' or 'hdbcds' (default).
* The following options control what is actually generated:
* options : {
* toRename.names : existing names, either 'quoted' or 'hdbcds' (default)
* }
* Return a dictionary of top-level artifacts by their names, like this:
* { "foo" : "RENAME TABLE \"foo\" ...",
* "bar::wiz" : "RENAME VIEW \"bar::wiz\" ..."
* }
*
* @todo clarify input parameters
* @param {CSN.Model} csn Augmented csn?
* @param {CSN.Options} options Transformation options
* @returns {object} A dictionary of name: rename statement
*/
function toRenameDdl(csn, options) {
// Merge options (arguments first, then model options and default)
options = mergeOptions({toRename: {names: 'hdbcds'}}, model.options, options);
let result = Object.create(null);
const result = Object.create(null);
// FIXME: Currently requires 'options.forHana', because it requires the 'forHana' transformations
if (!options.forHana) {
throw new Error('toRenameDdl can currently only be used with HANA preprocessing');
}
checkCSNVersion(csn, options);
// FIXME: This should happen in the caller
let csn = compactModel(model);
// Create artificial namespace objects, so that each artifact has parents up to top-level.
// FIXME: This is actually only necessary to make 'getParentNameOf' work - should be reworked
for (let artifactName in csn.definitions) {
for (let parentName of getParentNamesOf(artifactName)) {
if (!csn.definitions[parentName]) {
csn.definitions[parentName] = {
kind : 'namespace',
};
}
}
}
const { getNamespaceOfArtifact } = getUtils(csn);
// Render each artifact on its own
for (let artifactName in csn.definitions) {
let sourceStr = renameTableAndColumns(artifactName, csn.definitions[artifactName]);
for (const artifactName in csn.definitions) {
const sourceStr = renameTableAndColumns(artifactName, csn.definitions[artifactName]);
if (sourceStr !== '') {
if (sourceStr !== '')
result[artifactName] = sourceStr;
}
}
// Throw exception in case of errors
if (hasErrors(model.messages)) {
throw new CompilationError(sortMessages(model.messages), model);
}
if (hasErrors(options.messages))
throw new CompilationError(options.messages);
return result;
// If 'art' is a non-view entity, generate SQL statements to rename the corresponding
// table and its columns from the naming conventions given in 'options.toRename.name'
// (either 'quoted' or 'hdbcds') to 'plain'. In addition, drop any existing associations
// from the columns (they would likely become invalid anyway).
// Do not rename anything if the names are identical.
/**
* If 'art' is a non-view entity, generate SQL statements to rename the corresponding
* table and its columns from the naming conventions given in 'options.toRename.name'
* (either 'quoted' or 'hdbcds') to 'plain'. In addition, drop any existing associations
* from the columns (they would likely become invalid anyway).
* Do not rename anything if the names are identical.
*
* @param {string} artifactName Name of the artifact to rename
* @param {CSN.Artifact} art CSN artifact
* @returns {string} RENAME statements
*/
function renameTableAndColumns(artifactName, art) {
let resultStr = '';
if (art.kind === 'entity' && !art.query) {
let beforeTableName = quoteSqlId(absoluteCdsName(artifactName));
let afterTableName = plainSqlId(artifactName);
const beforeTableName = quoteSqlId(absoluteCdsName(artifactName));
const afterTableName = plainSqlId(artifactName);
if (beforeTableName != afterTableName) {
resultStr += " EXEC 'RENAME TABLE " + beforeTableName + ' TO ' + afterTableName + "';\n";
}
if (beforeTableName !== afterTableName)
resultStr += ` EXEC 'RENAME TABLE ${beforeTableName} TO ${afterTableName}';\n`;
resultStr += Object.keys(art.elements).map(name => {
let e = art.elements[name];
resultStr += Object.keys(art.elements).map((name) => {
const e = art.elements[name];
let result = '';
let beforeColumnName = quoteSqlId(name);
let afterColumnName = plainSqlId(name);
const beforeColumnName = quoteSqlId(name);
const afterColumnName = plainSqlId(name);

@@ -83,7 +81,7 @@ if (!e._ignore) {

resultStr += ' ';
result = " EXEC 'ALTER TABLE " + afterTableName + ' DROP ASSOCIATION ' + beforeColumnName + "';\n";
result = ` EXEC 'ALTER TABLE ${afterTableName} DROP ASSOCIATION ${beforeColumnName}';\n`;
}
else if (beforeColumnName != afterColumnName) {
else if (beforeColumnName !== afterColumnName) {
resultStr += ' ';
result = " EXEC 'RENAME COLUMN " + afterTableName + '.' + beforeColumnName + ' TO ' + afterColumnName + "';\n";
result = ` EXEC 'RENAME COLUMN ${afterTableName}.${beforeColumnName} TO ${afterColumnName}';\n`;
}

@@ -97,30 +95,44 @@ }

// Return 'name' in the form of an absolute CDS name - for the 'hdbcds' naming convention,
// this means converting '.' to '::' on the border between namespace and top-level artifact.
// For all other naming conventions, this is a no-op.
/**
* Return 'name' in the form of an absolute CDS name - for the 'hdbcds' naming convention,
* this means converting '.' to '::' on the border between namespace and top-level artifact.
* For all other naming conventions, this is a no-op.
*
* @param {string} name Name to absolutify
* @returns {string} Absolute name
*/
function absoluteCdsName(name) {
if (options.toRename.names !== 'hdbcds') {
if (options.toRename.names !== 'hdbcds')
return name;
}
let topLevelName = getTopLevelArtifactNameOf(name, csn);
let namespaceName = getParentNameOf(topLevelName);
if (namespaceName) {
const namespaceName = getNamespaceOfArtifact(name);
if (namespaceName)
return `${namespaceName}::${name.substring(namespaceName.length + 1)}`;
}
return name;
}
// Return 'name' with appropriate "-quotes, also replacing '::' by '.' if 'options.toRename.names'
// is 'quoted'
/**
* Return 'name' with appropriate "-quotes, also replacing '::' by '.' if 'options.toRename.names'
* is 'quoted'
*
* @param {string} name Name to quote
* @returns {string} Quoted string
*/
function quoteSqlId(name) {
if (options.toRename.names === 'quoted') {
if (options.toRename.names === 'quoted')
name = name.replace(/::/g, '.');
}
return '"' + name.replace(/"/g, '""') + '"';
return `"${name.replace(/"/g, '""')}"`;
}
// Return 'name' with uppercasing and appropriate "-quotes, also replacing '::' and '.' by '_'
// (to be used by 'plain' naming convention).
/**
* Return 'name' with uppercasing and appropriate "-quotes, also replacing '::' and '.' by '_'
* (to be used by 'plain' naming convention).
*
* @param {string} name Name to turn into a plain identifier
* @returns {string} A plain SQL identifier
*/
function plainSqlId(name) {
return '"' + name.toUpperCase().replace(/(::|\.)/g, '_').replace(/"/g, '""') + '"';
return `"${name.toUpperCase().replace(/(::|\.)/g, '_').replace(/"/g, '""')}"`;
}

@@ -127,0 +139,0 @@ }

'use strict';
const { getTopLevelArtifactNameOf, getParentNameOf, getParentNamesOf, getLastPartOf, getLastPartOfRef } = require('../model/modelUtils');
const { hasBoolAnnotation, isBuiltinType } = require('../model/csnUtils');
const alerts = require('../base/alerts');
const version = require('../../package.json').version;
const { renderFunc } = require('./renderUtil');
const {
getLastPartOf, getLastPartOfRef,
hasValidSkipOrExists, isBuiltinType, generatedByCompilerVersion, getNormalizedQuery,
forEachDefinition, getResultingName,
} = require('../model/csnUtils');
const {
renderFunc, processExprArray, cdsToSqlTypes,
} = require('./utils/common');
const {
renderReferentialConstraint, getIdentifierUtils,
} = require('./utils/sql');
const DuplicateChecker = require('./DuplicateChecker');
const { checkCSNVersion } = require('../json/csnVersion');
const { handleMessages } = require('../base/messages');
const { makeMessageFunction } = require('../base/messages');
const timetrace = require('../utils/timetrace');
const { isBetaEnabled } = require('../base/model');
const { smartId, smartFuncId, delimitedId } = require('../sql-identifier');
const { isBetaEnabled, isDeprecatedEnabled } = require('../base/model');
const { smartFuncId } = require('../sql-identifier');
const { sortCsn } = require('../json/to-csn');
// Type mapping from cds type names to DB type names:
// (in the future, we would introduce an option for the mapping table)
const cdsToSqlTypes = {
standard: {
// characters and binaries
'cds.String': 'NVARCHAR',
'cds.hana.NCHAR': 'NCHAR',
'cds.LargeString' : 'NCLOB',
'cds.hana.VARCHAR': 'VARCHAR',
'cds.hana.CHAR': 'CHAR',
'cds.hana.CLOB': 'CLOB',
'cds.Binary': 'VARBINARY', // not a Standard SQL type, but HANA and MS SQL Server
'cds.hana.BINARY': 'BINARY',
'cds.LargeBinary': 'BLOB',
// numbers: exact and approximate
'cds.Decimal': 'DECIMAL',
'cds.DecimalFloat': 'DECIMAL',
'cds.Integer64': 'BIGINT',
'cds.Integer': 'INTEGER',
'cds.hana.SMALLINT': 'SMALLINT',
'cds.hana.TINYINT': 'TINYINT', // not a Standard SQL type
'cds.Double': 'DOUBLE',
'cds.hana.REAL': 'REAL',
// other: date/time, boolean
'cds.Date': 'DATE',
'cds.Time': 'TIME',
'cds.DateTime': 'TIMESTAMP', // cds-compiler#2758
'cds.Timestamp': 'TIMESTAMP',
'cds.Boolean': 'BOOLEAN',
'cds.UUID': 'NVARCHAR', // changed to cds.String earlier
// (TODO: do it later; TODO: why not CHAR or at least VARCHAR?)
},
hana: {
'cds.hana.SMALLDECIMAL': 'SMALLDECIMAL',
'cds.DateTime': 'SECONDDATE',
'cds.hana.ST_POINT': 'ST_POINT',
'cds.hana.ST_GEOMETRY': 'ST_GEOMETRY',
},
sqlite: {
'cds.Binary': 'CHAR',
'cds.hana.BINARY': 'CHAR',
'cds.hana.SMALLDECIMAL': 'DECIMAL',
},
};

@@ -107,8 +70,12 @@ /**

*
* @param {CSN.Model} csn
* @param {object} [options=csn.options]
* @param {CSN.Model} csn HANA transformed CSN
* @param {CSN.Options} options Transformation options
* @returns {object} Dictionary of artifact-type:artifacts, where artifacts is a dictionary of name:content
*/
function toSqlDdl(csn, options = csn.options) {
function toSqlDdl(csn, options) {
timetrace.start('SQL rendering');
const { error, signal, warning, info } = alerts(csn, options);
const {
error, warning, info, throwWithError,
} = makeMessageFunction(csn, options, 'to.sql');
const { quoteSqlId, prepareIdentifier } = getIdentifierUtils(options);

@@ -122,17 +89,17 @@ // Utils to render SQL statements.

addColumns: {
fromElementStrings: function(tableName, eltStrings) {
return [`ALTER TABLE ${tableName} ADD (${eltStrings.join(', ')});`];
fromElementStrings(tableName, eltStrings) {
return [ `ALTER TABLE ${tableName} ADD (${eltStrings.join(', ')});` ];
},
fromElementsObj: function(artifactName, tableName, elementsObj, env, duplicateChecker) {
fromElementsObj(artifactName, tableName, elementsObj, env, duplicateChecker) {
// Only extend with 'ADD' for elements/associations
// TODO: May also include 'RENAME' at a later stage
const elements = Object.entries(elementsObj)
.map(([name, elt]) => renderElement(artifactName, name, elt, duplicateChecker, null, env))
.filter(s => s !== '');
.map(([ name, elt ]) => renderElement(artifactName, name, elt, duplicateChecker, null, env))
.filter(s => s !== '');
if (elements.length) {
if (elements.length)
return render.addColumns.fromElementStrings(tableName, elements);
}
return [];
}
},
},

@@ -143,7 +110,7 @@ /*

*/
addAssociations: function(artifactName, tableName, elementsObj, env) {
addAssociations(artifactName, tableName, elementsObj, env) {
return Object.entries(elementsObj)
.map(([name, elt]) => renderAssociationElement(name, elt, env))
.filter(s => s !== '')
.map(eltStr => `ALTER TABLE ${tableName} ADD ASSOCIATION (${eltStr});`);
.map(([ name, elt ]) => renderAssociationElement(name, elt, env))
.filter(s => s !== '')
.map(eltStr => `ALTER TABLE ${tableName} ADD ASSOCIATION (${eltStr});`);
},

@@ -153,4 +120,4 @@ /*

*/
dropColumns: function(tableName, sqlIds) {
return [`ALTER TABLE ${tableName} DROP (${sqlIds.join(', ')});`];
dropColumns(tableName, sqlIds) {
return [ `ALTER TABLE ${tableName} DROP (${sqlIds.join(', ')});` ];
},

@@ -160,4 +127,4 @@ /*

*/
dropAssociation: function(tableName, sqlId) {
return [`ALTER TABLE ${tableName} DROP ASSOCIATION ${sqlId};`];
dropAssociation(tableName, sqlId) {
return [ `ALTER TABLE ${tableName} DROP ASSOCIATION ${sqlId};` ];
},

@@ -167,31 +134,18 @@ /*

*/
alterColumns: function (tableName, definitionsStr) {
return [`ALTER TABLE ${tableName} ALTER (${definitionsStr});`];
}
alterColumns(tableName, definitionsStr) {
return [ `ALTER TABLE ${tableName} ALTER (${definitionsStr});` ];
},
};
// FIXME: Currently requires 'options.forHana', because it can only render HANA-ish SQL dialect
if (!options.forHana) {
if (!options.forHana)
throw new Error('toSql can currently only be used with HANA preprocessing');
}
checkCSNVersion(csn, options);
// Create artificial namespace objects, so that each artifact has parents up to top-level.
// FIXME: This is actually only necessary to make 'getParentNameOf' work - should be reworked
for (let artifactName in csn.definitions) {
for (let parentName of getParentNamesOf(artifactName)) {
if (!csn.definitions[parentName]) {
csn.definitions[parentName] = {
kind : 'namespace',
};
}
}
}
// The final result in hdb-kind-specific form, without leading CREATE, without trailing newlines
// (note that the order here is relevant for transmission into 'resultObj.sql' below and that
// the attribute names must be the HDI plugin names for --src hdi)
let resultObj = {
// The result object may have a `sql` dictionary for `toSql`.
const resultObj = {
hdbtabletype: Object.create(null),

@@ -202,15 +156,16 @@ hdbtable: Object.create(null),

hdbview: Object.create(null),
hdbconstraint: Object.create(null),
deletions: Object.create(null),
migrations: Object.create(null)
}
migrations: Object.create(null),
};
// Registries for artifact and element names per CSN section
let definitionsDuplicateChecker = new DuplicateChecker();
let deletionsDuplicateChecker = new DuplicateChecker();
let extensionsDuplicateChecker = new DuplicateChecker();
let removeElementsDuplicateChecker = new DuplicateChecker();
let changeElementsDuplicateChecker = new DuplicateChecker();
const definitionsDuplicateChecker = new DuplicateChecker();
const deletionsDuplicateChecker = new DuplicateChecker();
const extensionsDuplicateChecker = new DuplicateChecker();
const removeElementsDuplicateChecker = new DuplicateChecker();
const changeElementsDuplicateChecker = new DuplicateChecker();
// Render each artifact on its own
for (let artifactName in csn.definitions) {
forEachDefinition((options && options.testMode) ? sortCsn(csn, true) : csn, (artifact, artifactName) => {
// This environment is passed down the call hierarchy, for dealing with

@@ -222,10 +177,10 @@ // indentation issues

};
renderArtifactInto(artifactName, csn.definitions[artifactName], resultObj, env);
}
renderArtifactInto(artifactName, artifact, resultObj, env);
});
// Render each deleted artifact
for (let artifactName in csn.deletions) {
for (const artifactName in csn.deletions)
renderArtifactDeletionInto(artifactName, csn.deletions[artifactName], resultObj);
}
// Render each artifact extension

@@ -235,3 +190,3 @@ // Only HANA SQL is currently supported.

if (csn.extensions && options.toSql.dialect === 'hana') {
for (const extension of csn.extensions) {
for (const extension of options && options.testMode ? sortCsn(csn.extensions) : csn.extensions) {
if (extension.extend) {

@@ -248,3 +203,3 @@ const artifactName = extension.extend;

if (csn.migrations && options.toSql.dialect === 'hana') {
for (const migration of csn.migrations) {
for (const migration of options && options.testMode ? sortCsn(csn.migrations) : csn.migrations) {
if (migration.migrate) {

@@ -259,8 +214,8 @@ const artifactName = migration.migrate;

// trigger artifact and element name checks
definitionsDuplicateChecker.check(signal, error);
extensionsDuplicateChecker.check(signal, error);
deletionsDuplicateChecker.check(signal, error);
definitionsDuplicateChecker.check(error, options);
extensionsDuplicateChecker.check(error);
deletionsDuplicateChecker.check(error);
// Throw exception in case of errors
handleMessages(csn, options);
throwWithError();

@@ -270,4 +225,4 @@ // Transfer results from hdb-specific dictionaries into 'sql' dictionary in proper order if toSql.src === 'sql'

// FIXME: Should consider inter-view dependencies, too
let sql = Object.create(null);
let sqlVersionLine = `-- generated by cds-compiler version ${version}\n`;
const sql = Object.create(null);
const sqlVersionLine = `-- ${generatedByCompilerVersion()}\n`;

@@ -277,48 +232,52 @@ // Handle hdbKinds separately from alterTable case

const { deletions, migrations, ...hdbKinds } = resultObj;
for (let hdbKind of Object.keys(hdbKinds)) {
for (let name in resultObj[hdbKind]) {
for (const hdbKind of Object.keys(hdbKinds)) {
for (const name in resultObj[hdbKind]) {
if (options.toSql.src === 'sql') {
let sourceString = resultObj[hdbKind][name];
// Hack: Other than in 'hdbtable' files, in HANA SQL COLUMN is not mandatory but default.
if (options.toSql.dialect === 'hana' && hdbKind === 'hdbtable' && sourceString.startsWith('COLUMN ')) {
if (options.toSql.dialect === 'hana' && hdbKind === 'hdbtable' && sourceString.startsWith('COLUMN '))
sourceString = sourceString.slice('COLUMN '.length);
}
sql[name] = `${options.testMode ? '' : sqlVersionLine}CREATE ${sourceString};`;
}
else {
if (!options.testMode) {
resultObj[hdbKind][name] = sqlVersionLine + resultObj[hdbKind][name];
}
else if (!options.testMode) {
resultObj[hdbKind][name] = sqlVersionLine + resultObj[hdbKind][name];
}
}
if (options.toSql.src === 'sql') {
if (options.toSql.src === 'sql')
delete resultObj[hdbKind];
}
}
if (options.toSql.src === 'sql') {
if (options.toSql.src === 'sql')
resultObj.sql = sql;
}
for (let name in deletions) {
deletions[name] = `${options.testMode ? '' : sqlVersionLine}` + deletions[name];
}
for (const name in deletions)
deletions[name] = `${options.testMode ? '' : sqlVersionLine}${deletions[name]}`;
timetrace.stop();
return resultObj;
// Render an artifact into the appropriate dictionary of 'resultObj'.
/**
* Render an artifact into the appropriate dictionary of 'resultObj'.
*
* @param {string} artifactName Name of the artifact to render
* @param {CSN.Artifact} art Artifact to render
* @param {object} resultObj Result collector
* @param {object} env Render environment
*/
function renderArtifactInto(artifactName, art, resultObj, env) {
// Ignore whole artifacts if forHana says so
if (art._ignore || hasBoolAnnotation(art, '@cds.persistence.exists', true)) {
if (art.abstract || hasValidSkipOrExists(art))
return;
}
switch (art.kind) {
case 'entity':
case 'view':
if (art.query) {
let result = renderView(artifactName, art, env);
if (result) {
if (getNormalizedQuery(art).query) {
const result = renderView(artifactName, art, env);
if (result)
resultObj.hdbview[artifactName] = result;
}
} else {
}
else {
renderEntityInto(artifactName, art, resultObj, env);

@@ -328,6 +287,6 @@ }

case 'type': {
let result = renderType(artifactName, art, env);
if (result) {
const result = renderType(artifactName, art, env);
if (result)
resultObj.hdbview[artifactName] = result;
}
break;

@@ -342,26 +301,51 @@ }

case 'event':
case 'aspect':
// Ignore: not SQL-relevant
return;
default:
throw new Error('Unknown artifact kind: ' + art.kind);
throw new Error(`Unknown artifact kind: ${art.kind}`);
}
}
/**
* Render an artifact extension into the appropriate dictionary of 'resultObj'.
* Only HANA SQL is currently supported.
*
* @param {string} artifactName Name of the artifact to render
* @param {CSN.Artifact} ext Extension to render
* @param {object} resultObj Result collector
* @param {object} env Render environment
*/
function renderArtifactExtensionInto(artifactName, ext, resultObj, env) {
// Property kind is always omitted for elements and can be omitted for
// top-level type definitions, it does not exist for extensions.
if (artifactName && !ext.query)
renderExtendInto(artifactName, ext.elements, resultObj, env, extensionsDuplicateChecker);
if (!artifactName)
throw new Error(`Undefined artifact name: ${artifactName}`);
}
// Render an artifact deletion into the appropriate dictionary of 'resultObj'.
function renderArtifactDeletionInto(artifactName, art, resultObj) {
const tableName = quoteSqlId(absoluteCdsName(artifactName), art.$location);
const tableName = renderArtifactName(artifactName);
deletionsDuplicateChecker.addArtifact(tableName, art.$location, artifactName);
addDeletion(resultObj, artifactName, `DROP TABLE ${tableName}`)
addDeletion(resultObj, artifactName, `DROP TABLE ${tableName}`);
}
// Render an artifact extension into the appropriate dictionary of 'resultObj'.
// Only HANA SQL is currently supported.
function renderArtifactExtensionInto(artifactName, ext, resultObj, env) {
// Property kind is always omitted for elements and can be omitted for
// top-level type definitions, it does not exist for extensions.
if (artifactName && !ext.query) {
renderExtendInto(artifactName, ext.elements, resultObj, env, extensionsDuplicateChecker);
}
if (!artifactName) throw new Error('Undefined artifact name: ' + artifactName);
/**
* Given the following artifact name: namespace.prefix.entity.with.dot, render the following,
* depending on the naming mode:
* - plain: NAMESPACE_PREFIX_ENTITY_WITH_DOT
* - quoted: namespace.prefix.entity_with_dot
* - hdbcds: namespace::prefix.entity_with_dot
*
*
* @param {string} artifactName Artifact name to render
*
* @returns {string} Artifact name
*/
function renderArtifactName(artifactName) {
return quoteSqlId(getResultingName(csn, options.toSql.names, artifactName));
}

@@ -375,9 +359,9 @@

return def.old.type === def.new.type &&
['length', 'precision', 'scale'].some(param => def.new[param] < def.old[param]);
[ 'length', 'precision', 'scale' ].some(param => def.new[param] < def.old[param]);
}
function concat(...statements) {
return [statements.join('\n')];
return [ statements.join('\n') ];
}
const tableName = quoteSqlId(absoluteCdsName(artifactName));
const tableName = renderArtifactName(artifactName);

@@ -388,12 +372,12 @@ // Drop column (unsupported in sqlite)

if (entries.length) {
const removeCols = entries.filter(([, value]) => !value.target).map(([key, ]) => quoteSqlId(key));
const removeAssocs = entries.filter(([, value]) => value.target).map(([key, ]) => quoteSqlId(key));
const removeCols = entries.filter(([ , value ]) => !value.target).map(([ key ]) => quoteSqlId(key));
const removeAssocs = entries.filter(([ , value ]) => value.target).map(([ key ]) => quoteSqlId(key));
removeElementsDuplicateChecker.addArtifact(tableName, undefined, artifactName);
[...removeCols, ...removeAssocs].forEach(element => removeElementsDuplicateChecker.addElement(quoteSqlId(element), undefined, element));
[ ...removeCols, ...removeAssocs ].forEach(element => removeElementsDuplicateChecker.addElement(quoteSqlId(element), undefined, element));
// Remove columns.
if (removeCols.length) {
if (removeCols.length)
addMigration(resultObj, artifactName, true, render.dropColumns(tableName, removeCols));
}
// Remove associations.

@@ -406,4 +390,4 @@ removeAssocs.forEach(assoc => addMigration(resultObj, artifactName, true, render.dropAssociation(tableName, assoc)));

if (migration.change) {
changeElementsDuplicateChecker.addArtifact(tableName, undefined, artifactName)
for (const [eltName, def] of Object.entries(migration.change)) {
changeElementsDuplicateChecker.addArtifact(tableName, undefined, artifactName);
for (const [ eltName, def ] of Object.entries(migration.change)) {
const sqlId = quoteSqlId(eltName);

@@ -415,9 +399,10 @@ changeElementsDuplicateChecker.addElement(sqlId, undefined, eltName);

const drop = def.old.target
? render.dropAssociation(tableName, sqlId)
: render.dropColumns(tableName, [sqlId]);
? render.dropAssociation(tableName, sqlId)
: render.dropColumns(tableName, [ sqlId ]);
const add = def.new.target
? render.addAssociations(artifactName, tableName, {[eltName]: def.new}, env)
: render.addColumns.fromElementsObj(artifactName, tableName, {[eltName]: def.new}, env);
? render.addAssociations(artifactName, tableName, { [eltName]: def.new }, env)
: render.addColumns.fromElementsObj(artifactName, tableName, { [eltName]: def.new }, env);
addMigration(resultObj, artifactName, true, concat(...drop, ...add));
} else {
}
else {
// Lossless change: no associations directly affected, no size reduction.

@@ -431,7 +416,14 @@ const eltStr = renderElement(artifactName, eltName, def.new, null, null, env);

// Render a (non-projection, non-view) entity (and possibly its indices) into the appropriate
// dictionaries of 'resultObj'.
/**
* Render a (non-projection, non-view) entity (and possibly its indices) into the appropriate
* dictionaries of 'resultObj'.
*
* @param {string} artifactName Name of the artifact to render
* @param {CSN.Artifact} art Artifact to render
* @param {object} resultObj Result collector
* @param {object} env Render environment
*/
function renderEntityInto(artifactName, art, resultObj, env) {
let childEnv = increaseIndent(env);
let hanaTc = art.technicalConfig && art.technicalConfig.hana;
const childEnv = increaseIndent(env);
const hanaTc = art.technicalConfig && art.technicalConfig.hana;
let result = '';

@@ -442,4 +434,5 @@ // Only HANA has row/column tables

// Explicitly specified
result += art.technicalConfig.hana.storeType.toUpperCase() + ' ';
} else {
result += `${art.technicalConfig.hana.storeType.toUpperCase()} `;
}
else {
// in 'hdbtable' files, COLUMN or ROW is mandatory, and COLUMN is the default

@@ -449,60 +442,78 @@ result += 'COLUMN ';

}
let tableName = quoteSqlId(absoluteCdsName(artifactName), art.$location);
definitionsDuplicateChecker.addArtifact(tableName, art.$location, artifactName)
result += 'TABLE ' + tableName;
const tableName = renderArtifactName(artifactName);
definitionsDuplicateChecker.addArtifact(tableName, art.$location, artifactName);
result += `TABLE ${tableName}`;
result += ' (\n';
let elements = Object.keys(art.elements).map(eltName => {
return renderElement(artifactName, eltName, art.elements[eltName], definitionsDuplicateChecker, getFzIndex(eltName, hanaTc), childEnv);
}).filter(s => s !== '').join(',\n');
const elements = Object.keys(art.elements).map(eltName => renderElement(artifactName, eltName, art.elements[eltName], definitionsDuplicateChecker, getFzIndex(eltName, hanaTc), childEnv)).filter(s => s !== '').join(',\n');
if (elements !== '') {
result += elements;
} else {
}
else {
// TODO: Already be handled by 'empty-entity' reclassification; better location
signal(error`"${artifactName}": Entity must have at least one element that is non-virtual`, ['definitions', artifactName]);
error(null, [ 'definitions', artifactName ], 'Entities must have at least one element that is non-virtual');
}
let primaryKeys = Object.keys(art.elements).filter(name => art.elements[name].key)
.filter(name => !art.elements[name]._ignore)
.map(name => quoteSqlId(name, art.elements[name].$location))
.join(', ');
let uniqueFields = Object.keys(art.elements).filter(name => art.elements[name].unique && !art.elements[name]._ignore)
.map(quoteSqlId)
.join(', ');
if (uniqueFields !== '') {
result += ',\n' + childEnv.indent + 'UNIQUE(' + uniqueFields + ')'
const primaryKeys = Object.keys(art.elements).filter(name => art.elements[name].key)
.filter(name => !art.elements[name].virtual)
.map(name => quoteSqlId(name))
.join(', ');
const uniqueFields = Object.keys(art.elements).filter(name => art.elements[name].unique && !art.elements[name].virtual)
.map(name => quoteSqlId(name))
.join(', ');
if (uniqueFields !== '')
result += `,\n${childEnv.indent}UNIQUE(${uniqueFields})`;
if (primaryKeys !== '')
result += `,\n${childEnv.indent}PRIMARY KEY(${primaryKeys})`;
if (art.$tableConstraints && art.$tableConstraints.referential) {
const renderReferentialConstraintsAsHdbconstraint = options.toSql.src === 'hdi';
const referentialConstraints = {};
Object.entries(art.$tableConstraints.referential)
.forEach(([ identifier, referentialConstraint ]) => {
referentialConstraints[identifier] = renderReferentialConstraint(referentialConstraint, identifier, childEnv.indent, false, csn, options);
});
if (renderReferentialConstraintsAsHdbconstraint) {
Object.entries(referentialConstraints).forEach( ([ identifier, constraint ]) => {
resultObj.hdbconstraint[identifier] = constraint;
});
}
else {
Object.values(referentialConstraints).forEach( (constraint) => {
result += `,\n${constraint}`;
});
}
}
if (primaryKeys !== '') {
result += ',\n' + childEnv.indent + 'PRIMARY KEY(' + primaryKeys + ')'
}
// Append table constraints if any
// 'CONSTRAINT <name> UNIQUE (<column_list>)
// OR create a unique index for HDI
for(const cn in art.$tableConstraints) {
const c = art.$tableConstraints[cn];
if(options.toSql.src === 'hdi') {
resultObj.hdbindex[`${artifactName}.${cn}`] =
`UNIQUE INVERTED INDEX ${quoteSqlId( absoluteCdsName(artifactName) + '_' + cn )} ON ${tableName} (${c.map(cpath => quoteSqlId(cpath.ref[0])).join(', ')})`;
const uniqueConstraints = art.$tableConstraints && art.$tableConstraints.unique;
for (const cn in uniqueConstraints) {
const c = uniqueConstraints[cn];
if (options.toSql.src === 'hdi') {
resultObj.hdbindex[`${artifactName}.${cn}`]
= `UNIQUE INVERTED INDEX ${renderArtifactName(`${artifactName}_${cn}`)} ON ${tableName} (${c.map(cpath => quoteSqlId(cpath.ref[0])).join(', ')})`;
}
else {
result += ',\n' + childEnv.indent + 'CONSTRAINT ' + `${quoteSqlId( absoluteCdsName(artifactName) + '_' + cn )}` + ' UNIQUE (' +
c.map(cpath => quoteSqlId(cpath.ref[0])).join(', ') + ')';
result += `,\n${childEnv.indent}CONSTRAINT ${renderArtifactName(`${artifactName}_${cn}`)} UNIQUE (${
c.map(cpath => quoteSqlId(cpath.ref[0])).join(', ')})`;
}
}
result += env.indent + '\n)';
result += `${env.indent}\n)`;
if (options.toSql.dialect === 'hana') {
if (options.toSql.dialect === 'hana')
result += renderTechnicalConfiguration(art.technicalConfig, childEnv);
}
let associations = Object.keys(art.elements).map(name => renderAssociationElement(name, art.elements[name], childEnv))
.filter(s => s !== '')
.join(',\n');
const associations = Object.keys(art.elements).map(name => renderAssociationElement(name, art.elements[name], childEnv))
.filter(s => s !== '')
.join(',\n');
if (associations !== '' && options.toSql.dialect === 'hana') {
result += env.indent + ' WITH ASSOCIATIONS (\n' + associations + '\n';
result += env.indent + ')';
result += `${env.indent} WITH ASSOCIATIONS (\n${associations}\n`;
result += `${env.indent})`;
}
// Only HANA has indices
// FIXME: Really? We should provide a DB-agnostic way to specify that
if (options.toSql.dialect === 'hana') {
if (options.toSql.dialect === 'hana')
renderIndexesInto(art.technicalConfig && art.technicalConfig.hana.indexes, artifactName, resultObj, env);
}
resultObj.hdbtable[artifactName] = result;

@@ -512,18 +523,26 @@ }

// Render an extended entity into the appropriate dictionaries of 'resultObj'.
// Only HANA SQL is currently supported.
/**
* Render an extended entity into the appropriate dictionaries of 'resultObj'.
* Only HANA SQL is currently supported.
*
* @param {string} artifactName Name of the artifact to render
* @param {object} elementsObj ??
* @param {object} resultObj Result collector
* @param {object} env Render environment
* @param {DuplicateChecker} duplicateChecker
*/
function renderExtendInto(artifactName, elementsObj, resultObj, env, duplicateChecker) {
const tableName = quoteSqlId(absoluteCdsName(artifactName));
duplicateChecker && duplicateChecker.addArtifact(tableName, undefined, artifactName)
const tableName = renderArtifactName(artifactName);
if (duplicateChecker)
duplicateChecker.addArtifact(tableName, undefined, artifactName);
const elements = render.addColumns.fromElementsObj(artifactName, tableName, elementsObj, env, duplicateChecker);
const associations = render.addAssociations(artifactName, tableName, elementsObj, env);
if (elements.length + associations.length > 0) {
addMigration(resultObj, artifactName, false, [...elements, ...associations]);
}
if (elements.length + associations.length > 0)
addMigration(resultObj, artifactName, false, [ ...elements, ...associations ]);
}
function addMigration(resultObj, artifactName, drop, sqlArray) {
if (!(artifactName in resultObj.migrations)) {
if (!(artifactName in resultObj.migrations))
resultObj.migrations[artifactName] = [];
}
const migrations = sqlArray.map(sql => ({ drop, sql }));

@@ -537,7 +556,13 @@ resultObj.migrations[artifactName].push(...migrations);

// Retrieve the 'fzindex' (fuzzy index) property (if any) for element 'elemName' from hanaTc (if defined)
/**
* Retrieve the 'fzindex' (fuzzy index) property (if any) for element 'elemName' from hanaTc (if defined)
*
* @param {string} elemName Element to retrieve the index for
* @param {object} hanaTc Technical configuration object
* @returns {object} fzindex for the element
*/
function getFzIndex(elemName, hanaTc) {
if (!hanaTc || !hanaTc.fzindexes || !hanaTc.fzindexes[elemName]) {
if (!hanaTc || !hanaTc.fzindexes || !hanaTc.fzindexes[elemName])
return undefined;
}
if (hanaTc.fzindexes[elemName][0] instanceof Array) {

@@ -548,63 +573,79 @@ // FIXME: Should we allow multiple fuzzy search indices on the same column at all?

}
else {
return hanaTc.fzindexes[elemName];
}
return hanaTc.fzindexes[elemName];
}
// Render an element 'elm' with name 'elementName' (of an entity or type, not of a
// projection or view), optionally with corresponding fuzzy index 'fzindex' from the
// technical configuration.
// Ignore association elements (those are rendered later by renderAssociationElement).
// Use 'artifactName' only for error output.
// Return the resulting source string (no trailing LF).
/**
* Render an element 'elm' with name 'elementName' (of an entity or type, not of a
* projection or view), optionally with corresponding fuzzy index 'fzindex' from the
* technical configuration.
* Ignore association elements (those are rendered later by renderAssociationElement).
* Use 'artifactName' only for error output.
* Return the resulting source string (no trailing LF).
*
* @param {string} artifactName Name of the artifact containing the element
* @param {string} elementName Name of the element to render
* @param {CSN.Element} elm CSN element
* @param {DuplicateChecker} duplicateChecker Utility for detecting duplicates
* @param {object} fzindex Fzindex object for the element
* @param {object} env Render environment
* @returns {string} Rendered element
*/
function renderElement(artifactName, elementName, elm, duplicateChecker, fzindex, env) {
// Ignore if forHana says so, or if it is an association
if (elm.virtual)
elm._ignore = true; // this has the side effect, that it's also ignored in the primary key generation
if (elm._ignore || elm.target) {
if (elm.virtual || elm.target)
return '';
}
const quotedElementName = quoteSqlId(elementName, elm.$location);
duplicateChecker && duplicateChecker.addElement(quotedElementName, elm.$location, elementName);
let result = env.indent + quotedElementName + ' '
+ renderTypeReference(artifactName, elementName, elm)
+ renderNullability(elm, true);
if (elm.default) {
result += ' DEFAULT ' + renderExpr(elm.default, env);
}
// Only HANA has fuzzy indizes
if (fzindex && options.toSql.dialect === 'hana') {
result += ' ' + renderExpr(fzindex, env);
}
const quotedElementName = quoteSqlId(elementName);
if (duplicateChecker)
duplicateChecker.addElement(quotedElementName, elm.$location, elementName);
let result = `${env.indent + quotedElementName} ${
renderTypeReference(artifactName, elementName, elm)
}${renderNullability(elm, true)}`;
if (elm.default)
result += ` DEFAULT ${renderExpr(elm.default, env)}`;
// Only HANA has fuzzy indices
if (fzindex && options.toSql.dialect === 'hana')
result += ` ${renderExpr(fzindex, env)}`;
return result;
}
// Render an element 'elm' with name 'elementName' if it is an association, in the style required for
// HANA native associations (e.g. 'MANY TO ONE JOIN "source" AS "assoc" ON (condition)').
// Return a string with one line per association element, or an empty string if the element
// is not an association.
// Any change to the cardinality rendering must be reflected in A2J mapAssocToJoinCardinality() as well.
// TODO duplicity check
/**
* Render an element 'elm' with name 'elementName' if it is an association, in the style required for
* HANA native associations (e.g. 'MANY TO ONE JOIN "source" AS "assoc" ON (condition)').
* Return a string with one line per association element, or an empty string if the element
* is not an association.
* Any change to the cardinality rendering must be reflected in A2J mapAssocToJoinCardinality() as well.
*
* @todo Duplicate check
* @param {string} elementName Name of the element to render
* @param {CSN.Element} elm CSN element
* @param {object} env Render environment
* @returns {string} Rendered association element
*/
function renderAssociationElement(elementName, elm, env) {
let result = '';
if (elm.target && !elm._ignore) {
if (elm.target) {
result += env.indent;
if(elm.cardinality) {
if(isBetaEnabled(options, 'hanaAssocRealCardinality') && elm.cardinality.src && elm.cardinality.src === 1) {
if (elm.cardinality) {
if (isBetaEnabled(options, 'hanaAssocRealCardinality') && elm.cardinality.src && elm.cardinality.src === 1)
result += 'ONE TO ';
} else {
else
result += 'MANY TO ';
}
if (elm.cardinality.max && (elm.cardinality.max === '*' || Number(elm.cardinality.max) > 1)) {
if (elm.cardinality.max && (elm.cardinality.max === '*' || Number(elm.cardinality.max) > 1))
result += 'MANY';
} else {
else
result += 'ONE';
}
} else {
}
else {
result += 'MANY TO ONE';
}
result += ' JOIN ';
result += quoteSqlId(absoluteCdsName(elm.target)) + ' AS ' + quoteSqlId(elementName, elm.$location) + ` ON (`;
result += renderExpr(elm.on, env) + ')';
result += `${renderArtifactName(elm.target)} AS ${quoteSqlId(elementName)} ON (`;
result += `${renderExpr(elm.on, env, true, true)})`;
}

@@ -614,19 +655,26 @@ return result;

// Render the 'technical configuration { ... }' section of an entity that comes as a suffix
// to the CREATE TABLE statement (includes migration, unload prio, extended storage,
// auto merge, partitioning, ...).
// Return the resulting source string.
/**
* Render the 'technical configuration { ... }' section of an entity that comes as a suffix
* to the CREATE TABLE statement (includes migration, unload prio, extended storage,
* auto merge, partitioning, ...).
* Return the resulting source string.
*
* @param {object} tc Technical configuration
* @param {object} env Render environment
* @returns {string} Rendered technical configuration
*/
function renderTechnicalConfiguration(tc, env) {
let result = '';
if (!tc) {
if (!tc)
return result;
}
// FIXME: How to deal with non-HANA technical configurations?
// This also affects renderIndexes
tc = tc.hana;
if (!tc) {
if (!tc)
throw new Error('Expecting a HANA technical configuration');
}
if (tc.tableSuffix) {

@@ -639,3 +687,3 @@ // Although we could just render the whole bandwurm as one stream of tokens, the

// The ignore array contains technical configurations that are illegal in HANA SQL
let ignore = [
const ignore = [
'PARTITION BY KEEPING EXISTING LAYOUT',

@@ -645,9 +693,8 @@ 'ROW STORE',

'MIGRATION ENABLED',
'MIGRATION DISABLED'
'MIGRATION DISABLED',
];
for (let xpr of tc.tableSuffix) {
let clause = renderExpr(xpr, env);
if(!ignore.includes(clause.toUpperCase())) {
result += '\n' + env.indent + clause;
}
for (const xpr of tc.tableSuffix) {
const clause = renderExpr(xpr, env);
if (!ignore.includes(clause.toUpperCase()))
result += `\n${env.indent}${clause}`;
}

@@ -658,12 +705,18 @@ }

// Render the array `indexes` from the technical configuration of an entity 'artifactName'
/**
* Render the array `indexes` from the technical configuration of an entity 'artifactName'
*
* @param {object} indexes Indices to render
* @param {string} artifactName Artifact to render indices for
* @param {object} resultObj Result collector
* @param {object} env Render environment
*/
function renderIndexesInto(indexes, artifactName, resultObj, env) {
// Indices and full-text indices
for (let idxName in indexes || {}) {
for (const idxName in indexes || {}) {
let result = '';
if (indexes[idxName][0] instanceof Array) {
// FIXME: Should we allow multiple indices with the same name at all? (last one wins)
for (let index of indexes[idxName]) {
for (const index of indexes[idxName])
result = renderExpr(insertTableName(index), env);
}
}

@@ -674,30 +727,35 @@ else {

// FIXME: Full text index should already be different in compact CSN
if (result.startsWith('FULLTEXT')) {
if (result.startsWith('FULLTEXT'))
resultObj.hdbfulltextindex[`${artifactName}.${idxName}`] = result;
}
else {
else
resultObj.hdbindex[`${artifactName}.${idxName}`] = result;
}
}
// Insert 'artifactName' (quoted according to naming style) into the index
// definition 'index' in two places:
// CDS: unique index "foo" on (x, y)
// becomes
// SQL: unique index "<artifact>.foo" on "<artifact>"(x, y)
// CDS does not need this because the index lives inside the artifact, but SQL does.
/**
* Insert 'artifactName' (quoted according to naming style) into the index
* definition 'index' in two places:
* CDS: unique index "foo" on (x, y)
* becomes
* SQL: unique index "<artifact>.foo" on "<artifact>"(x, y)
* CDS does not need this because the index lives inside the artifact, but SQL does.
*
* @param {Array} index Index definition
* @returns {Array} Index with artifact name inserted
*/
function insertTableName(index) {
let i = index.indexOf('index');
let j = index.indexOf('(');
if (i > index.length - 2 || !index[i + 1].ref || j < i || j > index.length - 2) {
const i = index.indexOf('index');
const j = index.indexOf('(');
if (i > index.length - 2 || !index[i + 1].ref || j < i || j > index.length - 2)
throw new Error(`Unexpected form of index: "${index}"`);
}
let indexName = `${absoluteCdsName(artifactName)}.${index[i + 1].ref}`;
if (options.toSql.names === 'plain') {
let indexName = renderArtifactName(`${artifactName}.${index[i + 1].ref}`);
if (options.toSql.names === 'plain')
indexName = indexName.replace(/(\.|::)/g, '_');
}
let result = index.slice(0, i + 1); // CREATE UNIQUE INDEX
result.push({ ref: [indexName] }); // "<artifact>.foo"
const result = index.slice(0, i + 1); // CREATE UNIQUE INDEX
result.push({ ref: [ indexName ] }); // "<artifact>.foo"
result.push(...index.slice(i + 2, j)); // ON
result.push({ ref: [absoluteCdsName(artifactName)] }); // <artifact>
result.push({ ref: [ renderArtifactName(artifactName) ] }); // <artifact>
result.push(...index.slice(j)); // (x, y)

@@ -708,7 +766,14 @@ return result;

// Render the source of a query, which may be a path reference, possibly with an alias,
// or a subselect, or a join operation. Use 'artifactName' only for error output.
// FIXME: Misleading name, should be something like 'renderQueryFrom'. All the query
// parts should probably also be rearranged.
// Returns the source as a string.
/**
* Render the source of a query, which may be a path reference, possibly with an alias,
* or a subselect, or a join operation. Use 'artifactName' only for error output.
*
* Returns the source as a string.
*
* @todo Misleading name, should be something like 'renderQueryFrom'. All the query parts should probably also be rearranged.
* @param {string} artifactName Name of the artifact containing the query
* @param {object} source Query source
* @param {object} env Render environment
* @returns {string} Rendered view source
*/
function renderViewSource(artifactName, source, env) {

@@ -718,5 +783,5 @@ // Sub-SELECT

let result = `(${renderQuery(artifactName, source, increaseIndent(env))})`;
if (source.as) {
if (source.as)
result += ` AS ${quoteSqlId(source.as)}`;
}
return result;

@@ -730,8 +795,8 @@ }

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

@@ -742,21 +807,26 @@ }

// Ordinary path, possibly with an alias
else {
// Sanity check
if (!source.ref) {
throw new Error(`Expecting ref in ${JSON.stringify(source)}`);
}
return renderAbsolutePathWithAlias(artifactName, source, env);
}
// Sanity check
if (!source.ref)
throw new Error(`Expecting ref in ${JSON.stringify(source)}`);
return renderAbsolutePathWithAlias(artifactName, source, env);
}
/**
* Render the cardinality of a join/association
*
* @param {object} card CSN cardinality representation
* @returns {string} Rendered cardinality
*/
function renderJoinCardinality(card) {
let result = '';
if(card) {
if(card.srcmin && card.srcmin === 1)
if (card) {
if (card.srcmin && card.srcmin === 1)
result += 'EXACT ';
result += card.src && card.src === 1 ? 'ONE ' : 'MANY ';
result += 'TO ';
if(card.min && card.min === 1)
if (card.min && card.min === 1)
result += 'EXACT ';
if(card.max)
if (card.max)
result += (card.max === 1) ? 'ONE ' : 'MANY ';

@@ -767,13 +837,21 @@ }

// Render a path that starts with an absolute name (as used for the source of a query),
// possibly with an alias, with plain or quoted names, depending on options. Expects an object 'path' that has a
// 'ref' and (in case of an alias) an 'as'. If necessary, an artificial alias
// is created to the original implicit name. Use 'artifactName' only for error output.
// Returns the name and alias as a string.
/**
* Render a path that starts with an absolute name (as used for the source of a query),
* possibly with an alias, with plain or quoted names, depending on options. Expects an object 'path' that has a
* 'ref' and (in case of an alias) an 'as'. If necessary, an artificial alias
* is created to the original implicit name. Use 'artifactName' only for error output.
* Returns the name and alias as a string.
*
* @param {string} artifactName Name of the artifact containing the path - used for error output
* @param {object} path Path to render
* @param {object} env Render environment
* @returns {string} Rendered path
*/
function renderAbsolutePathWithAlias(artifactName, path, env) {
// This actually can't happen anymore because assoc2joins should have taken care of it
if (path.ref[0].where) {
if (path.ref[0].where)
throw new Error(`"${artifactName}": Filters in FROM are not supported for conversion to SQL`);
}
// SQL needs a ':' after path.ref[0] to separate associations

@@ -783,12 +861,12 @@ let result = renderAbsolutePath(path, ':', env);

// Take care of aliases
let implicitAlias = getLastPartOfRef(path.ref);
const implicitAlias = path.ref.length === 0 ? getLastPartOf(getResultingName(csn, options.toSql.names, path.ref[0])) : getLastPartOfRef(path.ref);
if (path.as) {
// Source had an alias - render it
result += ' AS ' + quoteSqlId(path.as);
result += ` AS ${quoteSqlId(path.as)}`;
}
else {
const quotedAlias = quoteSqlId(implicitAlias);
if (getLastPartOf(result) != quotedAlias) {
if (getLastPartOf(result) !== quotedAlias) {
// Render an artificial alias if the result would produce a different one
result += ' AS ' + quotedAlias;
result += ` AS ${quotedAlias}`;
}

@@ -799,16 +877,24 @@ }

// Render a path that starts with an absolute name (as used e.g. for the source of a query),
// with plain or quoted names, depending on options. Expects an object 'path' that has a 'ref'.
// Uses <seperator> (typically ':': or '.') to separate the first artifact name from any
// subsequent associations.
// Returns the name as a string.
/**
* Render a path that starts with an absolute name (as used e.g. for the source of a query),
* with plain or quoted names, depending on options. Expects an object 'path' that has a 'ref'.
* Uses <separator> (typically ':': or '.') to separate the first artifact name from any
* subsequent associations.
* Returns the name as a string.
*
* @param {object} path Path to render
* @param {string} sep Separator between path steps
* @param {object} env Render environment
* @returns {string} Rendered path
*/
function renderAbsolutePath(path, sep, env) {
// Sanity checks
if (!path.ref) {
throw new Error('Expecting ref in path: ' + JSON.stringify(path));
}
if (!path.ref)
throw new Error(`Expecting ref in path: ${JSON.stringify(path)}`);
// Determine the absolute name of the first artifact on the path (before any associations or element traversals)
let firstArtifactName = path.ref[0].id || path.ref[0];
const firstArtifactName = path.ref[0].id || path.ref[0];
let result = quoteSqlId(absoluteCdsName(firstArtifactName));
let result = renderArtifactName(firstArtifactName);
// store argument syntax hint in environment

@@ -825,3 +911,3 @@ // $syntax is set only by A2J and only at the first path step after FROM clause rewriting

}
else if (['udf'].includes(syntax)) {
else if ([ 'udf' ].includes(syntax)) {
// if syntax is user defined function, render empty argument list

@@ -831,61 +917,75 @@ // CV without parameters is called as simple view

}
if (path.ref[0].where) {
result += `[${path.ref[0].cardinality ? (path.ref[0].cardinality.max + ': ') : ''}${renderExpr(path.ref[0].where, env)}]`;
}
if (path.ref[0].where)
result += `[${path.ref[0].cardinality ? (`${path.ref[0].cardinality.max}: `) : ''}${renderExpr(path.ref[0].where, env, true, true)}]`;
// Add any path steps (possibly with parameters and filters) that may follow after that
if (path.ref.length > 1) {
result += `${sep}${renderExpr({ref: path.ref.slice(1)}, env)}`;
}
if (path.ref.length > 1)
result += `${sep}${renderExpr({ ref: path.ref.slice(1) }, env)}`;
return result;
}
// Render function arguments or view parameters (positional if array, named if object/dict),
// using 'sep' as separator for positional parameters
/**
* Render function arguments or view parameters (positional if array, named if object/dict),
* using 'sep' as separator for positional parameters
*
* @param {Array|object} args Arguments to render
* @param {string} sep Separator between args
* @param {object} env Render environment
* @param {string} syntax Some magic A2J paramter - for calcview parameter rendering
* @returns {string} Rendered arguments
* @throws Throws if args is not an array or object.
*/
function renderArgs(args, sep, env, syntax) {
// Positional arguments
if (args instanceof Array) {
if (args instanceof Array)
return args.map(arg => renderExpr(arg, env)).join(', ');
}
// Named arguments (object/dict)
else if (typeof args === 'object') {
else if (typeof args === 'object')
return Object.keys(args).map(key => `${decorateParameter(key, syntax)} ${sep} ${renderExpr(args[key], env)}`).join(', ');
}
else {
throw new Error('Unknown args: ' + JSON.stringify(args));
}
throw new Error(`Unknown args: ${JSON.stringify(args)}`);
/**
* Render the given argument/parameter correctly.
*
* @param {string} arg Argument to render
* @param {string} syntax Some magic A2J paramter - for calcview parameter rendering
* @returns {string} Rendered argument
*/
function decorateParameter(arg, syntax) {
if(syntax === 'calcview') {
return `PLACEHOLDER."$$${arg}$$"`
}
else {
return quoteSqlId(arg);
}
if (syntax === 'calcview')
return `PLACEHOLDER."$$${arg}$$"`;
return quoteSqlId(arg);
}
}
// Render a single view column 'col', as it occurs in a select list or projection list.
// Return the resulting source string (one line per column item, no CR).
/**
* Render a single view column 'col', as it occurs in a select list or projection list.
* Return the resulting source string (one line per column item, no CR).
*
* @param {object} col Column to render
* @param {object} env Render environment
* @returns {string} Rendered column
*/
function renderViewColumn(col, env) {
// Ignore if forHana says so
// FIXME: Have we already filtered out associations here?
if (col._ignore) {
return '';
}
let result = '';
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) {
const renderVirtual = options.toSql && isBetaEnabled(options, 'dontRenderVirtualElements') ? !!options.toSql.renderVirtualElements : true;
if(renderVirtual)
const leaf = col.as || col.ref && col.ref[col.ref.length - 1] || col.func;
if (leaf && env._artifact.elements[leaf] && env._artifact.elements[leaf].virtual) {
if (isDeprecatedEnabled(options, 'renderVirtualElements'))
// render a virtual column 'null as <alias>'
result += env.indent + 'NULL AS ' + quoteSqlId(col.as || leaf);
} else if (col.cast && !(options && options.toSql && options.toSql.dialect === 'sqlite')) {
result = env.indent + 'CAST(' + renderExpr(col, env, true) + ' AS ';
result += renderBuiltinType(col.cast.type) + renderTypeParameters(col.cast);
result += ') ' + quoteSqlId(leaf);
} else {
result += `${env.indent}NULL AS ${quoteSqlId(col.as || leaf)}`;
}
else {
result = env.indent + renderExpr(col, env, true);
if (col.as) {
result += ' AS ' + quoteSqlId(col.as);
}
if (col.as)
result += ` AS ${quoteSqlId(col.as)}`;
else if (col.func)
result += ` AS ${quoteSqlId(col.func)}`;
}

@@ -895,18 +995,25 @@ return result;

// Render a view
/**
* Render a view
*
* @param {string} artifactName Name of the view
* @param {CSN.Artifact} art CSN view
* @param {object} env Render environment
* @returns {string} Rendered view
*/
function renderView(artifactName, art, env) {
env._artifact = art;
let viewName = quoteSqlId(absoluteCdsName(artifactName), art.$location);
definitionsDuplicateChecker.addArtifact(viewName, art && art.$location, artifactName)
let result = 'VIEW ' + viewName;
const viewName = renderArtifactName(artifactName);
definitionsDuplicateChecker.addArtifact(viewName, art && art.$location, artifactName);
let result = `VIEW ${viewName}`;
result += renderParameterDefinitions(artifactName, art.params);
result += ' AS ' + renderQuery(artifactName, art.query, env);
let childEnv = increaseIndent(env);
let associations = Object.keys(art.elements).filter(name => !!art.elements[name].target)
.map(name => renderAssociationElement(name, art.elements[name], childEnv))
.filter(s => s !== '')
.join(',\n');
result += ` AS ${renderQuery(artifactName, getNormalizedQuery(art).query, env)}`;
const childEnv = increaseIndent(env);
const associations = Object.keys(art.elements).filter(name => !!art.elements[name].target)
.map(name => renderAssociationElement(name, art.elements[name], childEnv))
.filter(s => s !== '')
.join(',\n');
if (associations !== '' && options.toSql.dialect === 'hana') {
result += env.indent + '\nWITH ASSOCIATIONS (\n' + associations + '\n';
result += env.indent + ')';
result += `${env.indent}\nWITH ASSOCIATIONS (\n${associations}\n`;
result += `${env.indent})`;
}

@@ -916,16 +1023,25 @@ return result;

// Render the parameter definition of a view if any. Return the parameters in parentheses, or an empty string
/**
* Render the parameter definition of a view if any. Return the parameters in parentheses, or an empty string
*
* @param {string} artifactName Name of the view
* @param {Array} params Array of parameters
* @returns {string} Rendered parameters
*/
function renderParameterDefinitions(artifactName, params) {
let result = '';
if(params) {
let parray = [];
for(const pn in params) {
if (params) {
const parray = [];
for (const pn in params) {
const p = params[pn];
let pstr = 'IN ' + prepareIdentifier(pn) + ' ' + renderTypeReference(artifactName, pn, p);
if(p.default) {
pstr += ' DEFAULT ' + renderExpr(p.default);
}
if (p.notNull === true || p.notNull === false)
info(null, [ 'definitions', artifactName, 'params', pn ], 'Not Null constraints on SQL view parameters are not allowed and are ignored');
let pstr = `IN ${prepareIdentifier(pn)} ${renderTypeReference(artifactName, pn, p)}`;
if (p.default)
pstr += ` DEFAULT ${renderExpr(p.default)}`;
parray.push(pstr);
}
result = '(' + parray.join(', ') + ')';
result = `(${parray.join(', ')})`;
}

@@ -935,3 +1051,10 @@ return result;

// Render a query 'query', i.e. a select statement with where-condition etc. Use 'artifactName' only for error messages.
/**
* Render a query 'query', i.e. a select statement with where-condition etc. Use 'artifactName' only for error messages.
*
* @param {string} artifactName Artifact containing the query
* @param {CSN.Query} query CSN query
* @param {object} env Render environment
* @returns {string} Rendered query
*/
function renderQuery(artifactName, query, env) {

@@ -942,7 +1065,7 @@ let result = '';

result += query.SET.args
.map(arg => {
.map((arg) => {
// Wrap each query in the SET in parentheses that
// - is a SET itself (to preserve precedence between the different SET operations),
// - has an ORDER BY/LIMIT (because UNION etc. can't stand directly behind an ORDER BY)
let queryString = renderQuery(artifactName, arg, env);
const queryString = renderQuery(artifactName, arg, env);
return (arg.SET || arg.SELECT && (arg.SELECT.orderBy || arg.SELECT.limit)) ? `(${queryString})` : queryString;

@@ -958,8 +1081,7 @@ })

result = `(${result})`;
if (query.SET.orderBy) {
if (query.SET.orderBy)
result += `\n${env.indent}ORDER BY ${query.SET.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;
}
if (query.SET.limit) {
if (query.SET.limit)
result += `\n${env.indent}${renderLimit(query.SET.limit, env)}`;
}
}

@@ -970,34 +1092,39 @@ return result;

else if (!query.SELECT) {
throw new Error('Unexpected query operation ' + JSON.stringify(query));
throw new Error(`Unexpected query operation ${JSON.stringify(query)}`);
}
let select = query.SELECT;
let childEnv = increaseIndent(env);
result += 'SELECT' + (select.distinct ? ' DISTINCT' : '');
const select = query.SELECT;
const childEnv = increaseIndent(env);
result += `SELECT${select.distinct ? ' DISTINCT' : ''}`;
// FIXME: We probably also need to consider `excluding` here ?
result += '\n' +
(select.columns||['*']).filter(s => !s._ignore)
.filter(col => !(select.mixin || {})[firstPathStepId(col.ref)]) // No mixin columns
.map(col => renderViewColumn(col, childEnv))
.filter(s => s !== '')
.join(',\n') + '\n';
result += `\n${
(select.columns || [ '*' ])
.filter(col => !(select.mixin || Object.create(null))[firstPathStepId(col.ref)]) // No mixin columns
.map(col => renderViewColumn(col, childEnv))
.filter(s => s !== '')
.join(',\n')}\n`;
result += `${env.indent}FROM ${renderViewSource(artifactName, select.from, env)}`;
if (select.where) {
result += `\n${env.indent}WHERE ${renderExpr(select.where, env)}`;
}
if (select.groupBy) {
if (select.where)
result += `\n${env.indent}WHERE ${renderExpr(select.where, env, true, true)}`;
if (select.groupBy)
result += `\n${env.indent}GROUP BY ${select.groupBy.map(expr => renderExpr(expr, env)).join(', ')}`;
}
if (select.having) {
result += `\n${env.indent}HAVING ${renderExpr(select.having, env)}`;
}
if (select.orderBy) {
if (select.having)
result += `\n${env.indent}HAVING ${renderExpr(select.having, env, true, true)}`;
if (select.orderBy)
result += `\n${env.indent}ORDER BY ${select.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;
}
if (select.limit) {
if (select.limit)
result += `\n${env.indent}${renderLimit(select.limit, env)}`;
}
return result;
}
// Returns the id of the first path step in 'ref' if any, otherwise undefined
/**
* Returns the id of the first path step in 'ref' if any, otherwise undefined
*
* @param {Array} ref Array of refs
* @returns {string|undefined} Id of first path step
*/
function firstPathStepId(ref) {

@@ -1007,60 +1134,83 @@ return ref && ref[0] && (ref[0].id || ref[0]);

// Render a query's LIMIT clause, which may have also have OFFSET.
/**
* Render a query's LIMIT clause, which may have also have OFFSET.
*
* @param {CSN.QueryLimit} limit Limit clause
* @param {object} env Renderenvironment
* @returns {string} Rendered LIMIT clause
*/
function renderLimit(limit, env) {
let result = '';
if (limit.rows !== undefined) {
if (limit.rows !== undefined)
result += `LIMIT ${renderExpr(limit.rows, env)}`;
}
if (limit.offset !== undefined) {
result += `${result !== '' ? '\n' + env.indent : ''}OFFSET ${renderExpr(limit.offset, env)}`;
}
if (limit.offset !== undefined)
result += `${result !== '' ? `\n${env.indent}` : ''}OFFSET ${renderExpr(limit.offset, env)}`;
return result;
}
// Render one entry of a query's ORDER BY clause (which always has a 'value' expression, and may
// have a 'sort' property for ASC/DESC and a 'nulls' for FIRST/LAST
/**
* Render one entry of a query's ORDER BY clause (which always has a 'value' expression, and may
* have a 'sort' property for ASC/DESC and a 'nulls' for FIRST/LAST
*
* @param {object} entry Part of an ORDER BY
* @param {object} env Render environment
* @returns {string} Rendered ORDER BY entry
*/
function renderOrderByEntry(entry, env) {
let result = renderExpr(entry, env);
if (entry.sort) {
if (entry.sort)
result += ` ${entry.sort.toUpperCase()}`;
}
if (entry.nulls) {
if (entry.nulls)
result += ` NULLS ${entry.nulls.toUpperCase()}`;
}
return result;
}
// Render a type.
// Return the resulting source string.
/**
* Render a type.
* Return the resulting source string.
*
* @todo prop 'dbType' does not exist (anymore)
* @param {string} artifactName Name of the type
* @param {CSN.Type} art CSN type
* @param {object} env Render environment
*
* @returns {string} Rendered type
*/
function renderType(artifactName, art, env) {
// Only HANA table types are SQL-relevant
if (!art.dbType) {
if (!art.dbType)
return '';
}
// In Sqlite dialect do not generate table type and throw an info
if (options.toSql.dialect === 'sqlite') {
if (options.toSql.dialect === 'sqlite' || options.toSql.dialect === 'plain') {
// TODO: Signal is not covered by tests + better location
signal(info`"${artifactName}": HANA table types are not supported in SQLite`, ['definitions', artifactName]);
info(null, [ 'definitions', artifactName ], `"${artifactName}": SAP HANA table types are not supported in SQLite`);
return '';
}
const typeName = quoteSqlId(absoluteCdsName(artifactName), art.$location);
definitionsDuplicateChecker.addArtifact(typeName, art && art.$location, artifactName)
let result = 'TYPE ' + typeName + ' AS TABLE (\n';
let childEnv = increaseIndent(env);
const typeName = renderArtifactName(artifactName);
definitionsDuplicateChecker.addArtifact(typeName, art && art.$location, artifactName);
let result = `TYPE ${typeName} AS TABLE (\n`;
const childEnv = increaseIndent(env);
if (art.elements) {
// Structured type
let elements = Object.keys(art.elements).map(name => renderElement(artifactName, name, art.elements[name], definitionsDuplicateChecker, null, childEnv))
.filter(s => s !== '')
.join(',\n') + '\n';
const elements = `${Object.keys(art.elements).map(name => renderElement(artifactName, name, art.elements[name], definitionsDuplicateChecker, null, childEnv))
.filter(s => s !== '')
.join(',\n')}\n`;
if (elements !== '') {
result += elements;
result += env.indent + ')';
} else {
result += `${env.indent})`;
}
else {
// TODO: Signal is not covered by tests + better location
signal(error`"${artifactName}": HANA table type must have at least one element that is non-virtual`, ['definitions', artifactName]);
error(null, [ 'definitions', artifactName ], 'SAP HANA table types must have at least one element that is non-virtual');
}
} else {
}
else {
// TODO: Signal is not covered by tests + better location
// Non-structured HANA table type
signal(error`"${artifactName}": HANA table types must have structured types for conversion to SQL`, ['definitions', artifactName]);
error(null, [ 'definitions', artifactName ], 'SAP HANA table types must have structured types for conversion to SQL');
return '';

@@ -1071,3 +1221,10 @@ }

// Render a reference to the type used by 'elm' (with name 'elementName' in 'artifactName', both used only for error messages).
/**
* Render a reference to the type used by 'elm' (with name 'elementName' in 'artifactName', both used only for error messages).
*
* @param {string} artifactName Artifact containing the element
* @param {string} elementName Element referencing the type
* @param {CSN.Element} elm CSN element
* @returns {string} Rendered type reference
*/
function renderTypeReference(artifactName, elementName, elm) {

@@ -1078,7 +1235,8 @@ let result = '';

if (!elm.type) {
if (!elm.elements) {
throw new Error('Missing type of: ' + elementName);
}
if (!elm.elements)
throw new Error(`Missing type of: ${elementName}`);
// TODO: Signal is not covered by tests + better location
signal(error`"${artifactName}.${elementName}": Anonymous structured types are not supported for conversion to SQL`, ['definitions',artifactName, 'elements', elementName]);
error(null, [ 'definitions', artifactName, 'elements', elementName ],
'Anonymous structured types are not supported for conversion to SQL');
return result;

@@ -1091,3 +1249,4 @@ }

// We can't do associations yet
signal(error`"${artifactName}.${elementName}": Association and composition types are not yet supported for conversion to SQL`, ['definitions',artifactName, 'elements', elementName]);
error(null, [ 'definitions', artifactName, 'elements', elementName ],
'Association and composition types are not yet supported for conversion to SQL');
return result;

@@ -1100,5 +1259,6 @@ }

result += renderBuiltinType(elm.type);
} else {
throw new Error('Unexpected non-primitive type of: ' + artifactName + '.' + elementName);
}
else {
throw new Error(`Unexpected non-primitive type of: ${artifactName}.${elementName}`);
}
result += renderTypeParameters(elm);

@@ -1108,9 +1268,15 @@ return result;

// Render the name of a builtin CDS type
/**
* Render the name of a builtin CDS type
*
* @param {string} typeName Name of the type
* @returns {string} Rendered type
*/
function renderBuiltinType(typeName) {
const forHanaRenamesToEarly = {
'cds.UTCDateTime' : 'cds.DateTime',
'cds.UTCTimestamp' : 'cds.Timestamp',
'cds.LocalDate' : 'cds.Date',
'cds.LocalTime' : 'cds.Time',
'cds.UTCDateTime': 'cds.DateTime',
'cds.UTCTimestamp': 'cds.Timestamp',
'cds.LocalDate': 'cds.Date',
'cds.LocalTime': 'cds.Time',
};

@@ -1122,3 +1288,9 @@ const tName = forHanaRenamesToEarly[typeName] || typeName;

// 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)
*
* @param {object} obj Object to render for
* @param {boolean} treatKeyAsNotNull Wether to render KEY as not null
* @returns {string} NULL/NOT NULL or ''
*/
function renderNullability(obj, treatKeyAsNotNull = false) {

@@ -1132,51 +1304,46 @@ if (obj.notNull === undefined && !(obj.key && treatKeyAsNotNull)) {

// Render (primitive) type parameters of element 'elm', i.e.
// length, precision and scale (even if incomplete), plus any other unknown ones.
/**
* Render (primitive) type parameters of element 'elm', i.e.
* length, precision and scale (even if incomplete), plus any other unknown ones.
*
* @param {CSN.Element} elm CSN element
* @returns {string} Rendered type parameters
*/
function renderTypeParameters(elm) {
let params = [];
const params = [];
// Length, precision and scale (even if incomplete)
if (elm.length !== undefined) {
if (elm.length !== undefined)
params.push(elm.length);
}
if (elm.precision !== undefined) {
if (elm.precision !== undefined)
params.push(elm.precision);
}
if (elm.scale !== undefined) {
if (elm.scale !== undefined)
params.push(elm.scale);
}
if (elm.srid !== undefined) {
// Geometry types translate into CHAR on Sqlite (give them the default length of 5000)
if (options.toSql.dialect === 'sqlite')
params.push(5000);
// SAP HANA Geometry types translate into CHAR in plain/sqlite (give them the default length of 2000)
if (options.toSql.dialect !== 'hana')
params.push(2000);
else
params.push(elm.srid);
}
// Additional type parameters
// FIXME: Not yet clear how that looks in new CSN
for (let arg of elm.typeArguments || []) {
params.push(arg);
}
return params.length === 0 ? '' : '(' + params.join(', ') + ')';
return params.length === 0 ? '' : `(${params.join(', ')})`;
}
// FIXME: Reuse this together with `toCdl`.
// Render an expression (including paths and values) or condition 'x'.
// (no trailing LF, don't indent if inline)
function renderExpr(x, env, inline=true, nestedExpr=false) {
/**
* Render an expression (including paths and values) or condition 'x'.
* (no trailing LF, don't indent if inline)
*
* @todo Reuse this with toCdl
* @param {Array|object|string} x Expression to render
* @param {object} env Render environment
* @param {boolean} inline Wether to render the expression inline
* @param {boolean} nestedExpr Wether to treat the expression as nested
* @returns {string} Rendered expression
*/
function renderExpr(x, env, inline = true, nestedExpr = false) {
// Compound expression
if (x instanceof Array) {
// Simply concatenate array parts with spaces (with a tiny bit of beautification)
// FIXME: Take this for `toCdl`, too
let tokens = x.map(item => renderExpr(item, env, inline, nestedExpr));
let result = '';
for (let i = 0; i < tokens.length; i++) {
result += tokens[i];
// No space after last token, after opening parentheses, before closing parentheses, before comma
if (i != tokens.length - 1 && tokens[i] !== '(' && ![')', ','].includes(tokens[i + 1])) {
result += ' ';
}
}
return result;
// return x.map(item => renderExpr(item, env, inline, nestedExpr)).join(' ');
return processExprArray(x, renderExpr, env, inline, nestedExpr);
}

@@ -1190,41 +1357,18 @@ else if (typeof x === 'object' && x !== null) {

// FIXME: For the sake of simplicity, we should get away from all this uppercasing in toSql
else {
return String(x).toUpperCase();
}
// Various special cases represented as objects
return String(x).toUpperCase();
/**
* Various special cases represented as objects
*
* @returns {string} String representation of the expression
*/
function renderExprObject() {
// Literal value, possibly with explicit 'literal' property
if (x.val !== undefined) {
switch (x.literal || typeof x.val) {
case 'number':
case 'boolean':
case 'null':
// 17.42, NULL, TRUE
return String(x.val).toUpperCase();
case 'x':
// x'f000'
return `${x.literal}'${x.val}'`;
case 'date':
case 'time':
case 'timestamp':
if (options.toSql.dialect === 'sqlite') {
// simple string literal '2017-11-02'
return `'${x.val}'`;
} else {
// date'2017-11-02'
return `${x.literal}'${x.val}'`;
}
case 'string':
// 'foo', with proper escaping
return `'${x.val.replace(/'/g, "''")}'`;
case 'object':
if (x.val === null) {
return 'NULL';
}
// otherwise fall through to
default:
throw new Error('Unknown literal or type: ' + JSON.stringify(x));
}
if (x.list) {
return `(${x.list.map(item => renderExpr(item)).join(', ')})`;
}
else if (x.val !== undefined) {
return renderExpressionLiteral(x);
}
// Enum symbol

@@ -1235,3 +1379,3 @@ else if (x['#']) {

// FIXME: We can't do enums yet because they are not resolved (and we don't bother finding their value by hand)
signal(error`Enum values are not yet supported for conversion to SQL`, x.$location);
error(null, x.$location, 'Enum values are not yet supported for conversion to SQL');
return '';

@@ -1241,54 +1385,3 @@ }

else if (x.ref) {
if (!x.param && !x.global) {
if(x.ref[0] === '$user') {
// FIXME: this is all not enough: we might need an explicit select item alias
if (x.ref[1] === 'id') {
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.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 if (x.ref[1] === 'locale') {
return options.toSql.dialect === 'sqlite'
? options.toSql.user && options.toSql.user.locale
? `'${options.toSql.user && options.toSql.user.locale}'` : `'en'`
: "SESSION_CONTEXT('LOCALE')";
}
}
else if(x.ref[0] === '$at') {
// return current_time for all $at
if(options.toSql.dialect === 'sqlite') {
return 'current_timestamp';
}
else if(options.toSql.dialect === 'hana') {
if(x.ref[1] === 'from') {
return "SESSION_CONTEXT('VALID-FROM')";
}
else if(x.ref[1] === 'to') {
return "SESSION_CONTEXT('VALID-TO')";
}
}
}
}
// FIXME: We currently cannot distinguish whether '$parameters' was quoted or not - we
// assume that it was not if the path has length 2 (
if (firstPathStepId(x.ref) === '$parameters' && x.ref.length == 2) {
// Parameters must be uppercased and unquoted in SQL
return `:${x.ref[1].toUpperCase()}`;
}
if (x.param) {
return `:${x.ref[0].toUpperCase()}`;
}
return x.ref.map(renderPathStep)
.filter(s => s !== '')
.join('.');
return renderExpressionRef(x);
}

@@ -1302,2 +1395,5 @@ // Function call, possibly with args (use '=>' for named args)

else if (x.xpr) {
if (nestedExpr && !x.cast)
return `(${renderExpr(x.xpr, env, inline, true)})`;
return renderExpr(x.xpr, env, inline, true);

@@ -1314,10 +1410,122 @@ }

}
else {
throw new Error('Unknown expression: ' + JSON.stringify(x));
throw new Error(`Unknown expression: ${JSON.stringify(x)}`);
}
function renderExpressionLiteral(x) {
// Literal value, possibly with explicit 'literal' property
switch (x.literal || typeof x.val) {
case 'number':
case 'boolean':
case 'null':
// 17.42, NULL, TRUE
return String(x.val).toUpperCase();
case 'x':
// x'f000'
return `${x.literal}'${x.val}'`;
case 'date':
case 'time':
case 'timestamp':
if (options.toSql.dialect === 'sqlite') {
// simple string literal '2017-11-02'
return `'${x.val}'`;
}
// date'2017-11-02'
return `${x.literal}'${x.val}'`;
case 'string':
// 'foo', with proper escaping
return `'${x.val.replace(/'/g, '\'\'')}'`;
case 'object':
if (x.val === null)
return 'NULL';
// otherwise fall through to
default:
throw new Error(`Unknown literal or type: ${JSON.stringify(x)}`);
}
}
function renderExpressionRef(x) {
if (!x.param && !x.global) {
if (x.ref[0] === '$user') {
const result = render$user(x);
// Invalid second path step doesn't cause a return
if (result)
return result;
}
else if (x.ref[0] === '$at') {
const result = render$at(x);
// Invalid second path step doesn't cause a return
if (result)
return result;
}
}
// FIXME: We currently cannot distinguish whether '$parameters' was quoted or not - we
// assume that it was not if the path has length 2 (
if (firstPathStepId(x.ref) === '$parameters' && x.ref.length === 2) {
// Parameters must be uppercased and unquoted in SQL
return `:${x.ref[1].toUpperCase()}`;
}
if (x.param)
return `:${x.ref[0].toUpperCase()}`;
return x.ref.map(renderPathStep)
.filter(s => s !== '')
.join('.');
}
/**
* @param {object} x
* @returns {string|null} Null in case of an invalid second path step
*/
function render$user(x) {
// FIXME: this is all not enough: we might need an explicit select item alias
if (x.ref[1] === 'id') {
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}'`;
if (options.toSql.dialect === 'sqlite' || options.toSql.dialect === 'plain') {
warning(null, null, 'The "$user" variable is not supported. Use the "toSql.user" option to set a value for "$user.id"');
return '\'$user.id\'';
}
return 'SESSION_CONTEXT(\'APPLICATIONUSER\')';
}
else if (x.ref[1] === 'locale') {
return (options.toSql.dialect === 'sqlite' || options.toSql.dialect === 'plain')
? options.toSql.user && options.toSql.user.locale
? `'${options.toSql.user && options.toSql.user.locale}'` : '\'en\''
: 'SESSION_CONTEXT(\'LOCALE\')';
}
// Basically: Second path step was invalid, do nothing
return null;
}
/**
* @param {object} x
* @returns {string|null} Null in case of an invalid second path step
*/
function render$at(x) {
// return current_time for all $at
if (options.toSql.dialect === 'sqlite' || options.toSql.dialect === 'plain') {
return 'current_timestamp';
}
else if (options.toSql.dialect === 'hana') {
if (x.ref[1] === 'from')
return 'SESSION_CONTEXT(\'VALID-FROM\')';
else if (x.ref[1] === 'to')
return 'SESSION_CONTEXT(\'VALID-TO\')';
}
return null;
}
/**
* Renders an explicit `cast()` inside an 'xpr'.
* @param {string} value
*
* @param {string} value Value to cast
* @returns {string} CAST statement
*/

@@ -1329,12 +1537,18 @@ function renderExplicitTypeCast(value) {

// Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function
/**
* Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function
*
* @param {string|object} s Path step to render
* @param {number} idx index of the path step in the overall path
* @returns {string} Rendered path step
*/
function renderPathStep(s, idx) {
// Simple id or absolute name
if (typeof(s) === 'string') {
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('APPLICATIONUSER')",
'$user.locale': "SESSION_CONTEXT('LOCALE')",
}
$now: 'CURRENT_TIMESTAMP',
'$user.id': 'SESSION_CONTEXT(\'APPLICATIONUSER\')',
'$user.locale': 'SESSION_CONTEXT(\'LOCALE\')',
};
// Some magic for first path steps

@@ -1344,9 +1558,8 @@ if (idx === 0) {

// FIXME: this is all not enough: we might need an explicit select item alias
if (magicForHana[s]) {
if (magicForHana[s])
return magicForHana[s];
}
// Ignore initial $projection and initial $self
if (s === '$projection' || s === '$self') {
if (s === '$projection' || s === '$self')
return '';
}
}

@@ -1358,9 +1571,9 @@ return quoteSqlId(s);

// Sanity check
if (!s.func && !s.id) {
throw new Error('Unknown path step object: ' + JSON.stringify(s));
}
if (!s.func && !s.id)
throw new Error(`Unknown path step object: ${JSON.stringify(s)}`);
// Not really a path step but an object-like function call
if (s.func) {
if (s.func)
return `${s.func}(${renderArgs(s.args, '=>', env)})`;
}
// Path step, possibly with view parameters and/or filters

@@ -1375,83 +1588,20 @@ let result = `${quoteSqlId(s.id)}`;

// FIXME: Does SQL understand filter cardinalities?
result += `[${s.cardinality ? (s.cardinality.max + ': ') : ''}${renderExpr(s.where, env)}]`;
result += `[${s.cardinality ? (`${s.cardinality.max}: `) : ''}${renderExpr(s.where, env, true, true)}]`;
}
return result;
}
else {
throw new Error('Unknown path step: ' + JSON.stringify(s));
}
}
}
// Returns a copy of 'env' with increased indentation
function increaseIndent(env) {
return Object.assign({}, env, { indent: env.indent + ' ' });
}
// Return 'name' in the form of an absolute CDS name - for the 'hdbcds' naming convention,
// this means converting '.' to '::' on the border between namespace and top-level artifact.
// For all other naming conventions, this is a no-op.
function absoluteCdsName(name) {
if (options.toSql.names !== 'hdbcds') {
return name;
throw new Error(`Unknown path step: ${JSON.stringify(s)}`);
}
let topLevelName = getTopLevelArtifactNameOf(name, csn);
let namespaceName = getParentNameOf(topLevelName);
if (namespaceName) {
return `${namespaceName}::${name.substring(namespaceName.length + 1)}`;
}
return name;
}
/**
* Prepare an identifier:
* If 'options.toSql.names' is 'plain'
* - replace '.' or '::' by '_'
* else if 'options.toSql.names' is 'quoted'
* - replace '::' by '.'
*
* @param {String} name
*
* @returns {String}
* Returns a copy of 'env' with increased indentation
*
* @param {object} env Render environment
* @returns {object} Render environment with increased indent
*/
function prepareIdentifier(name) {
// Sanity check
if(options.toSql.dialect === 'sqlite' && options.toSql.names !== 'plain') {
throw new Error(`Not expecting ${options.toSql.names} names for 'sqlite' dialect`);
}
switch(options.toSql.names){
case 'plain':
return name.replace(/(\.|::)/g, '_');
case 'quoted':
return name.replace(/::/g, '.');
case 'hdbcds':
return name;
}
throw new Error(`No matching rendering found for naming mode ${options.toSql.names}`);
function increaseIndent(env) {
return Object.assign({}, env, { indent: `${env.indent} ` });
}
// Return 'name' with appropriate "-quotes.
// Additionally perform the following conversions on 'name'
// If 'options.toSql.names' is 'plain'
// - replace '.' or '::' by '_'
// else if 'options.toSql.names' is 'quoted'
// - replace '::' by '.'
// Complain about names that collide with known SQL keywords or functions
function quoteSqlId(name) {
name = prepareIdentifier(name);
switch(options.toSql.names){
case 'plain':
return smartId(name, options.toSql.dialect);
case 'quoted':
return delimitedId(name, options.toSql.dialect);
case 'hdbcds':
return delimitedId(name, options.toSql.dialect);
}
return undefined;
}
}

@@ -1458,0 +1608,0 @@

'use strict';
const alerts = require('../base/alerts');
const { handleMessages } = require('../base/messages');
const { setProp, isBetaEnabled } = require('../base/model');
const { handleMessages, makeMessageFunction } = require('../base/messages');
const { isDeprecatedEnabled } = require('../base/model');
const transformUtils = require('./transformUtilsNew');
const { mergeOptions } = require('../model/modelUtils');
const { getUtils,
cloneCsn,
forEachGeneric,
forEachDefinition,
forEachMemberRecursively,
forEachRef,
isEdmPropertyRendered,
isBuiltinType,
getArtifactDatabaseNameOf,
getElementDatabaseNameOf } = require('../model/csnUtils');
const { checkCSNVersion } = require('../json/csnVersion');
const validator = require('../checks/csn/validator');
const { validateOnCondition, validateMixinOnCondition } = require('../checks/csn/onConditions');
const validateForeignKeys = require('../checks/csn/foreignKeys');
const { validateDefaultValues }= require('../checks/csn/defaultValues');
const validateAssociationsInArrayOf = require('../checks/csn/assocsInArrayOf');
const typesExposure = require('./odata/typesExposure');
const validate = require('../checks/validator');
const { isArtifactInSomeService, getServiceOfArtifact, isLocalizedArtifactInService } = require('./odata/utils');
const ReferenceFlattener = require('./odata/referenceFlattener');
const { flattenCSN } = require('./odata/structureFlattener');
const processForeignKeys = require('./odata/generateForeignKeyElements');
const generateForeignKeys = require('./odata/generateForeignKeyElements');
const expandStructKeysInAssociations = require('./odata/expandStructKeysInAssociations');
const expandToFinalBaseType = require('./odata/toFinalBaseType');
const timetrace = require('../utils/timetrace');
const { addLocalizationViews, hasLocalizedConvenienceView, isInLocalizedNamespace } = require('./localized');
// Transformation for ODATA. Expects a CSN 'inputModel', processes it for ODATA.
// The twin of forOdata.js
// The result should be suitable for consumption by EDMX processors (annotations and metadata)
// and also as a final CSN output for the ODATA runtime (when compacted).
// Performs the following transformations:
//
// (0) Semantic checks before flattening regarding temporal data
// (1.1) Flatten structured elements (and foreign keys of managed associations pointing to
// keys that are themselves managed associations).
// TODO: probably only for OData V2?
// (1.2) Expose (named or anonymous) structured types used in structured types
// (1.3) Unravel derived types for elements, actions, action parameters, types and
// annotations (propagating annotations)
// (1.4) Mark fields with @odata.on.insert/update as @Core.Computed
// (1.5) Rename shorthand annotations according to a builtin list.
// and also as a final CSN output for the ODATA runtime.
// Performs the following:
// - Validate the input model. (forODataNew Candidate)
// - Unravel derived types for elements, actions, action parameters, types and
// annotations (propagating annotations).
// (EdmPreproc Candidate, don't know if flatten step depends on it)
// - If we execute in flat mode, flatten:
// -- structured elements
// -- all the references in the model
// -- foreign keys of managed associations (cover also the case when the foreign key is
// pointing to keys that are themselves managed associations)
// (long term EdmPreproc Candidate when RTs are able to map to flat)
// - Generate foreign keys for all the managed associations in the model as siblings to the association
// where ever the association is located (toplevel in flat or deep structured). (forODataNew Candidate)
// - Tackle on-conditions in unmanaged associations. In case of flat mode - flatten the
// on-condition, in structured mode - normalize it. (forODataNew Candidate)
// - Generate artificial draft fields if requested. (forODataNew Candidate)
// - Check associations for:
// TODO: move to validator (Is this really required here?
// EdmPreproc cuts off assocs or adds proxies/xrefs)
// -- exposed associations do not point to non-exposed targets
// -- structured types must not contain associations for OData V2
// - Element must not be an 'array of' for OData V2 TODO: move to the validator
// (Linter Candiate, move as hard error into EdmPreproc on V2 generation)
// - Perform checks for exposed non-abstract entities and views - check media type and
// key-ness (requires that containers have been identified) (Linter candidate, scenario check)
// Annotations related:
// - Annotate artifacts, elements, foreign keys, parameters etc with their DB names if requested
// (must remain in CSN => ForODataNewCandidate)
// - Mark fields with @odata.on.insert/update as @Core.Computed
// (EdmPreproc candidate, check with RT if @Core.Computed required by them)
// - Rename shorthand annotations according to a builtin list (EdmPreproc Candidate)
// e.g. @label -> @Common.Label or @important: [true|false] -> @UI.Importance: [#High|#Low]
// (1.6) Check annotations. If annotation starts with '@sap...' it must have a string or boolean value
// Transformations (1.1), (1.2), (1.3), (1.4), (1.5) and (1.6) are executed during the first walk through the model
//
// (2.1) For exposed actions and functions that use non-exposed or anonymous structured types,
// create artificial exposing types
// Transformation (2.1) is a second walk through the definitions
//
// (3.1) Generate foreign key fields for managed associations, depends on (1.1) in V4 or flatten case
// (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
//
// (4.1) Generate artificial draft fields if requested
// (4.3) Check associations for:
// - exposed associations do not point to non-exposed targets
// - structured types must not contain associations for OData V2
// (4.4) Element must not be an 'array of' for OData V2
// (4.5) If the association target is annotated with @cds.odata.valuelist, annotate the
// association with @Common.ValueList.viaAssociation
// (4.6) Check for @Analytics.Measure and @Aggregation.default
// Fourth loop: (4.1) to (4.6)
//
// (5.1) Annotate artifacts, elements, foreign keys, parameters etc with their DB names if requested
// (5.2) Perform checks for exposed non-abstract entities and views - check media type and
// key-ness (requires that containers have been identified in the fourth pass (4.2))
// Fifth walk through the model with (5.1) and (5.2)
// - If the association target is annotated with @cds.odata.valuelist, annotate the
// association with @Common.ValueList.viaAssociation (EdmPreproc Candidate)
// - Check for @Analytics.Measure and @Aggregation.default (Linter check candidate, remove)
// - Check annotations. If annotation starts with '@sap...' it must have a string or boolean value
// (Linter check candidate)
module.exports = { transform4odataWithCsn, getServiceNames };
function transform4odataWithCsn(inputModel, options) {
if (options.messages) reclassifyWarnings(options);
else if (inputModel.messages) reclassifyWarnings(inputModel);
// Throw exception in case of errors
handleMessages(inputModel, options);
timetrace.start('OData transformation');
// copy the model as we don't want to change the input model
// TODO: set protos to null here -> call the function from lib/json/nullprotos3.js
let csn = cloneCsn(inputModel);
let csn = cloneCsn(inputModel, options);
options = mergeOptions(inputModel.options, options);
setProp(csn, 'options', options);
const { error, warning, info, throwWithError } = makeMessageFunction(csn, options, 'for.odata');
throwWithError();
const { error, warning, info } = alerts.makeMessageFunction(csn, options);
addLocalizationViews(csn, options);
const keepLocalizedViews = isDeprecatedEnabled(options, 'createLocalizedViews');
forEachDefinition(csn, (artifact, artifactName) => {
if (hasLocalizedConvenienceView(csn, artifactName))
artifact.$localized = true;
else if (!keepLocalizedViews && isInLocalizedNamespace(artifactName))
delete csn.definitions[artifactName];
});

@@ -99,15 +89,10 @@ // the new transformer works only with new CSN

const {
createForeignKeyElement,
flattenOnCond,
checkExposedAssoc, toFinalBaseType,
createForeignKeyElement, flattenOnCond,
createAndAddDraftAdminDataProjection, createScalarElement,
createAssociationElement, createAssociationPathComparison,
addElement, createAction,
addAction,
addElement, createAction, assignAction,
checkForeignKeys, extractValidFromToKeyElement,
checkAssignment, checkMultipleAssignments,
recurseElements,
setAnnotation,
renameAnnotation,
expandStructsInOnConditions,
recurseElements, setAnnotation, renameAnnotation,
expandStructsInOnConditions
} = transformers;

@@ -119,4 +104,2 @@

getFinalType,
getFinalTypeDef,
getNamespaceOfArtifact,
getServiceName,

@@ -140,51 +123,26 @@ hasBoolAnnotation,

//handles reference flattening
let referenceFlattener = new ReferenceFlattener();
referenceFlattener.attachPaths(csn);
referenceFlattener.resolveAllReferences(csn, inspectRef, isStructured);
// (0) Semantic checks before flattening regarding temporal data and array of
forEachDefinition(csn, (artifact, artifactName, propertyName, path) => {
// Gather all element names with @cds.valid.from/to/key
let validFrom = [], validTo = [], validKey = [];
recurseElements(artifact, ['definitions', artifactName], (member, path) => {
let [f, t, k] = extractValidFromToKeyElement(member, path);
validFrom.push(...f);
validTo.push(...t);
validKey.push(...k);
});
// Check that @cds.valid.from/to/key is only in valid places
validFrom.forEach(obj => checkAssignment('@cds.valid.from', obj.element, obj.path, artifact));
validTo.forEach(obj => checkAssignment('@cds.valid.to', obj.element, obj.path, artifact));
validKey.forEach(obj => checkAssignment('@cds.valid.key', obj.element, obj.path, artifact));
checkMultipleAssignments(validFrom, '@cds.valid.from', artifact, artifactName);
checkMultipleAssignments(validTo, '@cds.valid.to', artifact, artifactName);
checkMultipleAssignments(validKey, '@cds.valid.key', artifact, artifactName);
if (validKey.length && !(validFrom.length && validTo.length)) {
error(null, path, '@cds.valid.key was used but @cds.valid.from and @cds.valid.to are missing');
}
validate.forOdata(csn, {
error, warning, info, inspectRef, effectiveType, artifactRef, csn, options, csnUtils, services, getFinalBaseType
});
// (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]);
});
referenceFlattener.attachPaths(csn);
// Throw exception in case of errors
handleMessages(csn, options);
validator(csn, {
error, warning, info, inspectRef, effectiveType, artifactRef, csn, getFinalBaseType,
},
/* Member Validators */ [ validateOnCondition, validateForeignKeys, validateAssociationsInArrayOf, validateDefaultValues ],
/* artifact validators */ [],
/* query validators */ [ validateMixinOnCondition ]);
// Semantic checks before flattening regarding temporal data
// TODO: Move in the validator
forEachDefinition(csn, [
checkTemporalAnnotationsAssignment,
(def) => {
// Convert a projection into a query for internal processing will be re-converted
// at the end of the OData processing
// TODO: handle artifact.projection instead of artifact.query correctly in future V2
if (def.kind === 'entity' && def.projection) {
def.query = { SELECT: def.projection };
}
}]
);
expandToFinalBaseType(csn, transformers, csnUtils, services, options);
// Check if structured elements and managed associations are compared in an ON condition

@@ -194,81 +152,16 @@ // and expand these structured elements. This tuple expansion allows all other

// If errors are detected, handleMessages will return from further processing
forEachDefinition(csn, expandStructsInOnConditions);
// handles reference flattening
let referenceFlattener = new ReferenceFlattener();
referenceFlattener.resolveAllReferences(csn, inspectRef, isStructured);
referenceFlattener.attachPaths(csn);
// Throw exception in case of errors
handleMessages(csn, options);
referenceFlattener.applyAliasesInOnCond(csn, inspectRef);
// (1.1) Unravel derived type chains for types and annotations (propagating annotations)
forEachDefinition(csn, (def, defName) => {
if (def.kind !== 'entity') {
let finalTypeDef = def;
if (def.type && def.type.ref) {
finalTypeDef = artifactRef(def.type);
if (!finalTypeDef.type) {
error(null, ['definitions', defName], { name: defName }, `${ defName } has no final type`);
return;
}
}
if (!isStructured(finalTypeDef)) {
try {
toFinalBaseType(def);
} catch (ex) {
error(null, ['definitions', defName], { name: defName }, `Final base type of ${ defName } not found`);
return
}
}
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;
}
// In case we have in the model something like:
// type Foo: array of Bar; type Bar: { qux: Integer };
// In the type Foo we expand the first level of elements of the items or
// type Foo: array of { qux: Integer };
if (def.kind === 'type' && def.items && isArtifactInSomeService(defName, services)) {
if(expandFirstLevelOfArrayed(def)) {
referenceFlattener.attachPaths(def,['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???
if (!structuredOData) {
flattenCSN(csn, csnUtils, referenceFlattener, error);
// flatten structures
flattenCSN(csn, csnUtils, options, referenceFlattener, error);
// flatten references
referenceFlattener.flattenAllReferences(csn);
}

@@ -279,37 +172,29 @@

if (!(structuredOData && (isBetaEnabled(options, 'odataProxies') && (options.toOdata.odataProxies || options.toOdata.odataXServiceRefs))))
// Expose user-defined types and anonymous types
typesExposure(csn, services, options, csnUtils, { error }, referenceFlattener);
// flatten references, attach new paths
referenceFlattener.flattenAllReferences(csn);
referenceFlattener.attachPaths(csn);
let flatKeys = !structuredOData || (structuredOData && options.toOdata.odataForeignKeys);
// Process associations
// 1. expand structured foreign keys, rewrite the 'ref' for such keys
expandStructKeysInAssociations(csn, referenceFlattener, csnUtils);
// 1. In case we generate flat mode, expand the structured foreign keys.
// This logic rewrites the 'ref' for such keys with the corresponding flattened
// elements.
if (!structuredOData)
expandStructKeysInAssociations(csn, referenceFlattener, csnUtils);
// 2. generate foreign keys for managed associations
processForeignKeys(csn, flatKeys, referenceFlattener, csnUtils, transformers, error);
generateForeignKeys(csn, options, referenceFlattener, csnUtils, error);
// Flatten on-conditions in unmanaged associations
// This must be done before 4.1, since all composition targets are annotated with @odata.draft.enabled in this step
// This must be done before all the draft logic as all
// composition targets are annotated with @odata.draft.enabled in this step
forEachDefinition(csn, processOnCond);
// Fourth walk through the model: Now all artificially generated things are in place
// (4.1) Generate artificial draft fields if requested
// Now all artificially generated things are in place
// - Generate artificial draft fields if requested
// TODO: should be done by the compiler - Check associations for valid foreign keys
// TODO: checkif needed at all: Remove '$projection' from paths in the element's ON-condition
// (4.2) Check associations for:
// TODO: check if needed at all: Remove '$projection' from paths in the element's ON-condition
// - Check associations for:
// - exposed associations do not point to non-exposed targets
// - structured types must not contain associations for OData V2
// (4.3) Element must not be an 'array of' for OData V2
// (4.4) If the association target is annotated with @cds.odata.valuelist, annotate the
// association with @Common.ValueList.viaAssociation
// (4.5) Check for @Analytics.Measure and @Aggregation.default
let visitedArtifacts = {};
// - Element must not be an 'array of' for OData V2 TODO: move to the validator
// - Perform checks for exposed non-abstract entities and views - check media type and key-ness
let visitedArtifacts = Object.create(null);
forEachDefinition(csn, (def, defName) => {
if (def.kind === 'entity' || def.kind === 'view') {
// (4.1) Generate artificial draft fields if requested
// Generate artificial draft fields if requested
if (def['@odata.draft.enabled']) {

@@ -319,3 +204,3 @@ // Ignore if not part of a service

warning(null, ['definitions', defName], { art: defName },
`Ignoring annotation "@odata.draft.enabled" because artifact "${ defName }" is not part of a service`);
'Ignoring annotation “@odata.draft.enabled” because artifact $(ART) is not part of a service');
}

@@ -327,4 +212,3 @@ else {

for (let elemName in def.elements) {
let elem = def.elements[elemName];
def.elements && Object.entries(def.elements).forEach( ([elemName, elem]) => {
// Check for valid foreign keys

@@ -334,165 +218,84 @@ if (isAssocOrComposition(elem.type)) {

}
}
});
}
forEachMemberRecursively(def, (member, memberName, propertyName, path) => {
if (isArtifactInSomeService(defName, services) || isLocalizedArtifactInService(defName, services)) {
let service = getServiceOfArtifact(defName, services);
// (4.2) Check associations
if (isAssocOrComposition(member.type)) {
// Check that exposed associations do not point to non-exposed targets
checkExposedAssoc(defName, member, memberName, service);
// CDXCORE-457
if (def.kind === 'type' && options.toOdata.version === 'v2') {
warning(null, path,
`"${defName}.${memberName}": Structured types must not contain associations for OData V2`);
}
}
// (4.3) CDXCORE-458
else if (propertyName === 'elements' && member.items) {
if (options.toOdata.version === 'v2') {
error(null, path,
`"${defName}.${memberName}": Element must not be an "array of" for OData V2`);
}
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) {
// error(null, path, `"${defName}.${memberName}": Element must not be an "array of anonymous type"`);
// }
}
}
// (4.4) If the member is an association and the target is annotated with @cds.odata.valuelist,
// annotate the association with @Common.ValueList.viaAssociation (but only for service member artifacts
// to avoid CSN bloating). The propagation of the @Common.ValueList.viaAssociation annotation
// to the foreign keys is done very late in edmPreprocessor.initializeAssociation()
addCommonValueListviaAssociation(member, memberName);
// (4.5) cdx/cds-compiler#837
// add check here for @Analytics.Measure and @Aggregation.default
// @Analytics has scope element
if (member['@Analytics.Measure'] && !member['@Aggregation.default']) {
info(null, path, // ['definitions', defName, 'elements', memberName]
`'@Analytics.Measure' expects '@Aggregation.default' to be assigned as well in element '${defName}.${memberName}'`,
);
}
}
visitedArtifacts[defName] = true;
}, ['definitions', defName]);
visitedArtifacts[defName] = true;
});
// Fifth walk through the model:
// (5.1) Annotate artifacts, elements, foreign keys, parameters etc with their DB names if requested
// (5.2) Perform checks for exposed non-abstract entities and views - check media type and
// key-ness (requires that containers have been identified in the fourth pass (4.2))
let illV2Prefix = RegExp('^(_|[0-9])');
// Deal with all kind of annotations manipulations here
forEachDefinition(csn, (def, defName) => {
// (5.1) Annotate artifacts, elements, foreign keys, parameters etc with their DB names if requested
if (options.toOdata.names) {
// Skip artifacts that have no DB equivalent anyway
if (!['service', 'context', 'namespace', 'annotation', 'action', 'function'].includes(def.kind)) {
def['@cds.persistence.name'] = getArtifactDatabaseNameOf(defName, options.toOdata.names, getNamespaceOfArtifact(defName));
}
forEachMemberRecursively(def, (member, memberName, prop) => {
// Only these are actually required and don't annotate virtual elements in entities or types
// as they have no DB representation (although in views)
if (typeof member === 'object' && !['action', 'function'].includes(member.kind) && prop !== 'enum' && (!member.virtual || def.query)) {
// If we have a 'preserved dotted name' (i.e. we are a result of flattening), use that for the @cds.persistence.name annotation
if (member._flatElementNameWithDots) {
memberName = member._flatElementNameWithDots;
}
member['@cds.persistence.name'] = getElementDatabaseNameOf(memberName, options.toOdata.names);
}
});
}
// Resolve annotation shorthands for entities, types, annotations, ...
renameShorthandAnnotations(def);
// (5.2) Perform checks for exposed non-abstract entities and views
if (isArtifactInSomeService(defName, services) && !def.abstract && (def.kind === 'entity' || def.kind === 'view')) {
/** @type {[string, CSN.Element][]} */
let mediaTypes = [];
// Walk the elements
// Annotate artifacts with their DB names if requested.
// Skip artifacts that have no DB equivalent anyway
if (options.toOdata.names && !['service', 'context', 'namespace', 'annotation', 'action', 'function'].includes(def.kind))
def['@cds.persistence.name'] = getArtifactDatabaseNameOf(defName, options.toOdata.names, csn);
for (let elemName in def.elements) {
let elem = def.elements[elemName];
// For ODATA V2, element names must not start with digit or '_'
if (options.toOdata.version === 'v2') {
if (illV2Prefix.test(elemName)) {
error(null, ['definitions', defName, 'elements', elemName],
`"${defName}.${elemName}: Element name must not begin with '${elemName[0]}' for OData V2`);
}
forEachMemberRecursively(def, (member, memberName, propertyName) => {
// Annotate elements, foreign keys, parameters, etc. with their DB names if requested
// Only these are actually required and don't annotate virtual elements in entities or types
// as they have no DB representation (although in views)
if (options.toOdata.names && typeof member === 'object' && !['action', 'function'].includes(member.kind) && propertyName !== 'enum' && (!member.virtual || def.query)) {
// If we have a 'preserved dotted name' (i.e. we are a result of flattening), use that for the @cds.persistence.name annotation
if (member._flatElementNameWithDots) {
memberName = member._flatElementNameWithDots;
}
// Count keys and elements annotated with @Core.MediaType
if (elem['@Core.MediaType']) {
mediaTypes.push([elemName, elem]);
}
member['@cds.persistence.name'] = getElementDatabaseNameOf(memberName, options.toOdata.names);
}
// Additional checks for ODATA V2 regarding remaining keys
if (options.toOdata.version === 'v2') {
// Today only one MediaType is allowed in V2
if (mediaTypes.length > 1) {
error(null, ['definitions', defName], `"${defName}: Multiple elements [${mediaTypes.map(e => e[0]).join(', ')}] annotated with '@Core.MediaType', OData V2 allows only one`);
}
}
// Check media type compatibility for all OData versions
let allowedTypes = ['cds.String', 'cds.Binary', 'cds.LargeBinary'];
mediaTypes.forEach(e => {
if (!allowedTypes.includes(e[1].type)) {
error(null, ['definitions', defName, 'elements', e[0]], `"${defName}.${e[0]}": Element annotated with '@Core.MediaType' must be of either type "${allowedTypes.join(', ')}"`);
}
});
}
});
// Deal with all kind of annotations manipulations here
forEachDefinition(csn, (def, defName, propertyName, path) => {
// (1.5) Resolve annotation shorthands for entities, types, annotations, ...
renameShorthandAnnotations(def, path);
// (1.6) check annotations
checkAnnotations(def, ['definitions', defName]);
forEachMemberRecursively(def, (member, memberName, propertyName, path) => {
// (1.2) Mark fields with @odata.on.insert/update as @Core.Computed
// 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);
// Resolve annotation shorthands for elements, actions, action parameters
renameShorthandAnnotations(member);
// (1.4) check annotations
checkAnnotations(member, path);
// - If the association target is annotated with @cds.odata.valuelist, annotate the
// association with @Common.ValueList.viaAssociation
// - Check for @Analytics.Measure and @Aggregation.default
if (isArtifactInSomeService(defName, services) || isLocalizedArtifactInService(defName, services)) {
// If the member is an association and the target is annotated with @cds.odata.valuelist,
// annotate the association with @Common.ValueList.viaAssociation (but only for service member artifacts
// to avoid CSN bloating). The propagation of the @Common.ValueList.viaAssociation annotation
// to the foreign keys is done very late in edmPreprocessor.initializeAssociation()
addCommonValueListviaAssociation(member, memberName);
}
}, ['definitions', defName]);
// Convert a query back into a projection for CSN compliance as
// the very last conversion step of the OData transformation
if (def.kind === 'entity' && def.query && def.query && def.projection) {
delete def.query;
}
})
/**
* 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) {
return csn.messages.map(message => {
switch (message.messageId) {
case 'enum-value-ref':
case 'check-proper-type-of':
case 'rewrite-not-supported':
case 'rewrite-undefined-key':
message.severity = 'Error';
break;
}
return message;
});
}
// Throw exception in case of errors
handleMessages(csn, options);
if (options.messages) setProp(csn, 'messages', options.messages);
if (options.testMode) csn = cloneCsn(csn, options); // sort, keep hidden properties
timetrace.stop();
return csn;
// (1.4) Mark elements that are annotated with @odata.on.insert/update with the annotation @Core.Computed.
// Implements: CDXCORE-62
// TODO: Move this to checks?
function checkTemporalAnnotationsAssignment(artifact, artifactName, propertyName, path) {
// Gather all element names with @cds.valid.from/to/key
let validFrom = [], validTo = [], validKey = [];
recurseElements(artifact, ['definitions', artifactName], (member, path) => {
let [f, t, k] = extractValidFromToKeyElement(member, path);
validFrom.push(...f);
validTo.push(...t);
validKey.push(...k);
});
// Check that @cds.valid.from/to/key is only in valid places
validFrom.forEach(obj => checkAssignment('@cds.valid.from', obj.element, obj.path, artifact));
validTo.forEach(obj => checkAssignment('@cds.valid.to', obj.element, obj.path, artifact));
validKey.forEach(obj => checkAssignment('@cds.valid.key', obj.element, obj.path, artifact));
checkMultipleAssignments(validFrom, '@cds.valid.from', artifact, artifactName);
checkMultipleAssignments(validTo, '@cds.valid.to', artifact, artifactName);
checkMultipleAssignments(validKey, '@cds.valid.key', artifact, artifactName);
if (validKey.length && !(validFrom.length && validTo.length)) {
error(null, path, 'Annotation “@cds.valid.key” was used but “@cds.valid.from” and “@cds.valid.to” are missing');
}
}
// Mark elements that are annotated with @odata.on.insert/update with the annotation @Core.Computed.
function annotateCoreComputed(node) {

@@ -507,5 +310,5 @@ // If @Core.Computed is explicitly set, don't overwrite it!

// (1.5) Rename shorthand annotations within artifact or element 'node' according to a builtin
// Rename shorthand annotations within artifact or element 'node' according to a builtin
// list.
function renameShorthandAnnotations(node, path) {
function renameShorthandAnnotations(node) {
// FIXME: Verify this list - are they all still required? Do we need any more?

@@ -524,8 +327,3 @@ const mappings = {

let rewriteCapabilities = true;
if (node['@readonly'] && node['@insertonly'] && ['entity', 'view'].includes(node.kind)) {
rewriteCapabilities = false;
warning(null, path, '"@readonly" and "@insertonly" cannot be assigned in combination');
}
for (let name in node) {
Object.keys(node).forEach( name => {
// Rename according to map above

@@ -539,3 +337,4 @@ if (mappings[name] != undefined)

let annotation = node['@UI.Importance'];
node['@UI.Importance'] = { '#': annotation ? 'High' : 'Low' }
if (annotation !== null)
node['@UI.Importance'] = { '#': annotation ? 'High' : 'Low' }
}

@@ -545,4 +344,4 @@

// but '@Core.Immutable' for everything else.
if (rewriteCapabilities) {
if (name === '@readonly') {
if (!(node['@readonly'] && node['@insertonly'])) {
if (name === '@readonly' && node[name] !== null) {
if (node.kind === 'entity' || node.kind === 'view') {

@@ -557,3 +356,3 @@ setAnnotation(node, '@Capabilities.DeleteRestrictions.Deletable', false);

// @insertonly is effective on entities/queries only
else if (name === '@insertonly') {
else if (name === '@insertonly' && node[name] !== null) {
if (node.kind === 'entity' || node.kind === 'view') {

@@ -567,20 +366,31 @@ setAnnotation(node, '@Capabilities.DeleteRestrictions.Deletable', false);

// Only on element level: translate @mandatory
if (name === '@mandatory' && node.kind === undefined && node['@Common.FieldControl'] === undefined) {
if (name === '@mandatory' && node[name] !== null &&
node.kind === undefined && node['@Common.FieldControl'] === undefined) {
setAnnotation(node, '@Common.FieldControl', { '#': 'Mandatory' });
}
if (name === '@assert.format') setAnnotation(node, '@Validation.Pattern', node['@assert.format']);
if (name === '@assert.format' && node[name] !== null)
setAnnotation(node, '@Validation.Pattern', node['@assert.format']);
if (name === '@assert.range') {
if (name === '@assert.range' && node[name] !== null) {
if (Array.isArray(node['@assert.range']) && node['@assert.range'].length === 2) {
setAnnotation(node, '@Validation.Minimum', node['@assert.range'][0]);
setAnnotation(node, '@Validation.Maximum', node['@assert.range'][1]);
} else if (node.enum) {
let enumValue = Object.keys(node.enum).map( enumName => {
let result = { '@Core.SymbolicName': enumName };
if (node.enum[enumName].val !== undefined)
result.Value = node.enum[enumName].val;
else if(node.type && node.type === 'cds.String')
}
// for enums @assert.range changes into a boolean annotation
else if (node.enum && node[name] === true) {
let enumValue = Object.keys(node.enum).map(enumSymbol => {
const enumSymbolDef = node.enum[enumSymbol];
let result = { '@Core.SymbolicName': enumSymbol };
if (enumSymbolDef.val !== undefined)
result.Value = enumSymbolDef.val;
else if (node.type && node.type === 'cds.String')
// the symbol is used as value only for type 'cds.String'
result.Value = enumName;
result.Value = enumSymbol;
// Can't rely that @description has already been renamed to @Core.Description
// Eval description according to precedence (doc comment must be considered already in Odata transformer
// as in contrast to the other doc commments as it is used to annotate the @Validation.AllowedValues)
const desc = enumSymbolDef['@Core.Description'] || enumSymbolDef['@description'] || enumSymbolDef.doc;
if (desc)
result['@Core.Description'] = desc;
return result;

@@ -591,35 +401,6 @@ });

}
}
});
}
// (1.6) Apply checks to all annotations in the model
// node: artifact/element/action/function/parameter/... that carries the annotations
function checkAnnotations(node, defPath) {
// currently there is only one check: annotations @sap:... must have a string or boolean value
// or no value (as shorcut for boolean value true)
let annoNames = Object.keys(node).filter(x => x.startsWith('@sap.'));
for (let name of annoNames) {
if (typeof node[name] !== 'boolean' && typeof node[name] !== 'string') {
warning(null, defPath, { name }, `Annotation "${ name }" must have a string or boolean value`);
}
}
}
// In case we have in the model something like:
// type Foo: array of Bar; type Bar: { qux: Integer };
// In the type Foo we expand the first level of elements of the items like we have in CDL this:
// type Foo: array of { qux: Integer };
function expandFirstLevelOfArrayed(def) {
if (def.items.type && !isBuiltinType(def.items.type)) {
let finalType = getFinalTypeDef(def.items.type);
if (isStructured(finalType)) {
def.items.elements = cloneCsn(finalType.elements);
delete def.items.type;
return true;
}
}
return false;
}
// (3.2) Flatten on-conditions in unmanaged associations
// Flatten on-conditions in unmanaged associations
function processOnCond(def, defName) {

@@ -633,19 +414,19 @@ const rootPath = ['definitions', defName];

else // structured-mode
normalizeOnCondForStructuredMode(member)
normalizeOnCondForStructuredMode(member);
}
});
}
// 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);
});
// 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,
// 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.

@@ -669,21 +450,2 @@ // 'rootArtifact' is the root artifact where composition traversal started.

// extract EDM exposed keys for UUID inspection
let keys = [];
forEachGeneric(artifact, 'elements', (elt) => {
if (elt.key && elt.key === true && isEdmPropertyRendered(elt, options))
keys.push(elt);
});
// A draft enabled entity should *expose* exactly one primary key of type cds.UUID in the EDM document
if (keys.length !== 1) {
warning(null, ['definitions', artifactName], `"${artifactName}": "@odata.draft.enabled" - Entity should expose exactly one key element`);
}
let uuidCount = keys.reduce((uuidCount, k) => {
return k.type === 'cds.UUID' ? ++uuidCount : uuidCount;
}, 0);
if (uuidCount === 0) {
warning(null, ['definitions', artifactName], `"${artifactName}": "@odata.draft.enabled" - Entity key element should be of type "cds.UUID"`);
}
// Generate the DraftAdministrativeData projection into the service, unless there is already one

@@ -698,3 +460,3 @@ let draftAdminDataProjectionName = `${getServiceOfArtifact(artifactName, services)}.DraftAdministrativeData`;

error(null, ['definitions', draftAdminDataProjectionName], { name: draftAdminDataProjectionName },
`Generated entity "${ draftAdminDataProjectionName }" conflicts with existing artifact`);
`Generated entity $(NAME) conflicts with existing artifact`);
}

@@ -710,4 +472,3 @@ // Generate the annotations describing the draft actions (only draft roots can be activated/edited)

for (let elemName in artifact.elements) {
let elem = artifact.elements[elemName];
artifact.elements && Object.values(artifact.elements).forEach( elem => {
// Make all non-key elements nullable

@@ -717,3 +478,3 @@ if (elem.notNull && elem.key !== true) {

}
}
});
// Generate the additional elements into the draft-enabled artifact

@@ -761,4 +522,3 @@

// Iterate elements
for (let elemName in artifact.elements) {
let elem = artifact.elements[elemName];
artifact.elements && Object.entries(artifact.elements).forEach( ([elemName, elem]) => {
if (elemName !== 'IsActiveEntity' && elem.key) {

@@ -782,11 +542,12 @@ // Amend the ON-condition above:

if (hasBoolAnnotation(draftNode, '@odata.draft.enabled', true)) {
error(null, ['definitions', artifactName, 'elements', elemName], `"${artifactName}.${elemName}": Composition in draft-enabled entity cannot lead to another entity with "@odata.draft.enabled"`);
error(null, ['definitions', artifactName, 'elements', elemName], 'Composition in draft-enabled entity can\'t lead to another entity with “@odata.draft.enabled”');
}
// Ignore composition if not part of a service
else if (!getServiceName(elem.target)) {
warning(null, ['definitions', artifactName, 'elements', elemName], `Target "${elem.target}" of composition "${artifactName}.${elemName}" cannot be a draft node because it is not part of a service`);
continue;
warning(null, ['definitions', artifactName, 'elements', elemName], { target: elem.target, name: `${artifactName}:${elemName}` },
`Target $(TARGET) of composition $(NAME) can't be a draft node because it is not part of a service`);
return;
}
else if (hasBoolAnnotation(draftNode, '@odata.draft.enabled', false)) {
continue;
return;
}

@@ -799,3 +560,3 @@ else {

}
}
});

@@ -806,3 +567,3 @@ // Generate the actions into the draft-enabled artifact (only draft roots can be activated/edited)

let draftPrepare = createAction('draftPrepare', artifactName, 'SideEffectsQualifier', 'cds.String');
addAction(draftPrepare, artifact, artifactName);
assignAction(draftPrepare, artifact);

@@ -812,7 +573,7 @@ if (artifact == rootArtifact) {

let draftActivate = createAction('draftActivate', artifactName);
addAction(draftActivate, artifact, artifactName);
assignAction(draftActivate, artifact);
// action draftEdit (PreserveChanges: Boolean) return <artifact>;
let draftEdit = createAction('draftEdit', artifactName, 'PreserveChanges', 'cds.Boolean');
addAction(draftEdit, artifact, artifactName);
assignAction(draftEdit, artifact);
}

@@ -819,0 +580,0 @@ }

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

/**
* This module runs through the model and for each managed assoication in it,
* This module runs through the model and for each managed association in it,
* in case the foreign keys are structured, it is expanding them. Example:
* entity A {
* entity A {
* toB: association to B { stru };
* } // -> CSN: keys:[ { ref:['stru'] } ]
*
* entity B {
* stru: {
*
* entity B {
* stru: {
* subid: Integer;

@@ -42,3 +42,7 @@ * }

generatedElements.forEach(elementName => {
newKeys.push({ ref: [elementName], as: elementName });
let newRef = { ref: [elementName] };
if (key.as) {
newRef.as = elementName.replace(key.ref[0], key.as);
}
newKeys.push(newRef);
})

@@ -45,0 +49,0 @@ continue;

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

const { copyAnnotations } = require('../../model/modelUtils');
const { forEach } = require('../udict');
const { copyAnnotations } = require('../../model/csnUtils');
const sortByAssociationDependency = require('./sortByAssociationDependency');
const { flattenStructure } = require('./structureFlattener');
const { setProp } = require('../../base/model');
const { implicitAs } = require('../../model/csnRefs');
module.exports = function (csn, flatKeys, referenceFlattener, csnUtils, transformers, error) {
/**
*
* @param {CSN.Model} csn
* @param {*} flatKeys
* @param {*} referenceFlattener
* @param {*} csnUtils
* @param {Function} error
*/
module.exports = function (csn, options, referenceFlattener, csnUtils, error) {
const structuredOData = options.toOdata.odataFormat === 'structured' && options.toOdata.version === 'v4';
const flatKeys = !structuredOData || (structuredOData && options.toOdata.odataForeignKeys);
// sort all associations by their dependencies

@@ -25,6 +36,6 @@ const sortedAssociations = sortByAssociationDependency(csn, referenceFlattener);

// The map will collect all generated foreign key names for the specific path
let generatedForeignKeyNamesForPath = {}; // map<path,[key-name]>
let generatedForeignKeyNamesForPath = Object.create(null); // map<path,[key-name]>
sortedAssociations.forEach(item => {
const { element, path } = item;
const { definitionName, elementName, element, parent, path } = item;

@@ -38,3 +49,3 @@ if (csnUtils.isManagedAssociationElement(element) && element.keys) {

let arrayOfGeneratedForeignKeyNames = generateForeignKeys(item);
let arrayOfGeneratedForeignKeyNames = generateForeignKeys(definitionName, elementName, element, parent, path);
generatedForeignKeyNamesForPath[item.path.join('/')] = arrayOfGeneratedForeignKeyNames;

@@ -52,5 +63,4 @@ })

let newResult = [];
for (const keyNumber in assoc.keys) {
let key = assoc.keys[keyNumber]
let keyPath = path.concat('keys', keyNumber);
assoc.keys.forEach( (key, keyIndex) => {
let keyPath = path.concat('keys', keyIndex);
let resolved = csnUtils.inspectRef(keyPath)

@@ -69,3 +79,3 @@ let targetElement = resolved.art;

}
}
});

@@ -82,3 +92,8 @@ function expandAssociationKey(key) {

generatedKeys.forEach(fkName => {
newResult.push({ ref: [fkName] });
let newFkRef = { ref: [fkName] };
if (key.as) {
let alias = fkName.replace(key.ref[0], key.as);
setProp(newFkRef, 'as', alias);
}
newResult.push(newFkRef);
})

@@ -105,63 +120,59 @@ } // expandAssociationKey

*/
function generateForeignKeys(item) {
let arrayOfGeneratedForeignKeyNames = [];
function generateForeignKeys(definitionName, assocName, assoc, parent, path) {
let foreignKeyElements = Object.create(null);
const { definitionName, elementName, element, parent, path } = item;
for (let keyIndex in element.keys) {
let key = element.keys[keyIndex];
// First, loop over the keys array of the association and generate the FKs.
// The result of all the FKs for the given association is accumulated
// in the 'foreignKeyElements' dictionary
assoc.keys.forEach( (key, keyIndex) => {
let keyPath = path.concat('keys', keyIndex);
let foreignKeyElements = flatKeys
? transformers.createForeignKeyElement(element, elementName, key, parent, definitionName, keyPath)
: creareForeignKeysInStructuredOData(element, elementName, key, parent, definitionName, keyPath);
let foreignKeyElementsForKey = generateForeignKeysForRef(assoc, assocName, key, keyPath);
Object.assign(foreignKeyElements, foreignKeyElementsForKey);
});
if (!foreignKeyElements) continue;
forEach(foreignKeyElements, (_name, foreignKeyElement) => {
copyAnnotations(element, foreignKeyElement, true);
})
arrayOfGeneratedForeignKeyNames.push(...Object.keys(foreignKeyElements));
}
return arrayOfGeneratedForeignKeyNames;
}
/**
* Generate the foreign keys for given association in structured OData flavour.
* We do generate flattened keys, but keep the reference in the association untouched.
* @param {*} assoc association for which keys will be generated
* @param {*} assocName assocation name
* @param {*} foreignKeyRef key object from the 'keys' array in the assoc
* @param {*} parent structure where the association is
* @param {*} defName name of the current definition
* @param {*} pathInKeysArr path to the key in the assocaition's array of keys
*/
function creareForeignKeysInStructuredOData(assoc, assocName, foreignKeyRef, parent, defName, pathInKeysArr) {
let result = generateForeignKeysForRef(assoc, assocName, foreignKeyRef, pathInKeysArr);
// Add the new elements to the definition. At the same time, check for coliding element's name
// After that, add the new elements to the definition.
// At the same time:
// -> Check for coliding element's name
// &
// -> Propagate annotations from the association
if (parent.items) // proceed to items of such
parent = parent.items;
for (const [foreignKeyName, foreignKey] of Object.entries(result)) {
let currElementsNames = Object.keys(parent.elements);
for (const [foreignKeyName, foreignKey] of Object.entries(foreignKeyElements)) {
// Insert artificial element into artifact, with all cross-links (must not exist already)
if (parent.elements[foreignKeyName]) {
const path = parent.elements.$path ? parent.elements.$path.concat([foreignKeyName]) : ['definitions', defName, 'elements', foreignKeyName];
const path = parent.elements.$path ? parent.elements.$path.concat([foreignKeyName]) : ['definitions', definitionName, 'elements', foreignKeyName];
error(null, path, `Generated foreign key element "${foreignKeyName}" for association "${assocName}" conflicts with existing element`);
}
parent.elements[foreignKeyName] = foreignKey;
copyAnnotations(assoc, foreignKey, true);
}
return result;
// make sure the generated foreign key(s) is added right after the association (that it belongs to) in the elements dictionary
const assocIndex = currElementsNames.findIndex(elemName => elemName === assocName);
// if (flatKeys)
currElementsNames.splice(assocIndex + 1, 0, ...Object.keys(foreignKeyElements));
parent.elements = currElementsNames.reduce((previous, name) => {
previous[name] = parent.elements[name] || foreignKeyElements[name];
return previous;
}, Object.create(null));
return Object.keys(foreignKeyElements);
}
// FIXME: Very similar code to
// transformUtilsNew::getForeignKeyArtifact & createForeignKeyElement
// Can this be streamlined?
function generateForeignKeysForRef(assoc, assocName, foreignKeyRef, pathInKeysArr, foreignKey4 = assocName) {
// in structured OData, might be more than one generated FKs
let generatedFks = Object.create(null);
let fkArtifact = csnUtils.inspectRef(pathInKeysArr).art;
const fkArtifact = csnUtils.inspectRef(pathInKeysArr).art;
if (csnUtils.isStructured(fkArtifact)) {
processStuctured(fkArtifact, assocName, foreignKeyRef);
processStucturedKey(fkArtifact, assocName, foreignKeyRef);
} else {
// built-in
let foreignKeyElementName = `${assocName.replace(/\./g, '_')}_${foreignKeyRef.as || foreignKeyRef.ref.join('_')}`;
const foreignKeyElementName = `${assocName.replace(/\./g, '_')}_${foreignKeyRef.as || foreignKeyRef.ref.join('_')}`;
newForeignKey(fkArtifact, foreignKeyElementName);

@@ -172,18 +183,8 @@ }

function processStuctured(fkArtifact, assocName, foreignKeyRef) {
let subStruct = fkArtifact.elements ? fkArtifact : csnUtils.getFinalBaseType(fkArtifact.type);
let flatElements = flattenStructure(subStruct, subStruct.$path, csnUtils, error, undefined, fkArtifact.$path.slice(-1) || []).newFlatElements;
function processStucturedKey(fkArtifact, assocName, foreignKeyRef) {
const subStruct = fkArtifact.elements ? fkArtifact : csnUtils.getFinalBaseType(fkArtifact.type);
const flatElements = flattenStructure(subStruct, subStruct.$path, csnUtils, options, error, undefined, fkArtifact.$path.slice(-1) || []).newFlatElements;
for (const [flatElemName, flatElem] of Object.entries(flatElements)) {
let foreignKeyElementName = `${assocName.replace(/\./g, '_')}_`;
if (foreignKeyRef.as) {
let namePathWithAlias = flatElemName.split('_');
// TODO: resolve this with the implicit as logic
namePathWithAlias.splice(0, 1, foreignKeyRef.as);
foreignKeyElementName = foreignKeyElementName.concat(namePathWithAlias.join('_'));
} else
foreignKeyElementName = foreignKeyElementName.concat(flatElemName);
// TODO: this is only a special case for workaround a probable bug in the sorting of assocs algo(when we take over elements from the types exposed with the
// types exposure) once the types exposure logic is part of the renderer, this will be fixed
if (flatElem['@odata.foreignKey4']) continue;
const foreignKeyElementName =
`${assocName.replace(/\./g, '_')}_${foreignKeyRef.as ? flatElemName.replace(implicitAs(foreignKeyRef.ref), foreignKeyRef.as) : flatElemName}`;
newForeignKey(flatElem, foreignKeyElementName);

@@ -199,2 +200,3 @@ }

// FIXME: better use transformUtlsNew::createRealFK(...);
let foreignKeyElement = Object.create(null);

@@ -204,3 +206,3 @@

// FIXME: There is currently no other way but to treat the annotation '@odata.Type' as a type property.
for (let prop of ['type', 'length', 'scale', 'precision', 'srid', 'default', '@odata.Type']) {
for (let prop of ['type', 'length', 'scale', 'precision', 'srid', '@odata.Type']) {
if (fkArtifact[prop] != undefined) {

@@ -218,2 +220,3 @@ foreignKeyElement[prop] = fkArtifact[prop];

foreignKeyElement['@odata.foreignKey4'] = foreignKey4;
if (flatKeys) foreignKeyRef.$generatedFieldName = foreignKeyElementName;
setProp(foreignKeyElement, '$path', pathInKeysArr); // attach $path to the newly created element - used for inspectRef in processAssociationOrComposition

@@ -220,0 +223,0 @@ if (assoc.$location) {

const { forEachRef } = require('../../model/csnUtils');
const { setProp } = require('../../base/model');
const { implicitAs } = require('../../model/csnRefs');
const walker = require('../../json/walker');

@@ -70,4 +71,4 @@

if (isNode) {
const descr = Object.getOwnPropertyDescriptor(node,'$path')
if(descr && descr.enumerable) // check if it is an element -> do not overwrite it
const descr = Object.getOwnPropertyDescriptor(node, '$path')
if (descr && descr.enumerable) // check if it is an element -> do not overwrite it
return;

@@ -109,9 +110,10 @@ if (!pathPrefix)

if (paths) {
paths.push(element.art.$path)
paths.push(element.art.$path);
}
});
if (paths)
setProp(node, '$paths', paths)
setProp(node, '$paths', paths);
setProp(node, '$scope', resolved.scope);
// cache also if structured or not
let structured = resolved.links.map(link => link.art ? (isStructured(link.art)) : undefined)
let structured = resolved.links.map(link => link.art ? (isStructured(link.art)) : undefined);
this.structuredReference[path.join('/')] = structured;

@@ -141,4 +143,4 @@ });

registerElementTransition(fromPath, toPath) {
let sFromPath = fromPath.join("/");
let sToPath = toPath.join("/");
let sFromPath = fromPath.join('/');
let sToPath = toPath.join('/');
if (sFromPath === sToPath) return; // same path -> no transition

@@ -157,3 +159,3 @@ this.elementTransitions[sFromPath] = toPath;

getElementTransition(path) {
let sPath = path.join("/");
let sPath = path.join('/');
return this.elementTransitions[sPath];

@@ -188,3 +190,3 @@ }

registerGeneratedElementsForPath(path, elementNames) {
this.generatedElementsForPath[path.join("/")] = elementNames;
this.generatedElementsForPath[path.join('/')] = elementNames;
}

@@ -199,3 +201,3 @@

getGeneratedElementsForPath(path) {
return this.generatedElementsForPath[path.join("/")];
return this.generatedElementsForPath[path.join('/')];
}

@@ -209,3 +211,3 @@

flattenAllReferences(csn) {
forEachRef(csn, (ref, node) => {
forEachRef(csn, (ref, node, path) => {
if (ref.length == 1) return; // too short, no flattening

@@ -217,3 +219,3 @@ if (node.$paths) {

node.$paths.forEach((path, i) => {
if (path == undefined) return;
if (path === undefined || !ref[i]) return;
let spath = path.join('/')

@@ -229,3 +231,3 @@ let flattened = this.flattenedElementPaths[spath];

});
if (newRef != undefined && anythingFlattened) { // do not replace unchanged references
if (newRef !== undefined && anythingFlattened) { // do not replace unchanged references
// check if this is a column and add alias if missing

@@ -238,2 +240,9 @@ if (isColumnInSelect(ref.$path)) {

setProp(newRef, '$path', node.$path.concat('ref'));
if (!node.as) {
// FIXME: rather use scope from inspectRef to determin if path leads to a key
if (ifKeysScope(path))
node.as = implicitAs(node.ref);
else
setProp(node, 'as', implicitAs(node.ref))
}
node.ref = newRef;

@@ -244,4 +253,39 @@ }

}
applyAliasesInOnCond(csn, inspectRef) {
forEachRef(csn, (ref, node, path) => {
// is there a better way to detect if we are dealing with on-cond?
if (!['on', '$self', null].includes(node.$scope)) return;
let { links } = inspectRef(path);
let keysOfPreviousStepWhenManagedAssoc = undefined;
let aliasedRef = [...ref];
for (let idx = 0; idx < ref.length; idx++) {
const currArt = links[idx].art;
if (keysOfPreviousStepWhenManagedAssoc) {
const usedKey = keysOfPreviousStepWhenManagedAssoc.find(key => key.ref[0] === ref[idx]);
if (usedKey && usedKey.as) {
aliasedRef.splice(idx, usedKey.ref.length, usedKey.as);
idx += usedKey.ref.length - 1;
keysOfPreviousStepWhenManagedAssoc = undefined;
}
} else
keysOfPreviousStepWhenManagedAssoc =
(currArt.type === 'cds.Association' || currArt.type === 'cds.Composition') && currArt.keys;
}
node.ref = aliasedRef;
})
}
}
// FIXME: rather use scope from inspectRef to determin if path leads to a key
function ifKeysScope(path) {
if (!path) return false;
if (Number.isInteger(path[path.length - 1]) && path[path.length - 2] === 'keys' && path[path.length - 4] === 'elements')
return true;
return false;
}
/**

@@ -251,3 +295,3 @@ * Checks if the provided path is a column inside a select node

*
* @param {string[]} path Path to explore
* @param {CSN.Path} path Path to explore
* @returns {boolean} True if the provided path matched the requirements to be a select node.

@@ -267,2 +311,2 @@ */

module.exports = ReferenceFlattener
module.exports = ReferenceFlattener;

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

let done = false;
// walk over all dependencies and add to the result if all dependants are already in the result
// walk over all dependencies and add to the result if all dependents are already in the result
while (!done) {

@@ -66,8 +66,8 @@ if (loops > maxLoops) {

forEach(dependencies, (name, item) => {
let countOfUnprocessedDependants = 0;
let countOfUnprocessedDependents = 0;
item.dependencies.forEach(path => {
let spath = path.join('/');
if (!inResult[spath]) countOfUnprocessedDependants++;
if (!inResult[spath]) countOfUnprocessedDependents++;
})
if (countOfUnprocessedDependants === 0) { // all dependants processed?
if (countOfUnprocessedDependents === 0) { // all dependents processed?
let spath = item.path.join('/');

@@ -74,0 +74,0 @@ if (!inResult[spath]) {

'use strict';
const { copyAnnotations } = require('../../model/modelUtils');
const { copyAnnotations } = require('../../model/csnUtils');
const { setProp } = require('../../base/model');

@@ -8,3 +8,3 @@ const { cloneCsn, forEachDefinition } = require('../../model/csnUtils');

// these functions are used for propagation of the annotations, collected along the path during flattening
const { addAnnotationsForPropagationFromElement, propagateAnnotationsToElement, resetAnnotationsForRropagation } = function () {
const { addAnnotationsForPropagationFromElement, propagateAnnotationsToElement, resetAnnotationsForPropagation } = function () {
let toBePropagatedAnnotations = Object.create(null);

@@ -18,3 +18,3 @@ return {

},
resetAnnotationsForRropagation: function () {
resetAnnotationsForPropagation: function () {
toBePropagatedAnnotations = Object.create(null);

@@ -52,9 +52,10 @@ }

* - _flatElementNameWithDots - names in the element path concatenated with dot
* @param {*} csn CSN-object to flatten
* @param {CSN.Model} csn CSN-object to flatten
* @param {*} csnUtils instances of utility functions
* @param {*} referenceFlattener
* @param {Function} error
*/
function flattenCSN(csn, csnUtils, referenceFlattener, signal) {
function flattenCSN(csn, csnUtils, options, referenceFlattener, error) {
forEachDefinition(csn, (def, defName, propertyName, path) =>
flattenDefinition(def, path, csnUtils, referenceFlattener, signal));
flattenDefinition(def, path, csnUtils, options, referenceFlattener, error));
}

@@ -64,10 +65,13 @@

* Flattens one single definition and all structures in it. Modifies the definition in place.
* @param {*} definition definition object to flatten
* @param {*} definitionPath path in CSN object
* @param {CSN.Definition} definition definition object to flatten
* @param {CSN.Path} definitionPath path in CSN object
* @param {*} csnUtils utility functions
* @param {*} referenceFlattener
* @param {Function} error
*/
function flattenDefinition(definition, definitionPath, csnUtils, referenceFlattener, signal) {
if (definition.kind !== 'entity' && definition.kind !== 'view') return;
function flattenDefinition(definition, definitionPath, csnUtils, options, referenceFlattener, error) {
if (definition.kind !== 'entity' && definition.kind !== 'view')
return;
let { newFlatElements } = flattenStructure(definition, definitionPath, csnUtils, signal, referenceFlattener);
let { newFlatElements } = flattenStructure(definition, definitionPath, csnUtils, options, error, referenceFlattener);

@@ -80,19 +84,23 @@ referenceFlattener.attachPaths(newFlatElements, definitionPath.concat('elements'));

/**
* Flattenes structured element by calling element flattener for each structured child.
* Flattens structured element by calling element flattener for each structured child.
* Returns a dictionary containing all the new elements for the given structure.
* @param {*} struct the structure to flatten
* @param {*} path the path of the structure in the CSN tree
* @param {*} isTopLevelElement states if this is a top level element
* @param {*} elementPathInStructure list of parent element names
* @param {*} isKey true if this or the parent element is a key - will be propagated to all child elements
* @param {CSN.Path} path the path of the structure in the CSN tree
* @param {*} csnUtils
* @param {Function} error Error message function
* @param {*} [referenceFlattener]
* @param {string[]} [elementPathInStructure] list of parent element names
* @param {*} [newFlatElements]
* @param {boolean} [isTopLevelElement] states if this is a top level element
* @param {boolean} [isKey] true if this or the parent element is a key - will be propagated to all child elements
* @param {boolean} [propagateAnnotations]
*/
function flattenStructure(struct, path, csnUtils, error, referenceFlattener = undefined, elementPathInStructure = [],
function flattenStructure(struct, path, csnUtils, options, error, referenceFlattener = undefined, elementPathInStructure = [],
newFlatElements = Object.create(null), isTopLevelElement = true, isKey = false, propagateAnnotations = false, isParentNotNull = false) {
isTopLevelElement ? resetAnnotationsForRropagation() : addAnnotationsForPropagationFromElement(struct);
isTopLevelElement ? resetAnnotationsForPropagation() : addAnnotationsForPropagationFromElement(struct);
let generatedNewFlatElementsNames = []; // holds the names of all new child elements of the structure
for (let elementName in struct.elements) {
let element = struct.elements[elementName];
struct.elements && Object.entries(struct.elements).forEach( ([elementName, element]) => {
let currPath = path.concat('elements', elementName);

@@ -116,3 +124,3 @@

const subStruct = element.elements ? element : csnUtils.getFinalBaseType(element.type);
let result = flattenStructure(subStruct, currPath, csnUtils, error, referenceFlattener, elementPathInStructure.concat(elementName), newFlatElements, false, isKey || element.key, true, isNotNull());
let result = flattenStructure(subStruct, currPath, csnUtils, options, error, referenceFlattener, elementPathInStructure.concat(elementName), newFlatElements, false, isKey || element.key, true, isNotNull());
generatedNewFlatElementsNames.push(...result.generatedNewFlatElementsNames); // accomulate names of produced elements

@@ -126,3 +134,3 @@

}
});

@@ -157,3 +165,3 @@ if (referenceFlattener) {

function createNewElement(element, elementNameWithDots) {
let newElement = cloneCsn(element);
let newElement = cloneCsn(element, options);
if (propagateAnnotations) propagateAnnotationsToElement(newElement);

@@ -160,0 +168,0 @@ if (isNotNull() === undefined) delete newElement.notNull;

@@ -5,20 +5,19 @@ 'use strict';

* In this module resides all the logic related to exposure of types as part of the OData backend
* The exposure is run only for definitions which reside in a service.
* @module typesExposure
*/
const { setProp, isBetaEnabled } = require('../../base/model');
const { defNameWithoutServiceName, getServiceOfArtifact, isArtifactInService, isArtifactInSomeService } = require('./utils');
const { cloneCsn, isBuiltinType, forEachDefinition, forEachMember, forEachMemberRecursively } = require('../../model/csnUtils');
const { copyAnnotations } = require('../../model/modelUtils');
const { setProp } = require('../../base/model');
const { defNameWithoutServiceOrContextName, isArtifactInService } = require('./utils');
const { cloneCsn, isBuiltinType, forEachDefinition, forEachMember } = require('../../model/csnUtils');
const { copyAnnotations } = require('../../model/csnUtils');
/**
* @param {CSN.Model} csn
* @param {string[]} services
* @param {function} whatsMyServiceName
* @param {CSN.Options} options
* @param {*} csnUtils
* @param {object} message message object with { error } function
* @param {*} referenceFlattener
*/
function typesExposure(csn, services, options, csnUtils, message, referenceFlattener = undefined) {
function typesExposure(csn, whatsMyServiceName, options, csnUtils, message) {
const { error } = message;

@@ -28,5 +27,6 @@ // are we working with structured OData or not

// are we working with OData proxies or cross-service refs
const isMultiSchema = structuredOData && (isBetaEnabled(options, 'odataProxies') && (options.toOdata.odataProxies || options.toOdata.odataXServiceRefs));
const isMultiSchema = structuredOData && ((options.toOdata.odataProxies || options.toOdata.odataXServiceRefs));
// collect in this variable all the newly exposed types
let exposedStructTypes = [];
const exposedStructTypes = [];
const schemas = Object.create(null);

@@ -36,11 +36,11 @@ // walk through the definitions of the given CSN and expose types where needed

// we do expose types only for definition from inside services
if (isArtifactInSomeService(defName, services)) {
let serviceName = getServiceOfArtifact(defName, services);
if (def.kind === 'type') {
const serviceName = whatsMyServiceName(defName, false);
if (serviceName) {
if (['type', 'entity', 'view'].includes(def.kind)) {
forEachMember(def, (element, elementName, propertyName, path) => {
if (propertyName === 'elements') {
exposeStructTypeOf(element, `${defName}.${elementName}`, defName, getServiceOfArtifact(defName, services), `${isMultiSchema ? defName : defName.replace(/\./g, '_')}_${elementName}`, structuredOData, path);
// TODO: use the next line once the array of logic is reworked
// exposeTypeOf(element, elementName, getServiceOfArtifact(defName, services), `${defName.replace(/\./g, '_')}_${elementName}`);
if (propertyName === 'elements' || propertyName === 'params') {
const artificialtName = `${isMultiSchema ?
defNameWithoutServiceOrContextName(defName, serviceName)
: defNameWithoutServiceOrContextName(defName, serviceName).replace(/\./g, '_')}_${elementName}`;
exposeTypeOf(element, elementName, defName, serviceName, artificialtName, path);
}

@@ -57,55 +57,28 @@ }, path);

// bound actions
for (let actionName in def.actions || {}) {
exposeTypesOfAction(def.actions[actionName], `${defName}_${actionName}`, defName, serviceName, path.concat(['actions', actionName]));
}
if (def.kind === 'entity' || def.kind === 'view') {
forEachMember(def, (element, elementName, propertyName, path) => {
if (propertyName === 'elements') {
if (csnUtils.isStructured(element)) {
exposeStructTypeOf(element, elementName, defName, serviceName, `${isMultiSchema ? defNameWithoutServiceName(defName, serviceName) : defNameWithoutServiceName(defName, serviceName).replace(/\./g, '_')}_${elementName}`, structuredOData, path);
// TODO: use the next line once the array of logic is reworked
// exposeTypeOf(element, elementName, getServiceOfArtifact(defName, services), `${defName.replace(/\./g, '_')}_${elementName}`);
}
// TODO: there should be a better plaace for this check
if (csnUtils.getServiceName(defName) && !element.type && !element.items && !element.elements) {
error(null, path, `Element "${defName}.${elementName}" does not have a type: Elements of ODATA entities must have a type`);
}
}
}, path);
// Expose types for 'array of/many' declarations
let isAction = false;
// If a member is of type 'array of T' where T is either user defined structured type outside of the service or anonymous type,
// then expose T and assign it do the member.
forEachMemberRecursively(def, (member, memberName, prop, path) => {
// we do apply array of exposure logic on actions/functions
// and on params and returns of action/function always,
// regardless of the OData version or format
if (member.kind === 'action' || member.kind === 'function') isAction = true;
if (isArrayed(member)) {
if (structuredOData)
exposeArrayOfTypeOf(member, memberName, defName, serviceName, `${defNameWithoutServiceName(defName, serviceName)}_${memberName}`, path);
else if (options.toOdata.version === 'v4' && !isAction) {
exposeArrayOfTypeOf(member, memberName, defName, serviceName, `${defNameWithoutServiceName(defName, serviceName)}_${memberName}`, path);
}
}
}, path);
}
def.actions && Object.entries(def.actions).forEach(([actionName, action]) => {
exposeTypesOfAction(action, `${defName}_${actionName}`, defName, serviceName, path.concat(['actions', actionName]));
});
}
});
if (referenceFlattener)
exposedStructTypes.forEach(typeName => referenceFlattener.attachPaths(csn.definitions[typeName], ['definitions', typeName]))
// still WIP function
return schemas;
/**
* General function used for exposing a type of given element
* @param {object} node
* @param {string} memberName
* @param {string} service
* @param {string} artificialName
* @param {CSN.Path} path
*/
function exposeTypeOf(node, memberName, defName, service, artificialName, path) {
if (isArrayed(node))
exposeArrayOfTypeOf(node, memberName, defName, service, artificialName, path);
else
exposeStructTypeOf(node, memberName, defName, service, artificialName, structuredOData, path);
else if (csnUtils.isStructured(node))
exposeStructTypeOf(node, memberName, defName, service, artificialName, path);
}
// still WIP function
/**
* Check if a node is arrayed
* @param {object} node
*/
function isArrayed(node) {

@@ -124,8 +97,11 @@ return node.items || (node.type && csnUtils.getFinalTypeDef(node.type).items);

function exposeTypesOfAction(action, actionName, defName, service, path) {
if (action.returns)
exposeTypeOf(action.returns, actionName, defName, service, `return_${actionName.replace(/\./g, '_')}`, path.concat(['returns']));
if (action.returns) {
const artificialName = `return_${actionName.replace(/\./g, '_')}`;
exposeTypeOf(action.returns, actionName, defName, service, artificialName, path.concat(['returns']));
}
for (let paramName in action.params || {}) {
exposeTypeOf(action.params[paramName], actionName, defName, service, `param_${actionName.replace(/\./g, '_')}_${paramName}`, path.concat(['params', paramName]));
}
action.params && Object.entries(action.params).forEach(([paramName, param]) => {
const artificialName = `param_${actionName.replace(/\./g, '_')}_${paramName}`;
exposeTypeOf(param, actionName, defName, service, artificialName, path.concat(['params', paramName]));
});
}

@@ -142,11 +118,5 @@

*/
function exposeStructTypeOf(node, memberName, defName, service, artificialName, deleteElems = structuredOData, path, parentName) {
if (!node) {
// TODO: when node will be undefined, if node is undefined this should not be reached
//console.log(parentName + ' --- ' + service + ' --- ' + artificialName);
return;
}
function exposeStructTypeOf(node, memberName, defName, service, artificialName, path, parentName) {
// TODO: call exposure of Arrayed types?
if (node.items) exposeStructTypeOf(node.items, memberName, defName, service, artificialName, deleteElems, path);
if (node.items) exposeStructTypeOf(node.items, memberName, defName, service, artificialName, path);

@@ -176,5 +146,5 @@ if (isExposableStructure(node)) {

// Recurse into elements of 'type' (if any) and expose them as well (is needed)
for (let elemName in newType.elements) {
if (node.elements && node.elements[elemName].$location) setProp(newType.elements[elemName], '$location', node.elements[elemName].$location);
exposeStructTypeOf(newType.elements[elemName],
newType.elements && Object.entries(newType.elements).forEach(([elemName, newElem]) => {
if (node.elements && node.elements[elemName].$location) setProp(newElem, '$location', node.elements[elemName].$location);
exposeStructTypeOf(newElem,
memberName,

@@ -184,8 +154,7 @@ typeDef.kind === 'type' ? node.type : defName,

isMultiSchema ? `${newTypeFullName}_${elemName}` : `${newTypeId}_${elemName}`,
deleteElems,
path,
newTypeFullName);
}
});
typeDef.kind === 'type' ? copyAnnotations(typeDef, newType) : copyAnnotations(node, newType);
if (deleteElems) delete node.elements;
delete node.elements;
node.type = newTypeFullName;

@@ -216,20 +185,20 @@ }

function getTypeNameInMultiSchema(typeName, service) {
if (isArtifactInSomeService(typeName, services)) {
const typeService = csnUtils.getServiceName(typeName);
const typeService = whatsMyServiceName(typeName);
if (typeService) {
// new type name without any prefixes
const typePlainName = defNameWithoutServiceName(typeName, typeService);
const newContextName = `${service}.${typeService}`;
createContext(newContextName);
const typePlainName = defNameWithoutServiceOrContextName(typeName, typeService);
const newSchemaName = `${service}.${typeService}`;
createSchema(newSchemaName);
// return the new type name
return `${newContextName}.${typePlainName.replace(/\./g, '_')}`;
return `${newSchemaName}.${typePlainName.replace(/\./g, '_')}`;
} else {
const typeContext = csnUtils.getContextOfArtifact(typeName);
const typeNamespace = csnUtils.getNamespaceOfArtifact(typeName);
const newContextName = `${service}.${typeContext || typeNamespace || 'root'}`;
const newSchemaName = `${service}.${typeContext || typeNamespace || 'root'}`;
// new type name without any prefixes
const typePlainName = typeContext ? defNameWithoutServiceName(typeName, typeContext)
const typePlainName = typeContext ? defNameWithoutServiceOrContextName(typeName, typeContext)
: typeName.replace(`${typeNamespace}.`, '');
createContext(newContextName);
createSchema(newSchemaName);
// return the new type name
return `${newContextName}.${typePlainName.replace(/\./g, '_')}`;
return `${newSchemaName}.${typePlainName.replace(/\./g, '_')}`;
}

@@ -247,8 +216,8 @@ }

let currPrefix = parentName.substring(0, parentName.lastIndexOf('.'));
const newContextName = currPrefix || 'root';
const newSchemaName = currPrefix || 'root';
// new type name without any prefixes
const typePlainName = defNameWithoutServiceName(typeName, newContextName);
const typePlainName = defNameWithoutServiceOrContextName(typeName, newSchemaName);
createContext(newContextName);
return `${newContextName}.${typePlainName.replace(/\./g, '_')}`;
createSchema(newSchemaName);
return `${newSchemaName}.${typePlainName.replace(/\./g, '_')}`;
}

@@ -258,7 +227,6 @@

* Tf does not exists, create a context with the given name in the CSN
* @param {string} newContextName
* @param {string} name
*/
function createContext(newContextName) {
if (!csn.definitions[`${newContextName}`])
csn.definitions[`${newContextName}`] = { kind: 'context' };
function createSchema(name) {
schemas[`${name}`] = { kind: 'schema', name };
}

@@ -293,3 +261,3 @@

// Duplicate the type's elements
for (let elemName in elements) {
Object.entries(elements).forEach(([elemName, element]) => {
if (type.elements[elemName]) {

@@ -299,5 +267,5 @@ const path = ['definitions', typeName, 'elements', elemName];

}
let cloned = cloneCsn(elements[elemName]);
let cloned = cloneCsn(element, options);
type.elements[elemName] = cloned;
}
});

@@ -320,3 +288,3 @@ // add to the CSN

if (node.items && !node.type) {
exposeStructTypeOf(node.items, memberName, defName, service, artificialName, true, path.concat('items'));
exposeStructTypeOf(node.items, memberName, defName, service, artificialName, path.concat('items'));
}

@@ -327,9 +295,7 @@ // we can have both of the 'type' and 'items' in the cases:

else if (node.type) {
// case 2. - in V2 we expand to the underlying base scalar and remove the type property
if (node.items && node.items.type && isBuiltinType(node.items.type)
&& options.toOdata.version === 'v2') delete node.type;
else if (csnUtils.getFinalTypeDef(node.type).items) {
let finalType = csnUtils.getFinalTypeDef(node.type);
if (finalType.items) {
if (!isArtifactInService(node.type, service)) {
let typeId = `${service}.${node.type.replace(/\./g, '_')}`;
let newType = exposeArrayedType(node.items || csnUtils.getFinalTypeDef(node.type).items, typeId);
let newType = exposeArrayedType(node.items || finalType.items, typeId);
// When we have in the model something like:

@@ -362,3 +328,3 @@ // type Foo: array of Bar; type Bar: { qux: Integer };

// copy over the items
newType.items = cloneCsn(items);
newType.items = cloneCsn(items, options);
csn.definitions[typeId] = newType;

@@ -378,3 +344,3 @@ exposedStructTypes.push(typeId);

if (csnUtils.isStructured(finalType)) {
if (!def.items.elements) def.items.elements = cloneCsn(finalType.elements);
if (!def.items.elements) def.items.elements = cloneCsn(finalType.elements, options);
delete def.items.type;

@@ -381,0 +347,0 @@ }

@@ -40,6 +40,6 @@ const {

* @param {string} name
* @param {string} service
* @param {string} srvOrCtx
*/
function defNameWithoutServiceName(name, service) {
return name.replace(`${service}.`, '');
function defNameWithoutServiceOrContextName(name, srvOrCtx) {
return name.replace(`${srvOrCtx}.`, '');
}

@@ -92,3 +92,3 @@

forEachManagedAssociation,
defNameWithoutServiceName,
defNameWithoutServiceOrContextName,
getServiceOfArtifact,

@@ -95,0 +95,0 @@ isArtifactInService,

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

const alerts = require('../base/alerts');
const { hasErrors } = require('../base/messages');
const { hasErrors, makeMessageFunction } = require('../base/messages');
const { setProp } = require('../base/model');
const { csnRefs } = require('../model/csnRefs');
const { copyAnnotations } = require('../model/modelUtils');
const { copyAnnotations } = require('../model/csnUtils');
const { cloneCsn, forEachMemberRecursively, forEachGeneric, forAllQueries,

@@ -23,6 +22,7 @@ forEachRef, getUtils, isBuiltinType } = require('../model/csnUtils');

function getTransformers(model, options, pathDelimiter = '_') {
const { error, warning, signal } = alerts(model, options);
const { error, warning, info } = makeMessageFunction(model, options);
const {
getCsnDef,
getFinalBaseType,
getFinalTypeDef,
hasBoolAnnotation,

@@ -43,2 +43,3 @@ inspectRef,

createForeignKeyElement,
getForeignKeyArtifact,
checkForeignKeys,

@@ -48,3 +49,2 @@ flattenStructuredElement,

flattenStructStepsInRef,
checkExposedAssoc,
toFinalBaseType,

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

createAction,
addAction,
assignAction,
extractValidFromToKeyElement,

@@ -84,14 +84,66 @@ checkMultipleAssignments,

if (element.type === 'cds.String' && element.length === undefined) {
element.length = model.options && model.options.length ? model.options.length : 5000;
if (!(model.options && model.options.length))
element.length = options.length ? options.length : 5000;
if (!options.length)
setProp(element, '$default', true);
}
if (element.type === 'cds.Decimal' && element.precision === undefined && model.options.precision) {
element.precision = model.options.precision;
if (element.type === 'cds.Decimal' && element.precision === undefined && options.precision) {
element.precision = options.precision;
}
if (element.type === 'cds.Decimal' && element.scale === undefined && model.options.scale) {
element.scale = model.options.scale;
if (element.type === 'cds.Decimal' && element.scale === undefined && options.scale) {
element.scale = options.scale;
}
}
// createRealFK 'really' creates the new foreign key element and is used by the two
// main FK generators getForeignKeyArtifact & createForeignKeyElement
// and (not yet) generateForeignKeyElements.js::generateForeignKeysForRef()
// TODO/FIXME: Can they be combined?
function createRealFK(fkArtifact, assoc, assocName, foreignKey, path, foreignKeyElementName) {
const foreignKeyElement = Object.create(null);
// Transfer selected type properties from target key element
// FIXME: There is currently no other way but to treat the annotation '@odata.Type' as a type property.
for (const prop of ['type', 'length', 'scale', 'precision', 'srid', '@odata.Type']) {
if (fkArtifact[prop] != undefined) {
foreignKeyElement[prop] = fkArtifact[prop];
}
}
if (!options.forHana)
copyAnnotations(assoc, foreignKeyElement, true);
// If the association is non-fkArtifact resp. key, so should be the foreign key field
for (const prop of ['notNull', 'key']) {
if (assoc[prop] != undefined) {
foreignKeyElement[prop] = assoc[prop];
}
}
// Establish the relationship between generated field and association:
// - generated field has annotation '@odata.foreignKey4'.
// - foreign key info has 'generatedFieldName'
foreignKeyElement['@odata.foreignKey4'] = assocName;
foreignKey.$generatedFieldName = foreignKeyElementName;
// attach $path to the newly created element - used for inspectRef in processAssociationOrComposition
setProp(foreignKeyElement, '$path', path);
if (assoc.$location) {
setProp(foreignKeyElement, '$location', assoc.$location);
}
return foreignKeyElement;
}
function getForeignKeyArtifact(assoc, assocName, foreignKey, path) {
const fkArtifact = inspectRef(path).art;
// FIXME: Duplicate code
// Assemble foreign key element name from assoc name, '_' and foreign key name/alias
const foreignKeyElementName = `${assocName.replace(/\./g, pathDelimiter)}${pathDelimiter}${foreignKey.as || foreignKey.ref.join(pathDelimiter)}`;
// In case of compiler errors the foreign key might be missing
if (!fkArtifact && hasErrors(options.messages)) {
return null;
}
return [ foreignKeyElementName, createRealFK(fkArtifact, assoc, assocName, foreignKey, path, foreignKeyElementName) ];
}
// (1) Create an artificial foreign key element for association 'assoc' (possibly part

@@ -107,7 +159,6 @@ // of nested struct, i.e. containing dots) in 'artifact', using foreign key info

let result = {};
let fkSeparator = pathDelimiter;
// FIXME: Duplicate code (postfix is added herein, can this be optimized?)
// Assemble foreign key element name from assoc name, '_' and foreign key name/alias
// let foreignKeyElementName = assocName.replace(/\./g, pathDelimiter) + fkSeparator + foreignKey.ref.join(pathDelimiter);
let foreignKeyElementName = `${assocName.replace(/\./g, pathDelimiter)}${fkSeparator}${foreignKey.as || foreignKey.ref.join(pathDelimiter)}`;
let foreignKeyElementName = `${assocName.replace(/\./g, pathDelimiter)}${pathDelimiter}${foreignKey.as || foreignKey.ref.join(pathDelimiter)}`;

@@ -118,3 +169,3 @@

// In case of compiler errors the foreign key might be missing
if (!fkArtifact && hasErrors((model.options && model.options.messages) || model.messages)) {
if (!fkArtifact && hasErrors(options.messages)) {
return null;

@@ -129,3 +180,3 @@ }

if (!iKeyArtifact && hasErrors((model.options && model.options.messages) || model.messages)) {
if (!iKeyArtifact && hasErrors(options.messages)) {
return;

@@ -146,20 +197,6 @@ }

let foreignKeyElement = Object.create(null);
// Transfer selected type properties from target key element
// FIXME: There is currently no other way but to treat the annotation '@odata.Type' as a type property.
for (let prop of ['type', 'length', 'scale', 'precision', 'srid', 'default', '@odata.Type']) {
if (fkArtifact[prop] != undefined) {
foreignKeyElement[prop] = fkArtifact[prop];
}
}
let foreignKeyElement = createRealFK(fkArtifact, assoc, assocName, foreignKey, path, foreignKeyElementName);
if (model.options && !model.options.forHana)
copyAnnotations(assoc, foreignKeyElement, true);
// If the association is non-fkArtifact resp. key, so should be the foreign key field
for (let prop of ['notNull', 'key']) {
if (assoc[prop] != undefined) {
foreignKeyElement[prop] = assoc[prop];
}
}
// FIXME: must this code go into createRealFK?
// Not present in getForeignKeyArtifact
if (artifact.items) // proceed to items of such

@@ -169,16 +206,8 @@ artifact = artifact.items;

if (artifact.elements[foreignKeyElementName]) {
signal(error`Generated foreign key element "${foreignKeyElementName}" for association "${assocName}" conflicts with existing element`,
artifact.elements.$path ? artifact.elements.$path.concat([foreignKeyElementName]) : ['definitions', artifactName, 'elements', foreignKeyElementName]);
const loc = artifact.elements.$path ? artifact.elements.$path.concat([foreignKeyElementName]) : ['definitions', artifactName, 'elements', foreignKeyElementName];
error(null, loc, { name: foreignKeyElementName, art: assocName },
'Generated foreign key element $(NAME) for association $(ART) conflicts with existing element');
}
artifact.elements[foreignKeyElementName] = foreignKeyElement;
// Establish the relationship between generated field and association:
// - generated field has annotation '@odata.foreignKey4'.
// - foreign key info has 'generatedFieldName'
foreignKeyElement['@odata.foreignKey4'] = assocName;
foreignKey.$generatedFieldName = foreignKeyElementName;
setProp(foreignKeyElement, '$path', path); // attach $path to the newly created element - used for inspectRef in processAssociationOrComposition
if (assoc.$location) {
setProp(foreignKeyElement, '$location', assoc.$location);
}
result[foreignKeyElementName] = foreignKeyElement;

@@ -202,4 +231,3 @@ } // function newForeignKey

for (let name in assoc.keys) {
let foreignKey = assoc.keys[name];
assoc.keys && Object.values(assoc.keys).forEach(foreignKey => {
let target = model.definitions[assoc.target];

@@ -220,5 +248,6 @@ // Sanity checks

if (target.elements[targetElementName] == undefined) {
signal(error`Foreign key "${targetElementName}" not found in target "${assoc.target}" of association "${artifactName}.${assocName}"`, ['definitions', artifactName, 'elements', assocName]);
error(null, ['definitions', artifactName, 'elements', assocName],
`Foreign key "${targetElementName}" not found in target "${assoc.target}" of association "${artifactName}.${assocName}"`);
}
}
});
}

@@ -265,3 +294,4 @@

if(result[eName]){
signal(error`Generated element ${eName} conflicts with other generated element`, pathInCsn)
error(null, pathInCsn, { name: eName },
'Generated element $(NAME) conflicts with other generated element')
} else {

@@ -271,5 +301,3 @@ result[eName] = e;

}
for (let childName in struct) {
let childElem = struct[childName];
Object.entries(struct).forEach(([childName, childElem]) => {
if (isStructured(childElem)) {

@@ -292,3 +320,3 @@ // Descend recursively into structured children

let flatElemName = elemName + pathDelimiter + childName;
let flatElem = cloneCsn(childElem);
let flatElem = cloneCsn(childElem, options);
// Don't take over notNull from leaf elements

@@ -300,7 +328,6 @@ delete flatElem.notNull;

}
}
});
// Fix all collected flat elements (names, annotations, properties, origin ..)
for (let name in result) {
let flatElem = result[name];
Object.values(result).forEach(flatElem => {
// Copy annotations from struct (not overwriting, because deep annotations should have precedence)

@@ -314,3 +341,3 @@ copyAnnotations(elem, flatElem, false);

}
}
});
return result;

@@ -417,16 +444,2 @@ }

// Check that exposed associations do not point to non-exposed targets
function checkExposedAssoc(artName, assocDef, assocName, service) {
// When we have an aspect and this aspect has element which is a composition of another aspect,
// then the 'assocDef' does not have 'target' property, but only 'targetAspect' property
// or the targetAspect might be an object, when the managed composition was defined anonymously
let assocTarget = assocDef.target || assocDef.targetAspect;
let assocTargetDef = typeof assocTarget === 'string' ? getCsnDef(assocTarget) : assocDef.targetAspect.elements;
if (!assocDef._ignore && assocTarget && assocTargetDef && (typeof assocTarget === 'string' && !assocTarget.startsWith(service))) {
// If we have a 'preserved dotted name' -> a result of flattening -> This scenario is not supported yet
if (assocDef._flatElementNameWithDots)
signal(error`Redirection for sub elements not supported yet - association "${artName}.${assocName}"`, ['definitions', artName, 'elements', assocName]);
}
}
/**

@@ -475,4 +488,7 @@ * Copy properties of the referenced type, but don't resolve to the final base type.

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

@@ -487,3 +503,3 @@ node.type=finalBaseType;

// The type might already be a full fledged type def (array of)
let typeDef = typeof node.type === 'string' ? getCsnDef(node.type) : node.type;
let typeDef = typeof node.type === 'string' ? getFinalTypeDef(node.type) : node.type;
// Nothing to do if type is an array or a struct type

@@ -493,3 +509,3 @@ if (typeDef.items || typeDef.elements) return;

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

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

if (existingProjection) {
signal(error`Cannot generate projection "${projectionAbsoluteName}" because of name conflict with existing artifact "${service.name.absolute}.$projectionId}"`);
error(null, ['definitions', projectionAbsoluteName],
`Cannot generate projection "${projectionAbsoluteName}" because of name conflict with existing artifact "${service.name.absolute}.$projectionId}"`);
return null;

@@ -521,4 +538,3 @@ }

let elements = Object.create(null);
for (let elemName in art.elements) {
let artElem = art.elements[elemName];
art.elements && Object.entries(art.elements).forEach(([elemName, artElem]) => {
let elem = Object.assign({}, artElem);

@@ -532,3 +548,4 @@ // Transfer xrefs, that are redirected to the projection

elements[elemName] = elem;
}
});
let query = {

@@ -546,5 +563,5 @@ 'SELECT': {

'kind': 'entity',
projection: query.SELECT, // it is important that projetion and query refer to the same object!
query,
elements,
//'$syntax': 'projection'
elements
};

@@ -574,3 +591,4 @@ // copy annotations from art to projection

if (draftAdminDataEntity.kind !== 'entity' || !draftAdminDataEntity.elements['DraftUUID']) {
signal(error`Generated entity "DRAFT.DraftAdministrativeData" conflicts with existing artifact`, ['definitions', 'DRAFT.DraftAdministrativeData']);
error(null, ['definitions', 'DRAFT.DraftAdministrativeData'],
`Generated entity "DRAFT.DraftAdministrativeData" conflicts with existing artifact`);
}

@@ -704,10 +722,8 @@

let targetArt = getCsnDef(target);
for (let keyElemName in targetArt.elements) {
let keyElem = targetArt.elements[keyElemName];
if (!keyElem.key) {
continue;
targetArt.elements && Object.entries(targetArt.elements).forEach(([keyElemName, keyElem]) => {
if (keyElem.key) {
let foreignKey = createForeignKey(keyElemName, keyElem);
addForeignKey(foreignKey, assoc);
}
let foreignKey = createForeignKey(keyElemName, keyElem);
addForeignKey(foreignKey, assoc);
}
});
}

@@ -750,3 +766,3 @@ return elem;

if (elem.keys.some(key => JSON.stringify(foreignKey) === JSON.stringify(key))) {
signal(error`Key already exists: ${JSON.stringify(foreignKey)}`);
error(null, null, `Key already exists: ${JSON.stringify(foreignKey)}`);
return;

@@ -776,7 +792,7 @@ }

if (artifact.elements[elemName]) {
let path = undefined;
let path = null;
if (artifactName) {
path = ['definitions', artifactName, 'elements', elemName];
}
signal(error`"${elemName}": Element name conflicts with existing element`, path);
error(null, path, { name: elemName }, `Generated element $(NAME) conflicts with existing element`);
return;

@@ -808,3 +824,3 @@ }

const path = ['definitions', artifactName, 'elements', elementName];
signal(error`"${elementName}": Element name conflicts with existing element`, path);
error(null, path, { name: elementName }, 'Generated element $(NAME) conflicts with existing element');
}

@@ -814,4 +830,5 @@

result[elementName] = {};
for (let prop in elem)
result[elementName][prop] = elem[prop];
elem && Object.entries(elem).forEach(([prop, value]) => {
result[elementName][prop] = value;
});
Object.assign(artifact.elements, result);

@@ -854,3 +871,3 @@ return result;

/**
* Add action 'action' to 'artifact'
* Add action 'action' to 'artifact' but don't overwrite existing action
*

@@ -860,5 +877,4 @@ * @param {object} action Action that shall be added to the given artifact.

* @param {CSN.Artifact} artifact Artifact in the form of `{ kind: 'entity', elements: ... }`
* @param {string} artifactName Name of the artifact (used for error locations).
**/
function addAction(action, artifact, artifactName) {
function assignAction(action, artifact) {
if (!artifact.actions) {

@@ -870,10 +886,6 @@ artifact.actions = Object.create(null);

// Element must not exist
if (artifact.actions[actionName]) {
signal(error`Generated action name conflicts with existing action "${actionName}"`,
['definitions', artifactName, 'actions', actionName]);
return;
if (!artifact.actions[actionName]) {
// Add the action
Object.assign(artifact.actions, action);
}
// Add the action
Object.assign(artifact.actions, action);
}

@@ -916,3 +928,3 @@

* @param {object} element Element to be checked
* @param {string[]} path
* @param {CSN.Path} path
* @param {CSN.Artifact} artifact

@@ -925,3 +937,3 @@ * @returns {boolean} True if no errors

if (element.elements || element.target || path.length > 4) {
signal(error`Element cannot be annotated with "${annoName}"`, path);
error(null, path, { anno: annoName }, 'Element can\'t be annotated with $(ANNO)');
return false;

@@ -944,6 +956,7 @@ }

if (array.length > 1) {
const loc = ['definitions', artifactName];
if (err == true) {
signal(error`"${annoName}" must be assigned only once`, ['definitions', artifactName]);
error(null, loc, { anno: annoName }, `Annotation $(ANNO) must be assigned only once`);
} else {
signal(warning`"${annoName}" must be assigned only once`, ['definitions', artifactName]);
warning(null, loc, { anno: annoName },`Annotation $(ANNO) must be assigned only once`);
}

@@ -957,4 +970,4 @@ }

* @param {CSN.Artifact} artifact the artifact
* @param {string[]} path path to get to `artifact` (mainly used for error messages)
* @param {(art: CSN.Artifact, path: string[]) => any} callback Function called for each element recursively.
* @param {CSN.Path} path path to get to `artifact` (mainly used for error messages)
* @param {(art: CSN.Artifact, path: CSN.Path) => any} callback Function called for each element recursively.
*/

@@ -966,7 +979,6 @@ function recurseElements(artifact, path, callback) {

path.push('elements', null);
for (let name in elements) {
let obj = elements[name];
Object.entries(elements).forEach(([name, obj]) => {
path[path.length - 1] = name;
recurseElements(obj, path, callback);
}
});
// reset path for subsequent usages

@@ -1119,5 +1131,5 @@ path.length -= 2; // equivalent to 2x pop()

let rc = []
for(const en in elements) {
const nps = { ref: [ (fullRef ? { id: en, _art: elements[en] } : en )] };
setProp(nps, '_art', elements[en]);
Object.entries(elements).forEach(([en, elt]) => {
const nps = { ref: [ (fullRef ? { id: en, _art: elt } : en )] };
setProp(nps, '_art', elt);
const paths = flattenPath( nps, fullRef, followMgdAssoc );

@@ -1127,3 +1139,3 @@ // prepend prefix path

rc.push(...paths);
}
});
return rc;

@@ -1217,5 +1229,5 @@ }

if(xn.length)
signal(signal.error`'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Sub path '${xn}' not found in ${((x.lhs ? rhs : lhs).ref.join('.'))}`, location)
error(null, location, `'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Sub path '${xn}' not found in ${((x.lhs ? rhs : lhs).ref.join('.'))}`)
else
signal(signal.error`'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Path '${((x.lhs ? lhs : rhs).ref.join('.'))}' does not match ${((x.lhs ? rhs : lhs).ref.join('.'))}`, location)
error(null, location, `'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Path '${((x.lhs ? lhs : rhs).ref.join('.'))}' does not match ${((x.lhs ? rhs : lhs).ref.join('.'))}`)
cont = false;

@@ -1227,3 +1239,3 @@ }

if(!isScalarOrNoType(x.lhs._art)) {
signal(signal.error`'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Path '${x.lhs.ref.join('.')}${(xn.length ? '.' + xn : '')}' must end on a scalar type`, location)
error(null, location, `'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Path '${x.lhs.ref.join('.')}${(xn.length ? '.' + xn : '')}' must end on a scalar type`)
cont = false;

@@ -1233,3 +1245,3 @@ }

if(!isScalarOrNoType(x.rhs._art)) {
signal(signal.error`'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Path '${x.rhs.ref.join('.')}${(xn.length ? '.' + xn : '')}' must end on a scalar type`, location)
error(null, location, `'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Path '${x.rhs.ref.join('.')}${(xn.length ? '.' + xn : '')}' must end on a scalar type`)
cont = false;

@@ -1242,3 +1254,3 @@ }

if(lhst !== rhst) {
signal(signal.info`'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Types for sub path '${xn}' don't match`, location)
info(null, location, `'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Types for sub path '${xn}' don't match`)
}

@@ -1245,0 +1257,0 @@ }

@@ -5,2 +5,4 @@ // Util functions for operations usually used with files.

const fs = require('fs');
/**

@@ -17,3 +19,2 @@ * Split the given source string into its lines. Respects Unix,

/**

@@ -29,6 +30,160 @@ * Change Windows style line endings to Unix style

/**
* Returns filesystem utils readFile(), isFile(), realpath() for _CDS_ usage.
* This includes a trace as well as usage of a file cache.
*
* Note: The synchronous versions accept a callback as well, which is executed
* immediately! This is different from NodeJS's readFileSync()!
* This is done to allow using it in places where fs.readFile (async) is used.
*
* @param {object} fileCache
* @param {boolean} enableTrace
*/
function cdsFs(fileCache, enableTrace) {
const readFile = _wrapReadFileCached(fs.readFile);
const readFileSync = _wrapReadFileCached((filename, enc, cb) => {
try {
cb(null, fs.readFileSync( filename, enc ));
}
catch (err) {
cb(err, null);
}
});
const isFile = _wrapIsFileCached(fs.stat);
const isFileSync = _wrapIsFileCached(( filename, cb) => {
try {
cb(null, fs.statSync( filename ));
}
catch (err) {
cb( err, null );
}
});
return {
readFile,
readFileSync,
isFile,
isFileSync,
realpath,
realpathSync,
};
function realpath(path, cb) {
return fs.realpath(path, cb);
}
function realpathSync(path, cb) {
try {
cb(null, fs.realpathSync(path));
}
catch (err) {
cb(err, null);
}
}
/**
* Wraps the given reader into a cached environment including a trace.
* The given @p reader must have the same signature as fs.readFile.
*
* @param {(filename: string, enc: string, cb: (err, data) => void) => void} reader
*/
function _wrapReadFileCached( reader ) {
return (filename, enc, cb) => {
if (typeof enc === 'function') { // moduleResolve uses old-style API
cb = enc;
enc = null;
}
let body = fileCache[filename];
if (body && typeof body === 'object' && body.realname) {
filename = body.realname; // use fs.realpath name
body = fileCache[filename];
}
if (body !== undefined && body !== true) { // true: we just know it is there
if (body === false) {
body = new Error( `ENOENT: no such file or directory, open '${ filename }'`);
body.code = 'ENOENT';
body.errno = -2;
body.syscall = 'open';
body.path = filename;
}
if (body instanceof Error) {
traceFS( 'READFILE:cache-error:', filename, body.message );
cb( body ); // no need for process.nextTick( cb, body ) with moduleResolve
}
else {
traceFS( 'READFILE:cache:', filename, body );
cb( null, body );
}
}
else {
traceFS( 'READFILE:start:', filename );
// TODO: set cache directly to some "delay" - store error differently?
// e.g. an error of callback functions!
reader(filename, enc, ( err, data ) => {
fileCache[filename] = err || data;
traceFS( 'READFILE:data:', filename, err || data );
cb( err, data );
});
}
};
}
/**
* Wraps the given fsStat into a cached environment including a trace.
* The given @p fsStat must have the same signature as fs.stat.
*
* @param {(filename: string, cb: (err, data) => void) => void} fsStat
*/
function _wrapIsFileCached(fsStat) {
return ( filename, cb ) => {
let body = fileCache[filename];
if (body !== undefined) {
traceFS( 'ISFILE:cache:', filename, body );
if (body instanceof Error)
cb( body ); // no need for process.nextTick( cb, body ) with moduleResolve
else
cb( null, !!body );
}
else {
traceFS( 'ISFILE:start:', filename, body );
// in the future (if we do module resolve ourself with just readFile),
// we avoid parallel readFile by storing having an array of `cb`s in
// fileCache[ filename ] before starting fs.readFile().
fsStat( filename, ( err, stat ) => {
if (err)
body = (err.code === 'ENOENT' || err.code === 'ENOTDIR') ? false : err;
else
body = !!(stat.isFile() || stat.isFIFO());
if (fileCache[filename] === undefined) // parallel readFile() has been processed
fileCache[filename] = body;
traceFS( 'ISFILE:data:', filename, body );
if (body instanceof Error)
cb( err );
else
cb( null, body );
});
}
};
}
function traceFS( intro, filename, data ) {
if (!enableTrace)
return;
if (typeof data === 'string' || data instanceof Buffer)
data = typeof data;
else if (data === undefined)
data = '?';
else
data = `${ data }`;
// eslint-disable-next-line no-console
console.log( intro, filename, data);
}
}
module.exports = {
splitLines,
normalizeLineEndings,
cdsFs,
};

@@ -9,7 +9,189 @@ // Custom resolve functionality for the CDS compiler

const path = require('path');
const fs = require('fs');
const { makeMessageFunction } = require('../base/messages');
const { cdsFs } = require('./file');
const DEFAULT_ENCODING = 'utf-8';
/**
* Default lookup-extensions. If a module "./index" is requested, then
* "./index.cds" is checked first, then "index.csn" and so on.
*/
const extensions = [ '.cds', '.csn', '.json' ];
/**
* A global cds.home configuration can be set that forces the cds-compiler to
* use a certain directory for all @sap/cds/ includes.
* This function handles such module paths.
*
* @todo Re-think:
* - Why can't a JAVA installation set a (symbolic) link?
* - Preferred to a local installation? Not the node-way!
* - Why a global? The Umbrella could pass it as an option.
*
* @param {string} modulePath
* @returns {string}
*/
function adaptCdsModule(modulePath) {
// eslint-disable-next-line
if (global['cds'] && global['cds'].home && modulePath.startsWith( '@sap/cds/' ))
// eslint-disable-next-line
return global['cds'].home + modulePath.slice(8)
return modulePath;
}
/**
* @param {object} dep
* @param {object} fileCache
* @param {CSN.Options} options
*/
function resolveModule( dep, fileCache, options ) {
const _fs = cdsFs(fileCache, options.traceFs);
// let opts = { extensions, packageFilter, basedir: dep.basedir, preserveSymlinks: false };
// `preserveSymlinks` option does not really work -> provide workaround anyway...
// Hm, the resolve package also does not follow the node recommendation:
// "Using fs.stat() to check for the existence of a file before calling
// fs.open(), fs.readFile() or fs.writeFile() is not recommended"
const opts = {
extensions,
packageFilter,
basedir: dep.basedir,
isFile: _fs.isFile,
readFile: _fs.readFile,
realpath: _fs.realpath,
};
return new Promise( (fulfill, reject) => {
const lookupPath = adaptCdsModule(dep.module);
resolveCDS( lookupPath, opts, (err, res) => {
// console.log('RESOLVE', dep, res, err)
if (err) {
reject(err);
}
else {
const body = fileCache[res];
if (body === undefined || body === true) { // use fs if no or just temp entry
dep.absname = res;
_fs.realpath( res, cb );
}
else if (body && typeof body === 'object' && body.realname) {
// dep.absname = body.realname;
cb( null, body.realname ); // use fs.realpath name
}
else {
// dep.absname = res;
cb( null, res );
}
}
});
function cb( err, res ) {
if (err) {
reject(err);
}
else {
if (dep.absname)
fileCache[dep.absname] = (dep.absname === res) || { realname: res };
dep.resolved = res; // store in dep that module resolve was successful
for (const from of dep.usingFroms)
from.realname = res;
fulfill(res);
}
}
}).catch( () => { // (err) TODO: check for expected exceptions
_errorFileNotFound(dep, options);
return false;
});
}
/**
* @param {object} dep
* @param {object} fileCache
* @param {CSN.Options} options
*/
function resolveModuleSync( dep, fileCache, options ) {
const _fs = cdsFs(fileCache, options.traceFs);
const opts = {
extensions,
packageFilter,
basedir: dep.basedir,
isFile: _fs.isFileSync,
readFile: _fs.readFileSync,
realpath: _fs.realpathSync,
};
let result = null;
let error = null;
const lookupPath = adaptCdsModule(dep.module);
resolveCDS( lookupPath, opts, (err, res) => {
if (err)
error = err;
if (res)
result = res;
});
if (error) {
_errorFileNotFound(dep, options); // TODO: Use (err);
return false;
}
const body = result ? fileCache[result] : undefined;
if (body === undefined || body === true) { // use fs if no or just temp entry
dep.absname = result;
_fs.realpathSync( result, (err, modulePath) => {
if (err)
error = err;
else
result = modulePath;
});
}
else if (body && typeof body === 'object' && body.realname) {
result = body.realname;
}
if (error) {
_errorFileNotFound(dep, options); // TODO: Use (error);
return false;
}
if (dep.absname)
fileCache[dep.absname] = (dep.absname === result) || { realname: result };
dep.resolved = result; // store in dep that module resolve was successful
for (const from of dep.usingFroms)
from.realname = result;
return result;
}
function _errorFileNotFound(dep, options) {
const { error } = makeMessageFunction( null, options, 'compile' );
if (dep.resolved) {
let resolved = path.relative( dep.basedir, dep.resolved );
if (options.testMode)
resolved = resolved.replace( /\\/g, '/' );
for (const from of dep.usingFroms) {
error( 'file-not-readable', from.location, { file: resolved },
'Cannot read file $(FILE)' );
}
}
else if (isLocalFile( dep.module ) ) {
for (const from of dep.usingFroms) {
error( 'file-unknown-local', from.location, { file: dep.module },
'Cannot find local module $(FILE)' );
}
}
else {
const internal = /[\\/]/.test( dep.module ) && 'internal';
for (const from of dep.usingFroms) {
error( 'file-unknown-package', from.location,
{ file: dep.module, '#': internal }, {
std: 'Cannot find package $(FILE)',
internal: 'Cannot find package module $(FILE)',
} );
}
}
}
/**
* Resolve the given path according to NodeJS's rules for `require()`.

@@ -30,3 +212,3 @@ *

// So neither do we.
fs.realpath(resolvedBaseDir, (realPathErr, realPath) => {
options.realpath(resolvedBaseDir, (realPathErr, realPath) => {
// There may be an error in resolving the symlink.

@@ -243,3 +425,4 @@ // We ignore the error and simply use the original path.

const moduleError = new Error(`Cannot find module '${ moduleName }' from '${ options.basedir }'`);
moduleError.code = 'MODULE_NOT_FOUND';
// eslint-disable-next-line
moduleError['code'] = 'MODULE_NOT_FOUND';
return moduleError;

@@ -272,8 +455,2 @@ }

/**
* Default lookup-extensions. If a module "./index" is requested, then
* "./index.cds" is checked first, then "index.csn" and so on.
*/
const extensions = [ '.cds', '.csn', '.csn.json', '.json' ];
/**
* @typedef {object} ResolveOptions

@@ -285,3 +462,4 @@ * @property {string} basedir

* @property {(path: string, callback: (err, foundAndIsFile) => void) => void} isFile
* @property {(path: string, encoding, callback: (err, content) => void) => void} readFile Function
* @property {(path: string, encoding, callback: (err, content) => void) => void} readFile
* @property {(path: string, callback: (err, realpath) => void) => void} realpath
* used to read `package.json` files.

@@ -291,6 +469,8 @@ */

module.exports = {
resolveModule,
resolveModuleSync,
// exported for unit tests
resolveCDS,
isLocalFile,
packageFilter,
extensions,
};

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

constructor() {
this.tracestack = [];
this.traceStack = [];
}
/**
* Start a new TimeTrace, using the given id for loggin etc.
* Start a new TimeTrace, using the given id for logging etc.
*

@@ -72,4 +72,4 @@ * @param {string} id A short description of whats going on

const b = new TimeTrace(id);
this.tracestack.push(b);
b.start(this.tracestack.length - 1);
this.traceStack.push(b);
b.start(this.traceStack.length - 1);
}

@@ -90,4 +90,4 @@ catch (e) {

try {
const current = this.tracestack.pop();
current.stop(this.tracestack.length);
const current = this.traceStack.pop();
current.stop(this.traceStack.length);
}

@@ -94,0 +94,0 @@ catch (e) {

{
"name": "@sap/cds-compiler",
"version": "1.50.2",
"version": "2.1.4",
"description": "CDS (Core Data Services) compiler and backends",

@@ -14,2 +14,5 @@ "homepage": "https://cap.cloud.sap/",

"main": "lib/main.js",
"scripts": {
"postinstall": "node lib/fix_antlr4-8_warning.js"
},
"keywords": [

@@ -19,5 +22,3 @@ "CDS"

"dependencies": {
"antlr4": "4.8.0",
"resolve": "1.8.1",
"sax": "^1.2.4"
"antlr4": "4.8.0"
},

@@ -24,0 +25,0 @@ "files": [

# Getting started
<!-- markdownlint-disable MD001 MD022 -->

@@ -4,0 +5,0 @@ ##### Table of Contents

# check-proper-type-of
An element in a `type of` expression does not have proper type information.
An element in a `type of` expression doesn’t have proper type information.
The message's severity is `Info` but may be raised to `Error` in the SQL, HANA
and OData backends. These backends require elements to have a type. Otherwise
they are not able to render elements (e.g. to SQL columns).
The message's severity is `Info` but may be raised to `Error` in the SQL,
SAP HANA, and OData backends. These backends require elements to have a type.
Otherwise they aren’t able to render elements (for example, to SQL columns).
## Example
Erroneous code example
Erroneous code example:
```
```cdl
entity Foo {

@@ -30,5 +30,5 @@ key id : Integer;

to have a proper type. However, because the referenced element is calculated,
the compiler is not able to determine the correct type.
the compiler isn’t able to determine the correct type.
The element still inherits `ViewFoo:calculatedField`'s annotations and other
properties but will not have a proper type which is required by some backends.
properties but won’t have a proper type, which is required by some backends.

@@ -39,3 +39,3 @@ ## Fix

```
```cdl
view ViewFoo as select from Foo {

@@ -46,4 +46,4 @@ 1+1 as calculatedField @(anno) : Integer

## Related messages
## Related Messages
- `check-proper-type`
# check-proper-type
A type artifact does not have proper type information.
A type artifact doesn’t have proper type information.
The message's severity is `Info` but may be raised to `Error` in the SQL, HANA
and OData backends. These backends require types to have type information.
Otherwise they are not able to render elements that use this type (e.g. to SQL
columns).
The message's severity is `Info` but may be raised to `Error` in the SQL,
SAP HANA, and OData backends. These backends require types to have type
information. Otherwise they aren’t able to render elements that use this
type (for example, to SQL columns).
This message affects CSN input and should not appear if CDL input is used.
This message affects CSN input and shouldn’t appear if CDL input is used.
## Example
Erroneous code example
Erroneous code example:

@@ -30,4 +30,4 @@ ```json

To fix the issue, add explicit type information to `MainType`, e.g. add an
`elements` property to make a structured type.
To fix the issue, add explicit type information to `MainType`, for example, add
an `elements` property to make a structured type.

@@ -49,4 +49,4 @@ ```json

## Related messages
## Related Messages
- `check-proper-type-of`
# extend-repeated-intralayer
The order of elements of an artifact may not be stable due to multiple extensions
in the same layer (e.g. same file).
The order of elements of an artifact may not be stable due to multiple
extensions in the same layer (for example in the same file).
A _layer_ can be seen as a group of connected sources, e.g. CDL files.
They form a cyclic connection through their dependencies (e.g. `using` in CDL).
A _layer_ can be seen as a group of connected sources, for example, CDL files.
They form a cyclic connection through their dependencies
(for example, `using` in CDL).
## Example
Erroneous code example with a single CDL file.
Erroneous code example with a single CDL file:
```
```cdl
entity FooBar { }

@@ -21,8 +22,8 @@

Due to multiple extensions in the example above, the order of `foo` and `bar`
inside `FooBar` may not be stable. You therefore cannot depend on it.
inside `FooBar` may not be stable. You therefore can’t depend on it.
It is also possible to trigger this warning with multiple files.
Take a look at the example below:
It's also possible to trigger this warning with multiple files.
Look at the following example:
```
```cdl
// (1) Definition.cds

@@ -39,11 +40,16 @@ using from './Extension.cds';

Here we have a cyclic dependency between (1) and (2). Together they form one
layer with multiple extensions. Again, the element order is not stable.
layer with multiple extensions. Again, the element order isn’t stable.
## Fix
To fix the issue, move extensions for the same artifact into the extension block:
To fix the issue, move extensions for the same artifact into the same extension
block:
```
```cdl
// (1) Definition.cds : No extension block
using from './Extension.cds';
entity FooBar { }
// (2) Extension.cds : Now contains both extensions
using from './Definition.cds';
extend FooBar {

@@ -55,4 +61,4 @@ foo : Integer;

## Related messages
## Related Messages
- `extend-unrelated-layer`
# extend-unrelated-layer
Unstable element order due to extensions for the same artifact in unrelated layers.
Unstable element order due to extensions for the same artifact in
unrelated layers.
A _layer_ can be seen as a group of connected sources, e.g. CDL files.
They form a cyclic connection through their dependencies (e.g. `using` in CDL).
A _layer_ can be seen as a group of connected sources, for example CDL files.
They form a cyclic connection through their dependencies
(for example, `using` in CDL).

@@ -12,3 +14,3 @@ ## Example

```
```cdl
// (1) Base.cds: Contains the artifact that should be extended

@@ -32,7 +34,7 @@ entity FooBar { }

That is because the extensions in (2) and (3) are in different layers and when
used in (4) it cannot be ensured which extension is applied first.
used in (4) it can’t be ensured which extension is applied first.
Instead of passing (4) to the compiler you can also pass (2) and (3) to it.
Because there are no cyclic dependencies between the files, each file represents
one layer.
Instead of passing (4) to the compiler, you can also pass (2) and (3) to it.
Because there are no cyclic dependencies between the files, each file
represents one layer.

@@ -42,7 +44,8 @@ ## Fix

To fix the issue, move extensions for the same artifact into the same layer,
i.e. same file.
that is, the same file.
For the erroneous example above, remove the extension from (3) and move it to (2):
For the erroneous example above, remove the extension from (3) and move
it to (2):
```
```cdl
// (2) FooExtend.cds

@@ -56,4 +59,4 @@ using from './Base';

## Related messages
## Related Messages
- `extend-repeated-intralayer`
# Message Explanations
This directory contains explanations for various compiler messages. These
long-form texts are not limited to errors but can also explain info
long-form texts aren’t limited to errors but can also explain info
messages or warnings.

@@ -12,3 +12,3 @@

1. Heading with message-id
2. Short description (one sentence, e.g. the compiler message's text)
2. Short description (one sentence, for example the compiler message's text)
3. Default severity (also mention when upgraded to error)

@@ -21,3 +21,3 @@ 4. Erroneous code example

7. Fixed code example
8. Related messages
8. Related Messages

@@ -29,3 +29,3 @@ All markdown conventions of cds-compiler shall be applied to the explanation

- Do not use passive but directly address the user (i.e. "you")
- Do not use passive but directly address the user (that means "you")
Passive is a German thing.

@@ -32,0 +32,0 @@ - Use a column width of 80

@@ -7,3 +7,3 @@ # redirected-to-ambiguous

The message's severity is `Error` and is raised by the compiler.
The error happens due to an ill-formed redirection which requires changes to
The error happens due to an ill-formed redirection, which requires changes to
your model.

@@ -13,5 +13,5 @@

Erroneous code example
Erroneous code example:
```
```cdl
entity Main {

@@ -31,3 +31,3 @@ key id : Integer;

{
// This redirection cannot be resolved:
// This redirection can’t be resolved:
Main.toTarget : redirected to View

@@ -37,6 +37,6 @@ };

Entity `Target` exists more than once in `View`. In the example above this
Entity `Target` exists more than once in `View`. In the previous example, this
happens through the *direct* sources in the select clause.
Because the original target exists twice in the redirected target, the compiler
is not able to correctly resolve the redirection due to ambiguities.
isn’t able to correctly resolve the redirection due to ambiguities.

@@ -50,6 +50,6 @@ This can also happen through *indirect* sources. For example if entity `Main`

To fix the issue, you must have the original target only once in your direct
and indirect sources. The example above can be fixed by removing `Duplicate`
from the select clause.
and indirect sources. The previous example can be fixed by removing
`Duplicate` from the select clause.
```
```cdl
view View as select from Main, Target {

@@ -60,6 +60,6 @@ Main.toTarget : redirected to View

If this is not feasible then you have to redefine the association using a mixin
If this isn’t feasible then you have to redefine the association using a mixin
clause.
```
```cdl
view View as select from Main, Target mixin {

@@ -74,4 +74,4 @@ toMain : Association to View on Main.id = Target.id;

## Related messages
## Related Messages
- `redirected-to-unrelated`
# redirected-to-unrelated
The redirected target does not originate from the original target.
The redirected target doesn’t originate from the original target.
The message's severity is `Error` and is raised by the compiler.
The error happens due to an ill-formed redirection which requires changes to
The error happens due to an ill-formed redirection, which requires changes to
your model.

@@ -13,3 +13,3 @@

```
```cdl
entity Main {

@@ -31,4 +31,4 @@ key id : Integer;

Projection `InvalidRedirect` tries to redirect `toMain` to `Secondary`.
However, that entity does not have any connection to the original target
`Main`, i.e. it does not originate from `Main`.
However, that entity doesn’t have any connection to the original target
`Main`, that means, it doesn’t originate from `Main`.

@@ -40,3 +40,3 @@ While this example may be clear, your model may have multiple redirections

```
```cdl
entity Main {

@@ -59,3 +59,3 @@ key id : Integer;

in `SecondRedirect`. But because `SecondRedirect` uses `toMain` from
`FirstRedirect`, the original target is `FirstRedirect`. And `Main` does not
`FirstRedirect`, the original target is `FirstRedirect`. And `Main` doesn’t
originate from `FirstRedirect` but only vice versa.

@@ -67,6 +67,6 @@

from the original target. In the first example above you could redirect
`SecondRedirect:toMain` to `SecondRedirect`. However, if that is not feasible
`SecondRedirect:toMain` to `SecondRedirect`. However, if that isn’t feasible
then you have to redefine the association using a mixin clause.
```
```cdl
view SecondRedirect as select from FirstRedirect mixin {

@@ -80,4 +80,4 @@ toMain : Association to Main on id = $self.id;

## Related messages
## Related Messages
- `redirected-to-ambiguous`
# rewrite-not-supported
The compiler is not able to rewrite ON conditions for some associations.
The compiler isn’t able to rewrite ON conditions for some associations.
They have to be explicitly defined by the user.
The message's severity is `Warning` but will be raised to `Error` in the SQL,
HANA and OData backends. These backends require associations to have proper
ON conditions.
The message's severity is `Error`.

@@ -14,3 +12,3 @@ ## Example

```
```cdl
entity Base {

@@ -35,9 +33,9 @@ key id : Integer;

id,
primary.secondary // Error: The ON condition is not rewritten here
primary.secondary // Error: The ON condition isn’t rewritten here
};
```
In the example above the ON condition in `View` of `secondary` cannot be
In the previous example, the ON condition in `View` of `secondary` can’t be
automatically rewritten because the associations are unmanaged and the
compiler cannot determine how to properly rewrite them for `View`.
compiler can’t determine how to properly rewrite them for `View`.

@@ -49,3 +47,3 @@ ## Fix

```
```cdl
entity View as select from Base {

@@ -59,4 +57,4 @@ id,

In the corrected view above the association `secondary` gets an explicit ON
In the corrected view above, the association `secondary` gets an explicit ON
condition. For this to work it is necessary to add `secondary_id` to the
selection list, i.e. we have to explicitly use the foreign key.
selection list, that means, we have to explicitly use the foreign key.

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

SocketSocket SOC 2 Logo

Product

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

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc