Socket
Socket
Sign inDemoInstall

@sap/cds-compiler

Package Overview
Dependencies
Maintainers
3
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.5.0 to 1.8.0

bin/cdshi.js

517

bin/cdsc.js

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

const program = require('commander');
const compiler = require('../lib/main');
const { getDefaultTntFlavorOptions } = require('../lib/transform/tntSpecific');
var util = require('util');

@@ -23,137 +21,4 @@ var fs = require('fs');

var reveal = require('./raw-output');
const { optionProcessor } = require('../lib/backends');
program
.usage('[options] <file> ...')
.description(`Compile a CDS model given from the input files. Input files may be CDS source files (.cds), CSN
model files (.json) and XML files (.xml)
for pre-processed ODATA annotations.`)
// Uncomment this to make it easier to check for the 100 columns limitation in the '--help' output
// .option('@@', '34567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890')
.option('@@', 'General Options')
.option('-h, --help', 'Display this help text and exit')
.option('-w, --warning <level>', 'Show warnings up to <level> (0: Error, 1: Warnings, 2: Info (default))',
verifyWarningOption)
.option(' --show-message-id', 'Show message ID in error, warning and info messages')
.option('-v, --version', 'Display version number and exit')
.option('@@', 'Generation options (can be combined, default if none given is "--to-csn client --out -")')
.option('-o, --out <dir>', 'Place generated files in directory <dir>, default is "-" for <stdout>')
.option('-H, --to-hana <flags>', `Generate HANA CDS source files, <flags> can be a comma-separated
combination of either "plain" (default), "quoted" or "hdbcds" for
entity names, either "assocs" (default) or "joins" for associations
and any of "src,csn"
plain : Produce HANA entity and element names in uppercase and
flattened with underscores. Do not generate structured
types.
quoted : Produce HANA entity and element names in original case
as in CDL. Keep nested contexts (resulting in entity names
with dots), but flatten element names with underscores.
Generate structured types, too.
hdbcds : Produce HANA entity end element names as HANA CDS would
generate them from the same CDS source (like "quoted", but
using element names with dots).
assocs : Keep associations in HANA CDS as far as possible
joins : Transform associations to HANA CDS joins
src : Generate HANA CDS source files "<artifact>.hdbcds"
csn : Generate "hana_csn.json" with HANA-preprocessed model`, verifyToHanaOption)
.option('-O, --to-odata <flags>', `Generate ODATA metadata and annotations, <flags> can be a comma-
separated combination of "xml,json,separate,combined,csn" and either
"v2" (default) or "v4" version. Additionally, one of
"plain,quoted,hdbcds" can be specified to annotate resulting DB names
in the generated CSN.
v2 : Generate ODATA V2 output
v4 : Generate ODATA V4 output
xml : Generate XML output (separate or combined)
json : Generate JSON output (not available for V2)
separate: Generate "<svc>_metadata.xml" and "<svc>_annotations.xml"
combined: Generate "<svc>.xml"
csn : Generate "odata_csn.json" with ODATA-preprocessed model
plain : Annotate DB names in "plain" style (see --to-hana,
--to-sql)
quoted : Annotate DB names in "quoted" style (see --to-hana,
--to-sql)
hdbcds : Annotate DB names in "hdbcds" style (see --to-hana,
--to-sql)`, verifyToOdataOption)
.option('-C, --to-cdl', 'Generate CDS source files "<artifact>.cds"')
.option('-S, --to-swagger <flags>', `Generate Swagger (OpenAPI) JSON, <flags> can be a comma-separated
combination of "json" and "csn"
json : Generate OpenAPI JSON output for each service as
"<svc>_swagger.json"
csn : Generate "swagger_csn.json" with Swagger-preprocessed
model`, verifyToSwaggerOption)
.option('-Q, --to-sql <flags>', `Generate SQL DDL statements to create tables and views, <flags> can be
a comma-separated combination of either "plain" (default), "quoted" or
"hdbcds" for entity names, either "assocs" (default) or "joins" for
associations, either "hana" or "sqlite" for the SQL dialect and any
of "src,csn"
plain : Produce SQL table and view names in uppercase and
flattened with underscores (no quotes required)
quoted : Produce SQL table and view names in original case
as in CDL (with dots), but flatten element names with
underscores (requires quotes)
hdbcds : Produce SQL table, view and column names as HANA CDS
would generate them from the same CDS source (like
"quoted", but using element names with dots).
assocs : Keep associations as far as possible (only usable for
HANA SQL)
joins : Transform associations to SQL joins
src : Generate SQL source files as "<artifact>.sql"
hana : Generate HANA specific SQL
sqlite : Generate SQLite specific SQL
csn : Generate "sql_csn.json" with SQL-preprocessed model`, verifyToSqlOption)
.option(' --to-csn <flavor>', `Generate original model as CSN to "csn.json", <flavor> can be either
"client" (default) or "gensrc"
client : Generate standard CSN consumable by clients and backends
gensrc : Generate CSN specifically for use as a source, e.g. for
combination with additional extend/annotate statements,
but not suitable for consumption by clients or backends`,
verifyToCsnOption)
.option('-l, --lint-mode', `Generate nothing, just produce single-file error messages if any (for
use by editors)`)
.option('@@', 'Backward compatibility options (deprecated, do not use)')
.option(' --oldstyle-self', 'Allow "self" alternatively to "$self" (implied by --tnt-flavor)') // FIXME: We should get rid of that
.option(' --tnt-flavor', 'Compile with backward compatibility for the "TNT" project')
.option(' --tnt-csn', 'Generate TNT-specific post-processed CSN') // FIXME: Should migrate towards --to-odata v2,xml,separate,csn'
.option('@@', 'Diagnostic options')
.option(' --trace-parser', 'Trace parser') // FIXME: Previously --trace-parse
.option(' --trace-parser-amb', 'Trace parser ambiguities') // FIXME: Previously --ambig-detection
.option(' --trace-fs', 'Trace file system access caused by "using from"')
.option('@@', 'Validation options')
.option(' --fuzzy-csn', 'Allow free-style CSN properties by disabling CSN input validation')
.option('@@', 'Internal options (for testing only, may be changed/removed at any time)')
.option('-R, --raw-output', 'Write raw augmented CSN and error output to stdout')
.option(' --beta-mode', 'Enable unsupported, incomplete (beta) features')
.option(' --new-csn', 'Produce new-style CSN (preview of planned future CSN format)')
.option(' --hana-flavor', 'Compile with backward compatibility for HANA CDS (incomplete)')
.option(' --parse-only', 'Stop compilation after parsing and write result to stdout')
.option(' --to-extensions', 'Generate augmented CSN for extensions from properties file') // FIXME: Previously 'generate-extensions', should later become part of normal compilation, producing plain CSN
.option(' --test-mode', `Produce extra-stable output for automated tests (normalize
filenames in errors, sort properties in CSN, omit version in CSN)`)
.option(' --extra-augment', 'Compile to plain CSN, augment and compile again, augmentation tests')
.option(' --re-augmented', 'Re-augmented CSN and error output') // FIXME: What does that mean/do? Isn't that what --extra-augment is supposed to do?
.option(' --old-sql-impl', `Use old "toSql" implementation (deprecated, only as fallback)`)
.option('@@', 'Table renaming (tentative, subject to change, requires "--beta-mode")')
.option(' --to-rename <flags>', `Generate SQL DDL statements to rename existing tables and their
columns so that they match the result of "--to-hana" or "--to-sql"
with the "plain" option. Possible values for <flags> are either
"quoted" or "hdbcds" (default) for the names of the existing tables.
Results are generated as "rename_<artifact>.sql"
quoted : Assume existing SQL tables and views were named in
original case as in CDL (with dots), but element names
were flattened with underscores (as a result e.g. from
"cdsc --to-hana src,quoted")
hdbcds : Assume existing SQL tables, views and columns were
generated by HANA CDS from the same CDS source (or from
"cdsc --to-sql src,hdbcds")`, verifyToRenameOption)
;
// Note: When adding any options where the '--foo-bar' part becomes longer than the longest existing one,
// you will need to adapt the indentation of those having multiple lines, so that '--help' comes out aligned.
// Generally, the alignment of the lines above needs a bit of care - the multi-line ones need to be just so...
// Also, we try to keep the output below 100 characters per line.
// Note: Instead of throwing ProcessExitError, we would rather just call process.exit(exitCode),

@@ -169,64 +34,41 @@ // but that might truncate the output of stdout and stderr, both of which are async (or rather,

// Deal with '--version' explicitly (so that it appears in the proper place in '--help')
program.on('option:version', function() {
process.stdout.write(compiler.version() + '\n');
throw new ProcessExitError(0);
});
// Deal with '--help' explicitly (needs some tweaking for the text formatting)
program.on('option:help', function() {
displayUsage(null, 0);
});
// Report unknown options explicitly (so that it looks like the other error messages)
program.unknownOption = (option) => {
displayUsage(`Unknown option "${option}"`, 2);
}
// Parse the command line and translate it into options
try {
program.parse(process.argv);
// Complain if no files given
// FIXME: Might later read from stdin instead
if (!program.args.length) {
displayUsage('Missing <file> ... arguments', 2);
let cmdLine = optionProcessor.processCmdLine(process.argv);
// Deal with '--version' explicitly
if (cmdLine.options.version) {
process.stdout.write(compiler.version() + '\n');
throw new ProcessExitError(0);
}
// Please keep these in the same order as the --help output
var options = {
// Default warning level is 2 (info)
warning : program.warning === undefined ? 2 : program.warning,
showMessageId : program.showMessageId,
// Default output goes to stdout
out : program.out || '-',
toHana: program.toHana,
toOdata : program.toOdata,
toCdl : program.toCdl,
toSwagger : program.toSwagger,
toSql : program.toSql,
lintMode : program.lintMode,
// By default, toCsn is on if no other to-option is set
toCsn : program.toCsn || (!program.toHana && !program.toOdata && !program.toCdl && !program.toSwagger && !program.toSql
&& !program.lintMode && !program.toRename),
oldstyleSelf : program.oldstyleSelf,
tntFlavor : program.tntFlavor && getDefaultTntFlavorOptions(),
tntCsn : program.tntCsn,
traceParser : program.traceParser,
traceParserAmb : program.traceParserAmb,
traceFs : program.traceFs,
fuzzyCsn: program.fuzzyCsn,
rawOutput : program.rawOutput,
betaMode : program.betaMode,
newCsn : program.newCsn, // TEMP
hanaFlavor : program.hanaFlavor,
parseOnly : program.parseOnly,
toExtensions : program.toExtensions,
testMode : program.testMode,
extraAugment : program.extraAugment,
reAugmented : program.reAugmented,
oldSqlImpl : program.oldSqlImpl,
toRename : program.toRename,
// Deal with '--help' explicitly
if (cmdLine.command) {
// Command specific help
if (cmdLine.options.help || cmdLine.options[cmdLine.command] && cmdLine.options[cmdLine.command].help) {
displayUsage(null, optionProcessor.commands[cmdLine.command].helpText, 0);
}
} else if (cmdLine.options.help) {
// General help
displayUsage(null, optionProcessor.helpText, 0);
}
// Report complaints if any
if (cmdLine.cmdErrors.length > 0) {
// Command specific errors
displayUsage(cmdLine.cmdErrors, optionProcessor.commands[cmdLine.command].helpText, 2);
} else if (cmdLine.errors.length > 0) {
// General errors
displayUsage(cmdLine.errors, optionProcessor.helpText, 2);
}
// Do the work for all options at once
executeCommandLine(options, program.args)
// Default warning level is 2 (info)
// FIXME: Is that not set anywhere in the API?
if (!cmdLine.options.warning) {
cmdLine.options.warning = 2;
}
// Default output goes to stdout
if (!cmdLine.options.out) {
cmdLine.options.out = '-';
}
// Do the work for the selected command (default 'toCsn'
executeCommandLine(cmdLine.command || 'toCsn', cmdLine.options, cmdLine.args);
} catch (err) {

@@ -242,13 +84,14 @@ // This whole try/catch is only here because process.exit does not work in combination with

// Display 'error' (if any) and the program's usage help text, then exit with exit code <code>
function displayUsage(error, code) {
program.outputHelp(helpText => {
return helpText
.replace(/\n\n {2}Options:\n\n/, '') // Get rid of the extra 'Options:' headline with many newlines
.replace(/ {4}@@ */g, '\n ') // Convert the '@@' fake-options to subsections
.replace(/ {4}-h, --help *output usage information\n/g, ''); // Get rid of auto-generated '--help'
});
// Display error at the end (more readable, no scrolling)
// Display help text 'helpText' and 'error' (if any), then exit with exit code <code>
function displayUsage(error, helpText, code) {
// Display non-error output (like help) to stdout
let out = (code == 0 && !error) ? process.stdout : process.stderr;
// Display help text first, error at the end (more readable, no scrolling)
out.write(`${helpText}\n`);
if (error) {
process.stderr.write(`\n ERROR: ${error}\n`);
if (error instanceof Array) {
out.write(error.map(error => `cdsc: ERROR: ${error}`).join('\n') + '\n');
} else {
out.write(`cdsc: ERROR: ${error}\n`);
}
}

@@ -258,198 +101,4 @@ throw new ProcessExitError(code);

// Check the value of --warning for legal values. Return value as an int.
function verifyWarningOption(value) {
let result = parseInt(value);
if (Number.isNaN(result) || result < 0 || result > 2) {
displayUsage(`Illegal <level> "${value}" for option "-w, --warning"`, 2);
}
return result;
}
// Check whether all 'flags' occur in array 'allowedFlags'. Complain if one doesn't, mentioning
// the option name 'optStr' and optionally the flag name 'flagId'. Return an object with each of 'flags'
// set to 'true'
function verifyFlags(allowedFlags, flags, optStr, flagId='flag') {
let result = {};
for (let flag of flags.split(',')) {
if (!allowedFlags.includes(flag)) {
displayUsage(`Illegal <${flagId}> "${flag}" for option "${optStr}", allowed are: "${allowedFlags.join(',')}"`, 2);
}
result[flag] = true;
}
return result;
}
// Check the value of --to-odata for legal values. Return the value as an object with sub-options.
function verifyToOdataOption(value) {
let result = verifyFlags([
'v4', 'v2',
'xml', 'json',
'separate', 'combined',
'csn',
'plain', 'quoted', 'hdbcds'], value, '-O, --to-odata');
if (result.v2) {
result.version = 'v2'
}
if (result.v4) {
result.version = 'v4'
}
if (result.v2 && result.v4) {
displayUsage(`Either "v2" (default) or "v4" can be specified for version with option "-O, --to-odata", but not both`, 2);
}
delete result.v2;
delete result.v4;
let nrOfNameFlags = 0;
if (result.plain) {
result.names = 'plain'
nrOfNameFlags++;
}
if (result.quoted) {
result.names = 'quoted'
nrOfNameFlags++;
}
if (result.hdbcds) {
result.names = 'hdbcds'
nrOfNameFlags++;
}
if (nrOfNameFlags > 1) {
displayUsage(`Only one of "plain", "quoted" or "hdbcds" can be specified for names with option "-O, --to-odata", but not combinations`, 2);
}
delete result.plain;
delete result.quoted;
delete result.hdbcds;
return result;
}
// Check the value of --to-swagger for legal values. Return the value as an object with sub-options.
function verifyToSwaggerOption(value) {
return verifyFlags(['json','csn'], value, '-S, --to-swagger');
}
// Check the value of --to-hana for legal values. Return the value as an object with sub-options.
function verifyToHanaOption(value) {
let result = verifyFlags([
'plain', 'quoted', 'hdbcds',
'assocs', 'joins',
'src', 'csn'], value, '-H, --to-hana');
let flagCount = 0;
if (result.plain) {
result.names = 'plain'
flagCount++;
}
if (result.quoted) {
result.names = 'quoted'
flagCount++;
}
if (result.hdbcds) {
result.names = 'hdbcds'
flagCount++;
}
if (flagCount > 1) {
displayUsage(`Only one of "plain" (default), "quoted" or "hdbcds" can be specified for names with option "-H, --to-hana", but not combinations`, 2);
}
delete result.plain;
delete result.quoted;
delete result.hdbcds;
if (result.assocs) {
result.associations = 'assocs'
}
if (result.joins) {
result.associations = 'joins'
}
if (result.assocs && result.joins) {
displayUsage(`Either "assocs" (default) or "joins" can be specified for associations with option "-H, --to-hana", but not both`, 2);
}
delete result.assocs;
delete result.joins;
return result;
}
// Check the value of --to-sql for legal values. Return the value as an object with sub-options.
function verifyToSqlOption(value) {
let result = verifyFlags([
'plain', 'quoted', 'hdbcds',
'assocs', 'joins',
'src', 'csn',
'hana', 'sqlite'], value, '-Q, --to-sql');
let flagCount = 0;
if (result.plain) {
result.names = 'plain'
flagCount++;
}
if (result.quoted) {
result.names = 'quoted'
flagCount++;
}
if (result.hdbcds) {
result.names = 'hdbcds'
flagCount++;
}
if (flagCount > 1) {
displayUsage(`Only one of "quoted" (default), "quoted" or "hdbcds" can be specified for names with option "-Q, --to-sql", but not combinations`, 2);
}
delete result.quoted;
delete result.quoted;
delete result.hdbcds;
flagCount = 0;
if (result.assocs) {
result.associations = 'assocs'
flagCount++;
}
if (result.joins) {
result.associations = 'joins'
flagCount++;
}
if (flagCount > 1) {
displayUsage(`Only one of "assocs" (default) or "joins" can be specified for associations with option "-Q, --to-sql", but not both`, 2);
}
delete result.assocs;
delete result.joins;
if (result.hana) {
result.dialect = 'hana';
}
if (result.sqlite) {
result.dialect = 'sqlite';
}
if (result.hana && result.sqlite) {
displayUsage(`Either "hana" or "sqlite" (default) can be specified for a dialect with option "-Q, --to-sql", but not both`, 2);
}
delete result.hana;
delete result.sqlite;
return result;
}
// Check the value of --to-rename for legal values. Return the value as an object with sub-options.
function verifyToRenameOption(value) {
let result = verifyFlags(['quoted','hdbcds'], value, '--to-rename');
if (result.quoted) {
result.names = 'quoted'
}
if (result.hdbcds) {
result.names = 'hdbcds'
}
if (result.quoted && result.hdbcds) {
displayUsage(`Either "quoted" or "hdbcds" (default) can be specified for names with option "--to-rename", but not both`, 2);
}
delete result.quoted;
delete result.hdbcds;
return result;
}
// Check the value of --to-csn for legal values. Return the value as an object with sub-options.
function verifyToCsnOption(value) {
verifyFlags(['client', 'gensrc'], value, '--to-csn', 'flavor');
return (value == 'gensrc') ? { gensrc: true } : undefined;
}
// Executes a command line that has been translated to 'options' (what to do) and 'args' (which files)
function executeCommandLine(options, args) {
// Executes a command line that has been translated to 'command' (what to do), 'options' (how) and 'args' (which files)
function executeCommandLine(command, options, args) {
const normalizeFilename = options.testMode && process.platform === 'win32';

@@ -465,56 +114,22 @@ const messageLevels = { Error: 0, Warning: 1, Info: 2, None: 3 };

var run;
if (options.toExtensions) {
// This option can disappear when each extension is tagged whether is has
// been applied - the compactor could then list the non-applied extensions
try {
console.log(JSON.stringify(compiler.generateExtensions(args[0], options), null, 2));
} catch (err) {
console.error(err.message);
process.exit(1);
}
// Add implementation functions corresponding to commands here
const commands = {
toCdl,
toCsn,
toHana,
toOdata,
toRename,
toSql,
toSwagger,
toTntSpecificOutput,
}
else {
run = compiler.compile( args, undefined, options );
}
// The backends (should be changed to async where necessary): ----------------
if (options.toExtensions) {
run = null; // already done (option to be deleted)
if (!commands[command]) {
throw new Error(`Missing implementation for command ${command}`);
}
else if (options.tntCsn) {
// FIXME: This is the only one that is not actually combinable with others
run = run.then( tntOutput );
} else {
// Backend options (in alphabetical order)
if (options.toCdl) {
run = run.then( toCdl );
}
if (options.toCsn) { // standard: output CSN
run = run.then( toCsn );
}
if (options.toHana) {
run = run.then( toHana );
}
if (options.toOdata) {
run = run.then( toOdata );
}
if (options.toRename) {
run = run.then( toRename );
}
if (options.toSql) {
run = run.then( toSql );
}
if (options.toSwagger) {
run = run.then( toSwagger );
}
}
if(run) {
run = run.then( displayMessages, displayErrors );
}
compiler.compile( args, undefined, options )
.then( commands[command] )
.then( displayMessages, displayErrors )
.catch( catchErrors );
if (run) {
run.catch( catchErrors );
}
// Execute the command line option '--to-cdl' and display the results.

@@ -619,3 +234,3 @@ // Return the original model (for chaining)

// Return the original model (for chaining)
function tntOutput( model ) {
function toTntSpecificOutput( model ) {
// TODO: use async file-system API

@@ -670,3 +285,3 @@ // Perform TNT-specific post-processing

if (messageLevels[ msg.severity ] <= options.warning)
console.error( compiler.messageString( msg, normalizeFilename, !options.showMessageId ) );
console.error( compiler.messageString( msg, normalizeFilename, !options.showMessageId, !options.testMode ) );
}

@@ -673,0 +288,0 @@ }

@@ -32,2 +32,3 @@ // Make internal properties of the augmented CSN visible

$layerNumber: n => n,
$extra: e => e,
_layerRepresentative: s => s.realname,

@@ -41,4 +42,9 @@ _layerExtends: layerExtends,

_navigation: artifactIdentifier,
_origTarget: artifactIdentifier,
_tableAlias: artifactIdentifier,
_projections: artifactIdentifier, // array
_redirected: artifactIdentifier, // array
_entities: artifactIdentifier, // array
_ancestors: artifactIdentifier, // array
_descendants: artifactDictionary, // dict of array
_$queryNode: n => (n.location && { location: locationString( n.location ) }),

@@ -73,2 +79,8 @@ _$next: artifactIdentifier,

return 'this';
if (node.kind === 'source')
return 'source:' + quoted( node.filename );
if (node.kind === '$magicVariables')
return '$magicVariables';
if (!node.name)
return JSON.stringify(node);
switch (node.kind) {

@@ -78,28 +90,18 @@ case undefined: // TODO: remove this `returns` property for actions

? artifactIdentifier( node._artifact )
: JSON.stringify(node);
: JSON.stringify(node.name);
case 'builtin':
return 'builtin("cds")';
return '$magicVariables/' + msg.artName(node);
case 'source':
return (node.name)
? 'source(' + quoted( node.filename ) + ',using:' + quoted( node.name.id ) + ')'
: 'source(' + quoted( node.filename ) + ')';
case 'using':
return 'using(' + quoted( node.name && node.name.id ) +
',source:' + quoted( node.location && node.location.filename ) + ')';
return 'source:' + quoted( node.location && node.location.filename ) +
'/using:' + quoted( node.name.id )
default: {
let names = [];
if (node.name) {
[ 'element', 'alias', 'query', 'param', 'action', 'absolute' ].forEach( function( prop ) {
if (prop in node.name)
names.push( (names.length || node.kind === 'annotate' ? prop + ':' : '') + quoted( node.name[prop] ) );
});
}
else
names = [ quoted(undefined) ];
if (!names.length) // TODO: should only be necessary temporarily
names = [JSON.stringify(node.name)];
// TODO: as long as we have no kind='mixin':
let kind = (node.kind === 'element' && !node.name.element) ? 'mixin' : node.kind;
return (kind || '<kind>') + outer + '(' + names.join(',') +
( node.name && node.name.$renamed ? ', orig:"' + node.name.$renamed + '")' : ')' );
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 ) : '');
}

@@ -106,0 +108,0 @@ }

@@ -1,3 +0,102 @@

# ChangeLog for cdx compiler and backends
# ChangeLog for cdx compiler and backends
## Version 1.8.0
Features
* Support the OData annotation vocabularies `PersonalData` and `Aggregation`.
The vocabulary for `PersonalData` contains a number of annotations that are flagged
as "experimental". Their usage will result in a warning.
* New option for specifying the locale in SQLite dialect. As part of the `toSql`
command is now available the options `'-l, --locale <locale>'` for specifying
value for the "$user.locale" variable.
Changes
* Entity definitions with elements of type `array` and structure type definitions with
association elements will now lead to an error message when generating edmx for OData v2.
These constructs are not allowed in OData v2, but there was no corresponding check in the
cds compiler yet.
## Version 1.7.1
Fixes
* Restore version function which was deleted by accident
## Version 1.7.0
Features
* Allow entities to have parameters. They can be referred to inside the query with
`:Param`. Entites with parameters are not allowed in `toSql` for dialect "sqlite".
When generating for HANA, parameters cannot be used in combination with associations:
an entity with parameters cannot have associations, and an association must not point
to an entity with parameters.
* The parameters and return value of actions and functions can now have structured types.
* In the annotation translation for OData, falsy values of the special variable `$value`
(that is used to provide nested annotations for scalar values) are correctly handled.
* When (new-style) csn is used as input, the compiler ignores unknown attributes.
* Implicit redirection and auto-exposure are now applied recursively, i.e. the associations
of an auto-exposed entity are considered for implicit redirection and auto-exposure,
if necessary.
Changes:
* With `--new-csn`, consider `redirected to` on projected associations and
adapt the `on` condition and the `keys` specification accordingly. There are
also Info messages if an element referred to in the `on` condition or `keys`
specification has not been projected to the new association target.
_The severity of these messages will be increased if implicit redirections
will have been performed by the core compiler._
* `toHana` and `toSql` now reject entities that only contain unmanaged associations.
Such entites would lead to a deployment error later.
* SQL name mapping modes `quoted` and `hdbcds` are only allowed when generating for HANA.
* In the csn, the csn language version is now stored in the top level attribute `$version`.
The version information via `version.csn` is deprecated and will be removed in a future
release. The information about the creator of the csn has been moved inside the new
top level attribute `meta`.
Fixes
* Provide code completion for references in complex select item expressions not
(yet) having an alias (complex = not consisting of just a reference).
* With `--new-csn`, avoid internal error while rewriting the `on` condition
from an element of a source entity which refers to a `mixin` definition with
an `on` condition containing a reference like `$projection.<elem>`.
* OData, edmx generation: correctly escape the characters `<`, `>`, `&`, and `"`.
* When an entity is auto-exposed, it's annotations are transferred to the generated
projection.
## Version 1.6.0
Features
* Provide code completion for `using` declarations.
* Support the OData annotation vocabulary "Validation".
* For compositions in EDM, add `<OnDelete Action="Cascade"/>` to the navigation
property where required.
Changes
* With `--new-csn`, complain more often about projected associations whose `on`
condition could not be rewritten correctly.
* Make `associations: 'joins'` the default for `toSql` (because the default for
`dialect` is already `sqlite`, which requires joins).
* Adapt the command line interface to use commands instead of the `--to...` generation
options (e.g. `cdsc toHana --src --names plain` instead of `cdsc --toHana src,plain`).
Please see the [Command Line Migration guide](doc/CommandLineMigration.md)
for details.
* Add a `generated by cds-compiler version x.y.z` comment to all generated SQL and `hdbcds`
sources.
* Replace the CSN validator (formerly `ajv`) with a new own implementation.
Fixes
* With `--new-csn`, do not change references to magic variables like `$user.id`
while rewriting the `on` conditition of a projected association.
* Apply OData specific checks (e.g. that all elements of an entity must have a type)
applied only to objects that are exposed in a service.
* When generating SQL for SQLite, replace the the special variables `$now`, `$user.id`
and `$user.locale` by `CURRENT_TIMESTAMP`, `'$user.id'`, and `'EN'`, respectively.
* Issue a warning for conflicting cardinality declarations (e.g. `association[1] to many ...`).
* Handle filters with cardinality correctly when translating associations to joins.
* Avoid crash when checking structured action parameters.
* Handle `$self` as the first of multiple path steps correctly in `toOdata`.
* In `toHana`, render the combination of enums and `type of` correctly.
* In mixins generated by `toHana`, handle special variables starting with `$` correctly.
## Version 1.5.0

@@ -878,3 +977,3 @@

Features
* New implementation of name resolution (according to [spec](https://github.wdf.sap.corp/CDS/cdsv/blob/master/doc/NameResolution.md))
* New implementation of name resolution (according to [spec](doc/NameResolution.md)
* Support for bound and unbound actions and functions

@@ -881,0 +980,0 @@ * More semantic checks

# Command Line Migration
With revision 1.0.24, the CDS compiler offers a new command line interface `cdsc`. The old `cdsv` command line is
deprecated, will not be extended with new features, and will be removed in a subsequent release.
With revision 1.5.1, the `cdsc` command line interface has been adapted to use commands with
options.
Please see `cdsc --help` for a description of the new command line options (snapshot of current version below).
Usage is now `cdsc <command> [options] <file...>` instead of `cdsc [options] <file...>`.
```
$ cdsc --help
The generation options (`--toHana`, `--toSql`, ...) have been replaced by commands
(`toHana`, `toSql`, ...). This allows for better per-command options, which can now be optional,
can use more single-letter abbreviations, and now match those from the `options` object in the API.
Usage: cdsc [options] <file> ...
Some examples:
Compile a CDS model given from the input files. Input files may be CDS source files (.cds), CSN
model files (.json) and XML files (.xml)
for pre-processed ODATA annotations.
| Old command line | New command line |
| -------------------------- | --------------------------------------------- |
| `cdsc --new-csn --toHana csn,plain foo.cds` | `cdsc --new-csn toHana --csn --names plain foo.cds` |
| `cdsc -R --H csn,plain foo.cds` | `cdsc -R H -c -n plain foo.cds` |
| `cdsc --toOdata xml,v2,separate foo.cds` | `cdsc toOdata --xml --version v2 --separate foo.cds` |
| `cdsc --toSql src foo.cds` | `cdsc toSql foo.cds` |
| `cdsc foo.cds` | `cdsc foo.cds` |
General Options
-h, --help Display this help text and exit
-w, --warning <level> Show warnings up to <level> (0: Error, 1: Warnings (default), 2: Info)
-v, --version Display version number and exit
List of commands (as of v1.5.1):
Generation options (default if none given: generate original model as CSN to <stdout>)
-o, --out <dir> Place generated files in directory <dir>, default is "-" for <stdout>
-H, --to-hana <flags> Generate HANA CDS source, <flags> can be a comma-separated combination
of either "plain" or "quoted" (default) for entity names, either "assocs"
(default) or "joins" for associations and any of "src,csn"
plain : produce uppercased flattened HANA entity names with
underscores
quoted : produce HANA entity names with dots and nested contexts
as in CDL
assocs : keep associations in HANA CDS as far as possible
joins : transform associations to HANA CDS joins
src : generate HANA CDS source files
csn : generate "hana_csn.json" with HANA-preprocessed model
-O, --to-odata <flags> Generate ODATA metadata and annotations, <flags> can be a comma-
separated combination of "xml,json,separate,combined,csn" and either
"v2" (default) or "v4" version
v2 : Generate ODATA V2 output
v4 : Generate ODATA V4 output
xml : generate XML output (separate or combined)
json : generate JSON output (not available for V2)
separate: generate "<svc>_metadata.xml" and "<svc>_annotations.xml"
combined: generate "<svc>.xml"
csn : generate "odata_csn.json" with ODATA-preprocessed model
-C, --to-cdl Generate CDS source
-S, --to-swagger Generate Swagger (OpenAPI) JSON
-Q, --to-sql <flags> Generate SQL DDL statements, <flags> can be a comma-separated
combination of either "plain" or "quoted" (default) for entity names,
either "assocs" (default) or "joins" for associations and any of
"src,csn"
plain : produce uppercased flattened table/view names with
underscores
quoted : produce quoted table/view names with dots
assocs : keep associations as far as possible (only usable for
HANA SQL)
joins : transform associations to SQL joins
src : generate SQL source files
csn : generate "sql_csn.json" with SQL-preprocessed model
-l, --lint-mode Generate nothing, just produce single-file error messages if any (for
use by editors)
--to-csn Generate original model as CSN to "csn.json"
```
Commands
H, toHana [options] <file...> Generate HANA CDS source files
O, toOdata [options] <file...> Generate ODATA metadata and annotations
C, toCdl <file...> Generate CDS source files
S, toSwagger [options] <file...> Generate Swagger (OpenAPI) JSON
Q, toSql [options] <file...> Generate SQL DDL statements
toCsn [options] <file...> (default) Generate original model as CSN
toTntSpecificOutput <file...> (internal) Generate TNT-specific post-processed CSN
toRename [options] <file...> (internal) Generate SQL DDL rename statements
```
Backward compatibility options (deprecated, do not use)
--check-model Perform extra checks on the model
--oldstyle-self Allow "self" alternatively to "$self" (implied by --tnt-flavor)
--tnt-flavor Compile with backward compatibility for the "TNT" project
--tnt-csn Generate TNT-specific post-processed CSN
Please see `cdsc --help` for the list of commands and general options, or `cdsc <command> --help`
for help regarding a specific command.
Diagnostic options
--trace-parser Trace parser
--trace-parser-amb Trace parser ambiguities
--trace-fs Trace file system access caused by "using from"
Internal options (for testing only, may be changed/removed at any time)
-R, --raw-output Write raw augmented CSN and error output to stdout
--beta-mode Enable unsupported, incomplete (beta) features
--hana-flavor Compile with backward compatibility for HANA CDS (incomplete)
--parse-only Stop compilation after parsing and write result to stdout
--to-extensions Generate augmented CSN for extensions from properties file
--test-mode Produce extra-stable output for automated tests (normalize filenames
in errors, sort properties in CSN, omit version in CSN)
--extra-augment Compile to plain CSN, augment and compile again, augmentation tests
--re-augmented Re-augmented CSN and error output
--omit-record-type Omit unnecessary type attribute for ODATA records
```
## Some helpful hints
Please note the following general concepts regarding the new command line
- All `--to-...` options are orthogonal, i.e. they may be freely combined to generate multiple kinds of output with one invocation.
- When no `--to-...` options at all are specified, the behavior is still the same as `cdsv` (generate CSN output to `stdout`).
- When no `--out` option is provided or if `-` is specified as output directory , all output will go to `<stdout>` instead of
being written to files - this is helpful for manual testing.
- The `--raw-output` option also affects all `--to-...` options where a CSN file is generated. Instead of `csn.json`, a `csn_raw.txt`
will be produced.
## Migration guide
The following table shows replacements for some common `cdsv` options
| Old command line | New command line |
| -------------------------- | --------------------------------------------- |
| `cdsv --odata-and-hana-output ...` | `cdsc --to-odata xml,json,separate,combined,csn --to-hana src ...` |
| `cdsv --odata-and-hana-output --odata-only ...` | `cdsc --to-odata xml,json,separate,combined,csn ...` |
| `cdsv --odata-and-hana-output --odata-only --odatav4 ...` | `cdsc --to-odata v4,xml,json,separate,combined,csn src ...` |
| `cdsv --odata-preprocess ...` | `cdsc --to-odata csn ...` |
| `cdsv --odata-preprocess --odatav4 ...` | `cdsc --to-odata csn,v4 ...` |
| `cdsv --to-hana ...` | `cdsc --to-hana src --out - ...` |
| `cdsv --hana-preprocess ...` | `cdsc --to-hana csn ...` |
| `cdsv --cdl-output ...` | `cdsc --to-cdl ...` |
| `cdsv --to-sql ...` | `cdsc --to-sql src ...` |
| `cdsv --tnt-output foo ...` | `cdsc --out foo --tnt-flavor --to-odata v2,xml,separate --tnt-csn ...` |
## Changes in behavior
The following changes have been made to `cdsc` (all affecting ODATA output).
- ODATA output is generated either for V2 or for V4, with identical filenames (i.e. the suffix `_v4` no longer appears in filenames).
- The ODATA files `metadata.xml` and `annotations.xml` are no longer produced (`cdsv` only produced them if there was exactly one
service in the model). Note that you can still produce `<service>_metadata.xml` and `<service>_annotations.xml` per service by
specifiying the flag `separate` for `--to-odata` .
- The former ODATA output file `csn.json` is now named `odata_csn.json`, to avoid the name clash with the output of `--to-csn`,
which is now `csn.json`.
- The content of ODATA output files for V2 is now equivalent to the new API results for V2 (`cdsv` produced the separate `annotations.xml`
file with `V4` even if `V2` was specified, resulting in slightly different output. The `combined.xml` file always had the correct versioning).
Please note the following general concepts regarding the new command line:
- General options can be placed anywhere, command specific options must appear after the command.
- In the unlikely case that a file name starts with `-`, please use `--` to indicate the end of options.
- The `src` argument of `toHana`, `toCdl`, `toSql` is now optional (and it would now be `--src`).
- If no command is specified, the default is `toCsn --flavor client` (as before).
- When no `--out` option is provided or if `-` is specified as output directory , all output will
go to `<stdout>` instead of being written to files (like before).
- The `--raw-output` option also affects all commands where a CSN file is generated.
Instead of `...csn.json`, a `...csn_raw.txt` will be produced (like before).

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

// FIXME: Adapt and unify all API docus.
const csnToSwagger = require('./render/toSwagger');

@@ -15,5 +13,4 @@ const { transformForHana } = require('./transform/forHana');

const { toSqlDdl } = require('./render/toSql');
const { toSqlDdlNew } = require('./render/toSqlNew');
const { toRenameDdl } = require('./render/toRename');
const { transform4odata, getServiceNames, compactForService } = require('./transform/forOdata');
const { transform4odata, getServiceNames } = require('./transform/forOdata');
const csn2edm = require('./edm/csn2edm');

@@ -23,22 +20,121 @@ const { mergeOptions } = require('./model/modelUtils');

const alerts = require('./base/alerts');
const { setProp } = require('./base/model');
var { CompilationError, sortMessages } = require('./base/messages');
const { createOptionProcessor } = require('./base/optionProcessor');
// This option processor is used both by the command line parser (to translate cmd line options
// into an options object) and by the API functions (to verify options)
let optionProcessor = createOptionProcessor();
// General options
// FIXME: Since they mainly affect the compiler, they could also live near main.compile
optionProcessor
.option('-h, --help')
.option('-v, --version')
.option('-w, --warning <level>', ['0', '1', '2'])
.option(' --show-message-id')
.option('-o, --out <dir>')
.option('-l, --lint-mode')
.option(' --fuzzy-csn-error')
.option(' --trace-parser')
.option(' --trace-parser-amb')
.option(' --trace-fs')
.option('-R, --raw-output')
.option(' --beta-mode')
.option(' --new-csn')
.option(' --hana-flavor')
.option(' --parse-only')
.option(' --test-mode')
.option(' --tnt-flavor')
.help(`
Usage: cdsc <command> [options] <file...>
Compile a CDS model given from input <file...>s and generate results according to <command>.
Input files may be CDS source files (.cds), CSN model files (.json) or pre-processed ODATA
annotation XML files (.xml). Output depends on <command>, see below. If no command is given,
"toCsn" is used by default.
Use "cdsc <command> --help" to get more detailed help for each command.
General options
-h, --help Show this help text
-v, --version Display version number and exit
-w, --warning <level> Show warnings up to <level>
0: Error
1: Warnings
2: (default) Info
--show-message-id Show message ID in error, warning and info messages
-o, --out <dir> Place generated files in directory <dir>, default is "-" for <stdout>
-l, --lint-mode Generate nothing, just produce single-file error messages if any (for
use by editors)
--fuzzy-csn-error Report free-style CSN properties as errors
-- Indicate the end of options (helpful if source names start with "-")
Diagnostic options
--trace-parser Trace parser
--trace-parser-amb Trace parser ambiguities
--trace-fs Trace file system access caused by "using from"
Internal options (for testing only, may be changed/removed at any time)
-R, --raw-output Write raw augmented CSN and error output to <stdout>
--beta-mode Enable unsupported, incomplete (beta) features
--new-csn Produce new-style CSN (preview of planned future CSN format)
--hana-flavor Compile with backward compatibility for HANA CDS (incomplete)
--parse-only Stop compilation after parsing and write result to <stdout>
--test-mode Produce extra-stable output for automated tests (normalize filenames
in errors, sort properties in CSN, omit version in CSN)
Backward compatibility options (deprecated, do not use)
--tnt-flavor Compile with backward compatibility for the "TNT" project
Commands
H, toHana [options] <file...> Generate HANA CDS source files
O, toOdata [options] <file...> Generate ODATA metadata and annotations
C, toCdl <file...> Generate CDS source files
S, toSwagger [options] <file...> Generate Swagger (OpenAPI) JSON
Q, toSql [options] <file...> Generate SQL DDL statements
toCsn [options] <file...> (default) Generate original model as CSN
toTntSpecificOutput <file...> (internal) Generate TNT-specific post-processed CSN
toRename [options] <file...> (internal) Generate SQL DDL rename statements
`);
// ----------- toHana -----------
optionProcessor.command('H, toHana')
.option('-h, --help')
.option('-n, --names <style>', ['plain', 'quoted', 'hdbcds'])
.option('-a, --associations <proc>', ['assocs', 'joins'])
.option('-s, --src')
.option('-c, --csn')
.help(`
Usage: cdsc toHana [options] <file...>
Generate HANA CDS source files, or CSN.
Options
-h, --help Show this help text
-n, --names <style> Naming style for generated entity and element names:
plain : (default) Produce HANA entity and element names in
uppercase and flattened with underscores. Do not generate
structured types.
quoted : Produce HANA entity and element names in original case as
in CDL. Keep nested contexts (resulting in entity names
with dots), but flatten element names with underscores.
Generate structured types, too.
hdbcds : Produce HANA entity end element names as HANA CDS would
generate them from the same CDS source (like "quoted", but
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
-s, --src (default) Generate HANA CDS source files "<artifact>.hdbcds"
-c, --csn Generate "hana_csn.json" with HANA-preprocessed model
`);
// Transform an augmented CSN 'model' into HANA-compatible CDS source.
// The following options control what is actually generated:
// The following options control what is actually generated (see help above):
// options : {
// toHana.names : either 'plain', 'quoted' (default) or 'hdbcds'
// 'plain': Produce HANA entity and element names in uppercase and
// flattened with underscores. Do not generate structured
// types.
// 'quoted': Produce HANA entity and element names in original case
// as in CDL. Keep nested contexts (resulting in entity names
// with dots), but flatten element names with underscores.
// Generate structured types, too.
// 'hdbcds: Produce HANA entity and element as HANA CDS would generate
// them from the same CDS source (like "quoted", but using
// element names with dots).
// toHana.associations : either 'assocs' (default, keep associations as they are if possible)
// or 'joins' (replace associations by joins)
// toHana.src : if true, generate HANA CDS source files (default)
// toHana.csn : if true, generate the transformed CSN model
// toHana.names
// toHana.associations
// toHana.src
// toHana.csn
// }

@@ -88,2 +184,5 @@ // Options provided here are merged with (and take precedence over) options from 'model'.

// 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'

@@ -95,4 +194,4 @@ // must leave namespaces, structs and associations alone.

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

@@ -116,2 +215,37 @@ // Assemble result

// ----------- toOdata -----------
optionProcessor.command('O, toOdata')
.option('-h, --help')
.option('-v, --version <version>', ['v2', 'v4'])
.option('-x, --xml')
.option('-j, --json')
.option(' --separate')
.option(' --combined')
.option('-c, --csn')
.option('-n, --names <style>', ['plain', 'quoted', 'hdbcds'])
.help(`
Usage: cdsc toOdata [options] <file...>
Generate ODATA metadata and annotations, or CSN.
Options
-h, --help Show this help text
-v, --version <version> ODATA version
v2: (default) ODATA V2
v4: ODATA V4
-x, --xml (default) Generate XML output (separate or combined)
-j, --json Generate JSON output as "<svc>.json" (not available for v2)
--separate Generate "<svc>_metadata.xml" and "<svc>_annotations.xml"
--combined (default) Generate "<svc>.xml"
-c, --csn Generate "odata_csn.json" with ODATA-preprocessed model
-n, --names <style> Annotate artifacts and elements with "@cds.persistence.name", which is
the corresponding database name (see "--names" for "toHana or "toSql")
plain : (default) Names in uppercase and flattened with underscores
quoted : Names in original case as in CDL. Entity names with dots,
but element names flattened with underscores
hdbcds : Names as HANA CDS would generate them from the same CDS
source (like "quoted", but using element names with dots)
`);
// Generate ODATA for augmented CSN `model` using `options`.

@@ -131,14 +265,11 @@ // Before anything is generated, the following transformations are applied to 'model':

// - Rename annotations according to a fixed list of short-hands
// The following options control what is actually generated:
// The following options control what is actually generated (see help above):
// options : {
// toOdata.version : either 'v2' or 'v4' (default)
// toOdata.xml : if true, generate XML output (default)
// toOdata.json : if true, generate JSON output (not available for ODATA V2)
// toOdata.separate : if true, generate XML 'metadata' and XML 'annotations' separately
// toOdata.combined : if true, generate XML metadata and XML annotations together as
// 'combined' (default)
// toOdata.csn : if true, generate the transformed CSN model
// toOdata.names : either 'plain', 'quoted' or 'hdbcds'. If set, artifacts and elements
// are annotated with "@cds.persistence.name" containing the
// corresponding database name (see "toHana.names" or "toSql.names")
// toOdata.version
// toOdata.xml
// toOdata.json
// toOdata.separate
// toOdata.combined
// toOdata.csn
// toOdata.names
// }

@@ -197,2 +328,5 @@ // Options provided here are merged with (and take precedence over) options from 'model'.

// Verify options
optionProcessor.verifyOptions(options, 'toOdata').map(complaint => signal(warning`${complaint}`));
// Perform extra-magic for TNT if requested

@@ -220,7 +354,9 @@ if (model.options.tntFlavor) {

if (options.toOdata.xml || options.toOdata.json) {
// Compact the model
let compactedModel = compactModel(forOdataAugmented);
setProp(compactedModel, 'messages', forOdataAugmented.messages);
for (let serviceName of getServiceNames(model))
{
let forOdata = compactForService(forOdataAugmented, serviceName);
// FIXME: Unify handling of version and tntFlavor (use original options)
let l_edm = csn2edm(forOdata, { version: options.toOdata.version, tntFlavor : options.tntFlavor });
let l_edm = csn2edm(compactedModel, serviceName, options);

@@ -255,2 +391,33 @@ result.services[serviceName] = {};

// Generate edmx for given 'service' based on 'csn' (new-style compact, already prepared for OData)
// using 'options'
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;
}
// Generate edm-json for given 'service' based on 'csn' (new-style compact, already prepared for OData)
// using 'options'
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();
return edmj;
}
// ----------- toCdl -----------
optionProcessor.command('C, toCdl')
.option('-h, --help')
.help(`
Usage: cdsc toCdl [options] <file...>
Generate CDS source files "<artifact>.cds".
Options
-h, --help Show this help text
`);
// Generate CDS source text for augmented CSN model 'model'.

@@ -272,7 +439,27 @@ // The following options control what is actually generated:

function toCdl(model, options) {
const { warning, signal } = alerts(model);
// Merge options with those from model
options = mergeOptions({ toCdl : true }, model.options, options);
// Verify options
optionProcessor.verifyOptions(options, 'toCdl').map(complaint => signal(warning`${complaint}`));
return toCdsSource(model, options);
}
// ----------- toSwagger -----------
optionProcessor.command('S, toSwagger')
.option('-h, --help')
.option('-j, --json')
.option('-c, --csn')
.help(`
Usage: cdsc toSwagger [options] <file...>
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
`);
// Generate OpenAPI JSON version 3 for the augmented CSN 'model'.

@@ -307,2 +494,3 @@ // The following options control what is actually generated:

function toSwagger(model, options) {
const { warning, signal } = alerts(model);
// Optional wrapper?

@@ -318,2 +506,4 @@ if (options && !options.toSwagger) {

}
// Verify options
optionProcessor.verifyOptions(options, 'toSwagger').map(complaint => signal(warning`${complaint}`));
// Actual implementation

@@ -323,21 +513,57 @@ return csnToSwagger(model, options);

// ----------- toSql -----------
optionProcessor.command('Q, toSql')
.option('-h, --help')
.option('-n, --names <style>', ['plain', 'quoted', 'hdbcds'])
.option('-a, --associations <proc>', ['assocs', 'joins'])
.option('-d, --dialect <dialect>', ['hana', 'sqlite'])
.option('-u, --user <user>')
.option('-l, --locale <locale>')
.option('-s, --src')
.option('-c, --csn')
.help(`
Usage: cdsc toSql [options] <file...>
Generate SQL DDL statements to create tables and views, or CSN
Options
-h, --help Show this help text
-n, --names <style> Naming style for generated entity and element names:
plain : (default) Produce SQL table and view names in uppercase
and flattened with underscores (no quotes required)
quoted : Produce SQL table and view names in original case as in
CDL (with dots), but flatten element names with
underscores (requires quotes). Can only be used in
combination with "hana" dialect.
hdbcds : Produce SQL table, view and column names as HANA CDS would
generate them from the same CDS source (like "quoted", but
using element names with dots). Can only be used in
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
-d, --dialect <dialect> SQL dialect to be generated:
hana : SQL with HANA specific language features
sqlite : (default) Common SQL for sqlite
-u, --user <user> Value for the "$user" variable in "sqlite" dialect
-l, --locale <locale> Value for the "$user.locale" variable in "sqlite" dialect
-s, --src (default) Generate SQL source files as "<artifact>.sql"
-c, --csn Generate "sql_csn.json" with SQL-preprocessed model
`);
// Generate SQL DDL statements for augmented CSN 'model'.
// The following options control what is actually generated:
// The following options control what is actually generated (see help above):
// options : {
// toSql.names : either 'plain' (default), 'quoted' or 'hdbcds'
// 'plain': Produce SQL table and view names in uppercase and
// flattened with underscores (no quotes required)
// 'quoted': Produce SQL table and view names in original case
// as in CDL (with dots), but flatten element names with
// underscores (requires quotes).
// 'hdbcds: Produce SQL table, view and column names as HANA CDS
// would generate them from the same CDS source (like "quoted",
// but using element names with dots).
// toSql.associations : either 'assocs' (default, keep associations as they are if possible)
// or 'joins' (replace associations by joins). 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.
// toSql.dialect : either 'hana' or 'sqlite' (default)
// toSql.src : if true, generate SQL DDL source files (default)
// toSql.csn : if true, generate the transformed CSN model
// toSql.names
// toSql.associations
// toSql.dialect
// toSql.user.id
// toSql.user.locale
// toSql.src
// toSql.csn
// }

@@ -360,2 +586,10 @@ // Options provided here are merged with (and take precedence over) options from 'model'.

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)) {
transforUserOption(model.options.toSql);
}
// Optional wrapper?

@@ -365,2 +599,9 @@ if (options && !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)){
transforUserOption(options.toSql);
}
// Provide defaults and merge options with those from model

@@ -376,3 +617,2 @@ options = mergeOptions({ toSql : getDefaultBackendOptions().toSql }, model.options, options);

// FIXME: Remove after a few releases
const { warning, signal } = alerts(model);
if (options.toSql.names == 'flat') {

@@ -387,2 +627,5 @@ signal(warning`Option "{ toSql.names: 'flat' }" is deprecated, use "{ toSql.names: 'plain' }" instead`);

// Verify options
optionProcessor.verifyOptions(options, 'toSql').map(complaint => signal(warning`${complaint}`));
// FIXME: Currently, '--to-sql' implies transformation for HANA (transferring the options to forHana)

@@ -397,4 +640,14 @@ let forHanaOptions = options.toSql;

// 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' }"`);
}
// CDXCORE-465, 'quoted' and 'hdbcds' are to be used in combination with dialect 'hana' only
if(options.toSql.dialect != 'hana' && ['quoted', 'hdbcds'].includes(options.toSql.names)) {
signal(error`Option "{ toSql.dialect: '${options.toSql.dialect}' }" cannot be combined with "{ toSql.names: '${options.toSql.names}' }"`);
}
// 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' or nothing
// the association processing must at least be 'mixin', even if callers specified 'assocs'
if (forHanaOptions.associations == 'assocs') {

@@ -412,3 +665,3 @@ forHanaOptions.associations = 'mixin';

if (options.toSql.src) {
result = options.oldSqlImpl ? toSqlDdl(forSqlAugmented) : toSqlDdlNew(forSqlAugmented);
result = toSqlDdl(forSqlAugmented);
}

@@ -425,4 +678,46 @@ if (options.toSql.csn) {

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 transforUserOption(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;
}
}
}
// ----------- toRename -----------
optionProcessor.command('toRename')
.option('-h, --help')
.option('-n, --names <style>', ['quoted', 'hdbcds'])
.help(`
Usage: cdsc toRename [options] <file...>
(internal, subject to change): Generate SQL DDL statements to "rename_<artifact>.sql" that
rename existing tables and their columns so that they match the result of "toHana" or "toSql"
with the "--names plain" option.
Options
-h, --help Display this help text
-n, --names <style> Assume existing tables were generated with "--names <style>":
quoted : Assume existing SQL tables and views were named in original
case as in CDL (with dots), but column names were flattened
with underscores (e.g. resulting from "toHana --names quoted")
hdbcds : (default) Assume existing SQL tables, views and columns were
generated by HANA CDS from the same CDS source (or resulting
from "toHana --names hdbcds")
`);
// FIXME: Not yet supported, only in beta mode

@@ -433,5 +728,5 @@ // Generate SQL DDL rename statements for a migration, renaming existing tables and their

// Expects the naming convention of the existing tables to be either 'quoted' or 'hdbcds' (default).
// The following options control what is actually generated:
// The following options control what is actually generated (see help above):
// options : {
// toRename.names : existing names, either 'quoted' or 'hdbcds' (default)
// toRename.names
// }

@@ -474,2 +769,5 @@ // Return a dictionary of top-level artifacts by their names, like this:

// Verify options
optionProcessor.verifyOptions(options, 'toRename').map(complaint => signal(warning`${complaint}`));
// Requires beta mode

@@ -501,2 +799,22 @@ if (!options.betaMode) {

// ----------- toCsn -----------
optionProcessor.command('toCsn')
.option('-h, --help')
.option('-f, --flavor <flavor>', ['client', 'gensrc'])
.help(`
Usage: cdsc toCsn [options] <file...>
Generate original model as CSN to "csn.json"
Options
-h, --help Show this help text
-f, --flavor <flavor> Generate CSN in one of two flavors:
client : (default) Standard CSN consumable by clients and backends
gensrc : CSN specifically for use as a source, e.g. for
combination with additional "extend" or "annotate"
statements, but not suitable for consumption by clients or
backends
`);
// Generate compact CSN for augmented CSN 'model'

@@ -516,3 +834,3 @@ // The following options control what is actually generated:

function toCsn(model, options) {
const { error, signal } = alerts(model);
const { error, warning, signal } = alerts(model);
// Can't have an optional wrapper here because 'testMode' and 'newCsn' are global options

@@ -523,2 +841,5 @@

// Verify options
optionProcessor.verifyOptions(options, 'toCsn').map(complaint => signal(warning`${complaint}`));
if (options.toCsn.gensrc && !options.newCsn) {

@@ -550,3 +871,3 @@ signal(error`CSN in "gensrc" flavor can only be generated as new-style CSN (option "newCsn" required)`);

names : 'plain',
associations: 'assocs',
associations: 'joins',
dialect: 'sqlite',

@@ -558,4 +879,7 @@ },

module.exports = {
optionProcessor,
toHana,
toOdata,
preparedCsnToEdmx,
preparedCsnToEdm,
toCdl,

@@ -562,0 +886,0 @@ toSwagger,

@@ -23,3 +23,3 @@ // Functions for dictionaries (Objects without prototype)

// Redefinitions from second source -> also complain in first source
if (messageCallback)
if (messageCallback && name)
messageCallback( name, found.name.location, found );

@@ -38,3 +38,3 @@ if (messageCallback !== null)

// TODO: with packages, we could also use the package hierarchy
if (messageCallback)
if (messageCallback && name)
messageCallback( name, found.name.location, found );

@@ -44,3 +44,3 @@ if (messageCallback !== null)

}
if (messageCallback)
if (messageCallback && name)
messageCallback( name, entry.name.location, entry );

@@ -47,0 +47,0 @@ if (messageCallback !== null)

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

'expected-const': 'A constant value is expected here',
'expected-struct': 'A structured type or a non-query entity is expected here',
'expected-struct': 'A structured type or a non-query entity without parameters is expected here',
'expected-context': 'A context or service is expected here',

@@ -86,3 +86,3 @@ 'expected-type': 'A type or an element of a type is expected here',

constructor(errs, model, text, ...args) {
super( text || 'CDS compilation failed\n' + errs.map( m => m.toString() ),
super( text || 'CDS compilation failed\n' + errs.map( m => m.toString() ).join('\n'),
...args );

@@ -95,3 +95,3 @@ this.errors = errs; // TODO: rename to messages

? this.message
: this.message + '\n' + this.errors.map( m => m.toString() );
: this.message + '\n' + this.errors.map( m => m.toString() ).join('\n');
}

@@ -103,5 +103,7 @@ }

class CompileMessage extends Error {
constructor(location, msg, severity = 'Error', id) {
constructor(location, msg, severity = 'Error', id, home) {
super(msg);
this.location = location;
if (home) // semantic location, e.g. 'entity:"E"/element:"x"'
this.home = home;
this.severity = severity;

@@ -144,3 +146,3 @@ if (id)

return function message( id, location, params = {}, severity = undefined, texts = undefined ) {
return function message( id, location, home, params = {}, severity = undefined, texts = undefined ) {
if (!severity) // TODO: check that they are always eq per messageId

@@ -154,3 +156,4 @@ severity = standardSeverities[id];

: messageText( texts || standardTexts[id], params );
let msg = new CompileMessage( location, text, s, id );
let msg = new CompileMessage( location, text, s, id,
(typeof home === 'string' ? home : homeName(home)) );
model.messages.push( msg );

@@ -162,9 +165,12 @@ return msg;

const paramsTransform = {
alias: msgName,
alias: quoted,
anno: transformAnno,
art: transformArg,
target: transformArg,
token: t => t.match( /^[a-zA-Z]+$/ ) ? t.toUpperCase() : "'" + t + "'",
code: n => '`' + n + '`',
name: msgName,
names: transformManyWith( msgName ),
id: msgName,
newcode: n => '`' + n + '`',
name: quoted,
names: transformManyWith( quoted ),
id: quoted,
file: s => "'" + s.replace( /'/g, "''" ) + "'", // sync ;

@@ -174,3 +180,3 @@ };

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

@@ -182,14 +188,17 @@ // anno = anno.slice(1);

function transformArg( arg, r, args, texts ) {
if (!arg || typeof arg !== 'object' || args['#'] || args.member )
return msgName( arg );
if (!arg || typeof arg !== 'object')
return quoted( arg );
if (args['#'] || args.member )
return artName( arg );
if (arg._artifact)
arg = arg._artifact;
if (arg.name)
arg = arg.name;
let prop = ['element','param','action','alias'].find( p => arg[p] );
let name = arg.name;
if (!name)
return quoted( name );
let prop = ['element','param','action','alias'].find( p => name[p] );
if (!prop || !texts[prop] )
return msgName( arg );
r['#'] = texts[ arg.$variant ] && arg.$variant || prop; // text variant (set by searchName)
r.member = msgName( arg[prop] );
return msgName( arg, prop );
return artName( arg );
r['#'] = texts[ name.$variant ] && name.$variant || prop; // text variant (set by searchName)
r.member = quoted( name[prop] );
return artName( arg, prop );
}

@@ -215,3 +224,2 @@

if (!variant) {
// TODO: probably use null instead undefinedArtifact in resolver.js
let type = art._finalType && art._finalType.kind !== 'undefined' ? art._finalType : art;

@@ -222,6 +230,5 @@ art = type.target && type.target._artifact || type;

let prop = nameProp[variant] || variant;
let name = Object.assign( {}, (art._artifact||art).name );
name[prop] = (name[prop]) ? name[prop] + '.' + id : id;
name.$variant = variant;
return name;
let name = Object.assign( { $variant: variant }, (art._artifact||art).name );
name[prop] = (name[prop]) ? name[prop] + '.' + id : id || '?';
return { name, kind: art.kind };
}

@@ -263,7 +270,8 @@

// Return message string with location if present
function messageString( err, normalizeFilename, noMessageId ) {
function messageString( err, normalizeFilename, noMessageId, noHome ) {
return (err.location ? locationString( err.location, normalizeFilename ) + ': ' : '') +
(err.severity||'Error') +
(err.messageId && !noMessageId ? ' ' + err.messageId + ': ' : ': ') +
err.message;
err.message +
(err.home && !noHome ? ' (in ' + err.home + ')' : '');
}

@@ -294,31 +302,46 @@

// Return string for complete reference
function refString( name, omit ) {
// prepare that resolvePath does not set ref.absolute etc:
if (name._artifact)
name = name._artifact;
if (name.name)
name = name.name;
let compact = '';
if (name.alias)
compact = '.$alias.' + name.alias;
function artName( art, omit ) {
let name = art.name;
let r = (name.absolute) ? [ quoted( name.absolute ) ] : [];
if (name.query || art.kind === 'block') // Yes, omit $query.0 - TODO: rename to block
r.push( (art.kind === 'block' ? 'block:' : 'query:') + name.query );
if (name.action && omit !== 'action')
compact = '.$action.' + name.action;
if (name.param && omit !== 'param')
compact += '.$param.' + name.param;
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.element && omit !== 'element')
compact += (compact ? '.' : '..') + name.element;
// Yes, omit $query.0 -> test is (name.query), not (name.query != null)
return name.absolute +
(name.query ? '.$query.' + name.query : '') + compact;
r.push( (art.kind === 'enum' ? 'enum:' : 'element:') + quoted( name.element ) );
return r.join('/');
}
// TODO: create error tag function which automatically calls msgName() on args,
// or better: named message parameters and having a name-dependent toString()
// function
function msgName( ref, omit ) {
let name = (typeof ref === 'string') ? ref : refString( ref, omit );
return '"' + name.replace( /"/g, '""' ) + '"'; // sync ";
function memberActionName( art ) {
while (art && art._main) {
if (art.kind === 'action' || art.kind === 'function')
return art.kind;
art = art._parent;
}
return 'action';
}
function homeName( art ) {
if (!art)
return art;
if (art._outer) // in returns / items property
return homeName( art._outer );
else if (art.kind === 'source' || !art.name) // error reported in parser or on source level
return null;
else if (art.kind === 'using')
return 'using:' + quoted( art.name.id );
else if (art.name._artifact) // block, extend, annotate
return homeName( art.name._artifact ); // use corresponding definition
else
return (art._main ? art._main.kind : art.kind) + ':' + artName( art );
}
function quoted( name ) {
return (name) ? '"' + name.replace( /"/g, '""' ) + '"' : '<?>'; // sync ";
}
module.exports = {

@@ -328,6 +351,5 @@ hasErrors,

messageString,
refString,
searchName,
getMessageFunction,
msgName,
artName,
handleMessages,

@@ -334,0 +356,0 @@ sortMessages: (m => m.sort(compareMessage)),

@@ -41,7 +41,7 @@ //

// Descend into nested members, too
forEachMember( member, callback );
forEachMemberRecursively( member, callback );
});
// If 'construct' has more than one query, descend into the elements of the remaining ones, too
if (construct.queries && construct.queries.length > 1) {
construct.queries.slice(1).forEach(query => forEachMemberRecursively(query, callback));
if (construct.$queries && construct.$queries.length > 1) {
construct.$queries.slice(1).forEach(query => forEachMemberRecursively(query, callback));
}

@@ -48,0 +48,0 @@ }

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

const { error, signal } = alerts(model);
for (let query of art.queries || []) {
for (let query of art.$queries || []) {
for (let groupByEntry of query.groupBy || []) {

@@ -36,0 +36,0 @@ if (groupByEntry._artifact && groupByEntry._artifact._finalType && groupByEntry._artifact._finalType.onCond) {

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

let target = elem.target;
// Not a managed assoc at all or not inferred => nothing to check
if (!target || elem.on || elem.onCond || elem.$inferred) {
// Not a managed assoc at all, inferred elem or redirected => nothing to check
if (!target || elem.on || elem.onCond || elem.$inferred || !elem.type || elem.type.$inferred) {
return;

@@ -18,0 +18,0 @@ }

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

}
// anonymous types not supported yet by OData processor
if (act.returns._finalType.elements && !act.returns._finalType.kind)
signal(warning`Anonymous types not supported`, location);
// check array return type

@@ -57,6 +54,2 @@ if (act.returns.items)

return;
// anonymous types not supported yet by OData processor
if (act.returns._finalType.items.elements && !act.returns._finalType.kind)
signal(warning`Anonymous types not supported`, location);
// array of array is not allowed

@@ -98,7 +91,2 @@ if (act.returns._finalType.items._finalType.items)

}
// check if user defined structured type is from the current service
let actReturnTypeServiceName = getAbsNameWithoutId(act.returns._finalType.name.absolute);
if (actReturnTypeServiceName !== serviceName)
signal(warning`The user defined return type of action '${act.name.id}' must be from the current service '${serviceName}'`, act.location);
}

@@ -110,10 +98,6 @@ }

let paramTypeArtifact = param.type._artifact;
let paramTypeArtifact = param.type && param.type._artifact || {};
// check if the entity type is from the current service
if (paramTypeArtifact.kind === 'entity')
checkEntityParam(paramTypeArtifact);
// check when the parameter is of user defined type
if (paramTypeArtifact.kind === 'type') {
checkUserDefinedTypeParam(param, paramTypeArtifact);
}

@@ -124,15 +108,2 @@ function checkEntityParam(paramTypeArtifact) {

}
function checkUserDefinedTypeParam(param, paramTypeArtifact) {
// if the type is resolved to a builtin
if (paramTypeArtifact._finalType.builtin)
return;
// user defined type resolved to builtin
if (paramTypeArtifact._finalType.type && paramTypeArtifact._finalType.type._artifact.builtin)
return;
if (getAbsNameWithoutId(paramTypeArtifact.name.absolute) !== serviceName)
signal(warning`The type of input parameter '${param.name.id}' must be from the current service`, location);
}
}

@@ -139,0 +110,0 @@ }

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

function checkEntity(entity) {
if (entity.source) { // projection
if (source(entity)) { // projection
checkProjection(entity, model);

@@ -145,6 +145,6 @@ }

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

@@ -154,7 +154,7 @@ }

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

@@ -164,3 +164,3 @@ }

// Check expressions in the various places where they may occur
for (let query of view.queries || []) {
for (let query of view.$queries || []) {
if (query.from) {

@@ -247,2 +247,8 @@ checkExpressionsInPaths(query.from);

// TODO: checks on one "source" are incomplete!
function source( view ) {
let from = view.query && view.query.from;
return from && from.length === 1 && from[0] && from[0].path && from[0];
}
module.exports = semanticCheck;

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

// - Standard object: object with `Object.prototype` as prototype - its
// property names are predefined (or at least their first: `@` in case of
// property names are predefined (or at least their first char: `@` for
// annotation assignments) and the value type depends on the property name.

@@ -47,3 +47,3 @@ // - Special object: currently just for the messages.

// - Optional sub properties are listed in `optional`, which can also be a
// - function returning true if property is allowed.
// function returning true if the property is allowed.
//

@@ -87,3 +87,3 @@ // The above mentioned restriction of the value space in certain contexts can

requires: ['messages','options','definitions','sources'],
optional: ['extensions','version','$magicVariables','$builtins','$internal'] // version without --test-mode
optional: ['extensions','version','$version','meta','$magicVariables','$builtins','$internal','_entities'] // version without --test-mode
},

@@ -99,2 +99,4 @@ ':parser': { // top-level from parser

'version', // TODO: do not set in parser
'$version',
'meta',
'@sql_mapping', // TODO: it is time that a 'header' attribute replaces 'version'

@@ -142,2 +144,4 @@ ]

version: { test: TODO }, // TODO: describe - better: 'header'
$version: { test: TODO, parser: true},
meta: { test: TODO },
namespace: {

@@ -180,3 +184,6 @@ test: (model.$frontend !== 'json') ? standard : TODO,

requires: ['op','location','args'],
optional: ['name','quantifier','orderBy','limit','offset','_leadingQuery']
optional: [
'name','quantifier','orderBy','limit','offset','_leadingQuery',
'name','kind','_parent','_main','_finalType','$navigation' // in FROM
]
},

@@ -265,4 +272,10 @@ select: { // sub query

'source','namespace','using',
'$tableAlias'
]
},
$syntax: {
parser: true,
kind: ['entity','view'],
test: isString // CSN parser should check for 'entity', 'view', 'projection'
},
value: {

@@ -282,3 +295,4 @@ kind: true,

requires: ['op','location'],
optional: ['args','func','quantifier','$inferred','augmented']
optional: ['args','func','quantifier','$inferred','augmented','_artifact']
// _artifact with "localized data"s 'coalesce'
},

@@ -354,3 +368,3 @@ query: { inherits: 'query' }

dbType: { kind: true, test: locationVal() },
source: { kind: true, test: TODO }, // TODO: remove in JSON/CDL parser
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

@@ -382,2 +396,3 @@ technicalConfig: { kind: ['entity'], test: TODO }, // TODO: some spec

_parent: { kind: true, test: TODO },
_service: { kind: true, test: TODO },
_main: { kind: true, test: TODO },

@@ -391,7 +406,9 @@ _artifact: { test: TODO },

queries: { kind: true, test: TODO }, // TODO: $queries with other structure
$queries: { kind: ['entity','view'], test: TODO },
_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
$from: { kind: true, test: TODO }, // all table refs necesary to compute elements
_origTarget: { kind: true, test: TODO }, // for REDIRECTED TO
_redirected: { kind: true, test: TODO }, // for REDIRECTED TO
_$next: { kind: true, test: TODO }, // next lexical search environment for values

@@ -406,2 +423,5 @@ _extend: { kind: true, test: TODO }, // for collecting extend/annotate on artifact

_projections: { kind: true, test: TODO }, // for mixin definitions
_entities: { test: TODO },
_ancestors: { kind: ['type','entity','view'], test: isArray( TODO ) },
_descendants: { kind: ['entity','view'], test: isDictionary( isArray( TODO ) ) },
$duplicate: { parser: true, kind: true, test: isBoolean },

@@ -414,2 +434,3 @@ $extension: { kind: true, test: TODO }, // TODO: introduce $applied instead or $status

redirected: { kind: true, test: TODO }, // TODO: do it with not-$inferred
$extra: { parser: true, test: TODO }, // for unexpectex properties in CSN
}

@@ -454,3 +475,4 @@ var _noSyntaxErrors = null;

}
(spec.test||standard)( node, parent, prop, spec );
(spec.test||standard)( node, parent, prop, spec,
typeof noPropertyTest === 'string' && noPropertyTest );
}

@@ -501,3 +523,3 @@

: optional( n, spec );
if (!(opt || requires.includes( n ))) {
if (!(opt || requires.includes( n ) || n === '$extra')) {
throw new Error( `Property '${n}' is not expected${at( [node[n], node, parent], prop, name )}` );

@@ -527,3 +549,3 @@ }

if (spec[op])
assertProp( node, parent, prop, spec[op], true );
assertProp( node, parent, prop, spec[op], op );
else {

@@ -530,0 +552,0 @@ throw new Error( `No specification for computed variant '${op}'${at( [node, parent], prop, idx )}` );

@@ -108,3 +108,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 );
reportCycle( dep.art, dep.location, w );
}

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

@@ -82,3 +82,3 @@ // Compiler phase "define": transform dictionary of AST-like CSNs into augmented CSN

const { msgName, getMessageFunction, searchName } = require('../base/messages');
const { getMessageFunction, searchName } = require('../base/messages');
const { queryOps, setProp, forEachGeneric, forEachInOrder, forEachMember }

@@ -88,3 +88,3 @@ = require('../base/model');

= require('../base/dictionaries');
const { dictKinds, kindProperties, fns, linkToOrigin, setMemberParent, storeExtension, combinedLocation } = require('./shared');
const { dictKinds, kindProperties, fns, setLink, linkToOrigin, setMemberParent, storeExtension, combinedLocation } = require('./shared');
const { compareLayer, layer } = require('./moduleLayers');

@@ -110,6 +110,6 @@ var initBuiltins = require('./builtins');

model.definitions = Object.create(null);
// model.annotations = Object.create(null);
setProp( model, '_entities', [] ); // for entities with includes
var extensionsDict = Object.create(null);
var lateExtensionsDict = Object.create(null);
var lateExtensionsDict = Object.create(null); // for generated artifacts
initBuiltins( model );

@@ -120,5 +120,8 @@ for (let name in model.sources) {

applyExtensions();
forEachGeneric( model, 'definitions', checkRedefinitions );
processLocalizedData();
forEachGeneric( model, 'definitions', processArtifact );
lateExtensions();
// Set _service link (sorted to set it on parent first). Could be set
// directly, but beware a namespace becoming a service later.
Object.keys( model.definitions ).sort().forEach( setAncestorsAndService );
forEachGeneric( model, 'definitions', postProcessArtifact );
return model;

@@ -128,5 +131,5 @@

forEachMember( obj, checkRedefinitions );
if (i == undefined)
if (i == null)
return;
message( 'duplicate-definition', obj.name.location,
message( 'duplicate-definition', obj.name.location, obj,
{ name, '#': (obj.kind === 'namespace') ? 'namespace' : dictKinds[prop] },

@@ -178,3 +181,3 @@ 'Error', {

if (builtin && !builtin.internal) {
message( 'ref-shadowed-builtin', src.namespace.location,
message( 'ref-shadowed-builtin', src.namespace.location, null, // no home artifact
{ id: last.id, art: src.namespace, code: `using ${builtin.name.absolute};` },

@@ -225,2 +228,3 @@ 'Warning', '$(ID) now refers to $(ART) - consider $(CODE)' );

addToDefinitions( context, absolute );
setProp( context, '_parent', parent || null );
}

@@ -248,9 +252,14 @@ setProp( item, '_artifact', context );

addToDict( context.artifacts, id.id, art );
setProp( art, '_parent', context );
}
else if (parent && art.name.path) {
addToDict( parent.artifacts, art.name.path[0].id, art );
setProp( art, '_parent', parent );
}
else if (!('_parent' in art)) {
setProp( art, '_parent', null );
}
if (absolute === 'cds') {
// TODO: move all 'cds' prefix checks into compiler
message( null, art.name.location,
message( null, art.name.location, parent,
`The namespace "cds" is reserved for CDS builtins` );

@@ -295,2 +304,3 @@ }

addToDict( context.artifacts, id, art );
setProp( art, '_parent', context );
}

@@ -323,4 +333,4 @@

if (art.kind === 'using') // repeated defs would be shown repeatedly otherwise
message( 'duplicate-using', loc, { name }, 'Error',
`Duplicate definition of top-level name ${msgName( name )}` );
message( 'duplicate-using', loc, null, { name }, 'Error',
'Duplicate definition of top-level name $(NAME)' );
} );

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

setProp( env.name, '_artifact', art );
setProp( env, '_parent', art );
art.blocks.push( env );

@@ -416,16 +427,11 @@ defineAnnotations( env, art, block ); // requires name.absolute of siblings!

defineAnnotations( art, art, block );
if (defProp && !options.newCsn) {
initMembers( art, art, block ); // old augmentor bug workaround
if (art.source) // from old-style CSN - TODO: error!?
return;
}
initDollarSelf( art ); // to allow extend projection with auto-mixin assoc, see #924
initParams( art );
art.$from = []; // for sequence of resolve steps
art.$queries = [];
art.queries = [];
setProp( art, '_leadingQuery', initQueryExpression( art, art.query ) );
setProp( art._leadingQuery, '_$next', art );
art.queries.forEach( initSubQuery ); // TODO: per art.query
// TODO: simplified for simple views = just one query with one table source
if (art.query.from && art.query.from.length === 1 && art.query.from[0] && art.query.from[0].path)
art.source = art.query.from[0];
// resolve parameters and actions:

@@ -440,3 +446,3 @@ initMembers( art, art, block ); // before setting art.elements!

if (art.dbType && !options.hanaFlavor)
message( null, art.dbType.location, `TABLE TYPE is not supported yet` );
message( null, art.dbType.location, art, `TABLE TYPE is not supported yet` );
defineAnnotations( art, art, block );

@@ -464,4 +470,2 @@ initMembers( art, art, block );

art.$tableAliases[selfname] = self;
if (options.oldstyleSelf)
art.$tableAliases.self = art.$tableAliases[selfname];
setProp( art, '_$next', model.$magicVariables );

@@ -494,3 +498,3 @@ }

initExprForQuery( query.on, query );
// TODO: MIXIN with name = ...subquery
// TODO: MIXIN with name = ...subquery (not yet supported anyway)
for (let elem of query.columns || []) {

@@ -507,6 +511,2 @@ if (elem && elem.value) {

initExprForQuery( query.having, query );
for (let sub of query.queries) {
initSubQuery( sub );
setProp( sub, '_$next', query ); // for name resolution
}
initMembers( query, query, query._block );

@@ -552,2 +552,6 @@ }

addQuery();
// TODO: use first tabalias name on the right side of the join (if much
// easier: last of left side) as 'param' in name of this "query" (do
// not use that for user msg, only for --raw-output)
initSubQuery( query );
parents = [...parents, query];

@@ -560,2 +564,3 @@ }

addQuery();
query._main.$queries.push( query ); // TODO: set number with it
if (parents.length)

@@ -584,6 +589,7 @@ addAlias( {}, query );

};
setProp( query.$tableAliases.$self, '_parent', query );
setProp( query.$tableAliases.$self, '_main', query._main );
setProp( query.$tableAliases.$self, '_finalType', query );
setProp( query.$tableAliases.$self, '_finalType', query );
}
initSubQuery( query ); // after from / mixin
}

@@ -604,3 +610,3 @@ else if (query.args) { // UNION, INTERSECT, ..., sub query

if (parents.length)
addAlias( {}, leading );
addAlias( query, leading );
}

@@ -611,3 +617,3 @@ // else: with parse error (`select from <EOF>`)

function signalDuplicate( name, loc ) {
message( 'duplicate-definition', loc, { name, '#': '$tableAlias' },
message( 'duplicate-definition', loc, query, { name, '#': '$tableAlias' },
'Error',

@@ -621,10 +627,13 @@ { '$tableAlias': 'Duplicate definition of table alias or mixin $(NAME)' } );

if (!query.name || !query.name.id) {
message( 'query-req-alias', query.location, {},
message( 'query-req-alias', query.location, query, {}, // TODO: not subquery.location ?
'Error', 'Table alias is required for this subquery' );
return;
}
let name = { id: query.name.id, location: query.name.location };
Object.assign( alias, { name, kind: '$tableAlias', location: query.location } );
if (alias !== query) {
alias.name = { id: query.name.id, location: query.name.location };
alias.location = query.location;
}
alias.kind = '$tableAlias';
let parent = parents[0];
setMemberParent( alias, name.id, parent );
setMemberParent( alias, alias.name.id, parent );
if (!parent._firstAliasInFrom)

@@ -659,3 +668,3 @@ setProp( parent, '_firstAliasInFrom', alias );

function addQuery() {
// TODO: set $next
setProp( query, '_$next', art );
setProp( query, '_block', art._block );

@@ -735,2 +744,3 @@ query.kind = 'query';

}
// if (!kindProperties[ elem.kind ]) console.log(elem.kind,elem.name)
if (kindProperties[ elem.kind ].isExtension) {

@@ -740,3 +750,4 @@ storeExtension( elem, name, prop, parent, block );

else if (isQueryExtension && elem.kind === 'element') {
message( 'extend-query', elem.location, { art: parent._main||parent },
message( 'extend-query', elem.location, construct, // TODO: searchName ?
{ art: parent._main||parent },
'Error', 'Query entity $(ART) can only be extended with actions' );

@@ -764,32 +775,27 @@ }

if (prop === 'actions') {
message( 'unexpected-actions', location, {}, 'Error',
message( 'unexpected-actions', location, {}, construct, 'Error',
'Actions and functions only exist top-level and for entities' );
}
else if (parent.kind === 'action' || parent.kind === 'function') {
message( 'extend-action', construct.location, {}, 'Error',
message( 'extend-action', construct.location, construct, {}, 'Error',
'Actions and functions cannot be extended, only annotated' );
}
else if (prop === 'params') {
if (!feature) {
if (!['entity','view'].includes(parent.kind) ||
!options.betaMode && !options.hanaFlavor && construct.kind !== 'annotate')
message( 'unexpected-params', location, {}, 'Error',
'Parameters only exist for actions or functions' );
else if (construct.kind === 'annotate')
return true;
}
if (!feature)
message( 'unexpected-params', location, construct, {}, 'Error',
'Parameters only exist for entities, actions or functions' );
else
message( 'extend-with-params', location, {}, 'Error', // remark: we could allow this
message( 'extend-with-params', location, construct, {}, 'Error', // remark: we could allow this
'Extending artifacts with parameters is not supported' );
}
else if (feature) { // allowed in principle, but not with extend
message( 'extend-type', location, {}, 'Error',
message( 'extend-type', location, construct, {}, 'Error',
'Only structures or enum types can be extended with elements/enums' );
}
else if (prop === 'elements') {
message( 'unexpected-elements', location, {}, 'Error',
message( 'unexpected-elements', location, construct, {}, 'Error',
'Elements only exist in entities, types or typed constructs' );
}
else { // if (prop === 'enum') {
message( 'unexpected-enum', location, {}, 'Error',
message( 'unexpected-enum', location, construct, {}, 'Error',
'Enum symbols can only be defined for types or typed constructs' );

@@ -800,2 +806,74 @@ }

// Set projection ancestors, and _service link for artifact with absolute name 'name':
// - not set: internal artifact
// - null: not within service
// - false: within abstract service
// - service: the artifact of the embedding service
// This function must be called ordered: parent first
function setAncestorsAndService( name ) {
let art = model.definitions[ name ];
if (!('_parent' in art))
return; // nothing to do for builtins and redefinitions
if (art.$from && !('_ancestors' in art)) {
setProjectionAncestors( art );
}
let parent = art._parent;
let service = (parent && parent.kind !== 'service') ? parent._service : parent;
setProp( art, '_service', service && !service.abstract && service );
if (service == null) // do not return on false (= in abstract service)
return;
// reconstruct service (in parent) as the value is false for abstract service
while (parent.kind !== 'service')
parent = parent._parent;
if (art.kind === 'service')
message( 'service-nested-service', art.name.location, art, { art: parent },
['Error'], 'A service cannot be nested within a service $(ART)' );
else if (art.kind === 'context')
message( 'service-nested-context', art.name.location, art, { art: parent },
['Error'], 'A context cannot be nested within a service $(ART)' );
}
function setProjectionAncestors( art ) {
// Must be run after processLocalizedData() as we could have a projection
// on a generated entity.
let chain = [];
while (art && !('_ancestors' in art) &&
art.$from && art.$from.length === 1 &&
art.query.op && art.query.op.val === 'query') {
chain.push( art );
setProp( art, '_ancestors', null ); // avoid infloop with cyclic from
let name = resolveUncheckedPath( art.$from[0], 'include', art );
// TODO: do not set _ancestors if params change
art = name && model.definitions[ name ];
}
let ancestors = art && (art._ancestors || []);
for (let a of chain.reverse()) {
ancestors = (ancestors ? [...ancestors, art] : []);
setProp( a, '_ancestors', ancestors );
art = a;
}
}
function postProcessArtifact( art ) {
if (!art._ancestors || art.kind === 'type')
return;
let service = art._service;
if (!service)
return;
let sname = service.name.absolute;
art._ancestors.forEach( expose );
return;
function expose( ancestor ) {
if (ancestor._service === service)
return;
let desc = ancestor._descendants ||
setLink( ancestor, Object.create(null), '_descendants' );
if (!desc[ sname ])
desc[ sname ] = [art];
else
desc[ sname ].push( art );
}
}
// Collect all artifact extensions

@@ -828,2 +906,3 @@ function collectArtifactExtensions( construct, block ) {

for (let ext of exts) {
delete ext.name.path[0]._artifact; // get message for root
resolvePath( ext.name, ext.kind, ext ); // should issue error/info

@@ -914,3 +993,3 @@ if (ext.kind === 'annotate')

if (!options.tntFlavor)
message( null, dictLocation( art.includes ),
message( null, dictLocation( art.includes ), art,
'Service includes are not supported yet' );

@@ -933,2 +1012,3 @@ for (let ref of art.includes) {

}
model._entities.push( art ); // add structure with includes in dep order
includeMembers( art, 'elements', forEachInOrder );

@@ -949,3 +1029,3 @@ includeMembers( art, 'actions', forEachGeneric );

if (noExtend && ext.kind === 'extend') {
message( 'extend-for-generated', ext.name.location, { art },
message( 'extend-for-generated', ext.name.location, ext, { art },
'Error', 'You cannot use EXTEND on the generated $(ART)' );

@@ -989,3 +1069,3 @@ continue;

for (let ext of extensions) {
let extLayer = layer( ext );
let extLayer = layer( ext ) || { realname: '', _layerExtends: Object.create(null) };
if (!open.length) {

@@ -997,7 +1077,7 @@ lastExt = ext;

if (lastExt) {
message( 'extend-repeated-intralayer', lastExt.location, {}, 'Warning',
message( 'extend-repeated-intralayer', lastExt.location, lastExt, {}, 'Warning',
'Unstable element order due to repeated extensions in same layer' );
lastExt = null;
}
message( 'extend-repeated-intralayer', ext.location, {}, 'Warning',
message( 'extend-repeated-intralayer', ext.location, ext, {}, 'Warning',
'Unstable element order due to repeated extensions in same layer' );

@@ -1008,3 +1088,3 @@ }

// report for lastExt if that is unrelated to other open exts or current ext
message( 'extend-unrelated-layer', lastExt.location, {}, 'Warning',
message( 'extend-unrelated-layer', lastExt.location, lastExt, {}, 'Warning',
'Unstable element order due to other extension in unrelated layer' );

@@ -1021,3 +1101,3 @@ }

for (let ext of extensions) {
message( 'extend-undefined', ext.name.location,
message( 'extend-undefined', ext.name.location, ext,
{ art: searchName( art, name, dictKinds[prop] ) },

@@ -1033,3 +1113,2 @@ 'Error', {

function includeMembers( art, prop, forEach ) {
// TODO: a projection cannot be used as include, right?
// TODO two kind of messages:

@@ -1040,5 +1119,9 @@ // Error 'More than one include defines element "A"' (at include ref)

clearDict( art, prop ); // TODO: do not set actions property if there are none
setProp( art, '_ancestors', [] ); // recursive array of includes
for (let ref of art.includes) {
let template = resolvePath( ref, 'include', art );
if (template) { // be robust
if (template._ancestors)
art._ancestors.push( ...template._ancestors );
art._ancestors.push( template );
forEach( template, prop, function( origin, name ) {

@@ -1065,21 +1148,27 @@ if (members && name in members)

function processLocalizedData() {
function processArtifact( art, name, prop, i ) {
checkRedefinitions( art, name, prop, i );
if (i != null)
return;
if (art.kind === 'entity' && art.elements && // check potential entity parse error
(!art.abstract || !art.abstract.val))
processLocalizedData( art );
}
function processLocalizedData( art ) {
if (!options.betaMode)
return;
for (let absolute in model.definitions) {
let art = model.definitions[absolute];
if (art instanceof Array || // redefininition
!art.elements || // potential entity parse error
art.kind !== 'entity' || art.query) // not non-query entity
continue;
let textsName = art.name.absolute + '_txts';
let localized = localizedData( art, textsName );
if (localized) {
createTextsEntity( art, textsName, localized );
addTextsAssociations( art, textsName, absolute );
}
let textsName = art.name.absolute + '_txts';
// If we re-introduce '::', search for '.' after '::'...
let dot = textsName.lastIndexOf('.') + 1;
let viewName = textsName.substring( 0, dot ) + 'localized_' + art.name.absolute.substring( dot );
let localized = localizedData( art, textsName, viewName );
if (localized) {
createTextsEntity( art, textsName, localized );
createLocalizedDataView( art, viewName, localized );
addTextsAssociations( art, textsName, localized );
}
}
function localizedData( art, textsName ) {
function localizedData( art, textsName, viewName ) {
let keys = 0;

@@ -1100,3 +1189,3 @@ let textElems = [];

}
else if (elem.localized && elem.localized.val)
else if (hasTruthyProp( elem, 'localized' ))
textElems.push( elem );

@@ -1108,22 +1197,36 @@ }

if (!keys) {
message( null, art.name.location, {}, 'Warning',
message( null, art.name.location, art, {}, 'Warning',
'No texts entity can be created when no key element exists' );
}
for (let elem of protectedElems) {
message( null, elem.name.location, { name: elem.name.id }, 'Warning',
message( null, elem.name.location, art, { name: elem.name.id }, 'Warning',
'No texts entity can be created when element $(NAME) exists' );
}
let textsEntity = model.definitions[ textsName ];
if (!textsEntity)
let viewEntity = model.definitions[ viewName ];
let names = [];
if (textsEntity) {
if (!(textsEntity instanceof Array))
message( null, textsEntity.name.location, textsEntity, { art }, 'Info',
'No texts entity for $(ART) can be created with this definition' );
names.push( textsName );
}
if (viewEntity) {
if (!(viewEntity instanceof Array))
message( null, viewEntity.name.location, viewEntity, { art }, 'Info',
'No texts entity for $(ART) can be created with this definition' );
names.push( viewName );
}
if (!names.length)
return !protectedElems.length && keys && textElems;
message( null, art.name.location, { art: textsName }, 'Warning',
'Name $(ART) for localized texts entity used by other definition' );
if (!(textsEntity instanceof Array)) {
message( null, textsEntity.name.location, { art }, 'Info',
'No texts entity for $(ART) can be created with this definition' );
}
message( null, art.name.location, art, { names }, 'Warning', {
std: 'Names $(NAMES) for localized data entities used by other definitions',
one: 'Name $(NAMES) for localized data entity used by other definition'
} );
return false;
}
// TODO: set _parent also for main artifacts!
function createTextsEntity( base, absolute, textElems ) {

@@ -1134,30 +1237,71 @@ let elements = Object.create(null);

kind: 'entity',
name: { absolute, location },
name: { path: splitIntoPath( location, absolute ), location },
location: base.location,
elements,
'@cds.autoexpose': { name: augmentGlobal( location, '@cds.autoexpose' ), location },
'@cds.autoexpose': { name: augmentPath( location, '@cds.autoexpose' ), location },
$inferred: 'localized'
}
setProp( art, '_block', model.$internal );
model.definitions[absolute] = art;
let locale = {
name: { location, id: 'locale' },
kind: 'element',
type: augmentGlobal( location, 'cds.String' ),
key: { val: true, location },
type: augmentPath( location, 'cds.String' ),
length: { literal: 'number', val: 5, location },
location
};
setMemberParent( locale, 'locale', art, 'elements' );
setProp( locale, '_block', model.$internal );
addToDictWithIndexNo( art, 'elements', 'locale', locale );
let artifacts = Object.create(null);
artifacts[ absolute ] = art;
initArtifacts( { artifacts }, null, model.$internal, false, '' );
for (let orig of textElems) {
let elem = linkToOrigin( orig, orig.name.id, art, 'elements' );
if (!orig.key || !orig.key.val) // use location of LOCALIZED keyword
elem.localized = { val: false, $inferred: 'localized', location: orig.localized.location };
if (orig.key && orig.key.val)
elem.key = { val: true, $inferred: 'localized', location };
else { // use location of LOCALIZED keyword
let localized = orig.localized || orig.type || orig.name;
elem.localized = { val: false, $inferred: 'localized', location: localized.location };
}
}
}
function addTextsAssociations( art, textsName ) {
function createLocalizedDataView( base, absolute, textElems ) {
let location = base.name.location;
let columns = [ { location, val: '*' } ];
let artifacts = Object.create(null);
let from = augmentPath( location, base.name.absolute );
from.name = { id: 'L', location };
artifacts[ absolute ] = {
kind: 'entity',
name: { location, path: splitIntoPath( location, absolute ) },
location: base.location,
query: { location, op: { val: 'query', location }, from: [ from ], columns },
$inferred: 'localized'
};
for (let orig of textElems) {
if (!orig.key || !orig.key.val) {
let location = orig.name.location;
let id = orig.name.id;
let value = { // TODO: special to allow different code in HANA?
op: { location, val: 'call' },
func: augmentPath( location, 'coalesce' ),
args: [
augmentPath( location, 'L', 'localized', id ),
augmentPath( location, 'L', id ) ],
location
};
setProp( value, '_artifact', orig );
let origin = {};
setProp( origin, '_artifact', orig );
// TODO: stay automatically silent if "shadowed" source element appears in expression
columns.push( { name: { id, location }, location: orig.location, value, origin, $replacement: 'silent' } );
}
}
// TODO: support for name.space::Base ?
initArtifacts( { artifacts }, null, model.$internal, false, '' );
}
function addTextsAssociations( art, textsName, textElems ) {
// texts : Composition of many Books_txts on texts.ID=ID;
let keys = textElems.filter( e => e.key && e.key.val );
let location = art.name.location;

@@ -1169,6 +1313,6 @@ let texts = {

$inferred: 'localized',
type: augmentGlobal( location, 'cds.Composition' ),
type: augmentPath( location, 'cds.Composition' ),
cardinality: { targetMax: { literal: 'string', val: '*', location }, location },
target: augmentGlobal( location, textsName ),
onCond: augmentEqual( location, ['texts.ID', 'ID'] )
target: augmentPath( location, textsName ),
onCond: augmentEqual( location, 'texts', keys )
}

@@ -1179,2 +1323,3 @@ setMemberParent( texts, 'texts', art, 'elements' );

// localized.ID=ID and localized.locale = $user.locale;
keys.push( ['localized.locale', '$user.locale'] );
let localized = {

@@ -1185,6 +1330,5 @@ name: { location, id: 'localized' },

$inferred: 'localized',
type: augmentGlobal( location, 'cds.Association' ),
target: augmentGlobal( location, textsName ),
onCond: augmentEqual( location,
['localized.ID', 'ID'], ['localized.locale', '$user.locale'] )
type: augmentPath( location, 'cds.Association' ),
target: augmentPath( location, textsName ),
onCond: augmentEqual( location, 'localized', keys )
}

@@ -1194,2 +1338,27 @@ setMemberParent( localized, 'localized', art, 'elements' );

}
function hasTruthyProp( art, prop ) {
// Returns whether art directly or indirectly has the property 'prop',
// following the 'origin' and the 'type' (not involving elements).
//
// TODO: we should issue a warning if we get localized via TYPE OF
let processed = Object.create(null); // avoid infloops with circular refs
let name = art.name.absolute; // is ok, since no recursive type possible
while (art && !processed[name]) {
if (art[prop])
return art[prop].val;
processed[name] = art;
if (art.origin && art.origin._artifact) {
art = art.origin._artifact;
name = art && art.name.absolute;
}
else if (art.type && art._block) { // TODO: not TYPE OF
name = resolveUncheckedPath( art.type, 'type', art );
art = name && model.definitions[ name ];
}
else
return false;
}
return false;
}
}

@@ -1203,7 +1372,15 @@

function augmentGlobal( location, id ) {
return { path: [{ id, location }], location };
function splitIntoPath( location, name ) {
// TODO: is currently needed to add the artifact into parent
// TODO: make it also work with path = [{id:absolute}]
let colons = name.indexOf('::');
let items = (colons < 0) ? name.split('.') : name.substring(colons).split('.');
return items.map( id => ({ id, location }) );
}
function augmentEqual( location, ...relations ) {
function augmentPath( location, ...args ) {
return { path: args.map( id => ({ id, location }) ), location };
}
function augmentEqual( location, prefix, relations ) {
let args = relations.map( eq );

@@ -1214,4 +1391,15 @@ return (args.length === 1)

function eq( args ) {
return { op: { val: '=', location }, args: args.map( ref ), location };
function eq( refs ) {
if (refs instanceof Array)
return { op: { val: '=', location }, args: refs.map( ref ), location };
else {
let id = refs.name.id;
return {
op: { val: '=', location },
args: [
{ path: [ { id: prefix, location }, { id, location } ], location },
{ path: [ { id, location } ], location } ],
location
};
}
}

@@ -1218,0 +1406,0 @@ function ref( path ) {

@@ -15,2 +15,4 @@ //

'@cds.persistence.table': never,
'@Analytics.hidden': never,
'@Analytics.visible': never,
'@': withKind, // always except in 'returns' and 'items'

@@ -20,3 +22,3 @@ default: withKind, // always except in 'returns' and 'items'

notNull, // a variant of notViaType()
targetElement: onlyViaParent,
targetElement: onlyViaParent, // in foreign keys
value: onlyViaParent, // enum symbol value

@@ -152,3 +154,3 @@ // masked: special = done in definer

function expensive( prop, target, source ) {
// console.log(prop,refString(source),'->',target.kind,refString(target),refString(type));
// console.log(prop,refString(source),'->',target.kind,refString(target));
if (prop !== 'foreignKeys' && availableAtType( prop, target, source ))

@@ -158,4 +160,7 @@ // foreignKeys must always be copied with target to avoid any confusion

return;
if (prop === 'params' && target.$inferred !== 'proxy' && target.$inferred !== 'include')
return;
let location = target.type && !target.type.$inferred && target.type.location
|| target.location;
|| target.location
|| target._outer && target._outer.location;
let dict = source[prop];

@@ -176,3 +181,3 @@ for (let name in dict) {

function onlyViaParent( prop, target, source ) {
if (target.$inferred === 'proxy')
if (target.$inferred === 'proxy') // assocs and enums do not have 'include'
always( prop, target, source );

@@ -179,0 +184,0 @@ }

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

service: { artifacts: true, normalized: 'namespace' }, // actions: true with "service-bound" actions
entity: { elements: true, actions: true }, // beta-mode - params: () => false
view: { elements: true, actions: true }, // beta-mode - params: () => false
entity: { elements: true, actions: true, params: () => false },
view: { elements: true, actions: true, params: () => false },
query: { elements: true },

@@ -75,5 +75,8 @@ $tableAlias: { normalized: 'alias', $navigation: true }, // table alias in select

expr: { next: '_$next', escape: 'param', noDep: true },
rewrite: { next: '_$next', escape: 'param', noDep: true, rewrite: true }, // TODO: assertion that there is no next/escape used
'order-by-union': { next: '_$next', escape: 'param', noDep: true, noExt: true },
// expr TODO: better - on condition for assoc, other on
// expr TODO: write dependency, but care for $self
param: { reject: rejectNonConst },
global: { useDefinitions: true, global: true }, // for using declaration
}

@@ -92,3 +95,3 @@

function rejectNonStruct( art ) {
return (['type', 'entity'].includes( art.kind ) && art.elements && !art.query)
return (['type', 'entity'].includes( art.kind ) && art.elements && !art.query && !art.params)
? undefined

@@ -141,3 +144,5 @@ : 'expected-struct';

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

@@ -175,3 +180,3 @@ art = ref.path[0]._artifact[0]; // array stored in head's _artifact

let variant = (env.$frontend && env.$frontend !== 'cdl') ? 'std' : 'cdl';
message( 'ref-unexpected-scope', head.location, { name: head.id, '#': variant },
message( 'ref-unexpected-scope', head.location, user, { name: head.id, '#': variant },
'Error', {

@@ -201,10 +206,11 @@ std: 'Unexpected parameter scope for name $(NAME)',

// queries: first tabaliases, then $magic - value refs: first $self, then $magic
// TODO: set extDict to query.$combined if extDict is not set
if (!extDict && !spec.noExt)
extDict = query && query.$combined ||
environment( user._main ? user._parent : user );
}
// if (!head) console.error(ref)
// 'global' for CSN later in value paths, CDL for Association/Composition:
let art = (ref.scope === 'global')
? getPathRoot( path, spec, {}, model.definitions )
: getPathRoot( path, spec, env, extDict, msgArt || 0 );
let art = (ref.scope === 'global' || spec.global)
? getPathRoot( path, spec, user, {}, model.definitions )
: getPathRoot( path, spec, user, env, extDict, msgArt || 0 );
if (!art)

@@ -235,3 +241,3 @@ return setLink( ref, art );

}
else { // FROM subquery, $projection
else { // FROM subquery, $projection, $self
setLink( head, art._finalType ); // the query (sub or self)

@@ -241,3 +247,3 @@ }

art = getPathItem( path, spec );
art = getPathItem( path, spec, user );
if (!art)

@@ -256,3 +262,3 @@ return setLink( ref, art );

if (msg) {
signalNotFound( msg, ref.location );
signalNotFound( msg, ref.location, user );
return setLink( ref, false );

@@ -282,4 +288,6 @@ }

// non-existing external using references if `unchecked` is truthy.
function getPathRoot( path, spec, env, extDict, msgArt ) {
function getPathRoot( path, spec, user, env, extDict, msgArt ) {
let head = path[0];
if (!head || !head.id)
return undefined; // parse error
if ('_artifact' in head)

@@ -309,4 +317,9 @@ return (head._artifact instanceof Array) ? false : head._artifact;

else if (r.kind === '$parameters') {
if (!head.quoted && path.length > 1)
if (!head.quoted && 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)' );
// TODO: replace it in to-csn correspondingly
return setLink( head, r );
}
}

@@ -327,3 +340,3 @@ else if (r.kind !== '$tableAlias' ||

if (names.length)
message( 'ref-ambiguous', head.location, { id: head.id, names },
message( 'ref-ambiguous', head.location, user, { id: head.id, names },
'Error', 'Ambiguous $(ID), replace by $(NAMES)' );

@@ -349,6 +362,15 @@ }

// navigation elements (for which you should use a table alias)
for (let name in extDict) {
if (!(extDict[name] instanceof Array && extDict[name][0].kind === '$navElement'))
e[name] = extDict[name];
if (extDict !== model.definitions) {
for (let name in extDict) {
let def = extDict[name];
if (!(def instanceof Array && def[0].kind === '$navElement'))
e[name] = def;
}
}
else {
for (let name in extDict) {
if (!name.includes('.'))
e[name] = extDict[name];
}
}
valid.push( e );

@@ -361,14 +383,14 @@ }

if (msgArt)
signalNotFound( 'ref-undefined-element', head.location, valid,
signalNotFound( 'ref-undefined-element', head.location, user, valid,
{ art: searchName( msgArt, head.id, 'element' ) } );
else
signalNotFound( 'ref-undefined-var', head.location, valid, { id: head.id },
signalNotFound( 'ref-undefined-var', head.location, user, valid, { id: head.id },
'Error', 'Element or variable $(ID) has not been found' );
}
else if (env.$frontend && env.$frontend !== 'cdl')
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, null,
signalNotFound( spec.undefinedDef || 'ref-undefined-def', head.location, user, valid,
{ art: head.id } );
else
signalNotFound( spec.undefinedArt || 'ref-undefined-art', head.location, valid,
signalNotFound( spec.undefinedArt || 'ref-undefined-art', head.location, user, valid,
{ name: head.id } );

@@ -382,6 +404,6 @@ return setLink( head, null );

// element item in the path)
function getPathItem( path, spec ) {
function getPathItem( path, spec, user ) {
var art;
for (let item of path) {
if (!item) // incomplete AST due to parse error
if (!item || !item.id) // incomplete AST due to parse error
return undefined;

@@ -408,9 +430,9 @@ if (item._artifact) { // should be there on first path element

// TODO: better for TYPE OF, FROM e.Assoc (even disallow for other refs)
signalNotFound( spec.undefinedDef || 'ref-undefined-def', item.location, [env],
{ art: searchName( art, item.id ) } );
signalNotFound( spec.undefinedDef || 'ref-undefined-def', item.location, user,
[env], { art: searchName( art, item.id ) } );
}
else if (art.name.query != null) {
// TODO: probably not extra messageId, but text variant
signalNotFound( 'query-undefined-element', item.location, [env],
{ id: item.id }, 'Error',
signalNotFound( 'query-undefined-element', item.location, user,
[env], { id: item.id }, 'Error',
'Element $(ID) has not been found in the elements of the query' );

@@ -421,9 +443,9 @@ // TODO: 'The current query has no element $(MEMBER)' with name.self

else if (art.kind === '$parameters') {
signalNotFound( 'ref-undefined-param', item.location, [env],
{ art: searchName( art._main, item.id, 'param' ) },
signalNotFound( 'ref-undefined-param', item.location, user,
[env], { art: searchName( art._main, item.id, 'param' ) },
'Error', { param: 'Entity $(ART) has no parameter $(MEMBER)' } );
}
else {
signalNotFound( 'ref-undefined-element', item.location, [env],
{ art: searchName( art, item.id ) } );
signalNotFound( 'ref-undefined-element', item.location, user,
[env], { art: searchName( art, item.id ) } );
}

@@ -434,7 +456,8 @@ return null;

function signalNotFound( msgId, location, valid, ...args ) {
function signalNotFound( msgId, location, home, valid, ...args ) {
// if (!location) console.log(msgId, valid, ...args)
if (location.$notFound)
return;
location.$notFound = true;
let err = message( msgId, location, ...args );
let err = message( msgId, location, home, ...args );
// console.log( Object.keys( Object.assign( Object.create(null), ...valid.reverse() ) ) )

@@ -445,3 +468,3 @@ if (valid && (options.attachValidNames || options.testMode))

let names = Object.keys( err.validNames );
message( null, location,
message( null, location, null,
names.length ? 'Valid: ' + names.sort().join(', ') : 'No valid names',

@@ -497,3 +520,3 @@ 'Info' );

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

@@ -572,2 +595,3 @@ prop = prop + '#' + item.name.variant.id; // TODO: check for double variants

setProp( elem.origin, '_artifact', origin );
// TODO: make this just elem._origin, remove elem.origin
return elem;

@@ -631,2 +655,3 @@ }

fns,
setLink,
linkToOrigin, setMemberParent,

@@ -633,0 +658,0 @@ storeExtension,

@@ -699,2 +699,91 @@ {

"AppliesTo": "Annotation"
},
"Validation.Pattern": {
"Type": "Edm.String",
"AppliesTo": "Property Parameter Term"
},
"Validation.Minimum": {
"Type": "Edm.Decimal",
"Scale": "variable",
"AppliesTo": "Property Parameter Term"
},
"Validation.Maximum": {
"Type": "Edm.Decimal",
"Scale": "variable",
"AppliesTo": "Property Parameter Term"
},
"Validation.Exclusive": {
"Type": "Core.Tag",
"AppliesTo": "Annotation"
},
"Validation.AllowedValues": {
"Type": "Collection(Validation.AllowedValue)",
"AppliesTo": "Property Parameter TypeDefinition"
},
"Validation.MultipleOf": {
"Type": "Edm.Decimal",
"Scale": "variable",
"AppliesTo": "Property Parameter Term"
},
"Validation.Constraint": {
"Type": "Validation.ConstraintType",
"AppliesTo": "Property EntityType ComplexType"
},
"Validation.ItemsOf": {
"Type": "Collection(Validation.ItemsOfType)",
"AppliesTo": "EntityType ComplexType"
},
"Validation.OpenPropertyTypeConstraint": {
"Type": "Collection(Core.QualifiedTypeName)",
"AppliesTo": "ComplexType EntityType"
},
"Validation.DerivedTypeConstraint": {
"Type": "Collection(Core.QualifiedTypeName)",
"AppliesTo": "EntitySet Singleton NavigationProperty Property TypeDefinition Parameter ReturnType"
},
"Validation.AllowedTerms": {
"Type": "Collection(Core.QualifiedTermName)",
"AppliesTo": "Term Property"
},
"Validation.MaxItems": {
"Type": "Edm.Int64",
"AppliesTo": "Collection"
},
"Validation.MinItems": {
"Type": "Edm.Int64",
"AppliesTo": "Collection"
},
"PersonalData.EntitySemantics": {
"Type": "PersonalData.EntitySemanticsType",
"AppliesTo": "EntitySet",
"$experimental": true
},
"PersonalData.DataSubjectRole": {
"Type": "Edm.String",
"AppliesTo": "EntitySet",
"$experimental": true
},
"PersonalData.DataSubjectRoleDescription": {
"Type": "Edm.String",
"AppliesTo": "EntitySet",
"$experimental": true
},
"PersonalData.FieldSemantics": {
"Type": "PersonalData.FieldSemanticsType",
"AppliesTo": "Property",
"$experimental": true
},
"PersonalData.IsPotentiallyPersonal": {
"Type": "Core.Tag",
"AppliesTo": "Property",
"$experimental": true
},
"PersonalData.IsPotentiallySensitive": {
"Type": "Core.Tag",
"AppliesTo": "Property"
},
"PersonalData.IsUserID": {
"Type": "Core.Tag",
"AppliesTo": "Property",
"$experimental": true
}

@@ -1932,4 +2021,32 @@ },

]
},
"Validation.AllowedValue": {
"$kind": "ComplexType",
"Properties": {
"Value": "Edm.PrimitiveType"
}
},
"Validation.ConstraintType": {
"$kind": "ComplexType",
"Properties": {
"FailureMessage": "Edm.String",
"Condition": "Edm.Boolean"
}
},
"Validation.ItemsOfType": {
"$kind": "ComplexType",
"Properties": {
"path": "Edm.NavigationPropertyPath",
"target": "Edm.NavigationPropertyPath"
}
},
"PersonalData.EntitySemanticsType": {
"$kind": "TypeDefinition",
"UnderlyingType": "Edm.String"
},
"PersonalData.FieldSemanticsType": {
"$kind": "TypeDefinition",
"UnderlyingType": "Edm.String"
}
}
}

@@ -144,4 +144,4 @@ const parseXml = require('./xmlParserWithLocations');

return {
literal: 'decimal',
val: Number(val)
literal: 'number',
val: Number.isSafeInteger(Number.parseFloat(val)) ? Number(val) : val
}

@@ -151,4 +151,4 @@ },

return {
literal: 'decimal',
val: Number(val)
literal: 'number',
val: Number.isSafeInteger(Number.parseFloat(val)) ? Number(val) : val
}

@@ -158,8 +158,14 @@ },

return {
literal: 'integer',
val: Number(val)
literal: 'number',
val: Number.isSafeInteger(Number.parseInt(val)) ? Number(val) : val
}
},
EnumMember: obj => handleEnum(obj),
Path: obj => handlePath(obj, false),
Binary: val => edmxAttrs.String(val),
Date: val => edmxAttrs.String(val),
DateTimeOffset: val => edmxAttrs.String(val),
Guid: val => edmxAttrs.String(val),
TimeOfDay: val => edmxAttrs.String(val),
Duration: val => edmxAttrs.String(val),
EnumMember: (obj, location) => handleEnum(obj, location),
Path: (obj, location) => handlePath(obj, false, location),
NavigationPropertyPath: obj => handlePath(obj, false),

@@ -179,4 +185,2 @@ PropertyPath: obj => handlePath(obj, false)

return handlePropValue(obj.PropertyValue, isCollection);
if (obj.String)
return handleString(obj.String, isCollection);
else

@@ -247,7 +251,7 @@ return handleValue(obj, isCollection);

function handlePath(path, isCollection) {
function handlePath(path, isCollection, location = obj._location) {
if (isCollection)
return path.map(e => handlePath(e, false))
return path.map(e => handlePath(e, false, location))
return {
path: (path._text || path).split('/').map(p => { return { id: p, location: obj._location } }),
path: (path._text || path).split('/').map(p => { return { id: p, location: location } }),
location: obj._location

@@ -265,8 +269,2 @@ };

function handleString(obj, isCollection) {
if (isCollection)
return obj.map(e => handleString(e, false));
return Object.assign(edmxAttrs.String(obj._text), { location: obj._location });
}
function handleValue(obj, isCollection) {

@@ -278,2 +276,10 @@ // this prevents the map execution of not an array, valid only for the collection tag

// handle the case of builtin as tag and collection of builtins
let builtinTag = Object.keys(edmxAttrs).find(atr => Object.keys(obj).includes(atr) || atr === obj._name);
if (builtinTag) {
if (isCollection)
return obj[builtinTag].map(e => handleValue(e, false));
return Object.assign(edmxAttrs[builtinTag](obj._text), { location: obj._location });
}
if (isCollection)

@@ -288,3 +294,3 @@ return obj.map(e => handleValue(e, false));

if (bltnAttr)
return Object.assign(edmxAttrs[bltnAttr](obj._attributes[bltnAttr]), { location: obj._location });
return Object.assign(edmxAttrs[bltnAttr](obj._attributes[bltnAttr], obj._location), { location: obj._location });
return {};

@@ -291,0 +297,0 @@ }

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

const knownVocabularies = ['Aggregation', 'Analytics', 'Core', 'Common', 'UI', 'Communication', 'Capabilities', 'Measures', 'Validation', 'PersonalData'];
/**************************************************************************************************

@@ -33,2 +35,3 @@ * csn2annotationEdm

}
let g_experimental_terms = {}; // take note of all experimental annos that have been used

@@ -67,5 +70,5 @@ let v = options.v;

let schema = Edm.Schema.create(v, serviceName, serviceName, g_annosArray, false);
let service = Edm.DataServices.create(v, schema);
let edm = Edm.create(v, service);
let schema = new Edm.Schema(v, serviceName, serviceName, g_annosArray, false);
let service = new Edm.DataServices(v, schema);
let edm = new Edm(v, service);
return edm;

@@ -270,3 +273,3 @@

// result objects that holds all the annotation objects to be created
let newAnnosStd = Edm.Annotations.create(v, stdName); // used in closure
let newAnnosStd = new Edm.Annotations(v, stdName); // used in closure
g_annosArray.push(newAnnosStd);

@@ -282,3 +285,3 @@ let newAnnosAlt = null; // used in closure

if (!newAnnosAlt) { // only create upon insertion of first anno
newAnnosAlt = Edm.Annotations.create(v, altName);
newAnnosAlt = new Edm.Annotations(v, altName);
g_annosArray.push(newAnnosAlt);

@@ -366,3 +369,3 @@ }

function handleTerm(termName, annoValue, context) {
let newAnno = Edm.Annotation.create(v, termName);
let newAnno = new Edm.Annotation(v, termName);

@@ -386,2 +389,7 @@ // termName may contain a qualifier: @UI.FieldGroup#shippingStatus

termTypeName = dictTerm.Type;
// issue warning for usage of experimental Terms, but only once per Term
if (!options.betaMode && dictTerm["$experimental"] && !g_experimental_terms[termNameWithoutQualifiers]) {
warningMessage(context, termNameWithoutQualifiers + " is experimental and can be changed or removed at any time, do not use productively!");
g_experimental_terms[termNameWithoutQualifiers] = true;
}
}

@@ -420,7 +428,4 @@ else {

let typeName = "Path";
if (dTypeName == "Edm.PropertyPath" ||
dTypeName == "Edm.AnnotationPath" ||
dTypeName == "Edm.NavigationPropertyPath") {
if( ['Edm.AnnotationPath', 'Edm.ModelElementPath', 'Edm.NavigationPropertyPath', 'Edm.PropertyPath', 'Edm.Path' ].includes(dTypeName) )
typeName = dTypeName.split('.')[1];
}

@@ -456,76 +461,159 @@ let val = expr;

if(dTypeName == 'Edm.PrimitiveType')
dTypeName = undefined;
if (typeof val === 'string') {
if (dTypeName == "Edm.Boolean") {
if (val == "true" || val == "false") {
typeName = "Bool";
}
else {
warningMessage(context, "found String, but expected type " + dTypeName);
}
// https://github.com/oasis-tcs/odata-abnf/blob/master/abnf/odata-abnf-construction-rules.txt#L923
let isBool = ['true', 'false'].includes(val);
let isNum = /^((\+|-)?\d+(.\d+)?(e(\+|-)?\d+)?[fFmMdD]?|NaN|-?INF)$/.test(val);
switch(dTypeName) {
case 'Edm.Boolean':
typeName = 'Bool';
if(!isBool) {
warningMessage(context, "found String, but expected type " + dTypeName);
}
break;
case 'Edm.Double':
case 'Edm.Single':
typeName = 'Float';
if(!isNum) {
warningMessage(context, "found non-numeric string, but expected type " + dTypeName);
}
break;
case 'Edm.Decimal':
typeName = 'Decimal';
if(!isNum) {
warningMessage(context, "found non-numeric string, but expected type " + dTypeName);
}
break;
default:
if(dTypeName) {
if(isComplexType(dTypeName)) {
warningMessage(context, "found String, but expected complex type " + dTypeName);
}
else if(isEnumType(dTypeName)) {
warningMessage(context, "found String, but expected enum type " + dTypeName);
typeName = "EnumMember";
}
else if(dTypeName.startsWith('Edm.')) {
typeName = dTypeName.substring(4);
}
else {
// TODO
//warningMessage(context, "type is not yet handled: found String, expected type: " + dTypeName);
}
}
else { // no dTypeName, best guessing
if(isBool) {
typeName = 'Bool';
dTypeName = 'Edm.Boolean';
}
else if(isNum) {
typeName = 'Int';
if(/^(\+|-)?\d{1,19}$/.test(val)) {
let n = parseInt(val);
if (n >= -32768 && n <= 32767) {
dTypeName = 'Edm.Int16';
} else if(n >= -2147483648 && n <= 2147483647) {
dTypeName = 'Edm.Int32';
} else {
dTypeName = 'Edm.Int64';
}
}
else {
typeName = 'Decimal';
dTypeName = 'Edm.Decimal';
}
}
else {
//8HEXDIG "-" 4HEXDIG "-" 4HEXDIG "-" 4HEXDIG "-" 12HEXDIG
if(/^[a-fA-F\d]{8}-[a-fA-F\d]{4}-[a-fA-F\d]{4}-[a-fA-F\d]{4}-[a-fA-F\d]{12}$/.test(val)) {
typeName = 'Guid';
dTypeName = 'Edm.Guid';
}
else if(/^(\+|-)?P(\d+D)?(T(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/.test(val)) {
typeName = 'Duration';
dTypeName = 'Edm.Duration';
}
// Date Matchgroup 1 ^((\d{1,4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))
// TimeOffset Matchgroup 5 (T([0-1]\d|2[[0-3])(:|%3[aA])[05]\d(:|%3[aA])[05]\d(\.\d{1,12})?(Z|-?([0-1]\d|2[[0-3])(:|%3[aA])[05]\d)?)?)$
else {
let m = val.match(/^((\d{1,4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))(T([0-1]\d|2[[0-3])(:|%3[aA])[05]\d(:|%3[aA])[05]\d(\.\d{1,12})?(Z|-?([0-1]\d|2[[0-3])(:|%3[aA])[05]\d)?)?)$/);
if(m) {
if(m[5]) {
typeName = 'DateTimeOffset';
dTypeName = 'Edm.DateTimeOffset';
}
else {
typeName = 'Date';
dTypeName = 'Edm.Date'
}
}
else {
typeName = 'String';
dTypeName = 'Edm.String';
}
}
}
}
}
else if (dTypeName == "Edm.Decimal") {
if (isNaN(val) || isNaN(parseFloat(val))) {
warningMessage(context, "found non-numeric string, but expected type " + dTypeName);
}
else {
typeName = "Decimal";
}
}
else if (dTypeName == "Edm.Double" || dTypeName == "Edm.Single") {
if (isNaN(val) || isNaN(parseFloat(val))) {
warningMessage(context, "found non-numeric string, but expected type " + dTypeName);
}
else {
typeName = "Float";
}
}
else if (isComplexType(dTypeName)) {
warningMessage(context, "found String, but expected complex type " + dTypeName);
}
else if (isEnumType(dTypeName)) {
warningMessage(context, "found String, but expected enum type " + dTypeName);
typeName = "EnumMember";
}
else if (dTypeName && dTypeName.startsWith("Edm.") && dTypeName !== "Edm.PrimitiveType") {
typeName = dTypeName.substring(4);
}
else {
// TODO
//warningMessage(context, "type is not yet handled: found String, expected type: " + dTypeName);
}
}
else if (typeof val === 'boolean') {
typeName = "Bool";
if (dTypeName == "Edm.Boolean") {
val = val ? "true" : "false";
if(dTypeName == undefined)
dTypeName = 'Edm.Boolean';
switch(dTypeName) {
case 'Edm.Boolean':
typeName = 'Bool';
break;
case 'Edm.String':
val = val.toString();
break;
default:
warningMessage(context, "found Boolean, but expected type " + dTypeName);
break;
}
else if (dTypeName == "Edm.String") {
typeName = "String";
}
else {
warningMessage(context, "found Boolean, but expected type " + dTypeName);
}
}
else if (typeof val === 'number') {
if (isComplexType(dTypeName)) {
warningMessage(context, "found number, but expected complex type " + dTypeName);
if(dTypeName == undefined) {
if(Number.isInteger(val)) {
if (val >= -32768 && val <= 32767) {
dTypeName = 'Edm.Int16';
} else if(val >= -2147483648 && val <= 2147483647) {
dTypeName = 'Edm.Int32';
} else {
dTypeName = 'Edm.Int64';
}
}
else {
dTypeName = 'Edm.Decimal';
}
}
else if (dTypeName === 'Edm.String') {
typeName = "String";
switch(dTypeName) {
case 'Edm.String':
typeName = 'String';
break;
case 'Edm.Single':
case 'Edm.Double':
typeName = 'Float';
break;
case 'Edm.Decimal':
typeName = 'Decimal';
break;
case 'Edm.Int16':
case 'Edm.Int32':
case 'Edm.Int64':
case 'Edm.Byte':
case 'Edm.SByte':
typeName = 'Int';
break;
default:
if(isComplexType(dTypeName)) {
warningMessage(context, "found number, but expected complex type " + dTypeName);
}
else {
// all others (Paths, Enums)
warningMessage(context, "found number, but expected type " + dTypeName);
}
}
else if (dTypeName == "Edm.PropertyPath") {
warningMessage(context, "found number, but expected " + dTypeName);
}
else if (dTypeName == "Edm.Boolean") {
warningMessage(context, "found number, but expected type " + dTypeName);
}
else if (dTypeName == "Edm.Decimal") {
typeName = "Decimal";
}
else if (dTypeName == "Edm.Double") {
typeName = "Float";
}
else {
typeName = Number.isInteger(val) ? 'Int' : 'Float';
}
}

@@ -536,4 +624,13 @@ else {

if(typeName == undefined)
throw Error('Please debug me: typeName unset for value: ' + val);
if(dTypeName == undefined)
throw Error('Please debug me: dtypeName unset for value: ' + val);
if( ['Edm.AnnotationPath', 'Edm.ModelElementPath', 'Edm.NavigationPropertyPath', 'Edm.PropertyPath', 'Edm.Path' ].includes(dTypeName) )
dTypeName = dTypeName.split('.')[1];
return {
name : typeName,
jsonName: dTypeName,
value : val

@@ -558,4 +655,4 @@ };

checkMultiEnumValue(cAnnoValue, dTypeName, context);
oTarget.setJSON({ "EnumMember@odata.type" : '#'+dTypeName, EnumMember: generateMultiEnumValue(cAnnoValue, dTypeName, false) });
oTarget.setXml({ "EnumMember": generateMultiEnumValue(cAnnoValue, dTypeName, true) });
oTarget.setJSON({ "EnumMember": generateMultiEnumValue(cAnnoValue, dTypeName, false), "EnumMember@odata.type" : '#'+dTypeName });
oTarget.setXml( { "EnumMember": generateMultiEnumValue(cAnnoValue, dTypeName, true) });
}

@@ -573,5 +670,6 @@ else

let res = handleExpression(cAnnoValue["="], dTypeName, context);
oTarget[res.name] = res.value;
oTarget.setXml( { [res.name] : res.value });
oTarget.setJSON( { [res.name] : res.value });
}
else if (cAnnoValue["#"] != undefined)
else if (cAnnoValue["#"] !== undefined)
{

@@ -581,12 +679,12 @@ if (dTypeName)

checkEnumValue(cAnnoValue["#"], dTypeName, context);
oTarget.setJSON({ "EnumMember@odata.type" : '#'+dTypeName, EnumMember: cAnnoValue["#"] });
oTarget.setXml({ "EnumMember": dTypeName + "/" + cAnnoValue["#"] });
oTarget.setJSON({ "EnumMember": cAnnoValue["#"], "EnumMember@odata.type" : '#'+dTypeName, });
oTarget.setXml( { "EnumMember": dTypeName + "/" + cAnnoValue["#"] });
}
else
{
oTarget.setJSON({ "EnumMember@odata.type" : '#'+oTermName + "Type/", EnumMember: cAnnoValue["#"] });
oTarget.setXml({ "EnumMember": oTermName + "Type/" + "/" + cAnnoValue["#"] });
oTarget.setJSON({ "EnumMember": cAnnoValue["#"], "EnumMember@odata.type" : '#'+oTermName + "Type/" });
oTarget.setXml( { "EnumMember": oTermName + "Type/" + "/" + cAnnoValue["#"] });
}
}
else if (cAnnoValue["$value"]) {
else if (cAnnoValue["$value"] !== undefined) {
// "pseudo-structure" used for annotating scalar annotations

@@ -615,3 +713,4 @@ handleValue(cAnnoValue["$value"], oTarget, oTermName, dTypeName, context);

let res = handleSimpleValue(cAnnoValue, dTypeName, context);
oTarget[res.name] = res.value;
oTarget.setXml( { [res.name] : res.value });
oTarget.setJSON( { [res.jsonName] : res.value });
}

@@ -662,3 +761,3 @@ }

function generateRecord(obj, termName, dictRecordTypeName, context) {
let newRecord = Edm.Record.create(v);
let newRecord = new Edm.Record(v);
let actualTypeName = null;

@@ -738,3 +837,3 @@

let newPropertyValue = Edm.PropertyValue.create(v, i);
let newPropertyValue = new Edm.PropertyValue(v, i);
handleValue(obj[i], newPropertyValue, termName, dictPropertyTypeName, context);

@@ -755,3 +854,3 @@ newRecord.append(newPropertyValue);

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

@@ -779,3 +878,4 @@ let innerTypeName = null;

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

@@ -793,3 +893,4 @@ }

let res = handleSimpleValue(value, innerTypeName, context);
let newThing = Edm.ValueThing.create(v, res.name, value );
let newThing = new Edm.ValueThing(v, res.name, value );
newThing.setJSON( { [res.jsonName] : res.value });
newCollection.append(newThing);

@@ -817,3 +918,3 @@ }

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

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

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

@@ -857,3 +958,3 @@

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

@@ -903,5 +1004,4 @@ }

// filter function, assumed to be used for array of string
// accepts those strings that start with a knwon vocabulary name
// accepts those strings that start with a known vocabulary name
function filterKnownVocabularies(name) {
let knownVocabularies = ['Analytics', 'Core', 'Common', 'UI', 'Communication', 'Capabilities', 'Measures'];
var match = name.match(/^(@)(\w+)/);

@@ -973,2 +1073,2 @@ if (match == null) return false;

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

@@ -276,4 +276,14 @@ 'use strict';

// expand shortcut form of ValueList annotation
if (aNameWithoutQualifier == "@Common.ValueList.entity") {
if (aNameWithoutQualifier == "@Common.ValueList.entity" ||
aNameWithoutQualifier == "@Common.ValueList.viaAssociation") {
try {
// note: we loop over all annotations that were originally present, even if they are
// removed from the carrier via this handler
// we don't remove anything from the array "annoNames"
if (aNameWithoutQualifier == "@Common.ValueList.viaAssociation" && !options.betaMode) {
signal(warning`annotation preprocessing: shortcut annotation ${aNameWithoutQualifier} is only available in beta-mode, ${ctx}`);
throw 'leave';
}
// if CollectionPath is explicitly given, no shortcut expansion is made

@@ -284,14 +294,50 @@ if (carrier["@Common.ValueList.CollectionPath"]) {

if (carrier.kind === 'entity' || carrier.kind === 'view') {
signal(warning`annotation preprocessing/${aNameWithoutQualifier}: annotation must not be used for an entity, ${ctx}`);
throw 'leave';
}
// check on "type"? e.g. if present, it must be #fixed ... ?
// the value list entity
let entityName = carrier["@Common.ValueList.entity"]; // name of value list entity
if (entityName["="]) {
signal(warning`in annotation preprocessing/value help shortcut: 'entity' must be a string, ${ctx}`);
// value list entity
let enameShort = null; // (string) name of value list entity, short (i.e. name within service)
let enameFull = null; // (string) name of value list entity, fully qualified name
if (aNameWithoutQualifier == "@Common.ValueList.viaAssociation") {
// value is expected to be an expression, namely the path to an association of the carrier entity
let assocName = carrier["@Common.ValueList.viaAssociation"]['='];
if (!assocName) {
signal(warning`in annotation preprocessing/${aNameWithoutQualifier}: value of 'viaAssociation' must be a path, ${ctx}`);
throw 'leave';
}
let assoc = csn.definitions[art].elements[assocName];
if (!assoc || !(assoc.type === 'cds.Association' || assoc.type === 'cds.Composition')) {
signal(warning`in annotation preprocessing/${aNameWithoutQualifier}: there is no association "${assocName}", ${ctx}`);
throw 'leave';
}
enameFull = assoc.target.name || assoc.target; // full name
enameShort = enameFull.split('.').pop();
}
let ename = entityName["="] || entityName;
let nameprefix = art.replace(/.[^.]+$/, ''); // better way of getting the service name?
let vlEntity = csn.definitions[nameprefix + '.' + ename];
else if (aNameWithoutQualifier == "@Common.ValueList.entity") {
// if both annotations are present, ignore 'entity' and raise a message
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}`);
throw "leave";
}
let annoVal = carrier["@Common.ValueList.entity"]; // name of value list entity
if (annoVal["="]) {
signal(warning`in annotation preprocessing/${aNameWithoutQualifier}: annotation value must be a string, ${ctx}`);
}
let nameprefix = art.replace(/.[^.]+$/, ''); // better way of getting the service name?
enameShort = annoVal["="] || annoVal;
enameFull = nameprefix + '.' + enameShort;
}
let vlEntity = csn.definitions[enameFull]; // (object) value list entity
if (!vlEntity) {
signal(warning`in annotation preprocessing/value help shortcut: entity ${ename} does not exist, ${ctx}`);
signal(warning`in annotation preprocessing/${aNameWithoutQualifier}: entity "${enameFull}" does not exist, ${ctx}`);
throw "leave";

@@ -302,5 +348,5 @@ }

// explicitly provided label wins
// TODO: once TnT excpetion for nonexisting vlEntity is removed, simplify condition
// TODO: once TnT exception for nonexisting vlEntity is removed, simplify condition
let label = carrier["@Common.ValueList.Label"] ||
carrier["@Common.Label"] || (vlEntity && vlEntity["@Common.Label"]) || ename;
carrier["@Common.Label"] || (vlEntity && vlEntity["@Common.Label"]) || enameShort;

@@ -320,7 +366,7 @@ // localDataProp

if (keys.length == 0) {
signal(warning`in annotation preprocessing/value help shortcut: entity ${ename} has no key, ${ctx}`);
signal(warning`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 ${ename} has more than one key, ${ctx}`);
signal(warning`in annotation preprocessing/value help shortcut: entity "${enameFull}" has more than one key, ${ctx}`);
valueListProp = keys[0];

@@ -366,5 +412,5 @@

for (let e in carrier) {
if (e == "@Common.ValueList.entity") {
if (e == "@Common.ValueList.entity" || e == "@Common.ValueList.viaAssociation") {
newObj["@Common.ValueList.Label"] = label;
newObj["@Common.ValueList.CollectionPath"] = entityName;
newObj["@Common.ValueList.CollectionPath"] = enameShort;
newObj["@Common.ValueList.Parameters"] = parameters;

@@ -393,3 +439,3 @@ if (textField && options && options.tntFlavor) {

// avoid subsequent warnings
delete carrier["@Common.ValueList.entity"];
delete carrier[aNameWithoutQualifier];
delete carrier["@Common.ValueList.type"];

@@ -396,0 +442,0 @@ }

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

module.exports = function csn2edm(csn, _options) {
module.exports = function csn2edm(csn, serviceName, _options) {

@@ -36,15 +36,18 @@ const options = glue.validateOptions(_options);

// Remove from the model all definitions that do not belong to the specified service
// TODO maybe this can be combined with initializeModel
if (serviceName) {
let allDefinitions = model.definitions;
let namesNotInService = Object.keys(allDefinitions).filter(n => !n.startsWith(serviceName + '.') && n != serviceName);
for (let name of namesNotInService) {
delete model.definitions[name];
}
}
let serviceCsn = glue.initializeModel(model, options);
if(serviceCsn == undefined)
throw "No Service found in model"
throw Error('No Service found in model');
let navigationProperties = [];
// FIXME: Temporary special handling for TNT
if (options && options.tntFlavor)
{
if(options.oldstyleSelf)
Edm.NavigationProperty.OLDSTYLE_SELF='self';
}
function baseName(str, del) { let l = str.lastIndexOf(del); // eslint-disable-line no-unused-vars

@@ -59,3 +62,3 @@ return (l >= 0) ? str.slice(l+del.length, str.length) : str; }

let Schema = Edm.Schema.create(v, serviceCsn.name, undefined /* unset alias */);
let Schema = new Edm.Schema(v, serviceCsn.name, undefined /* unset alias */);

@@ -66,4 +69,4 @@ // now namespace and alias are used to create the fullQualified(name)

let service = Edm.DataServices.create(v, Schema);
let edm = Edm.create(v, service);
let service = new Edm.DataServices(v, Schema);
let edm = new Edm(v, service);

@@ -109,3 +112,3 @@ /* create the entitytypes and sets

if(Schema._ec._children.length == 0)
throw "EntityContainer must contain at least one EntitySet"
throw Error('EntityContainer must contain at least one EntitySet');

@@ -136,7 +139,7 @@ return edm

Schema.append(Edm.EntityType.create(v, attributes, properties, entityCsn));
Schema.append(new Edm.EntityType(v, attributes, properties, entityCsn));
if (createEntitySet)
{
let entitySet = Edm.EntitySet.create(v, { Name: EntitySetName, EntityType: fqEntityTypeName }, entityCsn);
let entitySet = new Edm.EntitySet(v, { Name: EntitySetName, EntityType: fqEntityTypeName }, entityCsn);

@@ -170,4 +173,4 @@ // V4: Create NavigationPropertyBinding in EntitySet if NavigationProperty is not a Containment

let actionNode = (iAmAnAction) ? Edm.Action.create(v, attributes)
: Edm.FunctionDefinition.create(v, attributes);
let actionNode = (iAmAnAction) ? new Edm.Action(v, attributes)
: new Edm.FunctionDefinition(v, attributes);

@@ -178,3 +181,3 @@ if(entityCsn != undefined)

// Binding Parameter: 'in' at first position in sequence, this is decisive!
actionNode.append(Edm.Parameter.create(v, { Name: "in", Type: fullQualified(entityCsn.name) }, {} ));
actionNode.append(new Edm.Parameter(v, { Name: "in", Type: fullQualified(entityCsn.name) }, {} ));
}

@@ -184,4 +187,4 @@ else // unbound => produce Action/FunctionImport

let actionImport = iAmAnAction
? Edm.ActionImport.create(v, { Name: actionName, Action : fullQualified(actionName) })
: Edm.FunctionImport.create(v, { Name: actionName, Function : fullQualified(actionName) });
? new Edm.ActionImport(v, { Name: actionName, Action : fullQualified(actionName) })
: new Edm.FunctionImport(v, { Name: actionName, Function : fullQualified(actionName) });

@@ -202,3 +205,3 @@ let rt = actionCsn.returns && (actionCsn.returns.type || actionCsn.returns.items.type);

glue.forAll(actionCsn.params, (parameterCsn, parameterName) => {
actionNode.append(Edm.Parameter.create(v, { Name: parameterName }, parameterCsn ));
actionNode.append(new Edm.Parameter(v, { Name: parameterName }, parameterCsn ));
});

@@ -208,3 +211,3 @@

if(actionCsn.returns) {
actionNode._returnType = Edm.ReturnType.create(v, actionCsn.returns, fullQualified);
actionNode._returnType = new Edm.ReturnType(v, actionCsn.returns, fullQualified);
// if binding type matches return type add attribute EntitySetPath

@@ -221,3 +224,3 @@ if(entityCsn && fullQualified(entityCsn.name) === actionNode._returnType._type) {

{
let functionImport = Edm.FunctionImport.create(v, { Name: name.replace(namespace, '') } );
let functionImport = new Edm.FunctionImport(v, { Name: name.replace(namespace, '') } );

@@ -253,3 +256,3 @@ // inserted now to maintain attribute order with old odata generator...

else
throw "Please debug me: Neither function nor action";
throw Error('Please debug me: Neither function nor action');

@@ -267,3 +270,3 @@ if(entityCsn != undefined)

(elementCsn, elementName) => {
functionImport.append(Edm.Parameter.create(v, { Name: elementName }, elementCsn, 'In' ));
functionImport.append(new Edm.Parameter(v, { Name: elementName }, elementCsn, 'In' ));
}

@@ -281,5 +284,5 @@ );

// V2 XML spec does only mention default Nullable=true for Properties not for Parameters so omitting Nullable=true let
// the client assume that Nullable is false.... Correct Nullable Handling is done inside Parameter.create()
// the client assume that Nullable is false.... Correct Nullable Handling is done inside Parameter constructor
glue.forAll(actionCsn.params, (parameterCsn, parameterName) => {
functionImport.append(Edm.Parameter.create(v, { Name: parameterName }, parameterCsn, 'In' ));
functionImport.append(new Edm.Parameter(v, { Name: parameterName }, parameterCsn, 'In' ));
});

@@ -312,3 +315,4 @@

{
setProp(elementCsn, '_parent', parentCsn);
if(elementCsn._parent == undefined)
setProp(elementCsn, '_parent', parentCsn);

@@ -326,3 +330,3 @@ if(glue.isAssociationOrComposition(elementCsn))

{
let navProp = Edm.NavigationProperty.create(v, {
let navProp = new Edm.NavigationProperty(v, {
Name: elementName,

@@ -341,3 +345,3 @@ Type: fullQualified(elementCsn.target.name)

let anonymousComplexType = createAnonymousComplexType(elementCsn, prefix);
props.push(Edm.Property.create(v, {
props.push(new Edm.Property(v, {
Name: elementCsn.name,

@@ -360,3 +364,3 @@ Type: fullQualified(anonymousComplexType.Name)

!(options.isV4() && isContainerAssoc))
props.push(Edm.Property.create(v, { Name: elementName }, elementCsn));
props.push(new Edm.Property(v, { Name: elementName }, elementCsn));
else

@@ -399,3 +403,3 @@ {

let complexType = Edm.ComplexType.create(v, attributes, structuredTypeCsn);
let complexType = new Edm.ComplexType(v, attributes, structuredTypeCsn);
let elementsCsn = structuredTypeCsn.items || structuredTypeCsn;

@@ -419,5 +423,5 @@ complexType.append(...(createProperties(elementsCsn)[0]));

if((typeCsn.items && typeCsn.items.enum) || typeCsn.enum)
typeDef = Edm.EnumType.create(v, props, typeCsn);
typeDef = new Edm.EnumType(v, props, typeCsn);
else
typeDef = Edm.TypeDefinition.create(v, props, typeCsn );
typeDef = new Edm.TypeDefinition(v, props, typeCsn );
Schema.append(typeDef);

@@ -481,3 +485,3 @@ }

1) Counterpart NavigationProperty exists and is responsible to create the edm:Association element which needs to
be reused by this backlink association.
be reused by this backlink association. This is save because at this point of the processing all NavProps are created.
2) Counterpart NavigationProperty does not exist (@odata.navigable:false), then the missing edm:Association element

@@ -488,3 +492,4 @@ of the origin association needs to be created as if it would have been already available in case (1).

let reuseAssoc = false;
if(constraints._originAssocCsn)
let forwardAssocCsn = constraints._originAssocCsn;
if(forwardAssocCsn)
{

@@ -495,14 +500,36 @@ // This is a backlink, swap the roles and types, rewrite assocName

parentName = constraints._originAssocCsn._parent.name.replace(namespace, '');
assocName = parentName + NAVPROP_TRENNER + constraints._originAssocCsn.name.replace(VALUELIST_NAVPROP_PREFIX, '');
parentName = forwardAssocCsn._parent.name.replace(namespace, '');
assocName = parentName + NAVPROP_TRENNER + forwardAssocCsn.name.replace(VALUELIST_NAVPROP_PREFIX, '');
navigationProperty.Relationship = fullQualified(assocName)
reuseAssoc = !!constraints._originAssocCsn._NavigationProperty;
constraints = navigationProperty.getReferentialConstraints(constraints._originAssocCsn);
reuseAssoc = !!forwardAssocCsn._NavigationProperty;
constraints = navigationProperty.getReferentialConstraints(forwardAssocCsn);
}
if(reuseAssoc)
{
// Example:
// entity E { key id: Integer; toF: association to F; };
// entity F { key id: Integer; toE: composition of E on toE.toF = $self; };
//
// Consider we're in NavigationProperty 'toE' which is the backlink to F.
// Then forwardAssocCsn is 'E_toF' with two Ends: E, F.
// Backlink F.toE is a composition, making E existentially dependant on F.
// So End E of Association E_toF (which is End[0]) receives Edm.OnDelete.
// Depending on the order of the navigation properties it might be that the
// forward Edm.Association has not yet been produced. In this case Edm.OnDelete
// is parked at the forward NavigationProperty.
if(!forwardAssocCsn._NavigationProperty._edmAssociation && navigationProperty._csn.type == 'cds.Composition')
{
// TODO: to be specified via @sap.on.delete
forwardAssocCsn._NavigationProperty.set( { _OnDeleteSrcEnd: new Edm.OnDelete(v, { Action: 'Cascade' }) } );
}
return;
}
// create Association and AssociationSet if this is not a backlink association
let association = Edm.Association.create(v, { Name: assocName },
// Create Association and AssociationSet if this is not a backlink association.
// Store association at navigation property because in case the Ends must be modified
// later by the partner (backlink) association
navigationProperty._edmAssociation = new Edm.Association(v, { Name: assocName }, navigationProperty,
[ fromRole, fullQualified(fromEntityType) ],

@@ -514,8 +541,8 @@ [ toRole, fullQualified(toEntityType) ],

if(Object.keys(constraints.constraints).length > 0)
association.append(Edm.ReferentialConstraint.createV2(v,
navigationProperty._edmAssociation.append(Edm.ReferentialConstraint.createV2(v,
fromRole, toRole, constraints.constraints));
Schema.append(association);
Schema.append(navigationProperty._edmAssociation);
let assocSet = Edm.AssociationSet.create(v, { Name: assocName, Association: fullQualified(assocName) },
let assocSet = new Edm.AssociationSet(v, { Name: assocName, Association: fullQualified(assocName) },
fromRole, toRole, fromEntityType, toEntityType);

@@ -522,0 +549,0 @@ Schema._ec.append(assocSet);

@@ -5,17 +5,12 @@ 'use strict'

class Node {
static create(v, details=Object.create(null), csn=undefined)
{
return new this(v, details, csn);
}
class Node
{
constructor(v, attributes=Object.create(null), csn=undefined)
{
if(!attributes || typeof attributes !== 'object')
throw "Please debug me: attributes must be a dictionary"
throw Error('Please debug me: attributes must be a dictionary');
if(!(v instanceof Array))
throw "Please debug me: v is either undefined or not an array: " + v;
throw Error('Please debug me: v is either undefined or not an array: ' + v);
if(v.filter(v=>v).length != 1)
throw "Please debug me: exactly one version must be set"
throw Error('Please debug me: exactly one version must be set');
Object.assign(this, attributes);

@@ -39,3 +34,3 @@ this.set({ _children: [], _xmlOnlyAttributes: Object.create(null), _jsonOnlyAttributes: Object.create(null), _v: v });

if(!attributes || typeof attributes !== 'object')
throw "Please debug me: attributes must be a dictionary"
throw Error('Please debug me: attributes must be a dictionary');
let newAttributes = Object.create(null);

@@ -55,3 +50,3 @@ for (let p in attributes) newAttributes[p] = {

if(!attributes || typeof attributes !== 'object')
throw "Please debug me: attributes must be a dictionary"
throw Error('Please debug me: attributes must be a dictionary');
return Object.assign(this._xmlOnlyAttributes, attributes);

@@ -65,3 +60,3 @@ }

if(!attributes || typeof attributes !== 'object')
throw "Please debug me: attributes must be a dictionary"
throw Error('Please debug me: attributes must be a dictionary');
return Object.assign(this._jsonOnlyAttributes, attributes);

@@ -82,3 +77,3 @@ }

// $kind Property MAY be omitted in JSON for performance reasons
if(this.kind != 'Property')
if(![ 'Property', 'EntitySet', 'ActionImport', 'FunctionImport', 'Singleton', 'Schema' ].includes(this.kind))
json['$Kind'] = this.kind;

@@ -145,4 +140,7 @@

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

@@ -174,10 +172,9 @@ }

{
static
create(v, details)
constructor(v, details)
{
let node = super.create(v, details);
if(node.v2)
node['xmlns:edmx'] = 'http://docs.oasis-open.org/odata/ns/edmx';
return node;
super(v, details);
if(this.v2)
this['xmlns:edmx'] = 'http://docs.oasis-open.org/odata/ns/edmx';
}
get kind() { return 'edmx:Reference' }

@@ -208,4 +205,3 @@

{
static
create(v, ns, alias=undefined, annotations=[], withEntityContainer=true)
constructor(v, ns, alias=undefined, annotations=[], withEntityContainer=true)
{

@@ -216,5 +212,5 @@ let props = Object.create(null);

props.Alias = alias;
let schema = super.create(v, props);
schema.set( { _annotations: annotations, _actions: {} } );
schema.setXml( { xmlns: (schema.v2) ? "http://schemas.microsoft.com/ado/2008/09/edm" : "http://docs.oasis-open.org/odata/ns/edm" } );
super(v, props);
this.set( { _annotations: annotations, _actions: {} } );
this.setXml( { xmlns: (this.v2) ? "http://schemas.microsoft.com/ado/2008/09/edm" : "http://docs.oasis-open.org/odata/ns/edm" } );

@@ -224,11 +220,10 @@ if(withEntityContainer)

let ecprops = { Name: 'EntityContainer' };
let ec = EntityContainer.create(v, ecprops );
if(schema.v2)
let ec = new EntityContainer(v, ecprops );
if(this.v2)
ec.setXml( { 'm:IsDefaultEntityContainer': true } );
// append for rendering, ok ec has Name
schema.append(ec);
this.append(ec);
// set as attribute for later access...
schema.set({ _ec : ec })
this.set({ _ec : ec })
}
return schema
}

@@ -272,17 +267,2 @@

// no $Kind
toJSON()
{
let json = Object.create(null);
this.toJSONattributes(json);
this.toJSONchildren(json);
glue.forAll(this._actions, (actionArray, actionName) => {
json[actionName] = [];
actionArray.forEach(action => {
json[actionName].push(action.toJSON());
});
});
return json;
}
// no $Namespace

@@ -309,2 +289,9 @@ toJSONattributes(json)

}
glue.forAll(this._actions, (actionArray, actionName) => {
json[actionName] = [];
actionArray.forEach(action => {
json[actionName].push(action.toJSON());
});
});
return json;

@@ -317,10 +304,8 @@ }

{
static
create(v, schema)
constructor(v, schema)
{
let node = super.create(v);
node.append(schema);
if(node.v2)
node.setXml( { 'm:DataServiceVersion': '2.0' } )
return node;
super(v);
this.append(schema);
if(this.v2)
this.setXml( { 'm:DataServiceVersion': '2.0' } )
}

@@ -350,10 +335,9 @@

{
static
create(v, service)
constructor(v, service)
{
let edm = super.create(v, { Version : (v[1]) ? '4.0' : '1.0' });
edm.set( { _service: service, _defaultRefs: [] } );
super(v, { Version : (v[1]) ? '4.0' : '1.0' });
this.set( { _service: service, _defaultRefs: [] } );
let xmlProps = Object.create(null);
if(edm.v4)
if(this.v4)
{

@@ -368,5 +352,3 @@ xmlProps['xmlns:edmx'] = "http://docs.oasis-open.org/odata/ns/edmx";

}
edm.setXml(xmlProps);
return edm;
this.setXml(xmlProps);
}

@@ -381,33 +363,41 @@

{
let r = Reference.create(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1.xml" });
r.append(Include.create(this._v, {Alias : "Core", Namespace : "Org.OData.Core.V1"} ))
let r = new Reference(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1.xml" });
r.append(new Include(this._v, {Alias : "Core", Namespace : "Org.OData.Core.V1"} ))
this._defaultRefs.push(r);
r = Reference.create(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Measures.V1.xml" });
r.append(Include.create(this._v, {Alias : "Measures", Namespace : "Org.OData.Measures.V1"} ))
r = new Reference(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Measures.V1.xml" });
r.append(new Include(this._v, {Alias : "Measures", Namespace : "Org.OData.Measures.V1"} ))
this._defaultRefs.push(r);
r = Reference.create(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1.xml" });
r.append(Include.create(this._v, {Alias : "Capabilities", Namespace : "Org.OData.Capabilities.V1"} ))
r = new Reference(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1.xml" });
r.append(new Include(this._v, {Alias : "Capabilities", Namespace : "Org.OData.Capabilities.V1"} ))
this._defaultRefs.push(r);
r = Reference.create(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Aggregation.V1.xml" });
r.append(Include.create(this._v, {Alias : "Aggregation", Namespace : "Org.OData.Aggregation.V1"} ))
r = new Reference(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Aggregation.V1.xml" });
r.append(new Include(this._v, {Alias : "Aggregation", Namespace : "Org.OData.Aggregation.V1"} ))
this._defaultRefs.push(r);
r = Reference.create(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/462030211/Analytics.xml?api=v2" });
r.append(Include.create(this._v, {Alias : "Analytics", Namespace : "com.sap.vocabularies.Analytics.v1"} ))
r = new Reference(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Validation.V1.xml" });
r.append(new Include(this._v, {Alias : "Validation", Namespace : "Org.OData.Validation.V1"} ))
this._defaultRefs.push(r);
r = Reference.create(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/448470974/Common.xml?api=v2" });
r.append(Include.create(this._v, {Alias : "Common", Namespace : "com.sap.vocabularies.Common.v1"} ))
r = new Reference(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/462030211/Analytics.xml?api=v2" });
r.append(new Include(this._v, {Alias : "Analytics", Namespace : "com.sap.vocabularies.Analytics.v1"} ))
this._defaultRefs.push(r);
r = Reference.create(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/448470971/Communication.xml?api=v2" });
r.append(Include.create(this._v, {Alias : "Communication", Namespace : "com.sap.vocabularies.Communication.v1"} ))
r = new Reference(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/448470974/Common.xml?api=v2" });
r.append(new Include(this._v, {Alias : "Common", Namespace : "com.sap.vocabularies.Common.v1"} ))
this._defaultRefs.push(r);
r = Reference.create(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/448470968/UI.xml?api=v2" });
r.append(Include.create(this._v, {Alias : "UI", Namespace : "com.sap.vocabularies.UI.v1"} ))
r = new Reference(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/448470971/Communication.xml?api=v2" });
r.append(new Include(this._v, {Alias : "Communication", Namespace : "com.sap.vocabularies.Communication.v1"} ))
this._defaultRefs.push(r);
r = new Reference(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/448470968/UI.xml?api=v2" });
r.append(new Include(this._v, {Alias : "UI", Namespace : "com.sap.vocabularies.UI.v1"} ))
this._defaultRefs.push(r);
r = new Reference(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/496435637/PersonalData.xml?api=v2" });
r.append(new Include(this._v, {Alias : "PersonalData", Namespace : "com.sap.vocabularies.PersonalData.v1"} ))
this._defaultRefs.push(r);
}

@@ -487,12 +477,8 @@ }

class EntityContainer extends Node {}
class EntitySet extends Node
{
static
create(v, details, csn)
{
let node = super.create(v, details, csn);
return node;
}
// virtual
// virtual
setSapVocabularyAsAttributes(csn)

@@ -512,2 +498,4 @@ {

{
// OASIS ODATA-1231 $Collection=true
json['$Collection']=true;
for (let p in this)

@@ -539,11 +527,9 @@ {

{
static create(v, keys)
constructor(v, keys)
{
let node = undefined;
super(v);
if (keys && keys.length > 0)
{
node = super.create(v);
keys.forEach(k => node.append(PropertyRef.create(v, k)));
keys.forEach(k => this.append(new PropertyRef(v, k)));
}
return node;
}

@@ -569,8 +555,6 @@

{
static
create(v, details)
constructor(v, details)
{
let node = super.create(v, details);
node.set( { _returnType: undefined });
return node;
super(v, details);
this.set( { _returnType: undefined });
}

@@ -619,4 +603,3 @@

{
static
create(v, csn, fullQualified)
constructor(v, csn, fullQualified)
{

@@ -628,10 +611,9 @@ let type = csn.type || csn.items.type;

type = fullQualified(type);
let node = super.create(v, { Type: type });
glue.addTypeFacets(node, csn);
super(v, { Type: type });
glue.addTypeFacets(this, csn);
node.set( { _type: type, _isCollection: csn.items != undefined, _nullable: true });
this.set( { _type: type, _isCollection: csn.items != undefined, _nullable: true });
if(node._isCollection)
node.Type = `Collection(${node.Type})`
return node;
if(this._isCollection)
this.Type = `Collection(${this.Type})`
}

@@ -658,13 +640,12 @@

{
static
create(v, attributes, csn, typeName='Type')
constructor(v, attributes, csn, typeName='Type')
{
if(!(csn instanceof Object))
throw "Please debug me: csn must be an object"
throw Error('Please debug me: csn must be an object');
// ??? Is CSN still required? NavProp?
let node = super.create(v, attributes, csn);
node.set({ _isCollection: csn.items != undefined, _typeName: typeName });
super(v, attributes, csn);
this.set({ _isCollection: csn.items != undefined, _typeName: typeName });
if(node[typeName] == undefined)
if(this[typeName] == undefined)
{

@@ -678,9 +659,9 @@ let typecsn = csn.items || csn;

{
node[typeName] = glue.mapCdsToEdmType(typecsn.type, node.v2, csn['@Core.MediaType']);
this[typeName] = glue.mapCdsToEdmType(typecsn.type, this.v2, csn['@Core.MediaType']);
// CDXCORE-CDXCORE-173 ignore type facets for Edm.Stream
if(node[typeName] != 'Edm.Stream')
glue.addTypeFacets(node, typecsn);
if(this[typeName] != 'Edm.Stream')
glue.addTypeFacets(this, typecsn);
}
else
node[typeName] = typecsn.type;
this[typeName] = typecsn.type;

@@ -696,16 +677,15 @@ // CDXCORE-245:

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

@@ -741,10 +721,9 @@

{
static
create(v, details, properties, csn)
constructor(v, details, properties, csn)
{
let node = super.create(v, details, csn);
node.append(...properties);
let keys = Key.create(v, properties.filter(c => c.isKey).map(c => c.Name));
node.set( { _keys: keys } );
return node;
super(v, details, csn);
this.append(...properties);
let keys = properties.filter(c => c.isKey).map(c => c.Name);
if(keys.length > 0)
this.set( { _keys: new Key(v, keys) } );
}

@@ -773,7 +752,5 @@

{
static
create(v, attributes, csn)
constructor(v, attributes, csn)
{
let node = super.create(v, attributes, csn, 'UnderlyingType');
return node
super(v, attributes, csn, 'UnderlyingType');
}

@@ -791,6 +768,5 @@

{
static
create(v, attributes, csn)
constructor(v, attributes, csn)
{
let node = super.create(v, attributes, csn);
super(v, attributes, csn);

@@ -800,5 +776,4 @@ // array of enum not yet allowed

glue.forAll(enumValues, (e, en) => {
node.append(Member.create(v, { Name: en, Value: e.val } ));
this.append(new Member(v, { Name: en, Value: e.val } ));
});
return node
}

@@ -821,5 +796,2 @@

{
static create(v, attributes)
{ return super.create(v, attributes); }
toJSONattributes(json)

@@ -832,9 +804,9 @@ {

class PropertyBase extends TypeBase {
static
create(v, attributes, csn)
class PropertyBase extends TypeBase
{
constructor(v, attributes, csn)
{
let node = super.create(v, attributes, csn);
node.set({ _csn: csn });
if(node.v2)
super(v, attributes, csn);
this.set({ _csn: csn });
if(this.v2)
{

@@ -846,9 +818,8 @@ let typecsn = csn.items || csn;

// but not if Edm.DateTime is the result of a regular cds type mapping
if(node.Type == 'Edm.DateTime'
if(this.Type == 'Edm.DateTime'
&& (typecsn.type != 'cds.DateTime' && typecsn.type != 'cds.Timestamp'))
node.setXml( { 'sap:display-format' : "Date" } );
this.setXml( { 'sap:display-format' : "Date" } );
}
node.setNullable();
return node
this.setNullable();
}

@@ -887,5 +858,5 @@

class Property extends PropertyBase {
static
create(v, attributes, csn)
class Property extends PropertyBase
{
constructor(v, attributes, csn)
{

@@ -902,8 +873,8 @@ // the annotations in this array shall become exposed as Property attributes in

let node = super.create(v, attributes, csn);
super(v, attributes, csn);
// TIPHANACDS-4180
if(node.v2)
if(this.v2)
{
if(csn['@odata.etag'] == true)
node.ConcurrencyMode='Fixed'
this.ConcurrencyMode='Fixed'

@@ -914,7 +885,6 @@ // translate the following @sap annos as xml attributes to the Property

if (Property.SAP_Annotation_Attribute_WhiteList.includes(p))
node.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn[p] });
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn[p] });
}
}
node.set({isKey: csn.key != undefined });
return node;
this.set({isKey: csn.key != undefined });
}

@@ -928,3 +898,3 @@

{
static create(v, Name) { return super.create(v, { Name }); }
constructor(v, Name) { super(v, { Name }); }
}

@@ -934,19 +904,16 @@

{
static
create(v, attributes, csn={}, mode=null)
{
let node = super.create(v, attributes, csn);
constructor(v, attributes, csn={}, mode=null)
{
super(v, attributes, csn);
if(mode != null)
node.Mode = mode;
if(mode != null)
this.Mode = mode;
// V2 XML: Parameters that are not explicitly marked as Nullable or NotNullable in the CSN must become Nullable=true
// V2 XML Spec does only mention default Nullable=true for Properties not for Parameters so omitting Nullable=true let
// the client assume that Nullable is false.... Correct Nullable Handling is done inside Parameter.create()
if(node.v2 && node.Nullable === undefined)
node.setXml({Nullable: true});
// V2 XML: Parameters that are not explicitly marked as Nullable or NotNullable in the CSN must become Nullable=true
// V2 XML Spec does only mention default Nullable=true for Properties not for Parameters so omitting Nullable=true let
// the client assume that Nullable is false.... Correct Nullable Handling is done inside Parameter constructor
if(this.v2 && this.Nullable === undefined)
this.setXml({Nullable: true});
}
return node;
}
toJSON()

@@ -964,22 +931,30 @@ {

class NavigationProperty extends Property {
static
create(v, attributes, csn)
class NavigationProperty extends Property
{
constructor(v, attributes, csn)
{
NavigationProperty.DOLLAR_SELF = '$self'
let navProp = super.create(v, attributes, csn);
navProp.set( {
super(v, attributes, csn);
this.set( {
_type: attributes.Type,
_isCollection: glue.isToMany(csn),
_referentialConstraints: navProp.getReferentialConstraints(),
_referentialConstraints: this.getReferentialConstraints(),
_targetCsn: csn.target } );
if (navProp.v4) {
if (this.v4)
{
// either csn has multiplicity or we have to use the multiplicity of the backlink
if(navProp._isCollection || navProp._referentialConstraints.multiplicity[1] == '*') {
navProp.Type = `Collection(${attributes.Type})`
if(this._isCollection || this._referentialConstraints.multiplicity[1] == '*') {
this.Type = `Collection(${attributes.Type})`
// attribute Nullable is not allowed in combination with Collection (see Spec)
delete navProp.Nullable;
delete this.Nullable;
}
if(csn.type === 'cds.Composition')
{
// TODO: to be specified via @sap.on.delete
this.append(new OnDelete(v, { Action: 'Cascade' } ) );
}
/*

@@ -995,23 +970,20 @@ 1) If this navigation property belongs to an EntityType for a parameterized entity

if(csn['@odata.contained'] == true || csn.containsTarget)
navProp.ContainsTarget = true;
this.ContainsTarget = true;
// if backlink has established partner before this underlying NavProp is created, use it
if(csn.$Partner)
navProp.Partner = csn.$Partner;
if(csn._partnerCsn)
this.Partner = csn._partnerCsn.name;
}
if (navProp.v2 && navProp.isNotNullable()) {
if (this.v2 && this.isNotNullable()) {
// in V2 not null must be expressed with target cardinality of 1 or more,
// store Nullable=false and evaluate in determineMultiplicity()
delete navProp.Nullable;
delete this.Nullable;
}
// store NavProp reference in the model for bidirectional $Partner tagging (done in getReferentialConstraints())
csn._NavigationProperty = navProp;
csn._NavigationProperty = this;
// we don't want NavProps in the <KEY> list
delete navProp.isKey;
// TODO: Possible V4 Attributes: Partner, ContainsTarget
// Possible subelement <OnDelete>, may be specified via @odata.OnDelete annotation...
return navProp;
delete this.isKey;
}

@@ -1047,3 +1019,16 @@

let json_constraints = Object.create(null);
this._children.forEach(c => json_constraints[c.Property] = c.ReferencedProperty);
this._children.forEach(c => {
switch(c.kind) {
case 'ReferentialConstraint':
// collect ref constraints in dictionary
json_constraints[c.Property] = c.ReferencedProperty;
break;
case 'OnDelete':
json['$OnDelete'] = c.Action;
break;
default:
throw Error('Unhandled NavProp child: ' + c.kind);
}
});
// TODO Annotations

@@ -1057,3 +1042,3 @@ if(Object.keys(json_constraints).length > 0)

{
return NavigationPropertyBinding.create(this._v,
return new NavigationPropertyBinding(this._v,
{ Path: this.Name, Target: this._csn.target.name.replace(namespace, '') }

@@ -1066,3 +1051,3 @@ );

glue.forAll(this._referentialConstraints.constraints,
c => this.append(ReferentialConstraint.create(this._v,
c => this.append(new ReferentialConstraint(this._v,
{ Property: c[0], ReferencedProperty: c[1] }

@@ -1074,3 +1059,3 @@ ) ) );

{
let result = { multiplicity: [ ], constraints: Object.create(null), selfs: [] };
let result = { multiplicity: [ ], constraints: Object.create(null), selfs: [], termCount: 0 };

@@ -1085,3 +1070,3 @@ assocCsn = assocCsn || this._csn;

// for all $self conditions, fill constraints of partner (if any)
let isBacklink = result.selfs.length == 1;
let isBacklink = result.selfs.length == 1 && result.termCount == 1;

@@ -1100,3 +1085,10 @@ /* example for originalTarget:

result.selfs.forEach(partner => {
let originAssocCsn = assocCsn.target.elements[partner] || assocCsn.originalTarget.elements[partner];
let originAssocCsn = assocCsn.target.elements[partner];
if(originAssocCsn == undefined && assocCsn.originalTarget)
originAssocCsn = assocCsn.originalTarget.elements[partner];
if(originAssocCsn == undefined)
throw Error('Could not find forward association "' + assocCsn.target.name + '.' + partner +
' for backlink association "' + assocCsn._parent.name + '.' + assocCsn.name + '", maybe forward is not published in service?');
// let originAssocCsn = assocCsn.target.elements[partner] || assocCsn.originalTarget.elements[partner];
if(glue.isAssociationOrComposition(originAssocCsn)) {

@@ -1121,11 +1113,17 @@ // if the assoc is marked as primary key, add all its foreign keys as constraint

// association to the two corresponding csn's and to this NavProp
// (but only if originAssoc is navigable (undefined !== false) still evaluates to true)
if(this.v4 && originAssocCsn['@odata.navigable'] !== false)
// (but only if originAssoc is navigable (undefined !== false) still evaluates to true AND if the original assoc has no parter yet)
if(this.v4 && originAssocCsn['@odata.navigable'] !== false && originAssocCsn._partnerCsn === undefined)
{
this.Partner = assocCsn.$Partner = originAssocCsn.name;
// set the Partner attribute to this (backlink) NavProp
this.Partner = originAssocCsn.name;
// link the two CSNs with each other
assocCsn._partnerCsn = originAssocCsn;
originAssocCsn._partnerCsn = assocCsn;
// if the other NavProp has been created already, set NavProp.Partner
// if not, Partner will be set during creation of other NavProp
if(originAssocCsn._NavigationProperty)
originAssocCsn._NavigationProperty.Partner = assocCsn.name;
// if not, Partner will be set during creation of other NavProp
originAssocCsn.$Partner = assocCsn.name;
//console.log('NavigationProperty "' + assocCsn._parent.name + '.' + assocCsn.name + '" wants to establish partnership with NavigationProperty "' + originAssocCsn._parent.name + '.' + originAssocCsn.name + '" which is already partnered with NavigationProperty "' + originAssocCsn._partnerCsn._parent.name + '.' + originAssocCsn._partnerCsn.name + '"');
}

@@ -1140,3 +1138,3 @@ }

*/
throw "Backlink association element is not an association or composition: " + originAssocCsn.name;
throw Error('Backlink association element is not an association or composition: "' + originAssocCsn.name);
}

@@ -1188,3 +1186,3 @@

if(!expr.some(isNotAConstraintTerm))
expr.map(fillConstraints)
expr.forEach(fillConstraints)

@@ -1198,3 +1196,3 @@ // return true if token is not one of '=', 'and', '(', ')' or object

return tok.some(isNotAConstraintTerm);
return !(typeof tok === 'object' && tok !== null || allowedTokens.includes(tok));
return !(typeof tok === 'object' && tok != null || allowedTokens.includes(tok));
}

@@ -1210,25 +1208,28 @@

let rhs = expr[pos+1];
if(arg === '=' && lhs.ref && rhs.ref)
if(['='].includes(arg))
{
lhs = lhs.ref;
rhs = rhs.ref;
// if exactly one operand starts with the prefix then this is potentially a constraint
if((lhs[0] === assocCsn.name && rhs[0] !== assocCsn.name) ||
(lhs[0] !== assocCsn.name && rhs[0] === assocCsn.name))
result.termCount++;
if(lhs.ref && rhs.ref) // ref is a path
{
// order is always [ property, referencedProperty ]
//backlink [ self, assocName ]
let c;
if(lhs[0] === assocCsn.name)
c = [ rhs[0], lhs[1] ];
else
c = [ lhs[0], rhs[1] ];
lhs = lhs.ref;
rhs = rhs.ref;
// if exactly one operand starts with the prefix then this is potentially a constraint
if((lhs[0] === assocCsn.name && rhs[0] !== assocCsn.name) ||
(lhs[0] !== assocCsn.name && rhs[0] === assocCsn.name))
{
// order is always [ property, referencedProperty ]
//backlink [ self, assocName ]
let c;
if(lhs[0] === assocCsn.name)
c = [rhs[0], lhs[1]];
else
c = [lhs[0], rhs[1]];
// do we have a $self or optionally a 'self' id?
// if so, store partner in selfs array
if(c[0] === NavigationProperty.DOLLAR_SELF ||
(NavigationProperty.OLDSTYLE_SELF && c[0] === NavigationProperty.OLDSTYLE_SELF))
result.selfs.push(c[1]);
else
result.constraints[c] = c;
// do we have a $self id?
// if so, store partner in selfs array
if(c[0] === NavigationProperty.DOLLAR_SELF)
result.selfs.push(c[1]);
else
result.constraints[c] = c;
}
}

@@ -1307,6 +1308,11 @@ }

class OnDelete extends Node {}
// Annotations below
class AnnotationBase extends Node
{
toJSON() // no $Kind
// No Kind: AnnotationBase is base class for Thing and ValueThing with dynamic kinds,
// this requires an explicit constructor as the kinds cannot be blacklisted in
// Node.toJSON()
toJSON()
{

@@ -1322,33 +1328,24 @@ let json = Object.create(null);

// short form: key: value
let inlineConstExpr = [ 'Bool', 'Float', 'String' ];
// if not inline, represented as object (exceptions apply (INF/NAN))
let constExpr = [ 'Binary', 'Bool', 'Date', 'DateTimeOffset',
'Decimal', 'Duration', 'EnumMember', 'EnumMember@odata.type', 'Float',
'Guid', 'Int', 'String', 'TimeOfDay',
'Path', 'AnnotationPath', 'ModelElementPath',
'NavigationPropertyPath', 'PropertyPath' ];
let inlineConstExpr =
[ 'Edm.Binary', 'Edm.Boolean', 'Edm.Byte', 'Edm.Date', 'Edm.DateTimeOffset', 'Edm.Decimal', 'Edm.Double', 'Edm.Duration', 'Edm.Guid',
'Edm.Int16', 'Edm.Int32', 'Edm.Int64', 'Edm.SByte','Edm.Single', /*'Edm.Stream',*/ 'Edm.String', 'Edm.TimeOfDay',
/* UI.xml: defines Annotations with generic type 'Edm.PrimitiveType' */
'Edm.PrimitiveType' ];
// call this for 'all' properties and for JSON only properties
// eihter 'all' props or JSON only props must be filled but not both!
let constExpr = [ ...inlineConstExpr,
'AnnotationPath', 'ModelElementPath', 'NavigationPropertyPath', 'PropertyPath', 'Path',
'EnumMember', 'EnumMember@odata.type' ];
let expr = glue.intersect(constExpr, Object.keys(this));
let jsonOnlyExpr = glue.intersect(constExpr, Object.keys(this._jsonOnlyAttributes))
if(expr.length + jsonOnlyExpr.length == 0)
throw "Please debug me: neither child nor constant expression found on annotation";
let expr = glue.intersect(constExpr, Object.keys(this._jsonOnlyAttributes))
if(expr.length > 0 && jsonOnlyExpr.length > 0)
throw "Please debug me: either expr or jsonOnlyExpr must be used but not together" + expr + " " + jsonOnlyExpr;
if(expr.length == 0)
throw Error('Please debug me: neither child nor constant expression found on annotation');
return addExpressions(expr, this._jsonOnlyAttributes);
if(expr.length > 0)
return addExpressions(expr, this);
if(jsonOnlyExpr.length > 0)
return addExpressions(jsonOnlyExpr, this._jsonOnlyAttributes);
return undefined;
function addExpressions(expr, dict)
{
let json = Object.create(null);
let inline = glue.intersect(expr, inlineConstExpr);
if(inline.length > 1)
throw Error('Please debug me: more than one inline constant expression found on annotation ' + inline.join(', '));
if(inline.length==1)

@@ -1359,16 +1356,14 @@ {

{
case 'Bool':
return (v=='true'?true:(v=='false'?false:v));
case 'Float':
if(v=='INF'||v=='-INF'||v=='NaN')
{
json['$Float'] = v;
return json;
}
else
return v;
case 'String':
/* short notation for Edm.Boolean, Edm.String and Edm.Float, see:
https://github.wdf.sap.corp/edmx2csn-npm/edm-converters/blob/835d92a1aa6b0be25c56cef85e260c9188187429/lib/edmxV40ToJsonV40/README.md
*/
case 'Edm.Boolean':
v = (v=='true'?true:(v=='false'?false:v));
// eslint-no-fallthrough
case 'Edm.String':
// eslint-no-fallthrough
case 'Edm.Float':
return v;
default:
throw "Please debug me: default not reachable";
return { '$Cast': v, '$Type': inline[0] };
}

@@ -1378,2 +1373,3 @@ }

{
let json = Object.create(null);
for(let i = 0; i < expr.length; i++)

@@ -1389,9 +1385,7 @@ json['$' + expr[i]] = dict[expr[i]]

{
static
create(v, target)
constructor(v, target)
{
let node = super.create(v, { Target: target });
if (node.v2)
node.setXml( { xmlns : "http://docs.oasis-open.org/odata/ns/edm" } );
return node;
super(v, { Target: target });
if (this.v2)
this.setXml( { xmlns : "http://docs.oasis-open.org/odata/ns/edm" } );
}

@@ -1427,7 +1421,5 @@

{
static
create(v, termName)
constructor(v, termName)
{
let node = super.create(v, { Term: termName } );
return node;
super(v, { Term: termName } );
}

@@ -1449,5 +1441,3 @@

{
let json = [];
this._children.forEach(a => json.push(a.toJSON()));
return json;
return this._children.map(a => a.toJSON());
}

@@ -1478,3 +1468,3 @@ }

default:
throw "Please debug me: default not reachable";
throw Error('Please debug me: default not reachable');
}

@@ -1488,7 +1478,6 @@ json[name] = a.toJSON()

{
static create(v, property)
constructor(v, property)
{
let node = super.create(v);
node.Property = property;
return node;
super(v);
this.Property = property;
}

@@ -1509,7 +1498,6 @@

{
static create(v, kind, details)
constructor(v, kind, details)
{
let node = super.create(v, details);
node.setKind(kind);
return node;
super(v, details);
this.setKind(kind);
}

@@ -1526,7 +1514,6 @@

{
static create(v, kind, value)
constructor(v, kind, value)
{
let node = super.create(v, kind);
node.set( { _value : value });
return node;
super(v, kind, undefined);
this.set( { _value : value });
}

@@ -1538,14 +1525,19 @@

let xml = indent + "<" + kind + this.toXMLattributes();
xml += (this._value ? ">" + escapeString(this._value) + "</" + kind + ">" : + "/>");
xml += (this._value !== undefined ? ">" + escapeString(this._value) + "</" + kind + ">" : "/>");
return xml;
function escapeString(s) {
function escapeString(s)
{
// first regex: replace & if not followed by apos; or quot; or gt; or lt; or amp; or #
return (typeof s === 'string') ? s.replace(/&(?!(?:apos|quot|[gl]t|amp);|#)/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;') : s;
return (typeof s === 'string') ? s.replace(/&(?!(?:apos|quot|[gl]t|amp);|#)/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;') : s;
}
}
toJSONattributes(json)
toJSON()
{
json['$'+this.kind] = this._value;
if(this._children.length == 0) // must be a constant expression
return this.getConstantExpressionValue();
else
// annotation must have exactly one child (=record or collection)
return this._children[0].toJSON();
}

@@ -1559,12 +1551,21 @@ }

{
static
create(v, details, f, t, m)
constructor(v, details, navProp, fromRole, toRole, multiplicity)
{
let node = super.create(v, details);
node.set( { _end: [] });
node._end.push(
End.create(v, { Role: f[0], Type: f[1], Multiplicity: m[0] } ),
End.create(v, { Role: t[0], Type: t[1], Multiplicity: m[1] } )
);
return node;
super(v, details);
this.set( { _end: [] });
this._end.push(
new End(v, { Role: fromRole[0], Type: fromRole[1], Multiplicity: multiplicity[0] } ),
new End(v, { Role: toRole[0], Type: toRole[1], Multiplicity: multiplicity[1] } ) );
// if eventually a backlink is a composition and this (Forward-)Association has not yet been
// produced, add the OnDelete property now to the source end.
// undefined values are not appended
this._end[0].append(navProp._OnDeleteSrcEnd);
// if the NavProp is a composition, add the OnDelete to the target end
if(navProp._csn.type == 'cds.Composition') {
// TODO: to be specified via @sap.on.delete
this._end[1].append(new OnDelete(v, { Action: 'Cascade' } ) );
}
}

@@ -1583,11 +1584,9 @@

{
static
create(v, details, fromRole, toRole, fromEntitySet, toEntitySet)
constructor(v, details, fromRole, toRole, fromEntitySet, toEntitySet)
{
let node = super.create(v, details);
node.append(
End.create(v, { Role: fromRole, EntitySet: fromEntitySet } ),
End.create(v, { Role: toRole, EntitySet: toEntitySet } )
super(v, details);
this.append(
new End(v, { Role: fromRole, EntitySet: fromEntitySet } ),
new End(v, { Role: toRole, EntitySet: toEntitySet } )
);
return node;
}

@@ -1598,12 +1597,13 @@ }

class Principal extends Node {}
ReferentialConstraint.createV2 =
function(v, from, to, c)
{
let node = ReferentialConstraint.create(v, {});
node.set({ _d: Dependent.create(v, { Role: from } ) });
node.set({ _p: Principal.create(v, { Role: to } ) });
let node = new ReferentialConstraint(v, {});
node.set({ _d: new Dependent(v, { Role: from } ) });
node.set({ _p: new Principal(v, { Role: to } ) });
glue.forAll(c, cv => {
node._d.append(PropertyRef.create(v, cv[0]));
node._p.append(PropertyRef.create(v, cv[1]));
node._d.append(new PropertyRef(v, cv[0]));
node._p.append(new PropertyRef(v, cv[1]));
});

@@ -1638,2 +1638,3 @@ return node;

ReferentialConstraint,
OnDelete,
// Annotations

@@ -1640,0 +1641,0 @@ Annotations,

'use strict';
/* eslint max-statements-per-line:off */
const { setProp } = require('../base/model');

@@ -8,3 +9,8 @@ function validateOptions(_options)

{
// csn2edm expects "version" to be a top-level property of options
// set to 'v4' as default, override with value from incoming options
// (here version comes inside "toOdata")
const options = Object.assign({ version: 'v4'}, _options);
if (options.toOdata && options.toOdata.version)
options.version = options.toOdata.version;

@@ -17,3 +23,3 @@ const v2 = options.version.match(/v2/i) != undefined;

if(options.v.filter(v=>v).length != 1)
throw `Please debug me: EDM V2:${v2}, V4:${v4}`
throw Error(`Please debug me: EDM V2:${v2}, V4:${v4}`);

@@ -126,3 +132,3 @@ options.isV2 = function() { return this.v[0] == true; }

if(options == undefined)
throw "Please debug me: initializeModel must be invoked with options"
throw Error('Please debug me: initializeModel must be invoked with options');

@@ -154,3 +160,2 @@ // make sure options are complete

foreach(model.definitions, isStructuredArtifact, initializeAssociation);
// Attach name to actions and their parameters
return service;

@@ -242,2 +247,3 @@

element.name = element.Name = elementName;
setProp(element, '_parent', struct);

@@ -255,12 +261,3 @@ // Collect keys

// create dictionary to hold attributes that should belong to the
// resulting EntitySet
let newAttributes = Object.create(null);
newAttributes['_EntitySetAttributes'] = {
value: Object.create(null),
configurable: true,
enumerable: false,
writable: true
}
Object.defineProperties(struct, newAttributes)
setProp(struct, '_EntitySetAttributes', Object.create(null));

@@ -349,3 +346,3 @@ appSpecificLateCsnTranformations.atStructure(options, struct);

// add the value list association as new element into struct.elements
struct.elements[assocName] = {
let assoc = {
name: assocName,

@@ -356,2 +353,4 @@ target: valueListEntity,

}
setProp(assoc, '_parent', struct);
struct.elements[assocName] = assoc;
}

@@ -384,5 +383,23 @@ }

'cds.UUID': 'Edm.Guid',
/* unused but EDM defined
Edm.Geography
Edm.GeographyPoint
Edm.GeographyLineString
Edm.GeographyPolygon
Edm.GeographyMultiPoint
Edm.GeographyMultiLineString
Edm.GeographyMultiPolygon
Edm.GeographyCollection
Edm.Geometry
Edm.GeometryPoint
Edm.GeometryLineString
Edm.GeometryPolygon
Edm.GeometryMultiPoint
Edm.GeometryMultiLineString
Edm.GeometryMultiPolygon
Edm.GeometryCollection
*/
}[cdsType];
if (edmType == undefined)
throw "No edm type found for " + cdsType;
throw Error('No edm type found for ' + cdsType);
if(isV2)

@@ -389,0 +406,0 @@ {

@@ -111,2 +111,3 @@ let W = require("./walker");

location(node[name], path.concat(name))
throw Error("augmentor: Queries not supported");
}

@@ -363,15 +364,2 @@

function modifySource(node, name, path) {
/* TODO uncomment and test
node.query ={
op:
{
val: 'query' },
from:
[ { path:
[ { id: node.source } ] } ],
all:
{ value: true },
elements: {}
}
*/
if(options && options.augmentor && options.augmentor.oldProjections) {

@@ -378,0 +366,0 @@ node.source = {absolute:node.source, path:[{id:node.source}]}

@@ -54,2 +54,4 @@ let W = require("./walker");

let le = U.getLastElement(path)
if(le==="$extra") // skip $extra from validator
return false;
if(le[0]==="@")

@@ -56,0 +58,0 @@ return false; // do not walk annotations

@@ -240,2 +240,9 @@ // contains query relevant augmentor functions

return W.dmap(args, (arg,obj) => {
if(obj.param === true)
return {
name: { id: arg, location: U.newLocation(path.concat(arg), U.WILO_FULL) },
path: refAsPath(obj.ref, path.concat(arg,"ref")),
scope: "param",
location: U.newLocation(path.concat(arg), U.WILO_FULL)
}
return newValue(obj.val, path.concat(arg), arg);

@@ -301,2 +308,6 @@ });

E.value = {path, location: U.newLocation(refPath, U.WILO_FULL)};
if(C.param)
E.value.scope="param";
if(C.global)
E.value.scope="global";
}

@@ -303,0 +314,0 @@ if(C.as!==undefined) {

@@ -227,3 +227,3 @@ let W = require("./walker");

if(node.returns.items!==undefined) {
U.setLocation(node.returns.items, path.concat(["returns","items"]));
augmentItems(path.concat(["returns","items"]), node.returns.items)
}

@@ -273,2 +273,4 @@ }

}
if(node.items)
augmentItems(path.concat("items"),node.items)
}

@@ -299,3 +301,3 @@

if(def.returns.items !== undefined) {
U.setLocation(def.returns.items, path.concat("returns","items"));
augmentItems(path.concat("returns","items"), def.returns.items);
}

@@ -302,0 +304,0 @@ }

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

let cloneWithTransformations = require('../base/model').cloneWithTransformations;
const { refString } = require('../base/messages');
const { queryOps } = require('../base/model');

@@ -35,3 +34,3 @@ const { mergeOptions } = require('../model/modelUtils');

blocks: ignore,
columns: ( c, node, r ) => { if (c[0] && c[0].val === '*') r.all = true; },
columns: ( c, node, r ) => { if (c && c[0] && c[0].val === '*') r.all = true; },
annotationAssignments: ignore, // original with structure values

@@ -50,3 +49,2 @@ kind: filterKind,

target: compactName,
source: compactName,
scope: ignore,

@@ -134,3 +132,3 @@ query: compactQuery,

// We always filter these
if (kind !== 'element' && kind !== 'key' && kind !== 'enum')
if (!['element', 'key', 'enum', 'annotate', '$tableAlias'].includes(kind))
return kind;

@@ -218,3 +216,5 @@ return undefined;

function compactQuery( query, node ) { // not for projections - TEMP?
function compactQuery( query, node, result ) { // not for projections - TEMP?
if (query.from && query.from.length === 1 && query.from[0] && query.from[0].path)
result.source = compactName( query.from[0] );
return (node.projection) ? undefined : compactCondOrExpr( query );

@@ -287,22 +287,2 @@ }

//check if environment variable VALIDATE_CSN is set and if so perform CSN validation
let VALIDATE_CSN = "VALIDATE_CSN" in process.env;
if(VALIDATE_CSN) {
let validateCSN = require("./schema/validateCSN.js");
let errors = validateCSN(newModel, {ajv:{useDefaults: false}});
if(errors.length>0) {
// persist the results in files if environment variable VALIDATE_CSN_PERSIST_ERROR is set
let VALIDATE_CSN_PERSIST_ERROR = "VALIDATE_CSN_PERSIST_ERROR" in process.env;
if(VALIDATE_CSN_PERSIST_ERROR) {
let fs = require("fs");
fs.writeFileSync("lasterror.json", JSON.stringify(newModel,null,2));
fs.writeFileSync("lasterror.txt", errors.toString());
throw Error("Invalid CSN: see lasterror.json and lasterror.txt");
} else {
throw Error("Invalid CSN:"
+ JSON.stringify(newModel,null,2)
+ errors);
}
}
}
return newModel;

@@ -357,2 +337,22 @@ }

// Return string for complete reference
function refString( name ) {
// prepare that resolvePath does not set ref.absolute etc:
if (name._artifact)
name = name._artifact;
if (name.name)
name = name.name;
let compact = '';
if (name.alias)
compact = '.$alias.' + name.alias;
if (name.action)
compact = '.$action.' + name.action;
if (name.param)
compact += '.$param.' + name.param;
if (name.element)
compact += (compact ? '.' : '..') + name.element;
// Yes, omit $query.0 -> test is (name.query), not (name.query != null)
return name.absolute +
(name.query ? '.$query.' + name.query : '') + compact;
}

@@ -359,0 +359,0 @@ module.exports = {

@@ -18,11 +18,13 @@ /**

// CSN validation
let validateCSN = require("./schema/validateCSN.js");
validateCSN(model, options);
let newCsn=require("./csnVersion").isNewCSN(model,options);
// augment CSN
if(newCsn) {
if(model.version && model.version.csn === "0.1.99" || options && options.newCsn)
// CSN validation
let validateCSN = require("./validator/validateCSN.js");
validateCSN(model, options);
augmentor3.augment(model);
else
} else
augmentor.augment(model , filename, options );

@@ -29,0 +31,0 @@ delete model["locations"];

@@ -40,5 +40,6 @@ let W = require("./walker");

function cbParam(/*O*/) {
function cbParam(O) {
return [
]
nullProto(O.elements, cbElement),
].concat(directItems(O.items))
}

@@ -51,3 +52,3 @@

nullProto(O.returns && O.returns.enum, cbEnum)
]
].concat(directItems(O.returns && O.returns.items))
}

@@ -64,2 +65,3 @@

].concat(directItems(O.items))
.concat(directItems(O.returns && O.returns.items))
}

@@ -66,0 +68,0 @@

@@ -8,78 +8,80 @@ // Transform augmented CSN into compact "official" CSN

// in main:
// const { compactModel: compactSortedJson } = require('./json/to-csn')
//var strict = false;
// dictionary:
// exclude
// namedArgs/arrowedArgs: insertOrderDict
// struct: insertOrderDict
var csn_gensrc = true; // good enough here...
var mode_strict = false; // whether to dump with unknown properties (in standard)
// IMPORTANT: the order of these properties determine the order of properties
// in the resulting CSN !!!
const transformers = {
// early and modifiers (without null / not null) -------------------------------------
kind,
name: ignore, // as is provided extra
id: n => n, // in path item
'@': value,
// definitions, extensions, members ----------------------------------------
definitions: sortedDict,
extensions: standard, // is array - TODO: sort
kind,
name: ignore,
actions: nonEmptyDict,
elements,
enum: insertOrderDict,
foreignKeys: renameTo( 'keys', dictAsArray ), // XSN: rename?
mixin: insertOrderDict, // only in queries with special handling
abstract: value,
dbType: value, // TODO: currently with --hana-flavor only
virtual: value,
key: value,
masked: value,
params: insertOrderDict,
returns: standard, // storing the return type of actions
// type properties ---------------------------------------------------------
cardinality: standard, // sub: src, min, max
includes: arrayOf( artifactRef ), // also entities
items: standard,
// early expression / query properties -------------------------------------
op: o => (o.val !== 'query') ? o.val : undefined,
from: fromOld, // before elements! XSN TODO just one (cross if necessary)
// join done in from()
// func // in expression()
quantifier: ( q, csn ) => { csn[ q.val ] = true; },
all: ignore, // XSN TODO use quantifier
// type properties (without 'elements') ------------------------------------
localized: value,
type: artifactRef,
length: value,
on: (cond) => (typeof cond === 'string' ? undefined : condition( cond )),
onCond : renameTo( 'on', condition ), // XSN TODO: onCond -> on
precision: value,
scale: value,
cardinality: standard, // also for pathItem: after 'id', before 'where'
target: artifactRef,
type: artifactRef,
sourceMax: renameTo( 'src', value ), // TODO XSN: rename?
targetMin: renameTo( 'min', value ),
targetMax: renameTo( 'max', value ),
// general properties of constructs ----------------------------------------
abstract: value,
dbType: value, // TODO: currently with --hana-flavor only
default: expression,
key: value,
localized: value,
masked: value,
notNull: value,
// targetElement: ignore, // special display of foreign key, renameTo: select
value: enumValue, // do not list for select items as elements
virtual: value,
// queries, expressions ----------------------------------------------------
query,
from: fromOld, // XSN TODO just one (cross if necessary)
quantifier: ( q, csn ) => { csn[ q.val ] = true; },
foreignKeys: renameTo( 'keys', dictAsArray ), // XSN: rename?
on: (cond) => (typeof cond === 'string' ? undefined : condition( cond )), // also for join
onCond : renameTo( 'on', condition ), // XSN TODO: onCond -> on
enum: insertOrderDict,
items: standard,
includes: arrayOf( artifactRef ), // also entities
// late expressions / query properties -------------------------------------
mixin: insertOrderDict, // only in queries with special handling
columns,
exclude: renameTo( 'excluding', Object.keys ), // XSN TODO: exclude->excluding
groupBy: arrayOf( expression ),
where: condition, // also pathItem after 'cardinality' before 'args'
having: condition,
limit, // TODO XSN: include offset
offset: ignore, // TODO XSN: move into `limit`
offset: ignore, // TODO XSN: move into `limit` - see limit
orderBy: arrayOf( orderBy ), // TODO XSN: make `sort` and `nulls` sibling properties
where: condition,
// special HANA CDS featues ------------------------------------------------
args, // also pathItem after 'where'
namedArgs: renameTo( 'args', args ), // XSN TODO - use args
// definitions, extensions, members ----------------------------------------
returns: standard, // storing the return type of actions
notNull: value,
default: expression,
// targetElement: ignore, // special display of foreign key, renameTo: select
value: enumValue, // do not list for select items as elements
query,
elements,
sequenceOptions: ignore, // TODO: currently not in the JSON by HANA
actions: nonEmptyDict,
technicalConfig, // TODO: spec, re-check
// Old-style XSN/CSN -------------------------------------------------------
indexNo: ignore, // TODO XSN: remove
origin: ignore, // remove (introduce non-enum _origin link)
projection: ignore, // later in entity: $syntax: 'projection'
source: ignore, // remove
// protected (non-public) --------------------------------------------------
//'_' not here, as non-enumerable properties are not transformed anyway
'$': ignore,
// special: top-level, cardinality -----------------------------------------
definitions: sortedDict,
extensions: standard, // is array - TODO: sort
messages: ignore, // consider compactQuery / compactExpr
sourceMax: renameTo( 'src', value ), // TODO XSN: rename?
targetMin: renameTo( 'min', value ),
targetMax: renameTo( 'max', value ),
// late protected ----------------------------------------------------------
viaTransform: standard, // FIXME: not a standard prop, start with $
generatedFieldName: renameTo( '$generatedFieldName', n => n ), // TODO: XSN name
$syntax: s => s,
_containerEntity: n => n, // FIXME: prop starting with _ is link and non-enumerable
_ignore: a => a, // not yet obsolete - still required by toHana (FIXME: maybe rename to $ignore, or use an annotation instead?)
_ignoreMasked: standard, // FIXME: prop starting with _ is link and non-enumerable
_isToContainer: standard, // FIXME: prop starting with _ is link and non-enumerable
$extra: (e, csn) => { Object.assign( csn, e ); },
// IGNORED -----------------------------------------------------------------
artifacts: ignore, // well-introduced, hence not $artifacts

@@ -91,18 +93,24 @@ location: ignore, // TODO: think about $location with flat struct (w/o offset)

typeArguments: ignore, // FIXME: make it $typeArgs
// protected - to be subsumed by $inferred ---------------------------------
calculated: ignore, // TODO remove ($inferred: 'as')
implicitForeignKeys: ignore, // later in assoc: $inferred: { foreignKeys: 'fk' } or $inferred on each fk
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: '*'
'$': ignore,
//'_' not here, as non-enumerable properties are not transformed anyway
_typeIsExplicit: ignore,
calculated: ignore, // later in name: $inferred: 'as'
implicitForeignKeys: ignore, // later in assoc: $inferred: { foreignKeys: 'fk' } or $inferred on each fk
redirected: ignore, // TODO: no need for this
viaAll: ignore, // TODO remove, later in elem: $inferred: '*'
// protected created by transformers ---------------------------------------
_containerEntity: n => n, // FIXME: prop starting with _ is link and non-enumerable
_ignore: a => a, // not yet obsolete - still required by toHana (FIXME: maybe rename to $ignore, or use an annotation instead?)
_ignoreMasked: standard, // FIXME: prop starting with _ is link and non-enumerable
_isToContainer: standard, // FIXME: prop starting with _ is link and non-enumerable
generatedFieldName: renameTo( '$generatedFieldName', n => n ), // TODO: XSN name
viaTransform: standard, // FIXME: not a standard prop, start with $
}
const typeProperties = [ // currently just for select items
const propertyOrder = (function () {
let r = {};
let i = 0;
for (let n in transformers)
r[n] = ++i;
return r;
})();
const typeProperties = [ // just for `cast` in select items
'type', 'length', 'precision', 'scale', 'items', 'target', 'elements', 'enum'

@@ -136,3 +144,12 @@ ];

if (model.version)
csn.version = model.version;
csn.version = model.version; // TODO remove
if(!options.testMode) {
setMetaProperty(csn, model);
setCsnVersion(csn);
}
// Use $extra properties of first source as resulting $extra properties
for (let f in model.sources) {
set( '$extra', csn, model.sources[f] );
break;
}
return csn;

@@ -206,9 +223,2 @@ }

function elements( dict, csn, node ) {
if (!csn_gensrc || !node.query && !node.type)
return insertOrderDict( dict );
else
return undefined;
}
function set( prop, csn, node ) {

@@ -223,2 +233,11 @@ let val = node[prop];

function elements( dict, csn, node ) {
if (csn.from) // with SELECT
return undefined;
if (!csn_gensrc || !node.query && !node.type)
return insertOrderDict( dict );
else
return undefined;
}
// for csn_gensrc: return annotations from definition (annotated==false)

@@ -289,5 +308,6 @@ // or annotations (annotated==true)

if (art.kind === 'key') { // foreignkey
let key = addExplicitAs( expression( art.targetElement ), art.name );
let key = addExplicitAs( expression( art.targetElement ),
art.name, neqPath( art.targetElement ) );
set( 'generatedFieldName', key, art );
return key;
return extra( key, art );
}

@@ -305,5 +325,5 @@ else

}
if (k === 'view') // XSN TODO: kind: 'entity', $syntax: 'view'
if (k === 'view') // XSN TODO: kind: 'entity'
return 'entity';
if (['element', 'key', 'enum', 'annotate'].includes(k))
if (['element', 'key', 'enum', 'annotate', 'query', '$tableAlias'].includes(k))
return undefined;

@@ -352,10 +372,6 @@ return k;

function pathItem( item ) {
if (!item.args && !item.namedArgs && !item.where && !item.cardinality)
if (!item.args && !item.namedArgs && !item.where && !item.cardinality && !item.$extra)
return item.id;
let r = { id: item.id };
if (item.args || item.namedArgs) // XSN TODO: namedArgs -> args
r.args = args( item.args || item.namedArgs );
set( 'cardinality', r, item );
set( 'where', r, item );
return r;
else
return standard( item );
}

@@ -372,2 +388,3 @@

// "Short" value form, e.g. for annotation assignments
function value( node ) {

@@ -379,9 +396,9 @@ if (!node)

if (node.path)
return { '=': node.path.map( id => id.id ).join('.') };
return extra( { '=': node.path.map( id => id.id ).join('.') }, node );
if (node.literal == 'enum')
return { "#" : node.symbol.id };
return extra( { "#" : node.symbol.id }, node );
if (node.literal == 'array')
return node.val.map( value );
if (node.literal != 'struct')
// no val (undefined( as true only for annotation values (and struct elem values)
// no val (undefined) as true only for annotation values (and struct elem values)
return node.name && !('val' in node) || node.val;

@@ -408,3 +425,4 @@ let r = Object.create( null );

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

@@ -421,5 +439,5 @@ return node;

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

@@ -429,17 +447,19 @@ if (node.path) {

if (node.path.length !== 1)
return { ref: node.path.map( pathItem ) };
return extra( { ref: node.path.map( pathItem ) }, en );
let item = pathItem( node.path[0] );
if (typeof item === 'string' && !node.path[0].quoted &&
// TODO: use _artifact if available
magicFunctions.includes( item.toUpperCase() )) {
return { func: item };
return extra( { func: item }, en );
}
return { ref: [item] };
return extra( { ref: [item] }, en );
}
if (node.literal) {
if (typeof node.val === node.literal || node.val === null)
return { val: node.val };
return extra( { val: node.val }, en );
else if (node.literal === 'enum')
return { "#" : node.symbol.id };
return extra( { "#" : node.symbol.id }, en );
else // TODO XSN: literal 'hex'->'x'
return { val: node.val, literal: (node.literal==='hex') ? 'x' : node.literal };
return extra( { val: node.val, literal: (node.literal==='hex') ? 'x' : node.literal },
en );
}

@@ -450,8 +470,11 @@ if (node.func) { // TODO XSN: remove op: 'call', func is no path

call.args = args( node.args || node.namedArgs );
return call;
return extra( call, en );
}
if (queryOps[ node.op.val ])
return query( node );
else // do not use xpr() for xpr, as it would flatten inner xpr's (semantically ok)
return { xpr: (node.op.val === 'xpr') ? node.args.map( expression ) : xpr( node ) };
else if (node.op.val === 'xpr')
// do not use xpr() for xpr, as it would flatten inner xpr's (semantically ok)
return extra( { xpr: node.args.map( expression ) }, node );
else // other ops have no $extra
return { xpr: xpr( node ) };
}

@@ -487,9 +510,10 @@

function query( node ) {
// TODO: add "inferred" elements for leading query
while (node instanceof Array) // in parentheses -> remove
node = node[0];
if (node.op.val === 'query')
return { SELECT: standard( node ) };
let csn = {};
// for UNION, ... ----------------------------------------------------------
if (!['query', 'subquery'].includes( node.op.val )) {
if (node.op.val !== 'unionAll') // CSN TODO: quantifier: 'all'|'distinct'
if (node.op.val !== 'subquery') {
if (node.op.val !== 'unionAll') // XSN TODO: quantifier: 'all'|'distinct'
csn.op = node.op.val;

@@ -510,15 +534,6 @@ else

}
// for SELECT --------------------------------------------------------------
set( 'from', csn, node );
set( 'mixin', csn, node );
set( 'columns', csn, node );
set( 'quantifier', csn, node );
set( 'exclude', csn, node ); // XSN TODO: exclude->excluding
set( 'where', csn, node );
set( 'groupBy', csn, node );
set( 'having', csn, node );
// for both ----------------------------------------------------------------
set( 'orderBy', csn, node );
set( 'limit', csn, node );
return (node.op.val === 'query') ? { SELECT: csn } : { SET: csn };
set( 'limit', csn, node ); // TODO XSN: also offset
set( '$extra', csn, node );
return { SET: csn };
}

@@ -558,3 +573,3 @@

// TODO: CSN: FROM ((SELECT...)) as -> also add 'subquery' op? - Together
// with []-elimination in FROM...
// with []-elimination in FROM... -> normal standard()
if (node.join) { // XSN TODO: remove '…Outer'

@@ -568,16 +583,16 @@ // binary (without additions) -> n-ary - the while loop should be done in

set( 'on', join, node );
return join;
return extra( join, node );
}
else if (!node.path) {
return addExplicitAs( query( node ), node.name );
return addExplicitAs( query( node ), node.name ); // $extra inside SELECT/SET
}
else if (!node._artifact || node._artifact.main) {
return addExplicitAs( artifactRef( node, null ), node.name );
return extra( addExplicitAs( artifactRef( node, null ), node.name ), node );
}
else
return addExplicitAs( artifactRef( node, null ), node.name, function(id) {
return extra( addExplicitAs( artifactRef( node, null ), node.name, function(id) {
let name = node._artifact.name.absolute;
let dot = name.lastIndexOf('.');
return name.substring( dot+1 ) !== id;
});
}), node );
}

@@ -595,11 +610,7 @@

csn_gensrc = true;
addExplicitAs( Object.assign( col, expression(elem.value) ), elem.name, function(id) {
// $user should be rendered as { ref: ['$user','id'], as: '$user' }
let path = elem.value && elem.value.path;
let last = path[ path.length-1 ];
return (last && last.id) !== id;
});
set( 'key', col, elem );
addExplicitAs( Object.assign( col, expression(elem.value) ),
elem.name, neqPath( elem.value ) );
if (elem._typeIsExplicit || elem.redirected) { // TODO XSN: introduce $inferred
col.cast = {};
col.cast = {}; // TODO: what about $extra in cast?
for (let prop of typeProperties)

@@ -616,6 +627,6 @@ set( prop, col.cast, elem );

}
columns.push( col );
columns.push( extra( col, elem ) );
}
function orderBy( node ) { // TODO XSN: flatten (no extra 'value')
function orderBy( node ) { // TODO XSN: flatten (no extra 'value'), part of expression
let expr = expression( node.value );

@@ -626,6 +637,6 @@ if (node.sort)

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

@@ -637,2 +648,13 @@ return (node.offset)

function $extra( obj, csn ) {
for (let prop of Object.keys( obj ).sort())
csn[prop] = obj[prop];
}
function extra( csn, node ) {
if (node && node.$extra)
$extra( node.$extra, csn );
return csn;
}
function addExplicitAs( node, name, implicit ) {

@@ -644,5 +666,9 @@ if (name && (!name.calculated && !name.$inferred || implicit && implicit(name.id) ))

// normalize CSN: sort properties alphabetically if no prototype (also sort model.definitions)
// for "niceness", put the following properties first: op, kind, name
const earlyProperties = { op: '\x01', kind: '\x02', name: '\x03' };
function neqPath( ref ) {
let path = ref && ref.path;
return path && function( id ) {
let last = path[ path.length-1 ];
return (last && last.id) !== id;
};
}

@@ -652,6 +678,5 @@ function compareProperties( a, b ) {

return 0;
else if ((earlyProperties[a] || a) < (earlyProperties[b] || b))
return -1;
else
return 1;
let oa = propertyOrder[a] || propertyOrder[a.charAt()];
let ob = propertyOrder[b] || propertyOrder[b.charAt()];
return oa - ob || (a < b ? -1 : 1);
}

@@ -938,3 +963,16 @@

function getCompilerVersion() {
return require('../../package.json').version;
}
function setMetaProperty( csn, model ) {
csn.meta = model.meta || {};
csn.meta.creator = 'CDS Compiler v' + getCompilerVersion();
}
// 0.2 - pre-release version
function setCsnVersion( csn ) {
csn.$version = "0.2"
}
module.exports = { compactModel, compactQuery, compactExpr };

@@ -486,3 +486,3 @@ /**

walkNodesExFn,
walkNodesExFnPath,
walkNodesExFnPath,
walkWithPath,

@@ -489,0 +489,0 @@ walkNodesExWithPath,

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

case ATNState.STAR_LOOP_BACK:
// TODO: do not delete a '}'
this.reportUnwantedToken(recognizer);

@@ -212,3 +213,3 @@ var expecting = new IntervalSet.IntervalSet();

'Error', 'Extraneous $(OFFENDING), expecting $(EXPECTING)' );
err.expectedTokens = expecting;
err.expectedTokens = expecting; // TODO: remove next token?
if (!recognizer.avoidErrorListeners) // with --trace-parser or --trace-parser-ambig

@@ -327,4 +328,4 @@ recognizer.notifyErrorListeners( err.message, token, err );

}
if (recognizer.$nextTokensToken === recognizer.$removeSemiFor)
names = names.filter( n => n !== "';'" && n !== "'}'" );
if (recognizer.$adaptExpectedToken && recognizer.$nextTokensToken === recognizer.$adaptExpectedToken)
names = names.filter( n => !recognizer.$adaptExpectedExcludes.includes( n ) );
else if (names.includes("';'"))

@@ -389,2 +390,3 @@ names = names.filter( n => n !== "'}'" );

var identType = recognizer.constructor.Identifier;
var hideAltsType = recognizer.constructor.HideAlternatives;
var beforeUnreserved = recognizer.constructor.Number;

@@ -397,3 +399,5 @@ if (!identType || !beforeUnreserved || beforeUnreserved + 2 > identType)

var orig_addInterval = expected.addInterval;
var orig_addSet = expected.addSet;
expected.addInterval = addInterval;
expected.addSet = addSet;
let lookBusy = new antlr4.Utils.Set();

@@ -424,2 +428,7 @@ let calledRules = new antlr4.Utils.BitSet();

function addSet(other) {
if (!other.contains( hideAltsType ))
orig_addSet.call( this, other );
}
// Add an interval `v` to the IntervalSet `this`. If `v` contains the token

@@ -426,0 +435,0 @@ // type `Identifier`, do not add non-reserved keywords in `v`.

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

setOnce,
setMaxCardinality,
hanaFlavorOnly,

@@ -52,2 +53,3 @@ csnParseOnly,

noSemicolonHere,
excludeExpected,
isStraightBefore,

@@ -96,3 +98,3 @@ constructor: GenericAntlrParser // keep this last

(loc instanceof antlr4.CommonToken) ? this.tokenLocation(loc) : loc,
...args );
null, ...args );
}

@@ -132,3 +134,4 @@

var t = this.getCurrentToken();
this.$removeSemiFor = t;
this.$adaptExpectedToken = t;
this.$adaptExpectedExcludes = ["';'", "'}'"];
this.$nextTokensToken = t;

@@ -142,2 +145,11 @@ this.$nextTokensContext = null; // match() of WITH does not reset

function excludeExpected( excludes ) {
if (excludes) {
var t = this.getCurrentToken();
this.$adaptExpectedToken = t;
this.$adaptExpectedExcludes = (excludes instanceof Array) ? excludes : [excludes];
this.$nextTokensToken = t;
}
}
// // Special function for rule `requiredSemi` before return $ctx

@@ -235,3 +247,4 @@ // function braceForSemi() {

// identifer (TODO: check for dots etc/ here).
function identAst( token ) {
function identAst( token, category ) {
token.isIdentifier = category;
var id = token.text;

@@ -448,5 +461,16 @@ if (token.type !== this.constructor.Identifier && !/^[a-zA-Z]+$/.test( id ))

function setMaxCardinality( art, token, max ) {
let location = this.tokenLocation( token );
if (art.cardinality) {
this.message( 'syntax-repeated-cardinality', location, { token: token.text },
'Warning', 'The target cardinality has already been specified - ignored $(TOKEN)' );
}
else {
art.cardinality = { targetMax: Object.assign( {location}, max ), location };
}
}
module.exports = {
genericAntlrParser: GenericAntlrParser
};

@@ -35,2 +35,4 @@ // Main entry point for the Research Vanilla CDS Compiler

// 0.1.99 : Like 0.1.0, but with new-style CSN
// 0.2 : same as 0.1.99, but with new top-level properties: $version, meta
// TODO: move csn versioning in to-csn.js
function csnVersion( options ) {

@@ -40,3 +42,3 @@ // Merge default options

// New-style CSN vs old-style CSN
return options.newCsn ? '0.1.99' : '0.1.0';
return options.newCsn ? '0.2' : '0.1.0';
}

@@ -59,3 +61,2 @@

var fs = require('fs');
var { compactForService } = require('./transform/forOdata');
var { getDefaultTntFlavorOptions, propagateIncludesForTnt } = require('./transform/tntSpecific');

@@ -84,5 +85,2 @@ var csn2edm = require('./edm/csn2edm');

model.$frontend = 'json';
// TODO: I (CW) do not think that the following is a good idea...
if (model.version && model.version.csn === "0.1.99")
options.newCsn = true; // force new version TODO optimize?
return model;

@@ -95,3 +93,3 @@ } else if (options.fallbackParser || ['.cds', '.hdbcds', '.hdbdd'].includes(ext))

message( 'file-unknown-ext',
{ filename, start: { offset: 0, line: 1, column: 1 } },
{ filename, start: { offset: 0, line: 1, column: 1 } }, null,
{ file: ext && ext.slice(1), '#': !ext && 'none' },

@@ -374,3 +372,3 @@ 'Error', {

for (let from of dep.usingFroms)
message( 'file-not-readable', from.location, { file: resolved },
message( 'file-not-readable', from.location, null, { file: resolved },
'Error', 'Cannot read file $(FILE)' );

@@ -380,3 +378,3 @@ }

for (let from of dep.usingFroms)
message( 'file-unknown-local', from.location, { file: dep.module },
message( 'file-unknown-local', from.location, null, { file: dep.module },
'Error', 'Cannot find local module $(FILE)' );

@@ -387,3 +385,3 @@ }

for (let from of dep.usingFroms)
message( 'file-unknown-package', from.location,
message( 'file-unknown-package', from.location, null,
{ file: dep.module, '#': internal },

@@ -475,18 +473,11 @@ 'Error', {

var model = { sources, options, messages: [].concat( ...messagesArray ) }; // flatten
if (!options.testMode)
model.version = versionObject( options );
if (!options.parseOnly)
model = resolve( define(model) );
if (options.reAugmented) { // for testing
let filename = Object.keys(sources)[0];
let source = options.newCsn ? compactModel( model, options ) : compactJson( model, options );
let parseCSNfromJson = require("./json/from-json");
source = parseCSNfromJson( JSON.stringify(source), filename, options );
source.$frontend = 'json';
model = { sources: Object.create(null), options, messages: [] };
model.sources[filename] = source;
if (options.reAugmented === 'recompile')
model = resolve( define(model) );
if (!options.testMode) {
model.version = versionObject( options );//TODO remove
model.meta = {}; // provide initial central meta object
}
// if (!options.parseOnly) return define(model);
if (!options.parseOnly) {
define(model);
resolve(model);
}

@@ -545,2 +536,16 @@ assertConsistency( model );

backends.optionProcessor.command('toTntSpecificOutput')
.option('-h, --help')
.help(`
Usage: cdsc toTntSpecificOutput [options] <file...>
(internal, subject to change): Generate backward-compatible output for the TNT project. Should
ultimately be replaced by "cdsc toOdata --version v2 --xml --separate --csn".
Options
-h, --help Display this help text
`)
;
// TNT-specific, temporary: Transforms augmented CSN 'model' into an object '{ annotations, metadata, csn, services }'

@@ -594,5 +599,7 @@ // containing

// (unfortunately we used to deliver this really as a V4 version, which was probably unnecessary ...)
// Compact the model
let compactedModel = compactModel(odataResult._augmentedCsn);
setProp(compactedModel, 'messages', odataResult._augmentedCsn.messages);
for (let serviceName in result.services) {
let forOdata = compactForService(odataResult._augmentedCsn, serviceName);
let l_annotations_edm = csn2edm(forOdata, mergeOptions(options, { toOdata : { version : 'v4' }}));
let l_annotations_edm = csn2edm(compactedModel, serviceName, mergeOptions(options, { toOdata : { version : 'v4' }}));
result.services[serviceName].annotations = l_annotations_edm.toXML('annotations');

@@ -614,5 +621,4 @@ }

// Return the 'version' object that should appear in CSNs generated by this compiler.
function versionObject( options ) {
function versionObject( options ) { // TODO remove
return {
creator: 'CDS Compiler v' + version(),
csn: csnVersion( options ),

@@ -672,2 +678,4 @@ }

toOdata : backends.toOdata,
preparedCsnToEdmx : backends.preparedCsnToEdmx,
preparedCsnToEdm : backends.preparedCsnToEdm,
toCdl : backends.toCdl,

@@ -674,0 +682,0 @@ toSwagger,

@@ -5,15 +5,7 @@ 'use strict'

// Return true if 'node' is a projection entity
function isProjection(node) {
// FIXME: Looking at 'source' is probably not correct here?
return node.kind == 'entity' && node.source != undefined;
}
// TODO: Are these functions written with derived types in mind (or even that
// an element definition uses an association type)? Some are, others not...
// Return true if 'node' is a view entity
function isView(node) {
// FIXME: Do we really have to consider 'kind' or is it sufficient to look at 'queries'?
return node.kind == 'view' && node.queries != undefined;
}
// Return true if 'node is a managed association element
// TODO: what about elements having a type, which (finally) is a assoc?
function isManagedAssociationElement(node) {

@@ -411,4 +403,2 @@ return node.kind == 'element' && node.target != undefined && node.onCond == undefined;

module.exports = {
isProjection,
isView,
isManagedAssociationElement,

@@ -415,0 +405,0 @@ isAssociation,

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

const keywords = require('../base/keywords');
const version = require('../../package.json').version;

@@ -50,3 +51,4 @@ // Render the CSN model 'model' to CDS source text. One source is created per

result[plainNames ? uppercaseAndUnderscore(artifactName) : artifactName]
= renderNamespaceDeclaration(artifactName, env) + renderUsings(artifactName, env) + sourceStr;
= `${options.testMode ? '' : `// generated by cds-compiler version ${version} \n`}`
+ renderNamespaceDeclaration(artifactName, env) + renderUsings(artifactName, env) + sourceStr;
}

@@ -136,3 +138,3 @@ }

// somewhat difficult because this kind of absolute path is quite unusual). In order not to have to pass
// the current artifact name down through the stack to renderExpr, we just put it nto the env.
// the current artifact name down through the stack to renderExpr, we just put it into the env.
env.currentArtifactName = artifactName;

@@ -461,3 +463,3 @@ if (art.query) {

if (source.SELECT || source.SET) {
let result = `(${renderQuery(source, false, increaseIndent(env))})`;
let result = `(${renderQuery(source, false, 'view', increaseIndent(env))})`;
if (source.as) {

@@ -575,10 +577,20 @@ result += ` as ${quoteId(source.as)}`;

// Render a view
// Render a view. If '$syntax' is set (to 'projection', 'view', 'entity'),
// the view query is rendered in the requested syntax style, otherwise it
// is rendered as a view.
function renderView(artifactName, art, env) {
let syntax = art.$syntax || 'view';
let result = renderAnnotationAssignments(art, env);
result += env.indent + 'view ' + renderArtifactName(artifactName, env);
result += `${env.indent}${syntax == 'projection' ? 'entity' : syntax} ${renderArtifactName(artifactName, env)}`;
if (art.params) {
let childEnv = increaseIndent(env);
result += ' with parameters\n' + Object.keys(art.params).map(name => renderParameter(name, art.params[name], childEnv)).join(',\n') + '\n';
result += env.indent + 'as ';
let parameters = Object.keys(art.params).map(name => renderParameter(name, art.params[name], childEnv)).join(',\n');
// HANA only understands the 'with parameters' syntax'
if (options.forHana) {
result += ` with parameters\n${parameters}\n${env.indent}as `;
}
// Otherwise prefer simple parentheses
else {
result += `(\n${parameters}\n${env.indent}) as `;
}
}

@@ -588,3 +600,3 @@ else {

}
result += renderQuery(art.query, true, env);
result += renderQuery(art.query, true, syntax, env);
result += ';\n';

@@ -598,4 +610,5 @@ result += renderQueryElementAnnotations(artifactName, art, env);

// If 'isLeadingQuery' is true, mixins, actions and functions of 'art' are
// also rendered into the query.
function renderQuery(query, isLeadingQuery, env) {
// also rendered into the query. Use 'syntax'style ('projection', 'view',
// or 'entity')
function renderQuery(query, isLeadingQuery, syntax, env) {
let result = '';

@@ -605,6 +618,6 @@ // Set operator, like UNION, INTERSECT, ...

// First arg may be leading query
result += `(${renderQuery(query.SET.args[0], isLeadingQuery, env)}`
result += `(${renderQuery(query.SET.args[0], isLeadingQuery, 'view', env)}`
// FIXME: Clarify if set operators can be n-ary (assuming binary here)
if (query.SET.op) {
result += `\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} ${renderQuery(query.SET.args[1], false, env)}`;
result += `\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} ${renderQuery(query.SET.args[1], false, 'view', env)}`;
}

@@ -628,3 +641,9 @@ result += ')';

let childEnv = increaseIndent(env);
result += `select from ${renderViewSource(select.from, env)}`;
if (syntax == 'projection') {
result += `projection on ${renderViewSource(select.from, env)}`;
} else if (syntax == 'view' || syntax == 'entity') {
result += `select from ${renderViewSource(select.from, env)}`;
} else {
throw new Error(`Unknown query syntax: ${syntax}`);
}
if (isLeadingQuery && select.mixin) {

@@ -820,3 +839,7 @@ result += ' mixin {\n'

// For HANA CDS, we need a 'type of'
return (options.forHana ? 'type of ' : '') + renderAbsolutePath(elm.type, env);
result += (options.forHana ? 'type of ' : '') + renderAbsolutePath(elm.type, env);
if (elm.enum) {
result += renderEnum(elm.enum, env);
}
return result;
}

@@ -835,11 +858,3 @@

if (elm.enum) {
result += ' enum {\n';
let childEnv = increaseIndent(env);
for (let name in elm.enum) {
let enumConst = elm.enum[name];
result += renderAnnotationAssignments(enumConst, childEnv);
let enumValue = { val: enumConst.val === undefined ? name : enumConst.val };
result += childEnv.indent + quoteId(name) + ' = ' + renderExpr(enumValue, childEnv) + ';\n';
}
result += env.indent + '}';
result += renderEnum(elm.enum, env);
}

@@ -849,2 +864,16 @@ return result;

// Render the 'enum { ... } part of a type declaration
function renderEnum(enumPart, env) {
let result = ' enum {\n';
let childEnv = increaseIndent(env);
for (let name in enumPart) {
let enumConst = enumPart[name];
result += renderAnnotationAssignments(enumConst, childEnv);
let enumValue = { val: enumConst.val === undefined ? name : enumConst.val };
result += childEnv.indent + quoteId(name) + ' = ' + renderExpr(enumValue, childEnv) + ';\n';
}
result += env.indent + '}';
return result;
}
// Render an annotation value (somewhat like a simplified expression, with slightly different

@@ -947,7 +976,7 @@ // representation)

// renderQuery for SELECT does not bring its own parentheses (because it is also used in renderView)
return `(${renderQuery(x, false, increaseIndent(env))})`;
return `(${renderQuery(x, false, 'view', increaseIndent(env))})`;
}
else if (x.SET) {
// renderQuery for SET always brings its own parentheses (because it is also used in renderViewSource)
return `${renderQuery(x, false, increaseIndent(env))}`;
return `${renderQuery(x, false, 'view', increaseIndent(env))}`;
}

@@ -991,4 +1020,3 @@ else {

&& (['$projection', '$self', '$parameters'].includes(s)
|| getMagicVariables().map(id => id.toLowerCase()).includes(s.toLowerCase())
|| s == 'self' && options.oldstyleSelf)) {
|| getMagicVariables().map(id => id.toLowerCase()).includes(s.toLowerCase()))) {
return s;

@@ -1332,3 +1360,2 @@ }

if (id == '$projection' || id == '$self' ||
id == 'self' && options.oldstyleSelf ||
id == '$user' || id == '$now') {

@@ -1335,0 +1362,0 @@ return id;

@@ -5,3 +5,4 @@

const { CompilationError, hasErrors, sortMessages } = require('../base/messages');
const { mergeOptions, isAssociation, getTopLevelArtifactNameOf, getParentNameOf } = require('../model/modelUtils');
const { mergeOptions, getTopLevelArtifactNameOf, getParentNameOf, getParentNamesOf } = require('../model/modelUtils');
const { compactModel } = require('../json/to-csn');

@@ -29,5 +30,20 @@ // Render the augmented CSN 'model' to SQL DDL statements renaming existing tables and their

// 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',
};
}
}
}
// Render each artifact on its own
for (let artifactName in model.definitions) {
let sourceStr = renameTableAndColumns(model.definitions[artifactName]);
for (let artifactName in csn.definitions) {
let sourceStr = renameTableAndColumns(artifactName, csn.definitions[artifactName]);

@@ -49,7 +65,7 @@ if (sourceStr != '') {

// Do not rename anything if the names are identical.
function renameTableAndColumns(art) {
function renameTableAndColumns(artifactName, art) {
let resultStr = '';
if (art.kind == 'entity' && !art.source) {
let beforeTableName = quoteSqlId(absoluteCdsName(art.name.absolute));
let afterTableName = plainSqlId(art.name.absolute);
if (art.kind == 'entity' && !art.query) {
let beforeTableName = quoteSqlId(absoluteCdsName(artifactName));
let afterTableName = plainSqlId(artifactName);

@@ -64,7 +80,7 @@ if (beforeTableName != afterTableName) {

let beforeColumnName = quoteSqlId(e.name.id);
let afterColumnName = plainSqlId(e.name.id);
let beforeColumnName = quoteSqlId(name);
let afterColumnName = plainSqlId(name);
if (!e._ignore) {
if (isAssociation(e._finalType.type)) {
if (e.target) {
result = 'ALTER TABLE ' + afterTableName + ' DROP ASSOCIATION ' + beforeColumnName + ';\n'

@@ -89,3 +105,3 @@ }

}
let topLevelName = getTopLevelArtifactNameOf(name, model);
let topLevelName = getTopLevelArtifactNameOf(name, csn);
let namespaceName = getParentNameOf(topLevelName);

@@ -116,2 +132,2 @@ if (namespaceName) {

toRenameDdl,
};
};

@@ -5,6 +5,7 @@

const { CompilationError, hasErrors, sortMessages } = require('../base/messages');
const { isAssociation, getTopLevelArtifactNameOf, getParentNameOf } = require('../model/modelUtils');
const renderUtils = require('./renderUtils');
const { getTopLevelArtifactNameOf, getParentNameOf, getParentNamesOf, getLastPartOf, getLastPartOfRef } = require('../model/modelUtils');
const keywords = require('../base/keywords');
const alerts = require('../base/alerts');
const { compactModel } = require('../json/to-csn');
const version = require('../../package.json').version;

@@ -28,3 +29,3 @@ // Render the CSN model 'model' to SQL DDL statements. One statement is created

function toSqlDdl(model) {
const { error, signal } = alerts(model);
const { error, signal, warning, info } = alerts(model);

@@ -34,9 +35,2 @@ // Use model options

const { renderExpressionOrCondition, renderJoinOp } = renderUtils.getRenderUtils(model, options, {
renderPathOrValue,
renderQuery,
increaseIndent,
decreaseIndent,
});
// FIXME: Currently requires 'options.forHana', because it can only render HANA-ish SQL dialect

@@ -47,2 +41,17 @@ if (!options.forHana) {

// 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',
};
}
}
}
// The final result in hdb-kind-specific form, without leading CREATE, without trailing newlines

@@ -59,6 +68,3 @@ // (note that the order here is relevant for transmission into 'resultObj.sql' below)

// Render each artifact on its own
let artifactNames = Object.keys(model.definitions);
// Note: Sorting here just to minimize the diff with the new version based on new-style compact CSN, which is always sorted
artifactNames.sort();
for (let artifactName of artifactNames) {
for (let artifactName in csn.definitions) {
// This environment is passed down the call hierarchy, for dealing with

@@ -70,4 +76,5 @@ // indentation issues

}
renderArtifactInto(model.definitions[artifactName], resultObj, env);
renderArtifactInto(artifactName, csn.definitions[artifactName], resultObj, env);
}
// Throw up if we have errors

@@ -82,2 +89,3 @@ if (hasErrors(model.messages)) {

let sql = Object.create(null);
let sqlVersionLine = `-- generated by cds-compiler version ${version}\n`;
for (let hdbKind of Object.keys(resultObj)) {

@@ -90,11 +98,15 @@ for (let name in resultObj[hdbKind]) {

}
sql[name] = `CREATE ${sourceString};`;
sql[name] = `${options.testMode ? '' : sqlVersionLine}CREATE ${sourceString};`;
if (!options.testMode) {
resultObj[hdbKind][name] = sqlVersionLine + resultObj[hdbKind][name];
}
}
}
resultObj.sql = sql;
return resultObj;
// Render an artifact into the appropriate dictionary of 'resultObj'.
function renderArtifactInto(art, resultObj, env) {
function renderArtifactInto(artifactName, art, resultObj, env) {
// Ignore whole artifacts if forHana says so

@@ -106,25 +118,16 @@ if (art._ignore) {

case 'entity':
if (art.source) {
let result = renderProjection(art, env);
case 'view':
if (art.query) {
let result = renderView(artifactName, art, env);
if (result) {
resultObj.hdbview[art.name.absolute] = result;
resultObj.hdbview[artifactName] = result;
}
} else {
let result = renderEntityInto(art, resultObj, env);
if (result) {
resultObj.hdbentity[art.name.absolute] = result;
}
renderEntityInto(artifactName, art, resultObj, env);
}
break;
case 'view': {
let result = renderView(art, env);
if (result) {
resultObj.hdbview[art.name.absolute] = result;
}
break;
}
case 'type': {
let result = renderType(art, env);
let result = renderType(artifactName, art, env);
if (result) {
resultObj.hdbtabletype[art.name.absolute] = result;
resultObj.hdbview[artifactName] = result;
}

@@ -148,23 +151,23 @@ break;

// dictionaries of 'resultObj'.
function renderEntityInto(art, resultObj, env) {
// FIXME: Took this from toCdl, but do entities have parameters yet at all? Views apparently don't ... I am confused
if (art.params && Object.keys(art.params)[0]) {
let firstParam = art.params[Object.keys(art.params)[0]];
signal(error`"${art.name.absolute}": Entities with parameters are not supported for conversion to SQL`, firstParam.location);
}
function renderEntityInto(artifactName, art, resultObj, env) {
let childEnv = increaseIndent(env);
let tc = art.technicalConfig;
let storeType = (tc && tc.storeType) ? renderPathOrValue(tc.storeType) + ' ': '';
// in 'hdbtable' files, COLUMN or ROW is mandatory, and COLUMN is the default
if (options.toSql.dialect == 'hana' && storeType == '') {
storeType += 'COLUMN ';
let hanaTc = art.technicalConfig && art.technicalConfig.hana;
let result = '';
// Only HANA has row/column tables
if (options.toSql.dialect == 'hana') {
if (hanaTc && hanaTc.storeType) {
// Explicitly specified
result += art.technicalConfig.hana.storeType.toUpperCase() + ' ';
}
else if (options.toSql.dialect == 'hana') {
// in 'hdbtable' files, COLUMN or ROW is mandatory, and COLUMN is the default
result += 'COLUMN ';
}
}
let result = storeType + 'TABLE ' + quoteSqlId(absoluteCdsName(art.name.absolute));
result += 'TABLE ' + quoteSqlId(absoluteCdsName(artifactName));
result += ' (\n';
result += Object.keys(art.elements).map(name => renderElement(art.elements[name], childEnv))
result += Object.keys(art.elements).map(name => renderElement(artifactName, name, art.elements[name], getFzIndex(name, hanaTc), childEnv))
.filter(s => s != '')
.join(',\n');
let primaryKeys = Object.keys(art.elements).filter(name => art.elements[name].key && art.elements[name].key.val)
let primaryKeys = Object.keys(art.elements).filter(name => art.elements[name].key)
.filter(name => !art.elements[name]._ignore)

@@ -178,5 +181,9 @@ .map(name => quoteSqlId(name))

}
result += env.indent + ')' + renderTechnicalConfiguration(tc, childEnv);
result += env.indent + ')';
let associations = Object.keys(art.elements).map(name => renderAssociationElement(art.elements[name], childEnv))
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 != '')

@@ -188,34 +195,45 @@ .join(',\n');

}
// Only HANA has indices
// FIXME: Really? We should provide a DB-agnostic way to specify that
if (options.toSql.dialect === 'hana') {
renderIndexesInto(art.technicalConfig && art.technicalConfig.hana.indexes, artifactName, resultObj, env);
}
resultObj.hdbtable[artifactName] = result;
}
result += renderIndexesInto(tc, art.name.absolute, resultObj, childEnv);
resultObj.hdbtable[art.name.absolute] = result;
// Retrieve the 'fzindex' (fuzzy index) property (if any) for element 'elemName' from hanaTc (if defined)
function getFzIndex(elemName, hanaTc) {
if (!hanaTc || !hanaTc.fzindexes || !hanaTc.fzindexes[elemName]) {
return undefined;
}
if (hanaTc.fzindexes[elemName][0] instanceof Array) {
// FIXME: Should we allow multiple fuzzy search indices on the same column at all?
// And if not, why do we wrap this into an array?
return hanaTc.fzindexes[elemName][hanaTc.fzindexes[elemName].length - 1];
}
else {
return hanaTc.fzindexes[elemName];
}
}
// Render an element (of an entity or type, not a projection or view).
// Ignore association elements (those are rendered later by renderAssociationElement)
// 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).
function renderElement(elm, env) {
function renderElement(artifactName, elementName, elm, fzindex, env) {
// Ignore if forHana says so, or if it is an association
if (elm._ignore) {
if (elm._ignore || elm.target) {
return '';
}
if (isAssociation(elm._finalType.type)) {
return '';
}
let result = env.indent + quoteSqlId(elm.name.id) + ' '
+ renderTypeReference(elm)
let result = env.indent + quoteSqlId(elementName) + ' '
+ renderTypeReference(artifactName, elementName, elm)
+ renderNullability(elm);
if (elm.default) {
result += ' DEFAULT ' + renderPathOrValue(elm.default, env);
result += ' DEFAULT ' + renderExpr(elm.default, env);
}
if(elm._fzindex) {
result += ' FUZZY SEARCH INDEX ON';
if(elm._fzindex.fuzzy) {
result += ' FUZZY SEARCH MODE';
if(elm._fzindex.fuzzy.mode) {
result += ' ' + renderPathOrValue(elm._fzindex.fuzzy.mode);
}
}
// Only HANA has fuzzy indizes
if (fzindex && options.toSql.dialect === 'hana') {
result += ' ' + renderExpr(fzindex, env);
}

@@ -225,10 +243,11 @@ return result;

// Render those elements from 'elements' that are associations, in the style required for
// 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, or an empty string if there are none
function renderAssociationElement(elm, env) {
// Return a string with one line per association element, or an empty string if the element
// is not an association.
function renderAssociationElement(elementName, elm, env) {
let result = '';
if (isAssociation(elm._finalType.type)) {
if (elm.target) {
result += env.indent + 'MANY TO ';
if (elm.cardinality && elm.cardinality.targetMax && (elm.cardinality.targetMax.val == '*' || Number(elm.cardinality.targetMax.val) > 1)) {
if (elm.cardinality && elm.cardinality.max && (elm.cardinality.max == '*' || Number(elm.cardinality.max) > 1)) {
result += 'MANY';

@@ -239,4 +258,4 @@ } else {

result += ' JOIN ';
result += quoteSqlId(absoluteCdsName(elm.target._artifact.name.absolute)) + ' AS ' + quoteSqlId(elm.name.id) + ' ON (';
result += renderExpressionOrCondition(elm.onCond, env, true) + ')';
result += quoteSqlId(absoluteCdsName(elm.target)) + ' AS ' + quoteSqlId(elementName) + ' ON (';
result += renderExpr(elm.on, env) + ')';
}

@@ -246,405 +265,214 @@ return result;

// Render the 'technical configuration { ... }' section of an entity.
// 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.
function renderTechnicalConfiguration(tc, env) {
let result = '';
if (!tc) {
return result;
}
if (tc.migration) {
result += '\n' + env.indent + 'MIGRATION ' + renderPathOrValue(tc.migration, env);
}
if (tc.extendedStorage) {
result += '\n' + env.indent + 'USING EXTENDED STORAGE';
}
if (tc.unloadPrio) {
result += '\n' + env.indent + 'UNLOAD PRIORITY ' + renderPathOrValue(tc.unloadPrio, env);
}
if (tc.autoMerge) {
result += '\n' + env.indent + (tc.autoMerge.val == false ? 'NO ' : '') + 'AUTO MERGE';
}
if (tc.group) {
result += '\n' + env.indent;
if (tc.group.name) {
result += 'GROUP NAME ' + quoteSqlId(tc.group.name.id);
}
if (tc.group.name && tc.group.type) {
result += ' ';
}
if (tc.group.type) {
result += 'GROUP TYPE ' + quoteSqlId(tc.group.type.id);
}
if ((tc.group.name || tc.group.type) && tc.group.subType) {
result += ' ';
}
if (tc.group.subType) {
result += 'GROUP SUBTYPE ' + quoteSqlId(tc.group.subType.id);
}
// FIXME: How to deal with non-HANA technical configurations?
// This also affects renderIndexes
tc = tc.hana;
if (!tc) {
throw new Error('Expecting a HANA technical configuration');
}
if (tc.tableSuffix) {
// Although we could just render the whole bandwurm as one stream of tokens, the
// compactor has kindly stored each part (e.g. `migration enabled` `row store`, ...)
// in its own `xpr` (for the benefit of the `toCdl` renderer, which needs semicolons
// between parts). We use this here for putting each one one line)
if (tc.partition) {
result += '\n' + env.indent + 'PARTITION BY ';
let i = 0;
tc.partition.specs.forEach(p => {
if (i > 0) {
result += ', ';
// The ignore array contains technical configurations that are illegal in HANA SQL
let ignore = [
'PARTITION BY KEEPING EXISTING LAYOUT',
'ROW STORE',
'COLUMN STORE',
'MIGRATION ENABLED',
'MIGRATION DISABLED'
];
for (let xpr of tc.tableSuffix) {
let clause = renderExpr(xpr, env);
if(!ignore.includes(clause.toUpperCase())) {
result += '\n' + env.indent + clause;
}
result += renderPartition(p, env);
i++;
});
if (tc.partition.wpoac) {
result += ' ' + 'WITH PARTITIONING ON ANY COLUMNS ' + renderPathOrValue(tc.partition.wpoac, env);
}
}
return result;
}
// Render a partition spec of a technical configuration.
function renderPartition(partition, env) {
let result = renderPathOrValue(partition.scheme, env);
if (partition.columns) {
result += ' (';
let i = 0;
partition.columns.filter(column => !column._ignore).forEach(column => {
if (i > 0) {
result += ', ';
}
if (column.unit) {
result += renderPathOrValue(column.unit, env) + '(';
}
result += renderPathOrValue(column, env);
if (column.unit) {
result += ')';
}
i++;
});
result += ')';
// Render the array `indexes` from the technical configuration of an entity 'artifactName'
function renderIndexesInto(indexes, artifactName, resultObj, env) {
// Indices and full-text indices
for (let 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]) {
result = renderExpr(insertTableName(index), env);
}
}
if (partition.partitions) {
result += ' PARTITIONS ' + renderPathOrValue(partition.partitions, env);
else {
result = renderExpr(insertTableName(indexes[idxName]), env);
}
if (partition.ranges) {
result += ' (';
let oppStore = (partition.ranges[0].store == 'default' ? 'extended' : 'default');
let delimiter = false;
partition.ranges.forEach((range, env) => {
if (range.store != oppStore) {
if (partition.withStorageSpec) {
if (delimiter) {
result += ') ';
}
result += 'USING ' + range.store.toUpperCase() + ' STORAGE (';
}
delimiter = false;
oppStore = range.store;
}
if (delimiter) {
result += ', ';
}
result += renderPartitionRange(range, env);
delimiter = true;
});
if (partition.withStorageSpec) {
result += ')';
}
result += ')';
// FIXME: Full text index should already be different in compact CSN
if (result.startsWith('FULLTEXT')) {
resultObj.hdbfulltextindex[`${artifactName}.${idxName}`] = result;
}
return result;
function renderPartitionRange(range, env) {
let result = 'PARTITION ';
if (range.others) {
return result + renderPathOrValue(range.others, env);
}
if (!range.max) {
result += 'VALUE = '
}
result += renderPathOrValue(range.min, env);
if (range.isCurrent) {
result += ' IS CURRENT';
}
if (range.max) {
result += ' <= VALUES < ' + renderPathOrValue(range.max, env);
}
return result;
else {
resultObj.hdbindex[`${artifactName}.${idxName}`] = result;
}
}
}
// Render the indices belonging to 'artifactName' into the appropriate
// dictionary of 'resultObj'.
function renderIndexesInto(tc, artifactName, resultObj, env) {
let result = '';
if (tc && tc.indexes) {
for (let idxName in tc.indexes) {
let idx = tc.indexes[idxName];
if (Array.isArray(idx)) {
idx.forEach(i => renderIndexInto(i, artifactName, resultObj, env));
}
else {
renderIndexInto(idx, artifactName, resultObj, env);
}
// 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.
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) {
throw new Error(`Unexpected form of index: "${index}"`);
}
}
return result;
function renderIndexInto(idx, artifactName, resultObj, env) {
let result = '';
if (idx.kind === 'index') {
if (idx.unique) {
result += 'UNIQUE ';
}
result += 'INDEX ' + quoteSqlId(idx.name.id) + ' ON (' + renderArray(idx.columns) + ')';
if (idx.sort) {
result += ' ' + renderPathOrValue(idx.sort, env);
}
resultObj.hdbindex[`${artifactName}.${idx.name.id}`] = result;
let indexName = `${absoluteCdsName(artifactName)}.${index[i + 1].ref}`;
if (options.toSql.names == 'plain') {
indexName = indexName.replace(/(\.|::)/g, '_');
}
else if (idx.kind === 'fulltextindex') {
result += 'FULLTEXT INDEX ' + quoteSqlId(idx.name.id) + ' ON (' + renderArray(idx.columns) + ')';
if (idx.language) {
if (idx.language.column) {
result += ' ' + 'LANGUAGE COLUMN ' + renderPathOrValue(idx.language.column, env);
}
if (idx.language.detection) {
result += ' ' + 'LANGUAGE DETECTION (' + renderArray(idx.language.detection) + ')'
}
}
if (idx.mimeTypeColumn) {
result += ' ' + 'MIME TYPE COLUMN ' + renderPathOrValue(idx.mimeTypeColumn, env);
}
if (idx.fuzzySearchIndex) {
result += ' ' + 'FUZZY SEARCH INDEX ' + renderPathOrValue(idx.fuzzySearchIndex, env);
}
if (idx.phraseIndexRatio) {
result += ' ' + 'PHRASE INDEX RATIO ' + renderPathOrValue(idx.phraseIndexRatio, env);
}
if (idx.configuration) {
result += ' ' + 'CONFIGURATION ' + renderPathOrValue(idx.configuration, env);
}
if (idx.textAnalysis) {
result += ' ' + 'TEXT ANALYSIS ' + renderPathOrValue(idx.textAnalysis, env);
}
if (idx.searchOnly) {
result += ' ' + 'SEARCH ONLY ' + renderPathOrValue(idx.searchOnly, env);
}
if (idx.fastPreprocess) {
result += ' ' + 'FAST PREPROCESS ' + renderPathOrValue(idx.fastPreprocess, env);
}
if (idx.mimeType) {
result += ' ' + 'MIME TYPE ' + renderPathOrValue(idx.mimeType, env);
}
if (idx.tokenSeparators) {
result += ' ' + 'TOKEN SEPARATORS ' + renderPathOrValue(idx.tokenSeparators, env);
}
if (idx.textMining) {
if (idx.textMining.state) {
result += ' ' + 'TEXT MINING ' + renderPathOrValue(idx.textMining.state, env);
}
if (idx.textMining.config) {
result += ' ' + 'TEXT MINING CONFIGURATION ' + renderPathOrValue(idx.textMining.config, env);
}
if (idx.textMining.overlay) {
result += ' ' + 'TEXT MINING CONFIGURATION OVERLAY ' + renderPathOrValue(idx.textMining.overlay, env);
}
}
if (idx.changeTracking) {
let ct = idx.changeTracking;
result += ' ' + renderPathOrValue(ct.mode);
if (ct.asyncSpec) {
let asp = ct.asyncSpec;
result += ' FLUSH ';
if (asp.queue) {
result += renderPathOrValue(asp.queue, env) + ' ';
}
if (asp.minutes) {
result += 'EVERY ' + renderPathOrValue(asp.minutes, env) + ' MINUTES';
if (asp.documents) {
result += ' OR ';
}
}
if (asp.documents) {
result += 'AFTER ' + renderPathOrValue(asp.documents, env) + ' DOCUMENTS';
}
}
}
resultObj.hdbfulltextindex[`${artifactName}.${idx.name.id}`] = result;
}
return;
function renderArray(arr) {
let r = '', i = 0;
arr.filter(v => !v._ignore).forEach(v => {
if (i > 0) {
r += ', ';
}
r += renderPathOrValue(v, env);
if (v.sort) {
r += ' ' + renderPathOrValue(v.sort, env);
}
i++;
});
return r;
}
let 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(...index.slice(j)); // (x, y)
return result;
}
}
// Render a projection entity. Return the resulting source string.
// NOTE: Not actually used for now, because forHana is always applied first, converting projections to views.
function renderProjection(art, env) {
let childEnv = increaseIndent(env);
let result = 'VIEW ' + quoteSqlId(absoluteCdsName(art.name.absolute));
result += renderParameterDefinitions(art.params);
result += ' AS SELECT\n';
result += Object.keys(art.elements).map(name => renderViewOrProjectionElement(art.elements[name], childEnv))
.filter(s => s != '')
.join(',\n') + '\n';
result += env.indent + 'FROM ' + renderSourcePathWithAlias(art.source, art);
return result;
}
// Render the source of a query, which may either be a path with an alias or a join operation,
// as seen from artifact 'art'.
// 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.
function renderViewSource(source, art, env) {
if (source instanceof Array) {
// Join operations in parentheses
if (source.length != 1) {
throw new Error('Expecting only one join operation: ' + source);
function renderViewSource(artifactName, source, env) {
// Sub-SELECT
if (source.SELECT || source.SET) {
let result = `(${renderQuery(artifactName, source, increaseIndent(env))})`;
if (source.as) {
result += ` AS ${quoteSqlId(source.as)}`;
}
return `${renderViewSource(source[0], art, env)}`;
} else if (source.op && source.op.val == 'join') {
return result;
}
// JOIN
else if (source.join) {
// One join operation, possibly with ON-condition
let result = `${renderViewSource(source.args[0], art, env)} ${renderJoinOp(source.join).toUpperCase()} ${renderViewSource(source.args[1], art, env)}`;
// FIXME: Clarify if join operators can be n-ary (assuming binary here)
let result = `(${renderViewSource(artifactName, source.args[0], env)} ${source.join.toUpperCase()} JOIN ${renderViewSource(artifactName, source.args[1], env)}`;
if (source.on) {
result += ` ON ${renderExpressionOrCondition(source.on)}`;
result += ` ON ${renderExpr(source.on, env)}`;
}
// Always put parentheses around joins for readability (other than toCdl)
return `(${result})`;
} else if (source.op && source.op.val == 'query') {
// sub-select
let result = `(${renderQuery(source, art, false, increaseIndent(env))})`;
if (source.name && !source.name.calculated) {
// Source had an alias - render it
result += ' AS ' + quoteSqlId(source.name.id);
}
result += `)`;
return result;
} else {
// Ordinary path, possibly with an alias
return renderSourcePathWithAlias(source, art);
}
// 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);
}
}
// Render the source path of a projection or query, possibly with an alias, as seen from artifact 'art'.
// Expects an object 'source' that has a 'path' and (in case of an alias) a 'name'.
// 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.
function renderSourcePathWithAlias(source, art) {
// Sanity checks
if (!source.path || source.path.length < 1) {
throw new Error('Expecting path in source of ' + art.name.absolute + ': ' + JSON.stringify(source, null, 2));
function renderAbsolutePathWithAlias(artifactName, path, env) {
// This actually can't happen anymore because assoc2joins should have taken care of it
if (path.ref[0].where) {
throw new Error(`"${artifactName}": Filters in FROM are not supported for conversion to SQL`);
}
if (!source.path[0]._artifact) {
throw new Error('Expecting first path step in source of ' + art.name.absolute + ' to be resolved: ' + JSON.stringify(source, null, 2));
}
// FIXME: This is all very ugly (will likely improve once we consume new-style CSN here)
// Determine the index of the first entity on the path (skipping over contexts etc) - after that we may have assoc elements
let firstEntityIdx = 0;
for (let i = 0; i < source.path.length; i++) {
if (source.path[i]._artifact && source.path[i]._artifact.kind != 'element') {
// Still within the entity name
firstEntityIdx = i;
}
// SQL needs a ':' after path.ref[0] to separate associations
let result = renderAbsolutePath(path, ':', env);
// Take care of aliases
let implicitAlias = getLastPartOfRef(path.ref);
if (path.as) {
// Source had an alias - render it
result += ' AS ' + quoteSqlId(path.as);
}
if (source.path[firstEntityIdx].where) {
signal(error`"${art.name.absolute}": Filters in FROM are not supported for conversion to SQL`, source.location);
return '';
else if (getLastPartOf(result) != quoteSqlId(implicitAlias)) {
// Render an artificial alias if the result would produce a different one
result += ' AS ' + quoteSqlId(implicitAlias);
}
let firstEntity = source.path[firstEntityIdx]._artifact;
return result;
}
// Start with the absolute name of the first path step
let result = quoteSqlId(absoluteCdsName(firstEntity.name.absolute));
// Even the first step might have parameters
if (source.path[firstEntityIdx].namedArgs) {
result += renderNamedArgs(source.path[firstEntityIdx].namedArgs);
// 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.
function renderAbsolutePath(path, sep, env) {
// Sanity checks
if (!path.ref) {
throw new Error('Expecting ref in path: ' + JSON.stringify(path));
}
// Add any paths that may follow after that (separating the artifact name from the rest by ':' !)
for (let i = firstEntityIdx + 1; i < source.path.length; i++) {
// Sanity check
if (source.path[i]._artifact && source.path[i]._artifact.kind != 'element') {
throw new Error('Expecting an element here: ' + JSON.stringify(source.path[i], null, 2));
}
// Path continues with elements - complain if it has a filter
if (source.path[i].where) {
signal(error`"${art.name.absolute}": Filters in FROM are not supported for conversion to SQL`, source.location);
return '';
}
if (i > firstEntityIdx + 1) {
// We are already in the elements part of a path - append '.' and quoted id
result += '.' + quoteSqlId(source.path[i].id);
// Append view params if any
if (source.path[i].namedArgs) {
result += renderNamedArgs(source.path[i].namedArgs);
}
} else {
// Sanity check
if (i < 1) {
throw new Error('Cannot be the first path step: ' + JSON.stringify(source.path[i], null, 2));
}
// We have just left the artifact and entered elements - append ':' and the quoted id of the current path step
result += ':' + quoteSqlId(source.path[i].id);
if (source.path[i].namedArgs) {
result += renderNamedArgs(source.path[i].namedArgs);
}
}
// 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];
let firstArtifact = csn.definitions[firstArtifactName];
if (!firstArtifact) {
throw new Error('Expecting first path step in path to be resolvable: ' + JSON.stringify(path));
}
// Sanity check: must have a name
if (!source.name) {
throw new Error('Expecting source to have a name: ' + JSON.stringify(source));
let result = quoteSqlId(absoluteCdsName(firstArtifactName));
// Even the first step might have parameters and/or a filter
if (path.ref[0].args) {
result += `(${renderArgs(path.ref[0].args, '=>', env)})`;
}
// Render an alias unless it is redundant
if (quoteSqlId(source.name.id) != result) {
result += ' AS ' + quoteSqlId(source.name.id);
if (path.ref[0].where) {
result += `[${path.ref[0].cardinality ? (path.ref[0].cardinality.max + ': ') : ''}${renderExpr(path.ref[0].where, env)}]`;
}
// 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)}`;
}
return result;
}
function renderNamedArgs(args) {
let result = '(';
let i = 0;
for (let argName in args) {
let arg = args[argName];
if (i > 0) {
result += ', ';
}
result += quoteSqlId(arg.name.id) + ' => ' + renderPathOrValue(arg, { indent: '' });
i++;
// Render function arguments or view parameters (positional if array, named if object/dict),
// using 'sep' as separator for positional parameters
function renderArgs(args, sep, env) {
// Positional arguments
if (args instanceof Array) {
return args.map(arg => renderExpr(arg, env)).join(', ');
}
return result + ')';
// Named arguments (object/dict)
else if (typeof args == 'object') {
return Object.keys(args).map(key => `${quoteSqlId(key)} ${sep} ${renderExpr(args[key], env)}`).join(', ');
}
else {
throw new Error('Unknown args: ' + JSON.stringify(args));
}
}
// Render a single view or projection element 'elm', as it occurs in a select list or projection list,
// possibly with annotations. Return the resulting source string (no trailing LF).
function renderViewOrProjectionElement(elm, env) {
// Ignore if forHana says so, or if it is an association
if (elm._ignore) {
// 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).
function renderViewColumn(col, env) {
// Ignore if forHana says so
// FIXME: Have we already filtered out associations here?
if (col._ignore) {
return '';
}
if (isAssociation((elm._finalType || elm).type)) {
return '';
// FIXME: We may want to wrap a cast around 'col' if it has an explicit type?
let result = env.indent + renderExpr(col, env, true);
// Explicit or implicit alias?
if (col.as) {
result += ' AS ' + quoteSqlId(col.as);
}
let elementValue = renderExpressionOrCondition(elm.value, env, true);
if (elm._typeIsExplicit) {
// FIXME: We may want to wrap a cast around 'elementValue' in this case?
}
let result = env.indent + elementValue;
// Render an alias unless it is redundant
let path = elm.value.path;
if (!path || elm.name.id != path[path.length-1].id) {
result += ' AS ' + quoteSqlId(elm.name.id);
}
return result;

@@ -654,9 +482,9 @@ }

// Render a view
function renderView(art, env) {
let result = 'VIEW ' + quoteSqlId(absoluteCdsName(art.name.absolute));
result += renderParameterDefinitions(art.params);
result += ' AS ' + renderQuery(art.query, art, true, env);
function renderView(artifactName, art, env) {
let result = 'VIEW ' + quoteSqlId(absoluteCdsName(artifactName));
result += renderParameterDefinitions(artifactName, art.params);
result += ' AS ' + renderQuery(artifactName, art.query, env);
let childEnv = increaseIndent(env);
let associations = Object.keys(art.elements).filter(name => isAssociation(art.elements[name].type))
.map(name => renderAssociationElement(art.elements[name], childEnv))
let associations = Object.keys(art.elements).filter(name => !!art.elements[name].target)
.map(name => renderAssociationElement(name, art.elements[name], childEnv))
.filter(s => s != '')

@@ -671,54 +499,88 @@ .join(',\n');

function renderParameterDefinitions(params) {
let paramDefs = Object.keys(params || []).map(name =>
{ return 'IN ' + quoteSqlId(name) + ' ' + renderTypeReference(params[name])}).join(', ');
return (paramDefs == '') ? paramDefs : '(' + paramDefs + ')';
// Render the parameter definition of a view if any. Return the parameters in parentheses, or an empty string
function renderParameterDefinitions(artifactName, params) {
let result = Object.keys(params || {}).map(name => 'IN ' + quoteSqlId(name) + ' ' + renderTypeReference(artifactName, name, params[name]))
.join(', ');
if (result != '') {
result = '(' + result + ')';
}
return result;
}
// Render a query 'query', i.e. a select statement with where-condition etc, possibly as part of artifact 'art'.
// If 'isLeadingQuery' is true, mixins of 'art' are also rendered into the query.
// FIXME: No support for MIXINs yet.
// FIXME: currently only selection from multiple sources is supported, no JOIN yet, no UNION yet
function renderQuery(query, art, isLeadingQuery, env) {
// Render a query 'query', i.e. a select statement with where-condition etc. Use 'artifactName' only for error messages.
function renderQuery(artifactName, query, env) {
let result = '';
if(Array.isArray(query)) {
result = `(${renderQuery(query[0], art, isLeadingQuery, env)})`;
} else if (query.op && query.op.val == 'query') {
let childEnv = increaseIndent(env);
result += 'SELECT' + ((query.quantifier && query.quantifier.val) ? ` ${ query.quantifier.val.toUpperCase() }\n` : '\n');
result += Object.keys(query.elements).map(name => renderViewOrProjectionElement(query.elements[name], childEnv))
.filter(s => s != '')
.join(',\n') + '\n';
result += `${env.indent}FROM ${query.from.map(source => renderViewSource(source, art, env)).join(', ')}`;
} else if (query.op && query.op.val == 'subquery') {
// Magic special case: Subquery in parentheses can have ORDER BY, LIMIT and OFFSET outside the parentheses.
result += `(${renderQuery(query.args[0][0], art, isLeadingQuery, env)})`;
} else if (query.op && ['union', 'unionAll', 'intersect', 'except'].includes(query.op.val)) {
// Ordinary query operators (first may be leading query)
result += `(${renderQuery(query.args[0], art, isLeadingQuery, env)}`
result += `\n${env.indent}${query.op.val.replace('All', ' all').toUpperCase()} ${renderQuery(query.args[1], art, false, env)})`;
} else {
throw new Error('Unexpected query operation ' + query.op.val);
// Set operator, like UNION, INTERSECT, ...
if (query.SET) {
result += query.SET.args
.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);
return (arg.SET || arg.SELECT && (arg.SELECT.orderBy || arg.SELECT.limit)) ? `(${queryString})` : queryString;
})
.join(`\n${env.indent}${query.SET.op && query.SET.op.toUpperCase()}${query.SET.all ? ' ALL ' : ' '}`);
// Set operation may also have an ORDER BY and LIMIT/OFFSET (in contrast to the ones belonging to
// each SELECT)
// If the whole SET has an ORDER BY/LIMIT, wrap the part before that in parentheses
// (otherwise some SQL implementations (e.g. sqlite) would interpret the ORDER BY/LIMIT as belonging
// to the last SET argument, not to the whole SET)
if (query.SET.orderBy || query.SET.limit) {
result = `(${result})`;
if (query.SET.orderBy) {
result += `\n${env.indent}ORDER BY ${query.SET.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;
}
if (query.SET.limit) {
result += `\n${env.indent}${renderLimit(query.SET.limit, env)}`;
}
}
return result;
}
if (query.where) {
result += `\n${env.indent}WHERE ${renderExpressionOrCondition(query.where, env)}`;
// Otherwise must have a SELECT
else if (!query.SELECT) {
throw new Error('Unexpected query operation ' + JSON.stringify(query));
}
if (query.groupBy) {
result += `\n${env.indent}GROUP BY ${query.groupBy.map(exprOrCond => renderExpressionOrCondition(exprOrCond, env)).join(', ')}`;
let select = query.SELECT;
let childEnv = increaseIndent(env);
result += 'SELECT' + (select.distinct ? ' DISTINCT' : '');
// FIXME: We probably also need to consider `excluding` here ?
result += '\n' +
(select.columns||['*']).filter(col => !(select.mixin || {})[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 (query.having) {
result += `\n${env.indent}HAVING ${renderExpressionOrCondition(query.having, env)}`;
if (select.groupBy) {
result += `\n${env.indent}GROUP BY ${select.groupBy.map(expr => renderExpr(expr, env)).join(', ')}`;
}
// Need extra parentheses if ORDER BY or LIMIT is involved, because they have strange precedence in relation to UNION, INTERSECT, ...
if (query.orderBy || query.limit) {
result = `(${result})`;
if (select.having) {
result += `\n${env.indent}HAVING ${renderExpr(select.having, env)}`;
}
if (query.orderBy) {
result += `\n${env.indent}ORDER BY ${query.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;
if (select.orderBy) {
result += `\n${env.indent}ORDER BY ${select.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;
}
if (query.limit) {
result += `\n${env.indent}LIMIT ${renderPathOrValue(query.limit, env)}`;
if (select.limit) {
result += `\n${env.indent}${renderLimit(select.limit, env)}`;
}
if (query.offset) {
result += `\n${env.indent}OFFSET ${renderPathOrValue(query.offset, env)}`;
return result;
}
// Returns the id of the first path step in 'ref' if any, otherwise undefined
function firstPathStepId(ref) {
return ref && ref[0] && (ref[0].id || ref[0]);
}
// Render a query's LIMIT clause, which may have also have OFFSET.
function renderLimit(limit, env) {
let result = '';
if (limit.rows !== undefined) {
result += `LIMIT ${renderExpr(limit.rows, env)}`;
}
if (limit.offset !== undefined) {
result += `${result != '' ? '\n' + env.indent : ''}OFFSET ${renderExpr(limit.offset, env)}`;
}
return result;

@@ -730,8 +592,8 @@ }

function renderOrderByEntry(entry, env) {
let result = renderExpressionOrCondition(entry.value, env);
let result = renderExpr(entry, env);
if (entry.sort) {
result += ` ${entry.sort.val.toUpperCase()}`;
result += ` ${entry.sort.toUpperCase()}`;
}
if (entry.nulls) {
result += ` NULLS ${entry.nulls.val.toUpperCase()}`;
result += ` NULLS ${entry.nulls.toUpperCase()}`;
}

@@ -743,3 +605,3 @@ return result;

// Return the resulting source string.
function renderType(art, env) {
function renderType(artifactName, art, env) {
// Only HANA table types are SQL-relevant

@@ -749,13 +611,18 @@ if (!art.dbType) {

}
let result ='TYPE ' + quoteSqlId(absoluteCdsName(art.name.absolute)) + ' AS TABLE (\n';
// In Sqlite dialect do not generate table type and throw an info
if (options.toSql.dialect === 'sqlite') {
signal(info`"${artifactName}": HANA table types are not supported in SQLite`, art.location);
return '';
}
let result = 'TYPE ' + quoteSqlId(absoluteCdsName(artifactName)) + ' AS TABLE (\n';
let childEnv = increaseIndent(env);
if (art._finalType.elements) {
// Structured type or annotation with anonymous struct type
result += Object.keys(art._finalType.elements).map(name => renderElement(art._finalType.elements[name], childEnv))
.filter(s => s != '')
.join(',\n') + '\n';
if (art.elements) {
// Structured type
result += Object.keys(art.elements).map(name => renderElement(artifactName, name, art.elements[name], null, childEnv))
.filter(s => s != '')
.join(',\n') + '\n';
result += env.indent + ')';
} else {
// Non-structured HANA table type
signal(error`"${art.name.absolute}": HANA table types must have structured types for conversion to SQL`, art.location);
signal(error`"${artifactName}": HANA table types must have structured types for conversion to SQL`, art.location);
return '';

@@ -766,9 +633,9 @@ }

// Render a reference to the final type used by 'elm' (named or inline)
function renderTypeReference(elm) {
// Render a reference to the type used by 'elm' (with name 'elementName' in 'artifactName', both used only for error messages).
function renderTypeReference(artifactName, elementName, elm) {
let result = '';
// Array type: Not supported with SQL
if (elm._finalType.items) {
signal(error`"${elm._main.name.absolute}.${elm.name.element}": Array types are not supported for conversion to SQL`, elm.location);
if (elm.items) {
signal(error`"${artifactName}.${elementName}": Array types are not supported for conversion to SQL`, elm.location);
return result;

@@ -778,7 +645,7 @@ }

// Anonymous structured type: Not supported with SQL (but shouldn't happen anyway after forHana flattened them)
if (!elm._finalType.type) {
if (!elm._finalType.elements) {
throw new Error('Missing type of: ' + elm.name.id);
if (!elm.type) {
if (!elm.elements) {
throw new Error('Missing type of: ' + elementName);
}
signal(error`"${elm._main.name.absolute}.${elm.name.element}": Anonymous structured types are not supported for conversion to SQL`, elm.location);
signal(error`"${artifactName}.${elementName}": Anonymous structured types are not supported for conversion to SQL`, elm.location);
return result;

@@ -788,5 +655,5 @@ }

// Association type
if (elm._finalType.target) {
if (elm.target) {
// We can't do associations yet
signal(error`"${elm._main.name.absolute}.${elm.name.element}": Association and composition types are not yet supported for conversion to SQL`, elm.location);
signal(error`"${artifactName}.${elementName}": Association and composition types are not yet supported for conversion to SQL`, elm.location);
return result;

@@ -796,13 +663,9 @@ }

// If we get here, it must be a primitive (i.e. builtin) type
if (elm._finalType.type._artifact.builtin) {
if (isBuiltinType(elm.type)) {
// cds.Integer => render as INTEGER (no quotes)
result += renderBuiltinType(elm._finalType.type._artifact);
result += renderBuiltinType(elm.type);
} else {
throw new Error('Unexpected non-primitive type of: ' + elm._main.name.absolute + '.' + elm.name.element);
throw new Error('Unexpected non-primitive type of: ' + artifactName + '.' + elementName);
}
result += renderTypeParameters(elm._finalType);
// FIXME: Quickhack: Apparently we sometimes omit the default length for strings
if (result == 'NVARCHAR') {
result += '(5000)';
}
result += renderTypeParameters(elm);
return result;

@@ -812,3 +675,3 @@ }

// Render the name of a builtin CDS type
function renderBuiltinType(type) {
function renderBuiltinType(typeName) {
const cdsToSql = {

@@ -851,5 +714,5 @@ // CDS builtin types

let result = cdsToSql[type.name.absolute];
let result = cdsToSql[typeName];
if (!result) {
throw Error('Unknown primitive type: ' + JSON.stringify(type));
throw Error('Unknown primitive type: ' + typeName);
}

@@ -859,9 +722,17 @@ return result;

// Return true if 'typeName' is the name of a builtin type
function isBuiltinType(typeName) {
// FIXME: Rather than checking a list of builtin types, we just rely on the fact
// that in a valid model, the only types that do not occur in 'definitions' are
// the builtin ones.
return !csn.definitions[typeName];
}
// Render the nullability of an element or parameter (can be unset, true, or false)
function renderNullability(obj /* , env */) {
if (!obj.notNull) {
if (obj.notNull === undefined) {
// Attribute not set at all
return '';
}
return obj.notNull.val ? ' NOT NULL' : ' NULL';
return obj.notNull ? ' NOT NULL' : ' NULL';
}

@@ -874,14 +745,15 @@

// Length, precision and scale (even if incomplete)
if (elm.length) {
params.push(elm.length.val);
if (elm.length !== undefined) {
params.push(elm.length);
}
if (elm.precision) {
params.push(elm.precision.val);
if (elm.precision !== undefined) {
params.push(elm.precision);
}
if (elm.scale) {
params.push(elm.scale.val);
if (elm.scale !== undefined) {
params.push(elm.scale);
}
// Additional type parameters
// FIXME: Not yet clear how that looks in new CSN
for (let arg of elm.typeArguments || []) {
params.push(arg.val);
params.push(arg);
}

@@ -891,97 +763,183 @@ return params.length == 0 ? '' : '(' + params.join(', ') + ')';

// Render a single value (i.e. something that has 'path' or 'literal' and 'val')
// 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 renderPathOrValue(v, env, inline=true) {
let result = inline ? '' : env.indent;
if (v.path) {
// E.i
return result + renderPath(v.path, env, v);
} else if (v.literal == 'string') {
// 'foo', with proper escaping
return result + "'" + v.val.replace(/'/g, "''") + "'";
} else if (v.literal == 'enum') {
// #foo
// 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`, v.location);
return '';
} else if (v.literal == 'hex') {
// x'f000'
return result + "x'" + v.val + "'";
} else if (v.literal == 'date' || v.literal == 'time' || v.literal == 'timestamp') {
// date'2017-11-02'
// date('2017-11-02') if sqlite
return result + v.literal + `${options.toSql.dialect === 'sqlite' ?
"('" : "'"}` + v.val + `${options.toSql.dialect === 'sqlite' ? "')" : "'"}`;
} else if (v.literal == 'struct') {
// { foo: 1 }
// We can't do structs yet
// FIXME: Can that happen at all outside of annotations?
signal(error`: Struct values are not supported for conversion to SQL`, v.location);
return '';
} else if (v.literal == 'array') {
// [ 'foo', 'bar' ]
// We can't do arrays yet
// FIXME: Can that happen at all outside of annotations?
signal(error`: Array values are not supported for conversion to SQL`, v.location);
return '';
} else {
// 17.42, null, true
return result + String(v.val).toUpperCase();
function renderExpr(x, env, inline=true) {
// 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));
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)).join(' ');
}
}
// Render a path or query path (provided as an array of path steps, possibly with filters)
function renderPath(path, env) {
// Magic special case: SQL functions that have no parentheses (CURRENT_*) are not recognized as
// function expressions by the parser - instead they appear here as paths of length 1 with a
// 'builtin' artifact.
const magicForHana = {
'$now': 'CURRENT_TIMESTAMP',
'$user.id': "SESSION_CONTEXT('XS_APPLICATIONUSER')",
'$user.locale': "SESSION_CONTEXT('LOCALE')",
}
let ref = path.length && path[ path.length-1 ]._artifact;
if (ref) {
if(ref.kind == 'builtin') {
if (options.forHana) {
// HANA-specific translation of '$now' and '$user'
// FIXME: This should rather happen in forHana, but it is non-trivial to catch all the different
// flavors in which a path can be used there (e.g. for 'foo.origin.path', we would have to modify
// 'foo' to have a 'foo.value'). So much easier to do it here...
let impl = magicForHana[ ref.name.element ];
// FIXME: this is all not enough: we might need an explicit select item alias
if (impl)
return impl;
// Various special cases represented as objects
else if (typeof x == 'object' && x !== null) {
// 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') {
// date('2017-11-02')
return `${x.literal}('${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));
}
// TODO: really toUpperCase() if not forHana - even the $now etc?
let start = String(path[0].id).toUpperCase();
return (path.length > 1) ? start + renderPath(path.slice(1), env) : start;
}
if(ref.kind == 'param') {
let [ head, ...tail] = path;
if(head.id == '$parameters') {
path = tail;
// Enum symbol
else if (x['#']) {
// #foo
// 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);
return '';
}
// Reference: Array of path steps, possibly preceded by ':'
else if (x.ref) {
if (options.forHana && !x.param && !x.global && 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.forHana.dialect === 'sqlite') {
if (options.toSql.user && typeof options.toSql.user === 'string' || options.toSql.user instanceof String) {
return `'${options.toSql.user}'`;
}
else if ((options.toSql.user && options.toSql.user.id) && (typeof options.toSql.user.id === 'string' || options.toSql.user.id instanceof String)) {
return `'${options.toSql.user.id}'`;
} else {
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'`;
}
}
return "SESSION_CONTEXT('XS_APPLICATIONUSER')";
}
else if (x.ref[1] === 'locale') {
return options.forHana.dialect === 'sqlite'
? options.toSql.user && options.toSql.user.locale
? `'${options.toSql.user && options.toSql.user.locale}'` : `'EN'`
: "SESSION_CONTEXT('LOCALE')";
}
}
path[0].id = ':' + path[0].id;
// Note: parameters should be of path length 1
return path.map(p => p.id.toUpperCase()).join('.')
// 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('.');
}
// Function call, possibly with args (use '=>' for named args)
else if (x.func) {
if (x.args)
return `${x.func.toUpperCase()}(${renderArgs(x.args, '=>', env)})`;
else
return x.func;
}
// Nested expression
else if (x.xpr) {
return renderExpr(x.xpr, env);
}
// Sub-select
else if (x.SELECT) {
// renderQuery for SELECT does not bring its own parentheses (because it is also used in renderView)
return `(${renderQuery('<subselect>', x, increaseIndent(env))})`;
}
else if (x.SET) {
// renderQuery for SET always brings its own parentheses (because it is also used in renderViewSource)
return `${renderQuery('<union>', x, increaseIndent(env))}`;
}
else {
throw new Error('Unknown expression: ' + JSON.stringify(x));
}
}
// FIXME: Not the most elegant solution to do that here: filter out initial '$projection' and '$self' (because SQL
// neither understands nor needs it).
if (options.forHana && (path[0].id === '$projection' || path[0].id === '$self')) {
return renderPath(path.slice(1), env);
// Not a literal value but part of an operator, function etc - just leave as it is
// FIXME: For the sake of simplicity, we should get away from all this uppercasing in toSql
else {
return String(x).toUpperCase();
}
return path.map(step => {
let result = quoteSqlId(step.id);
if(step.namedArgs) {
result += renderNamedArgs(step.namedArgs);
// Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function
function renderPathStep(s, idx) {
// Simple id or absolute name
if (typeof(s) == 'string') {
const magicForHana = {
'$now': 'CURRENT_TIMESTAMP',
'$user.id': "SESSION_CONTEXT('XS_APPLICATIONUSER')",
'$user.locale': "SESSION_CONTEXT('LOCALE')",
}
// Some magic for first path steps
if (idx == 0) {
// HANA-specific translation of '$now' and '$user'
// FIXME: this is all not enough: we might need an explicit select item alias
if (magicForHana[s]) {
return magicForHana[s];
}
// Ignore initial $projection and initial $self
if (s == '$projection' || s == '$self') {
return '';
}
}
return quoteSqlId(s);
}
if (step.where) {
result += '[' + (step.cardinality ? step.cardinality.targetMax.val + ': ' : '') + renderExpressionOrCondition(step.where, env, true) + ']';
// ID with filters or parameters
else if (typeof s == 'object') {
// Sanity check
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) {
return `${s.func}(${renderArgs(s.args, '=>', env)})`;
}
// Path step, possibly with view parameters and/or filters
let result = `${quoteSqlId(s.id)}`;
if (s.args) {
// View parameters
result += `(${renderArgs(s.args, '=>', env)})`;
}
if (s.where) {
// Filter, possibly with cardinality
// FIXME: Does SQL understand filter cardinalities?
result += `[${s.cardinality ? (s.cardinality.max + ': ') : ''}${renderExpr(s.where, env)}]`;
}
return result;
}
return result;
}).join('.');
else {
throw new Error('Unknown path step: ' + JSON.stringify(s));
}
}
}

@@ -994,7 +952,2 @@

// Returns a copy of 'env' with decreased indentation
function decreaseIndent(env) {
return Object.assign({}, env, { indent: env.indent.substring(2) });
}
// Return 'name' in the form of an absolute CDS name - for the 'hdbcds' naming convention,

@@ -1007,3 +960,3 @@ // this means converting '.' to '::' on the border between namespace and top-level artifact.

}
let topLevelName = getTopLevelArtifactNameOf(name, model);
let topLevelName = getTopLevelArtifactNameOf(name, csn);
let namespaceName = getParentNameOf(topLevelName);

@@ -1017,4 +970,2 @@ if (namespaceName) {

// Return 'name' with appropriate "-quotes.
// FIXME: Should only quote where necessary (examining the id for magic characters and reserved
// keywords) - for now, simply quote everything
// Additionally perform the following conversions on 'name'

@@ -1021,0 +972,0 @@ // If 'options.toSql.names' is 'plain'

@@ -26,6 +26,3 @@ const schemaObjects = require('./swaggerSchemaObjects');

forEachDefinition(model, obj => {
if (!obj._service)
return;
if (obj.kind === 'service') {
if (obj.kind === 'service' && !obj.abstract) {
swaggerJson = schemaObjects.openAPIObject();

@@ -32,0 +29,0 @@ swaggerJson.info.title = obj.name.absolute;

'use strict';
const { forEachDefinition, forEachMemberRecursively, setProp } = require('../base/model');
const { compactModel } = require('../json/to-csn');
const deepCopy = require('../base/deepCopy');

@@ -40,10 +39,7 @@ const { CompilationError, hasErrors, sortMessages } = require('../base/messages');

const { flattenForeignKeys, createForeignKeyElement, checkForeignKeys,
flattenStructuredElement, flattenStructStepsInPath, preprocessAction,
setServiceProperty, checkExposedAssoc, toFinalBaseType,
flattenStructuredElement, flattenStructStepsInPath,
checkExposedAssoc, toFinalBaseType,
addImplicitRedirections, createAndAddDraftAdminDataProjection,
createScalarElement, createAssociationElement, createAssociationPathComparison,
addElement, createAction, addAction } = transformUtils.getTransformers(model, '_');
// First walk through the model: perform preparations only
// Set '_service' property for all artifacts and sub-artifacts, for each service in the model
setServiceProperty(model);

@@ -69,3 +65,3 @@ // Second walk: Flatten structs, unravel derived types, deal with annotations

}
if (!isElementWithType(elem)) {
if (artifact._service && !isElementWithType(elem)) {
signal(error`Element "${artifact.name.absolute}.${elemName}" does not have a type: Elements of ODATA entities must have a type`, elem.location);

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

// Types must not have anonymous structured elements
else if (artifact.kind == 'type') {
else if (artifact._service && artifact.kind == 'type') {
for (let elemName in artifact.elements) {

@@ -95,4 +91,4 @@ let elem = artifact.elements[elemName];

// Entities only: Flatten structs used in paths
if (artifact.kind == 'entity') {
// Entities and views only: Flatten structs used in paths
if (artifact.kind == 'entity' || artifact.kind == 'view') {
foreachPath(member, (path, pathOwner) => {

@@ -107,2 +103,3 @@ pathOwner.path = flattenStructStepsInPath(path);

toFinalBaseType(artifact.items);
toFinalBaseType(artifact.returns);
}

@@ -130,2 +127,13 @@ // If the artifact is a derived structured type, unravel that as well

// For exposed actions and functions that use non-exposed or anonymous structured types, create
// artificial exposing types
forEachDefinition(model, (artifact) => {
if (artifact._service && (artifact.kind == 'action' || artifact.kind == 'function')) {
exposeStructTypesForAction(artifact, artifact._service);
}
for (let actionName in artifact.actions || {}) {
exposeStructTypesForAction(artifact.actions[actionName], artifact._service);
}
});
// Third walk: Generate foreign key fields for managed associations (must be done

@@ -243,3 +251,3 @@ // after struct flattening, otherwise we might encounter already generated foreign

if (!containedElem.notNull) {
signal(error`"${containedArtifact.name.absolute}.${containedElemName}": Association to container entity must have "NOT NULL`, containedElem.location);
signal(error`"${containedArtifact.name.absolute}.${containedElemName}": Association to container entity must have "NOT NULL"`, containedElem.location);
}

@@ -270,18 +278,19 @@ }

}
forEachMemberRecursively(artifact, member => {
// Check that exposed associations do not point to non-exposed targets
if (artifact._service && isAssociation(member.type)) {
checkExposedAssoc(artifact, member);
forEachMemberRecursively(artifact, (member, memberName) => {
if (artifact._service) {
if (isAssociation(member.type)) {
// Check that exposed associations do not point to non-exposed targets
checkExposedAssoc(artifact, member);
// CDXCORE-457
if (artifact.kind === 'type' && options.toOdata.version == 'v2') {
signal(warning`"${artifact.name.absolute}.${memberName}": Structured types must not contain associations for OData V2`, member.location);
}
}
// CDXCORE-458
else if (member.kind == 'element' && member.items && options.toOdata.version == 'v2') {
signal(error`"${artifact.name.absolute}.${memberName}": Element must not be an "array of" for OData V2`, member.location);
}
}
});
// Preprocess bound actions/functions
if (artifact.kind === 'entity' && artifact.actions && artifact._service) {
Object.keys(artifact.actions).forEach(actName => preprocessAction(artifact.actions[actName]));
}
// Preprocess unbound actions/function
if ((artifact.kind === 'action' || artifact.kind === 'function') && artifact._service) {
preprocessAction(artifact);
}
});

@@ -541,2 +550,78 @@

}
// If 'action' uses structured types as parameters or return values that are not exposed in 'service'
// (because the types are anonymous or have a definition outside of 'service'), create equivalent types
// in 'service' and make 'action' use them instead
function exposeStructTypesForAction(action, service) {
exposeStructTypeOf(action.returns, service, `__return_${actionNameNoDots(action)}`);
for (let paramName in action.params || {}) {
exposeStructTypeOf(action.params[paramName], service, `__param_${actionNameNoDots(action)}_${paramName}`);
}
}
// Return the absolute name of an action/function, with all dots replaced by underscores
function actionNameNoDots(action) {
return action.name.absolute.replace(/\./g, '_') + (action.name.action ? `_${action.name.action}` : '');
}
// If 'node' exists and has a structured type that is not exposed in 'service', (because the type is
// anonymous or has a definition outside of 'service'), create an equivalent type in 'service', either
// using the type's name or (if anonymous) 'artificialName', and make 'node' use that type instead.
// If there is an error, complain on 'node.location'.
function exposeStructTypeOf(node, service, artificialName) {
if (!node) {
return;
}
if (node.items) {
exposeStructTypeOf(node.items, service, artificialName);
}
if ((node._finalType || node.type).elements && (!node.type || node.type._artifact._service != service)) {
let typeId = node.type ? `__${node.type._artifact.name.absolute.replace(/\./g, '_')}`
: artificialName;
let type = exposeStructType(typeId, node._finalType.elements, service, node.location);
if (!type) {
// Error already reported
return;
}
node.type = {
path : [ { id: type.name.absolute } ],
};
setProp(node.type, '_artifact', type);
setProp(node.type.path[0], '_artifact', type);
setProp(node, '_finalType', type);
}
}
// Expose an artificial structured type with ID 'typeId' with 'elements' in 'service' (reusing such a type
// if it already exists).
// Return the exposed type. Report any errors on 'location'
function exposeStructType(typeId, elements, service, location) {
let typeName = `${service.name.absolute}.${typeId}`;
// If type already exists, reuse it (complain if not created here)
let type = model.definitions[typeName];
if (type) {
if (type.$inferred == 'actionType') {
return type;
} else {
signal(error`Cannot create artificial type "${typeName}" for an action or function because the name is already used`, location);
return null;
}
}
// Create type (use elements as they are)
type = {
name: {
path: [ { id: typeName }],
id: typeId,
absolute: typeName,
},
kind: 'type',
elements: elements, // FIXME: Actually, we should deep-copy here and adapt _parent, _main, name , ...
$inferred: 'actionType',
};
setProp(type, '_finalType', type);
service.artifacts[typeId] = type;
setProp(type, '_parent', service);
model.definitions[typeName] = type;
return type;
}
}

@@ -603,24 +688,5 @@

// Compact model and remove everything that that does not belong to the specified service
// model: augmented csn, prepared for OData
function compactForService(model, serviceName) {
// Compact the model
let compactedModel = compactModel(model);
setProp(compactedModel, 'messages', model.messages);
// Remove definitions that don't belong to given service
if (serviceName) {
let dict = compactedModel.definitions;
let namesNotInService = Object.keys(dict).filter(n => !n.startsWith(serviceName + '.') && n != serviceName);
for (let name of namesNotInService) {
delete compactedModel.definitions[name];
}
}
return compactedModel;
}
module.exports = {
transform4odata,
getServiceNames,
compactForService
getServiceNames
}

@@ -10,5 +10,3 @@ const { setProp, forEachDefinition, forEachGeneric, forEachMemberRecursively } = require('../base/model');

const { error, signal } = alerts(model);
const { preprocessAction, setServiceProperty, checkExposedAssoc } = transformUtils.getTransformers(model, '_');
// Set '_service' property for all artifacts and sub-artifacts, for each service in the model
setServiceProperty(model);
const { checkExposedAssoc } = transformUtils.getTransformers(model, '_');

@@ -20,8 +18,4 @@ forEachDefinition(model, obj => {

processElements(art);
if (art.projection)
if (art.projection) // TODO: use art.query
processProjection(obj, art);
if (art.kind === 'entity' && art.actions)
Object.keys(art.actions).forEach(a => preprocessAction(art.actions[a]));
if (art.kind === 'action' || art.kind === 'function')
preprocessAction(art);
if (art.kind === 'type')

@@ -82,3 +76,3 @@ processType(obj, art);

function processProjection(parent, art) {
if (parent._service !== art.source._artifact._service) {
if (parent !== source(art)._service) { // parent is the service
forEachMemberRecursively(art, elem => {

@@ -93,3 +87,3 @@ if (elem.kind !== 'element')

// the projection source with which the reference to be replaced
parent.artifacts[projName].source._artifact.name.absolute === elem.target._artifact.name.absolute
source(parent.artifacts[projName]).name.absolute === elem.target._artifact.name.absolute
);

@@ -158,2 +152,10 @@ if (targetFromCurrectService)

}
// TODO: consider making the above functionality work with more than one source
function source( view ) {
let from = view.query && view.query.from;
return from && from[0] && from[0]._artifact;
}
module.exports = preprocessModel;

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

setProp(targetArtifact, '_parent', artifact);
setProp(targetArtifact, '_service', artifact);
if (sourceArtifact.source != targetArtifact.source) {

@@ -133,0 +134,0 @@ targetArtifact.source = sourceArtifact.source;

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

const { setProp, cloneWithTransformations, forEachDefinition, forEachGeneric,
const { setProp, cloneWithTransformations, forEachDefinition,
forEachMemberRecursively } = require('../base/model');

@@ -25,4 +25,2 @@ const { addStringAnnotationTo, printableName,

flattenStructStepsInPath,
preprocessAction,
setServiceProperty,
checkExposedAssoc,

@@ -396,84 +394,10 @@ toFinalBaseType,

}
// If the path starts with '$self', this is now redundant (because of flattening) and can be omitted,
// making life easier for consumers
if (result[0].id == '$self' && result.length > 1) {
result = result.slice(1);
}
return result;
}
// Takes an augmented action/function object and checks if a defined type is used in the declaration,
// if yes, then checks if it is from the same service as the action/function
// used for OData and Swagger transformation
function preprocessAction(action) {
// A bound action has a parent - the corresponding entity
// An unbound action does not have a parent, but a '_service'
let actionBlock = action._parent ? action._parent._service : action._service;
// an action can return a builtin, a defined type, an entity, an array of one of the first three or an inline structured type
// if the action has a return declaration
// and the returned artifact is not a builtin - check if it is from the current service
if (action.returns) {
let returnedTypes = action.returns.elements ?
Object.keys(action.returns.elements).map(e => action.returns.elements[e].type) :
action.returns.items
? [action.returns.items.type]
: [action.returns.type];
returnedTypes.forEach(returnType => {
if (!returnType._artifact.name.absolute.startsWith('cds.')) {
let returnTypeBlock = obtainTypesService(returnType);
if (returnTypeBlock !== actionBlock)
signal(error`The defined return type ${returnType._artifact.name.absolute} of action ${action.name.absolute}${action.name.action ? '.' + action.name.action : ''} is not from the current service ${actionBlock.name.absolute}`, action.location);
}
});
}
// if the action has parameters and a paremeter is not a builtin - check if the used type is from the current service
if (action.params) {
for (let p in action.params) {
let param = action.params[p];
if (param.type._artifact.name.absolute.startsWith('cds.'))
continue;
let paramTypeBlock = obtainTypesService(param.type);
// this can happens if the special magic for @extends is used
// ugly tnt magic -> to be removed
if (!actionBlock && paramTypeBlock === 'not in a service') {
if (/* actionBlock */ action.name.absolute.split('.').slice(0, -1).join('.') !== /* paramTypeBlock */ param.type._artifact.name.absolute.split('.').slice(0, -1).join('.'))
signal(error`The type ${param.type._artifact.name.absolute} of parameter ${param.name.absolute}.${param.name.id} in action ${action.name.absolute}${action.name.action ? '.' + action.name.action : ''} is not from the current service`, param.location);
} else if (paramTypeBlock !== actionBlock)
signal(error`The type ${param.type._artifact.name.absolute} of parameter ${param.name.absolute}.${param.name.id} in action ${action.name.absolute}${action.name.action ? '.' + action.name.action : ''} is not from the current service ${actionBlock && actionBlock.name.absolute}`, param.location);
}
}
// Returns the service where the type is declared or 'not in a service' if the type is outside of such.
// Covers also the case when an element of defined type is used, for specifying a type.
function obtainTypesService(type) {
if (type._artifact._service)
return type._artifact._service;
if (type._artifact.kind === 'element' && type._artifact._main._service)
return type._artifact._main._service;
else
return 'not in a service';
}
}
// Takes a model and looks for services inside it.
// When a non-abstract service is found - adds an attribute '_service' to all artifacts that are exposed
// in a service (pointing to the service artifact).
function setServiceProperty(model) {
forEachDefinition(model, artifact => {
// If this is a non-abstract service, let all its (recursive) artifacts know
if (artifact.kind == 'service' && !artifact.abstract) {
setService(artifact, artifact);
}
// Set '_service' for 'artifact' and its sub-artifacts to 'service'.
function setService(artifact, service) {
// Services must not be nested
if (artifact.kind == 'service' && artifact._service != undefined) {
signal(error`Services cannot be nested: Service "${artifact.name.absolute}" is nested within service "${artifact._service.name.absolute}"`, artifact.location);
}
// Contexts cannot be defined within services
if (artifact.kind == 'context') {
signal(error`Contexts cannot be defined within services: Context "${artifact.name.absolute}" is defined within service "${service.name.absolute}"`, artifact.location);
}
setProp(artifact, '_service', service);
forEachGeneric(artifact, 'artifacts', subartifact => setService(subartifact, service));
}
});
}
// Check that exposed associations do not point to non-exposed targets

@@ -486,3 +410,4 @@ function checkExposedAssoc(artifact, association) {

// Replace the type of 'node' with its final base type (in contrast to the compiler,
// also unravel derived enum types, i.e. take the final base type of the enum's base type.
// also unravel derived enum types, i.e. take the final base type of the enum's base type.
// Similar with associations and compositions (we probably need a _baseType link)
function toFinalBaseType(node) {

@@ -504,2 +429,5 @@ // Nothing to do if no type (or if array/struct type)

}
// If that is an e
while (baseType._artifact.target)
baseType = baseType._artifact.type;
node.type = {

@@ -529,2 +457,9 @@ path: [ { id: baseType._artifact.name.absolute } ],

}
considerForImplicitRedirectionOrAutoExposure(artifact, artifactName);
});
// For all elements of 'artifact', see whether they require implicit redirection or the creation of an
// artificial exposing projection (via `@cds.autoexpose`). Do this recursively for artificially generated
// projections, too
function considerForImplicitRedirectionOrAutoExposure(artifact, artifactName) {
// Perform implicit re-targeting of association elements based on exposure:

@@ -546,2 +481,4 @@ forEachMemberRecursively(artifact, (member, memberName) => {

// console.log(`Auto-exposing target ${member.target._artifact.name.absolute} of association ${artifactName}.${memberName} as ${projection.name.absolute}`);
// The newly created exposing projection may in turn contain associations to non-exposed artifacts
considerForImplicitRedirectionOrAutoExposure(projection, projection.name.absolute);
}

@@ -555,3 +492,3 @@ }

});
});
}

@@ -611,5 +548,5 @@ // Redirect element 'assoc' with name 'assocName' in 'artifact' with name 'artifactName' implicitly

// A projection or view with a single query and a single, simple source also exposes the source
if (exposedArtifact.queries && exposedArtifact.queries.length == 1
&& exposedArtifact.queries[0].from.length == 1 && exposedArtifact.queries[0].from[0].path) {
let from = exposedArtifact.queries[0].from;
if (exposedArtifact.$queries && exposedArtifact.$queries.length == 1
&& exposedArtifact.$queries[0].from.length == 1 && exposedArtifact.$queries[0].from[0].path) {
let from = exposedArtifact.$queries[0].from;
// Sanity check

@@ -676,4 +613,4 @@ if (!from[0]._artifact) {

id : elemName,
absolute: projectionAbsoluteName,
element: elemName,
// absolute: projectionAbsoluteName,
// element: elemName,
path: [ { id : art.name.id }, { id : elemName } ]

@@ -723,6 +660,11 @@ }

queries: [ query ],
source: source,
$queries: [ query ],
// source: source,
elements: query.elements,
$generatedByAutoExposure: true,
};
// copy annotations from art to projection
for (let a of Object.keys(art).filter(x => x.startsWith('@'))) {
projection[a] = art[a];
}
setProp(projection, '_service', service);

@@ -758,3 +700,3 @@ // Sanity check: Can't already be there (checked above)

function isDollarSelfOperand(arg) {
return arg.path && arg.path.length == 1 && (arg.path[0].id == '$self' || model.options.oldstyleSelf && arg.path[0].id == 'self');
return arg.path && arg.path.length == 1 && (arg.path[0].id == '$self');
}

@@ -761,0 +703,0 @@

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

{
// Paths that start with an artifact of protected kind are special
// either ignore them in QAT building or in path rewriting
const internalArtifactKinds = ['builtin'/*, '$parameters'*/, 'param'];

@@ -36,4 +39,3 @@ const { error, signal } = alerts(model);

{
let type = art._finalType;
if(art.kind === 'element' && type && type.target) // contexts have no type
if(art.kind === 'element' && art.target)
{

@@ -50,7 +52,7 @@ /* Create the prefix string up to the main artifact which is

*/
if(type.foreignKeys && !type.$fkPathPrefixTree)
if(art.foreignKeys && !art.$fkPathPrefixTree)
{
type.$fkPathPrefixTree = { children: Object.create(null) };
forEachGeneric(type, 'foreignKeys', fk => {
let ppt = type.$fkPathPrefixTree;
art.$fkPathPrefixTree = { children: Object.create(null) };
forEachGeneric(art, 'foreignKeys', fk => {
let ppt = art.$fkPathPrefixTree;
fk.targetElement.path.forEach(ps => {

@@ -69,7 +71,7 @@ if(!ppt.children[ps.id])

tableAliases: [ art.name.id ],
type,
art,
location: 'onCondAssoc',
callback: [ flyTrap ]
};
walk(type.onCond || type.on, env);
walk(art.onCond || art.on, env);
}

@@ -83,3 +85,3 @@ }

{
let queries = art.queries;
let queries = art.$queries;
let fullJoins = fullJoinOption;

@@ -90,6 +92,7 @@

/*
HANA cannot process associations with parameters, filters on first FROM path step and mixin-assoc usages
Filters and mixin usages will lead to a minimum join translation (only FROM clause and MIXINs)
An association path step with parameters requires a full join conversion or if parameter path step is
not at leaf position (entity/view with parameters followed by another association in FROM clause)
HANA cannot process associations with parameters, filters in FROM paths and mixin-assoc usages
Filters and mixin usages will lead to a minimum join translation (only FROM clause and MIXIN usages)
A full join conversion is required if an assoc path step has parameters or if parameter path step is
not at leaf position (entity/view with parameters followed by another association in FROM clause), or
if a filter has a cardinality ':1'.
*/

@@ -104,10 +107,10 @@ if(!fullJoinOption) {

env.minimum = env.minimum ||
(env.location == 'from' ? !!pathDict.path.filter(p => isEntityOrView(p._artifact))[0].where
(env.location == 'from' ? pathDict.path.some(p => p.where) //!!pathDict.path.filter(p => isEntityOrView(p._artifact))[0].where
: (pathDict.path[0]._navigation && pathDict.path[0]._navigation.kind == 'element' && pathDict.path.length > 1));
env.full = env.full || pathDict.path.some((e, i, a) => {
return !!e.namedArgs && (e._artifact.target || i < a.length-1) })
return !!e.namedArgs && (e._artifact.target || i < a.length-1) || e.cardinality })
}
}
queries.map(q => walkQuery(q, env));
queries.forEach(q => walkQuery(q, env));

@@ -133,5 +136,5 @@ if(!env.minimum && !env.full)

};
queries.map(q => createQAForMixinAssoc(q, env));
queries.map(q => walkQuery(q, env));
queries.map(q => createQAForFromClauseSubQuery(q, env));
queries.forEach(q => createQAForMixinAssoc(q, env));
queries.forEach(q => walkQuery(q, env));
queries.forEach(q => createQAForFromClauseSubQuery(q, env));

@@ -141,6 +144,6 @@ // 2) Walk over each from table path, transform it into a join tree

env.callback = createInnerJoins;
queries.map(q => walkQuery(q, env));
queries.forEach(q => walkQuery(q, env));
// 3) Transform toplevel FROM block into cross join
queries.map(q => createCrossJoins(q));
queries.forEach(q => createCrossJoins(q));

@@ -150,3 +153,3 @@ // 4) Transform all remaining join relevant paths into left outer joins and connect with

// of each $tableAlias.
queries.map(q => createLeftOuterJoins(q, env));
queries.forEach(q => createLeftOuterJoins(q, env));

@@ -158,6 +161,6 @@ // 5) Rewrite ON condition paths that are part of the original FROM block

env.callback = rewriteGenericPaths;
queries.map(q => walkQuery(q, env));
queries.forEach(q => walkQuery(q, env));
// 7) Attach firstFilterConds to Where Condition.
queries.map(q => attachFirstFilterConditions(q));
queries.forEach(q => attachFirstFilterConditions(q));

@@ -177,5 +180,2 @@ // TODO: support parameters

query.from.splice(0, query.from.length, { op: { val: 'join' }, join: 'cross', args: [...query.from ] });
// Recurse into sub queries
query.queries.map(q => createCrossJoins(q));
}

@@ -209,5 +209,2 @@ }

query.from = [ joinTree ];
// Recurse into sub queries
query.queries.map(q => createLeftOuterJoins(q, env));
}

@@ -226,11 +223,18 @@ }

{
for (let taName in query.$tableAliases) {
if (!['$self', '$projection'].includes(taName)) {
let ta = query.$tableAliases[taName];
if(!ta.$QA) {
ta.$QA = createQA(env, ta._finalType, undefined, taName);
incAliasCount(env, ta.$QA);
if(ta.name && ta.name.id) {
ta.name.id = ta.$QA.name.id;
}
}
}
}
// Only subqueries of the FROM clause have a name (which is the alias)
if(query.op.val === 'query' && query.name.id)
if(query.op.val === 'query' && query.name.id && query._tableAlias)
{
// Set the QA for the outer ON cond paths and rename the query name itself
let QA = query._tableAlias._parent.$tableAliases[query.name.id].$QA = createQA(env, query);
incAliasCount(env, QA);
query.name.id = QA.name.id;
query.queries.map(q => createQAForFromClauseSubQuery(q, env));
query.name.id = query._tableAlias._parent.$tableAliases[query.name.id].$QA.name.id;
}

@@ -261,4 +265,2 @@ }

});
query.queries.map(q => createQAForMixinAssoc(q, env));
}

@@ -277,3 +279,3 @@ }

{
if(pathNode.rewritten)
if(pathNode.$rewritten)
return;

@@ -336,4 +338,2 @@

}
// Recurse into sub queries
query.queries.map(q => attachFirstFilterConditions(q));
}

@@ -426,51 +426,146 @@ }

let srcTableAlias = { id: assocSourceQA.name.id, _artifact: assocSourceQA._artifact };
let tgtTableAlias = { id: assocQAT.$QA.name.id, _artifact: assocQAT.$QA._artifact };
let tgtTableAlias = { id: assocQAT.$QA.name.id, _artifact: assocQAT.$QA._artifact };
let assocElt = assocQAT.origin._artifact;
node.on = createOnCondition(assocQAT.origin._artifact, srcTableAlias, tgtTableAlias);
// Inject the ON condition of the managed association
if(assocElt._finalType.foreignKeys)
if(assocQAT._filter)
{
/*
Get both the source and the target column names for the EQ term.
For the src side provide a path prefix for all paths that is the assocElement name itself preceded by
the path up to the first lead artifact (usually the entity or view) (or in QAT speak: follow the parent
QATs until a QA has been found).
*/
let srcPaths = flattenElement(assocElt, true, assocElt.name.element.replace(/\./g, pathDelimiter));
let tgtPaths = flattenElement(assocElt, false);
// Filter conditions are unique for each JOIN, they don't need to be copied
let filter = assocQAT._filter;
rewritePathsInExpression(filter, function(pathNode) {
return [ tgtTableAlias, pathNode.path ];
});
if(srcPaths.length != tgtPaths.length)
throw Error('srcPaths length ['+srcPaths.length+'] != tgtPaths length ['+tgtPaths.length+']');
// If toplevel ON cond op is AND add filter condition to the args array,
// create a new toplevel AND op otherwise
let onCond = (Array.isArray(node.on) ? node.on[0] : node.on);
if(onCond.op.val == 'and')
// parenthesize filter
onCond.args.push( [ filter ] );
else
// parenthesize onCond and filter
node.on = [ { op: { val: 'and' }, args: [ [ onCond ], [ filter ] ] } ];
}
return node;
/*
Put all src/tgt path siblings into the EQ term and create the proper path objects
with the src/tgt table alias path steps in front.
// produce the ON condition for a given association
function createOnCondition(assoc, srcAlias, tgtAlias)
{
let prefixes = [ assoc.name.id ];
/* This is no art and can be removed once ON cond for published
and renamed backlink assocs are publicly available. Example:
entity E { ...; toE: association to E; toEb: association to E on $self = toEb.toE; };
entity EP as projection on E { *, toEb as foo };
This requires ON cond rewritten to: $self = foo.toE but instead its still $self = toEb.toE,
so prefix 'foo' won't match....
*/
let args = [];
for(let i = 0; i < srcPaths.length; i++)
if(assoc.origin && assoc.origin._artifact && !prefixes.includes(assoc.origin._artifact.name.id))
prefixes.push(assoc.origin._artifact.name.id);
// produce the ON condition of the managed association
if(assoc.foreignKeys)
{
args.push({op: {val: '=' },
args: [ constructPathNode( [ srcTableAlias, srcPaths[i] ] ),
constructPathNode( [ tgtTableAlias, tgtPaths[i] ] ) ] }); // eslint-disable-line indent-legacy
/*
Get both the source and the target column names for the EQ term.
For the src side provide a path prefix for all paths that is the assocElement name itself preceded by
the path up to the first lead artifact (usually the entity or view) (or in QAT speak: follow the parent
QATs until a QA has been found).
*/
let srcPaths = flattenElement(assoc, true, assoc.name.element.replace(/\./g, pathDelimiter));
let tgtPaths = flattenElement(assoc, false);
if(srcPaths.length != tgtPaths.length)
throw Error('srcPaths length ['+srcPaths.length+'] != tgtPaths length ['+tgtPaths.length+']');
/*
Put all src/tgt path siblings into the EQ term and create the proper path objects
with the src/tgt table alias path steps in front.
*/
let args = [];
for(let i = 0; i < srcPaths.length; i++)
{
args.push({op: {val: '=' },
args: [ constructPathNode( [ srcAlias, srcPaths[i] ] ),
constructPathNode( [ tgtAlias, tgtPaths[i] ] ) ] }); // eslint-disable-line indent-legacy
}
// Parenthesize each AND term
return [ (args.length > 1 ? { op: { val: 'and' }, args: [ ...args.map(a=>[a]) ] } : args[0] ) ];
}
// Parenthesize each AND term
node.on = [ (args.length > 1 ? { op: { val: 'and' }, args: [ ...args.map(a=>[a]) ] } : args[0] ) ];
else
return cloneOnCondition(assoc.onCond || assoc.on);
}
// Inject the ON condition of the unmanaged association
else if (assocElt.onCond || assocElt.on)
{
node.on = clone(assocElt.onCond || assocElt.on);
rewritePathsInExpression(node.on, function(pathNode)
// clone ON condition with rewritten paths and substituted backlink conditions
function cloneOnCondition(expr)
{
if(expr.op && expr.op.val === 'xpr')
return cloneOnCondExprStream(expr);
else
return cloneOnCondExprTree(expr);
}
function cloneOnCondExprStream(expr) {
let args = expr.args;
let result = { op: { val: expr.op.val }, args: [] };
for(let i = 0; i < args.length; i++)
{
if(args[i].op && args[i].op.val === 'xpr')
{
result.args.push(cloneOnCondition(args[i]));
}
// If this is a backlink condition, produce the
// ON cond of the forward assoc with swapped src/tgt aliases
else if(i < args.length-2 && args[i].path && args[i+1] == '=' && args[i+2].path)
{
let fwdAssoc = getForwardAssociation(args[i].path, args[i+2].path);
if(fwdAssoc)
{
result.args.push(createOnCondition(fwdAssoc, tgtAlias, srcAlias));
i += 2; // skip next two tokens and continue with loop
continue;
}
}
result.args.push(rewritePathNode(args[i]));
}
return result;
}
function cloneOnCondExprTree(expr) {
// keep parentheses intact
if(Array.isArray(expr))
return expr.map(cloneOnCondition);
// If this is a backlink condition, produce the
// ON cond of the forward assoc with swapped src/tgt aliases
let fwdAssoc = getForwardAssociationExpr(expr);
if(fwdAssoc)
return createOnCondition(fwdAssoc, tgtAlias, srcAlias);
// If this is an ordinary expression, clone it and mangle its arguments
// this will substitute multiple backlink conditions ($self = ... AND $self = ...AND ...)
if(expr.op)
return { op: { val: expr.op.val }, args: expr.args.map(cloneOnCondition) };
// If this is a regular path, rewrite it
return rewritePathNode(expr);
}
function rewritePathNode(pathNode)
{
let tableAlias;
let path = pathNode.path;
if(!path) // it's not a path return it
return pathNode;
let [head, ...tail] = path;
if(internalArtifactKinds.includes(head._artifact.kind)) // don't rewrite path
return pathNode;
if(assocSourceQA.mixin)
{
if(head.id === '$projection')
throw Error('Following mix-in association "' + assocElt.name.id +
throw Error('Following mix-in association "' + assoc.name.id +
'" in defining view is not allowed with ON condition from projection: ' +

@@ -510,11 +605,11 @@ pathAsStr(pathNode.path, '"'));

return constructTableAliasAndTailPath(path);
[ tableAlias, path ] = constructTableAliasAndTailPath(path);
}
else // ON condition of non-mixin association
{
if(head.id === assocQAT.name.id) // target side
if(prefixes.includes(head.id)) // target side
{
// no element prefix on target side
path = translateONCondPath(tail);
tableAlias = tgtTableAlias;
tableAlias = tgtAlias;
}

@@ -528,39 +623,31 @@ else // source side

tableAlias = srcTableAlias;
tableAlias = srcAlias;
// if path is not an absolute path, prepend element prefix
path = translateONCondPath(path, !isAbsolutePath ? assocElt.$elementPrefix : undefined);
path = translateONCondPath(path, !isAbsolutePath ? assoc.$elementPrefix : undefined);
}
}
return [ tableAlias, path ];
});
}
else
{
throw Error('assocQAT has neither foreign keys nor an on condition:' +
assocElt.name.absolute + pathDelimiter + assocElt.name.element);
}
let pathStr = path.map(ps => ps.id).join(pathDelimiter);
return constructPathNode([ tableAlias, { id: pathStr, _artifact: pathNode._artifact } ]);
}
if(assocQAT._filter)
{
// Filter conditions are unique for each JOIN, they don't need to be copied
let filter = assocQAT._filter;
rewritePathsInExpression(filter, function(pathNode) {
return [ tgtTableAlias, pathNode.path ];
});
// Return the original association if expr is a backlink term, undefined otherwise
function getForwardAssociationExpr(expr) {
if(expr.op && expr.op.val == '=' && expr.args.length == 2) {
return getForwardAssociation(expr.args[0].path, expr.args[1].path);
}
return undefined;
}
// If toplevel ON cond op is AND add filter condition to the args array,
// create a new toplevel AND op otherwise
let onCond = (Array.isArray(node.on) ? node.on[0] : node.on);
function getForwardAssociation(lhs, rhs) {
if(lhs && rhs) {
if(rhs.length == 1 && rhs[0].id == '$self' && lhs.length > 1 && prefixes.includes(lhs[lhs.length-2].id))
return lhs[lhs.length-1]._artifact;
if(lhs.length == 1 && lhs[0].id == '$self' && rhs.length > 1 && prefixes.includes(rhs[rhs.length-2].id))
return rhs[rhs.length-1]._artifact;
}
return undefined;
}
} // createOnCondition
} // createJoinQA
if(onCond.op.val == 'and')
// parenthesize filter
onCond.args.push( [ filter ] );
else
// parenthesize onCond and filter
node.on = [ { op: { val: 'and' }, args: [ [ onCond ], [ filter ] ] } ];
}
return node;
}
/*

@@ -622,12 +709,10 @@ A QA (QueryArtifact) is a representative for a table/view that must appear

let node = {
rewritten: true,
$rewritten: true,
path : pathSteps.map(p => {
let o = Object.assign({}, { id: p.id });
let o = {};
Object.keys(p).forEach(k => {
if(!['_'].includes(k[0]))
o[k] = p[k];
});
setProp(o, '_artifact', p._artifact );
if(p.where)
o.where = p.where;
if(p.namedArgs)
o.namedArgs = p.namedArgs;
if(p.args)
o.args = p.args;
return o; })

@@ -669,3 +754,3 @@ };

// terminate if element is unstructured
if(!element._finalType.foreignKeys && !element.elements)
if(!element.foreignKeys && !element.elements)
return [ { id: prefix, _artifact: element } ];

@@ -675,7 +760,7 @@

// get paths of managed assocs (unmanaged assocs are not allowed in FK paths)
if(element._finalType.foreignKeys)
if(element.foreignKeys)
{
for(let fkn in element._finalType.foreignKeys)
for(let fkn in element.foreignKeys)
{
let fk = element._finalType.foreignKeys[fkn];
let fk = element.foreignKeys[fkn];
// once a fk is to be followed, treat all sub patsh as srcSide, this will add fk.name.id only

@@ -757,3 +842,3 @@ if(srcSide)

pathStr += ps.id;
return (ps._artifact._finalType.target); // true if it has a target => is assoc => terminate find
return (ps._artifact.target); // true if it has a target => is assoc => terminate find
});

@@ -820,3 +905,3 @@ return [ assocStep, path.slice(path.indexOf(assocStep)+1), pathStr ];

{
if(path[path.length-1].id != '$self' && pathDict._artifact._finalType.elements)
if(path[path.length-1].id != '$self' && pathDict._artifact.elements)
signal(error`${'Only scalar types allowed in this location of a query: ' + pathAsStr(pathDict.path, "'")}`);

@@ -831,5 +916,5 @@

path.forEach(ps => {
if(ps._artifact._finalType.target)
if(ps._artifact.target)
{
if(ps._artifact._finalType.on)
if(ps._artifact.onCond || ps._artifact.on)
{

@@ -842,3 +927,3 @@ if(env.location !== 'onCondAssoc')

let la1 = pathDict.path[pathDict.path.indexOf(ps)+1];
if(la1 && !ps._artifact._finalType.$fkPathPrefixTree.children[la1.id])
if(la1 && !ps._artifact.$fkPathPrefixTree.children[la1.id])
signal(error`Pathstep ' + la1.id + ' is not foreign key of association ' +

@@ -975,4 +1060,10 @@ ps.id + ' in ON condition path: ' + pathAsStr(pathDict.path)`);

qat = linkToOrigin(pathStep._artifact, pathStep.id, qatParent, undefined, pathStep.location);
if(pathStep.where)
qat._filter = pathStep.where;
/*
Query filter have precedence over default filters.
Clone default filter for each usage to avoid path rewriting of the definition.
TODO: If Filter become JOIN relevant, default filters MUST BE cloned before starting the transformation
or the paths won't be added to the QAT and the rewriting would be done on the filter definition.
*/
if(pathStep.where /*|| pathStep._artifact.where*/)
qat._filter = pathStep.where /*|| clone(pathStep._artifact.where)*/;
if(pathStep.namedArgs)

@@ -1080,5 +1171,2 @@ qat._namedArgs= pathStep.namedArgs;

// walk all subqueries of this query
query.queries.map(q => walkQuery(q, env));
function walkFrom(query)

@@ -1107,3 +1195,3 @@ {

env.tableAliases = aliases;
walk(query.on, env)
walk(query.onCond || query.on, env)
delete env.tableAliases;

@@ -1150,3 +1238,3 @@ }

let art = path && path.length && path[path.length-1]._artifact;
if(art && !['$builtin', '$parameters', 'param'].includes(art.kind))
if(art && !internalArtifactKinds.includes(art.kind))
{

@@ -1157,3 +1245,3 @@ if(env.callback)

if(Array.isArray(env.callback))
env.callback.map(cb => cb(node, env));
env.callback.forEach(cb => cb(node, env));
else

@@ -1163,2 +1251,12 @@ env.callback(node, env);

/*
NOTE: As long as association path steps are not allowed in filters,
it is not required to walk over filter expressions.
Simple filter paths are rewritten inin createJoinTree (first filter)
and createJoinQA (subsequent one that belong to the ON condition).
If the filter becomes JOIN relevant, default FILTERS (part of the
association definition) MUST be CLONED to each assoc path step
BEFORE resolution.
let filterEnv = Object.assign({walkover: {} }, env);

@@ -1175,2 +1273,3 @@ filterEnv.location = 'filter';

}
*/
// TODO: Parameter expressions!

@@ -1228,3 +1327,2 @@ }

function clone(obj) {

@@ -1231,0 +1329,0 @@ let newObj;

{
"name": "@sap/cds-compiler",
"version": "1.5.0",
"version": "1.8.0",
"dependencies": {
"ajv": {
"version": "6.1.1",
"dependencies": {
"json-schema-traverse": {
"version": "0.3.1"
},
"fast-deep-equal": {
"version": "1.0.0"
},
"fast-json-stable-stringify": {
"version": "2.0.0"
}
}
},
"antlr4": {
"version": "4.7.1"
},
"commander": {
"version": "2.17.1"
},
"fs-extra": {
"version": "7.0.0",
"dependencies": {
"universalify": {
"version": "0.1.2"
},
"graceful-fs": {
"version": "4.1.15"
},
"jsonfile": {
"version": "4.0.0",
"dependencies": {
"graceful-fs": {
"version": "4.1.15"
}
}
}
}
},
"resolve": {
"version": "1.5.0",
"version": "1.8.1",
"dependencies": {

@@ -47,0 +11,0 @@ "path-parse": {

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

{"bin":{"cdsc":"bin/cdsc.js"},"bundleDependencies":false,"dependencies":{"ajv":"6.1.1","antlr4":"4.7.1","commander":"2.17.1","fs-extra":"7.0.0","resolve":"1.5.0","sax":"1.2.4"},"deprecated":false,"description":"Standard-Feature-Set Vanilla-CDS in Product Quality","keywords":["CDS"],"main":"lib/main.js","name":"@sap/cds-compiler","repository":{"type":"git","url":"git@github.wdf.sap.corp/CDS/cds-compiler.git"},"version":"1.5.0","license":"SEE LICENSE IN developer-license-3.1.txt"}
{"bin":{"cdsc":"bin/cdsc.js","cdshi":"bin/cdshi.js","cdsse":"bin/cdsse.js"},"bundleDependencies":false,"dependencies":{"antlr4":"4.7.1","resolve":"1.8.1","sax":"1.2.4"},"deprecated":false,"description":"CDS (Core Data Services) compiler and backends","keywords":["CDS"],"main":"lib/main.js","name":"@sap/cds-compiler","version":"1.8.0","license":"SEE LICENSE IN developer-license-3.1.txt"}

@@ -8,33 +8,6 @@ # Getting started

[Installation and Usage](#installation-and-usage)
[Documentation](#documentation)
[Fiori annotations](doc/FioriAnnotations.md)
[Command invocation](#command-invocation)
## Installation and Usage
### github
***Do not add direct dependency to cdsv's github project!***
### Snapshots/Milestones/Releases
Configure Nexus registry:
* snapshots
```
npm config set registry "http://nexus.wdf.sap.corp:8081/nexus/content/groups/build.snapshots.npm"
```
* milestones
```
npm config set registry "http://nexus.wdf.sap.corp:8081/nexus/content/groups/build.milestones.npm"
```
* releases
```
npm config set registry "http://nexus.wdf.sap.corp:8081/nexus/content/groups/build.releases.npm"
```
Install via npm:

@@ -59,5 +32,5 @@

```bash
cdsc [options] <file...>
cdsc <command> [options] <file...>
```
See `cdsc --help` for the options.
See `cdsc --help` for commands and options.

@@ -69,5 +42,1 @@ The exit code is similar to [`grep` and other commands](http://stackoverflow.com/questions/1101957/are-there-any-standard-exit-status-codes-in-linux):

* `2`: commmand invocation error (invalid options, repeated file name)
## Documentation
See <https://github.wdf.sap.corp/pages/cap/CDS>.

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 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

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