Socket
Socket
Sign inDemoInstall

@sap/cds-compiler

Package Overview
Dependencies
3
Maintainers
3
Versions
99
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.8.1 to 1.17.1

lib/base/message-builder.js

152

bin/cdsc.js

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

const compiler = require('../lib/main');
const { compactModel } = require('../lib/json/to-csn');
const { toOdataWithCsn, toHanaWithCsn } = require('../lib/backends');
var util = require('util');
var fs = require('fs');
var path = require('path');
var reveal = require('./raw-output');
const { optionProcessor } = require('../lib/backends');
var reveal = require('../lib/model/revealInternalProperties');
const { optionProcessor } = require('../lib/optionProcessor');
const { translatePathLocations } = require('../lib/base/messages')

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

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

@@ -71,4 +79,4 @@ if (cmdLine.options.version) {

// Do the work for the selected command (default 'toCsn'
executeCommandLine(cmdLine.command || 'toCsn', cmdLine.options, cmdLine.args);
// Do the work for the selected command (default 'toCsn')
executeCommandLine(cmdLine.command || 'toCsn', cmdLine.options, cmdLine.args, cmdLine.unknownOptions);
} catch (err) {

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

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

@@ -122,3 +130,2 @@ const messageLevels = { Error: 0, Warning: 1, Info: 2, None: 3 };

toSwagger,
toTntSpecificOutput,
}

@@ -129,3 +136,21 @@

}
compiler.compile( args, undefined, options )
// special case when we want to call a transformer directly
// without compiling before that (for now only available for toOdata, toHana, toSql)
// works only with combination of --beta-mode in the transformers
if (options.skipCompile && ['toOdata', 'toHana', 'toSql'].includes(command)) {
if (args.length !== 1)
throw new Error(`--skip-compile option works with only one file`);
let csn = '';
try {
csn = JSON.parse(fs.readFileSync(args[0]));
} catch (e) {
throw new Error(`${args[0]} must be a valid json file`);
}
options.betaMode = true;
csn.options = options;
commands[command](csn);
} else {
compiler.compile( args, undefined, options )
.then( commands[command] )

@@ -135,2 +160,9 @@ .then( displayMessages, displayErrors )

}
if (unknownOptions.length) {
let out = process.stdout;
unknownOptions.forEach(msg => out.write(`cdsc: INFO: ${msg}\n`));
}
// Execute the command line option '--to-cdl' and display the results.

@@ -150,3 +182,3 @@ // Return the original model (for chaining)

// Result already provided by caller
displayNamedCsn(model, 'csn', options);
displayNamedXsn(model, 'csn', options);
return model;

@@ -158,9 +190,7 @@ }

function toHana( model ) {
let hanaResult = compiler.toHana(model);
let hanaResult = options.newTransformers ? toHanaWithCsn(compactModel(model, options), options) : compiler.toHana(model);
for (let name in hanaResult.hdbcds) {
writeToFileOrDisplay(options.out, name + '.hdbcds', hanaResult.hdbcds[name]);
}
if (hanaResult.csn) {
displayNamedCsn(hanaResult._augmentedCsn, 'hana_csn', options);
}
displayNamedXsnOrCsn(hanaResult._augmentedCsn, hanaResult.csn, 'hana_csn', options);
return model;

@@ -172,3 +202,11 @@ }

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

@@ -192,6 +230,3 @@ // <service>_metadata.xml (metadata)

}
// odata_csn.json resp. odata_csn_raw.txt
if (odataResult._augmentedCsn) {
displayNamedCsn(odataResult._augmentedCsn, 'odata_csn', options);
}
displayNamedXsnOrCsn(odataResult._augmentedCsn, odataResult.csn, 'odata_csn', options);
return model;

@@ -204,5 +239,9 @@ }

let renameResult = compiler.toRename(model);
let storedProcedure = 'PROCEDURE RENAME_' + model.options.toRename.names.toUpperCase() + '_TO_PLAIN LANGUAGE SQLSCRIPT AS BEGIN\n';
for (let name in renameResult.rename) {
writeToFileOrDisplay(options.out, 'rename_' + name + '.sql', renameResult.rename[name] + '\n', true);
storedProcedure += ' --\n -- ' + name + '\n --\n';
storedProcedure += renameResult.rename[name];
}
storedProcedure += "END;\n";
writeToFileOrDisplay(options.out, 'storedProcedure_' + model.options.toRename.names + '_to_plain.sql', storedProcedure, true);
return model;

@@ -215,8 +254,9 @@ }

let sqlResult = compiler.toSql(model);
for (let name in sqlResult.sql) {
writeToFileOrDisplay(options.out, name + '.sql', sqlResult.sql[name] + '\n', true);
}
if (sqlResult.csn) {
displayNamedCsn(sqlResult._augmentedCsn, 'sql_csn', options);
}
['hdbtabletype', 'hdbtable', 'hdbindex', 'hdbfulltextindex', 'hdbview', 'sql'].forEach(pluginName => {
for(let name in sqlResult[pluginName]) {
writeToFileOrDisplay(options.out, name + '.' + pluginName, sqlResult[pluginName][name] + '\n', true);
}
});
displayNamedXsnOrCsn(sqlResult._augmentedCsn, sqlResult.csn, 'sql_csn', options);
return model;

@@ -232,4 +272,4 @@ }

}
if (swaggerResult.csn) {
displayNamedCsn(swaggerResult._augmentedCsn, 'swagger_csn', options);
if (swaggerResult._augmentedCsn) {
displayNamedXsn(swaggerResult._augmentedCsn, 'swagger_csn', options);
}

@@ -239,25 +279,2 @@ return model;

// Execute the (old) command line option '--tnt-output' and display the results.
// FIXME: This is the only one that cannot yet be composed from others
// Return the original model (for chaining)
function toTntSpecificOutput( model ) {
// TODO: use async file-system API
// Perform TNT-specific post-processing
let result = compiler.toTntSpecificOutput(model, model.options);
// Write result to files in target directory
// FIXME: Check in options which parts actually need to be generated
// Write annotations.xml and metadata.xml only if there is exactly one service
// FIXME: For backward compatibility only, should be removed soon
if (result.annotations && result.metadata) {
writeToFileOrDisplay(model.options.out, 'annotations.xml', result.annotations);
writeToFileOrDisplay(model.options.out, 'metadata.xml', result.metadata);
}
writeToFileOrDisplay(model.options.out, 'csn.json', result.csn);
for (let serviceName in result.services) {
writeToFileOrDisplay(model.options.out, serviceName + '_annotations.xml', result.services[serviceName].annotations);
writeToFileOrDisplay(model.options.out, serviceName + '_metadata.xml', result.services[serviceName].metadata);
}
return model;
}
// Display error messages in `err` resulting from a compilation. Also set

@@ -292,4 +309,6 @@ // process.exitCode - process.exit() will force the process to exit as quickly

for (let msg of messages) {
if (messageLevels[ msg.severity ] <= options.warning)
console.error( compiler.messageString( msg, normalizeFilename, !options.showMessageId, !options.testMode ) );
if (options.internalMsg)
console.error( util.inspect( msg, { depth: null, maxArrayLength: null} ) );
else if (messageLevels[ msg.severity ] <= options.warning)
console.error( compiler.messageString( msg, normalizeFilename, !options.showMessageId ) );
}

@@ -300,2 +319,10 @@ }

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

@@ -305,11 +332,24 @@ // or display it to stdout if 'options.out' is '-'.

// written in raw form to '<name>_raw.txt'.
function displayNamedCsn(model, name, options) {
function displayNamedXsn(xsn, name, options) {
if (options.rawOutput) {
writeToFileOrDisplay(options.out, name + '_raw.txt', util.inspect(reveal(model), false, null), true);
writeToFileOrDisplay(options.out, name + '_raw.txt', util.inspect(reveal(xsn), false, null), true);
}
else {
writeToFileOrDisplay(options.out, name + '.json', compiler.toCsn(model), true);
else if (options.internalMsg) {
writeToFileOrDisplay(options.out, name + '_raw.txt', util.inspect(reveal(xsn).messages, { depth: null, maxArrayLength: null}), true);
}
else if (!options.lintMode) {
let csn = compiler.toCsn(xsn, options);
writeToFileOrDisplay(options.out, name + '.json', csn, true);
}
}
function displayNamedCsn(csn, name, options) {
if (options.internalMsg) {
writeToFileOrDisplay(options.out, name + '_raw.txt', csn.messages, true);
}
else if (!options.lintMode && !options.internalMsg) {
writeToFileOrDisplay(options.out, name + '.json', csn, true);
}
}
// Write the result 'content' to a file 'filename' in directory 'dir', except if 'dir' is '-'.

@@ -321,2 +361,4 @@ // In that case, display 'content' to stdout.

function writeToFileOrDisplay(dir, filename, content, omitHeadline = false) {
if (options.lintMode && !options.rawOutput || options.internalMsg)
return;
filename = filename.replace(/[:/\\]/g, '_');

@@ -323,0 +365,0 @@ if (!(content instanceof String || typeof content == 'string')) {

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

const commands = {
complete
complete, lint
}

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

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

@@ -84,2 +84,20 @@ }

function lint( err, buf ) {
if (err)
return usage( err );
let fname = path.resolve( '', file );
compiler.compile( [file], '', { lintMode: true, betaMode: true } , { [fname]: buf } )
.then( display, display );
return true;
function display( xsnOrErr ) {
let messages = xsnOrErr.messages || xsnOrErr.errors;
if (!messages)
return usage( xsnOrErr );
for (let msg of messages)
console.log( compiler.messageString( msg ) );
return true;
}
}
function tokensAt( buf, offset, symbol ) {

@@ -86,0 +104,0 @@ let src = buf.substring( 0, offset ) + '≠' + buf.substring( offset );

@@ -473,3 +473,3 @@ # Name Resolution in CDL (CDx/Language)

`Date`, `Time`, `Timestamp`, `DateTime`, and `Boolean`.
More artifacts are defined with the options `--hana-flavor` or `--tnt-flavor`.
More artifacts are defined with the options `--hana-flavor`.

@@ -476,0 +476,0 @@ When searching for an annotation (after the initial `@`), the last search environment

@@ -8,129 +8,17 @@ 'use strict';

const { transformForHana } = require('./transform/forHana');
const { transformForHanaWithCsn } = require('./transform/forHanaNew');
const { compact, compactSorted } = require('./json/compactor')
const { compactModel } = require('./json/to-csn')
const { compactModel, sortCsn } = require('./json/to-csn')
const { toCdsSource } = require('./render/toCdl');
const { toSqlDdl } = require('./render/toSql');
const { toRenameDdl } = require('./render/toRename');
const { transform4odata, getServiceNames } = require('./transform/forOdata');
const csn2edm = require('./edm/csn2edm');
const { transform4odata } = require('./transform/forOdata');
const { transform4odataWithCsn } = require('./transform/forOdataNew');
const { csn2edm, csn2edmAll } = require('./edm/csn2edm');
const { mergeOptions } = require('./model/modelUtils');
const { transformTntExtensions } = require('./transform/tntSpecific');
const alerts = require('./base/alerts');
const { setProp } = require('./base/model');
var { CompilationError, sortMessages } = require('./base/messages');
const { createOptionProcessor } = require('./base/optionProcessor');
const { optionProcessor } = require('./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(' --new-redirect-impl')
.option(' --assoc-on-rewrite')
.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)
--new-redirect-impl (internal) Use new implementation of implicit redirection
--assoc-on-rewrite (internal) Rewrite ON conditions and keys specification for
associations and compositions, requires --new-redirect-impl
--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.

@@ -163,5 +51,7 @@ // The following options control what is actually generated (see help above):

function toHana(model, options) {
// Optional wrapper?
// In case of API usage the options are in the 'options' argument
// put the OData specific options under the 'options.toHana' wrapper
// and leave the rest under 'options'
if (options && !options.toHana) {
options = { toHana : options };
_wrapRelevantOptionsForCmd(options, 'toHana');
}

@@ -207,3 +97,5 @@ // Provide defaults and merge options with those from model

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

@@ -218,37 +110,62 @@

// ----------- toOdata -----------
// The twin of the toHana function but using CSN as a model
function toHanaWithCsn(csn, options) {
// In case of API usage the options are in the 'options' argument
// put the OData specific options under the 'options.toHana' wrapper
// and leave the rest under 'options'
if (options && !options.toHana) {
_wrapRelevantOptionsForCmd(options, 'toHana');
}
// Provide defaults and merge options with those from model
options = mergeOptions({ toHana : getDefaultBackendOptions().toHana }, csn.options, options);
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...>
// Provide something to generate if nothing else was given (conditional default)
if (!options.toHana.src && !options.toHana.csn) {
options.toHana.src = true;
}
Generate ODATA metadata and annotations, or CSN.
// Backward compatibility for old naming modes
// FIXME: Remove after a few releases
const { warning, signal } = alerts(csn);
if (options.toHana.names == 'flat') {
signal(warning`Option "{ toHana.names: 'flat' }" is deprecated, use "{ toHana.names: 'plain' }" instead`);
options.toHana.names = 'plain';
}
else if (options.toHana.names == 'deep') {
signal(warning`Option "{ toHana.names: 'deep' }" is deprecated, use "{ toHana.names: 'quoted' }" instead`);
options.toHana.names = 'quoted';
}
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)
`);
// Verify options
optionProcessor.verifyOptions(options, 'toHana').map(complaint => signal(warning`${complaint}`));
// Special case: For naming variant 'hdbcds' in combination with 'toHana' (and only there!), 'forHana'
// must leave namespaces, structs and associations alone.
if (options.toHana.names == 'hdbcds') {
options = mergeOptions(options, { forHana : { keepNamespaces: true, keepStructsAssocs: true } });
}
// Prepare model for HANA (transferring the options to forHana, and setting 'dialect' to 'hana', because 'toHana' is only used for that)
let forHanaCsn = transformForHanaWithCsn(csn, mergeOptions(options, { forHana: { dialect: 'hana' } }, { forHana : options.toHana } ));
// Assemble result
let result = {};
if (options.toHana.src) {
result.hdbcds = toCdsSource(forHanaCsn, options);
}
if (options.toHana.csn) {
result._augmentedCsn = options.testMode ? sortCsn(forHanaCsn) : forHanaCsn;
result.csn = options.testMode ? sortCsn(forHanaCsn) : forHanaCsn;
}
// Transfer warnings (errors would have resulted in an exception before we come here)
if (forHanaCsn.messages && forHanaCsn.messages.length > 0) {
result.messages = forHanaCsn.messages;
}
return result;
}
// ----------- toOdata -----------
// Generate ODATA for augmented CSN `model` using `options`.

@@ -304,5 +221,8 @@ // Before anything is generated, the following transformations are applied to 'model':

const { error, warning, signal } = alerts(model);
// Optional wrapper?
// In case of API usage the options are in the 'options' argument
// put the OData specific options under the 'options.toOdata' wrapper
// and leave the rest under 'options'
if (options && !options.toOdata) {
options = { toOdata : options };
_wrapRelevantOptionsForCmd(options, 'toOdata');
}

@@ -328,3 +248,3 @@ // Provide defaults and merge options with those from model

signal(warning`Option "{ toOdata.names: 'deep' }" is deprecated, use "{ toOdata.names: 'quoted' }" instead`);
options.toOdata.names = 'deep';
options.toOdata.names = 'quoted';
}

@@ -335,7 +255,2 @@

// Perform extra-magic for TNT if requested
if (model.options.tntFlavor) {
model = transformTntExtensions(model);
}
// Prepare model for ODATA processing

@@ -351,4 +266,4 @@ let forOdataAugmented = transform4odata(model, options);

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

@@ -362,7 +277,7 @@ }

setProp(compactedModel, 'messages', forOdataAugmented.messages);
for (let serviceName of getServiceNames(model))
{
// FIXME: Unify handling of version and tntFlavor (use original options)
let l_edm = csn2edm(compactedModel, serviceName, options);
let allServices = csn2edmAll(compactedModel, options);
for(let serviceName in allServices) {
let l_edm = allServices[serviceName];
result.services[serviceName] = {};

@@ -389,6 +304,80 @@ if (options.toOdata.xml) {

// Transfer warnings (errors would have resulted in an exception before we come here)
if (forOdataAugmented.messages && forOdataAugmented.messages.length > 0) {
result.messages = forOdataAugmented.messages;
return result;
}
// Generate ODATA for `csn` using `options`.
// The twin of the toOdata function but using CSN
function toOdataWithCsn(csn, options) {
const { error, warning, signal } = alerts(csn);
// In case of API usage the options are in the 'options' argument
// put the OData specific options under the 'options.toOdata' wrapper
// and leave the rest under 'options'
if (options && !options.toOdata) {
_wrapRelevantOptionsForCmd(options, 'toOdata');
}
// Provide defaults and merge options with those from csn
options = mergeOptions({ toOdata : getDefaultBackendOptions().toOdata }, csn.options, options);
// Provide something to generate if nothing else was given (conditional default)
if (!options.toOdata.xml && !options.toOdata.json && !options.toOdata.csn) {
options.toOdata.xml = true;
}
if (!options.toOdata.separate && !options.toOdata.combined) {
options.toOdata.combined = true;
}
// Backward compatibility for old naming modes
// FIXME: Remove after a few releases
if (options.toOdata.names == 'flat') {
signal(warning`Option "{ toOdata.names: 'flat' }" is deprecated, use "{ toOdata.names: 'plain' }" instead`);
options.toOdata.names = 'plain';
}
else if (options.toOdata.names == 'deep') {
signal(warning`Option "{ toOdata.names: 'deep' }" is deprecated, use "{ toOdata.names: 'quoted' }" instead`);
options.toOdata.names = 'deep';
}
// Verify options
optionProcessor.verifyOptions(options, 'toOdata').map(complaint => signal(warning`${complaint}`));
// Prepare model for ODATA processing
let forOdataCSN = transform4odataWithCsn(csn, options);
// Assemble result object
let result = {
services: Object.create(null),
messages: csn.messages,
}
if (options.toOdata.csn) {
result.csn = forOdataCSN;
}
// Create annotations and metadata once per service
if (options.toOdata.xml || options.toOdata.json) {
let allServices = csn2edmAll(forOdataCSN, options);
for(let serviceName in allServices) {
let l_edm = allServices[serviceName];
result.services[serviceName] = {};
if (options.toOdata.xml) {
if (options.toOdata.separate) {
result.services[serviceName].annotations = l_edm.toXML('annotations');
result.services[serviceName].metadata = l_edm.toXML('metadata');
}
if (options.toOdata.combined) {
result.services[serviceName].combined = l_edm.toXML('all');
}
}
if (options.toOdata.json) {
// JSON output is not available for ODATA V2
if (options.toOdata.version == 'v2') {
signal(error`ODATA JSON output is not available for ODATA V2`);
}
// FIXME: Why only metadata_json - isn't this rather a 'combined_json' ? If so, rename it!
result.services[serviceName].metadata_json = l_edm.toJSON();
}
}
}
return result;

@@ -417,13 +406,2 @@ }

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

@@ -446,2 +424,10 @@ // The following options control what is actually generated:

const { warning, signal } = alerts(model);
// In case of API usage the options are in the 'options' argument
// put the OData specific options under the 'options.toCdl' wrapper
// and leave the rest under 'options'
if (options && !options.toCdl) {
_wrapRelevantOptionsForCmd(options, 'toCdl');
}
// Merge options with those from model

@@ -456,17 +442,2 @@ options = mergeOptions({ toCdl : true }, model.options, options);

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

@@ -502,6 +473,10 @@ // The following options control what is actually generated:

const { warning, signal } = alerts(model);
// Optional wrapper?
// In case of API usage the options are in the 'options' argument
// put the OData specific options under the 'options.toSwagger' wrapper
// and leave the rest under 'options'
if (options && !options.toSwagger) {
options = { toSwagger : options };
_wrapRelevantOptionsForCmd(options, 'toSwagger');
}
// Merge options with those from model

@@ -521,45 +496,2 @@ options = mergeOptions(model.options, options);

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

@@ -600,5 +532,7 @@ // The following options control what is actually generated (see help above):

// Optional wrapper?
// In case of API usage the options are in the 'options' argument
// put the OData specific options under the 'options.toSql' wrapper
// and leave the rest under 'options'
if (options && !options.toSql) {
options = { toSql : options };
_wrapRelevantOptionsForCmd(options, 'toSql');
}

@@ -617,3 +551,3 @@

if (!options.toSql.src && !options.toSql.csn) {
options.toSql.src = true;
options.toSql.src = 'sql';
}

@@ -649,5 +583,11 @@

// 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}' }"`);
if(options.toSql.dialect != 'hana') {
// CDXCORE-465, 'quoted' and 'hdbcds' are to be used in combination with dialect 'hana' only
if(['quoted', 'hdbcds'].includes(options.toSql.names)) {
signal(error`Option "{ toSql.dialect: '${options.toSql.dialect}' }" cannot be combined with "{ toSql.names: '${options.toSql.names}' }"`);
}
// No non-HANA SQL for HDI
if(options.toSql.src === 'hdi') {
signal(error`Option "{ toSql.dialect: '${options.toSql.dialect}' }" cannot be combined with "{ toSql.src: '${options.toSql.src}' }"`);
}
}

@@ -673,3 +613,3 @@

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

@@ -705,23 +645,2 @@

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

@@ -754,6 +673,9 @@ // Generate SQL DDL rename statements for a migration, renaming existing tables and their

// Optional wrapper?
// In case of API usage the options are in the 'options' argument
// put the OData specific options under the 'options.toRename' wrapper
// and leave the rest under 'options'
if (options && !options.toRename) {
options = { toRename : options };
_wrapRelevantOptionsForCmd(options, 'toRename');
}
// Provide defaults and merge options with those from model

@@ -804,20 +726,2 @@ options = mergeOptions({ toRename : getDefaultBackendOptions().toRename }, model.options, options);

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'

@@ -827,5 +731,5 @@ // The following options control what is actually generated:

// testMode : if true, the result is extra-stable for automated tests (sorted, no 'version')
// newCsn : if true, CSN is returned in the (well-defined) new format '0.1.99' planned for future versions
// (default is the old format '0.1.0')
// toCsn.gensrc : if true, the result CSN is only suitable for use as a source, e.g. for combination with
// newCsn : if false, CSN is returned in the old format '0.1.0'
// (default is the new format '1.0')
// toCsn.flavor : if 'gensrc', the result CSN is only suitable for use as a source, e.g. for combination with
// additional extend/annotate statements, but not for consumption by clients or backends

@@ -841,4 +745,11 @@ // (default is to produce 'client' CSN with all properties propagated and inferred as required

// In case of API usage the options are in the 'options' argument
// put the OData specific options under the 'options.toCsn' wrapper
// and leave the rest under 'options'
if (options && !options.toCsn) {
_wrapRelevantOptionsForCmd(options, 'toCsn');
}
// Merge options with those from model
options = mergeOptions({ toCsn : true }, model.options, options);
options = mergeOptions({ toCsn : {} }, model.options, options);

@@ -848,8 +759,8 @@ // Verify options

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

@@ -860,3 +771,3 @@

// that depend in any way on other options (e.g. toSql provides 'src' if neither 'src' nor
// 'csn' is given: this is a conditional default).
// 'csn' is given: this is a conditional default).
function getDefaultBackendOptions() {

@@ -880,8 +791,24 @@ return {

};
}
}
// Internal function moving command specific options under a command
// wrapper in the options object
function _wrapRelevantOptionsForCmd(options, command) {
// take the command's specific options
let cmdOptions = optionProcessor.camelOptionsForCommand(command);
if (!options[command])
options[command] = Object.create(null);
for (let opt in options) {
if (cmdOptions.includes(opt)) {
Object.assign(options[command], { [opt]: options[opt] });
delete options[opt];
}
}
}
module.exports = {
optionProcessor,
toHana,
toHanaWithCsn,
toOdata,
toOdataWithCsn,
preparedCsnToEdmx,

@@ -888,0 +815,0 @@ preparedCsnToEdm,

@@ -39,6 +39,8 @@ // Implementation of alerts

//
function alerts(model) {
function alerts(model, options = model.options || {}) {
// If 'model' does not have a 'messages' array yet (may happen for plain CSNs), create one
model.messages = model.messages || [];
let collected = model.messages;
let collected = options.messages || model.messages ||
Object.defineProperty( model, 'messages',
{ value: [], configurable: true, writable: true } )
.messages;

@@ -91,7 +93,16 @@ return {

// Link an alert to a location (if provided). If no explicit severity is given, the severity is taken
// from the 'msg' itself (if provided). The alert is added to the list of alerts. If the alert is an
// exception, the corresponding error is thrown. Returns true, if no 'Error' alert was handled.
function signal(msg, location, severity) {
var err = new CompileMessage( location, msg, severity ? severity : msg._severity );
/**
* Link an alert to a location (if provided). If no explicit severity is given, the severity is taken
* from the 'msg' itself (if provided). The alert is added to the list of alerts. If the alert is an
* exception, the corresponding error is thrown.
*
* @param {any} msg Message text
* @param {any} location Location information
* @param {any} severity severity: Info, Warning, Error
* @param {string} [message_id=''] Message ID
* @param {any} context Further context information
* @returns {Boolean} true, if no 'Error' alert was handled.
*/
function signal(msg, location, severity, message_id='', context) {
var err = messageBuilder(model, msg, location, severity, message_id, context);
if (err.severity)

@@ -111,4 +122,4 @@ // don't collect stuff that doesn't have a severity

const { CompileMessage } = require('../base/messages');
const messageBuilder = require('./message-builder');
module.exports = alerts;

@@ -14,5 +14,23 @@ // Deep copy an object structure

let map = new WeakMap();
return clone(obj);
let copyStack = [];
let result = copy(obj);
function clone(obj) {
while (copyStack.length) {
let entry = copyStack.pop();
let {src, target, prop} = entry;
// copy element
let newObj = copy(src[prop]);
// store reference to copy in target property
let desc = (src && typeof src === 'object') ? Object.getOwnPropertyDescriptor(src, prop) : null;
if (desc && desc.enumerable === false && desc.get === undefined && desc.set === undefined) {
desc.value = newObj;
Object.defineProperty(target, prop, desc);
} else {
target[prop] = newObj;
}
}
return result;
function copy(obj) {
let newObj;

@@ -32,12 +50,14 @@ if (typeof obj !== 'object' || obj === null) // return primitive type, note that typeof null === 'object'

map.set(obj, newObj);
let props = Object.getOwnPropertyNames(obj); // we clone only own properties, not inherited one's
for (let p of props) {
let pd = Object.getOwnPropertyDescriptor(obj, p);
if (pd && pd.enumerable === false && pd.get === undefined && pd.set === undefined ) {
pd.value = clone(obj[p]);
Object.defineProperty(newObj, p, pd);
// loop over all properties and add them to copyStack
// in reverse order to keep same order in copied object
let propertiesToPush = [];
for (let propName of Object.getOwnPropertyNames(obj)) { // we clone only own properties, not inherited one's
if (propName === 'location' && Object.getPrototypeOf(obj)) {
// performance optimization: only link location instead of copying the object
newObj.location = obj.location;
} else {
propertiesToPush.push({ src: obj, prop: propName, target: newObj });
}
else
newObj[p] = clone(obj[p]);
}
copyStack.push(...propertiesToPush.reverse());
return newObj;

@@ -47,2 +67,2 @@ }

module.exports = deepCopy;
module.exports = deepCopy;

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

var found = dict[name];
if (!found) {
if (!found || found.builtin) { // do not replace a builtin definition
// XSN TODO: store duplicate definitions in prop $duplicates, do not use array (except $combined?)
dict[name] = entry; // also ok if array (redefined)

@@ -15,0 +16,0 @@ return entry;

module.exports = {
// CDL reserved keywords, used during toCdl transformation
// CDL reserved keywords, used for automatic quoting in 'toCdl' renderer
// Keep in sync with reserved keywords in language.g4
cdl: [
'ALL', 'ANY', 'AS',
'BY', 'CASE', 'CAST',
'DISTINCT', 'EXISTS', 'EXTRACT',
'FALSE', 'FROM', 'GROUP',
'IN', 'KEY', 'NEW',
'NOT', 'NULL', 'OF',
'ON', 'ORDER', 'SELECT',
'SOME', 'TRIM', 'TRUE',
'WHEN', 'WHERE', 'WITH'
'ALL',
'ANY',
'AS',
'BY',
'CASE',
'CAST',
'DISTINCT',
'EXISTS',
'EXTRACT',
'FALSE',
'FROM',
'IN',
'KEY',
'NEW',
'NOT',
'NULL',
'OF',
'ON',
'SELECT',
'SOME',
'TRIM',
'TRUE',
'WHERE',
'WITH',
],
// this group of keywords is relevant for both toHana and toSql
sql92: [
'ABSOLUTE', 'ACTION', 'ADD',
'ALL', 'ALLOCATE', 'ALTER',
'AND', 'ANY', 'ARE',
'AS', 'ASC', 'ASSERTION',
'AT', 'AUTHORIZATION', 'AVG',
'BEGIN', 'BETWEEN', 'BIT',
'BIT_LENGTH', 'BOTH', 'BY',
'CASCADE', 'CASCADED', 'CASE',
'CAST', 'CATALOG', 'CHAR',
'CHARACTER', 'CHARACTER_LENGTH', 'CHAR_LENGTH',
'CHECK', 'CLOSE', 'COALESCE',
'COLLATE', 'COLLATION', 'COLUMN',
'COMMIT', 'CONNECT', 'CONNECTION',
'CONSTRAINT', 'CONSTRAINTS', 'CONTINUE',
'CONVERT', 'CORRESPONDING', 'COUNT',
'CREATE', 'CROSS', 'CURRENT',
'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP',
'CURRENT_USER', 'CURSOR', 'DATE',
'DAY', 'DEALLOCATE', 'DEC',
'DECIMAL', 'DECLARE', 'DEFAULT',
'DEFERRABLE', 'DEFERRED', 'DELETE',
'DESC', 'DESCRIBE', 'DESCRIPTOR',
'DIAGNOSTICS', 'DISCONNECT', 'DISTINCT',
'DOMAIN', 'DOUBLE', 'DROP',
'ELSE', 'END', 'END-EXEC',
'ESCAPE', 'EXCEPT', 'EXCEPTION',
'EXEC', 'EXECUTE', 'EXISTS',
'EXTERNAL', 'EXTRACT', 'FALSE',
'FETCH', 'FIRST', 'FLOAT',
'FOR', 'FOREIGN', 'FOUND',
'FROM', 'FULL', 'GET',
'GLOBAL', 'GO', 'GOTO',
'GRANT', 'GROUP', 'HAVING',
'HOUR', 'IDENTITY', 'IMMEDIATE',
'IN', 'INDICATOR', 'INITIALLY',
'INNER', 'INPUT', 'INSENSITIVE',
'INSERT', 'INT', 'INTEGER',
'INTERSECT', 'INTERVAL', 'INTO',
'IS', 'ISOLATION', 'JOIN',
'KEY', 'LANGUAGE', 'LAST',
'LEADING', 'LEFT', 'LEVEL',
'LIKE', 'LOCAL', 'LOWER',
'MATCH', 'MAX', 'MIN',
'MINUTE', 'MODULE', 'MONTH',
'NAMES', 'NATIONAL', 'NATURAL',
'NCHAR', 'NEXT', 'NO',
'NOT', 'NULL', 'NULLIF',
'NUMERIC', 'OCTET_LENGTH', 'OF',
'ON', 'ONLY', 'OPEN',
'OPTION', 'OR', 'ORDER',
'OUTER', 'OUTPUT', 'OVERLAPS',
'PAD', 'PARTIAL', 'POSITION',
'PRECISION', 'PREPARE', 'PRESERVE',
'PRIMARY', 'PRIOR', 'PRIVILEGES',
'PROCEDURE', 'PUBLIC', 'READ',
'REAL', 'REFERENCES', 'RELATIVE',
'RESTRICT', 'REVOKE', 'RIGHT',
'ROLLBACK', 'ROWS', 'SCHEMA',
'SCROLL', 'SECOND', 'SECTION',
'SELECT', 'SESSION', 'SESSION_USER',
'SET', 'SIZE', 'SMALLINT', 'SOME',
'SPACE', 'SQL', 'SQLCODE',
'SQLERROR', 'SQLSTATE', 'SUBSTRING',
'SUM', 'SYSTEM_USER', 'TABLE',
'TEMPORARY', 'THEN', 'TIME',
'TIMESTAMP', 'TIMEZONE_HOUR', 'TIMEZONE_MINUTE',
'TO', 'TRAILING', 'TRANSACTION',
'TRANSLATE', 'TRANSLATION', 'TRIM',
'TRUE', 'UNION', 'UNIQUE',
'UNKNOWN', 'UPDATE', 'UPPER',
'USAGE', 'USER', 'USING',
'VALUE', 'VALUES', 'VARCHAR',
'VARYING', 'VIEW', 'WHEN',
'WHENEVER', 'WHERE', 'WITH',
'WORK', 'WRITE', 'YEAR', 'ZONE'
// CDL functions, used for automatic quoting in 'toCdl' renderer
cdl_functions: [
'CURRENT_CONNECTION',
'CURRENT_DATE',
'CURRENT_SCHEMA',
'CURRENT_TIME',
'CURRENT_TIMESTAMP',
'CURRENT_TRANSACTION_ISOLATION_LEVEL',
'CURRENT_USER',
'CURRENT_UTCDATE',
'CURRENT_UTCTIME',
'CURRENT_UTCTIMESTAMP',
'SESSION_USER',
'SYSUUID',
],
functions: [
'CURRENT_DATE', 'CURRENT_TIME', 'CURRENT_TIMESTAMP',
'CURRENT_USER', 'SESSION_USER', 'CURRENT_CONNECTION',
'CURRENT_SCHEMA', 'CURRENT_TRANSACTION_ISOLATION_LEVEL', 'CURRENT_UTCDATE',
'CURRENT_UTCTIME', 'CURRENT_UTCTIMESTAMP', 'SYSUUID'
// SQLite keywords, used to warn in 'toSql' renderer with dialect 'sqlite'
// Taken from http://www.sqlite.org/draft/lang_keywords.html
sqlite: [
"ABORT",
"ACTION",
"ADD",
"AFTER",
"ALL",
"ALTER",
"ANALYZE",
"AND",
"AS",
"ASC",
"ATTACH",
"AUTOINCREMENT",
"BEFORE",
"BEGIN",
"BETWEEN",
"BY",
"CASCADE",
"CASE",
"CAST",
"CHECK",
"COLLATE",
"COLUMN",
"COMMIT",
"CONFLICT",
"CONSTRAINT",
"CREATE",
"CROSS",
"CURRENT",
"CURRENT_DATE",
"CURRENT_TIME",
"CURRENT_TIMESTAMP",
"DATABASE",
"DEFAULT",
"DEFERRABLE",
"DEFERRED",
"DELETE",
"DESC",
"DETACH",
"DISTINCT",
"DO",
"DROP",
"EACH",
"ELSE",
"END",
"ESCAPE",
"EXCEPT",
"EXCLUSIVE",
"EXISTS",
"EXPLAIN",
"FAIL",
"FILTER",
"FOLLOWING",
"FOR",
"FOREIGN",
"FROM",
"FULL",
"GLOB",
"GROUP",
"HAVING",
"IF",
"IGNORE",
"IMMEDIATE",
"IN",
"INDEX",
"INDEXED",
"INITIALLY",
"INNER",
"INSERT",
"INSTEAD",
"INTERSECT",
"INTO",
"IS",
"ISNULL",
"JOIN",
"KEY",
"LEFT",
"LIKE",
"LIMIT",
"MATCH",
"NATURAL",
"NO",
"NOT",
"NOTHING",
"NOTNULL",
"NULL",
"OF",
"OFFSET",
"ON",
"OR",
"ORDER",
"OUTER",
"OVER",
"PARTITION",
"PLAN",
"PRAGMA",
"PRECEDING",
"PRIMARY",
"QUERY",
"RAISE",
"RANGE",
"RECURSIVE",
"REFERENCES",
"REGEXP",
"REINDEX",
"RELEASE",
"RENAME",
"REPLACE",
"RESTRICT",
"RIGHT",
"ROLLBACK",
"ROW",
"ROWS",
"SAVEPOINT",
"SELECT",
"SET",
"TABLE",
"TEMP",
"TEMPORARY",
"THEN",
"TO",
"TRANSACTION",
"TRIGGER",
"UNBOUNDED",
"UNION",
"UNIQUE",
"UPDATE",
"USING",
"VACUUM",
"VALUES",
"VIEW",
"VIRTUAL",
"WHEN",
"WHERE",
"WINDOW",
"WITH",
"WITHOUT",
],
sqlite: [
'ADD', 'ALL', 'ALTER',
'AND', 'AS', 'AUTOINCREMENT',
'BETWEEN', 'CASE', 'CHECK',
'COLLATE', 'COMMIT', 'CONSTRAINT',
'CREATE', 'CROSS', 'DEFAULT',
'DEFERRABLE', 'DELETE', 'DISTINCT',
'DROP', 'ELSE', 'ESCAPE',
'EXCEPT', 'EXISTS', 'FOREIGN',
'FROM', 'FULL', 'GROUP',
'HAVING', 'IN', 'INDEX',
'INNER', 'INSERT', 'INTERSECT',
'INTO', 'IS', 'ISNULL',
'JOIN', 'LEFT', 'LIMIT',
'NATURAL', 'NOT', 'NOTNULL',
'NULL', 'ON', 'OR',
'ORDER', 'OUTER', 'PRIMARY',
'REFERENCES', 'RIGHT', 'ROLLBACK',
'SELECT', 'SET', 'TABLE',
'THEN', 'TO', 'TRANSACTION',
'UNION', 'UNIQUE', 'UPDATE',
'USING', 'VALUES', 'WHEN',
'WHERE'
// HANA keywords, used to warn in 'toSql' renderer with dialect 'hana' or in 'toHana' renderer (both with 'plain' names only)
// Taken from https://help.sap.com/viewer/7c78579ce9b14a669c1f3295b0d8ca16/Cloud/en-US/28bcd6af3eb6437892719f7c27a8a285.html
hana: [
"ALL",
"ALTER",
"AS",
"BEFORE",
"BEGIN",
"BOTH",
"CASE",
"CHAR",
"CONDITION",
"CONNECT",
"CROSS",
"CUBE",
"CURRENT_CONNECTION",
"CURRENT_DATE",
"CURRENT_SCHEMA",
"CURRENT_TIME",
"CURRENT_TIMESTAMP",
"CURRENT_TRANSACTION_ISOLATION_LEVEL",
"CURRENT_USER",
"CURRENT_UTCDATE",
"CURRENT_UTCTIME",
"CURRENT_UTCTIMESTAMP",
"CURRVAL",
"CURSOR",
"DECLARE",
"DISTINCT",
"ELSE",
"ELSEIF",
"END",
"EXCEPT",
"EXCEPTION",
"EXEC",
"FALSE",
"FOR",
"FROM",
"FULL",
"GROUP",
"HAVING",
"IF",
"IN",
"INNER",
"INOUT",
"INTERSECT",
"INTO",
"IS",
"JOIN",
"LEADING",
"LEFT",
"LIMIT",
"LOOP",
"MINUS",
"NATURAL",
"NCHAR",
"NEXTVAL",
"NULL",
"ON",
"ORDER",
"OUT",
"PRIOR",
"RETURN",
"RETURNS",
"REVERSE",
"RIGHT",
"ROLLUP",
"ROWID",
"SELECT",
"SESSION_USER",
"SET",
"SQL",
"START",
"SYSUUID",
"TABLESAMPLE",
"TOP",
"TRAILING",
"TRUE",
"UNION",
"UNKNOWN",
"USING",
"UTCTIMESTAMP",
"VALUES",
"WHEN",
"WHERE",
"WHILE",
"WITH",
]
}

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

return '<???>';
loc = normalizeLocation( loc );
let filename = (loc.filename && (normalizeFilename || normalizeFilename === 0))

@@ -73,3 +74,3 @@ ? loc.filename.replace( /\\/g, '/' )

return loc;
return (!loc.end)
return (!loc.end || loc.$weak)
? `${filename}:${loc.start.line}:${loc.start.column}`

@@ -99,8 +100,24 @@ : (loc.start.line == loc.end.line)

// Class for individual compile errors.
// TODO: make it really extend Error?, change param order
/**
* Class for individual compile errors.
*
* @class CompileMessage
* @extends {Error}
*/
class CompileMessage extends Error {
constructor(location, msg, severity = 'Error', id, home) {
/**
* Creates an instance of CompileMessage.
* @param {any} location Location of the message
* @param {any} msg The message text
* @param {string} [severity='Error'] Severity: Debug, Info, Warning, Error
* @param {any} id The ID of the message - visible as property messageId
* @param {any} home
* @param {any} context Some further context information, if needed
*
* @memberOf CompileMessage
*/
constructor(location, msg, severity = 'Error', id, home, context) {
super(msg);
this.location = location;
this.location = normalizeLocation( location );
if (home) // semantic location, e.g. 'entity:"E"/element:"x"'

@@ -112,2 +129,4 @@ this.home = home;

// this.messageId = id; // ids not yet finalized
if (context)
this.context = context;
}

@@ -119,8 +138,22 @@ toString() { // should have no argument...

function handleMessages( model ) {
if (model.messages && model.messages.length) {
model.messages.sort( compareMessage );
if (hasErrors( model.messages ))
throw new CompilationError( model.messages, model );
// Normalize location: to old-style at the moment, TODO: should switch to new-style
function normalizeLocation( loc ) {
if (!loc || !loc.file )
return loc;
let location = { filename: loc.file, start: { line: loc.line, column: loc.col } };
if (loc.endLine)
location.end = { line: loc.endLine, column: loc.endCol };
else
location.$weak = true;
return location;
}
function handleMessages( model, options = model.options || {} ) {
let messages = options.messages || model.messages;
if (messages && messages.length) {
messages.sort( compareMessage );
if (hasErrors( messages ))
throw new CompilationError( messages, model );
}
return model;
}

@@ -142,6 +175,11 @@

function getMessageFunction( model ) {
if (!model.messages)
model.messages = [];
let config = model.options && model.options.severities || {};
// Return message function to issue errors, warnings, info and debug messages.
// Messages are put into the `messages` property of argument `options` or `model`.
// If those do not exist, define a non-enumerable property `messages` in `model`.
function getMessageFunction( model, options = model.options || {} ) {
let messages = options.messages || model.messages ||
Object.defineProperty( model, 'messages',
{ value: [], configurable: true, writable: true } )
.messages;
let config = options.severities || {};

@@ -159,3 +197,3 @@ return function message( id, location, home, params = {}, severity = undefined, texts = undefined ) {

(typeof home === 'string' ? home : homeName(home)) );
model.messages.push( msg );
messages.push( msg );
return msg;

@@ -170,2 +208,3 @@ }

target: transformArg,
type: transformArg,
token: t => t.match( /^[a-zA-Z]+$/ ) ? t.toUpperCase() : "'" + t + "'",

@@ -279,9 +318,8 @@ code: n => '`' + n + '`',

// Compare two messages `a` and `b`. Return 0 if they are equal, 1 if `a` is
// greater than `b`, and -1 if `a` is less than `b`. Messages without a location
// are considered equal, unless compared to a message with location, in which
// case they are considered larger, so they are put at the end of a sorted list
// larger than `b`, and -1 if `a` is smaller than `b`. Messages without a location
// are considered larger than messages with a location.
function compareMessage( a, b ) {
if (a.location && b.location) {
let aend = a.location.end || a.location.start;
let bend = b.location.end || b.location.start;
let aend = !a.location.$weak && a.location.end || { line: Number.MAX_SAFE_INTEGER, column: 0 };
let bend = !b.location.$weak && b.location.end || { line: Number.MAX_SAFE_INTEGER, column: 0 };
return ( c( a.location.filename, b.location.filename ) ||

@@ -294,4 +332,6 @@ c( a.location.start.line, b.location.start.line ) ||

}
else if (!a.location === !b.location)
return c( a.message, b.message )
else
return (!a.location ? (!b.location ? 0 : 1) : -1);
return (!a.location) ? 1 : -1;

@@ -306,4 +346,4 @@ function c( x, y ) {

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.query || name.query != null && art.kind !== 'element' || name.$mixin ) // Yes, omit $query.0 for element - TODO: rename to block
r.push( (art.kind === 'block' ? 'block:' : 'query:') + (name.query + 1) );
if (name.action && omit !== 'action')

@@ -348,2 +388,24 @@ r.push( memberActionName(art) + ':' + quoted( name.action ) );

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

@@ -360,4 +422,5 @@ hasErrors,

CompileMessage,
CompilationError
CompilationError,
translatePathLocations
}

@@ -38,4 +38,4 @@ //

function forEachMemberRecursively( construct, callback ) {
forEachMember( construct, ( member, memberName ) => {
callback( member, memberName );
forEachMember( construct, ( member, memberName, prop ) => {
callback( member, memberName, prop );
// Descend into nested members, too

@@ -118,2 +118,5 @@ forEachMemberRecursively( member, callback );

}
// Simply return if node is to be ignored
if (node === undefined || node._ignore)
return undefined;
// Transform arrays element-wise

@@ -120,0 +123,0 @@ if (node instanceof Array) {

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

if (!art.abstract && emptyOrOnlyVirtualElements(art)) {
signal(error`A type or an entity definition cannot be empty or contain only virtual elements with '--to-hana'.`, art.location);
signal(error`A type or an entity definition cannot be empty or contain only virtual elements`, art.location);
}

@@ -15,0 +15,0 @@

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

const alerts = require('../base/alerts');
const { getDefinerFunctions } = require('../compiler/definer');
const { isComposition } = require('../model/modelUtils.js')
function checkPrimaryKeyTypeCompatibility(elem, model) {
const { error, signal } = alerts(model);
let type = '';
// apparently this is the resolved type (over an derived type chain)
if(elem._finalType && elem._finalType.type && elem._finalType.type._artifact)
type = elem._finalType.type._artifact.name.absolute;
if(elem.key && elem.key.val === true && ['cds.hana.ST_POINT', 'cds.hana.ST_GEOMETRY'].includes(type)) {
signal(error`Type ${type} cannot be used as primary key`, elem.location);
}
}
// Perform checks for element (or type) 'elem' concerning managed associations,

@@ -27,3 +40,6 @@ // only for managed assocs directly declared on 'elem', not those from derived

if (targetMax == '*' || Number(targetMax) > 1) {
signal(warning`The association "${elem.name.id}" has cardinality "to many" but no ON-condition`, elem.location);
// FIXME: convenience function (reuse in forHana)?
const context = { EntityhasPersistenceSkipOrTrueOrAbstract: elem._parent.abstract || ((elem._parent['@cds.persistence.skip'] && elem._parent['@cds.persistence.skip'].val !== null && elem._parent['@cds.persistence.skip'].val !== false)|| (elem._parent['@cds.persistence.exists'] && elem._parent['@cds.persistence.exists'].val !== null && elem._parent['@cds.persistence.exists'].val !== false))};
let assocType = isComposition(elem.type) ? "composition" : "association";
signal(warning`The ${assocType} "${elem.name.id}" has cardinality "to many" but no ON-condition`, elem.location, undefined, 'to-many-no-on', context);
}

@@ -62,3 +78,4 @@ }

}
checkAssociationCondition(elem, model, elem.onCond);
if (elem.onCond && !elem.onCond.$inferred)
checkAssociationCondition(elem, model, elem.onCond);
}

@@ -90,3 +107,2 @@

singleCheckUnmanagedAssocCondArgumentNoFollowUnmanagedAssoc(elem, model, arg, op);
singleCheckNoVirtualElementsInAssociationConditions(model, arg);
}

@@ -108,3 +124,7 @@

if(artifactName.absolute && artifactName.element) {
let argTarget = model.definitions[artifactName.absolute].elements[artifactName.element];
let targetArtifact = model.definitions[artifactName.absolute];
if (targetArtifact instanceof Array) {
return;
}
let argTarget = targetArtifact.elements[artifactName.element];
//the check is valid for unmanaged associations

@@ -132,13 +152,2 @@ //TODO clarify if the full resolved path to the target field should consist of managed associations or just the first

function singleCheckNoVirtualElementsInAssociationConditions(model,arg) {
if (arg && arg._artifact) {
if (!arg._artifact) // name was not resolved -> _artifact: undefined - exception is thrown on the next steps of the compiler
return;
if (arg._artifact.virtual && arg._artifact.virtual.val === true) {
const { error, signal } = alerts(model);
signal(error`Virtual elements cannot be used in an ON-condition.`, arg.location);
}
}
}
function isStruct(obj) {

@@ -173,3 +182,3 @@ return has.call(obj, 'elements');

let param = parameters[name];
if (!node[param] && absolute != "hana.ST_POINT" && absolute != "hana.ST_GEOMETRY")
if (!node[param] && absolute != "cds.hana.ST_POINT" && absolute != "cds.hana.ST_GEOMETRY")
signal(error`Actual value for type parameter '${param}' missing in reference to type '${absolute}'`,

@@ -181,3 +190,3 @@ node.type.location );

case "cds.Binary":
case "hana.VARCHAR": {
case "cds.hana.VARCHAR": {
checkTypeParamValue(node, 'length', 'positiveInteger', {min: 1, max: 5000});

@@ -192,17 +201,13 @@ break;

case "hana.BINARY":
case "hana.NCHAR":
case "hana.CHAR": {
case "cds.hana.BINARY":
case "cds.hana.NCHAR":
case "cds.hana.CHAR": {
checkTypeParamValue(node, 'length', 'positiveInteger', {min: 1, max: 2000});
break;
}
case "hana.ALPHANUM": {
checkTypeParamValue(node, 'length', 'positiveInteger', {min: 1, max: 127});
case "cds.hana.ST_POINT":
case "cds.hana.ST_GEOMETRY": {
checkTypeParamValue(node, 'srid', 'positiveInteger', {max: Number.MAX_SAFE_INTEGER});
break;
}
case "hana.ST_POINT":
case "hana.ST_GEOMETRY": {
checkTypeParamValue(node, 'length', 'positiveInteger', {max: Number.MAX_SAFE_INTEGER});
break;
}
}

@@ -275,3 +280,23 @@ }

function checkLocalizedElement (elem, model) {
const { signal, warning } = alerts(model);
const definerFcts = getDefinerFunctions(model);
// if it is directly a localized element
if (elem.localized && elem.localized.val) {
// check if type is the builtin type cds.String (direct or indirect e.g. via typedefs)
let isBuiltinString = definerFcts.getOriginRecursive(elem, (art) => {
if (art.type && art.type.path && art.type.path.length) {
let type = art.type.path.map((part) => part.id).join('.');
return ["cds.String", "String"].includes(type) && model.definitions[type] && model.definitions[type].builtin;
}
return false;
});
if (!isBuiltinString) {
signal(warning`Element "${elem.name.absolute}.${elem.name.id}": "localized" may only be used in combination with type "String"`);
}
}
}
module.exports = {
checkPrimaryKeyTypeCompatibility,
checkManagedAssoc,

@@ -281,2 +306,3 @@ checkVirtualElement,

checkCardinality,
checkLocalizedElement
};

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

// Check an expression (or condition) for semantic validity
function checkExpression(xpr, model) {
/**
* Check wether the supplied argument is a virtual element
*
* TO CLARIFY: do we want the "no virtual element" check for virtual elements/columns, too?
*
* @param {any} arg Argument to check (part of an expression)
* @returns {Boolean}
*/
function isVirtualElement(arg) {
return arg.path && arg._artifact.virtual && arg._artifact.virtual.val === true && arg._artifact.kind && arg._artifact.kind === 'element';
}
/**
* Check a token-stream expression for semantic validity
*
* @param {any} xpr The expression to check
* @param {any} model The model
* @returns {undefined}
*/
function checkTokenStreamExpression(xpr, model){
const { error, signal } = alerts(model);
const { isAssociationOperand, isDollarSelfOperand } = transformUtils.getTransformers(model);
// We don't do checks on token-stream expressions, only on tree-like ones
if (xpr.op && xpr.op.val == 'xpr') {
return;
// Check for illegal argument usage within the expression
for (let arg of xpr.args || []) {
if(isVirtualElement(arg)){
signal(error`Virtual elements cannot be used in an expression.`, arg.location);
}
// Recursively traverse the argument expression
checkTokenStreamExpression(arg, model);
}
}
/**
* Check a tree-like expression for semantic validity
*
* @param {any} xpr The expression to check
* @param {any} model The model
* @returns {undefined}
*/
function checkTreeLikeExpression(xpr, model){
const { error, signal } = alerts(model);
const { isAssociationOperand, isDollarSelfOperand } = transformUtils.getTransformers(model);
// No further checks regarding associations and $self required if this is a backlink-like expression

@@ -22,6 +57,8 @@ // (a comparison of $self with an assoc)

}
// Check for illegal argument usage within the expression
for (let arg of xpr.args || []) {
// Arg must not be an association and not $self
if(isVirtualElement(arg)){
signal(error`Virtual elements cannot be used in an expression.`, arg.location);
}
// Arg must not be an association and not $self
if (isAssociationOperand(arg)) {

@@ -34,7 +71,12 @@ signal(error`An association cannot be used as a value in an expression`, arg.location);

// Recurse into argument expression
checkExpression(arg, model);
// Recursively traverse the argument expression
checkTreeLikeExpression(arg, model);
}
// Return true if 'xpr' is backlink-like expression (a comparison of "$self" with an assoc)
/**
* Return true if 'xpr' is backlink-like expression (a comparison of "$self" with an assoc)
*
* @param {any} xpr The expression to check
* @returns {Boolean}
*/
function isBinaryDollarSelfComparisonWithAssoc(xpr) {

@@ -48,5 +90,5 @@ // Must be an expression with arguments

if (xpr.op.val == '=' && xpr.args.length == 2) {
// Tree-ish expression from the compiler (not augmented)
// Tree-ish expression from the compiler (not augmented)
return (isAssociationOperand(xpr.args[0]) && isDollarSelfOperand(xpr.args[1])
|| isAssociationOperand(xpr.args[1]) && isDollarSelfOperand(xpr.args[0]));
|| isAssociationOperand(xpr.args[1]) && isDollarSelfOperand(xpr.args[0]));
}

@@ -59,4 +101,20 @@

/**
* Check an expression (or condition) for semantic validity
*
* @param {any} xpr The expression to check
* @param {any} model The model
* @returns {undefined}
*/
function checkExpression(xpr, model) {
// Since the checks for tree-like and token-stream expressions differ, check here what kind of expression we are looking at
if (xpr.op && xpr.op.val == 'xpr') {
return checkTokenStreamExpression(xpr, model);
} else {
return checkTreeLikeExpression(xpr, model);
}
}
module.exports = {
checkExpression,
}

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

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

@@ -36,3 +37,3 @@ const { foreachPath } = require('../model/modelUtils');

let options = model.options;
const { error, signal } = alerts(model);
const { error, info, signal } = alerts(model);
const { checkActionOrFunction, checkActionOrFunctionParameter} = getFunctionAndActionChecks(model);

@@ -102,2 +103,5 @@

}
if (construct.name.id && keywords.cdl.includes(construct.name.id.toUpperCase())) {
signal(info`Using reserved keyword "${construct.name.id}" as identifier is not recommended`, construct.name.location);
}
checkAnnotationAssignments(construct, model);

@@ -110,7 +114,2 @@ }

// exception: they can live in cds.foundation
if (art.kind != 'namespace' &&
(art.name.absolute == 'cds' ||
(art.name.absolute.match(/^cds\./) && !art.name.absolute.match(/^cds\.foundation\./)))) {
signal(error`The namespace "cds" is reserved for CDS builtins`, art.name.location);
}
if (options.toHana) {

@@ -128,5 +127,14 @@ checkNotEmptyOrOnlyVirtualElems(art, model);

// Called for each container (no need to iterate contained artifacts)
// eslint-disable-next-line no-unused-vars
function checkGenericContainer(container) {
// No checks yet
if (container.kind === 'namespace' && container.name.absolute === 'localized') {
baseModel.forEachGeneric(container, "artifacts", checkLocalizedObjects);
}
function checkLocalizedObjects(artifact) {
if (artifact.kind === "namespace") {
baseModel.forEachGeneric(artifact, "artifacts", checkLocalizedObjects);
} else if (!artifact.query) {
signal(error`The namespace "localized" is reserved for localization views`, artifact.name.location);
}
}
}

@@ -220,2 +228,3 @@

function checkElement(elem) {
checkPrimaryKeyTypeCompatibility(elem, model);
checkVirtualElement(elem, model);

@@ -230,2 +239,3 @@ checkManagedAssoc(elem, model);

}
checkLocalizedElement(elem, model);
}

@@ -232,0 +242,0 @@

@@ -69,13 +69,9 @@ // Consistency checker on model (XSN = augmented CSN)

const { locationString } = require('../base/messages');
const { locationString, hasErrors } = require('../base/messages');
const { queryOps } = require('../base/model');
function assertConsistency( model, stage ) {
let stageParser = typeof stage === 'object';
let options = stageParser && stage || model.options || { testMode: true };
// The new-style CSN parser is always checked, independently from --test-mode:
if (stageParser
? !options.testMode && (model.$frontend !== 'json' || !options.newCsn)
: !options.testMode || options.parseOnly)
//if (!options.testMode || options.parseOnly && !stageParser)
const stageParser = typeof stage === 'object';
const options = stageParser && stage || model.options || { testMode: true };
if (!options.testMode || options.parseOnly && !stageParser)
return;

@@ -85,10 +81,10 @@

':model': { // top-level from compiler
requires: ['messages','options','definitions','sources'],
optional: ['extensions','version','$version','meta','$magicVariables','$builtins','$internal','_entities'] // version without --test-mode
requires: [ 'options', 'definitions', 'sources' ],
optional: [ 'messages', 'extensions', 'version', '$version', 'meta', '$magicVariables', '$builtins', '$internal', '$compositionTargets', '$lateExtensions', '_entities', '$entity' ], // version without --test-mode
},
':parser': { // top-level from parser
requires: ['$frontend'],
requires: [ '$frontend' ],
optional: [
'messages','options','definitions','extensions',
'artifacts','namespace','usings', // CDL parser
'messages', 'options', 'definitions', 'extensions',
'artifacts', 'namespace', 'usings', // CDL parser
'filename', 'dirname', // TODO: move filename into a normal location?

@@ -101,20 +97,22 @@ 'dependencies', // for USING..FROM

'@sql_mapping', // TODO: it is time that a 'header' attribute replaces 'version'
]
],
},
location: { // location req if at least one property:
isRequired: parent => noSyntaxErrors() || Object.keys( parent ).length,
requires: ['filename','start'],
optional: ['end','$weak','$notFound'],
requires: [ 'filename', 'start' ],
optional: [ 'end', '$weak', '$notFound' ],
schema: {
start: {
requires: ['line','column'], optional: ['offset'],
schema: { line: {test:isNumber}, column: {test:isNumber}, offset: {test:TODO} }
requires: [ 'line', 'column' ],
optional: [ 'offset' ],
schema: { line: { test: isNumber }, column: { test: isNumber }, offset: { test: TODO } },
},
end: {
requires: ['line','column'], optional: ['offset'],
schema: { line: {test:isNumber}, column: {test:isNumber}, offset: {test:TODO} }
requires: [ 'line', 'column' ],
optional: [ 'offset' ],
schema: { line: { test: isNumber }, column: { test: isNumber }, offset: { test: TODO } },
},
$weak: { test: isBoolean },
$notFound: { test: isBoolean }
}
$notFound: { test: isBoolean },
},
},

@@ -126,16 +124,21 @@ sources: { test: isDictionary( isObject ) },

dependencies: { test: TODO }, // TODO: describe
$frontend: { parser: true, test: isString, enum: ['cdl','json','xml'] },
messages: { test: isArray( TODO ) }, // TODO: check message object
$frontend: { parser: true, test: isString, enum: [ 'cdl', 'json', 'xml' ] },
messages: {
enumerable: () => true, // does not matter (non-enum std), enum in CSN/XML parser
test: isArray( TODO ),
},
options: { test: TODO }, // TODO: check option object
definitions: {
test: isDictionary( definition ),
requires: ['kind','location','name'],
optional: thoseWithKind
requires: [ 'kind', 'location', 'name' ],
optional: thoseWithKind,
},
extensions: {
kind: ['context'], // syntax error (as opposed to HANA CDS), but still there
kind: [ 'context' ], // syntax error (as opposed to HANA CDS), but still there
inherits: 'definitions',
test: isArray(),
schema: { name: { inherits: 'name', isRequired: noSyntaxErrors } } // name is required in parser, too
schema: { name: { inherits: 'name', isRequired: noSyntaxErrors } }, // name is required in parser, too
},
$localizedElements: { kind: 'entity', test: isArray(TODO), inherits: 'element' },
$localized: { kind: 'entitiy', test: isObject },
$magicVariables: { test: TODO },

@@ -145,3 +148,3 @@ $builtins: { test: TODO },

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

@@ -151,29 +154,29 @@ namespace: {

// TODO: the JSON parser should augment 'namespace' correctly or better: hide it
requires: ['location'],
optional: ['path','dcPath'] // dcPath with --hana-flavor
requires: [ 'location' ],
optional: [ 'path', 'dcPath' ], // dcPath with --hana-flavor
},
usings: {
test: isArray(),
requires: ['kind','location'],
optional: ['name','extern','usings','annotationAssignments'], // TODO: get rid of annos: []
requires: [ 'kind', 'location' ],
optional: [ 'name', 'extern', 'usings', 'annotationAssignments' ], // TODO: get rid of annos: []
},
extern: {
requires: ['location','path'],
optional: ['dcPath'],
schema: { path: { inherits: 'path', optional: ['quoted'] } },
requires: [ 'location', 'path' ],
optional: [ 'dcPath' ],
schema: { path: { inherits: 'path', optional: [ 'quoted' ] } },
},
dcPath: { inherits: 'path', optional: ['quoted'] },
dcPath: { inherits: 'path', optional: [ 'quoted' ] },
elements: { kind: true, inherits: 'definitions' },
elements_: { kind: true, parser: true, test: TODO }, // TODO: remove
_elementsIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove
elements_: { kind: true, parser: true, test: TODO }, // TODO: remove
_elementsIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove
actions: { kind: true, inherits: 'definitions' },
actions_: { kind: true, parser: true, test: TODO }, // TODO: remove
actions_: { kind: true, parser: true, test: TODO }, // TODO: remove
enum: { kind: true, inherits: 'definitions' },
enum_: { kind: true, parser: true, test: TODO }, // TODO: remove
enum_: { kind: true, parser: true, test: TODO }, // TODO: remove
foreignKeys: { kind: true, inherits: 'definitions' },
foreignKeys_: { kind: true, parser: true, test: TODO }, // TODO: remove
_foreignKeysIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove
foreignKeys_: { kind: true, parser: true, test: TODO }, // TODO: remove
_foreignKeysIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove
params: { kind: true, inherits: 'definitions' },
params_: { kind: true, parser: true, test: TODO }, // TODO: remove
_paramsIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove
params_: { kind: true, parser: true, test: TODO }, // TODO: remove
_paramsIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove
mixin: { inherits: 'definitions' },

@@ -185,42 +188,47 @@ query: {

schema: { args: { inherits: 'query', test: isArray( query ) } },
requires: ['op','location','args'],
requires: [ 'op', 'location', 'args' ],
optional: [
'name','quantifier','orderBy','limit','offset','_leadingQuery',
'name','kind','_parent','_main','_finalType','$navigation' // in FROM
]
'name', 'quantifier', 'orderBy', 'limit', 'offset', '_leadingQuery',
'name', 'kind', '_parent', '_main', '_finalType', '$navigation', // in FROM
],
},
select: { // sub query
requires: ['op','location','from'],
requires: [ 'op', 'location', 'from' ],
optional: [
'_tableAlias', // for sub query in FROM
'name','quantifier','mixin','exclude', 'columns', 'elements', '_deps',
'where','groupBy','having','orderBy','$orderBy','limit','offset',
'_elementsIndexNo','_projections','_block','_parent','_main', '_finalType',
'$tableAliases','kind','_firstAliasInFrom','queries','_$next','$combined',
'$dictOrderBy'
]
}
'name', 'quantifier', 'mixin', 'exclude', 'columns', 'elements', '_deps',
'where', 'groupBy', 'having', 'orderBy', '$orderBy', 'limit', 'offset',
'_elementsIndexNo', '_projections', '_block', '_parent', '_main', '_finalType',
'$tableAliases', 'kind', '_firstAliasInFrom', 'queries', '_$next', '$combined',
'$dictOrderBy',
],
},
none: { optional: () => true }, // parse error
},
from: {
inherits: 'query', test: isArray( query ), // TODO: not array
inherits: 'query',
test: isArray( query ), // TODO: not array
op: { // join
schema: { args: { inherits: 'from', test: isArray( query ) } },
requires: ['op','location','args','join'],
requires: [ 'op', 'location', 'args', 'join' ],
optional: [
'on','kind','name',
'$tableAliases','queries','$combined',
'_block','_parent','_main','_leadingQuery','_$next', '_deps',
]
'on', 'kind', 'name',
'$tableAliases', 'queries', '$combined',
'_block', '_parent', '_main', '_leadingQuery', '_$next', '_deps',
],
},
path: {
requires: ['location','path'],
requires: [ 'location', 'path' ],
optional: [
'name', '_tableAlias', '_status', // TODO: only in from
'scope','_artifact','$inferred', // TODO: remove the rest
]
}
'scope', '_artifact', '$inferred', // TODO: remove the rest
],
},
},
columns: {
inherits: 'definitions', test: isArray( column ), enum: ['*'],
requires: ['location']
kind: [ 'extend' ],
inherits: 'definitions',
test: isArray( column ),
enum: [ '*' ],
requires: [ 'location' ],
// schema: { kind: { isRequired: () => {} } } // kind not required

@@ -230,8 +238,8 @@ },

test: isDictionary( definition ), // definition since redef
requires: ['location','name'],
optional: ['annotationAssignments'] // TODO: get rid of annos: []
requires: [ 'location', 'name' ],
optional: [ 'annotationAssignments' ], // TODO: get rid of annos: []
},
orderBy: { test: isArray(), requires: ['value'], optional: ['sort','nulls','_$queryNode'] },
sort: { test: locationVal( isString ), enum: ['asc','desc'] },
nulls: { test: locationVal( isString ), enum: ['first','last'] },
orderBy: { test: isArray(), requires: [ 'value' ], optional: [ 'sort', 'nulls', '_$queryNode' ] },
sort: { test: locationVal( isString ), enum: [ 'asc', 'desc' ] },
nulls: { test: locationVal( isString ), enum: [ 'first', 'last' ] },
$orderBy: { inherits: 'orderBy' },

@@ -244,11 +252,15 @@ _$queryNode: { test: TODO }, // TODO: remove

$combined: { test: TODO },
_typeIsExplicit: { kind: true, enumerable: true, parser: true, test: TODO }, // TODO: remove
_typeIsExplicit: {
kind: true,
enumerable: true,
parser: true,
test: TODO,
}, // TODO: remove
type: {
kind: true,
requires: ['location','path'],
requires: [ 'location', 'path' ],
optional: [
'scope','_artifact','$inferred', // TODO: remove the rest
'scope', '_artifact', '$inferred', // TODO: remove the rest
'calculated', // TODO: remove calculated
'resolveSemantics', // replace by scope: 'typeOf'
]
],
},

@@ -258,5 +270,5 @@ target: { kind: true, inherits: 'type' },

test: isArray( pathItem ),
requires: ['location','id'], // TODO: it can be `func` instead of `id` later
requires: [ 'location', 'id' ], // TODO: it can be `func` instead of `id` later
// TODO: rename namedArgs to args
optional: ['quoted','args','namedArgs','where','cardinality','_artifact','_navigation','$inferred']
optional: [ 'quoted', 'args', 'namedArgs', 'where', 'cardinality', '_artifact', '_navigation', '$inferred' ],
},

@@ -267,19 +279,19 @@ id: { test: isString },

func: { test: TODO }, // TODO: change structure
resolveSemantics: { test: TODO }, // replace by scope: 'typeOf'
kind: {
isRequired: () => true, // required even with parse errors
isRequired: !stageParser && (() => true),
// required to be set by Core Compiler even with parse errors
test: isString,
enum: [
'context','service','view','entity','type','const','annotation',
'element','enum','action','function','param','key',
'annotate','extend',
'query','mixin',
'source','namespace','using',
'$tableAlias'
]
'context', 'service', 'view', 'entity', 'type', 'const', 'annotation',
'element', 'enum', 'action', 'function', 'param', 'key',
'annotate', 'extend',
'query', 'mixin',
'source', 'namespace', 'using',
'$tableAlias',
],
},
$syntax: {
parser: true,
kind: ['entity','view'],
test: isString // CSN parser should check for 'entity', 'view', 'projection'
kind: [ 'entity', 'view' ],
test: isString, // CSN parser should check for 'entity', 'view', 'projection'
},

@@ -290,26 +302,26 @@ value: {

ref: { inherits: 'type' },
none: { optional: ['location'] }, // TODO: why optional / enough in name?
none: { optional: [ 'location' ] }, // TODO: why optional / enough in name? - TODO: "yes" instread "none": val: true, optional literal/location
val: {
requires: ['literal','location'],
requires: [ 'literal', 'location' ],
// TODO: remove augmented, rename symbol to sym
// TODO: struct only for annotation assignments
optional: ['val','symbol','name','$inferred','augmented']
optional: [ 'val', 'symbol', 'name', '$inferred', 'augmented' ],
},
op: {
schema: { args: { inherits: 'args', args: 'positional' } },
requires: ['op','location'],
optional: ['args','func','quantifier','$inferred','augmented','_artifact']
requires: [ 'op', 'location' ],
optional: [ 'args', 'namedArgs', 'func', 'quantifier', '$inferred', 'augmented', '_artifact' ],
// _artifact with "localized data"s 'coalesce'
},
query: { inherits: 'query' }
query: { inherits: 'query' },
},
literal: { // TODO: check value against literal
test: isString,
enum: ['string','number','boolean','hex','time','date','timestamp','struct','array','enum','null']
enum: [ 'string', 'number', 'boolean', 'hex', 'time', 'date', 'timestamp', 'struct', 'array', 'enum', 'null', 'token' ],
},
symbol: { requires: ['location','id'], optional: ['quoted','augmented'] },
symbol: { requires: [ 'location', 'id' ], optional: [ 'quoted', 'augmented' ] },
val: {
test: isVal, // the following for array/struct value
requires: ['location'],
optional: ['literal','val','symbol','struct','path','name','augmented','$duplicate']
requires: [ 'location' ],
optional: [ 'literal', 'val', 'symbol', 'struct', 'path', 'name', 'augmented', '$duplicate' ],
// TODO: restrict path to #simplePath

@@ -319,3 +331,3 @@ },

args: { inherits: 'value', test: args },
namedArgs: { inherits: 'value', optional: ['name','$duplicate'], test: args },
namedArgs: { inherits: 'value', optional: [ 'name', '$duplicate' ], test: args },
onCond: { kind: true, inherits: 'value' },

@@ -333,5 +345,5 @@ on: { kind: true, inherits: 'value', test: expressionOrString }, // TODO: rename 'onCond' to 'on', remove 'on'

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

@@ -341,11 +353,14 @@ name: {

kind: true,
schema: { query: { test: TODO }, $mixin: { test: TODO } }, // TODO: rename query prop in name, delete $mixin
requires: ['location'],
schema: {
query: { test: TODO },
$mixin: { test: TODO },
}, // TODO: rename query prop in name, delete $mixin
requires: [ 'location' ],
optional: [
'path','id','quoted', // TODO: req path, opt id for main, req id for member
'path', 'id', 'quoted', // TODO: req path, opt id for main, req id for member
'$mixin', // TODO: delete, use kind = 'mixin'
'_artifact','$inferred',
'_artifact', '$inferred',
'calculated', // TODO: remove calculated
'absolute','element','alias','query','action','param'
]
'absolute', 'element', 'alias', 'query', 'action', 'param',
],
},

@@ -357,6 +372,6 @@ absolute: { test: isString },

alias: { test: isString },
expectedKind: { kind: ['extend'], inherits: 'kind' },
expectedKind: { kind: [ 'extend' ], inherits: 'kind' },
abstract: { kind: true, test: locationVal() },
virtual: { kind: true, test: locationVal() },
key: { kind: true, test: locationVal(), also: [undefined] },
key: { kind: true, test: locationVal(), also: [ undefined ] },
masked: { kind: true, test: locationVal() },

@@ -366,10 +381,10 @@ notNull: { kind: true, test: locationVal() },

returns: {
kind: ['action','function'],
requires: ['location'],
kind: [ 'action', 'function' ],
requires: [ 'location' ],
optional: [
'type','typeArguments','length','precision','scale','enum',
'elements','cardinality','target','on','onCond','foreignKeys','items',
'_outer','_finalType',
'elements_','_elementsIndexNo', // TODO: remove
]
'type', 'typeArguments', 'length', 'precision', 'scale', 'srid', 'enum',
'elements', 'cardinality', 'target', 'on', 'onCond', 'foreignKeys', 'items',
'_outer', '_finalType',
'elements_', '_elementsIndexNo', // TODO: remove
],
},

@@ -380,3 +395,3 @@ items: { kind: true, inherits: 'returns' }, // yes, also optional 'items'

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

@@ -386,3 +401,3 @@ targetElement: { kind: true, inherits: 'type' }, // for foreign keys

artifacts: { kind: true, inherits: 'definitions', test: isDictionary( inDefinitions ) },
artifacts_: { kind: true, parser: true, test: TODO }, // TODO: remove
artifacts_: { kind: true, parser: true, test: TODO }, // TODO: remove
blocks: { kind: true, test: TODO }, // TODO: make it $blocks ?

@@ -393,13 +408,14 @@ viaTransform: { kind: true, test: TODO }, // TODO remove

scale: { kind: true, inherits: 'value' },
srid: { kind: true, inherits: 'value' },
localized: { kind: true, test: locationVal() },
cardinality: {
kind: true,
requires: ['location'],
optional: ['sourceMax','targetMin','targetMax']
requires: [ 'location' ],
optional: [ 'sourceMax', 'targetMin', 'targetMax' ],
},
sourceMax: { test: locationVal( isNumber ), also: ['*'] },
sourceMax: { test: locationVal( isNumber ), also: [ '*' ] },
targetMin: { test: locationVal( isNumber ) },
targetMax: { test: locationVal( isNumber ), also: ['*'] },
targetMax: { test: locationVal( isNumber ), also: [ '*' ] },
default: { kind: true, inherits: 'value' },
typeArguments: { kind: true, test: TODO }, // TODO $typeArgs: only in CDL parser or compiler w errors
typeArguments: { kind: true, test: TODO }, // TODO $typeArgs: only in CDL parser or w errors
$tableAliases: { kind: true, test: TODO }, // containing $self outside queries

@@ -417,3 +433,3 @@ _block: { kind: true, test: TODO },

queries: { kind: true, test: TODO }, // TODO: $queries with other structure
$queries: { kind: ['entity','view'], test: TODO },
$queries: { kind: [ 'entity', 'view' ], test: TODO },
_leadingQuery: { kind: true, test: TODO },

@@ -424,3 +440,4 @@ $navigation: { kind: true, test: TODO },

$from: { kind: true, test: TODO }, // all table refs necesary to compute elements
_redirected: { kind: true, test: TODO }, // for REDIRECTED TO
_redirected: { kind: true, test: TODO }, // for REDIRECTED TO:
// ...array of table aliases for targets from orig to new
_$next: { kind: true, test: TODO }, // next lexical search environment for values

@@ -431,2 +448,3 @@ _extend: { kind: true, test: TODO }, // for collecting extend/annotate on artifact

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

@@ -436,8 +454,13 @@ _sccCaller: { kind: true, test: TODO }, // for cyclic calculation

_projections: { kind: true, test: TODO }, // for mixin definitions
$entity: { kind: true, test: TODO },
_entities: { test: TODO },
_ancestors: { kind: ['type','entity','view'], test: isArray( TODO ) },
_descendants: { kind: ['entity','view'], test: isDictionary( isArray( TODO ) ) },
$compositionTargets: { test: TODO },
$lateExtensions: { 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 },
$extension: { kind: true, test: TODO }, // TODO: introduce $applied instead or $status
$inferred: { parser:true, kind: true, test: isString },
$inferred: { parser: true, kind: true, test: isString },
$autoexpose: { kind: [ 'entity' ], test: isBoolean, also: [ null, 'Composition' ] },
_ignore: { kind: true, enumerable: true, test: TODO }, // TODO: rename to $ignore/$delete
calculated: { kind: true, test: TODO }, // TODO remove

@@ -448,23 +471,10 @@ viaAll: { kind: true, test: TODO }, // TODO remove

$extra: { parser: true, test: TODO }, // for unexpectex properties in CSN
}
var _noSyntaxErrors = null;
};
let _noSyntaxErrors = null;
assertProp( model, null, stageParser ? ':parser' : ':model', null, true );
return
return;
function noSyntaxErrors() {
if (_noSyntaxErrors != null)
return _noSyntaxErrors;
// TODO: currently, we only have errors - use hasErrors() later
if (stageParser || !model.sources) {
_noSyntaxErrors = !(model.messages && model.messages.length);
return _noSyntaxErrors;
}
for (let file in model.sources) {
let s = model.sources[file];
if (s.messages && model.messages.length) {
_noSyntaxErrors = false;
return _noSyntaxErrors;
}
}
_noSyntaxErrors = true;
if (_noSyntaxErrors == null)
_noSyntaxErrors = !hasErrors( options.messages || model.messages ); // TODO: check messageId?
return _noSyntaxErrors;

@@ -474,43 +484,45 @@ }

function assertProp( node, parent, prop, extraSpec, noPropertyTest ) {
let spec = extraSpec || schema[ prop ] || schema[ prop.charAt() ];
let spec = extraSpec || schema[prop] || schema[prop.charAt()];
if (!spec)
throw new Error( `Property '${prop}' has not been specified`);
throw new Error( `Property '${ prop }' has not been specified`);
spec = inheritSpec( spec );
if (!noPropertyTest) {
let char = prop.charAt();
let parser = ('parser' in spec) ? spec.parser : char !== '_' && char !== '$';
const char = prop.charAt();
const parser = ('parser' in spec) ? spec.parser : char !== '_' && char !== '$';
if (stageParser && !parser)
throw new Error( `Non-parser property '${prop}' set by ${model.$frontend||''} parser${at( [node, parent] )}` );
let enumerable = ('enumerable' in spec) ? spec.enumerable : char !== '_';
if (parent.propertyIsEnumerable(prop) !== enumerable)
throw new Error( `Unexpected enumerability ${!enumerable}${at( [node, parent], prop )}` );
throw new Error( `Non-parser property '${ prop }' set by ${ model.$frontend || '' } parser${ at( [ node, parent ] ) }` );
const enumerable = ('enumerable' in spec) ? spec.enumerable : char !== '_';
if (enumerable instanceof Function
? !enumerable( parent, prop )
: {}.propertyIsEnumerable.call( parent, prop ) !== enumerable)
throw new Error( `Unexpected enumerability ${ !enumerable }${ at( [ node, parent ], prop ) }` );
}
(spec.test||standard)( node, parent, prop, spec,
typeof noPropertyTest === 'string' && noPropertyTest );
(spec.test || standard)( node, parent, prop, spec,
typeof noPropertyTest === 'string' && noPropertyTest );
}
function definition( node, parent, prop, spec, name ) {
if (name === 'cds' && node.kind === 'namespace') // do not check namespace 'cds'
// do not check namespaces 'cds' and 'localised' as they have no location
if ((name === 'cds' || name === 'localized') && node.kind === 'namespace')
return;
if (!(node instanceof Array))
node = [node];
node = [ node ];
// TODO: else check that there is a redefinition error
for (let art of node) {
for (const art of node)
standard( art, parent, prop, spec, name );
}
}
function column( node, ...args ) {
function column( node, ...rest ) {
if (node.val)
locationVal( isString )( node, ...args )
locationVal( isString )( node, ...rest );
else if (stageParser)
standard( node, ...args );
standard( node, ...rest );
else
isObject( node, ...args ); // TODO: and inside elements
isObject( node, ...rest ); // TODO: and inside elements
}
function pathItem( node, ...args ) {
function pathItem( node, ...rest ) {
if (node !== null || noSyntaxErrors())
standard( node, ...args );
standard( node, ...rest );
}

@@ -521,20 +533,20 @@

let names = Object.getOwnPropertyNames( node );
let requires = spec.requires || [];
const names = Object.getOwnPropertyNames( node );
const requires = spec.requires || [];
// Do not test 'requires' with parse errors:
for (let p of requires) {
for (const p of requires) {
if (!names.includes(p)) {
let req = spec.schema && spec.schema[p] && spec.schema[p].isRequired;
const req = spec.schema && spec.schema[p] && spec.schema[p].isRequired;
if ((req || schema[p] && schema[p].isRequired || noSyntaxErrors)( node ))
throw new Error( `Required property '${p}' missing in object${at( [node, parent], prop, name )}` );
throw new Error( `Required property '${ p }' missing in object${ at( [ node, parent ], prop, name ) }` );
}
}
let optional = spec.optional || [];
for (let n of names) {
let opt = (optional instanceof Array)
? optional.includes( n ) || optional.includes( n.charAt() )
: optional( n, spec );
if (!(opt || requires.includes( n ) || n === '$extra')) {
throw new Error( `Property '${n}' is not expected${at( [node[n], node, parent], prop, name )}` );
}
const optional = spec.optional || [];
for (const n of names) {
const opt = (optional instanceof Array)
? optional.includes( n ) || optional.includes( n.charAt() )
: optional( n, spec );
if (!(opt || requires.includes( n ) || n === '$extra'))
throw new Error( `Property '${ n }' is not expected${ at( [ node[n], node, parent ], prop, name ) }` );
assertProp( node[n], node, n, spec.schema && spec.schema[n] );

@@ -545,3 +557,3 @@ }

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

@@ -553,15 +565,18 @@ }

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

@@ -573,5 +588,5 @@ }

return spec;
let chain = [spec];
const chain = [ spec ];
while (spec.inherits) {
spec = schema[ spec.inherits ];
spec = schema[spec.inherits];
chain.push( spec );

@@ -583,8 +598,10 @@ }

function expressionOrString( node, ...args ) { // TODO: remove with onCond
function expressionOrString( node, ...rest ) { // TODO: remove with onCond
if (typeof node !== 'string')
expression( node, ...args );
expression( node, ...rest );
}
function expression( node, parent, prop, spec, idx ) {
if (typeof node === 'string') // TODO CSN parser: { val: <token>, literal: 'token' } for keywords
return;
while (node instanceof Array) {

@@ -596,3 +613,3 @@ // TODO: also check getOwnPropertyNames(node)

}
node = node[0];
[ node ] = node;
}

@@ -603,10 +620,10 @@ if (node == null && !noSyntaxErrors())

let s = spec[ expressionSpec(node) ] || {};
let sub = Object.assign( {}, s.inherits && schema[ s.inherits ], s );
const s = spec[expressionSpec(node)] || {};
const sub = Object.assign( {}, s.inherits && schema[s.inherits], s );
if (spec.requires && sub.requires)
sub.requires = [...sub.requires, ...spec.requires];
sub.requires = [ ...sub.requires, ...spec.requires ];
if (spec.optional && sub.optional)
sub.optional = [...sub.optional, ...spec.optional];
sub.optional = [ ...sub.optional, ...spec.optional ];
// console.log(expressionSpec(node) );
(sub.test||standard)( node, parent, prop, sub, idx );
(sub.test || standard)( node, parent, prop, sub, idx );
}

@@ -621,6 +638,5 @@

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

@@ -634,19 +650,20 @@

}
else if (spec.args === 'positional')
throw new Error( `Expected array ${at( [null, parent], prop )}` );
else if (node && typeof node === 'object' && !Object.getPrototypeOf( node ))
{
for (let n in node)
else if (spec.args === 'positional') {
throw new Error( `Expected array${ at( [ null, parent ], prop ) }` );
}
else if (node && typeof node === 'object' && !Object.getPrototypeOf( node )) {
for (const n in node)
expression( node[n], parent, prop, spec, n );
}
else
throw new Error( `Expected array or dictionary${at( [null, parent], prop )}` );
else {
throw new Error( `Expected array or dictionary${ at( [ null, parent ], prop ) }` );
}
}
function at( nodes, prop, name ) {
let n = (typeof name === 'number') ? ` for index ${name}` : name ? ` for "${name}"` : '';
let loc = nodes.find( o => o && typeof o === 'object' && (o.location||o.start) );
let f = (prop) ? ` in property '${prop}'` : '';
let l = loc && locationString( loc.location||loc ) || model.filename;
return (!l) ? n+f : n+f+' at '+l;
const n = name && (typeof name === 'number' ? ` for index ${ name }` : ` for "${ name }"`);
const loc = nodes.find( o => o && typeof o === 'object' && (o.location || o.start) );
const f = (prop) ? `${ n || '' } in property '${ prop }'` : n;
const l = loc && locationString( loc.location || loc ) || model.filename;
return (!l) ? f : `${ f } at ${ l }`;
}

@@ -659,6 +676,6 @@

if (!node || typeof node !== 'object' || Object.getPrototypeOf( node ))
throw new Error( `Expected dictionary${at( [null, parent], prop )}` );
for (let n in node)
throw new Error( `Expected dictionary${ at( [ null, parent ], prop ) }` );
for (const n in node)
func( node[n], parent, prop, spec, n );
}
};
}

@@ -669,5 +686,5 @@

if (!(node instanceof Array))
throw new Error( `Expected array${at( [null, parent], prop )}` );
throw new Error( `Expected array${ at( [ null, parent ], prop ) }` );
node.forEach( (item, n) => func( item, parent, prop, spec, n ) );
}
};
}

@@ -677,6 +694,6 @@

return function valWithLocation( node, parent, prop, spec, name ) {
var schema = { val: Object.assign( {}, spec, { test: func } ) };
var requires = ['val','location'];
var optional = ['literal','$inferred','augmented']; // TODO: remove augmented
standard( node, parent, prop, { schema, requires, optional }, name );
const valSchema = { val: Object.assign( {}, spec, { test: func } ) };
const requires = [ 'val', 'location' ];
const optional = [ 'literal', '$inferred', 'augmented' ]; // TODO: remove augmented
standard( node, parent, prop, { schema: valSchema, requires, optional }, name );
};

@@ -689,3 +706,3 @@ }

if (typeof node !== 'boolean')
throw new Error( `Expected boolean${at( [node, parent], prop )}` );
throw new Error( `Expected boolean${ at( [ node, parent ], prop ) }` );
}

@@ -697,3 +714,3 @@

if (typeof node !== 'number')
throw new Error( `Expected number${at( [node, parent], prop )}` );
throw new Error( `Expected number${ at( [ node, parent ], prop ) }` );
}

@@ -703,6 +720,6 @@

if (typeof node !== 'string')
throw new Error( `Expected string${at( [node, parent], prop )}` );
throw new Error( `Expected string${ at( [ node, parent ], prop ) }` );
// TODO: also check getOwnPropertyNames(node)
if (spec.enum && !spec.enum.includes( node ))
throw new Error( `Unexpected value '${ node }'${at( [node, parent], prop )}` );
throw new Error( `Unexpected value '${ node }'${ at( [ node, parent ], prop ) }` );
}

@@ -713,4 +730,4 @@

node.forEach( (item, n) => standard( item, parent, prop, spec, n ) );
else if (node !== null && !['string','number','boolean'].includes( typeof node ))
throw new Error( `Expected array or simple value${at( [null, parent], prop )}` );
else if (node !== null && ![ 'string', 'number', 'boolean' ].includes( typeof node ))
throw new Error( `Expected array or simple value${ at( [ null, parent ], prop ) }` );
}

@@ -720,3 +737,3 @@

if (!node || typeof node !== 'object' || Object.getPrototypeOf( node ) !== Object.prototype)
throw new Error( `Expected standard object${at( [null, parent], prop, name )}` );
throw new Error( `Expected standard object${ at( [ null, parent ], prop, name ) }` );
}

@@ -727,3 +744,3 @@

return;
isObject( art, parent, prop, spec, name )
isObject( art, parent, prop, spec, name );
if (stageParser) {

@@ -733,4 +750,5 @@ if (prop === 'artifacts')

}
else if (!art.name.absolute || !model.definitions[ art.name.absolute ])
throw new Error( `Expected definition${at( [art, parent], prop, name )}` );
else if (!art.name.absolute || !model.definitions[art.name.absolute]) {
throw new Error( `Expected definition${ at( [ art, parent ], prop, name ) }` );
}
}

@@ -737,0 +755,0 @@

@@ -7,8 +7,8 @@ // The builtin artifacts of CDS

var core = {
String: { parameters: ['length'] },
const core = {
String: { parameters: [ 'length' ] },
LargeString: {},
Binary: { parameters: ['length'] },
Binary: { parameters: [ 'length' ] },
LargeBinary: {},
Decimal: { parameters: ['precision', 'scale'] },
Decimal: { parameters: [ 'precision', 'scale' ] },
DecimalFloat: {},

@@ -23,9 +23,9 @@ Integer64: {},

Boolean: {},
Association: {internal:true},
Composition: {internal:true},
UUID: {},
Association: { internal: true },
Composition: { internal: true },
};
var hanaHana = {
ALPHANUM: { parameters: ['length'] },
const coreHana = {
// ALPHANUM: { parameters: [ 'length' ] },
SMALLINT: {},

@@ -35,20 +35,21 @@ TINYINT: {},

REAL: {},
CHAR: { parameters: ['length'] },
NCHAR: { parameters: ['length'] },
VARCHAR: { parameters: ['length'] },
CHAR: { parameters: [ 'length' ] },
NCHAR: { parameters: [ 'length' ] },
VARCHAR: { parameters: [ 'length' ] },
CLOB: {},
BINARY: { parameters: ['length'] },
ST_POINT: { parameters: [ {name: 'length', literal: 'number', val: 0} ]},
ST_GEOMETRY: { parameters: [ {name: 'length', literal: 'number', val: 0} ] }
}
BINARY: { parameters: [ 'length' ] },
// TODO: probably remove default for ST_POINT, ST_GEOMETRY (to be done in backend);
ST_POINT: { parameters: [ { name: 'srid', literal: 'number', val: 0 } ] },
ST_GEOMETRY: { parameters: [ { name: 'srid', literal: 'number', val: 0 } ] },
};
var hana = {
BinaryFloat: {},
LocalDate: {},
LocalTime: {},
UTCDateTime: {},
UTCTimestamp: {},
WithStructuredPrivilegeCheck: { kind: 'annotation' },
hana: { kind: 'context' }
}
// const hana = {
// BinaryFloat: {},
// LocalDate: {},
// LocalTime: {},
// UTCDateTime: {},
// UTCTimestamp: {},
// WithStructuredPrivilegeCheck: { kind: 'annotation' },
// hana: { kind: 'context' },
// };

@@ -62,49 +63,71 @@ const magicVariables = { // in SQL-92

// SQL-92: also SYSTEM_USER, just USER (is with parens in HANA SQL), VALUE
'$user': {
$user: {
elements: { id: {}, locale: {} },
$autoElement: 'id'
$autoElement: 'id',
}, // CDS-specific, not part of SQL
'$now': {}, // Dito
}
$at: {
elements: { from: {}, to: {} },
},
$now: {}, // Dito
};
const magicVariables_hana = {
CURRENT_CONNECTION: {},
CURRENT_SCHEMA: {},
CURRENT_TRANSACTION_ISOLATION_LEVEL: {},
CURRENT_UTCDATE: {},
CURRENT_UTCTIME: {},
CURRENT_UTCTIMESTAMP: {},
SYSUUID: {},
}
// const magicVariablesHana = {
// CURRENT_CONNECTION: {},
// CURRENT_SCHEMA: {},
// CURRENT_TRANSACTION_ISOLATION_LEVEL: {},
// CURRENT_UTCDATE: {},
// CURRENT_UTCTIME: {},
// CURRENT_UTCTIMESTAMP: {},
// SYSUUID: {},
// };
function initBuiltins( model ) {
if (!model.options.hanaFlavor) {
setMagicVariables( magicVariables );
let cds = {
setMagicVariables( magicVariables );
// namespace:"cds" stores the builtins ---
const cds = createNamespace( 'cds', 'reserved' );
// setProp( model.definitions, 'cds', cds );
model.definitions.cds = cds; // not setProp - oData - TODO: still needed?
model.$builtins = env( core, 'cds.', cds );
model.$builtins.cds = cds;
// namespace:"cds.hana" stores HANA-specific builtins ---
const hana = createNamespace( 'cds.hana', 'reserved' );
setProp( model.definitions, 'cds.hana', hana );
model.$builtins.hana = hana;
cds.artifacts.hana = hana;
env( coreHana, 'cds.hana.', hana );
// namespace:"localized" stores localized convenience views ---
const localized = createNamespace( 'localized', true );
model.definitions.localized = localized;
model.$internal = { $frontend: '$internal' };
return;
function createNamespace( name, builtin ) {
return {
kind: 'namespace',
name: { absolute: 'cds' },
name: { absolute: name, location: createDummyLocation() },
blocks: [],
artifacts: Object.create(null) };
// setProp( model.definitions, 'cds', cds );
model.definitions.cds = cds; // not setProp - oData
model.$builtins = env( core, 'cds.', undefined, cds );
model.$builtins.cds = cds;
artifacts: Object.create(null),
builtin,
location: createDummyLocation(),
};
// buitlin namespaces don't have a cds file, so no location available
function createDummyLocation() {
return {
filename: '<built-in>',
start: { offset: 0, line: 1, column: 1 },
end: { offset: 0, line: 1, column: 1 },
};
}
}
else {
setMagicVariables( Object.assign( {}, magicVariables, magicVariables_hana ) );
let artifacts = env( Object.assign( {}, core, hana ), 'cds.', 'sap.cds::' );
artifacts.hana.artifacts = env( hanaHana, 'hana.' );
model.$builtins = artifacts;
}
model.$internal = { $frontend: '$internal' };
return;
function env( builtins, prefix, extraPrefix, parent ) {
let artifacts = Object.create(null);
for (let name in builtins) {
let absolute = prefix + name;
let art = { kind: 'type', builtin: true, name: {absolute}, type: { path:[{id:absolute}] } };
function env( builtins, prefix, parent ) {
const artifacts = Object.create(null);
for (const name of Object.keys( builtins )) {
const absolute = prefix + name;
const art = {
kind: 'type', builtin: true, name: { absolute }, type: { path: [ { id: absolute } ] },
};
setProp( art.type, '_artifact', art );
if (parent)
parent.artifacts[ name ] = art;
parent.artifacts[name] = art;
setProp( art, '_finalType', art );

@@ -116,4 +139,2 @@ setProp( art, '_deps', [] );

setProp( model.definitions, absolute, art );
if (extraPrefix)
setProp( model.definitions, extraPrefix + name, art );
}

@@ -124,10 +145,10 @@ return artifacts;

function setMagicVariables( builtins ) {
let artifacts = Object.create(null);
for (let name in builtins) {
let magic = builtins[name];
const artifacts = Object.create(null);
for (const name in builtins) {
const magic = builtins[name];
// TODO: rename to $builtinFunction
let art = { kind: 'builtin', name: { id: name, element: name } };
const art = { kind: 'builtin', name: { id: name, element: name } };
artifacts[name] = art;
if (magic.elements)
art.elements = forEachInDict( magic.elements, (e,n) => magicElement( e, n, art ));
art.elements = forEachInDict( magic.elements, (e, n) => magicElement( e, n, art ));
if (magic.$autoElement)

@@ -141,5 +162,5 @@ art.$autoElement = magic.$autoElement;

function magicElement( spec, name, parent ) {
let magic = {
const magic = {
kind: 'builtin',
name: { id: name, element: parent.name.element + '.' + name }
name: { id: name, element: `${ parent.name.element }.${ name }` },
};

@@ -153,4 +174,4 @@ setProp( magic, '_parent', parent );

// Like `obj.prop = value`, but not contained in JSON / CSN
function setProp ( obj, prop, value ) {
Object.defineProperty( obj, prop, { value, configurable: true, writable: true } ); // not enumerable!
function setProp( obj, prop, value ) {
Object.defineProperty( obj, prop, { value, configurable: true, writable: true } );
return value;

@@ -157,0 +178,0 @@ }

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

function detectCycles( definitions, reportCycle, cbScc ) {
var index = 0;
var stack = [];
let index = 0;
const stack = [];
if (!cbScc)
cbScc = defaultCb; // cannot use as default in parameter def
for (let name in definitions) {
let a = definitions[name];
for (const name in definitions) {
const a = definitions[name];
if (a instanceof Array)

@@ -39,3 +39,3 @@ a.forEach( strongConnectRec );

let nodes = Object.getOwnPropertyNames(definitions).map( n => definitions[n] );
while (nodes.length) {
while (nodes.length) { // still nodes to cleaned
nodes = cleanup(nodes);

@@ -57,3 +57,8 @@ }

if (!v._scc) {
setProp( v, '_scc', { index, lowlink: index, onStack: true, depIndex: 0 } );
setProp( v, '_scc', {
index,
lowlink: index,
onStack: true,
depIndex: 0,
} );
stack.push(v);

@@ -64,7 +69,7 @@ // console.log('PUSH: ', v.kind,v.name)

setProp(v, '_deps', []);
//assert( v._scc.onStack );
// assert( v._scc.onStack );
// Now consider successors of v (called w):
while (v._scc.depIndex < v._deps.length) {
let w = v._deps[v._scc.depIndex++].art;
const w = v._deps[v._scc.depIndex++].art;
if (!w._scc) { // node has not yet been visited

@@ -83,3 +88,3 @@ setProp(w, '_sccCaller', v);

// If v is a root node, pop the stack and process SCC
if (v._scc.lowlink == v._scc.index) {
if (v._scc.lowlink === v._scc.index) {
let w;

@@ -104,3 +109,3 @@ // First set `lowlink` to `lowlink` of root to => all nodes in a SCC have

// Now do the stuff after call in recursive algorithm
let caller = v._sccCaller;
const caller = v._sccCaller;
if (caller && caller._scc.lowlink > v._scc.lowlink)

@@ -112,4 +117,4 @@ caller._scc.lowlink = v._scc.lowlink;

function defaultCb( w, v, r ) {
for (let dep of w._deps) {
if (dep.art._scc.lowlink == w._scc.lowlink) // in same SCC
for (const dep of w._deps) {
if (dep.art._scc.lowlink === w._scc.lowlink) // in same SCC
reportCycle( dep.art, dep.location, w );

@@ -122,9 +127,9 @@ }

// return further objects to be cleaned.
function cleanup( nodes ) {
var todos = [];
for (let v of nodes) {
function cleanup( nodeSet ) {
const todos = [];
for (const v of nodeSet) {
if (v._scc) {
delete v._scc;
delete v._sccCaller;
for (let w of v._deps) {
for (const w of v._deps) {
if (w.art._scc)

@@ -141,3 +146,3 @@ todos.push(w.art);

// Like `obj.prop = value`, but not contained in JSON / CSN
function setProp (obj, prop, value) {
function setProp( obj, prop, value ) {
Object.defineProperty( obj, prop, { value, configurable: true } ); // not enumerable!

@@ -144,0 +149,0 @@ }

@@ -83,9 +83,23 @@ // Compiler phase "define": transform dictionary of AST-like CSNs into augmented CSN

const { getMessageFunction, searchName, weakLocation } = require('../base/messages');
const { queryOps, setProp, forEachGeneric, forEachInOrder, forEachMember }
const {
queryOps, setProp, forEachGeneric, forEachInOrder, forEachMember,
}
= require('../base/model');
var { addToDict, addToDictWithIndexNo, clearDict, dictLocation, pushToDict }
const {
addToDict, addToDictWithIndexNo, clearDict, dictLocation, pushToDict,
}
= require('../base/dictionaries');
const { dictKinds, kindProperties, fns, setLink, linkToOrigin, setMemberParent, storeExtension, combinedLocation } = require('./shared');
const {
dictKinds,
kindProperties,
fns,
setLink,
linkToOrigin,
setMemberParent,
storeExtension,
combinedLocation,
} = require('./shared');
const { compareLayer, layer } = require('./moduleLayers');
var initBuiltins = require('./builtins');
const initBuiltins = require('./builtins');
const { addBoolAnnotationTo } = require('../model/modelUtils');

@@ -100,27 +114,36 @@ // Export function of this file. Transform argument `sources` = dictionary of

// artifacts.
function define( model, options = model.options || {} ) {
function getDefinerFunctions( model ) {
const { options } = model;
// Get simplified "resolve" functionality and the message function:
const message = getMessageFunction( model );
const {
resolveUncheckedPath,
resolvePath,
defineAnnotations,
} = fns( model );
model.definitions = Object.create(null);
setProp( model, '_entities', [] ); // for entities with includes
resolveUncheckedPath, resolvePath, defineAnnotations, setAutoExposed,
}
= fns( model );
const extensionsDict = Object.create(null);
return {
define, initArtifacts, lateExtensions, getOriginRecursive, hasTruthyProp,
};
var extensionsDict = Object.create(null);
var lateExtensionsDict = Object.create(null); // for generated artifacts
initBuiltins( model );
for (let name in model.sources) {
initSource( model.sources[name] );
function define() {
model.definitions = Object.create(null);
setProp( model, '_entities', [] ); // for entities with includes
model.$entity = 0;
model.$compositionTargets = Object.create(null);
model.$lateExtensions = Object.create(null); // for generated artifacts
initBuiltins( model );
for (const name in model.sources)
initSource( model.sources[name] );
applyExtensions();
forEachGeneric( model, 'definitions', processArtifact );
lateExtensions( true );
// 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;
}
applyExtensions();
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;

@@ -131,13 +154,18 @@ function checkRedefinitions( obj, name, prop, i ) {

return;
if (obj.name.location.filename === '<built-in>') {
// builtin types like namespace 'cds' or namespace 'localized' shouldn't be printed.
// The error shall only be printed for the user-defined conflicting artifact.
return;
}
message( 'duplicate-definition', obj.name.location, obj,
{ name, '#': (obj.kind === 'namespace') ? 'namespace' : dictKinds[prop] },
'Error', {
std: 'Duplicate definition of $(NAME)',
absolute: 'Duplicate definition of artifact $(NAME)',
std: 'Duplicate definition of $(NAME)',
absolute: 'Duplicate definition of artifact $(NAME)',
namespace: 'Other definition blocks $(NAME) for namespace name',
element: 'Duplicate definition of element $(NAME)',
enum: 'Duplicate definition of enum $(NAME)',
key: 'Duplicate definition of key $(NAME)',
action: 'Duplicate definition of action or function $(NAME)',
param: 'Duplicate definition of parameter $(NAME)',
element: 'Duplicate definition of element $(NAME)',
enum: 'Duplicate definition of enum $(NAME)',
key: 'Duplicate definition of key $(NAME)',
action: 'Duplicate definition of action or function $(NAME)',
param: 'Duplicate definition of parameter $(NAME)',
});

@@ -151,8 +179,8 @@ }

if (src.definitions) {
forEachGeneric( src, 'definitions', function( art, name ) {
if (!art.kind) // TODO: should be done by augmentor
art.kind = 'type';
let dot = name.lastIndexOf('.');
forEachGeneric( src, 'definitions', ( art, name ) => {
if (!art.kind) // Wrong CSN input:
art.kind = 'type'; // ...use default as defined for CSN (TODO: or extra to avoid further errors?)
const dot = name.lastIndexOf('.');
if (dot > name.indexOf('::')) // there is a dot (after '::')
addAsContextArtifact( art, name.substring(dot+1), name.substring(0,dot), src );
addAsContextArtifact( art, name.substring(dot + 1), name.substring(0, dot), src );
setProp( art, '_block', src );

@@ -167,5 +195,6 @@ addToDefinitions( art, name );

initUsings( src, src.usings );
let decl = src.namespace;
let prefix = (decl && decl.dcPath) ? pathName(decl.dcPath) + '::' : '';
let namespace = decl && decl.path && namespaceContext( decl.path, prefix, undefined, decl.location );
const decl = src.namespace;
const prefix = (decl && decl.dcPath) ? `${ pathName(decl.dcPath) }::` : '';
const namespace = decl && decl.path &&
namespaceContext( decl.path, prefix, undefined, decl.location );
if (!namespace) {

@@ -178,21 +207,24 @@ // also handles `extensions` property

// create using for own namespace:
let last = decl.path[ decl.path.length - 1 ];
let builtin = model.$builtins[ last.id ];
if (builtin && !builtin.internal) {
message( 'ref-shadowed-builtin', src.namespace.location, null, // no home artifact
{ id: last.id, art: src.namespace, code: `using ${builtin.name.absolute};` },
const last = decl.path[decl.path.length - 1];
const builtin = model.$builtins[last.id];
const { absolute } = namespace.name;
if (builtin && !builtin.internal && absolute !== 'cds') {
message( 'ref-shadowed-builtin', namespace.location, null, // no home artifact
{ id: last.id, art: namespace, code: `using ${ builtin.name.absolute };` },
'Warning', '$(ID) now refers to $(ART) - consider $(CODE)' );
}
let block = {
kind: 'source', filename: src.filename,
const block = {
kind: 'source',
filename: src.filename,
name: { id: last.id, location: last.location }, // for --raw-output
artifacts: Object.create(null)
artifacts: Object.create(null),
};
let absolute = namespace.name.absolute;
block.artifacts[ last.id ] = { // assert( last.id === namespace.name.id )
block.artifacts[last.id] = { // assert( last.id === namespace.name.id )
kind: 'using', // TODO: or store namespace directly?
//implicit: true,
name: { id: last.id, absolute, location: last.location, calculated: true, $inferred: 'as' },
// implicit: true,
name: {
id: last.id, absolute, location: last.location, calculated: true, $inferred: 'as',
},
extern: decl,
location: decl.location
location: decl.location,
};

@@ -205,12 +237,14 @@ setProp( src, '_block', block );

function namespaceContext( path, prefix = '', parent, declLocation ) {
// declLocation also means: check reserved cds on last item
let absolute;
if (path.broken)
return parent;
for (let item of path) {
let id = item.id;
absolute = (absolute) ? absolute + '.' + id : prefix + id;
const check = declLocation && path[path.length - 1];
for (const item of path) {
const { id } = item;
absolute = (absolute) ? `${ absolute }.${ id }` : prefix + id;
let context = model.definitions[absolute];
if (!context || context instanceof Array ||
!kindProperties[ context.kind ].artifacts) {
let location = combinedLocation( path[0], item );
!kindProperties[context.kind].artifacts) {
const location = combinedLocation( path[0], item );
context = {

@@ -221,3 +255,3 @@ kind: 'namespace',

artifacts: Object.create(null),
location: declLocation || location
location: declLocation || location,
};

@@ -229,3 +263,3 @@ if (parent) {

}
addToDefinitions( context, absolute );
addToDefinitions( context, absolute, undefined, undefined, item !== check );
setProp( context, '_parent', parent || null );

@@ -242,11 +276,10 @@ }

// another artifact has already been defined with the same absolute name.
// TODO: do our own check for reserved names in compiler !!!
function addToDefinitions( art, absolute = art.name.absolute, prefix, parent ) {
function addToDefinitions( art, absolute = art.name.absolute, prefix, parent, noReservedCheck ) {
let context = reuse();
//console.log(context?'Reuse':'Def',art.name,prefix ,parent&&parent.name)
// console.log(context?'Reuse':'Def',art.name,prefix ,parent&&parent.name)
if (context)
return context;
if (prefix != null && art.name.path && art.name.path.length > 1) {
let path = [...art.name.path];
let id = path.pop();
const path = [ ...art.name.path ];
const id = path.pop();
art.name.id = id.id;

@@ -264,18 +297,22 @@ context = namespaceContext( path, prefix, parent );

}
if (absolute === 'cds') {
// TODO: move all 'cds' prefix checks into compiler
message( null, art.name.location, parent,
`The namespace "cds" is reserved for CDS builtins` );
if (!noReservedCheck &&
(absolute === 'cds' ||
absolute.match(/^cds\./) && !absolute.match(/^cds\.foundation(\.|$)/))) {
message( null, art.name.location, null,
'The namespace "cds" is reserved for CDS builtins' );
}
else
addToDict( model.definitions, absolute, art );
else if (!noReservedCheck && absolute === 'localized') {
message( null, art.name.location, null,
'The namespace "localized" is reserved for localized convenience views' );
}
addToDict( model.definitions, absolute, art );
return art;
function reuse( ) {
if (!kindProperties[ art.kind ].artifacts) // no context, service or namespace
if (!kindProperties[art.kind].artifacts) // no context, service or namespace
return undefined;
let found = model.definitions[absolute];
const found = model.definitions[absolute];
if (!found || found instanceof Array)
return undefined;
if (art.kind === 'namespace' && kindProperties[ found.kind ].artifacts)
if (art.kind === 'namespace' && kindProperties[found.kind].artifacts)
// we are namespace, `found` is context, service or namespace

@@ -291,18 +328,18 @@ return found;

function addAsContextArtifact( art, id, contextName, src ) {
function addAsContextArtifact( art, name, contextName, src ) {
let context = src.definitions[contextName];
if (!context) {
let location = art.name.location;
const { location } = art.name;
let colons = contextName.indexOf('::');
colons = (colons < 0) ? 0 : colons + 2;
let path = contextName.substring(colons).split('.').map( id => ({ id, location }) );
context = namespaceContext( path, contextName.substring(0,colons), undefined, location );
const path = contextName.substring(colons).split('.').map( id => ({ id, location }) );
context = namespaceContext( path, contextName.substring(0, colons) );
}
if (context instanceof Array || !context.kind) // no proper construct
return;
if (!kindProperties[ context.kind ].artifacts) // no context or service
if (!kindProperties[context.kind].artifacts) // no context or service
return;
if (!context.artifacts)
context.artifacts = Object.create(null);
addToDict( context.artifacts, id, art );
addToDict( context.artifacts, name, art );
setProp( art, '_parent', context );

@@ -319,3 +356,3 @@ }

src.artifacts = Object.create(null);
for (let def of usings) {
for (const def of usings) {
if (def.usings) {

@@ -325,16 +362,17 @@ initUsings( src, def.usings );

}
let ue = def.extern;
const ue = def.extern;
if (!def.name) {
def.name = Object.assign( { calculated: true, $inferred: 'as' },
ue.path[ ue.path.length - 1] );
ue.path[ue.path.length - 1] );
}
if (!(ue.path.broken || ue.dcPath && ue.dcPath.broken)) {
def.name.absolute = (ue.dcPath)
? pathName(ue.dcPath) + '::' + pathName(ue.path)
? `${ pathName(ue.dcPath) }::${ pathName(ue.path) }`
: pathName(ue.path);
}
addToDict( src.artifacts, def.name.id, def, function( name, loc, art ) {
if (art.kind === 'using') // repeated defs would be shown repeatedly otherwise
addToDict( src.artifacts, def.name.id, def, ( name, loc, art ) => {
if (art.kind === 'using') { // repeated defs would be shown repeatedly otherwise
message( 'duplicate-using', loc, null, { name }, 'Error',
'Duplicate definition of top-level name $(NAME)' );
}
} );

@@ -350,13 +388,13 @@ }

function initArtifacts( construct, parent, block, collectExts,
prefix = parent.name.absolute + '.', defProp ) {
prefix = `${ parent.name.absolute }.`, defProp ) {
// if (parent && !checkDefinitions( construct, parent, 'artifacts' ))
// return;
// TODO: the checkDefinitions must be propably checked by the parsers
//let dottedNames = [];
//let namespaces = Object.create(null);
for (let name of Object.keys( construct.artifacts || {} )) {
// no `forEachGeneric` as setName might add artifacts, e.g. "A" for "A.B"
let def = construct.artifacts[name];
// let dottedNames = [];
// let namespaces = Object.create(null);
for (const name of Object.keys( construct.artifacts || {} )) {
// no `forEachGeneric` as setName might add artifacts, e.g. "A" for "A.B"
const def = construct.artifacts[name];
if (def instanceof Array) {
for (let art of def)
for (const art of def)
setName( art, name );

@@ -376,17 +414,17 @@ }

function setName( art, pathName ) {
function setName( art, localName ) {
if (art.kind === 'using')
return;
setProp( art, '_block', block );
art.name.absolute = prefix + pathName;
let path = art.name.path;
art.name.absolute = prefix + localName;
const { path } = art.name;
if (!path || path.length <= 1)
return;
let root = path[0];
let id = root.id;
let namespace = namespaceContext( [root], prefix, parent );
const root = path[0];
const { id } = root;
const namespace = namespaceContext( [ root ], prefix, parent );
if (construct.artifacts[id])
// TODO: complain if USING uses that name
return; // TODO: do we need to set root._artifact?
let name = { absolute: prefix + id, id, location: root.location };
const name = { absolute: prefix + id, id, location: root.location };
setProp( name, '_artifact', namespace );

@@ -399,6 +437,6 @@ construct.artifacts[id] = { kind: 'block', name, pathPrefix: true };

return;
if (kindProperties[ art.kind ].artifacts) { // context or service
if (kindProperties[art.kind].artifacts) { // context or service
if (defProp) { // from JSON parser
defineAnnotations( art, art, block );
art.blocks = [null]; // TODO: assert that there are no blocks before
art.blocks = [ null ]; // TODO: assert that there are no blocks before
if (!art.artifacts) // if art had been no namespace before

@@ -409,3 +447,3 @@ art.artifacts = Object.create(null); // TODO: test with A.B.C then A.B in definitions

}
let env = art;
const env = art;
art = addToDefinitions( shallowContext(env), undefined, prefix, parent );

@@ -440,7 +478,8 @@ env.kind = 'block';

art.$queries = [];
art.queries = [];
art.queries = []; // TODO: remove
setProp( art, '_leadingQuery', initQueryExpression( art, art.query ) );
setProp( art._leadingQuery, '_$next', art );
if (art._leadingQuery) // null in case of parser error
setProp( art._leadingQuery, '_$next', art );
// resolve parameters and actions:
initMembers( art, art, block ); // before setting art.elements!
initMembers( art, art, block ); // before setting art.elements!
// TODO: Also allow extension if without union (even then, but later

@@ -453,6 +492,6 @@ // check checks whether union can be used)?

if (art.dbType && !options.hanaFlavor)
message( null, art.dbType.location, art, `TABLE TYPE is not supported yet` );
message( null, art.dbType.location, art, 'TABLE TYPE is not supported yet' );
defineAnnotations( art, art, block );
initMembers( art, art, block );
let absolute = art.name.absolute;
const { absolute } = art.name;
if (art.includes && !(absolute in extensionsDict))

@@ -465,9 +504,11 @@ extensionsDict[absolute] = []; // structure with includes must be "extended"

function initDollarSelf( art ) {
let selfname = (options.hanaFlavor) ? art.name.id : '$self';
const selfname = (options.hanaFlavor) ? art.name.id : '$self';
// TODO: users setMemberParent() ?
let self = {
const self = {
name: { id: selfname, alias: selfname, absolute: art.name.absolute },
kind: '$tableAlias', self: true,
kind: '$tableAlias',
self: true,
type: {},
location: art.location };
location: art.location,
};
setProp( self, '_parent', art );

@@ -483,6 +524,7 @@ setProp( self, '_main', art ); // used on main artifact

// TODO: users setMemberParent() ?
let parameters = {
const parameters = {
name: { id: '$parameters', param: '$parameters', absolute: art.name.absolute },
kind: '$parameters',
location: art.location };
location: art.location,
};
setProp( parameters, '_parent', art );

@@ -507,3 +549,3 @@ setProp( parameters, '_main', art );

// TODO: MIXIN with name = ...subquery (not yet supported anyway)
for (let elem of query.columns || []) {
for (const elem of query.columns || []) {
if (elem && elem.value) {

@@ -527,5 +569,5 @@ setProp( elem, '_block', query._block );

return;
else if (expr.op && queryOps[ expr.op.val ]) {
else if (expr.op && queryOps[expr.op.val])
initQueryExpression( query, expr ); // adds to query.queries
}
else if (expr.args)

@@ -538,3 +580,3 @@ expr.args.forEach( e => initExprForQuery( e, query ) );

while (query instanceof Array) // query / join args in parentheses
query = query[0];
[ query ] = query;
if (!query) // parse error

@@ -548,7 +590,9 @@ return query;

if (!query.name) {
let last = query.path[ query.path.length-1 ];
let dot = last.id.lastIndexOf('.');
let id = (dot < 0) ? last.id : last.id.substring( dot+1 );
const last = query.path[query.path.length - 1];
const dot = last.id.lastIndexOf('.');
const id = (dot < 0) ? last.id : last.id.substring( dot + 1 );
// TODO: if we have too much time, we can calculate the real location with '.'
query.name = { calculated: true, $inferred: 'as', id, location: last.location };
query.name = {
calculated: true, $inferred: 'as', id, location: last.location,
};
}

@@ -561,19 +605,24 @@ addAlias( { type: query }, query );

if (query.on) {
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)
addQuery( parents[0].name.query );
initSubQuery( query );
parents = [...parents, query];
parents = [ ...parents, query ];
}
for (let tab of query.args)
for (const tab of query.args)
initQueryExpression( art, tab, parents );
if (query.name) { // with ON (after processing args to get the $tableAliases
const aliases = Object.keys( query.$tableAliases || {} );
// Use first tabalias name on the right side of the join to name the
// (internal) query, should only be relevant for --raw-output, not for
// user messages or references
query.name.param = aliases[1] || aliases[0] || '<unknown>';
}
}
else if (query.op && query.op.val === 'query') { // select
addQuery();
query._main.$queries.push( query ); // TODO: set number with it
else if (query.from) { // select
addQuery( (art._main || art).$queries.length );
// TODO: 1-based if view elems are proxies to leading queries elements
query._main.$queries.push( query );
if (parents.length)
addAlias( {}, query );
for (let tab of query.from)
initQueryExpression( art, tab, [query] );
for (const tab of query.from)
initQueryExpression( art, tab, [ query ] );
if (query.mixin)

@@ -585,4 +634,5 @@ addMixin();

name: { alias: '$projection', query: query.name.query, absolute: art.name.absolute },
kind: '$tableAlias', self: true,
location: query.location
kind: '$tableAlias',
self: true,
location: query.location,
};

@@ -596,4 +646,5 @@ setProp( query.$tableAliases.$projection, '_parent', query );

name: { alias: '$self', query: query.name.query, absolute: art.name.absolute },
kind: '$tableAlias', self: true,
location: query.location
kind: '$tableAlias',
self: true,
location: query.location,
};

@@ -607,7 +658,7 @@ setProp( query.$tableAliases.$self, '_parent', query );

else if (query.args) { // UNION, INTERSECT, ..., sub query
let leading = initQueryExpression( art, query.args[0], [] );
for (let q of query.args.slice(1))
const leading = initQueryExpression( art, query.args[0], [] );
for (const q of query.args.slice(1))
initQueryExpression( art, q, [] );
setProp( query, '_leadingQuery', leading );
if (query.orderBy) {
if (leading && query.orderBy) {
if (leading.$orderBy)

@@ -623,3 +674,3 @@ leading.$orderBy.push( ...query.orderBy );

}
// else: with parse error (`select from <EOF>`)
// else: with parse error (`select from <EOF>`, `select from E { *, ( select }`)
return query._leadingQuery || query;

@@ -630,3 +681,3 @@

'Error',
{ '$tableAlias': 'Duplicate definition of table alias or mixin $(NAME)' } );
{ $tableAlias: 'Duplicate definition of table alias or mixin $(NAME)' } );
}

@@ -647,3 +698,3 @@

alias.kind = '$tableAlias';
let parent = parents[0];
const parent = parents[0];
setMemberParent( alias, alias.name.id, parent );

@@ -653,4 +704,4 @@ if (!parent._firstAliasInFrom)

setProp( subquery, '_tableAlias', alias );
for (let p of parents) {
//console.log('ADD:', query.name.id, parents.length, p)
for (const p of parents) {
// console.log('ADD:', query.name.id, parents.length, p)
addToDict( p.$tableAliases, alias.name.id, alias, p === parent && signalDuplicate );

@@ -662,4 +713,4 @@ }

// TODO: re-check if mixins have already duplicates
for (let name in query.mixin) {
let mixin = query.mixin[name];
for (const name in query.mixin) {
const mixin = query.mixin[name];
if (!(mixin instanceof Array)) {

@@ -680,3 +731,3 @@ setMemberParent( mixin, name, query );

function addQuery() {
function addQuery( name ) {
setProp( query, '_$next', art );

@@ -687,3 +738,3 @@ setProp( query, '_block', art._block );

query.name = { location: query.location };
setMemberParent( query, art.queries.length, art );
setMemberParent( query, name, art );
art.queries.push( query );

@@ -695,3 +746,3 @@ query.$tableAliases = Object.create( null ); // table aliases and mixin definitions

function shallowContext( ext ) {
let art = {
const art = {
kind: ext.kind,

@@ -701,3 +752,3 @@ name: Object.assign( {}, ext.name ),

artifacts: Object.create(null),
location: ext.location
location: ext.location,
};

@@ -714,4 +765,4 @@ setProp( art, '_block', ext._block ); // keep _block for service includes

function initMembers( construct, parent, block ) {
let isQueryExtension = kindProperties[ construct.kind ].isExtension &&
(parent._main||parent).query;
const isQueryExtension = kindProperties[construct.kind].isExtension &&
(parent._main || parent).query;
let obj = construct.returns || construct; // why the extra `returns` for actions?

@@ -726,4 +777,4 @@ if (construct.returns)

if (obj !== parent && obj.elements && parent.enum) {
for (let n in obj.elements) {
let e = obj.elements[n];
for (const n in obj.elements) {
const e = obj.elements[n];
if (e.kind === 'element')

@@ -751,8 +802,10 @@ e.kind = 'enum';

function init ( elem, name, prop ) {
function init( elem, name, prop ) {
if (!elem.kind) // wrong CSN input
elem.kind = dictKinds[prop];
if (!elem.name) {
let ref = elem.targetElement || elem.kind === 'element' && elem.value;
const ref = elem.targetElement || elem.kind === 'element' && elem.value;
if (ref && ref.path) {
elem.name = Object.assign( { calculated: true, $inferred: 'as' },
ref.path[ ref.path.length-1 ] );
ref.path[ref.path.length - 1] );
}

@@ -764,3 +817,3 @@ else { // if JSON parser misses to set name

// if (!kindProperties[ elem.kind ]) console.log(elem.kind,elem.name)
if (kindProperties[ elem.kind ].isExtension) {
if (kindProperties[elem.kind].isExtension) {
storeExtension( elem, name, prop, parent, block );

@@ -770,10 +823,11 @@ }

message( 'extend-query', elem.location, construct, // TODO: searchName ?
{ art: parent._main||parent },
{ art: parent._main || parent },
'Error', 'Query entity $(ART) can only be extended with actions' );
}
else {
setProp( elem, '_block', block );
const bl = elem._block || block;
setProp( elem, '_block', bl );
setMemberParent( elem, name, parent, construct !== parent && prop );
defineAnnotations( elem, elem, block );
initMembers( elem, elem, block );
defineAnnotations( elem, elem, bl );
initMembers( elem, elem, bl );
}

@@ -788,9 +842,12 @@ }

return false;
let feature = kindProperties[ parent.kind ][prop];
const names = Object.getOwnPropertyNames( dict );
if (!names.length)
return false;
const feature = kindProperties[parent.kind][prop];
if (feature &&
(feature === true || construct.kind !== 'extend' || feature( prop, parent )))
return true;
let location = dictLocation( dict );
const location = dictLocation( names.map( name => dict[name] ) );
if (prop === 'actions') {
message( 'unexpected-actions', location, {}, construct, 'Error',
message( 'unexpected-actions', location, construct, {}, 'Error',
'Actions and functions only exist top-level and for entities' );

@@ -803,8 +860,10 @@ }

else if (prop === 'params') {
if (!feature)
if (!feature) {
message( 'unexpected-params', location, construct, {}, 'Error',
'Parameters only exist for entities, actions or functions' );
else
}
else {
message( 'extend-with-params', location, construct, {}, 'Error', // remark: we could allow this
'Extending artifacts with parameters is not supported' );
}
}

@@ -833,10 +892,10 @@ else if (feature) { // allowed in principle, but not with extend

function setAncestorsAndService( name ) {
let art = model.definitions[ name ];
const art = model.definitions[name];
if (!('_parent' in art))
return; // nothing to do for builtins and redefinitions
if (art.$from && !('_ancestors' in art)) {
if (art.$from && !('_ancestors' in art))
setProjectionAncestors( art );
}
let parent = art._parent;
let service = (parent && parent.kind !== 'service') ? parent._service : parent;
const service = (parent && parent.kind !== 'service') ? parent._service : parent;
setProp( art, '_service', service && !service.abstract && service );

@@ -848,8 +907,10 @@ if (service == null) // do not return on false (= in abstract service)

parent = parent._parent;
if (art.kind === 'service')
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')
[ '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)' );
[ 'Error' ], 'A context cannot be nested within a service $(ART)' );
}
}

@@ -860,4 +921,5 @@

// on a generated entity.
let chain = [];
while (art && !('_ancestors' in art) &&
const chain = [];
const autoexposed = art['@cds.autoexposed'];
while (art && !('_ancestors' in art) &&
art.$from && art.$from.length === 1 &&

@@ -867,9 +929,11 @@ art.query.op && art.query.op.val === 'query') {

setProp( art, '_ancestors', null ); // avoid infloop with cyclic from
let name = resolveUncheckedPath( art.$from[0], 'include', art );
const name = resolveUncheckedPath( art.$from[0], 'include', art );
// TODO: do not set _ancestors if params change
art = name && projectionAncestor( model.definitions[ name ], art.params );
art = name && projectionAncestor( model.definitions[name], art.params );
if (autoexposed)
break; // only direct projection for auto-exposed
}
let ancestors = art && (art._ancestors || []);
for (let a of chain.reverse()) {
ancestors = (ancestors ? [...ancestors, art] : []);
let ancestors = art && (!autoexposed && art._ancestors || []);
for (const a of chain.reverse()) {
ancestors = (ancestors ? [ ...ancestors, art ] : []);
setProp( a, '_ancestors', ancestors );

@@ -885,20 +949,21 @@ art = a;

return !source.params && source;
let sourceParams = source.params || Object.create(null);
for (let n in sourceParams) {
const sourceParams = source.params || Object.create(null);
for (const n in sourceParams) {
if (!(n in params)) // source param is not projection param
return null; // -> cannot be used as implicit redirection target
}
for (let n in params) {
let pp = params[n];
let sp = sourceParams[n];
for (const n in params) {
const pp = params[n];
const sp = sourceParams[n];
if (sp) {
if (sp.default && !pp.default)
if (sp.default && !pp.default) // param DEFAULT clause not supported yet
return null; // param is not optional anymore
let pt = pp.type && resolveUncheckedPath( pp.type, 'type', pp );
let st = sp.type && resolveUncheckedPath( sp.type, 'type', sp );
const pt = pp.type && resolveUncheckedPath( pp.type, 'type', pp );
const st = sp.type && resolveUncheckedPath( sp.type, 'type', sp );
if ((pt || null) !== (st || null))
return null; // params have different type
}
else if (!pp.default)
return null; // non-optional param in projection, but not source
else if (!pp.default) {
return null;
} // non-optional param in projection, but not source
}

@@ -909,8 +974,9 @@ return source;

function postProcessArtifact( art ) {
tagCompositionTargets( art );
if (!art._ancestors || art.kind === 'type')
return;
let service = art._service;
const service = art._service;
if (!service)
return;
let sname = service.name.absolute;
const sname = service.name.absolute;
art._ancestors.forEach( expose );

@@ -922,18 +988,29 @@ return;

return;
let desc = ancestor._descendants ||
const desc = ancestor._descendants ||
setLink( ancestor, Object.create(null), '_descendants' );
if (!desc[ sname ])
desc[ sname ] = [art];
if (!desc[sname])
desc[sname] = [ art ];
else
desc[ sname ].push( art );
desc[sname].push( art );
}
}
function tagCompositionTargets( elem ) {
const type = elem.type && elem.type && elem.type.path;
if (elem.target && type && type[0] && type[0].id === 'cds.Composition') {
// Compostion always via [{id:'cds.Composition'}] in both CSN and CDL
const name = resolveUncheckedPath( elem.target, 'target', elem );
if (name)
model.$compositionTargets[name] = true;
}
forEachGeneric( elem, 'elements', tagCompositionTargets );
}
// Collect all artifact extensions
function collectArtifactExtensions( construct, block ) {
for (let ext of construct.extensions || []) {
for (const ext of construct.extensions || []) {
setProp( ext, '_block', block );
// complain about non-existent path base, but do not check complete path:
// normal ref in outer extend, relative ref inside EXTEND CONTEXT
let name = resolveUncheckedPath( ext.name, ext.kind, ext );
const name = resolveUncheckedPath( ext.name, ext.kind, ext );
// TODO: somehow provide expectedKind as filter?

@@ -948,13 +1025,16 @@ if (name) {

function lateExtensions() {
for (let name in lateExtensionsDict) {
function lateExtensions( early ) {
for (const name in model.$lateExtensions) {
let art = model.definitions[name];
let exts = lateExtensionsDict[name];
const exts = model.$lateExtensions[name];
if (art) { // created texts entity
extendArtifact( exts, art, 'noExtend' );
if (exts) {
extendArtifact( exts, art, 'gen' );
model.$lateExtensions[name] = null; // done
}
}
else if (!options.lintMode) {
else if (!early) {
// If not lint-mode, complain about unused extensions, i.e. those
// which do not point to a valid artifact
for (let ext of exts) {
for (const ext of exts) {
delete ext.name.path[0]._artifact; // get message for root

@@ -966,8 +1046,8 @@ resolvePath( ext.name, ext.kind, ext ); // should issue error/info

// create "super" ANNOTATE containing all non-applied ones
let first = exts[0];
let location = first.name.location;
const first = exts[0];
const { location } = first.name;
art = {
kind: 'annotate',
name: { path: [ { id: name, location } ], absolute: name, location },
location: first.location
location: first.location,
};

@@ -977,3 +1057,3 @@ if (model.extensions)

else
model.extensions = [art];
model.extensions = [ art ];
extendArtifact( exts, art ); // also sets _artifact link in extensions

@@ -998,7 +1078,7 @@ }

while (extNames.length) {
let length = extNames.length;
for (let name of extNames) {
let art = model.definitions[name];
const { length } = extNames;
for (const name of extNames) {
const art = model.definitions[name];
if (!art) {
lateExtensionsDict[name] = extensionsDict[name];
model.$lateExtensions[name] = extensionsDict[name];
delete extensionsDict[name];

@@ -1025,6 +1105,6 @@ }

// (ext.expectedKind == art.kind) already checked by parser (TODO: modulo context/service)
if (!kindProperties[ art.kind ].artifacts) // no context or service
if (!kindProperties[art.kind].artifacts) // no context or service
return false;
for (let ext of extensionsDict[name]) {
for (const ext of extensionsDict[name]) {
setProp( ext.name, '_artifact', art );

@@ -1049,8 +1129,6 @@ if (ext.artifacts) { // extend context with artifacts

if (art.includes) { // service includes - just resolve them now
if (!options.tntFlavor)
message( null, dictLocation( art.includes ), art,
'Service includes are not supported yet' );
for (let ref of art.includes) {
message( null, dictLocation( art.includes ), art,
'Service includes are not supported yet' );
for (const ref of art.includes)
resolvePath( ref, 'context', art );
}
}

@@ -1062,15 +1140,38 @@ // let absolute = art.name.absolute;

// Extend artifact `art` by `extensions`. `noIncludes` can have values:
// - false: includes are applied, extend and annotate is performed
// - true: includes are not applied, extend and annotate is performed
// - 'gen': no includes and no extensions allowed, annotate is performed
function extendArtifact( extensions, art, noIncludes ) {
if (!noIncludes && art.includes) {
for (let ref of art.includes) {
let template = resolvePath( ref, 'include', art );
if (!noIncludes && !(canApplyIncludes( art ) && extensions.every( canApplyIncludes )))
return false;
if (!art.query) {
model._entities.push( art ); // add structure with includes in dep order
art.$entity = ++model.$entity;
}
if (!noIncludes && art.includes)
applyIncludes( art, art );
extendMembers( extensions, art, noIncludes === 'gen' );
if (!noIncludes && art.includes && setAutoExposed( art )) { // need to inherit @cds.autoexpose
for (const ref of art.includes) {
const template = ref._artifact;
if (template) {
setAutoExposed( template );
if (typeof template.$autoexpose === 'boolean')
art.$autoexpose = template.$autoexpose;
}
}
}
// TODO: complain about element extensions inside projection
return true;
}
function canApplyIncludes( art ) {
if (art.includes) {
for (const ref of art.includes) {
const template = resolvePath( ref, 'include', art );
if (template && template.name.absolute in extensionsDict)
return false;
}
model._entities.push( art ); // add structure with includes in dep order
includeMembers( art, 'elements', forEachInOrder );
includeMembers( art, 'actions', forEachGeneric );
}
extendMembers( extensions, art, noIncludes === 'noExtend' );
// TODO: complain about element extensions inside projection
return true;

@@ -1080,5 +1181,7 @@ }

function extendMembers( extensions, art, noExtend ) {
let elemExtensions = [];
const elemExtensions = [];
if (!art._deps)
setProp( art, '_deps', [] );
extensions.sort( compareLayer );
for (let ext of extensions) {
for (const ext of extensions) {
if (!('_artifact' in ext.name)) { // not already applied

@@ -1091,9 +1194,21 @@ setProp( ext.name, '_artifact', art );

}
if (ext.includes) {
// TODO: currently, re-compiling from gensrc does not give the exact
// element sequence - we need something like
// includes = ['Base1',3,'Base2']
// where 3 means adding the next 3 elements before applying include 'Base2'
if (art.includes)
art.includes.push(...ext.includes);
else
art.includes = [ ...ext.includes ];
applyIncludes( ext, art );
}
defineAnnotations( ext, art, ext._block, ext.kind );
// TODO: do we allow to add elements with array of {...}? If yes, adapt
initMembers( ext, art, ext._block ); // might set _extend, _annotate
art._deps.push( { art: ext } ); // art depends silently on ext (inverse to normal dep!)
}
for (let name in ext.elements) {
let elem = ext.elements[ name ];
if (elem.kind === 'element') {
for (const name in ext.elements) {
const elem = ext.elements[name];
if (elem.kind === 'element') { // i.e. not extend or annotate
elemExtensions.push( elem );

@@ -1104,2 +1219,4 @@ break;

if (ext.columns) // extend projection
extendColumns( ext, art );
}

@@ -1109,6 +1226,6 @@ if (elemExtensions.length > 1)

['elements', 'actions'].forEach( function ext(prop) {
let dict = art._extend && art._extend[prop];
for (let name in dict) {
let member = (art[prop] || prop === 'elements' && art.enum)[name];
[ 'elements', 'actions' ].forEach( (prop) => {
const dict = art._extend && art._extend[prop];
for (const name in dict) {
const member = (art[prop] || prop === 'elements' && art.enum)[name];
if (!member)

@@ -1122,2 +1239,29 @@ extendNothing( dict[name], prop, name, art );

// Copy columns for EXTEND PROJECTION
function extendColumns( ext, art ) {
// TODO: consider reportUnstableExtensions
const { location } = ext;
const { query } = art;
if (!query) {
if (art.kind !== 'annotate') {
message( 'extend-columns', location, ext, { art }, 'Error',
'Artifact $(ART) cannot be extended with columns, only projections can' );
}
return;
}
if (!query.from || query.from.length !== 1 || !query.from[0].path) {
message( 'extend-columns', location, ext, { art }, 'Error',
'Artifact $(ART) cannot be extended with columns, only projections can' );
}
else {
if (!query.columns)
query.columns = [ { location, val: '*' } ];
for (const column of ext.columns) {
setProp( column, '_block', ext._block );
query.columns.push(column);
}
}
}
function reportUnstableExtensions( extensions ) {

@@ -1129,4 +1273,4 @@ // Report 'Warning: Unstable element order due to repeated extensions'.

let open = []; // the "highest" layers
for (let ext of extensions) {
let extLayer = layer( ext ) || { realname: '', _layerExtends: Object.create(null) };
for (const ext of extensions) {
const extLayer = layer( ext ) || { realname: '', _layerExtends: Object.create(null) };
if (!open.length) {

@@ -1136,3 +1280,3 @@ lastExt = ext;

}
else if (extLayer.realname === open[ open.length-1 ]) { // in same layer
else if (extLayer.realname === open[open.length - 1]) { // in same layer
if (lastExt) {

@@ -1147,3 +1291,3 @@ message( 'extend-repeated-intralayer', lastExt.location, lastExt, {}, 'Warning',

else {
if (lastExt && (open.length > 1 || !extLayer._layerExtends[ open[0] ])) {
if (lastExt && (open.length > 1 || !extLayer._layerExtends[open[0]])) {
// report for lastExt if that is unrelated to other open exts or current ext

@@ -1154,3 +1298,3 @@ message( 'extend-unrelated-layer', lastExt.location, lastExt, {}, 'Warning',

lastExt = ext;
open = open.filter( name => !extLayer._layerExtends[ name ] );
open = open.filter( name => !extLayer._layerExtends[name] );
open.push( extLayer.realname );

@@ -1162,9 +1306,9 @@ }

function extendNothing( extensions, prop, name, art ) {
for (let ext of extensions) {
for (const ext of extensions) {
message( 'extend-undefined', ext.name.location, ext,
{ art: searchName( art, name, dictKinds[prop] ) },
'Error', {
std: 'Unknown $(ART) - nothing to extend',
std: 'Unknown $(ART) - nothing to extend',
element: 'Artifact $(ART) has no element or enum $(MEMBER) - nothing to extend',
action: 'Artifact $(ART) has no action $(MEMBER) - nothing to extend',
action: 'Artifact $(ART) has no action $(MEMBER) - nothing to extend',
} );

@@ -1174,19 +1318,32 @@ }

function includeMembers( art, prop, forEach ) {
function applyIncludes( ext, art ) {
if (!art._ancestors)
setProp( art, '_ancestors', [] ); // recursive array of includes
for (const ref of ext.includes) {
const template = ref._artifact; // already resolved
if (template) {
if (template._ancestors)
art._ancestors.push( ...template._ancestors );
art._ancestors.push( template );
}
}
includeMembers( ext, 'elements', forEachInOrder, ext === art && art );
includeMembers( ext, 'actions', forEachGeneric, ext === art && art );
}
function includeMembers( ext, prop, forEach, parent ) {
// TODO two kind of messages:
// Error 'More than one include defines element "A"' (at include ref)
// Warning 'Overwrites definition from include "I" (at elem def)
let members = art[prop];
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 );
const members = ext[prop];
clearDict( ext, prop ); // TODO: do not set actions property if there are none
for (const ref of ext.includes) {
const template = ref._artifact; // already resolved
if (template) { // be robust
if (template._ancestors)
art._ancestors.push( ...template._ancestors );
art._ancestors.push( template );
forEach( template, prop, function( origin, name ) {
forEach( template, prop, ( origin, name ) => {
if (members && name in members)
return; // TODO: warning for overwritten element
let elem = linkToOrigin( origin, name, art, prop, weakLocation( ref.location ) );
const elem = linkToOrigin( origin, name, parent, prop, weakLocation( ref.location ) );
if (!parent) // not yet set for EXTEND foo WITH bar
addToDictWithIndexNo( ext, prop, name, elem );
elem.$inferred = 'include';

@@ -1203,4 +1360,4 @@ if (origin.masked)

if (members) {
forEach( { [prop]: members }, prop, function( elem, name ) {
addToDictWithIndexNo( art, prop, name, elem );
forEach( { [prop]: members }, prop, ( elem, name ) => {
addToDictWithIndexNo( ext, prop, name, elem );
});

@@ -1214,3 +1371,3 @@ }

return;
if (art.kind === 'entity' && art.elements && // check potential entity parse error
if (art.kind === 'entity' && !art.query && art.elements && // check potential entity parse error
(!art.abstract || !art.abstract.val))

@@ -1221,12 +1378,10 @@ processLocalizedData( art );

function processLocalizedData( art ) {
if (!options.betaMode)
return;
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 );
const textsName = `${ art.name.absolute }_texts`;
const viewName = `localized.${ art.name.absolute }`;
const localized = localizedData( art, textsName, viewName );
// store localized entities (needed by localized convenience view creation later)
art.$localizedElements = (localized || []).map(elem => ({ elem, assoc: 'default' }));
if (localized) {
createTextsEntity( art, textsName, localized );
createLocalizedDataView( art, viewName, localized );
addTextsAssociations( art, textsName, localized );

@@ -1238,10 +1393,10 @@ }

let keys = 0;
let textElems = [];
let protectedElems = [];
const textElems = [];
const protectedElems = [];
for (let name in art.elements) {
let elem = art.elements[name];
for (const name in art.elements) {
const elem = art.elements[name];
if (elem instanceof Array)
return false; // no localized-data unfold with redefined elems
if (['locale','texts','localized'].includes( name ))
if ([ 'locale', 'texts', 'localized' ].includes( name ))
protectedElems.push( elem );

@@ -1253,4 +1408,5 @@

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

@@ -1264,20 +1420,22 @@ if (textElems.length <= keys)

}
for (let elem of protectedElems) {
for (const elem of protectedElems) {
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 ];
let viewEntity = model.definitions[ viewName ];
let names = [];
const textsEntity = model.definitions[textsName];
const viewEntity = model.definitions[viewName];
const names = [];
if (textsEntity) {
if (!(textsEntity instanceof Array))
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))
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 );

@@ -1290,3 +1448,3 @@ }

std: 'Names $(NAMES) for localized data entities used by other definitions',
one: 'Name $(NAMES) for localized data entity used by other definition'
one: 'Name $(NAMES) for localized data entity used by other definition',
} );

@@ -1298,5 +1456,5 @@ return false;

function createTextsEntity( base, absolute, textElems ) {
let elements = Object.create(null);
let location = base.name.location;
let art = {
const elements = Object.create(null);
const { location } = base.name;
const art = {
kind: 'entity',

@@ -1307,5 +1465,6 @@ name: { path: splitIntoPath( location, absolute ), location },

'@cds.autoexpose': { name: augmentPath( location, '@cds.autoexpose' ), location },
$inferred: 'localized'
}
let locale = {
$inferred: 'localized',
};
addBoolAnnotationTo('@odata.draft.enabled', false, art);
const locale = {
name: { location, id: 'locale' },

@@ -1316,15 +1475,16 @@ kind: 'element',

length: { literal: 'number', val: 5, location },
location
location,
};
addToDictWithIndexNo( art, 'elements', 'locale', locale );
let artifacts = Object.create(null);
artifacts[ absolute ] = art;
const 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)
for (const orig of textElems) {
const elem = linkToOrigin( orig, orig.name.id, art, 'elements' );
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;
const localized = orig.localized || orig.type || orig.name;
elem.localized = { val: false, $inferred: 'localized', location: localized.location };

@@ -1335,43 +1495,7 @@ }

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;
let texts = {
// texts : Composition of many Books_texts on texts.ID=ID;
const keys = textElems.filter( e => e.key && e.key.val );
const { location } = art.name;
const texts = {
name: { location, id: 'texts' },

@@ -1384,10 +1508,10 @@ kind: 'element',

target: augmentPath( location, textsName ),
onCond: augmentEqual( location, 'texts', keys )
}
onCond: augmentEqual( location, 'texts', keys ),
};
setMemberParent( texts, 'texts', art, 'elements' );
setProp( texts, '_block', model.$internal );
// localized : Association to Books_txts on
// localized : Association to Books_texts on
// localized.ID=ID and localized.locale = $user.locale;
keys.push( ['localized.locale', '$user.locale'] );
let localized = {
keys.push( [ 'localized.locale', '$user.locale' ] );
const localized = {
name: { location, id: 'localized' },

@@ -1397,6 +1521,7 @@ kind: 'element',

$inferred: 'localized',
$localized: { val: 'assoc' },
type: augmentPath( location, 'cds.Association' ),
target: augmentPath( location, textsName ),
onCond: augmentEqual( location, 'localized', keys )
}
onCond: augmentEqual( location, 'localized', keys ),
};
setMemberParent( localized, 'localized', art, 'elements' );

@@ -1406,12 +1531,22 @@ setProp( localized, '_block', model.$internal );

// Returns the value of the property 'prop' for a given artifact or one of its origins.
// Returns false if none of them have the corresponding property.
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
const artWithProp = getOriginRecursive(art, artToCheck => artToCheck[prop]);
if (!artWithProp)
return null;
const { val } = artWithProp[prop];
// TODO XSN: for anno short form, use { val: true, location, <no literal prop> }
return val === undefined || val;
}
// Descends from artifact 'art' to its origins and returns the first one for which the
// function 'cond' evaluates to true. Returns false if no such artifact is found.
function getOriginRecursive(art, cond) {
const 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;
if (cond(art))
return art;
processed[name] = art;

@@ -1422,8 +1557,10 @@ if (art.origin && art.origin._artifact) {

}
else if (art.type && art._block) { // TODO: not TYPE OF
else if (art.type && art._block && art.type.scope !== 'typeOf') {
// TODO: also do something special for TYPE OF inside `art`s own elements
name = resolveUncheckedPath( art.type, 'type', art );
art = name && model.definitions[ name ];
art = name && model.definitions[name];
}
else
else {
return false;
}
}

@@ -1436,3 +1573,3 @@ return false;

// locations):
function pathName (path) {
function pathName(path) {
return path.map( id => id.id ).join('.');

@@ -1444,4 +1581,4 @@ }

// TODO: make it also work with path = [{id:absolute}]
let colons = name.indexOf('::');
let items = (colons < 0) ? name.split('.') : name.substring(colons).split('.');
const colons = name.indexOf('::');
const items = (colons < 0) ? name.split('.') : name.substring(colons).split('.');
return items.map( id => ({ id, location }) );

@@ -1455,3 +1592,3 @@ }

function augmentEqual( location, prefix, relations ) {
let args = relations.map( eq );
const args = relations.map( eq );
return (args.length === 1)

@@ -1464,12 +1601,12 @@ ? args[0]

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
};
}
const { id } = refs.name;
return {
op: { val: '=', location },
args: [
{ path: [ { id: prefix, location }, { id, location } ], location },
{ path: [ { id, location } ], location },
],
location,
};
}

@@ -1481,2 +1618,7 @@ function ref( path ) {

module.exports = define;
module.exports = {
define: model => getDefinerFunctions( model ).define(),
getDefinerFunctions,
augmentPath,
splitIntoPath,
};

@@ -9,8 +9,8 @@ // Module handling, layers and packages

// set dependencies
for (let name in sources) {
let ast = sources[name];
for (const name in sources) {
const ast = sources[name];
ast.realname = name;
setProp( ast, '_deps', [] );
for (let d of ast.dependencies || []) {
let art = sources[ d.realname ];
for (const d of ast.dependencies || []) {
const art = sources[d.realname];
if (art)

@@ -34,10 +34,10 @@ ast._deps.push( { art } );

for (let dep of node._deps) {
for (const dep of node._deps) {
if (dep.art._scc.lowlink !== node._scc.lowlink) { // not in same SCC
let depRepr = dep.art._layerRepresentative;
sccDeps[ depRepr.realname ] = depRepr;
const depRepr = dep.art._layerRepresentative;
sccDeps[depRepr.realname] = depRepr;
}
}
if (node === representative) {
let exts = Object.keys( sccDeps ).map( name => sccDeps[name]._layerExtends );
const exts = Object.keys( sccDeps ).map( name => sccDeps[name]._layerExtends );
Object.assign( sccDeps, ...exts );

@@ -66,3 +66,3 @@ setProp( representative, '_layerExtends', sccDeps );

// Like `obj.prop = value`, but not contained in JSON / CSN
function setProp ( obj, prop, value ) {
function setProp( obj, prop, value ) {
Object.defineProperty( obj, prop, { value, configurable: true, writable: true } );

@@ -76,2 +76,2 @@ return value;

compareLayer,
}
};

@@ -7,6 +7,7 @@ //

= require( '../base/model');
var { linkToOrigin, withAssociation } = require('./shared');
const { linkToOrigin, withAssociation } = require('./shared');
// const { refString } = require( '../base/messages')
function propagate( model, options = model.options || {} ) {
function propagate( model ) {
const { options } = model;
const props = {

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

'@Analytics.visible': never,
'@cds.autoexposed': never, // in case people set it themselves
'@': withKind, // always except in 'returns' and 'items'

@@ -32,2 +34,3 @@ default: withKind, // always except in 'returns' and 'items'

scale: always,
srid: always,
localized: always,

@@ -37,3 +40,5 @@ target: always,

onCond: always, // is expensive, but often rewritten - TODO: on
on: ( prop, target, source ) => { target[prop] = source[prop]; }, // TODO: get rid of this soon!
on: ( prop, target, source ) => {
target[prop] = source[prop];
}, // TODO: get rid of this soon!
foreignKeys: expensive, // actually always, but dictionary copy

@@ -59,3 +64,3 @@ items,

// console.log('RUN:',refString(art), art.elements ? Object.keys(art.elements) : 0)
let chain = [];
const chain = [];
let target = art;

@@ -72,3 +77,3 @@ let source = getOrigin( target );

else if (target._main) { // source is element, which has not inherited props yet
run( target._main ) // run on main artifact first
run( target._main ); // run on main artifact first
}

@@ -78,6 +83,6 @@ else if (target.includes) {

while (targets.length) {
let news = [];
for (let t of targets) {
for (let ref of t.includes || []) {
let s = ref._artifact;
const news = [];
for (const t of targets) {
for (const ref of t.includes || []) {
const s = ref._artifact;
if (!s) // ref error

@@ -113,8 +118,8 @@ continue;

// console.log('PROPS:',source&&refString(source),'->',refString(target))
let viaType = target.type && !target.type.$inferred;
let keys = Object.keys( source );
for (let prop of keys) {
const viaType = target.type && !target.type.$inferred;
const keys = Object.keys( source );
for (const prop of keys) {
if (prop in target) // TODO: warning with competing props from multi-includes
continue;
let transformer = props[prop] || props[prop.charAt(0)];
const transformer = props[prop] || props[prop.charAt(0)];
if (transformer)

@@ -136,6 +141,6 @@ transformer( prop, target, source, viaType );

}
//setProp( target, '_status', 'shallow-propagated' );
// setProp( target, '_status', 'shallow-propagated' );
}
function never () {}
function never() {}

@@ -151,4 +156,4 @@ function always( prop, target, source ) {

return false;
let ref = target.type || source.type;
let type = ref && ref._artifact;
const ref = target.type || source.type;
const type = ref && ref._artifact;
return type && !type._main && type[prop];

@@ -169,11 +174,10 @@ }

return;
let location = target.type && !target.type.$inferred && target.type.location
|| target.location
|| target._outer && target._outer.location;
let dict = source[prop];
for (let name in dict) {
let member = linkToOrigin( dict[name], name, target, prop, location );
const location = target.type && !target.type.$inferred && target.type.location ||
target.location ||
target._outer && target._outer.location;
const dict = source[prop];
for (const name in dict) {
const member = linkToOrigin( dict[name], name, target, prop, location );
member.$inferred = 'proxy';
if ('_finalType' in dict[name])
setProp( member, '_finalType', dict[name]._finalType);
setFinalType(member, dict[name]);
}

@@ -205,4 +209,5 @@ }

if (ok || target.$inferred === 'proxy' || target.$inferred === 'include' ) {
let origin = {};
const origin = {};
target[prop] = { $inferred: 'proxy', origin };
setFinalType( target[prop], source[prop] );
setProp( origin, '_artifact', source[prop] );

@@ -216,3 +221,3 @@ setProp( target[prop], '_outer', target._outer || target ); // for setMemberParent

// - array of Entity, array of String(3), array of DerivedScalar
let line = availableAtType( prop, target, source );
const line = availableAtType( prop, target, source );
if (!line ||

@@ -232,12 +237,11 @@ line.type && line.type._artifact && line.type._artifact.kind === 'entity' ||

if (art.$from && art.$from.length) { // query
let tabref = art.$from[0]._artifact;
const tabref = art.$from[0]._artifact;
return (tabref && tabref.kind === 'element')
? tabref._finalType && tabref._finalType.target
? tabref._finalType && tabref._finalType.target && tabref._finalType.target._artifact
: tabref;
}
else {
return (art.type && !art.type.$inferred)
? art.type._artifact
: art.origin && art.origin._artifact;
}
return (art.type && !art.type.$inferred)
? art.type._artifact
: art.origin && art.origin._artifact;
}

@@ -252,4 +256,9 @@

function setFinalType( target, source ) {
if ('_finalType' in source)
setProp( target, '_finalType', source._finalType);
}
module.exports = {
propagate,
};

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

params: 'param',
}
};

@@ -29,3 +29,3 @@ const kindProperties = {

$navElement: { normalized: 'element', $navigation: true },
type: { elements: propExists, enum: propExists },
type: { elements: propExists, enum: propExists },
annotation: { elements: propExists, enum: propExists },

@@ -35,4 +35,8 @@ const: {},

element: { elements: propExists, enum: propExists, dict: 'elements' },
action: { params: () => false, elements: () => false, enum: () => false, dict: 'actions' }, // no extend params, only annotate
function: { params: () => false, elements: () => false, enum: () => false, normalized: 'action' }, // no extend params, only annotate
action: {
params: () => false, elements: () => false, enum: () => false, dict: 'actions',
}, // no extend params, only annotate
function: {
params: () => false, elements: () => false, enum: () => false, normalized: 'action',
}, // no extend params, only annotate
key: { normalized: 'element' },

@@ -43,10 +47,12 @@ param: { elements: () => false, enum: () => false, dict: 'params' },

using: {},
extend: { isExtension: true },
annotate: { isExtension: true, elements: true, enum: true, actions: true, params: true },
extend: { isExtension: true, noDep: 'special' },
annotate: {
isExtension: true, noDep: 'special', elements: true, enum: true, actions: true, params: true,
},
builtin: {}, // = CURRENT_DATE, TODO: improve
$parameters: {}, // $parameters in query entitis
}
};
function propExists( prop, parent ) {
let obj = parent.returns || parent;
const obj = parent.returns || parent;
return (obj.items || obj)[prop];

@@ -61,26 +67,42 @@ }

// the argument.
function fns( model, environment = (o => o.artifacts || Object.create(null) ) ) {
var options = model.options || {};
function artifactsEnv( art ) {
return art.artifacts || Object.create(null);
}
// TODO: delete envFn again and specify some reject for extend/annotate ?
function fns( model, environment = artifactsEnv ) {
const options = model.options || {};
const message = getMessageFunction( model );
const specExpected = {
annotation: { useDefinitions: true, noMessage: true },
extend: { useDefinitions: true }, // ref in top-level EXTEND
annotate: { useDefinitions: true, undefinedDef: 'anno-undefined-def', undefinedArt: 'anno-undefined-art' },
extend: { useDefinitions: true, envFn: artifactsEnv }, // ref in top-level EXTEND
annotate: {
useDefinitions: true, envFn: artifactsEnv, undefinedDef: 'anno-undefined-def', undefinedArt: 'anno-undefined-art',
},
type: { reject: rejectNonType }, // TODO: more detailed later (e.g. for enum base type?)
typeOf: { next: '_$next' },
typeOf: { next: '_$next', assoc: false }, // warn
include: { reject: rejectNonStruct },
context: { reject: rejectNonContext },
target: { reject: rejectNonEntity, noDep: true }, // TODO: dep for (explicit+implicit!) foreign keys
element: { next: '__none_' },
target: { reject: rejectNonEntity, noDep: true },
// TODO: dep for (explicit+implicit!) foreign keys
element: { next: '__none_' }, // foreign key element
filter: { next: '_$next', lexical: 'main' },
from: { reject: rejectNonSource },
from: { reject: rejectNonSource, assoc: 'from' },
const: { next: '_$next', reject: rejectNonConst },
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: {
next: '_$next', escape: 'param', noDep: true, assoc: 'nav',
},
unmanaged: { next: '_$next', escape: 'param', noDep: true }, // TODO: special assoc for only on user
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
}
};

@@ -91,10 +113,11 @@ return {

defineAnnotations,
setAutoExposed,
};
function rejectNonConst( art ) {
return ['builtin','param','const'].includes( art.kind ) ? undefined : 'expected-const';
return [ 'builtin', 'param', 'const' ].includes( art.kind ) ? undefined : 'expected-const';
}
function rejectNonStruct( art ) {
return (['type', 'entity'].includes( art.kind ) && art.elements && !art.query && !art.params)
return ([ 'type', 'entity' ].includes( art.kind ) && art.elements && !art.query && !art.params)
? undefined

@@ -105,12 +128,12 @@ : 'expected-struct';

function rejectNonContext( art ) {
return (['context', 'service'].includes( art.kind ))
return ([ 'context', 'service' ].includes( art.kind ))
? undefined
: 'expected-context'
: 'expected-context';
}
function rejectNonType( art ) {
return (['type', 'entity', 'view'].includes( art.kind ) ||
return ([ 'type', 'entity', 'view' ].includes( art.kind ) ||
// art.kind === 'type' || // too strong for positive/BoundFunctions
// art._main && art._main.kind === 'type') // currently too strong
art._main && ['type', 'entity', 'view'].includes( art._main.kind ))
art._main && [ 'type', 'entity', 'view' ].includes( art._main.kind ))
? undefined

@@ -121,3 +144,3 @@ : 'expected-type';

function rejectNonEntity( art ) {
return (['view', 'entity'].includes( art.kind ))
return ([ 'view', 'entity' ].includes( art.kind ))
? undefined

@@ -128,6 +151,6 @@ : 'expected-entity';

function rejectNonSource( art, path ) {
if (['view', 'entity'].includes( art.kind ))
if ([ 'view', 'entity' ].includes( art.kind ))
return undefined;
let main = [...path].reverse().find( item => !item._artifact._main );
if (!['view', 'entity'].includes( main._artifact.kind ))
const main = [ ...path ].reverse().find( item => !item._artifact._main );
if (![ 'view', 'entity' ].includes( main._artifact.kind ))
return 'expected-source'; // orig: 'A source must start at an entity, projection or view';

@@ -149,6 +172,6 @@ environment( art ); // sets _finalType on art

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

@@ -160,5 +183,4 @@ art = ref.path[0]._artifact[0]; // array stored in head's _artifact

if (ref.path.length > 1)
return art.name.absolute + '.' + pathName( ref.path.slice(1) );
else
return art.name.absolute;
return `${ art.name.absolute }.${ pathName( ref.path.slice(1) ) }`;
return art.name.absolute;
}

@@ -175,42 +197,50 @@

return ref._artifact;
if (!ref.path || ref.path.broken || !ref.path.length) // incomplete type AST or empty env (already reported)
return undefined;
if (!ref.path || ref.path.broken || !ref.path.length) {
// incomplete type AST or empty env (already reported)
return setLink( ref, undefined );
}
setLink( ref, 0 ); // avoid cycles for type T: association to T.m;
let spec = specExpected[expected];
let path = ref.path;
let head = path[0];
//message(null,head.location,{art:user,expected, id: head.id},'Info','User $(ART), $(EXPECTED) $(ID)')
let env = user._block; // artifact references: block
const { path } = ref;
const head = path[0];
// message(null,head.location,{art:user,expected, id: head.id},
// 'Info','User $(ART), $(EXPECTED) $(ID)')
let env = user._block; // artifact references: block
if (ref.scope === 'param') {
if (!spec.escape) {
let variant = (env.$frontend && env.$frontend !== 'cdl') ? 'std' : 'cdl';
const variant = (env.$frontend && env.$frontend !== 'cdl') ? 'std' : 'cdl';
message( 'ref-unexpected-scope', head.location, user, { name: head.id, '#': variant },
'Error', {
std: 'Unexpected parameter scope for name $(NAME)',
cdl: 'Unexpected `:` before name $(NAME)'
cdl: 'Unexpected `:` before name $(NAME)',
} );
return setLink( ref, null );
}
spec = specExpected[ spec.escape ];
spec = specExpected[spec.escape];
// In queries and query entities, the first lexical search environment
// are the parameters, otherwise the block. It is currently ensured that
// _block in queries is the same as _block of the query entity:
let lexical = (user._main||user).$tableAliases; // queries (but also query entities)
const lexical = (user._main || user).$tableAliases; // queries (but also query entities)
env = lexical && lexical.$parameters || user._block;
extDict = null; // let getPathRoot() choose it
}
else if (spec.next === '__none_')
else if (spec.next === '__none_') {
env = {};
}
else if (spec.next) { // TODO: combine spec.next / spec.lexical to spec.lexical
let query = (spec.lexical === 'main')
? user._main // in path filter, just $magic (and $parameters)
: (user.kind === 'query')
? user
: user._parent && user._parent.kind === 'query' && user._parent;
// TODO: SIMPLIFY this function
// eslint-disable-next-line no-nested-ternary
const query = (spec.lexical === 'main')
? user._main // in path filter, just $magic (and $parameters)
: (user.kind === 'query')
? user
: user._parent && user._parent.kind === 'query' && user._parent;
env = (spec.lexical === 'from') ? query._parent : query || user._main || user;
// queries: first tabaliases, then $magic - value refs: first $self, then $magic
if (!extDict && !spec.noExt)
if (!extDict && !spec.noExt) {
extDict = query && query.$combined ||
environment( user._main ? user._parent : user );
}
}

@@ -220,8 +250,9 @@

let art = (ref.scope === 'global' || spec.global)
? getPathRoot( path, spec, user, {}, model.definitions )
: getPathRoot( path, spec, user, env, extDict, msgArt || 0 );
if (!art)
? getPathRoot( path, spec, user, {}, model.definitions )
: getPathRoot( path, spec, user, env, extDict, msgArt || 0 );
if (!art) {
return setLink( ref, art );
}
else if (art.kind === 'using') {
art = model.definitions[ art.name.absolute ];
art = model.definitions[art.name.absolute];
if (!art)

@@ -234,12 +265,19 @@ return setLink( ref, art );

else if (art.name.$mixin) { // TODO: art.kind === 'mixin'
// console.log(message( null, art.location, art, {}, 'Info','MIX').toString())
setLink( head, art, '_navigation' );
}
else if (art.kind === '$navElement') {
// console.log(message( null, art.location, art, {}, 'Info','NE').toString())
setLink( head, art, '_navigation' );
setLink( head, art.origin._artifact );
setXref( head._artifact, user, head );
}
else if (art.kind === '$tableAlias') {
setLink( head, art, '_navigation' );
// console.log( message( null, art.location, art,
// {type: art.type, target: art._finalType && art._finalType.target},
// 'Info','NAV').toString())
// TODO: probable better set the _artifact link of FROM.assoc to target!
if (art.type) { // FROM reference
let assoc = art._finalType && art._finalType.target;
const assoc = art._finalType && art._finalType.target;
art = setLink( head, assoc ? assoc._artifact : art.type._artifact );

@@ -259,5 +297,5 @@ if (!art)

if (art.$autoElement) {
let location = path[ path.length-1 ].location;
let step = { id: art.$autoElement, $inferred: '$autoElement', location };
art = art.elements[ step.id ];
const { location } = path[path.length - 1];
const step = { id: art.$autoElement, $inferred: '$autoElement', location };
art = art.elements[step.id];
setLink( step, art );

@@ -267,3 +305,3 @@ path.push( step );

if (spec.reject) {
let msg = spec.reject( art, path );
const msg = spec.reject( art, path );
if (msg) {

@@ -275,9 +313,16 @@ signalNotFound( msg, ref.location, user );

if (user && !spec.noDep) {
let location = ref.location; // || combinedLocation( head, path[tail.length] );
const { location } = ref; // || combinedLocation( head, path[tail.length] );
// TODO: location of last path item if not main artifact
if (!user._deps)
setProp( user, '_deps', [] );
user._deps.push( { art, location } );
if (!art._main || spec.assoc !== 'from') {
user._deps.push( { art, location } );
}
else {
user._deps.push( { art: art._main, location } );
environment( art, location, user );
// Without on-demand resolve, we can simply signal 'undefined "x"'
// instead of 'illegal cycle' in the following case:
// element elem: type of elem.x;
}
}

@@ -287,2 +332,15 @@ return setLink( ref, art );

// Set a cross-reference from the 'user' in artifact 'art'
// 'user' is a navigatable node, while 'where' gives a closer hint (e.g. an item in a path)
// For example in 'a.b.c as d' the definition of b gets a xref object with user d and where = b
function setXref(art, user, where = user) {
if (!user)
return;
if (!art._xref)
setProp( art, '_xref', [] );
// if (!art._xref.includes(user))
// art._xref.push(user);
art._xref.push( { user, where } );
}
function transformMagicName( name ) {

@@ -298,21 +356,21 @@ // TODO: store magic variable in lower case (nicer for code completion)

function getPathRoot( path, spec, user, env, extDict, msgArt ) {
let head = path[0];
if (!head || !head.id)
const head = path[0];
if (!head || !head.id || !env)
return undefined; // parse error
if ('_artifact' in head)
return (head._artifact instanceof Array) ? false : head._artifact;
// console.log(pathName(path), !spec.next && !extDict && (spec.useDefinitions || env.$frontend === 'json' || env))
// console.log(pathName(path), !spec.next && !extDict &&
// (spec.useDefinitions || env.$frontend === 'json' || env))
if (!spec.next && !extDict) {
extDict = (spec.useDefinitions || env.$frontend && env.$frontend !== 'cdl')
? model.definitions
: model.$builtins;
? model.definitions
: model.$builtins;
}
// TODO: remove resolveSemanics, just have typeOf for type refs
let nextProp = spec.next || '_block';
const nextProp = spec.next || '_block';
for (let art = env; art; art = art[nextProp]) {
let e = art.artifacts || art.$tableAliases || Object.create(null);
let r = (art.kind !== '$magicVariables')
? e[ head.id ]
// do not find magic variables if quoted:
: (!head.quoted) && e[ transformMagicName( head.id ) ];
const e = art.artifacts || art.$tableAliases || Object.create(null);
const r = (art.kind !== '$magicVariables')
? e[head.id]
// do not find magic variables if quoted:
: (!head.quoted) && e[transformMagicName( head.id )];
if (r) {

@@ -323,9 +381,10 @@ if (r instanceof Array) { // redefinitions

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

@@ -336,17 +395,19 @@ return setLink( head, r );

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

@@ -356,4 +417,5 @@ setLink( head, r );

}
else if (r)
else if (r) {
return setLink( head, r );
}
}

@@ -363,14 +425,14 @@ if (spec.noMessage || msgArt === true && extDict === model.definitions)

let valid = [];
const valid = [];
for (let art = env; art; art = art[nextProp]) {
let e = art.artifacts || art.$tableAliases || Object.create(null);
const e = art.artifacts || art.$tableAliases || Object.create(null);
valid.push( e );
}
if (extDict) {
let e = Object.create(null);
const e = Object.create(null);
// the names of the external dictionary are valid, too, except duplicate
// navigation elements (for which you should use a table alias)
if (extDict !== model.definitions) {
for (let name in extDict) {
let def = extDict[name];
for (const name in extDict) {
const def = extDict[name];
if (!(def instanceof Array && def[0].kind === '$navElement'))

@@ -381,3 +443,3 @@ e[name] = def;

else {
for (let name in extDict) {
for (const name in extDict) {
if (!name.includes('.'))

@@ -393,16 +455,20 @@ e[name] = extDict[name];

// TODO: also something special if it starts with '$'
if (msgArt)
if (msgArt) {
signalNotFound( 'ref-undefined-element', head.location, user, valid,
{ art: searchName( msgArt, head.id, 'element' ) } );
else
}
else {
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' || spec.global)
// IDE can inspect <model>.definitions - provide null for valid
else if (env.$frontend && env.$frontend !== 'cdl' || spec.global) {
// IDE can inspect <model>.definitions - provide null for valid
signalNotFound( spec.undefinedDef || 'ref-undefined-def', head.location, user, valid,
{ art: head.id } );
else
}
else {
signalNotFound( spec.undefinedArt || 'ref-undefined-art', head.location, user, valid,
{ name: head.id } );
}
return setLink( head, null );

@@ -416,4 +482,5 @@ }

function getPathItem( path, spec, user ) {
var art;
for (let item of path) {
let art;
const assoc = spec.assoc && (spec.assoc == null || user);
for (const item of path) {
if (!item || !item.id) // incomplete AST due to parse error

@@ -427,9 +494,10 @@ return undefined;

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

@@ -443,15 +511,16 @@ }

signalNotFound( spec.undefinedDef || 'ref-undefined-def', item.location, user,
[env], { art: searchName( art, item.id ) } );
[ env ], { art: searchName( art, item.id, spec.envFn && 'absolute' ) } );
}
else if (art.name.query != null) {
else if (art.name.query) {
// TODO: 'The current query has no element $(MEMBER)' with $self.MEMBER
// and 'The sub query for alias $(ALIAS) has no element $(MEMBER)'
// TODO: probably not extra messageId, but text variant
// TODO: views elements are proxies to query-0 elements, not the same
signalNotFound( 'query-undefined-element', item.location, user,
[env], { id: item.id }, 'Error',
[ env ], { id: item.id }, 'Error',
'Element $(ID) has not been found in the elements of the query' );
// TODO: 'The current query has no element $(MEMBER)' with name.self
// and 'The sub query $(NAME) has no element $(MEMBER)'
}
else if (art.kind === '$parameters') {
signalNotFound( 'ref-undefined-param', item.location, user,
[env], { art: searchName( art._main, item.id, 'param' ) },
[ env ], { art: searchName( art._main, item.id, 'param' ) },
'Error', { param: 'Entity $(ART) has no parameter $(MEMBER)' } );

@@ -461,3 +530,3 @@ }

signalNotFound( 'ref-undefined-element', item.location, user,
[env], { art: searchName( art, item.id ) } );
[ env ], { art: searchName( art, item.id ) } );
}

@@ -473,10 +542,11 @@ return null;

location.$notFound = true;
let err = message( msgId, location, home, ...args );
const err = message( msgId, location, home, ...args );
// console.log( Object.keys( Object.assign( Object.create(null), ...valid.reverse() ) ) )
if (valid && (options.attachValidNames || options.testMode))
err.validNames = Object.assign( Object.create(null), ...valid.reverse() );
// TODO: remove internal, i.e. cds.Association
if (options.testMode && valid) {
let names = Object.keys( err.validNames );
const names = Object.keys( err.validNames );
message( null, location, null,
names.length ? 'Valid: ' + names.sort().join(', ') : 'No valid names',
names.length ? `Valid: ${ names.sort().join(', ') }` : 'No valid names',
'Info' );

@@ -498,8 +568,8 @@ }

return;
for (let annoProp in construct) {
for (const annoProp in construct) {
if (annoProp.charAt(0) === '@') {
let annos = construct[annoProp];
if (!(annos instanceof Array))
annos = [annos];
for (let a of annos) {
annos = [ annos ];
for (const a of annos) {
setProp( a, '_block', block );

@@ -512,8 +582,8 @@ addToDict( art, annoProp, a );

}
for (let anno of construct.annotationAssignments) {
let ref = anno.name;
let name = resolveUncheckedPath( ref, 'annotation', { _block: block } );
let annoProp = (anno.name.variant)
? '@' + name + '#' + anno.name.variant.id
: '@' + name;
for (const anno of construct.annotationAssignments) {
const ref = anno.name;
const name = resolveUncheckedPath( ref, 'annotation', { _block: block } );
const annoProp = (anno.name.variant)
? `@${ name }#${ anno.name.variant.id }`
: `@${ name }`;
flatten( ref.path, annoProp, anno.value || {}, anno.name.variant, anno.name.location );

@@ -529,20 +599,26 @@ }

if (value.literal === 'struct') {
for (let item of value._struct || []) {
for (const item of value._struct || []) {
let prop = pathName(item.name.path);
if (item.name.variant) {
if (iHaveVariant)
if (iHaveVariant) {
message( 'anno-duplicate-variant', item.name.variant.location, construct, {}, // TODO: params
'Error', 'Annotation variant has been already provided' );
prop = prop + '#' + item.name.variant.id; // TODO: check for double variants
}
prop = `${ prop }#${ item.name.variant.id }`; // TODO: check for double variants
}
flatten( [...path, ...item.name.path], annoProp + '.' + prop, item, iHaveVariant || item.name.variant);
flatten( [ ...path, ...item.name.path ], `${ annoProp }.${ prop }`, item, iHaveVariant || item.name.variant);
}
for (let prop in value.struct) {
let item = value.struct[prop];
flatten( [...path, item.name], annoProp + '.' + prop, item, iHaveVariant );
for (const prop in value.struct) {
const item = value.struct[prop];
flatten( [ ...path, item.name ], `${ annoProp }.${ prop }`, item, iHaveVariant );
}
return;
}
let anno = Object.assign( {}, value ); // shallow copy
anno.name = { path, location: location || value.name && value.name.location || value.path && value.path.location };
const anno = Object.assign( {}, value ); // shallow copy
anno.name = {
path,
location: location ||
value.name && value.name.location ||
value.path && value.path.location,
};
if (priority)

@@ -561,2 +637,18 @@ anno.priority = priority;

}
function setAutoExposed( art, setForComposition ) {
if (art.$autoexpose !== undefined)
return false;
const anno = art['@cds.autoexpose'];
if (anno && anno.val !== null) { // XSN TODO: set val, but no location for anno short form
// @cds.autoexpose:true or @cds.autoexpose:false
art.$autoexpose = anno.val === undefined || !!anno.val;
return false;
}
else if (setForComposition) {
art.$autoexpose = model.$compositionTargets[art.name.absolute] ? 'Composition' : null;
}
// no @cds.autoexpose or @cds.autoexpose:null
return true;
}
}

@@ -570,3 +662,3 @@

start: start.location.start,
end: end.location.end
end: end.location.end,
};

@@ -577,3 +669,3 @@ }

// locations):
function pathName (path) {
function pathName(path) {
return (path.broken) ? '' : path.map( id => id.id ).join('.');

@@ -583,3 +675,3 @@ }

// Like `obj.prop = value`, but not contained in JSON / CSN
function setProp ( obj, prop, value ) {
function setProp( obj, prop, value ) {
Object.defineProperty( obj, prop, { value, configurable: true, writable: true } );

@@ -595,3 +687,3 @@ return value;

// - 0 (for _finalType only): circular reference
function setLink ( obj, value = null, prop = '_artifact' ) {
function setLink( obj, value = null, prop = '_artifact' ) {
Object.defineProperty( obj, prop, { value, configurable: true, writable: true } );

@@ -602,7 +694,7 @@ return value;

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

@@ -623,3 +715,3 @@ if (origin.name.$inferred)

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

@@ -631,11 +723,10 @@ if (parent._outer)

elem.name.absolute = elem._main.name.absolute;
[ 'element', 'alias', 'query', 'param', 'action' ].forEach( function( kind ) {
// console.log(elem.kind || elem)
let normalized = kindProperties[ elem.kind ].normalized || elem.kind;
if (normalized === kind) {
elem.name[kind] = (parent.name[kind] != null) ? parent.name[kind] + '.' + name : name;
}
else if (parent.name[kind] != null) {
const normalized = kindProperties[elem.kind].normalized || elem.kind;
[ 'element', 'alias', 'query', 'param', 'action' ].forEach( ( kind ) => {
if (normalized === kind)
elem.name[kind] = (parent.name[kind] != null && kind !== 'query') ? `${ parent.name[kind] }.${ name }` : name;
else if (parent.name[kind] != null)
elem.name[kind] = parent.name[kind];
}
else

@@ -651,7 +742,7 @@ delete elem.name[kind];

setProp( elem, '_block', block );
let kind = '_' + elem.kind; // _extend or _annotate
const kind = `_${ elem.kind }`; // _extend or _annotate
if (!parent[kind])
setProp( parent, kind, {} );
if (!parent[kind][prop])
parent[kind][prop] = Object.create(null)
parent[kind][prop] = Object.create(null);
pushToDict( parent[kind][prop], name, elem );

@@ -663,7 +754,7 @@ }

// without truthy optional argument `alsoTestLast`.
function withAssociation( ref, test = ()=>true, alsoTestLast ) {
for (let item of ref.path || []) {
let art = item && item._artifact; // item can be null with parse error
function withAssociation( ref, test = () => true, alsoTestLast ) {
for (const item of ref.path || []) {
const art = item && item._artifact; // item can be null with parse error
if (art && art._finalType && art._finalType.target && test( art._finalType, item ))
return (alsoTestLast || item !== ref.path[ ref.path.length-1 ]) && item;
return (alsoTestLast || item !== ref.path[ref.path.length - 1]) && item;
}

@@ -678,6 +769,7 @@ return false;

setLink,
linkToOrigin, setMemberParent,
linkToOrigin,
setMemberParent,
storeExtension,
withAssociation,
combinedLocation
combinedLocation,
};

@@ -31,3 +31,3 @@ const parseXml = require('./xmlParserWithLocations');

*/
function edmx2csn(src, filename) {
function edmx2csn(src, filename, options) {
let source = parseXml(src, filename);

@@ -41,2 +41,5 @@

forEachAnnotationsTag(source);
// The EDMX parser do not use the normal message function:
if (options.messages && csn.messages)
options.messages.push( ...csn.messages );
return csn;

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

'use strict';
const Edm = require('../edm.js');
const glue = require('../glue.js');
const edmUtils = require('../edmUtils.js');
const preprocessAnnotations = require('./preprocessAnnotations.js');

@@ -9,4 +8,54 @@ const oDataDictionary = require('./Dictionary.json');

const knownVocabularies = ['Aggregation', 'Analytics', 'Core', 'Common', 'UI', 'Communication', 'Capabilities', 'Measures', 'Validation', 'PersonalData'];
var vocabularyDefinitions = {
'Aggregation': {
'ref': { Uri: "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Aggregation.V1.xml" },
'inc': { Alias: "Aggregation", Namespace: "Org.OData.Aggregation.V1" }
},
'Analytics': {
'ref': { Uri: "https://wiki.scn.sap.com/wiki/download/attachments/462030211/Analytics.xml?api=v2" },
'inc': { Alias: "Analytics", Namespace: "com.sap.vocabularies.Analytics.v1" }
},
'Authorization': {
'ref': { Uri: "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Authorization.V1.xml" },
'inc': { Alias: "Authorization", Namespace: "Org.OData.Authorization.V1" }
},
'Capabilities': {
'ref': { Uri: "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1.xml" },
'inc': { Alias: "Capabilities", Namespace: "Org.OData.Capabilities.V1" }
},
'Common': {
'ref': { Uri: "https://wiki.scn.sap.com/wiki/download/attachments/448470974/Common.xml?api=v2" },
'inc': { Alias: "Common", Namespace: "com.sap.vocabularies.Common.v1" }
},
'Communication': {
'ref': { Uri: "https://wiki.scn.sap.com/wiki/download/attachments/448470971/Communication.xml?api=v2" },
'inc': { Alias: "Communication", Namespace: "com.sap.vocabularies.Communication.v1" }
},
'Core': {
'ref': { Uri: "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1.xml" },
'inc': { Alias: "Core", Namespace: "Org.OData.Core.V1" }
},
'Measures': {
'ref': { Uri: "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Measures.V1.xml" },
'inc': { Alias: "Measures", Namespace: "Org.OData.Measures.V1" }
},
'PersonalData': {
'ref': { Uri: "https://wiki.scn.sap.com/wiki/download/attachments/496435637/PersonalData.xml?api=v2" },
'inc': { Alias: "PersonalData", Namespace: "com.sap.vocabularies.PersonalData.v1" }
},
'UI': {
'ref': { Uri: "https://wiki.scn.sap.com/wiki/download/attachments/448470968/UI.xml?api=v2" },
'inc': { Alias: "UI", Namespace: "com.sap.vocabularies.UI.v1" }
},
'Validation': {
'ref': { Uri: "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Validation.V1.xml" },
'inc': { Alias: "Validation", Namespace: "Org.OData.Validation.V1" }
},
};
const knownVocabularies = Object.keys(vocabularyDefinitions);
/**************************************************************************************************

@@ -17,17 +66,22 @@ * csn2annotationEdm

* v - array with two boolean entries, first is for v2, second is for v4
* tntFlavor
* dictReplacement: for test purposes, replaces the standard oDataDictionary
*/
function csn2annotationEdm(csn, options=undefined) {
function csn2annotationEdm(csn, serviceName, options=undefined) {
// annotation preprocessing, partially tnt specific
// the only option used in preprocessAnnotations is tntFlavor
preprocessAnnotations.preprocessAnnotations(csn, options);
if(!options)
throw "Please debug me: csn2annotationsEdm must be invoked with options"
let usedVocabs = {};
const edmlib = require('../edm.js')(options);
// use closure to avoid making "dict" and "experimental" global variables
// annotation preprocessing
preprocessAnnotations.preprocessAnnotations(csn, serviceName, options);
// we take note of which vocabularies are actually used in a service in order to avoid
// producing useless references; reset everything to "unused"
knownVocabularies.forEach(n => {
vocabularyDefinitions[n].used = false;
});
// provide functions for dictionary lookup
// use closure to avoid making "dict" and "experimental" global variables
let { getDictTerm, getDictType } = function(){

@@ -39,4 +93,4 @@ let dict = options.dictReplacement || oDataDictionary; // tests can set different dictionary via options

// called to look-up a term in the dictionary
// in addition, note usage of the respective vocabulary and issue a warning if the term
// is flagged as "experimental"
// in addition: - note usage of the respective vocabulary
// - issue a warning if the term is flagged as "experimental"
getDictTerm: function(termName, context) {

@@ -46,3 +100,3 @@ let dictTerm = dict.terms[termName]

// register usage of vocabulary
usedVocabs[termName.slice(0, termName.indexOf('.'))] = true;
vocabularyDefinitions[termName.slice(0, termName.indexOf('.'))].used = true;
// issue warning for usage of experimental Terms, but only once per Term

@@ -62,3 +116,3 @@ if (dictTerm["$experimental"] && !experimental[termName] && !options.betaMode) {

// register usage of vocabulary
usedVocabs[typeName.slice(0, typeName.indexOf('.'))] = true;
vocabularyDefinitions[typeName.slice(0, typeName.indexOf('.'))].used = true;
}

@@ -70,31 +124,29 @@ return dictType;

let g_annosArray = [];
let v = options.v;
const { warning, signal } = alerts(csn);
const { error, warning, signal } = alerts(csn);
// global variable where we store all the generated annotations
let g_annosArray = [];
// Crawl over the csn and trigger the annotation translation for all kinds
// of annotated things.
// Note: only works for single service
// Note: we assume that all objects ly flat in the service, i.e. objName always
// looks like <service name, can contain dots>.<id>
// Note: it is NOT safe to assume that the service itself is the first definition in the csn
// -> in general "serviceName" is NOT correctly set during processing of other objects
let serviceName = null;
for (let objName in csn.definitions) {
let object = csn.definitions[objName];
if (object.kind == "service") {
serviceName = objName;
if(objName == serviceName || objName.startsWith(serviceName + '.')) {
let object = csn.definitions[objName];
if (object.kind == "action" || object.kind == "function") {
handleAction(objName, object, null);
}
else { // service, entity, anything else?
// handle the annotations directly tied to the object
handleAnnotations(objName, object);
// handle the annotations of the object's elements
handleElements(objName, object);
// handle the annotations of the object's actions
handleBoundActions(objName, object);
}
}
if (object.kind == "action" || object.kind == "function") {
handleAction(objName, object, null);
}
else { // service, entity, anything else?
// handle the annotations directly tied to the object
handleAnnotations(objName, object);
// handle the annotations of the object's elements
handleElements(objName, object);
// handle the annotations of the object's actions
handleBoundActions(objName, object);
}
}

@@ -105,14 +157,18 @@

let schema = new Edm.Schema(v, serviceName, serviceName, g_annosArray, false);
let service = new Edm.DataServices(v, schema);
let edm = new Edm(v, service);
// generate the edmx "frame" around the annotations
let schema = new edmlib.Schema(v, serviceName, serviceName, g_annosArray, false);
let service = new edmlib.DataServices(v, schema);
let edm = new edmlib.Edm(v, service);
// add references for the used vocabularies
knownVocabularies.forEach(n => {
if(vocabularyDefinitions[n].used) {
let r = new edmlib.Reference(v, vocabularyDefinitions[n].ref);
r.append(new edmlib.Include(v, vocabularyDefinitions[n].inc))
edm._defaultRefs.push(r);
}
})
return edm;
// helper to determine the OData version
// tnt is always v2
// TODO: improve option handling and revoke this hack for tnt
function isV2() {
return options.tntFlavor || (v && v[0]);
}
//-------------------------------------------------------------------------------------------------

@@ -122,8 +178,11 @@ //-------------------------------------------------------------------------------------------------

// helper to determine the OData version
// TODO: improve option handling
function isV2() {
return v && v[0];
}
//
// this function is called in the translation code to issue a warning message
// messages are reported via the alerts attribute of csn
//
// context contains "semantic location"
function warningMessage(context, message) {

@@ -143,14 +202,44 @@ let fullMessage = "in annotation translation: " + message;

// there are 4 possible kinds of targets for annotations
// csn: annotation at entity
// depending on the term, the target in edm is the corresponding
// - entity type
// - or entity set
// csn: annotation at element of entity
// target in edm is the corresponding element of the entity type
// csn: annotation at parameter of action of entity
// target in edm is the action parameter in the EntityContainer
/*
Mapping annotated thing in cds/csn => annotated thing in edmx:
carrier: the annotated thing in cds, can be: service, entity, structured type, element of entity or structured type,
action/function, parameter of action/function
target: the annotated thing in OData
In the edmx, all annotations for a OData thing are put into an element
<Annotations Target="..."> where Target is the full name of the target
There is one exception (Schema), see below
carrier = service
the target is the EntityContainer, unless the annotation has an "AppliesTo" where only Schema is given, but not EntityContainer
then the <Annotation ...> is directly put into <Schema ...> without an enclosing <Annotations ...>
carrier = entity (incl. view/projection)
the target is the corresponding EntityType, unless the annotation has an "AppliesTo" where only EntitySet is given, but not EntityType
then the target is the corresponding EntitySet
carrier = structured type
the target is the corresponding ComplexType
carrier = element of entity or structured type
the target is the corresponding Property of the EntityType/ComplexType: Target = <entity/type>/<element>
carrier = action/function
v2, unbound: Target = <service>.EntityContainer/<action/function>
v2, bound: Target = <service>.EntityContainer/<entity>_<action/function>
v4, unbound action: Target = <service>.<action>()
v4, bound action: Target = <service>.<action>(<service>.<entity>)
v4, unbound function: Target = <service>.<function>(<1st param type>, <2nd param type>, ...)
v4, bound function: Target = <service>.<function>(<service>.<entity>, <1st param type>, <2nd param type>, ...)
carrier = parameter of action/function
like above, but append "/<parameter" to the Target
*/
// handle the annotations of the elements of an object

@@ -163,3 +252,2 @@ // in: objname : name of the object

let element = object.elements[elemName];
// determine the name of the target in the resulting edm

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

// annotations for actions and functions (and their parameters)
// Annotations for actions and functions (and their parameters)
// v2, unbound: Target = <service>.EntityContainer/<action/function>

@@ -207,3 +295,3 @@ // v2, bound: Target = <service>.EntityContainer/<entity>_<action/function>

function handleBoundActions(cObjectname, cObject) {
// service name -> remove last part of the object name
// get service name: remove last part of the object name
// only works if all objects ly flat in the service

@@ -245,8 +333,12 @@ let nameParts = cObjectname.split(".")

let params = [];
if (bindingParam) params.push(bindingParam);
if (bindingParam) {
params.push(action['@cds.odata.bindingparameter.collection'] ? 'Collection(' + bindingParam + ')' : bindingParam);
}
if (action.kind === 'function') {
let mapType = (p) => (p.type.startsWith('cds.') && !p.type.startsWith('cds.foundation.')) ?
edmUtils.mapCdsToEdmType(p, signal, error, false /*is only called for v4*/) : p.type;
for (let n in action.params) {
let p = action.params[n];
let otype = p.type.startsWith('cds.') ? glue.mapCdsToEdmType(p.type, false /*is only called for v4*/) : p.type;
params.push(otype);
let isArrayType = !p.type && p.items && p.items.type;
params.push(isArrayType ? 'Collection(' + mapType(p.items) + ')' : mapType(p));
}

@@ -258,25 +350,28 @@ }

// note: in csn, all annotations are flattened out
// => values can be
// - primitive values (string, number)
// - pseudo-records with "#" or "="
// - arrays
// handle the annotations for a specific object or element or action parameter,
// here called carrier
// edmCarrierName : string, name of the annotated object in edm,
// element path is separated from object name by "/"
// TODO: handling of nested elements?
// carrier: object, the annotated object, contains all the annotations
// handle all the annotations for a given cds thing, here called carrier
// edmTargetName : string, name of the target in edm
// carrier: object, the annotated cds thing, contains all the annotations
// as properties with names starting with @
function handleAnnotations(edmCarrierName, carrier) {
function handleAnnotations(edmTargetName, carrier) {
// collect the names of the carrier's annotation properties
// keep only those annotations that - start with a known vocabulary name
// - have a value other than null
if(carrier['@cds.api.ignore']) {
return;
}
let annoNames = Object.keys(carrier).filter( x => x.substr(0,1) == "@" );
let knownAnnos = annoNames.filter(filterKnownVocabularies).filter(x => carrier[x] !== null);
let nullWhitelist = [ '@Core.OperationAvailable' ];
let knownAnnos = annoNames.filter(filterKnownVocabularies).filter(x => carrier[x] !== null || nullWhitelist.includes(x));
if (knownAnnos.length == 0) return;
// build prefix tree for the annotations attached to the carrier
// in csn, all annotations are flattened
// => values can be - primitive values (string, number)
// - pseudo-records with "#" or "="
// - arrays
// in OData, there are "structured" annotations -> we first need to regroup the cds annotations
// by building a "prefix tree" for the annotations attached to the carrier
// see example at definition of function mergePathStepsIntoPrefixTree
let prefixTree = {};

@@ -294,8 +389,19 @@ for (let a of knownAnnos) {

// addAnnotation() is used to actually add an <Annotation ...> to the
// construct a function that is used to add an <Annotation ...> to the
// respective <Annotations ...> element
// this function is specific to the actual carrier, following the mapping rules given above
let addAnnotation = function() {
let appliesTest = null; // used in closure
let stdName = edmCarrierName;
let altName = null; // used in closure
// usually, for a given carrier there is one target
// stdName: name of this target
// newAnnosStd: the corresponding <Annotations ...> tag
// for some carriers (service, entity), there can be an alternative target
// altName: name of alternative target
// newAnnosAlt: the corresponding <Annotations ...> tag
// which one to choose depends on the "AppliesTo" info of the single annotations, so we have
// to defer this decision; this is why we here construct a function that can make the decision
// later when looking at single annotations
let appliesTest = null; // the condition to decide between std and alt
let stdName = edmTargetName;
let altName = null;
if (carrier.kind === 'entity' || carrier.kind === 'view') {

@@ -306,3 +412,3 @@ // if annotated object is an entity, annotation goes to the EntityType,

// find last . in name and insert "EntityContainer/"
altName = edmCarrierName.replace(/\.(?=[^.]*$)/, '.EntityContainer/');
altName = edmTargetName.replace(/\.(?=[^.]*$)/, '.EntityContainer/');
}

@@ -313,10 +419,10 @@ else if (carrier.kind === 'service') {

appliesTest = (x => x.match(/Schema/) && !x.match(/EntityContainer/));
stdName = edmCarrierName + '.EntityContainer';
altName = edmCarrierName;
stdName = edmTargetName + '.EntityContainer';
altName = edmTargetName;
}
// result objects that holds all the annotation objects to be created
let newAnnosStd = new Edm.Annotations(v, stdName); // used in closure
let newAnnosStd = new edmlib.Annotations(v, stdName); // used in closure
g_annosArray.push(newAnnosStd);
let newAnnosAlt = null; // used in closure
let newAnnosAlt = null; // create only on demand

@@ -334,3 +440,3 @@ return function(annotation, appliesTo) {

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

@@ -349,3 +455,3 @@ }

// and put them into the elements property of the result object
handleAnno2(addAnnotation, edmCarrierName /*used for messages*/, prefixTree);
handleAnno2(addAnnotation, edmTargetName /*used for messages*/, prefixTree);
}

@@ -391,7 +497,11 @@

function handleAnno2(addAnnotationFunc, edmCarrierName, prefixTree) {
// handle all the annotations for a given carrier
// addAnnotationFunc: a function that adds the <Annotation ...> tags created here into the
// correct parent tag (see handleAnnotations())
// edmTargetName: name of the edmx target, only used for messages
// prefixTree: the annotations
function handleAnno2(addAnnotationFunc, edmTargetName, prefixTree) {
// first level names of prefix tree are the vocabulary names
// second level names are the term names
// create an annotation object for each term
// create an annotation tag <Annotation ...> for each term
for (let voc of Object.keys(prefixTree)) {

@@ -401,9 +511,10 @@ for (let term of Object.keys(prefixTree[voc])) {

// context is "semantic" location info used for messages
let context = { target: edmTargetName, term: fullTermName, stack: [] };
// anno is the full <Annotation Term=...>
let context = { target: edmCarrierName, term: fullTermName, stack: [] };
let anno = handleTerm(fullTermName, prefixTree[voc][term], context);
// addAnnotationFunc needs AppliesTo info from dictionary to decide where to put the anno
fullTermName = fullTermName.replace(/#(\w+)$/g, "");
let dictTerm = getDictTerm(fullTermName, context); // message for unknown term issued in handleTerm
fullTermName = fullTermName.replace(/#(\w+)$/g, ""); // remove qualifier
let dictTerm = getDictTerm(fullTermName, context); // message for unknown term was already issued in handleTerm
addAnnotationFunc(anno, dictTerm && dictTerm.AppliesTo);

@@ -421,3 +532,4 @@ }

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

@@ -446,2 +558,3 @@ // termName may contain a qualifier: @UI.FieldGroup#shippingStatus

// handle the annotation value and put the result into the <Annotation ...> tag just created above
handleValue(annoValue, newAnno, termNameWithoutQualifiers, termTypeName, context);

@@ -451,243 +564,13 @@ return newAnno;

// found an enum value ("#"), check whether this fits
// the expected type "expectedTypeName"
function checkEnumValue(enumValue, expectedTypeName, context) {
let expectedType = getDictType(expectedTypeName);
if (!expectedType && !isPrimitiveType(expectedTypeName)) {
warningMessage(context, "internal error: dictionary inconsistency: type '" + expectedTypeName + "' not found");
}
else if (isComplexType(expectedTypeName)) {
warningMessage(context, "found enum value, but expected complex type " + expectedTypeName);
}
else if (isPrimitiveType(expectedTypeName) || expectedType["$kind"] != "EnumType") {
warningMessage(context, "found enum value, but expected non-enum type " + expectedTypeName);
}
else if (!expectedType["Members"].includes(enumValue)) {
warningMessage(context, "enumeration type " + expectedTypeName + " has no value " + enumValue);
}
return;
}
// found an expression value ("=") "expr"
// expected type is dTypeName
// note: expr can also be provided if an enum/complex type/collection is expected
function handleExpression(expr, dTypeName, context) {
let typeName = "Path";
if( ['Edm.AnnotationPath', 'Edm.ModelElementPath', 'Edm.NavigationPropertyPath', 'Edm.PropertyPath', 'Edm.Path' ].includes(dTypeName) )
typeName = dTypeName.split('.')[1];
let val = expr;
if (!expr) {
warningMessage(context, "empty expression value");
}
else {
// replace all occurrences of '.' by '/' up to first '@'
val = expr.split('@').map((o,i) => (i==0 ? o.replace(/\./g, '/') : o)).join('@');
}
return {
name : typeName,
value : val
}
}
// found a simple value "val"
// expected type is dTypeName
// mappping rule for values:
// if expected type is ... the expression to be generated is ...
// floating point type except Edm.Decimal -> Float
// Edm.Decimal -> Decimal
// integer tpye -> Int
function handleSimpleValue(val, dTypeName, context) {
// caller already made sure that val is neither object nor array
dTypeName = resolveType(dTypeName);
let typeName = "String";
if(dTypeName == 'Edm.PrimitiveType')
dTypeName = undefined;
if (typeof val === 'string') {
// 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 (typeof val === 'boolean') {
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 (typeof val === 'number') {
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';
}
}
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 {
warningMessage(context, "expected simple value, but found value '" + val + "' with type '" + typeof val + "'");
}
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
};
}
// handle the actual value cAnnoValue
// oTarget: the result object
// handle an annotation value
// cAnnoValue: the annotation value (c : csn)
// oTarget: the result object (o: odata)
// oTermName: current term
// dTypeName: expected type of cAnnoValue according to dictionary, may be null
// dTypeName: expected type of cAnnoValue according to dictionary, may be null (d: dictionary)
function handleValue(cAnnoValue, oTarget, oTermName, dTypeName, context) {
// value can be: array, expression, enum, pseudo-record, record, simple value
// this function basically only figures out what kind of annotation value we have
// (can be: array, expression, enum, pseudo-record, record, simple value),
// then calls a more specific function to deal with it and puts
// the result into the oTarget object

@@ -713,2 +596,3 @@ if (Array.isArray(cAnnoValue))

else if ("=" in cAnnoValue) {
// expression
let res = handleExpression(cAnnoValue["="], dTypeName, context);

@@ -718,6 +602,5 @@ oTarget.setXml( { [res.name] : res.value });

}
else if (cAnnoValue["#"] !== undefined)
{
if (dTypeName)
{
else if (cAnnoValue["#"] !== undefined) {
// enum
if (dTypeName) {
checkEnumValue(cAnnoValue["#"], dTypeName, context);

@@ -727,6 +610,6 @@ oTarget.setJSON({ "EnumMember": cAnnoValue["#"], "EnumMember@odata.type" : '#'+dTypeName, });

}
else
{
else {
// do something seemingly reasonable even if there is no dictionary info
oTarget.setJSON({ "EnumMember": cAnnoValue["#"], "EnumMember@odata.type" : '#'+oTermName + "Type/" });
oTarget.setXml( { "EnumMember": oTermName + "Type/" + "/" + cAnnoValue["#"] });
oTarget.setXml( { "EnumMember": oTermName + "Type/" + cAnnoValue["#"] });
}

@@ -752,2 +635,3 @@ }

else if ( Object.keys(cAnnoValue).filter( x => x.substr(0,1) !== "@" ).length === 0) {
// object consists only of properties starting with "@"
warningMessage(context, "nested annotations without corresponding base annotation");

@@ -762,3 +646,9 @@ }

let res = handleSimpleValue(cAnnoValue, dTypeName, context);
oTarget.setXml( { [res.name] : res.value });
if(oTermName == "Core.OperationAvailable" && dTypeName === "Edm.Boolean" && cAnnoValue === null) {
oTarget.append(new edmlib.ValueThing(v, 'Null'));
oTarget._ignoreChildren = true;
}
else {
oTarget.setXml( { [res.name] : res.value });
}
oTarget.setJSON( { [res.jsonName] : res.value });

@@ -769,2 +659,21 @@ }

// found an enum value ("#"), check whether this fits
// the expected type "dTypeName"
function checkEnumValue(enumValue, dTypeName, context) {
let expectedType = getDictType(dTypeName);
if (!expectedType && !isPrimitiveType(dTypeName)) {
warningMessage(context, "internal error: dictionary inconsistency: type '" + dTypeName + "' not found");
}
else if (isComplexType(dTypeName)) {
warningMessage(context, "found enum value, but expected complex type " + dTypeName);
}
else if (isPrimitiveType(dTypeName) || expectedType["$kind"] != "EnumType") {
warningMessage(context, "found enum value, but expected non-enum type " + dTypeName);
}
else if (!expectedType["Members"].includes(enumValue)) {
warningMessage(context, "enumeration type " + dTypeName + " has no value " + enumValue);
}
return;
}
// cAnnoValue: array

@@ -803,21 +712,156 @@ // dTypeName: expected type, already identified as enum type

// obj: object representing the record
// dictRecordTypeName : name of the expected record type according to vocabulary, may be null
// found an expression value ("=") "expr"
// expected type is dTypeName
// note: expr can also be provided if an enum/complex type/collection is expected
function handleExpression(expr, dTypeName, context) {
let typeName = "Path";
if( ['Edm.AnnotationPath', 'Edm.ModelElementPath', 'Edm.NavigationPropertyPath', 'Edm.PropertyPath', 'Edm.Path' ].includes(dTypeName) )
typeName = dTypeName.split('.')[1];
let val = expr;
if (!expr) {
warningMessage(context, "empty expression value");
}
else {
// replace all occurrences of '.' by '/' up to first '@'
val = expr.split('@').map((o,i) => (i==0 ? o.replace(/\./g, '/') : o)).join('@');
}
return {
name : typeName,
value : val
}
}
// found a simple value "val"
// expected type is dTypeName
// mappping rule for values:
// if expected type is ... the expression to be generated is ...
// floating point type except Edm.Decimal -> Float
// Edm.Decimal -> Decimal
// integer tpye -> Int
function handleSimpleValue(val, dTypeName, context) {
// caller already made sure that val is neither object nor array
dTypeName = resolveType(dTypeName);
let typeName = "String";
if (typeof val === 'string') {
if (dTypeName == "Edm.Boolean") {
typeName = "Bool";
if (!['true','false'].includes(val)) {
warningMessage(context, "found String, but expected type " + dTypeName);
}
}
else if (dTypeName == "Edm.Decimal") {
typeName = "Decimal";
if (isNaN(val) || isNaN(parseFloat(val))) {
warningMessage(context, "found non-numeric string, but expected type " + dTypeName);
}
}
else if (dTypeName == "Edm.Double" || dTypeName == "Edm.Single") {
typeName = "Float";
if (isNaN(val) || isNaN(parseFloat(val))) {
warningMessage(context, "found non-numeric string, but expected type " + dTypeName);
}
}
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") {
// this covers also all paths
typeName = dTypeName.substring(4);
}
else {
if(dTypeName == undefined || dTypeName == 'Edm.PrimitiveType')
dTypeName = 'Edm.String';
// TODO
//warningMessage(context, "type is not yet handled: found String, expected type: " + dTypeName);
}
}
else if (typeof val === 'boolean') {
if(dTypeName == undefined || dTypeName == "Edm.Boolean" || dTypeName == 'Edm.PrimitiveType') {
typeName = "Bool";
dTypeName = 'Edm.Boolean';
}
if (dTypeName == "Edm.Boolean") {
val = val ? "true" : "false";
}
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);
}
else if (dTypeName === 'Edm.String') {
typeName = "String";
}
else if (dTypeName == "Edm.PropertyPath") {
warningMessage(context, "found number, but expected type " + 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';
if(Number.isInteger(val)) {
typeName = 'Int';
if(dTypeName == undefined || dTypeName == 'Edm.PrimitiveType')
dTypeName = 'Edm.Int64';
}
else {
typeName = 'Float';
if(dTypeName == undefined || dTypeName == 'Edm.PrimitiveType')
dTypeName = 'Edm.Double';
}
}
}
else {
warningMessage(context, "expected simple value, but found value '" + val + "' with type '" + typeof 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
};
}
// obj: object representing the record
// dTypeName : name of the expected record type according to vocabulary, may be null
//
// can be called for a record directly below a term, or at a deeper level
// if the corresponding complex type is unique, it needs not to be written into the
// record; if it is abstract or part of a type hierarchy, it must be written
// into the record as attribute "Type"
function generateRecord(obj, termName, dictRecordTypeName, context) {
let newRecord = new Edm.Record(v);
let actualTypeName = null;
function generateRecord(obj, termName, dTypeName, context) {
let newRecord = new edmlib.Record(v);
if (dictRecordTypeName && !isComplexType(dictRecordTypeName)) {
if (!getDictType(dictRecordTypeName) && !isPrimitiveType(dictRecordTypeName) && !isCollection(dictRecordTypeName))
warningMessage(context, "internal error: dictionary inconsistency: type '" + dictRecordTypeName + "' not found");
// first determine what is the actual type to be used for the record
if (dTypeName && !isComplexType(dTypeName)) {
if (!getDictType(dTypeName) && !isPrimitiveType(dTypeName) && !isCollection(dTypeName))
warningMessage(context, "internal error: dictionary inconsistency: type '" + dTypeName + "' not found");
else
warningMessage(context, "found complex type, but expected type '" + dictRecordTypeName + "'");
warningMessage(context, "found complex type, but expected type '" + dTypeName + "'");
return newRecord;
}
let actualTypeName = null;
if (obj["$Type"]) { // type is explicitly specified

@@ -829,7 +873,7 @@ actualTypeName = obj["$Type"];

}
else if (dictRecordTypeName && !isDerivedFrom(actualTypeName, dictRecordTypeName)) {
else if (dTypeName && !isDerivedFrom(actualTypeName, dTypeName)) {
// this type doesn't fit the expected one
warningMessage(context, "explicitly specified type '" + actualTypeName
+ "' is not derived from expected type '" + dictRecordTypeName + "'");
actualTypeName = dictRecordTypeName;
+ "' is not derived from expected type '" + dTypeName + "'");
actualTypeName = dTypeName;
}

@@ -839,3 +883,3 @@ else if (isAbstractType(actualTypeName)) {

warningMessage(context, "explicitly specified type '" + actualTypeName + "' is abstract, specify a concrete type");
actualTypeName = dictRecordTypeName;
actualTypeName = dTypeName;
}

@@ -847,14 +891,14 @@ else {

}
else if (dictRecordTypeName) { // there is an expected type name according to dictionary
else if (dTypeName) { // there is an expected type name according to dictionary
// convenience for common situation:
// if DataFieldAbstract is expected and no explicit type is provided, automatically choose DataField
if (dictRecordTypeName == "UI.DataFieldAbstract") {
if (dTypeName == "UI.DataFieldAbstract") {
actualTypeName = "UI.DataField";
}
else {
actualTypeName = dictRecordTypeName;
actualTypeName = dTypeName;
}
if (isAbstractType(actualTypeName)) {
warningMessage(context, "type '" + dictRecordTypeName + "' is abstract, use '$Type' to specify a concrete type");
warningMessage(context, "type '" + dTypeName + "' is abstract, use '$Type' to specify a concrete type");
}

@@ -868,2 +912,3 @@

// now the type is clear, so look ath the value
let dictProperties = getAllProperties(actualTypeName);

@@ -876,5 +921,6 @@

if (i == "$Type") {
// nop
// ignore, this is an "artificial" property used to indicate the type
}
else if (i.charAt(0) == "@") {
// not a regular property, but a nested annotation
let newAnno = handleTerm(i.substring(1, i.length), obj[i], context);

@@ -884,2 +930,3 @@ newRecord.append(newAnno);

else {
// regular property
let dictPropertyTypeName = null;

@@ -893,3 +940,4 @@ if (dictProperties) {

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

@@ -908,5 +956,4 @@ newRecord.append(newPropertyValue);

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

@@ -928,2 +975,5 @@ let innerTypeName = null;

// for dealing with the single array entries we unfortunately cannot call handleValue(),
// as the values inside an array are represented differently from the values
// in a record or term
if (Array.isArray(value)) {

@@ -935,3 +985,3 @@ warningMessage(context, "nested collections are not supported");

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

@@ -950,3 +1000,3 @@ newCollection.append(newPropertyPath);

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

@@ -962,6 +1012,12 @@ newCollection.append(newThing);

// Not everything that can occur in OData annotations can be expressed with
// corresponding constructs in cds annotations. For these special cases
// we have a kind of "inline assembler" mode, i.e. you can in cds provide
// as annotation value a json snippet that looks like the final edm-json.
// See example in test/odataAnnotations/smallTests/edmJson_noReverse_ok
function handleEdmJson(obj, context)
{
let specialProperties = [ '$Apply', '$LabeledElement' ];
let subset = glue.intersect(specialProperties, Object.keys(obj));
let subset = edmUtils.intersect(specialProperties, Object.keys(obj));

@@ -976,3 +1032,3 @@ if(subset.length > 1) { // doesn't work for three or more...

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

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

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

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

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

@@ -1061,2 +1117,6 @@ }

//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
// filter function, assumed to be used for array of string

@@ -1070,8 +1130,2 @@ // accepts those strings that start with a known vocabulary name

//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
//-------------------------------------------------------------------------------------------------
// resolve "derived types"

@@ -1120,3 +1174,2 @@ // -> if dTypeName is a TypeDefinition, replace by

// return dictionary of all properties of typeName, including those of base types

@@ -1123,0 +1176,0 @@ function getAllProperties(typeName) {

'use strict';
const glue = require('../glue');
const edmUtils = require('../edmUtils.js');
const alerts = require('../../base/alerts');

@@ -11,3 +11,3 @@

* options:
* tntFlavor
* v
*

@@ -18,13 +18,7 @@ * This module never produces errors. In case of "unexpected" situations we issue a warning and

*/
function preprocessAnnotations(csn, options) {
function preprocessAnnotations(csn, serviceName, options) {
const { warning, signal } = alerts(csn);
let fkSeparator = (options && options.tntFlavor && !options.tntFlavor.skipGeneratedFKsWithout_) ? '' : '_';
let fkSeparator = '_';
if (options && options.tntFlavor) {
addCommonTextAndCommonValueListToAssociations();
moveAnnotationsFromAssocToForeignKeys();
}
// for resolveShortcuts... the check for tntFlavor is done inside the function,
// because not all transformations are TnT specific
resolveShortcutsAndTNTspecificModifications();
resolveShortcuts();

@@ -37,13 +31,7 @@

// helper to determine the OData version
// tnt is always v2
// TODO: improve option handling and revoke this hack for tnt
// TODO: improve option handling
function isV2() {
return options.tntFlavor || (options.v && options.v[0]);
return options.v && options.v[0];
}
function getTargetOfAssoc(assoc) {
// assoc.target can be the name of the target or the object itself
return (typeof assoc.target === 'object') ? assoc.target : csn.definitions[assoc.target];
}
// return value can be null is target has no key

@@ -71,151 +59,20 @@ function getKeyOfTargetOfManagedAssoc(assoc) {

function addCommonTextAndCommonValueListToAssociations() {
// TNT - TextAndCommonValueListToAssociations
if (!(options && options.tntFlavor && options.tntFlavor.skipAnnosTextAndValueListForAssocs)) {
glue.forAll(csn.definitions, (art) => {
glue.forAll(art.elements, (elt, eltName) => {
handleAnnotations(eltName, elt, art);
})
});
}
function handleAnnotations(elemName, elem, object) {
if(!((object.kind == 'entity' || object.kind == 'view') &&
(elem.type == "cds.Association" || elem.type == "Association") && elem.on == undefined && elem.onCond == undefined))
return;
let assoc = elem;
if (assoc["@odata.navigable"] != undefined && assoc["@odata.navigable"] == false) {
return;
}
let targetName = assoc.target.name;
let targetNameShort = targetName.split(".").pop();
let fkName = getKeyOfTargetOfManagedAssoc(assoc);
let targetEntity = assoc.target; // csn.definitions[targetName];
// ValueList.Label
// 1) Common.Label of assoc
// 2) Common.Label of target entity
// 3) name of target entity
let label = "UNKNOWN1";
if (assoc["@Common.Label"] != undefined) {
label = assoc["@Common.Label"];
}
else if (targetEntity["@Common.Label"] != undefined) {
label = targetEntity["@Common.Label"];
}
else {
label = targetNameShort;
}
// for Common.Text and DisplayOnly
let uiId = null;
let tEId = targetEntity["@UI.Identification"];
if (tEId && tEId[0])
{
if (tEId[0]["="]) {
uiId = tEId[0]["="];
}
else if (tEId[0]["Value"] && tEId[0]["Value"]["="]) {
uiId = tEId[0]["Value"]["="];
}
}
// @Common.Text
if (uiId != null) {
assoc["@Common.Text"] = {
"$value": { "=": elemName + "/" + uiId },
"@UI.TextArrangement": { "#": "TextOnly" }
};
}
assoc["@Common.ValueList.Label"] = label;
assoc["@Common.ValueList.CollectionPath"] = targetNameShort;
assoc["@Common.ValueList.Parameters"] = [
{
"$Type": "Common.ValueListParameterInOut",
"LocalDataProperty": { "=": elemName + fkSeparator + fkName },
"ValueListProperty": fkName
}
];
if (uiId != null) {
assoc["@Common.ValueList.Parameters"].push(
{
"$Type": "Common.ValueListParameterDisplayOnly",
"ValueListProperty": uiId
}
);
}
}
}
/*
Forward all annotations from the association element to its generated foreign key elements
and remove the annotations from the association afterwards (move)
Input: OData preprocessed CSN with entity definitions
*/
function moveAnnotationsFromAssocToForeignKeys() {
// managed association
// (not at compositions, because we think they never are managed ... ???)
glue.foreach(csn.definitions, obj => (obj.kind == 'entity' || obj.kind == 'view'), entity => {
glue.foreach(entity.elements, e => glue.isManagedAssociation(e), (element, elementName) =>
{
// copy annotations from assoc to all generated foreign key fields
// FIXME: This should actually be done always, regardless of TNT (the ODATA preprocesor has already
// done it for all existing annotations, but all creation/modification magic happening during this
// post-processing needs to go to the foreign key fields as well). Alternatively, one could make
// sure that all magic happening here is directly applied to foreign key fields, too.
if (element.foreignKeys) {
glue.forAll(element.foreignKeys, fk => {
addAnnos(fk.generatedFieldName)
});
}
else if (element.keys) {
for (let x of element.keys) {
addAnnos(elementName + fkSeparator + x.ref[0])
}
}
function addAnnos(fk_generatedFieldName) {
glue.forAll(element, (attr, attrName) => {
if(attrName[0] == '@')
entity.elements[fk_generatedFieldName][attrName] = attr;
});
}
// remove annotations from assoc (separated from copy because there might be multiple foreign keys)
if (!(options && options.tntFlavor && options.tntFlavor.skipAnnosRemoveManagedAssociationAnnos)) {
for (let a in element) {
if (a[0] == "@") {
delete element[a];
}
}
}
});
});
}
// resolve shortcuts and do some TNT specific modifications
function resolveShortcutsAndTNTspecificModifications() {
// resolve shortcuts
function resolveShortcuts() {
let art = null;
glue.forAll(csn.definitions, (artifact, artifactName) =>{
art = artifactName;
handleAnnotations(artifactName, artifact);
glue.forAll(artifact.elements, (element, elementName) => {
handleAnnotations(elementName, element);
});
glue.forAll(artifact.actions, (action) => {
glue.forAll(action.params, (param, paramName) => {
handleAnnotations(paramName, param);
edmUtils.forAll(csn.definitions, (artifact, artifactName) => {
if(artifactName == serviceName || artifactName.startsWith(serviceName + '.')) {
art = artifactName;
handleAnnotations(artifactName, artifact);
edmUtils.forAll(artifact.elements, (element, elementName) => {
handleAnnotations(elementName, element);
});
});
edmUtils.forAll(artifact.actions, (action) => {
edmUtils.forAll(action.params, (param, paramName) => {
handleAnnotations(paramName, param);
});
});
}
});

@@ -225,22 +82,2 @@

function replaceManagedAssocByFK(ae) {
let path = ae["="];
let steps = path.split('.');
let ent = carrier;
for (let i in steps) {
if (!ent || ent.kind != 'entity') return;
let el = ent.elements[steps[i]];
if (el && glue.isAssociation(el)) {
if (i < steps.length-1) {
ent = getTargetOfAssoc(el);
}
else { //last step
if (!el.onCond && !el.on) { // only for managed
ae["="] += fkSeparator + getKeyOfTargetOfManagedAssoc(el);
}
}
}
}
}
// collect the names of the carrier's annotation properties

@@ -251,3 +88,2 @@ let annoNames = Object.keys(carrier).filter( x => x.substr(0,1) == "@")

let aNameWithoutQualifier = aName.split("#")[0];
let a = carrier[aName];

@@ -257,11 +93,25 @@ //for warning messages

// Always - draft annotations, value is action name
// - v2: prefix with entity name
// - prefix with service name
draftAnnotations(carrier, aName, aNameWithoutQualifier);
// Always - FixedValueListShortcut
// expand shortcut form of ValueList annotation
fixedValueListShortCut(carrier, aNameWithoutQualifier, ctx);
// Always - TextArrangementReordering
// convert @Common.TextArrangement annotation that is on same level as Text annotation into a nested annotation
textArrangementReordering(carrier, aName, aNameWithoutQualifier, ctx);
}
// inner functions
function draftAnnotations(carrier, aName, aNameWithoutQualifier) {
if ((carrier.kind === 'entity' || carrier.kind === 'view') &&
(aNameWithoutQualifier == "@Common.DraftRoot.PreparationAction" ||
aNameWithoutQualifier == "@Common.DraftRoot.ActivationAction" ||
aNameWithoutQualifier == "@Common.DraftRoot.EditAction" ||
aNameWithoutQualifier == "@Common.DraftNode.PreparationAction")
) {
aNameWithoutQualifier == "@Common.DraftRoot.ActivationAction" ||
aNameWithoutQualifier == "@Common.DraftRoot.EditAction" ||
aNameWithoutQualifier == "@Common.DraftNode.PreparationAction")
) {
let value = carrier[aName];

@@ -280,5 +130,5 @@ // prefix with service name, if not already done

}
}
// Always - FixedValueListShortcut
// expand shortcut form of ValueList annotation
function fixedValueListShortCut(carrier, aNameWithoutQualifier, ctx) {
if (aNameWithoutQualifier == "@Common.ValueList.entity" ||

@@ -291,7 +141,2 @@ aNameWithoutQualifier == "@Common.ValueList.viaAssociation") {

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

@@ -355,5 +200,4 @@ if (carrier["@Common.ValueList.CollectionPath"]) {

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

@@ -364,6 +208,15 @@ // localDataProp

let localDataProp = carrierName.split("/").pop();
if (glue.isManagedAssociation(carrier)) {
if (edmUtils.isManagedAssociation(carrier)) {
localDataProp = localDataProp + fkSeparator + getKeyOfTargetOfManagedAssoc(carrier);
}
// if this carrier is a generated foreign key field and the association is marked @cds.api.ignore
// rename the localDataProp to be 'assocName/key'
if(carrier['@cds.api.ignore']) {
let assocName = carrier['@odata.foreignKey4'];
if(assocName && options.isV4()) {
localDataProp = localDataProp.replace(assocName+fkSeparator, assocName+'/');
}
}
// valueListProp: the (single) key field of the value list entity

@@ -383,4 +236,4 @@ // if no key or multiple keys -> warning

// first entry of @UI.Identification
// can be an expression (tnt shortcut before expansion)
// can be a record with property 'Value' and expression as its value
// a record with property 'Value' and expression as its value
// or shortcut expansion array of paths
// OR

@@ -394,4 +247,3 @@ // the (single) non-key string field, if there is one

textField = Identification[0]["Value"]['='];
}
else {
} else {
let stringFields = Object.keys(vlEntity.elements).filter(

@@ -425,9 +277,2 @@ x => !vlEntity.elements[x].key && vlEntity.elements[x].type == "cds.String")

newObj["@Common.ValueList.Parameters"] = parameters;
if (textField && options && options.tntFlavor) {
newObj["@Common.Text"] = {
// here we rely on ValueListMagic: an association has been created based on @Common.ValueList.fixed
"$value": { "=": "to_" + carrierName + "/" + textField },
"@UI.TextArrangement": { "#": "TextOnly" }
};
}
}

@@ -452,9 +297,6 @@ else if (e == "@Common.ValueList.type" ||

}
}
// Always - TextArrangementReordering
// convert @Common.TextArrangement annotation that is on same level as Text annotation into a nested annotation
// TnT only: also accept @UI.TextArrangement
let tntTextArrangement = options && options.tntFlavor && !options.tntFlavor.skipAnnosTextArrangementReordering;
if (aNameWithoutQualifier == "@Common.TextArrangement" ||
(tntTextArrangement && aNameWithoutQualifier == "@UI.TextArrangement")) {
function textArrangementReordering(carrier, aName, aNameWithoutQualifier, ctx) {
if (aNameWithoutQualifier == "@Common.TextArrangement") {
let value = carrier[aName];

@@ -467,9 +309,2 @@ let textAnno = carrier["@Common.Text"];

// TNT: this fixes a bug in the TNT model where a string is given instead of an enum
if (tntTextArrangement) {
if (value === "TextFirst") {
value = { "#": value };
}
}
//change the scalar anno into a "pseudo-structured" one

@@ -481,30 +316,5 @@ // TODO should be flattened, but then alphabetical order is destroyed

}
// TNT - SubstitutingFKeysForAssocs
if (options && options.tntFlavor && !options.tntFlavor.skipAnnosSubstitutingFKeysForAssocs) {
// replace association by fk field, mind nested annotatinos
if (aNameWithoutQualifier == "@UI.LineItem" || aNameWithoutQualifier == "@UI.LineItem.$value" ||
aNameWithoutQualifier == "@UI.Identification" || aNameWithoutQualifier == "@UI.Identification.$value" ||
aNameWithoutQualifier == "@UI.FieldGroup" || aNameWithoutQualifier == "@UI.FieldGroup.$value") {
for (let ae of a) {
if (ae["Value"] && ae["Value"]["="]) {
replaceManagedAssocByFK(ae["Value"]);
}
}
}
// replace association by fk field
if (aNameWithoutQualifier == "@UI.SelectionFields") {
for (let ae of a) {
if ("=" in ae) {
replaceManagedAssocByFK(ae);
}
}
}
}
}
}
}
}

@@ -511,0 +321,0 @@

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

const COMPLEXTYPE_TRENNER='_'
let NAVPROP_TRENNER = '_' // possibly overruled for TNT below
let VALUELIST_NAVPROP_PREFIX = '' // possibly overruled for TNT below
let NAVPROP_TRENNER = '_';
let VALUELIST_NAVPROP_PREFIX = '';
const glue = require('./glue.js')
const Edm = require('./edm.js');
const edmUtils = require('./edmUtils.js')
const { initializeModel } = require('./edmPreprocessor.js');
const translate = require('./annotations/genericTranslation.js');
const alerts = require('../base/alerts');
const { setProp } = require('../base/model');
const { getUtils, cloneCsn } = require('../model/csnUtils');
/*

@@ -25,524 +25,569 @@ OData V2 spec 06/01/2017 PDF version is available from here:

function csn2edm(csn, serviceName, _options) {
return csn2edmAll(csn, _options, serviceName)[serviceName];
}
module.exports = function csn2edm(csn, serviceName, _options) {
function csn2edmAll(csn, _options, serviceName=undefined) {
const options = glue.validateOptions(_options);
const { error, warning, info, signal } = alerts(csn);
const { isBuiltinType } = getUtils(csn);
// get us a fresh model copy that we can work with
let model = cloneCsn(csn);
let [services, options] = initializeModel(model, _options, signal, error, warning, info);
const Edm = require('./edm.js')(options, signal, error);
let v = options.v;
if(services.length == 0)
signal(error`No Services found in model`);
// get us a fresh model copy that we can work with
let model = JSON.parse(JSON.stringify(csn));
model.messages = csn.messages;
// 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 rc = Object.create(null);
if(serviceName) {
let serviceCsn = services.filter(s => s.name == serviceName)[0];
if(serviceCsn == undefined) {
signal(warning`No service definition with name "${serviceName}" found in the model`);
}
else {
rc[serviceName] = createEdm(serviceCsn);
}
return rc;
}
else {
return services.reduce((services, serviceCsn) => {
services[serviceCsn.name] = createEdm(serviceCsn);
return services; }, rc);
}
let serviceCsn = glue.initializeModel(model, options);
if(serviceCsn == undefined)
throw Error('No Service found in model');
//--------------------------------------------------------------------------------
// embedded functions
//--------------------------------------------------------------------------------
function createEdm(serviceCsn) {
let navigationProperties = [];
let navigationProperties = [];
function baseName(str, del) { let l = str.lastIndexOf(del); // eslint-disable-line no-unused-vars
return (l >= 0) ? str.slice(l+del.length, str.length) : str; }
function baseName(str, del) { let l = str.lastIndexOf(del); // eslint-disable-line no-unused-vars
return (l >= 0) ? str.slice(l+del.length, str.length) : str; }
// if we have a real alias take it, otherwise use basename of service
// let alias = serviceCsn.alias || baseName(baseName(serviceCsn.name, '::'), '.');
// if we have a real alias take it, otherwise use basename of service
// let alias = serviceCsn.alias || baseName(baseName(serviceCsn.name, '::'), '.');
// FIXME: UI5 cannot deal with spec conforming simpleid alias names
let serviceName = serviceCsn.name;
let alias = serviceName;
// FIXME: UI5 cannot deal with spec conforming simpleid alias names
let alias = serviceCsn.name;
let Schema = new Edm.Schema(v, serviceName, undefined /* unset alias */);
let Schema = new Edm.Schema(v, serviceCsn.name, undefined /* unset alias */);
// now namespace and alias are used to create the fullQualified(name)
const namespace = serviceName + '.'
alias += '.'
// now namespace and alias are used to create the fullQualified(name)
const namespace = serviceCsn.name + '.'
alias += '.'
let service = new Edm.DataServices(v, Schema);
let edm = new Edm.Edm(v, service);
let service = new Edm.DataServices(v, Schema);
let edm = new Edm(v, service);
/* create the entitytypes and sets
Do not create an entity set if:
V4 containment: _containerEntity is set and not equal with the artifact name
Entity starts with 'localserviceNameized.' or ends with '_localized'
*/
edmUtils.foreach(model.definitions,
a => edmUtils.isEntityOrView(a) && !a.abstract && a.name.startsWith(serviceName + '.'),
a => createEntityTypeAndSet(a, !(options.isV4() && edmUtils.isContainee(a)) && !a.$proxy)
);
// create unbound actions/functions
edmUtils.foreach(model.definitions, a => edmUtils.isActionOrFunction(a) && a.name.startsWith(serviceName + '.'),
(options.isV4()) ? createActionV4 : createActionV2);
/* create the entitytypes and sets
Do not create an entity set if:
V4 containment: _containerEntity is set and not equal with the artifact name
*/
glue.foreach(model.definitions,
a => glue.isEntityOrView(a) && !a.abstract,
(a,n) => createEntityTypeAndSet(a, n,
!( options.isV4() && (glue.isContainee(a,n)) )
)
);
// create the complex types
edmUtils.foreach(model.definitions, a => edmUtils.isStructuredType(a) && a.name.startsWith(serviceName + '.'), createComplexType);
// create unbound actions/functions
glue.foreach(model.definitions, glue.isActionOrFunction,
(options.isV4()) ? createActionV4 : createActionV2);
if(options.isV4())
{
edmUtils.foreach(model.definitions,
artifact => edmUtils.isDerivedType(artifact) &&
!edmUtils.isAssociationOrComposition(artifact) &&
artifact.name.startsWith(serviceName + '.'),
createTypeDefinition);
}
// create the complex types
glue.foreach(model.definitions, glue.isStructuredType, createComplexType);
// fetch all exising children names in a map
let NamesInSchemaXRef = Schema._children.reduce((acc, cur) => {
if(acc[cur.Name] === undefined) {
acc[cur.Name] = [ cur ];
} else {
acc[cur.Name].push(cur);
}
return acc;
}, Object.create(null) );
if(options.isV4())
{
glue.foreach(model.definitions,
artifact => glue.isDerivedType(artifact) &&
!glue.isAssociationOrComposition(artifact),
createTypeDefinition);
}
navigationProperties.forEach(np => {
if(options.isV4()) {
// V4: No referential constraints for Containment Relationships
if(!np.isContainment() && !np.isToMany())
np.addReferentialConstraintNodes();
}
else
addAssociation(np);
});
navigationProperties.forEach(np => {
if(options.isV4()) {
// V4: No referential constraints for Containment Relationships
if(!np.isContainment())
np.addReferentialConstraintNodes();
createAnnotations(edm);
for(let name in NamesInSchemaXRef) {
if(NamesInSchemaXRef[name].length > 1) {
let artifactName = `${Schema.Namespace}.${name}`;
signal(error`Duplicate name "${name}" in Namespace "${Schema.Namespace}"`, ['definitions',artifactName]);
}
}
else
addAssociation(np);
});
if(Schema._ec._children.length == 0)
signal(error`EntityContainer must contain at least one EntitySet`, ['definitions',Schema.Namespace]);
createAnnotations(edm);
return edm
if(Schema._ec._children.length == 0)
throw Error('EntityContainer must contain at least one EntitySet');
function createEntityTypeAndSet(entityCsn, createEntitySet=true)
{
// EntityType attributes are: Name, BaseType, Abstract, OpenType, HasStream
// Sub Elements are: Key, Property, NavigationProperty
return edm
let EntityTypeName = entityCsn.name.replace(namespace, '');
let EntitySetName = (entityCsn.setName || entityCsn.name).replace(namespace, '');
//--------------------------------------------------------------------------------
// embedded functions
//--------------------------------------------------------------------------------
let [ properties, hasStream ] = createProperties(entityCsn);
function createEntityTypeAndSet(entityCsn, entityName, createEntitySet=true)
{
// EntityType attributes are: Name, BaseType, Abstract, OpenType, HasStream
// Sub Elements are: Key, Property, NavigationProperty
if(properties.length === 0) {
signal(error`EntityType "${serviceName}/${EntityTypeName}" has no properties`, ['definitions',entityCsn.name]);
}
// construct EntityType attributes
let attributes = { Name : EntityTypeName };
let EntityTypeName = entityCsn.name.replace(namespace, '');
let EntitySetName = (entityCsn.setName || entityCsn.name).replace(namespace, '');
let fqEntityTypeName = fullQualified(entityName);
// CDXCORE-CDXCORE-173
if(options.isV2() && hasStream)
attributes['m:HasStream'] = hasStream;
let [ properties, hasStream ] = createProperties(entityCsn);
Schema.append(new Edm.EntityType(v, attributes, properties, entityCsn));
// construct EntityType attributes
let attributes = { Name : EntityTypeName };
if (createEntitySet)
{
let entitySet = new Edm.EntitySet(v, { Name: EntitySetName, EntityType: fullQualified(EntityTypeName) }, entityCsn);
// CDXCORE-CDXCORE-173
if(options.isV2() && hasStream)
attributes['m:HasStream'] = hasStream;
// V4: Create NavigationPropertyBinding in EntitySet
// if NavigationProperty is not a Containment and if the target is not a containee
if(options.isV4())
properties.filter(np =>
np instanceof Edm.NavigationProperty &&
!np.isContainment() && !edmUtils.isContainee(np._targetCsn) && !np._targetCsn.$proxy
). forEach(np =>
entitySet.append(np.createNavigationPropertyBinding(namespace)));
Schema.append(new Edm.EntityType(v, attributes, properties, entityCsn));
Schema._ec.append(entitySet);
}
if (createEntitySet)
{
let entitySet = new Edm.EntitySet(v, { Name: EntitySetName, EntityType: fqEntityTypeName }, entityCsn);
// V4: Create NavigationPropertyBinding in EntitySet if NavigationProperty is not a Containment
if(options.isV4())
properties.filter(np => np instanceof Edm.NavigationProperty && !np.isContainment()).
forEach(np => entitySet.append(np.createNavigationPropertyBinding(namespace)));
Schema._ec.append(entitySet);
// put actions behind entity types in Schema/EntityContainer
edmUtils.forAll(entityCsn.actions, (a, n) => {
(options.isV4()) ? createActionV4(a, n, entityCsn)
: createActionV2(a, n, entityCsn)
});
}
// put actions behind entity types in Schema/EntityContainer
glue.forAll(entityCsn.actions, (a, n) => {
(options.isV4()) ? createActionV4(a, n, entityCsn)
: createActionV2(a, n, entityCsn)
});
}
// add bound/unbound actions/functions for V4
function createActionV4(actionCsn, name, entityCsn=undefined)
{
let iAmAnAction = actionCsn.kind == "action";
// add bound/unbound actions/functions for V4
function createActionV4(actionCsn, name, entityCsn=undefined)
{
let iAmAnAction = actionCsn.kind == "action";
let actionName = actionCsn.name.replace(namespace, '');
let actionName = actionCsn.name.replace(namespace, '');
let attributes = { Name: actionName, IsBound : false };
let attributes = { Name: actionName, IsBound : false };
if(!iAmAnAction)
attributes.IsComposable = false;
if(!iAmAnAction)
attributes.IsComposable = false;
let actionNode = (iAmAnAction) ? new Edm.Action(v, attributes)
: new Edm.FunctionDefinition(v, attributes);
let actionNode = (iAmAnAction) ? new Edm.Action(v, attributes)
: new Edm.FunctionDefinition(v, attributes);
// bpName is eventually used later for EntitySetPath
let bpNameAnno = actionCsn['@cds.odata.bindingparameter.name'];
let bpName = bpNameAnno !== undefined ? (bpNameAnno['='] || bpNameAnno) : 'in';
if(entityCsn != undefined)
{
actionNode.IsBound = true;
// Binding Parameter: 'in' at first position in sequence, this is decisive!
actionNode.append(new Edm.Parameter(v, { Name: "in", Type: fullQualified(entityCsn.name) }, {} ));
}
else // unbound => produce Action/FunctionImport
{
let actionImport = iAmAnAction
? new Edm.ActionImport(v, { Name: actionName, Action : fullQualified(actionName) })
: new Edm.FunctionImport(v, { Name: actionName, Function : fullQualified(actionName) });
if(entityCsn != undefined)
{
actionNode.IsBound = true;
let bpType = actionCsn['@cds.odata.bindingparameter.collection'] ?
'Collection('+fullQualified(entityCsn.name)+')' : fullQualified(entityCsn.name);
// Binding Parameter: 'in' at first position in sequence, this is decisive!
actionNode.append(new Edm.Parameter(v, { Name: bpName, Type: bpType }, {} ));
}
else // unbound => produce Action/FunctionImport
{
let actionImport = iAmAnAction
? new Edm.ActionImport(v, { Name: actionName, Action : fullQualified(actionName) })
: new Edm.FunctionImport(v, { Name: actionName, Function : fullQualified(actionName) });
let rt = actionCsn.returns && (actionCsn.returns.type || actionCsn.returns.items.type);
if(rt) // add EntitySet attribute only if return type is a non abstract entity
{
let definition = model.definitions[rt];
if(definition && definition.kind == 'entity' && !definition.abstract)
let rt = actionCsn.returns && ((actionCsn.returns.items && actionCsn.returns.items.type) || actionCsn.returns.type);
if(rt) // add EntitySet attribute only if return type is a non abstract entity
{
actionImport.EntitySet = rt.replace(namespace, '');
let definition = model.definitions[rt];
if(definition && definition.kind == 'entity' && !definition.abstract)
{
actionImport.EntitySet = rt.replace(namespace, '');
}
}
Schema._ec.append(actionImport);
}
Schema._ec.append(actionImport);
}
// Parameter Nodes
glue.forAll(actionCsn.params, (parameterCsn, parameterName) => {
actionNode.append(new Edm.Parameter(v, { Name: parameterName }, parameterCsn ));
});
// Parameter Nodes
edmUtils.forAll(actionCsn.params, (parameterCsn, parameterName) => {
actionNode.append(new Edm.Parameter(v, { Name: parameterName }, parameterCsn ));
});
// return type if any
if(actionCsn.returns) {
actionNode._returnType = new Edm.ReturnType(v, actionCsn.returns, fullQualified);
// if binding type matches return type add attribute EntitySetPath
if(entityCsn && fullQualified(entityCsn.name) === actionNode._returnType._type) {
actionNode.EntitySetPath = 'in';
// return type if any
if(actionCsn.returns) {
actionNode._returnType = new Edm.ReturnType(v, actionCsn.returns, fullQualified);
// if binding type matches return type add attribute EntitySetPath
if(entityCsn != undefined && fullQualified(entityCsn.name) === actionNode._returnType._type) {
actionNode.EntitySetPath = bpName;
}
}
Schema.addAction(actionNode);
}
Schema.addAction(actionNode);
}
// add bound/unbound actions/functions for V2
function createActionV2(actionCsn, name, entityCsn=undefined)
{
let functionImport = new Edm.FunctionImport(v, { Name: name.replace(namespace, '') } );
// add bound/unbound actions/functions for V2
function createActionV2(actionCsn, name, entityCsn=undefined)
{
let functionImport = new Edm.FunctionImport(v, { Name: name.replace(namespace, '') } );
// inserted now to maintain attribute order with old odata generator...
/*
V2 says (p33):
* If the return type of FunctionImport is a collection of entities, the EntitySet
attribute is defined.
* If the return type of FunctionImport is of ComplexType or scalar type,
the EntitySet attribute cannot be defined.
The spec doesn't mention single ET: Ralf Handls confirmed that there is a gap
in the spec and advised mention it as in V4
*/
// inserted now to maintain attribute order with old odata generator...
/*
V2 says (p33):
* If the return type of FunctionImport is a collection of entities, the EntitySet
attribute is defined.
* If the return type of FunctionImport is of ComplexType or scalar type,
the EntitySet attribute cannot be defined.
The spec doesn't mention single ET: Ralf Handls confirmed that there is a gap
in the spec and advised mention it as in V4
*/
let rt = actionCsn.returns && (actionCsn.returns.type || actionCsn.returns.items.type);
if(rt) // add EntitySet attribute only if return type is an entity
{
let defintion = model.definitions[rt];
if(defintion && glue.isEntityOrView(defintion))
let rt = actionCsn.returns && ((actionCsn.returns.items && actionCsn.returns.items.type) || actionCsn.returns.type);
if(rt) // add EntitySet attribute only if return type is an entity
{
functionImport.EntitySet = rt.replace(namespace, '');
let defintion = model.definitions[rt];
if(defintion && edmUtils.isEntityOrView(defintion))
{
functionImport.EntitySet = rt.replace(namespace, '');
}
}
}
if(actionCsn.returns)
functionImport.ReturnType = getReturnType(actionCsn);
if(actionCsn.returns)
functionImport.ReturnType = getReturnType(actionCsn);
if(actionCsn.kind == 'function')
functionImport.setXml( {'m:HttpMethod': 'GET' });
else if(actionCsn.kind == 'action')
functionImport.setXml( {'m:HttpMethod': 'POST'});
else
throw Error('Please debug me: Neither function nor action');
if(actionCsn.kind == 'function')
functionImport.setXml( {'m:HttpMethod': 'GET' });
else if(actionCsn.kind == 'action')
functionImport.setXml( {'m:HttpMethod': 'POST'});
else
throw Error('Please debug me: Neither function nor action');
if(entityCsn != undefined)
{
// Make bound function names always unique as per Ralf's recommendation
functionImport.setXml( {'sap:action-for': fullQualified(entityCsn.name) } );
functionImport.Name = entityCsn.name.replace(namespace, '') + '_' + functionImport.Name;
if(entityCsn != undefined)
{
// Make bound function names always unique as per Ralf's recommendation
functionImport.setXml( {'sap:action-for': fullQualified(entityCsn.name) } );
functionImport.Name = entityCsn.name.replace(namespace, '') + '_' + functionImport.Name;
// Binding Parameter: Primary Keys at first position in sequence, this is decisive!
// V2 XML: Nullable=false is set because we reuse the primary key property for the parameter
glue.foreach(entityCsn.elements,
elementCsn => elementCsn.key && !glue.isAssociationOrComposition(elementCsn),
(elementCsn, elementName) => {
functionImport.append(new Edm.Parameter(v, { Name: elementName }, elementCsn, 'In' ));
}
);
}
// Binding Parameter: Primary Keys at first position in sequence, this is decisive!
// V2 XML: Nullable=false is set because we reuse the primary key property for the parameter
edmUtils.foreach(entityCsn.elements,
elementCsn => elementCsn.key && !edmUtils.isAssociationOrComposition(elementCsn),
(elementCsn, elementName) => {
functionImport.append(new Edm.Parameter(v, { Name: elementName }, elementCsn, 'In' ));
}
);
}
// is this still required?
for (let p in actionCsn)
if (p.match(/^@sap./))
functionImport.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : actionCsn[p] });
// is this still required?
for (let p in actionCsn)
if (p.match(/^@sap./))
functionImport.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : actionCsn[p] });
// then append all other parameters
// 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
glue.forAll(actionCsn.params, (parameterCsn, parameterName) => {
functionImport.append(new Edm.Parameter(v, { Name: parameterName }, parameterCsn, 'In' ));
});
// then append all other parameters
// 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
edmUtils.forAll(actionCsn.params, (parameterCsn, parameterName) => {
functionImport.append(new Edm.Parameter(v, { Name: parameterName }, parameterCsn, 'In' ));
});
Schema._ec.append(functionImport);
}
Schema._ec.append(functionImport);
}
function getReturnType(action)
{
// it is safe to assume that either type or items.type are set
let type = action.returns && (action.returns.type || action.returns.items.type);
if(type && type.startsWith('cds.'))
type = glue.mapCdsToEdmType(type, options.isV2());
function getReturnType(action)
{
// it is safe to assume that either type or items.type are set
let returns = action.returns.items || action.returns;
let type = returns.type;
if(type && isBuiltinType(type))
type = edmUtils.mapCdsToEdmType(returns, signal, error, options.isV2());
if(type && action.returns.items)
type = `Collection(${type})`
if(type && action.returns.items)
type = `Collection(${type})`
return type;
}
return type;
}
// returns a [ [ Edm Properties ], boolean hasStream ]
// array of Edm Properties
// boolean hasSstream : true if at least one element has @Core.MediaType assignment
function createProperties(parentCsn, prefix='')
{
let props = [];
let hasStream = false;
glue.forAll(parentCsn.elements, (elementCsn, elementName) =>
// returns a [ [ Edm Properties ], boolean hasStream ]
// array of Edm Properties
// boolean hasSstream : true if at least one element has @Core.MediaType assignment
function createProperties(parentCsn)
{
if(elementCsn._parent == undefined)
setProp(elementCsn, '_parent', parentCsn);
if(glue.isAssociationOrComposition(elementCsn))
let props = [];
let hasStream = false;
edmUtils.forAll(parentCsn.elements, (elementCsn, elementName) =>
{
// Foreign keys are part of the generic elementCsn.elements property creation
if(elementCsn._parent == undefined)
setProp(elementCsn, '_parent', parentCsn);
// This is the V4 edmx:NavigationProperty
// gets rewritten for V2 in addAssocations()
if(!elementCsn._ignore) {
if(edmUtils.isAssociationOrComposition(elementCsn))
{
// Foreign keys are part of the generic elementCsn.elements property creation
// suppress navprop creation only if @odata.navigable:false is not annotated.
// (undefined !== false) still evaluates to true
if (elementCsn['@odata.navigable'] !== false)
{
let navProp = new Edm.NavigationProperty(v, {
Name: elementName,
Type: fullQualified(elementCsn.target.name)
}, elementCsn);
// This is the V4 edmx:NavigationProperty
// gets rewritten for V2 in addAssocations()
props.push(navProp);
// save the navProp in the global array for late constraint building
navigationProperties.push(navProp);
}
}
else if(glue.isStructuredArtifact(elementCsn))
{
// TODO: Makeover anonymous structured types
let anonymousComplexType = createAnonymousComplexType(elementCsn, prefix);
props.push(new Edm.Property(v, {
Name: elementCsn.name,
Type: fullQualified(anonymousComplexType.Name)
}, elementCsn));
}
else
{
// CDXCORE-CDXCORE-173
// V2: filter @Core.MediaType
// V4: filter foreignKey elements of Container associations
let isContainerAssoc = false;
if(elementCsn['@odata.foreignKey4']) {
let assoc = parentCsn.elements[elementCsn['@odata.foreignKey4']];
isContainerAssoc = assoc._isToContainer || assoc['@odata.contained'];
}
// suppress navprop creation only if @odata.navigable:false is not annotated.
// (undefined !== false) still evaluates to true
if (!elementCsn.target.abstract && elementCsn['@odata.navigable'] !== false)
{
let navProp = new Edm.NavigationProperty(v, {
Name: elementName,
Type: fullQualified(elementCsn.target.name)
}, elementCsn);
if(!(options.isV2() && elementCsn['@Core.MediaType']) &&
!(options.isV4() && isContainerAssoc))
props.push(new Edm.Property(v, { Name: elementName }, elementCsn));
else
{
hasStream = true;
// CDXCORE-CDXCORE-177: remove elementCsn from elements dictionary
delete parentCsn.elements[elementName];
}
}
});
return [ props, hasStream ];
}
props.push(navProp);
// save the navProp in the global array for late constraint building
navigationProperties.push(navProp);
}
}
else if(!elementCsn['@cds.api.ignore'] || elementCsn['@cds.api.ignore'] !== true)
{
// CDXCORE-CDXCORE-173
// V2: filter @Core.MediaType
// V4: filter foreignKey elements of Container associations
let isContainerAssoc = false;
if(elementCsn['@odata.foreignKey4']) {
function createAnonymousComplexType(element, prefix = '') {
// Add named ComplexType as auxiliary for anonyous inner struct
let name = element.name.replace(namespace, '')
let elements = parentCsn.elements;
let assoc = undefined;
let paths = elementCsn['@odata.foreignKey4'].split('.')
for(let p of paths) {
assoc = elements[p];
if(assoc) // could be that the @odata.foreignKey4 was propagated...
elements = assoc.elements;
}
// array of complex type not yet allowed
let typecsn = /*element.items ||*/ element;
if(assoc)
//let assoc = parentCsn.elements[elementCsn['@odata.foreignKey4']];
isContainerAssoc = assoc._isToContainer || assoc['@odata.contained'];
}
if ( options.isV2() && elementCsn['@Core.MediaType']
|| options.isV4() && isContainerAssoc && !elementCsn.key ) {
hasStream = true;
// CDXCORE-CDXCORE-177: remove elementCsn from elements dictionary
delete parentCsn.elements[elementName];
} else
props.push(new Edm.Property(v, { Name: elementName }, elementCsn));
}
}
let type = new Edm.ComplexType({
Name: prefix.replace(/\//g, COMPLEXTYPE_TRENNER) + name
}).append(
...(createProperties(typecsn, prefix + name + '/')[0])
);
// collect anonymous complex type in Auxiliaries array, they will
// be added to the Schema later in createComplexType...
(Schema.Auxillaries || Schema.set({
Auxillaries: []
}).Auxillaries).push(type)
return type
}
});
return [ props, hasStream ];
}
function createComplexType(structuredTypeCsn)
{
// V4 attributes: Name, BaseType, Abstract, OpenType
let attributes = { Name: structuredTypeCsn.name.replace(namespace, '') };
function createComplexType(structuredTypeCsn)
{
// V4 attributes: Name, BaseType, Abstract, OpenType
let attributes = { Name: structuredTypeCsn.name.replace(namespace, '') };
let complexType = new Edm.ComplexType(v, attributes, structuredTypeCsn);
let elementsCsn = structuredTypeCsn.items || structuredTypeCsn;
complexType.append(...(createProperties(elementsCsn)[0]));
Schema.append(complexType);
let complexType = new Edm.ComplexType(v, attributes, structuredTypeCsn);
let elementsCsn = structuredTypeCsn.items || structuredTypeCsn;
complexType.append(...(createProperties(elementsCsn)[0]));
Schema.append(complexType);
}
// append new anonymous complex types to the schema (and overwrite existing ones...)
if (Schema.Auxillaries)
// V4 <TypeDefintion>
function createTypeDefinition(typeCsn)
{
Schema.append(...Schema.Auxillaries)
// derived types are already resolved to base types
let typeDef;
let props = { Name: typeCsn.name.replace(namespace, '') };
if((typeCsn.items && typeCsn.items.enum) || typeCsn.enum)
typeDef = new Edm.EnumType(v, props, typeCsn);
else
typeDef = new Edm.TypeDefinition(v, props, typeCsn );
Schema.append(typeDef);
}
}
// V4 <TypeDefintion>
function createTypeDefinition(typeCsn)
{
// derived types are already resolved to base types
let typeDef;
let props = { Name: typeCsn.name.replace(namespace, '') };
if((typeCsn.items && typeCsn.items.enum) || typeCsn.enum)
typeDef = new Edm.EnumType(v, props, typeCsn);
else
typeDef = new Edm.TypeDefinition(v, props, typeCsn );
Schema.append(typeDef);
}
/*
* addAssociation() constructs a V2 association.
* In V4 all this has been simplified very much, the only thing actually left over is
* <ReferentialConstriant> that is then a sub element to <NavigationProperty>.
* However, referential constraints are substantially different to its V2 counterpart,
* so it is better to reimplement proper V4 construction of<NavigationProperty> in a separate
* function.
*
* This method does:
* rewrite <NavigationProperty> attributes to be V2 compliant
* add <Association> elements to the schema
* add <End>, <ReferentialConstraint>, <Dependent> and <Principal> sub elements to <Association>
* add <AssociationSet> to the EntityContainer for each <Association>
*/
function addAssociation(navigationProperty)
{
let constraints = navigationProperty._csn._constraints;
let parentName = navigationProperty._csn._parent.name.replace(namespace, '');
let plainAssocName = parentName + NAVPROP_TRENNER + navigationProperty.Name.replace(VALUELIST_NAVPROP_PREFIX, '');
let assocName = plainAssocName;
let i = 1;
while(NamesInSchemaXRef[assocName] !== undefined) {
assocName = plainAssocName + '_' + i++;
}
/*
* addAssociation() constructs a V2 association.
* In V4 all this has been simplified very much, the only thing actually left over is
* <ReferentialConstriant> that is then a sub element to <NavigationProperty>.
* However, referential constraints are substantially different to its V2 counterpart,
* so it is better to reimplement proper V4 construction of<NavigationProperty> in a separate
* function.
*
* This method does:
* rewrite <NavigationProperty> attributes to be V2 compliant
* add <Association> elements to the schema
* add <End>, <ReferentialConstraint>, <Dependent> and <Principal> sub elements to <Association>
* add <AssociationSet> to the EntityContainer for each <Association>
*/
function addAssociation(navigationProperty)
{
let constraints = navigationProperty._referentialConstraints;
let parentName = navigationProperty._csn._parent.name.replace(namespace, '');
let assocName = parentName + NAVPROP_TRENNER + navigationProperty.Name.replace(VALUELIST_NAVPROP_PREFIX, '');
let fromRole = parentName;
let toRole = navigationProperty.Type.replace(alias, ''); // <= navprops type should be prefixed with alias
let fromEntityType = fromRole;
let toEntityType = toRole;
let fromRole = parentName;
let toRole = navigationProperty.Type.replace(alias, ''); // <= navprops type should be prefixed with alias
let fromEntityType = fromRole;
let toEntityType = toRole;
// The entity set name may not be the same as the type name (parameterized entities have
// differing set names (<T>Parameters => <T>, <T>Type => <T>Set)
let fromEntitySet = ( navigationProperty._csn._parent.setName || fromEntityType).replace(namespace, '');
let toEntitySet = (navigationProperty._targetCsn.setName || toEntityType).replace(namespace, '');
// from and to roles must be distinguishable (in case of self association entity E { toE: association to E; ... })
// from and to roles must be distinguishable (in case of self association entity E { toE: association to E; ... })
if(fromRole === toRole) {
if(constraints._originAssocCsn)
fromRole += '1';
else
toRole += '1';
}
if(fromRole === toRole) {
if(constraints._originAssocCsn)
fromRole += '1';
else
toRole += '1';
}
// add V2 attributes to navigationProperty
navigationProperty.Relationship = fullQualified(assocName);
navigationProperty.FromRole = fromRole;
navigationProperty.ToRole = toRole;
// add V2 attributes to navigationProperty
navigationProperty.Relationship = fullQualified(assocName);
navigationProperty.FromRole = fromRole;
navigationProperty.ToRole = toRole;
// remove V4 attributes
if(navigationProperty.Type != undefined)
delete navigationProperty.Type;
/* V2 erwaehnt kein Nullable genauso wie fuer Properties, TNT hats aber drin...
if(navigationProperty.Nullable != undefined)
delete navigationProperty.Nullable;
*/
if(navigationProperty.Partner != undefined)
delete navigationProperty.Partner;
if(navigationProperty.ContainsTarget != undefined)
delete navigationProperty.ContainsTarget;
// remove V4 attributes
if(navigationProperty.Type != undefined)
delete navigationProperty.Type;
if(navigationProperty.Partner != undefined)
delete navigationProperty.Partner;
if(navigationProperty.ContainsTarget != undefined)
delete navigationProperty.ContainsTarget;
/*
If NavigationProperty is a backlink association (constraints._originAssocCsn is set), then there are two options:
1) Counterpart NavigationProperty exists and is responsible to create the edm:Association element which needs to
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
of the origin association needs to be created as if it would have been already available in case (1).
*/
/*
If NavigationProperty is a backlink association (constraints._originAssocCsn is set), then there are two options:
1) Counterpart NavigationProperty exists and is responsible to create the edm:Association element which needs to
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
of the origin association needs to be created as if it would have been already available in case (1).
*/
let reuseAssoc = false;
let forwardAssocCsn = constraints._originAssocCsn;
if(forwardAssocCsn)
{
// This is a backlink, swap the roles and types, rewrite assocName
[ fromRole, toRole ] = [ toRole, fromRole ];
[ fromEntityType, toEntityType ] = [ toEntityType, fromEntityType ];
let reuseAssoc = false;
let forwardAssocCsn = constraints._originAssocCsn;
if(forwardAssocCsn)
{
// This is a backlink, swap the roles and types, rewrite assocName
[ fromRole, toRole ] = [ toRole, fromRole ];
[ fromEntityType, toEntityType ] = [ toEntityType, fromEntityType ];
[ fromEntitySet, toEntitySet ] = [ toEntitySet, fromEntitySet ];
parentName = forwardAssocCsn._parent.name.replace(namespace, '');
assocName = parentName + NAVPROP_TRENNER + forwardAssocCsn.name.replace(VALUELIST_NAVPROP_PREFIX, '');
navigationProperty.Relationship = fullQualified(assocName)
parentName = forwardAssocCsn._parent.name.replace(namespace, '');
assocName = plainAssocName = parentName + NAVPROP_TRENNER + forwardAssocCsn.name.replace(VALUELIST_NAVPROP_PREFIX, '');
i = 1;
while(NamesInSchemaXRef[assocName] !== undefined && !(NamesInSchemaXRef[assocName][0] instanceof Edm.Association)) {
assocName = plainAssocName + '_' + i++;
}
reuseAssoc = !!forwardAssocCsn._NavigationProperty;
constraints = navigationProperty.getReferentialConstraints(forwardAssocCsn);
}
navigationProperty.Relationship = fullQualified(assocName)
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.
reuseAssoc = !!forwardAssocCsn._NavigationProperty;
constraints = edmUtils.getReferentialConstraints(forwardAssocCsn, signal, warning);
constraints._multiplicity = edmUtils.determineMultiplicity(forwardAssocCsn);
}
if(!forwardAssocCsn._NavigationProperty._edmAssociation && navigationProperty._csn.type == 'cds.Composition')
if(reuseAssoc)
{
// TODO: to be specified via @sap.on.delete
forwardAssocCsn._NavigationProperty.set( { _OnDeleteSrcEnd: new Edm.OnDelete(v, { Action: 'Cascade' }) } );
// 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;
}
return;
}
// 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) ],
[ toRole, fullQualified(toEntityType) ],
constraints.multiplicity );
// 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) ],
[ toRole, fullQualified(toEntityType) ],
constraints._multiplicity );
if(NamesInSchemaXRef[assocName] === undefined) {
NamesInSchemaXRef[assocName] = [ navigationProperty._edmAssociation ];
}
else {
navigationProperty._edmAssociation.push(navigationProperty._edmAssociation);
}
// Add ReferentialConstraints if any
if(!navigationProperty._isCollection && Object.keys(constraints.constraints).length > 0) {
// A managed composition is treated as association
if(navigationProperty._csn.type == 'cds.Composition' && (navigationProperty._csn.on || navigationProperty._csn.onCond)) {
navigationProperty._edmAssociation.append(Edm.ReferentialConstraint.createV2(v,
toRole, fromRole, constraints.constraints));
}
else {
navigationProperty._edmAssociation.append(Edm.ReferentialConstraint.createV2(v,
fromRole, toRole, constraints.constraints));
}
}
// Add ReferentialConstraints if any
if(Object.keys(constraints.constraints).length > 0)
navigationProperty._edmAssociation.append(Edm.ReferentialConstraint.createV2(v,
fromRole, toRole, constraints.constraints));
Schema.append(navigationProperty._edmAssociation);
if(!navigationProperty._targetCsn.$proxy) {
let assocSet = new Edm.AssociationSet(v, { Name: assocName, Association: fullQualified(assocName) },
fromRole, toRole, fromEntitySet, toEntitySet);
Schema._ec.append(assocSet);
}
}
Schema.append(navigationProperty._edmAssociation);
// produce a full qualified name replacing the namespace with the alias (if provided)
function fullQualified(name)
{
if (name == serviceCsn.name)
return Schema.Alias
else
return alias + name.replace(namespace, '')
}
let assocSet = new Edm.AssociationSet(v, { Name: assocName, Association: fullQualified(assocName) },
fromRole, toRole, fromEntityType, toEntityType);
Schema._ec.append(assocSet);
}
// produce a full qualified name replacing the namespace with the alias (if provided)
function fullQualified(name)
{
if (name == serviceCsn.name)
return Schema.Alias
else
return alias + name.replace(namespace, '')
}
function createAnnotations(edm)
{
let annoEdm;
annoEdm = translate.csn2annotationEdm(model, options);
for(let i = 0; i < annoEdm.getSchemaCount(); i++)
function createAnnotations(edm)
{
edm.setAnnotations(annoEdm.getAnnotations(i), i);
let annoEdm = translate.csn2annotationEdm(model, serviceName, options);
for(let i = 0; i < annoEdm.getSchemaCount(); i++)
{
edm.setAnnotations(annoEdm.getAnnotations(i), i);
}
edm._defaultRefs = annoEdm._defaultRefs;
}
}
}
module.exports = { csn2edm, csn2edmAll };
'use strict'
const glue = require('./glue.js')
const edmUtils = require('./edmUtils.js');
class Node
{
constructor(v, attributes=Object.create(null), csn=undefined)
module.exports = function (options, signal, error) {
class Node
{
if(!attributes || typeof attributes !== 'object')
throw Error('Please debug me: attributes must be a dictionary');
if(!(v instanceof Array))
throw Error('Please debug me: v is either undefined or not an array: ' + v);
if(v.filter(v=>v).length != 1)
throw Error('Please debug me: exactly one version must be set');
Object.assign(this, attributes);
this.set({ _children: [], _xmlOnlyAttributes: Object.create(null), _jsonOnlyAttributes: Object.create(null), _v: v });
constructor(v, attributes=Object.create(null), csn=undefined)
{
if(!attributes || typeof attributes !== 'object')
throw Error('Please debug me: attributes must be a dictionary');
if(!(v instanceof Array))
throw Error('Please debug me: v is either undefined or not an array: ' + v);
if(v.filter(v=>v).length != 1)
throw Error('Please debug me: exactly one version must be set');
Object.assign(this, attributes);
this.set({ _children: [], _xmlOnlyAttributes: Object.create(null), _jsonOnlyAttributes: Object.create(null), _v: v, _ignoreChildren: false });
if(this.v2)
this.setSapVocabularyAsAttributes(csn);
}
if(this.v2)
this.setSapVocabularyAsAttributes(csn);
}
get v2() { return this._v[0] }
get v4() { return this._v[1] }
get v2() { return this._v[0] }
get v4() { return this._v[1] }
get kind() {
return this.constructor.name
}
get kind() {
return this.constructor.name
}
// set's additional properties that are invisible for the iterators
set(attributes)
{
if(!attributes || typeof attributes !== 'object')
throw Error('Please debug me: attributes must be a dictionary');
let newAttributes = Object.create(null);
for (let p in attributes) newAttributes[p] = {
value: attributes[p],
configurable: true,
enumerable: false,
writable: true
// set's additional properties that are invisible for the iterators
set(attributes)
{
if(!attributes || typeof attributes !== 'object')
throw Error('Please debug me: attributes must be a dictionary');
let newAttributes = Object.create(null);
for (let p in attributes) newAttributes[p] = {
value: attributes[p],
configurable: true,
enumerable: false,
writable: true
}
return Object.defineProperties(this, newAttributes)
}
return Object.defineProperties(this, newAttributes)
}
// set properties that should only appear in the XML representation
setXml(attributes)
{
if(!attributes || typeof attributes !== 'object')
throw Error('Please debug me: attributes must be a dictionary');
return Object.assign(this._xmlOnlyAttributes, attributes);
}
// set properties that should only appear in the XML representation
setXml(attributes)
{
if(!attributes || typeof attributes !== 'object')
throw Error('Please debug me: attributes must be a dictionary');
return Object.assign(this._xmlOnlyAttributes, attributes);
}
// set properties that should only appear in the JSON representation
// today JSON attributes are not rendered in toJSONattributes()
setJSON(attributes)
{
if(!attributes || typeof attributes !== 'object')
throw Error('Please debug me: attributes must be a dictionary');
return Object.assign(this._jsonOnlyAttributes, attributes);
}
// set properties that should only appear in the JSON representation
// today JSON attributes are not rendered in toJSONattributes()
setJSON(attributes)
{
if(!attributes || typeof attributes !== 'object')
throw Error('Please debug me: attributes must be a dictionary');
return Object.assign(this._jsonOnlyAttributes, attributes);
}
append(...children)
{
// remove undefined entries
this._children.push(...children.filter(c => c));
return this
}
append(...children)
{
// remove undefined entries
this._children.push(...children.filter(c => c));
return this
}
// virtual
toJSON()
{
let json = Object.create(null);
// $kind Property MAY be omitted in JSON for performance reasons
if(![ 'Property', 'EntitySet', 'ActionImport', 'FunctionImport', 'Singleton', 'Schema' ].includes(this.kind))
json['$Kind'] = this.kind;
// virtual
toJSON()
{
let json = Object.create(null);
// $kind Property MAY be omitted in JSON for performance reasons
if(![ 'Property', 'EntitySet', 'ActionImport', 'FunctionImport', 'Singleton', 'Schema' ].includes(this.kind))
json['$Kind'] = this.kind;
this.toJSONattributes(json);
this.toJSONchildren(json);
return json
}
// virtual
toJSONattributes(json)
{
for (let p in this)
this.toJSONattributes(json);
this.toJSONchildren(json);
return json
}
// virtual
toJSONattributes(json)
{
if (p != 'Name')
json[p[0] == '@' ? p : '$' + p] = this[p];
for (let p in this)
{
if (p != 'Name')
json[p[0] == '@' ? p : '$' + p] = this[p];
}
return json;
}
return json;
}
// virtual
toJSONchildren(json)
{
// any child with a Name should be added by it's name into the JSON object
// all others must overload toJSONchildren()
this._children.filter(c => c.Name).forEach(c => json[c.Name] = c.toJSON());
}
// virtual
toJSONchildren(json)
{
// any child with a Name should be added by it's name into the JSON object
// all others must overload toJSONchildren()
this._children.filter(c => c.Name).forEach(c => json[c.Name] = c.toJSON());
}
// virtual
toXML(indent = '', what='all')
{
let kind = this.kind;
let head = indent + "<" + kind;
// virtual
toXML(indent = '', what='all')
{
let kind = this.kind;
let head = indent + "<" + kind;
head += this.toXMLattributes();
head += this.toXMLattributes();
let inner = this.innerXML(indent + ' ', what)
if (inner.length < 1) {
head += "/>"
let inner = this.innerXML(indent + ' ', what)
if (inner.length < 1) {
head += "/>"
}
else if (inner.length < 77 && inner.indexOf('<') < 0) {
head += ">" + inner.slice(indent.length + 1, -1) + "</" + kind + ">"
} else {
head += ">\n" + inner + indent + "</" + kind + ">"
}
return head;
}
else if (inner.length < 77 && inner.indexOf('<') < 0) {
head += ">" + inner.slice(indent.length + 1, -1) + "</" + kind + ">"
} else {
head += ">\n" + inner + indent + "</" + kind + ">"
// virtual
toXMLattributes()
{
let tmpStr = ""
for (let p in this) {
if (typeof this[p] !== 'object')
tmpStr += ' ' + p + '="' + escapeString(this[p]) + '"'
}
for (let p in this._xmlOnlyAttributes)
{
if (typeof this._xmlOnlyAttributes[p] !== 'object')
tmpStr += ' ' + p + '="' + escapeString(this._xmlOnlyAttributes[p]) + '"'
}
return tmpStr;
function escapeString(s)
{
// first regex: replace & if not followed by apos; or quot; or gt; or lt; or amp; or #
// Do not escape > as it is a marker for {bi18n>...} translated string values
return (typeof s === 'string') ? s.replace(/&(?!(?:apos|quot|[gl]t|amp);|#)/g, '&amp;')./*.replace(/>/g, '&gt;').*/replace(/</g, '&lt;').replace(/"/g, '&quot;') : s;
}
}
return head;
}
// virtual
toXMLattributes()
{
let tmpStr = ""
for (let p in this) {
if (typeof this[p] !== 'object')
tmpStr += ' ' + p + '="' + escapeString(this[p]) + '"'
}
for (let p in this._xmlOnlyAttributes)
// virtual
innerXML(indent, what='all')
{
if (typeof this._xmlOnlyAttributes[p] !== 'object')
tmpStr += ' ' + p + '="' + escapeString(this._xmlOnlyAttributes[p]) + '"'
let xml = "";
if(options.testMode===true) // sort nodes only in test mode
this._children.sort(compareNodes)
this._children.forEach(e =>
xml += e.toXML(indent, what) + '\n');
return xml;
}
return tmpStr;
function escapeString(s)
// virtual
setSapVocabularyAsAttributes(csn)
{
// 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;
if(csn)
{
for (let p in csn)
if (p.match(/^@sap./))
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn[p] } );
}
}
}
// virtual
innerXML(indent, what='all')
class Reference extends Node
{
let xml = "";
this._children.forEach(e =>
xml += e.toXML(indent, what) + '\n');
return xml;
}
// virtual
setSapVocabularyAsAttributes(csn)
{
if(csn)
constructor(v, details)
{
for (let p in csn)
if (p.match(/^@sap./))
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn[p] } );
super(v, details);
if(this.v2)
this['xmlns:edmx'] = 'http://docs.oasis-open.org/odata/ns/edmx';
}
}
}
class Reference extends Node
{
constructor(v, details)
{
super(v, details);
if(this.v2)
this['xmlns:edmx'] = 'http://docs.oasis-open.org/odata/ns/edmx';
}
get kind() { return 'edmx:Reference' }
get kind() { return 'edmx:Reference' }
toJSON()
{
let json = Object.create(null);
let includes = [];
toJSON()
{
let json = Object.create(null);
let includes = [];
this._children.forEach(c => includes.push(c.toJSON()));
if(includes.length > 0)
json['$Include'] = includes;
return json;
if(options.testMode===true) // sort nodes only in test mode
this._children.sort(compareNodes)
this._children.forEach(c => includes.push(c.toJSON()));
if(includes.length > 0)
json['$Include'] = includes;
return json;
}
}
}
class Include extends Node
{
get kind() { return 'edmx:Include' }
toJSON()
class Include extends Node
{
let json = Object.create(null);
return this.toJSONattributes(json);
get kind() { return 'edmx:Include' }
toJSON()
{
let json = Object.create(null);
return this.toJSONattributes(json);
}
}
}
class Schema extends Node
{
constructor(v, ns, alias=undefined, annotations=[], withEntityContainer=true)
class Schema extends Node
{
let props = Object.create(null);
props.Namespace = ns;
if(alias != undefined)
props.Alias = alias;
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" } );
constructor(v, ns, alias=undefined, annotations=[], withEntityContainer=true)
{
let props = Object.create(null);
props.Namespace = ns;
if(alias != undefined)
props.Alias = alias;
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" } );
if(withEntityContainer)
if(withEntityContainer)
{
let ecprops = { Name: 'EntityContainer' };
let ec = new EntityContainer(v, ecprops );
if(this.v2)
ec.setXml( { 'm:IsDefaultEntityContainer': true } );
// append for rendering, ok ec has Name
this.append(ec);
// set as attribute for later access...
this.set({ _ec : ec })
}
}
// hold actions and functions in V4
addAction(action)
{
let ecprops = { Name: 'EntityContainer' };
let ec = new EntityContainer(v, ecprops );
if(this.v2)
ec.setXml( { 'm:IsDefaultEntityContainer': true } );
// append for rendering, ok ec has Name
this.append(ec);
// set as attribute for later access...
this.set({ _ec : ec })
if(this._actions[action.Name])
this._actions[action.Name].push(action);
else
this._actions[action.Name] = [action];
}
}
// hold actions and functions in V4
addAction(action)
{
if(this._actions[action.Name])
this._actions[action.Name].push(action);
else
this._actions[action.Name] = [action];
}
setAnnotations(annotations)
{
if(annotations instanceof Array && annotations.length > 0)
this._annotations.push(...annotations);
}
innerXML(indent, what)
{
let xml = '';
if(what=='metadata' || what=='all')
{
xml += super.innerXML(indent);
edmUtils.forAll(this._actions, actionArray => {
actionArray.forEach(action => {
xml += action.toXML(indent, what) + '\n'; });
});
}
if(what=='annotations' || what=='all')
{
if(this._annotations.length > 0) {
this._annotations.filter(a => a.Term).forEach(a => xml += a.toXML(indent) + '\n');
this._annotations.filter(a => a.Target).forEach(a => xml += a.toXML(indent) + '\n');
}
}
return xml;
}
setAnnotations(annotations)
{
if(annotations instanceof Array && annotations.length > 0)
this._annotations.push(...annotations);
}
innerXML(indent, what)
{
let xml = '';
if(what=='metadata' || what=='all')
// no $Namespace
toJSONattributes(json)
{
xml += super.innerXML(indent);
glue.forAll(this._actions, actionArray => {
actionArray.forEach(action => {
xml += action.toXML(indent, what) + '\n'; });
});
for (let p in this)
{
if (p != 'Name' && p != 'Namespace')
json[p[0] == '@' ? p : '$' + p] = this[p];
}
return json;
}
if(what=='annotations' || what=='all')
toJSONchildren(json)
{
// 'edmx:DataServices' should not appear in JSON
super.toJSONchildren(json);
if(this._annotations.length > 0) {
this._annotations.filter(a => a.Term).forEach(a => xml += a.toXML(indent) + '\n');
this._annotations.filter(a => a.Target).forEach(a => xml += a.toXML(indent) + '\n');
this._annotations.filter(a => a.Term).forEach(a => json['@'+a.Term] = a.toJSON());
let json_Annotations = Object.create(null);
this._annotations.filter(a => a.Target).forEach(a => json_Annotations[a.Target] = a.toJSON());
json['$Annotations'] = json_Annotations;
}
edmUtils.forAll(this._actions, (actionArray, actionName) => {
json[actionName] = [];
actionArray.forEach(action => {
json[actionName].push(action.toJSON());
});
});
return json;
}
return xml;
}
// no $Namespace
toJSONattributes(json)
class DataServices extends Node
{
for (let p in this)
constructor(v, schema)
{
if (p != 'Name' && p != 'Namespace')
json[p[0] == '@' ? p : '$' + p] = this[p];
super(v);
this.append(schema);
if(this.v2)
this.setXml( { 'm:DataServiceVersion': '2.0' } )
}
return json;
}
toJSONchildren(json)
{
// 'edmx:DataServices' should not appear in JSON
super.toJSONchildren(json);
if(this._annotations.length > 0) {
this._annotations.filter(a => a.Term).forEach(a => json['@'+a.Term] = a.toJSON());
let json_Annotations = Object.create(null);
this._annotations.filter(a => a.Target).forEach(a => json_Annotations[a.Target] = a.toJSON());
json['$Annotations'] = json_Annotations;
get kind() { return 'edmx:DataServices'; }
toJSONchildren(json)
{
// 'edmx:DataServices' should not appear in JSON
if(options.testMode===true) // sort nodes only in test mode
this._children.sort(compareNodes)
this._children.forEach(s => json[s.Namespace] = s.toJSON());
return json;
}
glue.forAll(this._actions, (actionArray, actionName) => {
json[actionName] = [];
actionArray.forEach(action => {
json[actionName].push(action.toJSON());
});
});
return json;
}
}
/* <edmx:Edmx> must contain exactly one <edmx:DataServices> with 1..n <edm:Schema> elements
may contain 0..n <edmx:Reference> elements
class DataServices extends Node
{
constructor(v, schema)
{
super(v);
this.append(schema);
if(this.v2)
this.setXml( { 'm:DataServiceVersion': '2.0' } )
}
For Odata 1.0..3.0 EDMX is an independent container with its own version 1.0.
The OData version can be found at the DataServices Version attribute.
From OData 4.0 onwards, EDMX is no longer a separate 'container' object but
is used for OData exclusively. Therefore the version attribute reflects the
OData version
*/
get kind() { return 'edmx:DataServices'; }
toJSONchildren(json)
class Edm extends Node
{
// 'edmx:DataServices' should not appear in JSON
this._children.forEach(s => json[s.Namespace] = s.toJSON());
return json;
}
}
constructor(v, service)
{
super(v, { Version : (v[1]) ? '4.0' : '1.0' });
this.set( { _service: service, _defaultRefs: [] } );
/* <edmx:Edmx> must contain exactly one <edmx:DataServices> with 1..n <edm:Schema> elements
may contain 0..n <edmx:Reference> elements
let xmlProps = Object.create(null);
if(this.v4)
{
xmlProps['xmlns:edmx'] = "http://docs.oasis-open.org/odata/ns/edmx";
}
else
{
xmlProps['xmlns:edmx'] = "http://schemas.microsoft.com/ado/2007/06/edmx";
xmlProps['xmlns:m'] = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata";
xmlProps['xmlns:sap'] = "http://www.sap.com/Protocols/SAPData";
}
this.setXml(xmlProps);
}
For Odata 1.0..3.0 EDMX is an independent container with its own version 1.0.
The OData version can be found at the DataServices Version attribute.
From OData 4.0 onwards, EDMX is no longer a separate 'container' object but
is used for OData exclusively. Therefore the version attribute reflects the
OData version
*/
get kind() { return 'edmx:Edmx' }
class EDM extends Node
{
constructor(v, service)
{
super(v, { Version : (v[1]) ? '4.0' : '1.0' });
this.set( { _service: service, _defaultRefs: [] } );
let xmlProps = Object.create(null);
if(this.v4)
hasAnnotations()
{
xmlProps['xmlns:edmx'] = "http://docs.oasis-open.org/odata/ns/edmx";
let rc = false;
this._service._children.forEach(c =>
{ if(c._annotations.length > 0) rc = true; } )
return rc;
}
else
getSchemaCount()
{
xmlProps['xmlns:edmx'] = "http://schemas.microsoft.com/ado/2007/06/edmx";
xmlProps['xmlns:m'] = "http://schemas.microsoft.com/ado/2007/08/dataservices/metadata";
xmlProps['xmlns:sap'] = "http://www.sap.com/Protocols/SAPData";
return this._service._children.length;
}
this.setXml(xmlProps);
}
setDefaultReferences()
{
// default refs are necessary only if edm has annotations
// existing EDMs render default refs always in V4
this._defaultRefs = [];
if(this.v4 || (this.v2 && this.hasAnnotations()))
getAnnotations(schemaIndex=0)
{
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 = 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 = 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 = 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 = 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 = 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 = 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 = 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);
if(this._service && this._service._children[schemaIndex])
return this._service._children[schemaIndex]._annotations;
else
return undefined;
}
}
get kind() { return 'edmx:Edmx' }
hasAnnotations()
{
let rc = false;
this._service._children.forEach(c =>
{ if(c._annotations.length > 0) rc = true; } )
return rc;
}
getSchemaCount()
{
return this._service._children.length;
}
getAnnotations(schemaIndex=0)
{
if(this._service && this._service._children[schemaIndex])
return this._service._children[schemaIndex]._annotations;
else
return undefined;
}
setAnnotations(annotations, schemaIndex=0)
{
if(this._service && this._service._children[schemaIndex])
this._service._children[schemaIndex]._annotations = annotations;
}
toJSON()
{
this.setDefaultReferences();
setAnnotations(annotations, schemaIndex=0)
{
if(this._service && this._service._children[schemaIndex])
this._service._children[schemaIndex]._annotations = annotations;
}
toJSON()
{
let schema = this._service._children[0];
let schema = this._service._children[0];
let json = Object.create(null);
json['$Version'] = this.Version;
json['$EntityContainer'] = schema.Namespace + '.' + schema._ec.Name;
let json = Object.create(null);
json['$Version'] = this.Version;
json['$EntityContainer'] = schema.Namespace + '.' + schema._ec.Name;
let reference_json = Object.create(null);
this._defaultRefs.forEach(r => reference_json[r.Uri] = r.toJSON());
if(options.testMode===true) // sort nodes only in test mode
this._children.sort(compareNodes)
this._children.forEach(r => reference_json[r.Uri] = r.toJSON());
let reference_json = Object.create(null);
this._defaultRefs.forEach(r => reference_json[r.Uri] = r.toJSON());
this._children.forEach(r => reference_json[r.Uri] = r.toJSON());
if(Object.keys(reference_json).length)
json['$Reference'] = reference_json;
if(Object.keys(reference_json).length)
json['$Reference'] = reference_json;
this._service.toJSONattributes(json);
this._service.toJSONchildren(json);
return json;
}
this._service.toJSONattributes(json);
this._service.toJSONchildren(json);
return json;
}
// all(default), metadata, annotations
toXML(what='all')
{
return '<?xml version="1.0" encoding="utf-8"?>\n' + super.toXML('', what);
}
// all(default), metadata, annotations
toXML(what='all')
{
this.setDefaultReferences();
return '<?xml version="1.0" encoding="utf-8"?>\n' + super.toXML('', what);
}
innerXML(indent, what)
{
let xml = "";
innerXML(indent, what)
{
let xml = "";
if(this.v4 || (this.v2 && (what == 'all' || what == 'annotations')))
this._defaultRefs.forEach(r => xml += r.toXML(indent) + '\n');
this._children.forEach(e => xml += e.toXML(indent) + '\n');
xml += this._service.toXML(indent, what) + '\n';
return xml;
if(this.v4 || (this.v2 && (what == 'all' || what == 'annotations')))
this._defaultRefs.forEach(r => xml += r.toXML(indent) + '\n');
if(options.testMode===true) // sort nodes only in test mode
this._children.sort(compareNodes)
this._children.forEach(e => xml += e.toXML(indent) + '\n');
xml += this._service.toXML(indent, what) + '\n';
return xml;
}
}
}
class EntityContainer extends Node {}
class EntityContainer extends Node {}
class EntitySet extends Node
{
// virtual
setSapVocabularyAsAttributes(csn)
class EntitySet extends Node
{
if(csn)
// virtual
setSapVocabularyAsAttributes(csn)
{
for (let p in csn._EntitySetAttributes)
if(csn)
{
if (p.match(/^@sap./))
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn._EntitySetAttributes[p] } );
for (let p in csn._EntitySetAttributes)
{
if (p.match(/^@sap./))
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn._EntitySetAttributes[p] } );
}
}
}
}
toJSONattributes(json)
{
// OASIS ODATA-1231 $Collection=true
json['$Collection']=true;
for (let p in this)
toJSONattributes(json)
{
if (p != 'Name')
// OASIS ODATA-1231 $Collection=true
json['$Collection']=true;
for (let p in this)
{
if(p == 'EntityType') // it's $Type in json
json['$Type'] = this[p];
else
json[p[0] == '@' ? p : '$' + p] = this[p];
if (p != 'Name')
{
if(p == 'EntityType') // it's $Type in json
json['$Type'] = this[p];
else
json[p[0] == '@' ? p : '$' + p] = this[p];
}
}
return json;
}
return json;
}
toJSONchildren(json)
{
let json_navPropBinding = Object.create(null);
this._children.forEach(npb => json_navPropBinding[npb.Path] = npb.Target);
if(Object.keys(json_navPropBinding).length > 0)
json['$NavigationPropertyBinding'] = json_navPropBinding;
toJSONchildren(json)
{
let json_navPropBinding = Object.create(null);
if(options.testMode===true) // sort nodes only in test mode
this._children.sort(compareNodes)
this._children.forEach(npb => json_navPropBinding[npb.Path] = npb.Target);
if(Object.keys(json_navPropBinding).length > 0)
json['$NavigationPropertyBinding'] = json_navPropBinding;
return json;
return json;
}
}
}
class Key extends Node
{
constructor(v, keys)
class Key extends Node
{
super(v);
if (keys && keys.length > 0)
constructor(v, keys)
{
keys.forEach(k => this.append(new PropertyRef(v, k)));
super(v);
if (keys && keys.length > 0)
{
keys.forEach(k => this.append(new PropertyRef(v, k)));
}
}
}
toJSON()
{
let json = [];
this._children.forEach(c => json.push(c.Name));
return json;
toJSON()
{
let json = [];
if(options.testMode===true) // sort nodes only in test mode
this._children.sort(compareNodes)
this._children.forEach(c => json.push(c.Name));
return json;
}
}
}
/* Base class to Action/Function that provides
overloaded XML and JSON rendering of parameters and
return type. Parameters are _children.
_returnType holds the eventually existing ReturnType in V4.
In V2 the return type is a direct attribute called ReturnType
to the FunctionImport. See comment in class FunctionImport.
*/
/* Base class to Action/Function that provides
overloaded XML and JSON rendering of parameters and
return type. Parameters are _children.
_returnType holds the eventually existing ReturnType in V4.
In V2 the return type is a direct attribute called ReturnType
to the FunctionImport. See comment in class FunctionImport.
*/
class ActionFunctionBase extends Node
{
constructor(v, details)
class ActionFunctionBase extends Node
{
super(v, details);
this.set( { _returnType: undefined });
}
constructor(v, details)
{
super(v, details);
this.set( { _returnType: undefined });
}
innerXML(indent)
{
let xml = super.innerXML(indent);
if(this._returnType != undefined)
xml += this._returnType.toXML(indent) + '\n';
return xml
}
innerXML(indent)
{
let xml = super.innerXML(indent);
if(this._returnType != undefined)
xml += this._returnType.toXML(indent) + '\n';
return xml
}
toJSONchildren(json)
{
let json_parameters = [];
this._children.forEach(p => json_parameters.push(p.toJSON()));
if(json_parameters.length > 0)
json['$Parameter'] = json_parameters;
if(this._returnType)
toJSONchildren(json)
{
json['$ReturnType'] = this._returnType.toJSON();
let json_parameters = [];
if(options.testMode===true) // sort nodes only in test mode
this._children.sort(compareNodes)
this._children.forEach(p => json_parameters.push(p.toJSON()));
if(json_parameters.length > 0)
json['$Parameter'] = json_parameters;
if(this._returnType)
{
json['$ReturnType'] = this._returnType.toJSON();
}
return json;
}
return json;
}
}
// FunctionDefinition should be named 'Function', but this would
// collide with a method 'Function' of the Istanbul/NYC tool
class FunctionDefinition extends ActionFunctionBase
{
get kind() { return 'Function'; }
}
class Action extends ActionFunctionBase {}
// FunctionDefinition should be named 'Function', but this would
// collide with a method 'Function' of the Istanbul/NYC tool
class FunctionDefinition extends ActionFunctionBase
{
get kind() { return 'Function'; }
}
class Action extends ActionFunctionBase {}
/* FunctionImport is derived from ActionFunctionBase
because in V2 Parameters need to be rendered as sub elements
to Function Import. The ReturnType property is set in the
assembly code above (the invisible returnType is left undefined)
*/
class FunctionImport extends Node {} //ActionFunctionBase {}
class ActionImport extends Node {}
/* FunctionImport is derived from ActionFunctionBase
because in V2 Parameters need to be rendered as sub elements
to Function Import. The ReturnType property is set in the
assembly code above (the invisible returnType is left undefined)
*/
class FunctionImport extends Node {} //ActionFunctionBase {}
class ActionImport extends Node {}
/* ReturnType is only used in v4, mapCdsToEdmType can be safely
called with V2=false */
class ReturnType extends Node
{
constructor(v, csn, fullQualified)
/* ReturnType is only used in v4, mapCdsToEdmType can be safely
called with V2=false */
class ReturnType extends Node
{
let type = csn.type || csn.items.type;
if(type && type.startsWith('cds.'))
type = glue.mapCdsToEdmType(type, false, false);
else
type = fullQualified(type);
super(v, { Type: type });
glue.addTypeFacets(this, csn);
this.set( { _type: type, _isCollection: csn.items != undefined, _nullable: true });
constructor(v, csn, fullQualified)
{
let typecsn = csn.items || csn;
let type = typecsn.type;
if(type && type.startsWith('cds.') && !type.startsWith('cds.foundation.')) {
type = edmUtils.mapCdsToEdmType(typecsn, signal, error, false, false);
}
else
type = fullQualified(type);
super(v, { Type: type });
edmUtils.addTypeFacets(this, csn);
this.set( { _type: type, _isCollection: csn.items != undefined, _nullable: true });
if(this._isCollection)
this.Type = `Collection(${this.Type})`
}
if(this._isCollection)
this.Type = `Collection(${this.Type})`
}
// we need Name but NO $kind, can't use standard to JSON()
toJSON()
{
let rt = Object.create(null);
// use the original type, not the decorated one
if(this._type != 'Edm.String')
rt['$Type'] = this._type;
if(this._isCollection)
rt['$Collection'] = this._isCollection;
// we need Name but NO $kind, can't use standard to JSON()
toJSON()
{
let rt = Object.create(null);
// use the original type, not the decorated one
if(this._type != 'Edm.String')
rt['$Type'] = this._type;
if(this._isCollection)
rt['$Collection'] = this._isCollection;
// !this._nullable if Nullable=true become default
if(this._nullable)
rt['$Nullable'] = this._nullable;
return rt;
// !this._nullable if Nullable=true become default
if(this._nullable)
rt['$Nullable'] = this._nullable;
return rt;
}
}
}
class TypeBase extends Node
{
constructor(v, attributes, csn, typeName='Type')
class TypeBase extends Node
{
if(!(csn instanceof Object))
throw Error('Please debug me: csn must be an object');
constructor(v, attributes, csn, typeName='Type')
{
if(!(csn instanceof Object || (typeof csn === 'object' && csn !== null)))
throw Error('Please debug me: csn must be an object');
// ??? Is CSN still required? NavProp?
super(v, attributes, csn);
this.set({ _isCollection: csn.items != undefined, _typeName: typeName });
// ??? Is CSN still required? NavProp?
super(v, attributes, csn);
this.set({ _isCollection: csn.items != undefined, _typeName: typeName });
if(this[typeName] == undefined)
{
let typecsn = csn.items || csn;
// Complex/EntityType are derived from TypeBase
// but have no type attribute in their CSN
if(typecsn.type)
if(this[typeName] == undefined)
{
if(typecsn.type.startsWith('cds.'))
let typecsn = csn.items || csn;
// Complex/EntityType are derived from TypeBase
// but have no type attribute in their CSN
if(typecsn.type)
{
this[typeName] = glue.mapCdsToEdmType(typecsn.type, this.v2, csn['@Core.MediaType']);
// CDXCORE-CDXCORE-173 ignore type facets for Edm.Stream
if(this[typeName] != 'Edm.Stream')
glue.addTypeFacets(this, typecsn);
}
else
this[typeName] = typecsn.type;
if(typecsn.type.startsWith('cds.') && !typecsn.type.startsWith('cds.foundation.'))
{
this[typeName] = edmUtils.mapCdsToEdmType(typecsn, signal, error, this.v2, csn['@Core.MediaType']);
// CDXCORE-CDXCORE-173 ignore type facets for Edm.Stream
if(this[typeName] != 'Edm.Stream')
edmUtils.addTypeFacets(this, typecsn);
}
else
this[typeName] = typecsn.type;
// CDXCORE-245:
// map type to @odata.Type
// optionally add @odata.MaxLength but only in combination with @odata.Type
// In absence of checks restrict @odata.Type to 'Edm.String'
let odataType = csn['@odata.Type'];
let odataTypeMaxLen = csn['@odata.MaxLength'];
// CDXCORE-245:
// map type to @odata.Type
// optionally add @odata.MaxLength but only in combination with @odata.Type
// In absence of checks restrict @odata.Type to 'Edm.String'
let odataType = csn['@odata.Type'];
let odataTypeMaxLen = csn['@odata.MaxLength'];
if(odataType === 'Edm.String')
{
this[typeName] = odataType;
if(odataTypeMaxLen)
this['MaxLength'] = odataTypeMaxLen;
if(odataType === 'Edm.String')
{
this[typeName] = odataType;
if(odataTypeMaxLen)
this['MaxLength'] = odataTypeMaxLen;
}
// store undecorated type for JSON
this.set( { _type : this[typeName] });
// decorate for XML (not for Complex/EntityType)
if(this._isCollection)
this[typeName] = `Collection(${this[typeName]})`
}
// store undecorated type for JSON
this.set( { _type : this[typeName] });
// decorate for XML (not for Complex/EntityType)
if(this._isCollection)
this[typeName] = `Collection(${this[typeName]})`
}
}
}
toJSONattributes(json)
{
// $Type Edm.String, $Nullable=false MAY be omitted
// @ property and parameter for performance reasons
if(this._type != 'Edm.String' && this._type) // Edm.String is default)
json['$'+this._typeName] = this._type;
toJSONattributes(json)
{
// $Type Edm.String, $Nullable=false MAY be omitted
// @ property and parameter for performance reasons
if(this._type != 'Edm.String' && this._type) // Edm.String is default)
json['$'+this._typeName] = this._type;
for (let p in this)
{
if (p != 'Name' && p != this._typeName
// remove this line if Nullable=true becomes default
&& !(p == 'Nullable' && this[p] == false))
for (let p in this)
{
json[p[0] == '@' ? p : '$' + p] = this[p]
if (p != 'Name' && p != this._typeName
// remove this line if Nullable=true becomes default
&& !(p == 'Nullable' && this[p] == false))
{
json[p[0] == '@' ? p : '$' + p] = this[p]
}
}
}
if(this._isCollection)
json['$Collection'] = this._isCollection;
if(this._isCollection)
json['$Collection'] = this._isCollection;
return json;
return json;
}
}
}
class ComplexType extends TypeBase { }
class EntityType extends ComplexType
{
constructor(v, details, properties, csn)
class ComplexType extends TypeBase { }
class EntityType extends ComplexType
{
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) } );
}
constructor(v, details, properties, csn)
{
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) } );
}
innerXML(indent)
{
let xml = "";
if(this._keys)
xml += this._keys.toXML(indent) + '\n';
return xml + super.innerXML(indent);
}
innerXML(indent)
{
let xml = "";
if(this._keys)
xml += this._keys.toXML(indent) + '\n';
return xml + super.innerXML(indent);
}
toJSONattributes(json)
{
super.toJSONattributes(json);
if(this._keys)
toJSONattributes(json)
{
json['$Key'] = this._keys.toJSON();
super.toJSONattributes(json);
if(this._keys)
{
json['$Key'] = this._keys.toJSON();
}
return json;
}
return json;
}
}
class TypeDefinition extends TypeBase
{
constructor(v, attributes, csn)
class TypeDefinition extends TypeBase
{
super(v, attributes, csn, 'UnderlyingType');
}
constructor(v, attributes, csn)
{
super(v, attributes, csn, 'UnderlyingType');
}
toJSONattributes(json)
{
super.toJSONattributes(json);
json['$UnderlyingType'] = this._type;
return json;
toJSONattributes(json)
{
super.toJSONattributes(json);
json['$UnderlyingType'] = this._type;
return json;
}
}
}
class EnumType extends TypeDefinition
{
constructor(v, attributes, csn)
class EnumType extends TypeDefinition
{
super(v, attributes, csn);
constructor(v, attributes, csn)
{
super(v, attributes, csn);
// array of enum not yet allowed
let enumValues = /*(csn.items && csn.items.enum) ||*/ csn.enum;
glue.forAll(enumValues, (e, en) => {
this.append(new Member(v, { Name: en, Value: e.val } ));
});
}
// array of enum not yet allowed
let enumValues = /*(csn.items && csn.items.enum) ||*/ csn.enum;
edmUtils.forAll(enumValues, (e, en) => {
this.append(new Member(v, { Name: en, Value: e.val } ));
});
}
toJSONattributes(json)
{
super.toJSONattributes(json);
return json;
}
toJSONattributes(json)
{
super.toJSONattributes(json);
return json;
}
toJSONchildren(json)
{
this._children.forEach(c => c.toJSONattributes(json));
return json;
toJSONchildren(json)
{
if(options.testMode===true) // sort nodes only in test mode
this._children.sort(compareNodes)
this._children.forEach(c => c.toJSONattributes(json));
return json;
}
}
}
class Member extends Node
{
toJSONattributes(json)
class Member extends Node
{
json[this.Name] = this.Value;
return json;
toJSONattributes(json)
{
json[this.Name] = this.Value;
return json;
}
}
}
class PropertyBase extends TypeBase
{
constructor(v, attributes, csn)
class PropertyBase extends TypeBase
{
super(v, attributes, csn);
this.set({ _csn: csn });
if(this.v2)
constructor(v, attributes, csn)
{
let typecsn = csn.items || csn;
super(v, attributes, csn);
this.set({ _csn: csn });
if(this.v2)
{
let typecsn = csn.items || csn;
// see glue.mapsCdsToEdmType => add sap:display-format annotation
// only if Edm.DateTime is the result of a cast from Edm.Date
// but not if Edm.DateTime is the result of a regular cds type mapping
if(this.Type == 'Edm.DateTime'
&& (typecsn.type != 'cds.DateTime' && typecsn.type != 'cds.Timestamp'))
this.setXml( { 'sap:display-format' : "Date" } );
// see edmUtils.mapsCdsToEdmType => add sap:display-format annotation
// only if Edm.DateTime is the result of a cast from Edm.Date
// but not if Edm.DateTime is the result of a regular cds type mapping
if(this.Type == 'Edm.DateTime'
&& (typecsn.type != 'cds.DateTime' && typecsn.type != 'cds.Timestamp'))
this.setXml( { 'sap:display-format' : "Date" } );
}
this.setNullable();
}
this.setNullable();
}
setNullable()
{
// Nullable=true is default, mention Nullable=false only in XML
// Nullable=false is default for EDM JSON representation 4.01
// When a key explicitly (!) has 'notNull = false', it stays nullable
if(this.isNotNullable())
setNullable()
{
this.Nullable = false;
// Nullable=true is default, mention Nullable=false only in XML
// Nullable=false is default for EDM JSON representation 4.01
// When a key explicitly (!) has 'notNull = false', it stays nullable
if(this.isNotNullable())
{
this.Nullable = false;
}
}
}
isNotNullable(csn=undefined) {
let nodeCsn = csn || this._csn;
// Nullable=true is default, mention Nullable=false only in XML
// Nullable=false is default for EDM JSON representation 4.01
// When a key explicitly (!) has 'notNull = false', it stays nullable
return ((nodeCsn.key && !(nodeCsn.notNull === false)) || nodeCsn.notNull == true);
}
isNotNullable(csn=undefined) {
let nodeCsn = csn || this._csn;
// Nullable=true is default, mention Nullable=false only in XML
// Nullable=false is default for EDM JSON representation 4.01
// When a key explicitly (!) has 'notNull = false', it stays nullable
return ((nodeCsn.key && !(nodeCsn.notNull === false)) || nodeCsn.notNull == true);
}
toJSONattributes(json)
{
super.toJSONattributes(json);
// mention all nullable elements explictly, remove if Nullable=true becomes default
if(this.Nullable == undefined || this.Nullable == true)
toJSONattributes(json)
{
json['$Nullable'] = true;
super.toJSONattributes(json);
// mention all nullable elements explictly, remove if Nullable=true becomes default
if(this.Nullable == undefined || this.Nullable == true)
{
json['$Nullable'] = true;
}
return json;
}
return json;
}
}
class Property extends PropertyBase
{
constructor(v, attributes, csn)
class Property extends PropertyBase
{
// the annotations in this array shall become exposed as Property attributes in
// the V2 metadata.xml
Property.SAP_Annotation_Attribute_WhiteList = [
'@sap.hierarchy.node.for', //-> sap:hierarchy-node-for
'@sap.hierarchy.parent.node.for', // -> sap:hierarchy-parent-node-for
'@sap.hierarchy.level.for', // -> sap:hierarchy-level-for
'@sap.hierarchy.drill.state.for', // -> sap:hierarchy-drill-state-for
'@sap.hierarchy.node.descendant.count.for' // -> sap:hierarchy-node-descendant-count-for
];
super(v, attributes, csn);
// TIPHANACDS-4180
if(this.v2)
constructor(v, attributes, csn)
{
if(csn['@odata.etag'] == true)
this.ConcurrencyMode='Fixed'
// the annotations in this array shall become exposed as Property attributes in
// the V2 metadata.xml
Property.SAP_Annotation_Attribute_WhiteList = [
'@sap.hierarchy.node.for', //-> sap:hierarchy-node-for
'@sap.hierarchy.parent.node.for', // -> sap:hierarchy-parent-node-for
'@sap.hierarchy.level.for', // -> sap:hierarchy-level-for
'@sap.hierarchy.drill.state.for', // -> sap:hierarchy-drill-state-for
'@sap.hierarchy.node.descendant.count.for' // -> sap:hierarchy-node-descendant-count-for
];
// translate the following @sap annos as xml attributes to the Property
for (let p in csn)
super(v, attributes, csn);
// TIPHANACDS-4180
if(this.v2)
{
if (Property.SAP_Annotation_Attribute_WhiteList.includes(p))
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn[p] });
}
}
this.set({isKey: csn.key != undefined });
}
if(csn['@odata.etag'] == true || csn['@cds.etag'] == true)
this.ConcurrencyMode='Fixed'
// required for walker to identify property handling....
// static get isProperty() { return true }
}
// translate the following @sap annos as xml attributes to the Property
for (let p in csn)
{
if (Property.SAP_Annotation_Attribute_WhiteList.includes(p))
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn[p] });
}
}
this.set({isKey: csn.key != undefined });
}
class PropertyRef extends Node
{
constructor(v, Name) { super(v, { Name }); }
}
class Parameter extends PropertyBase
{
constructor(v, attributes, csn={}, mode=null)
{
super(v, attributes, csn);
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 constructor
if(this.v2 && this.Nullable === undefined)
this.setXml({Nullable: true});
// required for walker to identify property handling....
// static get isProperty() { return true }
}
toJSON()
class PropertyRef extends Node
{
// we need Name but NO $kind, can't use standard to JSON()
let json = Object.create(null);
for (let p in this)
json[p[0] == '@' ? p : '$' + p] = this[p]
return json;
constructor(v, Name) { super(v, { Name }); }
}
}
class NavigationPropertyBinding extends Node {}
class NavigationProperty extends Property
{
constructor(v, attributes, csn)
class Parameter extends PropertyBase
{
NavigationProperty.DOLLAR_SELF = '$self'
super(v, attributes, csn);
this.set( {
_type: attributes.Type,
_isCollection: glue.isToMany(csn),
_referentialConstraints: this.getReferentialConstraints(),
_targetCsn: csn.target } );
if (this.v4)
constructor(v, attributes, csn={}, mode=null)
{
// either csn has multiplicity or we have to use the multiplicity of the backlink
if(this._isCollection || this._referentialConstraints.multiplicity[1] == '*') {
this.Type = `Collection(${attributes.Type})`
// attribute Nullable is not allowed in combination with Collection (see Spec)
delete this.Nullable;
super(v, attributes, csn);
}
if(mode != null)
this.Mode = mode;
if(csn.type === 'cds.Composition')
{
// TODO: to be specified via @sap.on.delete
this.append(new OnDelete(v, { Action: 'Cascade' } ) );
}
/*
1) If this navigation property belongs to an EntityType for a parameterized entity
```entity implemented in calcview (P1: T1, ..., Pn: Tn) { ... }```
and if the csn.containsTarget for this NavigationProperty is true,
then this is the generated 'Results' association to the underlying entityType.
Only this special association may have an explicit ContainsTarget attribute.
See csn2edm.createParmeterizedEntityTypeAndSet() for details
2) ContainsTarget stems from the @odata.contained annotation
*/
if(csn['@odata.contained'] == true || csn.containsTarget)
this.ContainsTarget = true;
// if backlink has established partner before this underlying NavProp is created, use it
if(csn._partnerCsn)
this.Partner = csn._partnerCsn.name;
// 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});
}
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 this.Nullable;
toJSON()
{
// we need Name but NO $kind, can't use standard to JSON()
let json = Object.create(null);
for (let p in this)
json[p[0] == '@' ? p : '$' + p] = this[p]
return json;
}
// store NavProp reference in the model for bidirectional $Partner tagging (done in getReferentialConstraints())
csn._NavigationProperty = this;
// we don't want NavProps in the <KEY> list
delete this.isKey;
}
// if the backlink association is annotated with @odata.contained or the underlying association
// is marked with _isToContainer, then the association is a Containment relationship
isContainment() {
return this._csn._isToContainer || this._csn['@odata.contained'];
}
class NavigationPropertyBinding extends Node {}
isNotNullable(csn=undefined) {
let nodeCsn = csn || this._csn;
// Set Nullable=false only if 'NOT NULL' was specified in the model
// Do not derive Nullable=false from key attribute.
return (nodeCsn.notNull == true);
}
toJSONattributes(json)
class NavigationProperty extends Property
{
// use the original type, not the decorated one
super.toJSONattributes(json);
json['$Type'] = this._type;
constructor(v, attributes, csn)
{
super(v, attributes, csn);
// attribute Nullable is not allowed in combination with Collection (see Spec)
if(json['$Collection'])
delete json['$Nullable'];
return json;
}
let [src, tgt] = edmUtils.determineMultiplicity(csn._constraints._originAssocCsn || csn);
csn._constraints._multiplicity = csn._constraints._originAssocCsn ? [tgt, src] : [src, tgt];
toJSONchildren(json)
{
let json_constraints = Object.create(null);
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);
this.set( {
_type: attributes.Type,
_isCollection: this.isToMany(),
_targetCsn: csn.target
} );
}
});
// TODO Annotations
if(Object.keys(json_constraints).length > 0)
json['$ReferentialConstraint'] = json_constraints;
return json;
}
if (this.v4)
{
// either csn has multiplicity or we have to use the multiplicity of the backlink
if(this._isCollection) {
this.Type = `Collection(${attributes.Type})`
// attribute Nullable is not allowed in combination with Collection (see Spec)
delete this.Nullable;
createNavigationPropertyBinding(namespace)
{
return new NavigationPropertyBinding(this._v,
{ Path: this.Name, Target: this._csn.target.name.replace(namespace, '') }
);
}
}
addReferentialConstraintNodes()
{
glue.forAll(this._referentialConstraints.constraints,
c => this.append(new ReferentialConstraint(this._v,
{ Property: c[0], ReferencedProperty: c[1] }
) ) );
}
if(csn.type === 'cds.Composition')
{
// TODO: to be specified via @sap.on.delete
this.append(new OnDelete(v, { Action: 'Cascade' } ) );
}
getReferentialConstraints(assocCsn = undefined)
{
let result = { multiplicity: [ ], constraints: Object.create(null), selfs: [], termCount: 0 };
let partner = (csn._partnerCsn.length > 0) ? csn._partnerCsn[0] : csn._constraints._originAssocCsn;
if(partner && partner['@odata.navigable'] !== false) {
this.Partner = partner.name;
}
assocCsn = assocCsn || this._csn;
/*
1) If this navigation property belongs to an EntityType for a parameterized entity
```entity implemented in calcview (P1: T1, ..., Pn: Tn) { ... }```
and if the csn.containsTarget for this NavigationProperty is true,
then this is the generated 'Results' association to the underlying entityType.
Only this special association may have an explicit ContainsTarget attribute.
See csn2edm.createParmeterizedEntityTypeAndSet() for details
2) ContainsTarget stems from the @odata.contained annotation
*/
if(csn['@odata.contained'] == true || csn.containsTarget)
this.ContainsTarget = true;
}
if(assocCsn.on)
{
// fill constraint array with [prop, depProp]
getExpressionArguments(assocCsn.on);
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 this.Nullable;
}
// for all $self conditions, fill constraints of partner (if any)
let isBacklink = result.selfs.length == 1 && result.termCount == 1;
// store NavProp reference in the model for bidirectional $Partner tagging (done in getReferentialConstraints())
csn._NavigationProperty = this;
/* example for originalTarget:
entity E (with parameters) {
... keys and all the stuff ...
toE: association to E;
back: association to E on back.toE = $self
}
toE target 'E' is redirected to 'EParameters' (must be as the new parameter list is required)
back target 'E' is also redirected to 'EParameters' (otherwise backlink would fail)
ON Condition back.toE => parter=toE cannot be resolved in EParameters, originalTarget 'E' is
required for that
*/
result.selfs.forEach(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?');
// we don't want NavProps in the <KEY> list
delete this.isKey;
}
// let originAssocCsn = assocCsn.target.elements[partner] || assocCsn.originalTarget.elements[partner];
if(glue.isAssociationOrComposition(originAssocCsn)) {
// if the assoc is marked as primary key, add all its foreign keys as constraint
// as they are primary keys of the other entity as well
if(!assocCsn.target.isParamEntity && originAssocCsn.key) {
if(originAssocCsn.keys) {
for(let fk of originAssocCsn.keys) {
let c = [ fk.ref[0], fk.$generatedFieldName ];
result.constraints[c] = c;
}
}
}
// if the backlink association is annotated with @odata.contained or the underlying association
// is marked with _isToContainer, then the association is a Containment relationship
isContainment() {
return this._csn._isToContainer || this._csn['@odata.contained'];
}
// Mark this association as backlink if $self appears exactly once
// to surpress edm:Association generation in V2 mode
if(isBacklink) {
result._originAssocCsn = originAssocCsn;
isNotNullable(csn=undefined) {
let nodeCsn = csn || this._csn;
// Set Nullable=false only if 'NOT NULL' was specified in the model
// Do not derive Nullable=false from key attribute.
return (nodeCsn.notNull == true);
}
isToMany() {
return (this._isCollection || this._csn._constraints._multiplicity[1] == '*');
}
// V4 Set the Partner attribute with the name of the opposite
// association to the two corresponding csn's and to this NavProp
// (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)
{
// 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;
toJSONattributes(json)
{
// use the original type, not the decorated one
super.toJSONattributes(json);
json['$Type'] = this._type;
// 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;
// attribute Nullable is not allowed in combination with Collection (see Spec)
if(json['$Collection'])
delete json['$Nullable'];
return json;
}
//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 + '"');
}
}
toJSONchildren(json)
{
let json_constraints = Object.create(null);
if(options.testMode===true) // sort nodes only in test mode
this._children.sort(compareNodes)
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);
}
else {
/*
entity E {
key id : Integer;
toMe: associaton to E on toMe.id = $self; };
*/
throw Error('Backlink association element is not an association or composition: "' + originAssocCsn.name);
}
});
// TODO Annotations
if(Object.keys(json_constraints).length > 0)
json['$ReferentialConstraint'] = json_constraints;
return json;
}
if(!assocCsn.target.isParamEntity) {
// remove all arget elements that are not key
glue.foreach(result.constraints, c => !(assocCsn.target.elements[c[1]] && assocCsn.target.elements[c[1]].key),
(c, cn) => { delete result.constraints[cn]; } );
}
createNavigationPropertyBinding(namespace)
{
return new NavigationPropertyBinding(this._v,
{ Path: this.Name, Target: this._csn.target.name.replace(namespace, '') }
);
}
// this is a managed association
else
// V4 referential constraints!
addReferentialConstraintNodes()
{
// If FK is key in target => constraint
// Don't consider primary key associations (fks become keys on the source entity) as
// this would impose a constraint against the target.
// FIXME: If path is something structured, perform a path resolution (or use augmented CSN)
edmUtils.forAll(this._csn._constraints.constraints,
c => this.append(new ReferentialConstraint(this._v, { Property: c[0], ReferencedProperty: c[1] } ) ) );
}
}
if(!assocCsn.target.isParamEntity && assocCsn.keys) {
for(let fk of assocCsn.keys) {
if(assocCsn.target.elements[fk.ref[0]].key)
{
let c = [ fk.$generatedFieldName, fk.ref[0] ];
result.constraints[c] = c;
}
}
class ReferentialConstraint extends Node
{
innerXML(indent)
{
if(this._d && this._p)
{
return this._p.toXML(indent) + '\n' + this._d.toXML(indent) + '\n';
}
else
return super.innerXML(indent);
}
}
// If this association points to a redirected Parameter EntityType, do not calculate any constraints,
// continue with multiplicity
if(assocCsn.target.isParamEntity)
class OnDelete extends Node {}
// Annotations below
class AnnotationBase extends Node
{
// 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()
{
result.constraints = Object.create(null);
let json = Object.create(null);
this.toJSONattributes(json);
this.toJSONchildren(json);
return json
}
determineMultiplicity(result._originAssocCsn);
return result;
// nested functions
function getExpressionArguments(expr)
getConstantExpressionValue()
{
let allowedTokens = [ '=', 'and', '(', ')' ];
if(expr && Array.isArray(expr))
// if some returns true, this term is not usable as a constraint term
if(!expr.some(isNotAConstraintTerm))
expr.forEach(fillConstraints)
// short form: key: value
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', 'Bool' ];
// return true if token is not one of '=', 'and', '(', ')' or object
function isNotAConstraintTerm(tok)
{
if(tok.xpr)
return tok.xpr.some(isNotAConstraintTerm);
if(Array.isArray(tok))
return tok.some(isNotAConstraintTerm);
return !(typeof tok === 'object' && tok != null || allowedTokens.includes(tok));
}
let constExpr = [ ...inlineConstExpr,
'AnnotationPath', 'ModelElementPath', 'NavigationPropertyPath', 'PropertyPath', 'Path',
'EnumMember', 'EnumMember@odata.type' ];
function fillConstraints(arg, pos)
let expr = edmUtils.intersect(constExpr, Object.keys(this._jsonOnlyAttributes))
if(expr.length == 0)
throw Error('Please debug me: neither child nor constant expression found on annotation');
return addExpressions(expr, this._jsonOnlyAttributes);
function addExpressions(expr, dict)
{
if(arg.xpr)
arg.xpr.map(fillConstraints);
else if(pos > 0 && pos < expr.length)
let inline = edmUtils.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)
{
let lhs = expr[pos-1];
let rhs = expr[pos+1];
if(['='].includes(arg))
let v = dict[inline[0]];
switch(inline[0])
{
result.termCount++;
if(lhs.ref && rhs.ref) // ref is a path
{
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 id?
// if so, store partner in selfs array
if(c[0] === NavigationProperty.DOLLAR_SELF)
result.selfs.push(c[1]);
else
result.constraints[c] = c;
}
}
/* 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:
return { '$Cast': v, '$Type': inline[0] };
}
}
else
{
let json = Object.create(null);
for(let i = 0; i < expr.length; i++)
json['$' + expr[i]] = dict[expr[i]]
return json;
}
}
}
}
function determineMultiplicity(originCsn)
class Annotations extends AnnotationBase
{
constructor(v, target)
{
/*
=> SRC Cardinality
CDS => EDM
------------
undef => '*' // CDS default mapping
1 => 0..1
n => '*'
n/a => 1 // not expressable
* => *
super(v, { Target: target });
if (this.v2)
this.setXml( { xmlns : "http://docs.oasis-open.org/odata/ns/edm" } );
}
=> TGT Cardinality
CDS => EDM
------------
undef => 0..1 // CDS default mapping
0..1 => 0..1
1 => 0..1
1 not null => 1 (targetMin=1 is set by transform/toOdata.js)
1..1 => 1
0..m => '*'
m => '*'
1..n => '*'
n..m => '*'
* => '*'
*/
toJSONattributes(json)
{
for (let p in this)
if (p != 'Target')
json[p[0] == '@' ? p : '$' + p] = this[p]
return json;
}
let csn = originCsn || assocCsn;
/* new csn:
src, min, max
*/
if(!csn.cardinality)
csn.cardinality = Object.create(null);
// set missing defaults
if(!csn.cardinality.src)
csn.cardinality.src = '*';
if(!csn.cardinality.min)
csn.cardinality.min = 0;
if(!csn.cardinality.max)
csn.cardinality.max = 1;
let srcCardinality = (csn.cardinality.src == 1) ? '0..1' : '*';
let tgtCardinality = (csn.cardinality.max > 1 || csn.cardinality.max == '*') ? '*' :
(csn.cardinality.min == 1) ? '1' : '0..1';
// if originCsn was provided, reverse multiplicity for backlink
result.multiplicity = originCsn ? [tgtCardinality, srcCardinality] : [srcCardinality, tgtCardinality];
toJSONchildren(json)
{
if(options.testMode===true) // sort nodes only in test mode
this._children.sort(compareNodes)
this._children.forEach(a => json['@' + a.Term] = a.toJSON())
}
}
}
class ReferentialConstraint extends Node
{
innerXML(indent)
// An Annotation must contain either children or a constant value
// The value attribute is rendered by getConstantExpressionValue().
// However, in case the constant expression value differs for XML an JSON
// (EnumMember & EnumMember@odata.type) then the value properties must
// be separated by using setJSON(attibute) and setXML(attribute).
// See genericTranslation::handleValue() for details (especially the code
// that sets the EnumMember code). All this has been done because the
// Annotation object is passed around in genericTranslation and the
// properties are set all over the place. The initial assumption was that
// the constant expression value is the same for both XML and JSON. But
// since it was discovered, that in JSON the EnumMember type must be
// transported this is no longer the case....
class Annotation extends AnnotationBase
{
if(this._d && this._p)
constructor(v, termName)
{
return this._p.toXML(indent) + '\n' + this._d.toXML(indent) + '\n';
super(v, { Term: termName } );
}
else
return super.innerXML(indent);
toJSON()
{
if(this._children.length == 0 || this._ignoreChildren) // must be a constant expression
return this.getConstantExpressionValue();
else
// annotation must have exactly one child (=record or collection)
return this._children[0].toJSON();
}
}
}
class OnDelete extends Node {}
// Annotations below
class AnnotationBase extends Node
{
// 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()
class Collection extends AnnotationBase
{
let json = Object.create(null);
this.toJSONattributes(json);
this.toJSONchildren(json);
return json
toJSON()
{
return this._children.map(a => a.toJSON());
}
}
getConstantExpressionValue()
class Record extends AnnotationBase
{
// short form: key: value
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' ];
toJSONattributes(json)
{
let keys = Object.keys(this).filter(k => k != 'Type');
for(let i = 0; i < keys.length; i++)
json['$'+keys[i]] = this[keys[i]];
}
let constExpr = [ ...inlineConstExpr,
'AnnotationPath', 'ModelElementPath', 'NavigationPropertyPath', 'PropertyPath', 'Path',
'EnumMember', 'EnumMember@odata.type' ];
let expr = glue.intersect(constExpr, Object.keys(this._jsonOnlyAttributes))
if(expr.length == 0)
throw Error('Please debug me: neither child nor constant expression found on annotation');
return addExpressions(expr, this._jsonOnlyAttributes);
function addExpressions(expr, dict)
toJSONchildren(json)
{
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)
{
let v = dict[inline[0]];
switch(inline[0])
if(options.testMode===true) // sort nodes only in test mode
this._children.sort(compareNodes)
this._children.forEach(a => {
let name;
switch(a.kind)
{
/* 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;
case 'Annotation':
name = '@' + a.Term;
break;
case 'PropertyValue':
name = a.Property;
break;
default:
return { '$Cast': v, '$Type': inline[0] };
throw Error('Please debug me: default not reachable');
}
}
else
{
let json = Object.create(null);
for(let i = 0; i < expr.length; i++)
json['$' + expr[i]] = dict[expr[i]]
return json;
}
json[name] = a.toJSON()
});
}
}
}
class Annotations extends AnnotationBase
{
constructor(v, target)
class PropertyValue extends AnnotationBase
{
super(v, { Target: target });
if (this.v2)
this.setXml( { xmlns : "http://docs.oasis-open.org/odata/ns/edm" } );
}
constructor(v, property)
{
super(v);
this.Property = property;
}
toJSONattributes(json)
{
for (let p in this)
if (p != 'Target')
json[p[0] == '@' ? p : '$' + p] = this[p]
return json;
toJSON()
{
if(this._children.length == 0 || this._ignoreChildren)
return this.getConstantExpressionValue();
else
{
return this._children[0].toJSON();
}
}
}
toJSONchildren(json)
class Thing extends AnnotationBase
{
this._children.forEach(a => json['@' + a.Term] = a.toJSON())
}
}
constructor(v, kind, details)
{
super(v, details);
this.setKind(kind);
}
// An Annotation must contain either children or a constant value
// The value attribute is rendered by getConstantExpressionValue().
// However, in case the constant expression value differs for XML an JSON
// (EnumMember & EnumMember@odata.type) then the value properties must
// be separated by using setJSON(attibute) and setXML(attribute).
// See genericTranslation::handleValue() for details (especially the code
// that sets the EnumMember code). All this has been done because the
// Annotation object is passed around in genericTranslation and the
// properties are set all over the place. The initial assumption was that
// the constant expression value is the same for both XML and JSON. But
// since it was discovered, that in JSON the EnumMember type must be
// transported this is no longer the case....
class Annotation extends AnnotationBase
{
constructor(v, termName)
{
super(v, { Term: termName } );
setKind(kind)
{
Object.defineProperty(this, 'kind',
{ get: function() { return kind; }});
}
}
toJSON()
class ValueThing extends Thing
{
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();
}
}
constructor(v, kind, value)
{
super(v, kind, undefined);
this.set( { _value : value });
}
class Collection extends AnnotationBase
{
toJSON()
{
return this._children.map(a => a.toJSON());
}
}
toXML(indent='')
{
let kind = this.kind;
let xml = indent + "<" + kind + this.toXMLattributes();
xml += (this._value !== undefined ? ">" + escapeString(this._value) + "</" + kind + ">" : "/>");
return xml;
class Record extends AnnotationBase
{
toJSONattributes(json)
{
let keys = Object.keys(this).filter(k => k != 'Type');
for(let i = 0; i < keys.length; i++)
json['$'+keys[i]] = this[keys[i]];
}
toJSONchildren(json)
{
this._children.forEach(a => {
let name;
switch(a.kind)
function escapeString(s)
{
case 'Annotation':
name = '@' + a.Term;
break;
case 'PropertyValue':
name = a.Property;
break;
default:
throw Error('Please debug me: default not reachable');
// first regex: replace & if not followed by apos; or quot; or gt; or lt; or amp; or #
return (typeof s === 'string') ? s.replace(/&(?!(?:apos|quot|[gl]t|amp);|#)/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;') : s;
}
json[name] = a.toJSON()
});
}
}
}
class PropertyValue extends AnnotationBase
{
constructor(v, property)
{
super(v);
this.Property = property;
}
toJSON()
{
if(this._children.length == 0)
return this.getConstantExpressionValue();
else
toJSON()
{
return this._children[0].toJSON();
if(this._children.length == 0 || this._ignoreChildren) // must be a constant expression
return this.getConstantExpressionValue();
else
// annotation must have exactly one child (=record or collection)
return this._children[0].toJSON();
}
}
}
class Thing extends AnnotationBase
{
constructor(v, kind, details)
{
super(v, details);
this.setKind(kind);
}
setKind(kind)
// V2 specials
class End extends Node {}
class Association extends Node
{
Object.defineProperty(this, 'kind',
{ get: function() { return kind; }});
}
}
constructor(v, details, navProp, fromRole, toRole, multiplicity)
{
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] } ) );
class ValueThing extends Thing
{
constructor(v, kind, value)
{
super(v, kind, undefined);
this.set( { _value : value });
}
// 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);
toXML(indent='')
{
let kind = this.kind;
let xml = indent + "<" + kind + this.toXMLattributes();
xml += (this._value !== undefined ? ">" + escapeString(this._value) + "</" + kind + ">" : "/>");
return xml;
// 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' } ) );
}
}
function escapeString(s)
innerXML(indent)
{
// first regex: replace & if not followed by apos; or quot; or gt; or lt; or amp; or #
return (typeof s === 'string') ? s.replace(/&(?!(?:apos|quot|[gl]t|amp);|#)/g, '&amp;').replace(/>/g, '&gt;').replace(/</g, '&lt;').replace(/"/g, '&quot;') : s;
let xml = "";
this._end.forEach(e => xml += e.toXML(indent) + '\n');
xml += super.innerXML(indent);
return xml;
}
}
toJSON()
class AssociationSet extends Node
{
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();
constructor(v, details, fromRole, toRole, fromEntitySet, toEntitySet)
{
super(v, details);
this.append(
new End(v, { Role: fromRole, EntitySet: fromEntitySet } ),
new End(v, { Role: toRole, EntitySet: toEntitySet } )
);
}
}
}
class Dependent extends Node {}
class Principal extends Node {}
// V2 specials
class End extends Node {}
class Association extends Node
{
constructor(v, details, navProp, fromRole, toRole, multiplicity)
{
super(v, details);
this.set( { _end: [] });
ReferentialConstraint.createV2 =
function(v, from, to, c)
{
let node = new ReferentialConstraint(v, {});
node.set({ _d: new Dependent(v, { Role: from } ) });
node.set({ _p: new Principal(v, { Role: to } ) });
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' } ) );
edmUtils.forAll(c, cv => {
node._d.append(new PropertyRef(v, cv[0]));
node._p.append(new PropertyRef(v, cv[1]));
});
return node;
}
}
innerXML(indent)
{
let xml = "";
this._end.forEach(e => xml += e.toXML(indent) + '\n');
xml += super.innerXML(indent);
return xml;
return {
Edm,
Reference,
Include,
Schema,
DataServices,
EntityContainer,
EntitySet,
TypeDefinition,
EnumType,
ComplexType,
EntityType,
Key,
//ActionFunctionBase,
FunctionDefinition,
Action,
FunctionImport,
ActionImport,
ReturnType,
// PropertyBase,
Property,
PropertyRef,
Parameter,
NavigationPropertyBinding,
NavigationProperty,
ReferentialConstraint,
OnDelete,
// Annotations
Annotations,
Annotation,
Collection,
Record,
Thing,
ValueThing,
PropertyValue,
// V2 specials
End,
Association,
AssociationSet,
Dependent,
Principal
}
}
class AssociationSet extends Node
{
constructor(v, details, fromRole, toRole, fromEntitySet, toEntitySet)
{
super(v, details);
this.append(
new End(v, { Role: fromRole, EntitySet: fromEntitySet } ),
new End(v, { Role: toRole, EntitySet: toEntitySet } )
);
} // instance function
function compareNodes(A,B) {
if(A.kind<B.kind) return -1;
if(A.kind>B.kind) return 1;
if(A.Name !== undefined && B.Name !== undefined) {
if(A.Name<B.Name) return -1;
if(A.Name>B.Name) return 1;
}
if(A.Role !== undefined && B.Role !== undefined) {
if(A.Role<B.Role) return -1;
if(A.Role>B.Role) return 1;
}
if(A.Path !== undefined && B.Path !== undefined) {
if(A.Path<B.Path) return -1;
if(A.Path>B.Path) return 1;
}
if(A.Property !== undefined && B.Property !== undefined) {
if(A.Property<B.Property) return -1;
if(A.Property>B.Property) return 1;
}
return 0;
}
class Dependent extends Node {}
class Principal extends Node {}
ReferentialConstraint.createV2 =
function(v, from, to, c)
{
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(new PropertyRef(v, cv[0]));
node._p.append(new PropertyRef(v, cv[1]));
});
return node;
}
module.exports = Object.assign(EDM, {
Reference,
Include,
Schema,
DataServices,
EntityContainer,
EntitySet,
TypeDefinition,
EnumType,
ComplexType,
EntityType,
Key,
//ActionFunctionBase,
FunctionDefinition,
Action,
FunctionImport,
ActionImport,
ReturnType,
// PropertyBase,
Property,
PropertyRef,
Parameter,
NavigationPropertyBinding,
NavigationProperty,
ReferentialConstraint,
OnDelete,
// Annotations
Annotations,
Annotation,
Collection,
Record,
Thing,
ValueThing,
PropertyValue,
// V2 specials
End,
Association,
AssociationSet,
Dependent,
Principal
})

@@ -28,10 +28,27 @@ // Generated from JSON.g4 by ANTLR 4.7.1

function unquote(s) {
return JSON.parse(s); // unquote for free
function unquoteNumber(s) {
return JSON.parse(s); // unquote
}
function unquoteString(s) {
if(s.startsWith("'") && s.endsWith("'")) {
let ss = s.substring(1,s.length-1);
ss = ss.replace(/\\/g, '\\\\')
ss = ss.replace(/\"/g, '\\"')
s = '"' + ss + '"';
}
if(!s.startsWith('"') && !s.endsWith('"') && !isCharNumber(s[0])) { // plain string
s = '"' + s + '"';
}
return JSON.parse(s); // unquote
function isCharNumber(c) {
return c >= '0' && c <= '9';
}
}
var serializedATN = ["\u0003\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964",
"\u0002\u0010\u00a5\b\u0001\u0004\u0002\t\u0002\u0004\u0003\t\u0003\u0004",
"\u0002\u0010\u00ed\b\u0001\u0004\u0002\t\u0002\u0004\u0003\t\u0003\u0004",
"\u0004\t\u0004\u0004\u0005\t\u0005\u0004\u0006\t\u0006\u0004\u0007\t",

@@ -41,100 +58,151 @@ "\u0007\u0004\b\t\b\u0004\t\t\t\u0004\n\t\n\u0004\u000b\t\u000b\u0004",

"\t\u0010\u0004\u0011\t\u0011\u0004\u0012\t\u0012\u0004\u0013\t\u0013",
"\u0004\u0014\t\u0014\u0003\u0002\u0003\u0002\u0003\u0003\u0003\u0003",
"\u0003\u0004\u0003\u0004\u0003\u0005\u0003\u0005\u0003\u0006\u0003\u0006",
"\u0003\u0007\u0003\u0007\u0003\b\u0003\b\u0003\b\u0003\b\u0003\b\u0003",
"\t\u0003\t\u0003\t\u0003\t\u0003\t\u0003\t\u0003\n\u0003\n\u0003\n\u0003",
"\n\u0003\n\u0003\u000b\u0003\u000b\u0003\u000b\u0007\u000bI\n\u000b",
"\f\u000b\u000e\u000bL\u000b\u000b\u0003\u000b\u0003\u000b\u0003\f\u0003",
"\f\u0003\f\u0005\fS\n\f\u0003\r\u0003\r\u0003\r\u0003\r\u0003\r\u0003",
"\r\u0003\u000e\u0003\u000e\u0003\u000f\u0005\u000f^\n\u000f\u0003\u000f",
"\u0003\u000f\u0003\u000f\u0006\u000fc\n\u000f\r\u000f\u000e\u000fd\u0003",
"\u000f\u0005\u000fh\n\u000f\u0003\u000f\u0005\u000fk\n\u000f\u0003\u000f",
"\u0003\u000f\u0003\u000f\u0003\u000f\u0005\u000fq\n\u000f\u0003\u000f",
"\u0005\u000ft\n\u000f\u0003\u0010\u0003\u0010\u0003\u0010\u0007\u0010",
"y\n\u0010\f\u0010\u000e\u0010|\u000b\u0010\u0005\u0010~\n\u0010\u0003",
"\u0011\u0003\u0011\u0005\u0011\u0082\n\u0011\u0003\u0011\u0003\u0011",
"\u0003\u0012\u0006\u0012\u0087\n\u0012\r\u0012\u000e\u0012\u0088\u0003",
"\u0012\u0003\u0012\u0003\u0013\u0003\u0013\u0003\u0013\u0003\u0013\u0007",
"\u0013\u0091\n\u0013\f\u0013\u000e\u0013\u0094\u000b\u0013\u0003\u0013",
"\u0003\u0013\u0003\u0013\u0003\u0013\u0003\u0013\u0003\u0014\u0003\u0014",
"\u0003\u0014\u0003\u0014\u0007\u0014\u009f\n\u0014\f\u0014\u000e\u0014",
"\u00a2\u000b\u0014\u0003\u0014\u0003\u0014\u0003\u0092\u0002\u0015\u0003",
"\u0003\u0005\u0004\u0007\u0005\t\u0006\u000b\u0007\r\b\u000f\t\u0011",
"\n\u0013\u000b\u0015\f\u0017\u0002\u0019\u0002\u001b\u0002\u001d\r\u001f",
"\u0002!\u0002#\u000e%\u000f\'\u0010\u0003\u0002\u000b\u0004\u0002$$",
"^^\n\u0002$$11^^ddhhppttvv\u0005\u00022;CHch\u0003\u00022;\u0003\u0002",
"3;\u0004\u0002GGgg\u0004\u0002--//\u0005\u0002\u000b\f\u000f\u000f\"",
"\"\u0004\u0002\f\f\u000f\u000f\u0002\u00af\u0002\u0003\u0003\u0002\u0002",
"\u0002\u0002\u0005\u0003\u0002\u0002\u0002\u0002\u0007\u0003\u0002\u0002",
"\u0002\u0002\t\u0003\u0002\u0002\u0002\u0002\u000b\u0003\u0002\u0002",
"\u0002\u0002\r\u0003\u0002\u0002\u0002\u0002\u000f\u0003\u0002\u0002",
"\u0002\u0002\u0011\u0003\u0002\u0002\u0002\u0002\u0013\u0003\u0002\u0002",
"\u0002\u0002\u0015\u0003\u0002\u0002\u0002\u0002\u001d\u0003\u0002\u0002",
"\u0002\u0002#\u0003\u0002\u0002\u0002\u0002%\u0003\u0002\u0002\u0002",
"\u0002\'\u0003\u0002\u0002\u0002\u0003)\u0003\u0002\u0002\u0002\u0005",
"+\u0003\u0002\u0002\u0002\u0007-\u0003\u0002\u0002\u0002\t/\u0003\u0002",
"\u0002\u0002\u000b1\u0003\u0002\u0002\u0002\r3\u0003\u0002\u0002\u0002",
"\u000f5\u0003\u0002\u0002\u0002\u0011:\u0003\u0002\u0002\u0002\u0013",
"@\u0003\u0002\u0002\u0002\u0015E\u0003\u0002\u0002\u0002\u0017O\u0003",
"\u0002\u0002\u0002\u0019T\u0003\u0002\u0002\u0002\u001bZ\u0003\u0002",
"\u0002\u0002\u001ds\u0003\u0002\u0002\u0002\u001f}\u0003\u0002\u0002",
"\u0002!\u007f\u0003\u0002\u0002\u0002#\u0086\u0003\u0002\u0002\u0002",
"%\u008c\u0003\u0002\u0002\u0002\'\u009a\u0003\u0002\u0002\u0002)*\u0007",
"}\u0002\u0002*\u0004\u0003\u0002\u0002\u0002+,\u0007.\u0002\u0002,\u0006",
"\u0003\u0002\u0002\u0002-.\u0007\u007f\u0002\u0002.\b\u0003\u0002\u0002",
"\u0002/0\u0007<\u0002\u00020\n\u0003\u0002\u0002\u000212\u0007]\u0002",
"\u00022\f\u0003\u0002\u0002\u000234\u0007_\u0002\u00024\u000e\u0003",
"\u0002\u0002\u000256\u0007v\u0002\u000267\u0007t\u0002\u000278\u0007",
"w\u0002\u000289\u0007g\u0002\u00029\u0010\u0003\u0002\u0002\u0002:;",
"\u0007h\u0002\u0002;<\u0007c\u0002\u0002<=\u0007n\u0002\u0002=>\u0007",
"u\u0002\u0002>?\u0007g\u0002\u0002?\u0012\u0003\u0002\u0002\u0002@A",
"\u0007p\u0002\u0002AB\u0007w\u0002\u0002BC\u0007n\u0002\u0002CD\u0007",
"n\u0002\u0002D\u0014\u0003\u0002\u0002\u0002EJ\u0007$\u0002\u0002FI",
"\u0005\u0017\f\u0002GI\n\u0002\u0002\u0002HF\u0003\u0002\u0002\u0002",
"HG\u0003\u0002\u0002\u0002IL\u0003\u0002\u0002\u0002JH\u0003\u0002\u0002",
"\u0002JK\u0003\u0002\u0002\u0002KM\u0003\u0002\u0002\u0002LJ\u0003\u0002",
"\u0002\u0002MN\u0007$\u0002\u0002N\u0016\u0003\u0002\u0002\u0002OR\u0007",
"^\u0002\u0002PS\t\u0003\u0002\u0002QS\u0005\u0019\r\u0002RP\u0003\u0002",
"\u0002\u0002RQ\u0003\u0002\u0002\u0002S\u0018\u0003\u0002\u0002\u0002",
"TU\u0007w\u0002\u0002UV\u0005\u001b\u000e\u0002VW\u0005\u001b\u000e",
"\u0002WX\u0005\u001b\u000e\u0002XY\u0005\u001b\u000e\u0002Y\u001a\u0003",
"\u0002\u0002\u0002Z[\t\u0004\u0002\u0002[\u001c\u0003\u0002\u0002\u0002",
"\\^\u0007/\u0002\u0002]\\\u0003\u0002\u0002\u0002]^\u0003\u0002\u0002",
"\u0002^_\u0003\u0002\u0002\u0002_`\u0005\u001f\u0010\u0002`b\u00070",
"\u0002\u0002ac\t\u0005\u0002\u0002ba\u0003\u0002\u0002\u0002cd\u0003",
"\u0002\u0002\u0002db\u0003\u0002\u0002\u0002de\u0003\u0002\u0002\u0002",
"eg\u0003\u0002\u0002\u0002fh\u0005!\u0011\u0002gf\u0003\u0002\u0002",
"\u0002gh\u0003\u0002\u0002\u0002ht\u0003\u0002\u0002\u0002ik\u0007/",
"\u0002\u0002ji\u0003\u0002\u0002\u0002jk\u0003\u0002\u0002\u0002kl\u0003",
"\u0002\u0002\u0002lm\u0005\u001f\u0010\u0002mn\u0005!\u0011\u0002nt",
"\u0003\u0002\u0002\u0002oq\u0007/\u0002\u0002po\u0003\u0002\u0002\u0002",
"pq\u0003\u0002\u0002\u0002qr\u0003\u0002\u0002\u0002rt\u0005\u001f\u0010",
"\u0002s]\u0003\u0002\u0002\u0002sj\u0003\u0002\u0002\u0002sp\u0003\u0002",
"\u0002\u0002t\u001e\u0003\u0002\u0002\u0002u~\u00072\u0002\u0002vz\t",
"\u0006\u0002\u0002wy\t\u0005\u0002\u0002xw\u0003\u0002\u0002\u0002y",
"|\u0003\u0002\u0002\u0002zx\u0003\u0002\u0002\u0002z{\u0003\u0002\u0002",
"\u0002{~\u0003\u0002\u0002\u0002|z\u0003\u0002\u0002\u0002}u\u0003\u0002",
"\u0002\u0002}v\u0003\u0002\u0002\u0002~ \u0003\u0002\u0002\u0002\u007f",
"\u0081\t\u0007\u0002\u0002\u0080\u0082\t\b\u0002\u0002\u0081\u0080\u0003",
"\u0002\u0002\u0002\u0081\u0082\u0003\u0002\u0002\u0002\u0082\u0083\u0003",
"\u0002\u0002\u0002\u0083\u0084\u0005\u001f\u0010\u0002\u0084\"\u0003",
"\u0002\u0002\u0002\u0085\u0087\t\t\u0002\u0002\u0086\u0085\u0003\u0002",
"\u0002\u0002\u0087\u0088\u0003\u0002\u0002\u0002\u0088\u0086\u0003\u0002",
"\u0002\u0002\u0088\u0089\u0003\u0002\u0002\u0002\u0089\u008a\u0003\u0002",
"\u0002\u0002\u008a\u008b\b\u0012\u0002\u0002\u008b$\u0003\u0002\u0002",
"\u0002\u008c\u008d\u00071\u0002\u0002\u008d\u008e\u0007,\u0002\u0002",
"\u008e\u0092\u0003\u0002\u0002\u0002\u008f\u0091\u000b\u0002\u0002\u0002",
"\u0090\u008f\u0003\u0002\u0002\u0002\u0091\u0094\u0003\u0002\u0002\u0002",
"\u0092\u0093\u0003\u0002\u0002\u0002\u0092\u0090\u0003\u0002\u0002\u0002",
"\u0093\u0095\u0003\u0002\u0002\u0002\u0094\u0092\u0003\u0002\u0002\u0002",
"\u0095\u0096\u0007,\u0002\u0002\u0096\u0097\u00071\u0002\u0002\u0097",
"\u0098\u0003\u0002\u0002\u0002\u0098\u0099\b\u0013\u0002\u0002\u0099",
"&\u0003\u0002\u0002\u0002\u009a\u009b\u00071\u0002\u0002\u009b\u009c",
"\u00071\u0002\u0002\u009c\u00a0\u0003\u0002\u0002\u0002\u009d\u009f",
"\n\n\u0002\u0002\u009e\u009d\u0003\u0002\u0002\u0002\u009f\u00a2\u0003",
"\u0002\u0002\u0002\u00a0\u009e\u0003\u0002\u0002\u0002\u00a0\u00a1\u0003",
"\u0002\u0002\u0002\u00a1\u00a3\u0003\u0002\u0002\u0002\u00a2\u00a0\u0003",
"\u0002\u0002\u0002\u00a3\u00a4\b\u0014\u0002\u0002\u00a4(\u0003\u0002",
"\u0002\u0002\u0012\u0002HJR]dgjpsz}\u0081\u0088\u0092\u00a0\u0003\b",
"\u0002\u0002"].join("");
"\u0004\u0014\t\u0014\u0004\u0015\t\u0015\u0004\u0016\t\u0016\u0004\u0017",
"\t\u0017\u0004\u0018\t\u0018\u0004\u0019\t\u0019\u0004\u001a\t\u001a",
"\u0004\u001b\t\u001b\u0004\u001c\t\u001c\u0004\u001d\t\u001d\u0004\u001e",
"\t\u001e\u0004\u001f\t\u001f\u0004 \t \u0003\u0002\u0003\u0002\u0003",
"\u0003\u0003\u0003\u0003\u0004\u0003\u0004\u0003\u0005\u0003\u0005\u0003",
"\u0006\u0003\u0006\u0003\u0007\u0003\u0007\u0003\b\u0003\b\u0003\b\u0003",
"\b\u0003\b\u0003\t\u0003\t\u0003\t\u0003\t\u0003\t\u0003\t\u0003\n\u0003",
"\n\u0003\n\u0003\n\u0003\n\u0003\u000b\u0003\u000b\u0003\u000b\u0005",
"\u000ba\n\u000b\u0003\f\u0003\f\u0003\f\u0003\f\u0007\fg\n\f\f\f\u000e",
"\fj\u000b\f\u0003\r\u0003\r\u0003\r\u0003\r\u0003\r\u0003\r\u0005\r",
"r\n\r\u0003\u000e\u0003\u000e\u0003\u000f\u0003\u000f\u0003\u0010\u0003",
"\u0010\u0003\u0011\u0003\u0011\u0003\u0012\u0003\u0012\u0003\u0013\u0003",
"\u0013\u0003\u0014\u0003\u0014\u0003\u0015\u0003\u0015\u0003\u0016\u0003",
"\u0016\u0003\u0016\u0007\u0016\u0087\n\u0016\f\u0016\u000e\u0016\u008a",
"\u000b\u0016\u0003\u0016\u0003\u0016\u0003\u0017\u0003\u0017\u0003\u0017",
"\u0007\u0017\u0091\n\u0017\f\u0017\u000e\u0017\u0094\u000b\u0017\u0003",
"\u0017\u0003\u0017\u0003\u0018\u0003\u0018\u0003\u0018\u0005\u0018\u009b",
"\n\u0018\u0003\u0019\u0003\u0019\u0003\u0019\u0003\u0019\u0003\u0019",
"\u0003\u0019\u0003\u001a\u0003\u001a\u0003\u001b\u0005\u001b\u00a6\n",
"\u001b\u0003\u001b\u0003\u001b\u0003\u001b\u0006\u001b\u00ab\n\u001b",
"\r\u001b\u000e\u001b\u00ac\u0003\u001b\u0005\u001b\u00b0\n\u001b\u0003",
"\u001b\u0005\u001b\u00b3\n\u001b\u0003\u001b\u0003\u001b\u0003\u001b",
"\u0003\u001b\u0005\u001b\u00b9\n\u001b\u0003\u001b\u0005\u001b\u00bc",
"\n\u001b\u0003\u001c\u0003\u001c\u0003\u001c\u0007\u001c\u00c1\n\u001c",
"\f\u001c\u000e\u001c\u00c4\u000b\u001c\u0005\u001c\u00c6\n\u001c\u0003",
"\u001d\u0003\u001d\u0005\u001d\u00ca\n\u001d\u0003\u001d\u0003\u001d",
"\u0003\u001e\u0006\u001e\u00cf\n\u001e\r\u001e\u000e\u001e\u00d0\u0003",
"\u001e\u0003\u001e\u0003\u001f\u0003\u001f\u0003\u001f\u0003\u001f\u0007",
"\u001f\u00d9\n\u001f\f\u001f\u000e\u001f\u00dc\u000b\u001f\u0003\u001f",
"\u0003\u001f\u0003\u001f\u0003\u001f\u0003\u001f\u0003 \u0003 \u0003",
" \u0003 \u0007 \u00e7\n \f \u000e \u00ea\u000b \u0003 \u0003 \u0003",
"\u00da\u0002!\u0003\u0003\u0005\u0004\u0007\u0005\t\u0006\u000b\u0007",
"\r\b\u000f\t\u0011\n\u0013\u000b\u0015\f\u0017\u0002\u0019\u0002\u001b",
"\u0002\u001d\u0002\u001f\u0002!\u0002#\u0002%\u0002\'\u0002)\u0002+",
"\u0002-\u0002/\u00021\u00023\u00025\r7\u00029\u0002;\u000e=\u000f?\u0010",
"\u0003\u0002\u000e\u0007\u0002##%(--`a\u0080\u0080\u0004\u0002C\\c|",
"\u0003\u00022;\u0004\u0002))^^\u0004\u0002$$^^\n\u0002$$11^^ddhhppt",
"tvv\u0005\u00022;CHch\u0003\u00023;\u0004\u0002GGgg\u0004\u0002--//",
"\u0005\u0002\u000b\f\u000f\u000f\"\"\u0004\u0002\f\f\u000f\u000f\u0002",
"\u00f7\u0002\u0003\u0003\u0002\u0002\u0002\u0002\u0005\u0003\u0002\u0002",
"\u0002\u0002\u0007\u0003\u0002\u0002\u0002\u0002\t\u0003\u0002\u0002",
"\u0002\u0002\u000b\u0003\u0002\u0002\u0002\u0002\r\u0003\u0002\u0002",
"\u0002\u0002\u000f\u0003\u0002\u0002\u0002\u0002\u0011\u0003\u0002\u0002",
"\u0002\u0002\u0013\u0003\u0002\u0002\u0002\u0002\u0015\u0003\u0002\u0002",
"\u0002\u00025\u0003\u0002\u0002\u0002\u0002;\u0003\u0002\u0002\u0002",
"\u0002=\u0003\u0002\u0002\u0002\u0002?\u0003\u0002\u0002\u0002\u0003",
"A\u0003\u0002\u0002\u0002\u0005C\u0003\u0002\u0002\u0002\u0007E\u0003",
"\u0002\u0002\u0002\tG\u0003\u0002\u0002\u0002\u000bI\u0003\u0002\u0002",
"\u0002\rK\u0003\u0002\u0002\u0002\u000fM\u0003\u0002\u0002\u0002\u0011",
"R\u0003\u0002\u0002\u0002\u0013X\u0003\u0002\u0002\u0002\u0015`\u0003",
"\u0002\u0002\u0002\u0017b\u0003\u0002\u0002\u0002\u0019q\u0003\u0002",
"\u0002\u0002\u001bs\u0003\u0002\u0002\u0002\u001du\u0003\u0002\u0002",
"\u0002\u001fw\u0003\u0002\u0002\u0002!y\u0003\u0002\u0002\u0002#{\u0003",
"\u0002\u0002\u0002%}\u0003\u0002\u0002\u0002\'\u007f\u0003\u0002\u0002",
"\u0002)\u0081\u0003\u0002\u0002\u0002+\u0083\u0003\u0002\u0002\u0002",
"-\u008d\u0003\u0002\u0002\u0002/\u0097\u0003\u0002\u0002\u00021\u009c",
"\u0003\u0002\u0002\u00023\u00a2\u0003\u0002\u0002\u00025\u00bb\u0003",
"\u0002\u0002\u00027\u00c5\u0003\u0002\u0002\u00029\u00c7\u0003\u0002",
"\u0002\u0002;\u00ce\u0003\u0002\u0002\u0002=\u00d4\u0003\u0002\u0002",
"\u0002?\u00e2\u0003\u0002\u0002\u0002AB\u0007}\u0002\u0002B\u0004\u0003",
"\u0002\u0002\u0002CD\u0007.\u0002\u0002D\u0006\u0003\u0002\u0002\u0002",
"EF\u0007\u007f\u0002\u0002F\b\u0003\u0002\u0002\u0002GH\u0007<\u0002",
"\u0002H\n\u0003\u0002\u0002\u0002IJ\u0007]\u0002\u0002J\f\u0003\u0002",
"\u0002\u0002KL\u0007_\u0002\u0002L\u000e\u0003\u0002\u0002\u0002MN\u0007",
"v\u0002\u0002NO\u0007t\u0002\u0002OP\u0007w\u0002\u0002PQ\u0007g\u0002",
"\u0002Q\u0010\u0003\u0002\u0002\u0002RS\u0007h\u0002\u0002ST\u0007c",
"\u0002\u0002TU\u0007n\u0002\u0002UV\u0007u\u0002\u0002VW\u0007g\u0002",
"\u0002W\u0012\u0003\u0002\u0002\u0002XY\u0007p\u0002\u0002YZ\u0007w",
"\u0002\u0002Z[\u0007n\u0002\u0002[\\\u0007n\u0002\u0002\\\u0014\u0003",
"\u0002\u0002\u0002]a\u0005+\u0016\u0002^a\u0005-\u0017\u0002_a\u0005",
"\u0017\f\u0002`]\u0003\u0002\u0002\u0002`^\u0003\u0002\u0002\u0002`",
"_\u0003\u0002\u0002\u0002a\u0016\u0003\u0002\u0002\u0002bh\u0005\u0019",
"\r\u0002cg\u0005\u0019\r\u0002dg\u0005)\u0015\u0002eg\u0005%\u0013\u0002",
"fc\u0003\u0002\u0002\u0002fd\u0003\u0002\u0002\u0002fe\u0003\u0002\u0002",
"\u0002gj\u0003\u0002\u0002\u0002hf\u0003\u0002\u0002\u0002hi\u0003\u0002",
"\u0002\u0002i\u0018\u0003\u0002\u0002\u0002jh\u0003\u0002\u0002\u0002",
"kr\u0005\u001d\u000f\u0002lr\u0005\u001b\u000e\u0002mr\u0005\u001f\u0010",
"\u0002nr\u0005!\u0011\u0002or\u0005#\u0012\u0002pr\u0005\'\u0014\u0002",
"qk\u0003\u0002\u0002\u0002ql\u0003\u0002\u0002\u0002qm\u0003\u0002\u0002",
"\u0002qn\u0003\u0002\u0002\u0002qo\u0003\u0002\u0002\u0002qp\u0003\u0002",
"\u0002\u0002r\u001a\u0003\u0002\u0002\u0002st\t\u0002\u0002\u0002t\u001c",
"\u0003\u0002\u0002\u0002uv\t\u0003\u0002\u0002v\u001e\u0003\u0002\u0002",
"\u0002wx\u0007,\u0002\u0002x \u0003\u0002\u0002\u0002yz\u0007A\u0002",
"\u0002z\"\u0003\u0002\u0002\u0002{|\u00070\u0002\u0002|$\u0003\u0002",
"\u0002\u0002}~\u0007/\u0002\u0002~&\u0003\u0002\u0002\u0002\u007f\u0080",
"\u0007B\u0002\u0002\u0080(\u0003\u0002\u0002\u0002\u0081\u0082\t\u0004",
"\u0002\u0002\u0082*\u0003\u0002\u0002\u0002\u0083\u0088\u0007)\u0002",
"\u0002\u0084\u0087\u0005/\u0018\u0002\u0085\u0087\n\u0005\u0002\u0002",
"\u0086\u0084\u0003\u0002\u0002\u0002\u0086\u0085\u0003\u0002\u0002\u0002",
"\u0087\u008a\u0003\u0002\u0002\u0002\u0088\u0086\u0003\u0002\u0002\u0002",
"\u0088\u0089\u0003\u0002\u0002\u0002\u0089\u008b\u0003\u0002\u0002\u0002",
"\u008a\u0088\u0003\u0002\u0002\u0002\u008b\u008c\u0007)\u0002\u0002",
"\u008c,\u0003\u0002\u0002\u0002\u008d\u0092\u0007$\u0002\u0002\u008e",
"\u0091\u0005/\u0018\u0002\u008f\u0091\n\u0006\u0002\u0002\u0090\u008e",
"\u0003\u0002\u0002\u0002\u0090\u008f\u0003\u0002\u0002\u0002\u0091\u0094",
"\u0003\u0002\u0002\u0002\u0092\u0090\u0003\u0002\u0002\u0002\u0092\u0093",
"\u0003\u0002\u0002\u0002\u0093\u0095\u0003\u0002\u0002\u0002\u0094\u0092",
"\u0003\u0002\u0002\u0002\u0095\u0096\u0007$\u0002\u0002\u0096.\u0003",
"\u0002\u0002\u0002\u0097\u009a\u0007^\u0002\u0002\u0098\u009b\t\u0007",
"\u0002\u0002\u0099\u009b\u00051\u0019\u0002\u009a\u0098\u0003\u0002",
"\u0002\u0002\u009a\u0099\u0003\u0002\u0002\u0002\u009b0\u0003\u0002",
"\u0002\u0002\u009c\u009d\u0007w\u0002\u0002\u009d\u009e\u00053\u001a",
"\u0002\u009e\u009f\u00053\u001a\u0002\u009f\u00a0\u00053\u001a\u0002",
"\u00a0\u00a1\u00053\u001a\u0002\u00a12\u0003\u0002\u0002\u0002\u00a2",
"\u00a3\t\b\u0002\u0002\u00a34\u0003\u0002\u0002\u0002\u00a4\u00a6\u0007",
"/\u0002\u0002\u00a5\u00a4\u0003\u0002\u0002\u0002\u00a5\u00a6\u0003",
"\u0002\u0002\u0002\u00a6\u00a7\u0003\u0002\u0002\u0002\u00a7\u00a8\u0005",
"7\u001c\u0002\u00a8\u00aa\u00070\u0002\u0002\u00a9\u00ab\t\u0004\u0002",
"\u0002\u00aa\u00a9\u0003\u0002\u0002\u0002\u00ab\u00ac\u0003\u0002\u0002",
"\u0002\u00ac\u00aa\u0003\u0002\u0002\u0002\u00ac\u00ad\u0003\u0002\u0002",
"\u0002\u00ad\u00af\u0003\u0002\u0002\u0002\u00ae\u00b0\u00059\u001d",
"\u0002\u00af\u00ae\u0003\u0002\u0002\u0002\u00af\u00b0\u0003\u0002\u0002",
"\u0002\u00b0\u00bc\u0003\u0002\u0002\u0002\u00b1\u00b3\u0007/\u0002",
"\u0002\u00b2\u00b1\u0003\u0002\u0002\u0002\u00b2\u00b3\u0003\u0002\u0002",
"\u0002\u00b3\u00b4\u0003\u0002\u0002\u0002\u00b4\u00b5\u00057\u001c",
"\u0002\u00b5\u00b6\u00059\u001d\u0002\u00b6\u00bc\u0003\u0002\u0002",
"\u0002\u00b7\u00b9\u0007/\u0002\u0002\u00b8\u00b7\u0003\u0002\u0002",
"\u0002\u00b8\u00b9\u0003\u0002\u0002\u0002\u00b9\u00ba\u0003\u0002\u0002",
"\u0002\u00ba\u00bc\u00057\u001c\u0002\u00bb\u00a5\u0003\u0002\u0002",
"\u0002\u00bb\u00b2\u0003\u0002\u0002\u0002\u00bb\u00b8\u0003\u0002\u0002",
"\u0002\u00bc6\u0003\u0002\u0002\u0002\u00bd\u00c6\u00072\u0002\u0002",
"\u00be\u00c2\t\t\u0002\u0002\u00bf\u00c1\t\u0004\u0002\u0002\u00c0\u00bf",
"\u0003\u0002\u0002\u0002\u00c1\u00c4\u0003\u0002\u0002\u0002\u00c2\u00c0",
"\u0003\u0002\u0002\u0002\u00c2\u00c3\u0003\u0002\u0002\u0002\u00c3\u00c6",
"\u0003\u0002\u0002\u0002\u00c4\u00c2\u0003\u0002\u0002\u0002\u00c5\u00bd",
"\u0003\u0002\u0002\u0002\u00c5\u00be\u0003\u0002\u0002\u0002\u00c68",
"\u0003\u0002\u0002\u0002\u00c7\u00c9\t\n\u0002\u0002\u00c8\u00ca\t\u000b",
"\u0002\u0002\u00c9\u00c8\u0003\u0002\u0002\u0002\u00c9\u00ca\u0003\u0002",
"\u0002\u0002\u00ca\u00cb\u0003\u0002\u0002\u0002\u00cb\u00cc\u00057",
"\u001c\u0002\u00cc:\u0003\u0002\u0002\u0002\u00cd\u00cf\t\f\u0002\u0002",
"\u00ce\u00cd\u0003\u0002\u0002\u0002\u00cf\u00d0\u0003\u0002\u0002\u0002",
"\u00d0\u00ce\u0003\u0002\u0002\u0002\u00d0\u00d1\u0003\u0002\u0002\u0002",
"\u00d1\u00d2\u0003\u0002\u0002\u0002\u00d2\u00d3\b\u001e\u0002\u0002",
"\u00d3<\u0003\u0002\u0002\u0002\u00d4\u00d5\u00071\u0002\u0002\u00d5",
"\u00d6\u0007,\u0002\u0002\u00d6\u00da\u0003\u0002\u0002\u0002\u00d7",
"\u00d9\u000b\u0002\u0002\u0002\u00d8\u00d7\u0003\u0002\u0002\u0002\u00d9",
"\u00dc\u0003\u0002\u0002\u0002\u00da\u00db\u0003\u0002\u0002\u0002\u00da",
"\u00d8\u0003\u0002\u0002\u0002\u00db\u00dd\u0003\u0002\u0002\u0002\u00dc",
"\u00da\u0003\u0002\u0002\u0002\u00dd\u00de\u0007,\u0002\u0002\u00de",
"\u00df\u00071\u0002\u0002\u00df\u00e0\u0003\u0002\u0002\u0002\u00e0",
"\u00e1\b\u001f\u0002\u0002\u00e1>\u0003\u0002\u0002\u0002\u00e2\u00e3",
"\u00071\u0002\u0002\u00e3\u00e4\u00071\u0002\u0002\u00e4\u00e8\u0003",
"\u0002\u0002\u0002\u00e5\u00e7\n\r\u0002\u0002\u00e6\u00e5\u0003\u0002",
"\u0002\u0002\u00e7\u00ea\u0003\u0002\u0002\u0002\u00e8\u00e6\u0003\u0002",
"\u0002\u0002\u00e8\u00e9\u0003\u0002\u0002\u0002\u00e9\u00eb\u0003\u0002",
"\u0002\u0002\u00ea\u00e8\u0003\u0002\u0002\u0002\u00eb\u00ec\b \u0002",
"\u0002\u00ec@\u0003\u0002\u0002\u0002\u0018\u0002`fhq\u0086\u0088\u0090",
"\u0092\u009a\u00a5\u00ac\u00af\u00b2\u00b8\u00bb\u00c2\u00c5\u00c9\u00d0",
"\u00da\u00e8\u0003\b\u0002\u0002"].join("");

@@ -191,5 +259,7 @@

"T__5", "T__6", "T__7", "T__8", "STRING",
"ESC", "UNICODE", "HEX", "NUMBER", "INT",
"EXP", "WHITESPACE", "MULTI_LINE_COMMENT",
"SINGLE_LINE_COMMENT" ];
"ASTRING", "STRSYM", "SPECHAR", "LETTER",
"STAR", "QUESTION", "DOT", "MINUS", "AtSIGN",
"DIGIT", "Q1STRING", "Q2STRING", "ESC",
"UNICODE", "HEX", "NUMBER", "INT", "EXP",
"WHITESPACE", "MULTI_LINE_COMMENT", "SINGLE_LINE_COMMENT" ];

@@ -196,0 +266,0 @@ JSONLexer.prototype.grammarFileName = "JSON.g4";

@@ -27,54 +27,75 @@ // Generated from JSON.g4 by ANTLR 4.7.1

function unquote(s) {
return JSON.parse(s); // unquote for free
function unquoteNumber(s) {
return JSON.parse(s); // unquote
}
function unquoteString(s) {
if(s.startsWith("'") && s.endsWith("'")) {
let ss = s.substring(1,s.length-1);
ss = ss.replace(/\\/g, '\\\\')
ss = ss.replace(/\"/g, '\\"')
s = '"' + ss + '"';
}
if(!s.startsWith('"') && !s.endsWith('"') && !isCharNumber(s[0])) { // plain string
s = '"' + s + '"';
}
return JSON.parse(s); // unquote
function isCharNumber(c) {
return c >= '0' && c <= '9';
}
}
var grammarFileName = "JSON.g4";
var serializedATN = ["\u0003\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964",
"\u0003\u0010H\u0004\u0002\t\u0002\u0004\u0003\t\u0003\u0004\u0004\t",
"\u0003\u0010N\u0004\u0002\t\u0002\u0004\u0003\t\u0003\u0004\u0004\t",
"\u0004\u0004\u0005\t\u0005\u0004\u0006\t\u0006\u0003\u0002\u0003\u0002",
"\u0005\u0002\u000f\n\u0002\u0003\u0003\u0003\u0003\u0003\u0003\u0003",
"\u0003\u0007\u0003\u0015\n\u0003\f\u0003\u000e\u0003\u0018\u000b\u0003",
"\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0003\u0005\u0003\u001e\n",
"\u0003\u0003\u0004\u0003\u0004\u0003\u0004\u0003\u0004\u0003\u0004\u0003",
"\u0004\u0003\u0005\u0003\u0005\u0003\u0005\u0003\u0005\u0003\u0005\u0003",
"\u0005\u0003\u0005\u0003\u0005\u0003\u0005\u0007\u0005/\n\u0005\f\u0005",
"\u000e\u00052\u000b\u0005\u0003\u0005\u0003\u0005\u0003\u0005\u0003",
"\u0005\u0005\u00058\n\u0005\u0003\u0006\u0003\u0006\u0003\u0006\u0003",
"\u0006\u0003\u0006\u0003\u0006\u0003\u0006\u0003\u0006\u0003\u0006\u0003",
"\u0006\u0003\u0006\u0003\u0006\u0005\u0006F\n\u0006\u0003\u0006\u0002",
"\u0002\u0007\u0002\u0004\u0006\b\n\u0002\u0002\u0002M\u0002\u000e\u0003",
"\u0002\u0002\u0002\u0004\u001d\u0003\u0002\u0002\u0002\u0006\u001f\u0003",
"\u0002\u0002\u0002\b7\u0003\u0002\u0002\u0002\nE\u0003\u0002\u0002\u0002",
"\f\u000f\u0005\u0004\u0003\u0002\r\u000f\u0005\b\u0005\u0002\u000e\f",
"\u0003\u0002\u0002\u0002\u000e\r\u0003\u0002\u0002\u0002\u000f\u0003",
"\u0003\u0002\u0002\u0002\u0010\u0011\u0007\u0003\u0002\u0002\u0011\u0016",
"\u0005\u0006\u0004\u0002\u0012\u0013\u0007\u0004\u0002\u0002\u0013\u0015",
"\u0005\u0006\u0004\u0002\u0014\u0012\u0003\u0002\u0002\u0002\u0015\u0018",
"\u0003\u0002\u0002\u0002\u0016\u0014\u0003\u0002\u0002\u0002\u0016\u0017",
"\u0003\u0002\u0002\u0002\u0017\u0019\u0003\u0002\u0002\u0002\u0018\u0016",
"\u0003\u0002\u0002\u0002\u0019\u001a\u0007\u0005\u0002\u0002\u001a\u001e",
"\u0003\u0002\u0002\u0002\u001b\u001c\u0007\u0003\u0002\u0002\u001c\u001e",
"\u0007\u0005\u0002\u0002\u001d\u0010\u0003\u0002\u0002\u0002\u001d\u001b",
"\u0003\u0002\u0002\u0002\u001e\u0005\u0003\u0002\u0002\u0002\u001f ",
"\u0007\f\u0002\u0002 !\b\u0004\u0001\u0002!\"\u0007\u0006\u0002\u0002",
"\"#\u0005\n\u0006\u0002#$\b\u0004\u0001\u0002$\u0007\u0003\u0002\u0002",
"\u0002%&\u0007\u0007\u0002\u0002&\'\b\u0005\u0001\u0002\'(\u0005\n\u0006",
"\u0002(0\b\u0005\u0001\u0002)*\u0007\u0004\u0002\u0002*+\b\u0005\u0001",
"\u0002+,\u0005\n\u0006\u0002,-\b\u0005\u0001\u0002-/\u0003\u0002\u0002",
"\u0002.)\u0003\u0002\u0002\u0002/2\u0003\u0002\u0002\u00020.\u0003\u0002",
"\u0002\u000201\u0003\u0002\u0002\u000213\u0003\u0002\u0002\u000220\u0003",
"\u0002\u0002\u000234\u0007\b\u0002\u000248\u0003\u0002\u0002\u00025",
"6\u0007\u0007\u0002\u000268\u0007\b\u0002\u00027%\u0003\u0002\u0002",
"\u000275\u0003\u0002\u0002\u00028\t\u0003\u0002\u0002\u00029:\u0007",
"\f\u0002\u0002:F\b\u0006\u0001\u0002;<\u0007\r\u0002\u0002<F\b\u0006",
"\u0001\u0002=F\u0005\u0004\u0003\u0002>F\u0005\b\u0005\u0002?@\u0007",
"\t\u0002\u0002@F\b\u0006\u0001\u0002AB\u0007\n\u0002\u0002BF\b\u0006",
"\u0001\u0002CD\u0007\u000b\u0002\u0002DF\b\u0006\u0001\u0002E9\u0003",
"\u0002\u0002\u0002E;\u0003\u0002\u0002\u0002E=\u0003\u0002\u0002\u0002",
"E>\u0003\u0002\u0002\u0002E?\u0003\u0002\u0002\u0002EA\u0003\u0002\u0002",
"\u0002EC\u0003\u0002\u0002\u0002F\u000b\u0003\u0002\u0002\u0002\b\u000e",
"\u0016\u001d07E"].join("");
"\u0003\u0003\u0005\u0003\u001b\n\u0003\u0003\u0003\u0003\u0003\u0003",
"\u0003\u0003\u0003\u0005\u0003!\n\u0003\u0003\u0004\u0003\u0004\u0003",
"\u0004\u0003\u0004\u0003\u0004\u0003\u0004\u0003\u0005\u0003\u0005\u0003",
"\u0005\u0003\u0005\u0003\u0005\u0003\u0005\u0003\u0005\u0003\u0005\u0003",
"\u0005\u0007\u00052\n\u0005\f\u0005\u000e\u00055\u000b\u0005\u0003\u0005",
"\u0005\u00058\n\u0005\u0003\u0005\u0003\u0005\u0003\u0005\u0003\u0005",
"\u0005\u0005>\n\u0005\u0003\u0006\u0003\u0006\u0003\u0006\u0003\u0006",
"\u0003\u0006\u0003\u0006\u0003\u0006\u0003\u0006\u0003\u0006\u0003\u0006",
"\u0003\u0006\u0003\u0006\u0005\u0006L\n\u0006\u0003\u0006\u0002\u0002",
"\u0007\u0002\u0004\u0006\b\n\u0002\u0002\u0002U\u0002\u000e\u0003\u0002",
"\u0002\u0002\u0004 \u0003\u0002\u0002\u0002\u0006\"\u0003\u0002\u0002",
"\u0002\b=\u0003\u0002\u0002\u0002\nK\u0003\u0002\u0002\u0002\f\u000f",
"\u0005\u0004\u0003\u0002\r\u000f\u0005\b\u0005\u0002\u000e\f\u0003\u0002",
"\u0002\u0002\u000e\r\u0003\u0002\u0002\u0002\u000f\u0003\u0003\u0002",
"\u0002\u0002\u0010\u0011\u0007\u0003\u0002\u0002\u0011\u0016\u0005\u0006",
"\u0004\u0002\u0012\u0013\u0007\u0004\u0002\u0002\u0013\u0015\u0005\u0006",
"\u0004\u0002\u0014\u0012\u0003\u0002\u0002\u0002\u0015\u0018\u0003\u0002",
"\u0002\u0002\u0016\u0014\u0003\u0002\u0002\u0002\u0016\u0017\u0003\u0002",
"\u0002\u0002\u0017\u001a\u0003\u0002\u0002\u0002\u0018\u0016\u0003\u0002",
"\u0002\u0002\u0019\u001b\u0007\u0004\u0002\u0002\u001a\u0019\u0003\u0002",
"\u0002\u0002\u001a\u001b\u0003\u0002\u0002\u0002\u001b\u001c\u0003\u0002",
"\u0002\u0002\u001c\u001d\u0007\u0005\u0002\u0002\u001d!\u0003\u0002",
"\u0002\u0002\u001e\u001f\u0007\u0003\u0002\u0002\u001f!\u0007\u0005",
"\u0002\u0002 \u0010\u0003\u0002\u0002\u0002 \u001e\u0003\u0002\u0002",
"\u0002!\u0005\u0003\u0002\u0002\u0002\"#\u0007\f\u0002\u0002#$\b\u0004",
"\u0001\u0002$%\u0007\u0006\u0002\u0002%&\u0005\n\u0006\u0002&\'\b\u0004",
"\u0001\u0002\'\u0007\u0003\u0002\u0002\u0002()\u0007\u0007\u0002\u0002",
")*\b\u0005\u0001\u0002*+\u0005\n\u0006\u0002+3\b\u0005\u0001\u0002,",
"-\u0007\u0004\u0002\u0002-.\b\u0005\u0001\u0002./\u0005\n\u0006\u0002",
"/0\b\u0005\u0001\u000202\u0003\u0002\u0002\u00021,\u0003\u0002\u0002",
"\u000225\u0003\u0002\u0002\u000231\u0003\u0002\u0002\u000234\u0003\u0002",
"\u0002\u000247\u0003\u0002\u0002\u000253\u0003\u0002\u0002\u000268\u0007",
"\u0004\u0002\u000276\u0003\u0002\u0002\u000278\u0003\u0002\u0002\u0002",
"89\u0003\u0002\u0002\u00029:\u0007\b\u0002\u0002:>\u0003\u0002\u0002",
"\u0002;<\u0007\u0007\u0002\u0002<>\u0007\b\u0002\u0002=(\u0003\u0002",
"\u0002\u0002=;\u0003\u0002\u0002\u0002>\t\u0003\u0002\u0002\u0002?@",
"\u0007\r\u0002\u0002@L\b\u0006\u0001\u0002AB\u0007\f\u0002\u0002BL\b",
"\u0006\u0001\u0002CL\u0005\u0004\u0003\u0002DL\u0005\b\u0005\u0002E",
"F\u0007\t\u0002\u0002FL\b\u0006\u0001\u0002GH\u0007\n\u0002\u0002HL",
"\b\u0006\u0001\u0002IJ\u0007\u000b\u0002\u0002JL\b\u0006\u0001\u0002",
"K?\u0003\u0002\u0002\u0002KA\u0003\u0002\u0002\u0002KC\u0003\u0002\u0002",
"\u0002KD\u0003\u0002\u0002\u0002KE\u0003\u0002\u0002\u0002KG\u0003\u0002",
"\u0002\u0002KI\u0003\u0002\u0002\u0002L\u000b\u0003\u0002\u0002\u0002",
"\n\u000e\u0016\u001a 37=K"].join("");

@@ -243,5 +264,5 @@

try {
this.state = 27;
this.state = 30;
this._errHandler.sync(this);
var la_ = this._interp.adaptivePredict(this._input,2,this._ctx);
var la_ = this._interp.adaptivePredict(this._input,3,this._ctx);
switch(la_) {

@@ -256,13 +277,24 @@ case 1:

this._errHandler.sync(this);
_la = this._input.LA(1);
while(_la===JSONParser.T__1) {
this.state = 16;
this.match(JSONParser.T__1);
this.state = 17;
this.pair(localctx.ret.value);
var _alt = this._interp.adaptivePredict(this._input,1,this._ctx)
while(_alt!=2 && _alt!=antlr4.atn.ATN.INVALID_ALT_NUMBER) {
if(_alt===1) {
this.state = 16;
this.match(JSONParser.T__1);
this.state = 17;
this.pair(localctx.ret.value);
}
this.state = 22;
this._errHandler.sync(this);
_la = this._input.LA(1);
_alt = this._interp.adaptivePredict(this._input,1,this._ctx);
}
this.state = 23;
this.state = 24;
this._errHandler.sync(this);
_la = this._input.LA(1);
if(_la===JSONParser.T__1) {
this.state = 23;
this.match(JSONParser.T__1);
}
this.state = 26;
this.match(JSONParser.T__2);

@@ -273,5 +305,5 @@ break;

this.enterOuterAlt(localctx, 2);
this.state = 25;
this.state = 28;
this.match(JSONParser.T__0);
this.state = 26;
this.state = 29;
this.match(JSONParser.T__2);

@@ -337,10 +369,10 @@ break;

this.enterOuterAlt(localctx, 1);
this.state = 29;
this.state = 32;
localctx._STRING = this.match(JSONParser.STRING);
this.state = 31;
this.state = 34;
this.match(JSONParser.T__3);
this.state = 32;
this.state = 35;
this.value(localctx.val);
localctx.ret[unquote((localctx._STRING===null ? null : localctx._STRING.text))]=localctx.val
localctx.ret[unquoteString((localctx._STRING===null ? null : localctx._STRING.text))]=localctx.val
this._ctx.stop = this._input.LT(-1);

@@ -404,29 +436,40 @@ getLoc(localctx,localctx.val)

try {
this.state = 53;
this.state = 59;
this._errHandler.sync(this);
var la_ = this._interp.adaptivePredict(this._input,4,this._ctx);
var la_ = this._interp.adaptivePredict(this._input,6,this._ctx);
switch(la_) {
case 1:
this.enterOuterAlt(localctx, 1);
this.state = 35;
this.state = 38;
this.match(JSONParser.T__4);
localctx.val={}
this.state = 37;
this.state = 40;
this.value(localctx.val);
localctx.ret.value[ID++]=localctx.val
this.state = 46;
this.state = 49;
this._errHandler.sync(this);
var _alt = this._interp.adaptivePredict(this._input,4,this._ctx)
while(_alt!=2 && _alt!=antlr4.atn.ATN.INVALID_ALT_NUMBER) {
if(_alt===1) {
this.state = 42;
this.match(JSONParser.T__1);
localctx.val={}
this.state = 44;
this.value(localctx.val);
localctx.ret.value[ID++]=localctx.val
}
this.state = 51;
this._errHandler.sync(this);
_alt = this._interp.adaptivePredict(this._input,4,this._ctx);
}
this.state = 53;
this._errHandler.sync(this);
_la = this._input.LA(1);
while(_la===JSONParser.T__1) {
this.state = 39;
if(_la===JSONParser.T__1) {
this.state = 52;
this.match(JSONParser.T__1);
localctx.val={}
this.state = 41;
this.value(localctx.val);
localctx.ret.value[ID++]=localctx.val
this.state = 48;
this._errHandler.sync(this);
_la = this._input.LA(1);
}
this.state = 49;
this.state = 55;
this.match(JSONParser.T__5);

@@ -437,5 +480,5 @@ break;

this.enterOuterAlt(localctx, 2);
this.state = 51;
this.state = 57;
this.match(JSONParser.T__4);
this.state = 52;
this.state = 58;
this.match(JSONParser.T__5);

@@ -472,4 +515,4 @@ break;

this.ret = null
this._NUMBER = null; // Token
this._STRING = null; // Token
this._NUMBER = null; // Token
this.ret = ret || null;

@@ -482,2 +525,6 @@ return this;

ValueContext.prototype.NUMBER = function() {
return this.getToken(JSONParser.NUMBER, 0);
};
ValueContext.prototype.STRING = function() {

@@ -487,6 +534,2 @@ return this.getToken(JSONParser.STRING, 0);

ValueContext.prototype.NUMBER = function() {
return this.getToken(JSONParser.NUMBER, 0);
};
ValueContext.prototype.obj = function() {

@@ -511,20 +554,20 @@ return this.getTypedRuleContext(ObjContext,0);

try {
this.state = 67;
this.state = 73;
this._errHandler.sync(this);
switch(this._input.LA(1)) {
case JSONParser.STRING:
case JSONParser.NUMBER:
this.enterOuterAlt(localctx, 1);
this.state = 55;
localctx._STRING = this.match(JSONParser.STRING);
localctx.ret.type="string"; localctx.ret.value=unquote((localctx._STRING===null ? null : localctx._STRING.text));
this.state = 61;
localctx._NUMBER = this.match(JSONParser.NUMBER);
localctx.ret.type="number"; localctx.ret.value=unquoteNumber((localctx._NUMBER===null ? null : localctx._NUMBER.text));
break;
case JSONParser.NUMBER:
case JSONParser.STRING:
this.enterOuterAlt(localctx, 2);
this.state = 57;
localctx._NUMBER = this.match(JSONParser.NUMBER);
localctx.ret.type="number"; localctx.ret.value=unquote((localctx._NUMBER===null ? null : localctx._NUMBER.text));
this.state = 63;
localctx._STRING = this.match(JSONParser.STRING);
localctx.ret.type="string"; localctx.ret.value=unquoteString((localctx._STRING===null ? null : localctx._STRING.text));
break;
case JSONParser.T__0:
this.enterOuterAlt(localctx, 3);
this.state = 59;
this.state = 65;
this.obj(localctx.ret);

@@ -534,3 +577,3 @@ break;

this.enterOuterAlt(localctx, 4);
this.state = 60;
this.state = 66;
this.array(localctx.ret);

@@ -540,3 +583,3 @@ break;

this.enterOuterAlt(localctx, 5);
this.state = 61;
this.state = 67;
this.match(JSONParser.T__6);

@@ -547,3 +590,3 @@ localctx.ret.type="boolean"; localctx.ret.value=true;

this.enterOuterAlt(localctx, 6);
this.state = 63;
this.state = 69;
this.match(JSONParser.T__7);

@@ -554,3 +597,3 @@ localctx.ret.type="boolean"; localctx.ret.value=false;

this.enterOuterAlt(localctx, 7);
this.state = 65;
this.state = 71;
this.match(JSONParser.T__8);

@@ -557,0 +600,0 @@ localctx.ret.type="null"; localctx.ret.value=null;

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

target: modifyTarget,
source: modifySource, // for tnt, handled specially to reconstruct 'query'
source: ignore,
path: modifyPath,

@@ -27,2 +27,3 @@ length: modifyNumber,

scale: modifyNumber,
srid: modifyNumber,
sourceMax: modifyValue,

@@ -364,17 +365,2 @@ targetMin: modifyValue,

function modifySource(node, name, path) {
if(options && options.augmentor && options.augmentor.oldProjections) {
node.source = {absolute:node.source, path:[{id:node.source}]}
location(node.source, path.concat(name), WILO_LAST);
} else if(node.kind==="entity") {
delete node["source"]
node.elements = Object.create(null);
} else if(node.kind==="view") {
node.source = {absolute:node.source, path:[{id:node.source}]} //TODO remove absolute
location(node.source, path.concat(name), WILO_LAST);
} else {
throw Error("Unknown kind: " + node.kind)
}
}
function elementCardinality(el, name, path) {

@@ -381,0 +367,0 @@ let lpath = path.concat([name,"cardinality"]);

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

//console.log(JSON.stringify(model,null,2))
} // function augment

@@ -87,0 +86,0 @@

// contains query relevant augmentor functions
let W = require("./walker");
let udict = require("../transform/udict");

@@ -239,3 +240,3 @@ // creates a new instance which export all functions

function augmentViewArgs(args,path) {
return W.dmap(args, (arg,obj) => {
return udict.dmap(args, (obj,arg) => {
if(obj.param === true)

@@ -323,4 +324,5 @@ return {

for(let prop in cast) {
if(prop == '$extra') continue;
let T = parent.transformers[prop]
T(cast,prop,castpath.concat(prop))
T(cast,prop,castpath)
E[prop]=cast[prop]

@@ -388,7 +390,21 @@ }

op:{val: "call", location},
func: {path:[{id:val.func}]}, // to-csn.js has TODO XSN: remove op: 'call', func is no path
func: {path:[{id:val.func, location}]}, // to-csn.js has TODO XSN: remove op: 'call', func is no path
location
};
if (val.args)
r.args = augmentExpression(val.args,path.concat("args"));
if (val.args) {
let aPath = path.concat("args")
if(typeof val.args == "object" && Array.isArray(val.args)==false) {
r.namedArgs=udict.dmap(val.args, (O,N) => {
return newValue(O.val,aPath.concat(N), N, U.WILO_FULL, undefined, U.WILO_FIRST);
})
} else if(val.args[0] && val.args[0]==="*") { // count(*)
r.args = [{
val:"*",
literal: "token",
location: U.newLocation(aPath)
}]
} else {
r.args = augmentExpression(val.args,aPath);
}
}
return r;

@@ -437,9 +453,11 @@ } else if(val.hasOwnProperty("#")) {

let location = U.newLocation(path)
let literal = typeof val;
let literal = (val === null) ? 'null' : typeof val;
return {val, literal, location};
}
function newValue(val, path, name=undefined, which=undefined, literal=undefined) {
function newValue(val, path, name=undefined, which=undefined, literal=undefined, nameWhich=undefined) {
if(which===undefined)
which=U.WILO_FULL;
if(nameWhich===undefined)
nameWhich=U.WILO_FULL;
if(val===undefined)

@@ -479,3 +497,3 @@ return undefined;

ret.name = {id:name};
U.setLocation(ret.name, path);
U.setLocation(ret.name, path, nameWhich);
}

@@ -482,0 +500,0 @@ U.setLocation(ret, path, which);

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

let location = U.newLocation(path.concat(name, iX), U.WILO_FULL)
return {
path: [ { id:X, location } ],
location
}
return asPlainPath(X,location);
})

@@ -33,2 +30,9 @@ node[name] = r;

function asPlainPath(id, location) {
return {
path: [ { id, location } ],
location
}
}
function modifyValue(node, name, path) {

@@ -138,12 +142,6 @@ let value = newValue(node[name], path.concat(name))

function augmentTypeRef(val,path) {
let location = U.newLocation(path, U.WILO_LAST)
let ids = [val];
let location = U.newLocation(path, U.WILO_FULL)
let newPath;
if(typeof val == 'string') {
ids = val.split(".");
if(ids[0]==="cds") // cds.XYZ
ids=[val]
newPath = ids.map(id => {
return {id, location};
});
newPath = [{id:val, location}]
} else if(val.ref) {

@@ -316,2 +314,6 @@ newPath = AQ.refAsPath(val.ref, path.concat("ref"))

absolute: name, // TODO only for semanticChecks.js/checkGenericArtifact ?
// the path is used in transformUtils.createExposingProjection
// this one can be removed once the auto exposure is done by the compiler instead of the transformer
path: [ { id: name, location } ],
location

@@ -349,15 +351,12 @@ }

} else { // dict
if(getLastElement(PATH) === "elements") {
let lastElement = getLastElement(PATH);
if(lastElement === "elements") {
augmentElement(key, node, PATH);
}
if(getLastElement(PATH) === "mixin") {
} else if(lastElement === "mixin") {
augmentElement(key, node, PATH);
}
if(getLastElement(PATH) === "enum") {
} else if(lastElement === "enum") {
augmentEnumItem(key, node, PATH);
}
if(getLastElement(PATH) === "params") {
} else if(lastElement === "params") {
augmentParam(key, node, PATH);
}
if(getLastElement(PATH) === "actions") {
} else if(lastElement === "actions") {
augmentAction(key, node, PATH);

@@ -368,4 +367,4 @@ }

}, (path,obj) => { // check function
let le = U.getLastElement(path)
if(le[0]==="@")
let lastElement = U.getLastElement(path)
if(lastElement[0]==="@")
return false; // do not walk annotations

@@ -433,2 +432,3 @@ if(U.isAugmented(obj)) {

scale: modifyValue,
srid: modifyValue,
target: artifactRef,

@@ -435,0 +435,0 @@ type: artifactRef,

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

const { mergeOptions } = require('../model/modelUtils');
const { sortMessages } = require('../base/messages');

@@ -58,2 +59,3 @@

scale: value,
srid: value,
typeArguments: ignore,

@@ -98,3 +100,3 @@ sourceMax: value,

const typeProperties = [ // used in followTypeOf()
'length', 'precision', 'scale', 'items', 'target', 'source',
'length', 'precision', 'scale', 'srid', 'items', 'target', 'source',
'elements', 'enum'

@@ -293,3 +295,6 @@ ]

function compactSorted( ...args ) {
let model = compact( ...args );
return sortCsn(compact( ...args ));
}
function sortCsn(model) {
if (model.definitions) {

@@ -302,2 +307,5 @@ let result = Object.create(null);

}
if (model.messages) {
model.messages = sortMessages(model.messages);
}
return model;

@@ -361,2 +369,3 @@ }

getCompactors,
sortCsn,
};
// csn version functions
function isNewCSN(csn,options) {
if(options && options.newCsn)
return true;
if(csn.version && csn.version.csn === "0.1.99")
return true;
if(csn.version && csn.version.csn === "0.2")
return true;
if(csn.$version && csn.$version === "0.2")
return true;
if(csn.version && csn.version.csn === "0.2.0")
return true;
if(csn.$version && csn.$version === "0.2.0")
return true;
return false; // default
const newCSNVersions = ["0.1.99","0.2","0.2.0","1.0"];
// checks if new-csn is requested vie the options of already specified in the CSN
// default: old-style
function isNewCSN(csn, options) {
if( (options && options.newCsn === false) ||
(csn.version && !newCSNVersions.includes(csn.version.csn)) ||
(csn.$version && !newCSNVersions.includes(csn.$version)))
{
return false;
}
return true;
}

@@ -18,0 +16,0 @@

@@ -9,4 +9,2 @@ /**

function fromJson(source, filename, options) {
let augmentor = require("./augmentor.js");
let augmentor3 = require("./augmentor3.js");
let jlParser = require("./parse")

@@ -28,6 +26,12 @@

let augmentor3 = require("./augmentor3.js");
augmentor3.augment(model);
} else
} else {
let augmentor = require("./augmentor.js");
augmentor.augment(model , filename, options );
}
delete model["locations"];
// The CSN parser does not use the normal message function:
if (options.messages && model.messages)
options.messages.push( ...model.messages );
return model;

@@ -34,0 +38,0 @@ }

let W = require("./walker");
// Adapt augmented or compacted CSN 'model' in place, by turning those objects that may
// contain properties with user-defined names (e.g. 'elements') into dictionaries, i.e.
// by setting their prototype to 'null' and return the modified model.
/**
* Sets the prototype of all dictionary-object to null
* @param model CSN (old-style) to process
* @param options processing configuration:
* setAllMissingProtos - prepare the input for processing when setting all null-protos to Object.prototype
* @returns the modified CSN model
*/
function nullProtos(model, options={}) {

@@ -8,0 +11,0 @@

let W = require("./walker");
// Adapt augmented or compacted CSN 'model' in place, by turning those objects that may
// contain properties with user-defined names (e.g. 'elements') into dictionaries, i.e.
// by setting their prototype to 'null' and return the modified model.
/**
* Sets the prototype of all dictionary-object to null
* @param model CSN to process
* @param options processing configuration:
* setAllMissingProtos - prepare the input for processing when setting all null-protos to Object.prototype
* @returns the modified CSN model
*/
function nullProtos(model, options={}) {

@@ -66,3 +69,3 @@

function cbExtension(O) { // TODO improve as it is cloned
function cbExtension(O) {
return [

@@ -69,0 +72,0 @@ nullProto(O.elements, cbElement),

@@ -35,3 +35,3 @@ /**

function parse( source, filename, attachMessages=true) {
function parse( source, filename, attachMessages=true, defaultObject=true) {
var lexer = new Lexer( new antlr4.InputStream(source) );

@@ -57,8 +57,16 @@

tree.ret.filename = filename;
if(defaultObject)
setDefaultRootLocation(tree.ret);//needed when the json is completely wrong
tree.ret.getPathLocation = function (path) {
return getPathLocation(tree.ret, path);
}
tree.ret.condense = function ( options = {attachLocations: true, attachMessages: true} ) {
tree.ret.condense = function ( options = {attachLocations: true, attachMessages: true, defaultObject} ) {
const condense = require('./condense.js');
let R = condense(tree.ret, options);
if(typeof R !== "object") { // NO OBJECT RETURNED -> invalid JSON - initialize anyway
if(options.defaultObject)
R={};
else
return undefined; // used in tests
}
if(options.attachLocations)

@@ -75,2 +83,7 @@ attachHiddenProperty(R, "locations", tree.ret);

function setDefaultRootLocation(O) {
O.start={line:1,column:1,offset:0,start:1,stop:1};
O.stop={line:1,column:1,offset:0,start:1,stop:1};
}
function attachHiddenProperty(obj,name,value) {

@@ -77,0 +90,0 @@ Object.defineProperty(obj, name, {

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

const { locationString } = require('../base/messages');
const { assignAll } = require('../model/csnUtils');
const creator = 'CDS Compiler v' + require('../../package.json').version;
const csnVersion = '1.0';
var csn_gensrc = true; // good enough here...

@@ -13,7 +17,6 @@ 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 !!!
// in the resulting CSN !!! Also check const `csnPropertyNames`.
const transformers = {
// early and modifiers (without null / not null) -------------------------------------
kind,
name: ignore, // as is provided extra
id: n => n, // in path item

@@ -25,2 +28,3 @@ '@': value,

key: value,
unique: value,
masked: value,

@@ -41,7 +45,6 @@ params: insertOrderDict,

scale: value,
srid: value,
cardinality: standard, // also for pathItem: after 'id', before 'where'
target: artifactRef,
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,

@@ -57,7 +60,9 @@ items: standard,

having: condition,
args, // also pathItem after 'where', before 'on'/'orderBy'
orderBy: arrayOf( orderBy ), // TODO XSN: make `sort` and `nulls` sibling properties
limit, // TODO XSN: include offset
offset: ignore, // TODO XSN: move into `limit` - see limit
orderBy: arrayOf( orderBy ), // TODO XSN: make `sort` and `nulls` sibling properties
args, // also pathItem after 'where'
namedArgs: renameTo( 'args', args ), // XSN TODO - use args
on: (cond) => (typeof cond === 'string' ? undefined : condition( cond )), // also for join
onCond : renameTo( 'on', condition ), // XSN TODO: onCond -> on
// definitions, extensions, members ----------------------------------------

@@ -77,3 +82,4 @@ returns: standard, // storing the return type of actions

extensions: standard, // is array - TODO: sort
messages: ignore, // consider compactQuery / compactExpr
messages, // consider compactQuery / compactExpr
options: ignore,
sourceMax: renameTo( 'src', value ), // TODO XSN: rename?

@@ -83,3 +89,4 @@ targetMin: renameTo( 'min', value ),

// late protected ----------------------------------------------------------
viaTransform: standard, // FIXME: not a standard prop, start with $
name: ignore, // as is provided extra (for select items, in FROM)
viaTransform: b => b, // FIXME: not a standard prop, start with $
generatedFieldName: renameTo( '$generatedFieldName', n => n ), // TODO: XSN name

@@ -89,10 +96,11 @@ $syntax: s => s,

_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
_ignoreMasked: b => b, // FIXME: prop starting with _ is link and non-enumerable
_isToContainer: b => b, // FIXME: prop starting with _ is link and non-enumerable
location, // -> $location
$extra: (e, csn) => { Object.assign( csn, e ); },
// IGNORED -----------------------------------------------------------------
artifacts: ignore, // well-introduced, hence not $artifacts
location: ignore, // TODO: think about $location with flat struct (w/o offset)
annotationAssignments: ignore, // FIXME: make it $annotations
blocks: ignore, // FIXME: make it $blocks
builtin: ignore, // XSN: $builtin, "cds" namespace probably exposed by current transformers
queries: ignore, // FIXME: make it $queries (flat)

@@ -113,7 +121,27 @@ typeArguments: ignore, // FIXME: make it $typeArgs

// Dictionary mapping XSN property names to corresponding CSN property names
// which should appear at that place in order.
const csnPropertyNames = {
kind: ['annotate','extend'],
op: ['join','func','xpr'],
quantifier: ['some','any','distinct', 'ref','param', 'val','literal', 'SELECT','SET'], // 'all' explicitly listed
foreignKeys: ['keys'],
exclude: ['excluding'],
limit: ['rows'], //'offset',
sourceMax: ['src'],
targetMin: ['min'],
targetMax: ['max'],
name: ['as','cast'],
generatedFieldName: ['$generatedFieldName'],
location: ['$location'],
}
const propertyOrder = (function () {
let r = {};
let i = 0;
for (let n in transformers)
for (let n in transformers) {
r[n] = ++i;
for (let c of csnPropertyNames[n] || [])
r[c] = ++i;
}
return r;

@@ -123,3 +151,3 @@ })();

const typeProperties = [ // just for `cast` in select items
'type', 'length', 'precision', 'scale', 'items', 'target', 'elements', 'enum'
'type', 'length', 'precision', 'scale', 'srid', 'items', 'target', 'elements', 'enum'
];

@@ -142,4 +170,38 @@

const csnDictionaries = ['params','enum','mixin','elements','actions', 'definitions'];
const csnDirectValues = ['val','messages']; // + all starting with '@'
// Sort property names of CSN according to sequence which is also used by the compactModel function
// Only intended to be used for tests, as no non-enumerable properties are kept.
function sortCsn( csn ) { // only returns enumerable properties
if (csn instanceof Array)
return csn.map( v => (!v || typeof v !== 'object' ? v : sortCsn(v) ) );
let r = {};
for (let n of Object.keys(csn).sort( compareProperties ) ) {
const val = csn[n];
if (!val || typeof val !== 'object' || n.charAt() === '@' || csnDirectValues.includes(n)) {
r[n] = val;
}
else if (csnDictionaries.includes(n)) {
r[n] = csnDictionary( val, n === 'definitions' );
}
else {
r[n] = sortCsn(val);
}
}
return r;
}
function csnDictionary( csn, sort ) {
if (!csn || csn instanceof Array) // null or strange CSN
return csn;
let r = Object.create(null);
for (let n of (sort) ? Object.keys(csn).sort() : Object.keys(csn)) {
r[n] = sortCsn( csn[n] );
}
return r;
}
function compactModel( model, options = model.options || {} ) {
csn_gensrc = options.toCsn && options.toCsn.gensrc;
csn_gensrc = options.toCsn && options.toCsn.flavor === 'gensrc';
mode_strict = options.testMode;

@@ -152,7 +214,8 @@ let csn = {};

extensions( csn, model );
set( 'messages', csn, model );
if (model.version)
csn.version = model.version; // TODO remove
if(!options.testMode) {
setMetaProperty(csn, model);
setCsnVersion(csn);
csn.version = model.version; // TODO remove with CSN version 1.1
if (!options.testMode) {
csn.meta = Object.assign( {}, model.meta, { creator } );
csn.$version = csnVersion;
}

@@ -220,8 +283,4 @@ // Use $extra properties of first source as resulting $extra properties

for (let prop of keys) {
let transformer = transformers[prop] || transformers[prop.charAt(0)];
if (mode_strict && !transformer) {
let loc = node[prop] && node[prop].location || node.location;
throw new Error( `Unexpected property ${prop} in ${ locationString(loc) }`);
}
let sub = (transformer || standard)( node[prop], csn, node, prop );
let transformer = transformers[prop] || transformers[prop.charAt(0)] || unexpected;
let sub = transformer( node[prop], csn, node, prop );
if (sub !== undefined)

@@ -233,2 +292,10 @@ csn[prop] = sub;

function unexpected( val, csn, node, prop ) {
if (mode_strict) {
let loc = val && val.location || node.location;
throw new Error( `Unexpected property ${prop} in ${ locationString(loc) }`);
}
// otherwise, just ignore the unexpected property
}
function set( prop, csn, node ) {

@@ -272,2 +339,24 @@ let val = node[prop];

function messages( value, csn ) {
if (value && value.length )
setHidden( csn, 'messages', value );
}
function location( loc, csn, xsn ) {
if (xsn.kind && xsn.kind.charAt() !== '$' && !xsn.$inferred && xsn.kind !== 'query') {
// Also include $location for elements in queries (if not via '*')
let l = xsn.name && xsn.name.location || loc;
// csn.$location
let value = { file: l.filename, line: l.start.line, col: l.start.column };
setHidden( csn, '$location', value );
}
}
// Add location to SELECT
function addLocation( loc, csn ) {
if (loc)
location( loc, csn, { kind: 'entity' } );
return csn;
}
function insertOrderDict( dict ) {

@@ -318,3 +407,3 @@ let keys = Object.keys( dict );

if (art.kind === 'key') { // foreignkey
let key = addExplicitAs( expression( art.targetElement ),
let key = addExplicitAs( { ref: art.targetElement.path.map( pathItem ) },
art.name, neqPath( art.targetElement ) );

@@ -347,3 +436,3 @@ set( 'generatedFieldName', key, art );

let path = node.path;
if (!path) // does not work with current augmentor
if (!path)
return undefined; // TODO: complain with strict

@@ -359,5 +448,8 @@ let length = path.length;

if (index) {
id = path[ index-1 ]._artifact.name.absolute;
// target._artifact is different to _artifact from path with explicit target
// to model entity with @cds.autoexpose
let art = !csn_gensrc && index===length && node._artifact || path[ index-1 ]._artifact;
id = (art instanceof Array ? art[0] : art).name.absolute;
}
else if (node.resolveSemantics === 'typeOf' && path[0]._artifact) {
else if (node.scope === 'typeOf' && path[0]._artifact) { // TYPE OF without ':' in path
let name = path[0]._artifact.name;

@@ -367,4 +459,3 @@ return { ref: [ name.absolute, ...name.element.split('.'), ...path.slice(1).map( pathItem ) ] };

else if (typeof node.scope === 'number') {
// TODO: just use the first with CSN input - CDL should provide scope:0
index = node.scope || length;
index = node.scope || length; // artifact refs in CDL have scope:0 in XSN
id = (node.scope ? path.slice(0,index) : path).map( id => id.id ).join('.');

@@ -434,2 +525,15 @@ }

function pathRef( path ) {
const ref = { ref: path.map( pathItem ) };
const nav = path[0]._navigation;
if (nav) {
setHidden( ref, '$env', (nav.kind === '$navElement')
? nav.name.alias
: nav.name.query + 1 );
}
else if ( path[0]._artifact && path[0]._artifact.query )
setHidden( ref, '$env', true );
return ref;
}
function expression( node, withExtra ) {

@@ -455,3 +559,3 @@ let en = withExtra != null && node;

if (node.path.length !== 1)
return extra( { ref: node.path.map( pathItem ) }, en );
return extra( pathRef( node.path ), en );
let item = pathItem( node.path[0] );

@@ -463,3 +567,3 @@ if (typeof item === 'string' && !node.path[0].quoted &&

}
return extra( { ref: [item] }, en );
return extra( pathRef( node.path ), en );
}

@@ -471,2 +575,4 @@ if (node.literal) {

return extra( { "#" : node.symbol.id }, en );
else if (node.literal === 'token')
return node.val; // * in COUNT(*)
else // TODO XSN: literal 'hex'->'x'

@@ -521,4 +627,9 @@ return extra( { val: node.val, literal: (node.literal==='hex') ? 'x' : node.literal },

node = node[0];
if (node.op.val === 'query')
return { SELECT: standard( node ) };
if (node.op.val === 'query') {
const select = { SELECT: standard( node ) };
const elements = node.elements;
if (elements && node._main && node._main.$queries && node !== node._main.$queries[0])
setHidden( select, 'elements', elements );
return addLocation( node.location, select );
}
let csn = {};

@@ -546,3 +657,3 @@ // for UNION, ... ----------------------------------------------------------

set( '$extra', csn, node );
return { SET: csn };
return addLocation( node.location, { SET: csn } );
}

@@ -596,6 +707,6 @@

}
else if (!node._artifact || node._artifact.main) {
else if (!node._artifact || node._artifact._main) { // CQL or follow assoc
return extra( addExplicitAs( artifactRef( node, null ), node.name ), node );
}
else
else // if FROM ref is from USING, we might need an AS
return extra( addExplicitAs( artifactRef( node, null ), node.name, function(id) {

@@ -619,3 +730,3 @@ let name = node._artifact.name.absolute;

set( 'key', col, elem );
addExplicitAs( Object.assign( col, expression(elem.value) ),
addExplicitAs( assignAll( col, expression(elem.value) ),
elem.name, neqPath( elem.value ) );

@@ -632,5 +743,8 @@ if (elem._typeIsExplicit || elem.redirected) { // TODO XSN: introduce $inferred

// FIXME: Currently toHana requires that an '_ignore' property on the elem is also visible on the column
if (elem._ignore) {
// Don't ignore virtual columns, let the renderer decide how to render that column
if (!elem.virtual && elem._ignore) {
col._ignore = true;
}
if (elem.value && elem.value.location && !elem.$inferred)
location( elem.value.location, col, { kind: 'col' } );
columns.push( extra( col, elem ) );

@@ -666,2 +780,6 @@ }

function setHidden( obj, prop, value ) {
Object.defineProperty( obj, prop, { value, configurable: true, writable: true } );
}
function addExplicitAs( node, name, implicit ) {

@@ -684,4 +802,4 @@ if (name && (!name.calculated && !name.$inferred || implicit && implicit(name.id) ))

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

@@ -702,2 +820,4 @@ }

// TODO: use style as in rest of this file: property transformators, coding style
// TODO: document CSN and XSN for technical configurations
function technicalConfig( tc/*, parentCsn, parentArtifact, prop */) {

@@ -970,16 +1090,2 @@ let csn = { [tc.backend.val]: { } };

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 };
module.exports = { compactModel, compactQuery, compactExpr, sortCsn };
function Context(options) {
return {
options,
messages:[]
messages:[],
toRemove:[]
}

@@ -6,0 +7,0 @@ }

@@ -85,2 +85,14 @@ let W = require("../walker")

// remove marked for deletion
ctx.toRemove.forEach(P => {
let node = csn;
P.forEach((N,I) => {
if(node===undefined) return;
if(I===P.length-1) { // last node in path?
delete node[N]; // delete the property
} else {
node = node[N];
}
})
})
}

@@ -130,2 +142,7 @@

function rmv(ctx,p) {
if(ctx && ctx.toRemove)
ctx.toRemove.push(p.filter(X => X[0]!=='{'))
}
module.exports = {

@@ -138,2 +155,3 @@ checkTypeOf,

fuzzy,
rmv
}

@@ -34,2 +34,20 @@ /**

/**
* Callback of the forEach function called for each node it walks
* @callback forEachCallback
* @param {string} name of the node
* @param {object} node
*/
/**
* Loops over all elements in an object and calls the specified callback(key,obj)
* @param {object} obj
* @param {forEachCallback} callback
*/
function forEach(obj, callback) {
for(var key in obj) {
callback(key, obj[key]);
}
}
/**
* Callback of the walk function

@@ -310,92 +328,4 @@ * @callback walkCallback

/**
* Callback of the forEach function called for each node it walks
* @callback forEachCallback
* @param {string} name of the node
* @param {object} node
*/
/**
* Loops over all elements in an object and calls the specified callback(key,obj)
* @param {object} obj
* @param {forEachCallback} callback
*/
function forEach(obj, callback) {
for(var key in obj) {
callback(key, obj[key]);
}
}
/**
* Callback of the forEachObject function called for each node it walks
* @callback forEachObjectCallback
* @param {string} name of the node
* @param {object} node
* @return false stops looping
*/
/**
* Loops over all object-elements in an object and calls the specified callback
* @param {object} obj
* @param {forEachObjectCallback} callback
*/
function forEachObject(obj, callback) {
for(var key in obj) {
let iobj = obj[key];
if(isObject(iobj)) {
if(callback(key, iobj)===false)
break; //early exit
}
}
}
/**
* Callback of the forEachProp function called for each leaf it walks
* @callback forEachPropCallback
* @param {string} name of the node
* @param {object} leaf
* @return false stops looping
*/
/**
* Loops over all leafs in an object and calls the specified callback
* @param {object} obj
* @param {forEachPropCallback} callback
*/
function forEachProp(obj, callback) {
for(var key in obj) {
let iobj = obj[key];
if(!isObject(iobj)) {
if(callback(key, iobj)===false)
break; //early exit
}
}
}
/**
* Callback of the dmap function called for each leaf it walks
* @callback dmapCallback
* @param {string} name of the node
* @param {object} leaf
* @return resulting object
*/
/**
* Transforms all elements of a dictionary into another.
* Loops over all elements in an object and calls the specified callback.
* The callback function returns the new representation of the passed element.
* @param {object} obj
* @param {dmapCallback} callback
* @return resulting dictionary
*/
function dmap(obj, callback) {
let R = Object.create(null);
for(var key in obj) {
let iobj = obj[key];
R[key] = callback(key,iobj);
}
return R;
}
/**
* Callback of the walkNodesEx function to obtain the next node to walk

@@ -481,6 +411,3 @@ * @callback getNextElements

module.exports = {
dmap,
forEach,
forEachObject,
forEachProp,
walk,

@@ -487,0 +414,0 @@ walkNodesEx,

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

parser.options = options;
parser.$message = getMessageFunction( parser ); // sets parser.messages
parser.$message = getMessageFunction( parser, options ); // sets parser.messages

@@ -132,6 +132,10 @@ initTokenRewrite( parser, tokenStream );

var ast = tree && tree[ rulespec.returns ] || {};
ast.options = options;
if (rulespec.frontend)
ast.$frontend = rulespec.frontend;
ast.messages = parser.messages;
if (parser.messages) {
Object.defineProperty( ast, 'messages',
{ value: parser.messages, configurable: true, writable: true } );
}
if (options.attachTokens === true || options.attachTokens === filename)

@@ -138,0 +142,0 @@ ast.tokenStream = tokenStream;

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

hanaFlavorOnly,
betaModeOnly,
csnParseOnly,

@@ -116,2 +117,12 @@ noAssignmentInSameLine,

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

@@ -118,0 +129,0 @@ // not really compile, just use to produce a CSN for functions parseToCqn() and

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

options = mergeOptions(backends.getDefaultBackendOptions(), options);
// New-style CSN vs old-style CSN
return options.newCsn ? '0.2' : '0.1.0';
// Old-style CSN vs new-style CSN
return options.newCsn === false ? '0.1.0' : '1.0';
}

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

const moduleLayers = require('./compiler/moduleLayers');
var define = require('./compiler/definer');
const { define } = require('./compiler/definer');
var resolve = require('./compiler/resolver');

@@ -61,7 +61,4 @@ var propagator = require('./compiler/propagator');

var fs = require('fs');
var { getDefaultTntFlavorOptions, propagateIncludesForTnt } = require('./transform/tntSpecific');
var csn2edm = require('./edm/csn2edm');
const emdx2csn = require('./edm/annotations/edmx2csnNew'); // translate edmx annotations into csn
const { mergeOptions } = require('../lib/model/modelUtils');
const { setProp } = require('./base/model');

@@ -79,4 +76,5 @@ const path = require('path');

let ext = path.extname( filename ).toLowerCase();
if (ext === '.xml')
if (ext === '.xml') {
return emdx2csn( source, filename, options );
}
else if (['.json', '.csn'].includes(ext)) {

@@ -91,3 +89,3 @@ let parseCSNfromJson = require("./json/from-json");

let model = {};
const message = getMessageFunction( model );
const message = getMessageFunction( model, options );
message( 'file-unknown-ext',

@@ -114,4 +112,3 @@ { filename, start: { offset: 0, line: 1, column: 1 } }, null,

// - Truthy `parseOnly`: stop compilation after parsing.
// - Truthy `lintMode`: do not report errors for using directives pointing to
// non-existing artifacts.
// - Truthy `lintMode`: do not do checks and propagation
// - many others - TODO

@@ -147,5 +144,5 @@

var messagesArray = [[]];
const message = getMessageFunction({ messages: messagesArray[0] });
let model = { sources: a.sources, options };
const message = getMessageFunction( model );
const parseOptions = optionsWithMessages( options, model );
var all = promiseAllDoNotRejectImmediately( a.files.map(readAndParse) );

@@ -161,15 +158,12 @@

});
if (!options.lintMode && !options.parseOnly)
if (!options.parseOnly)
all = all.then( readDependencies );
return all.then( function() {
moduleLayers.setLayers( a.sources );
for (let name in a.sources)
messagesArray.push( a.sources[name].messages || [] );
if (options.collectSources)
return collect();
return compileDo( a.sources, messagesArray, options, a.fileContentDict );
return compileDo( model, a.fileContentDict );
});
function collect() {
let model = { messages: [].concat( ...messagesArray ) };
handleMessages( model );

@@ -205,7 +199,7 @@ let dependencies = Object.create(null);

a.fileContentDict[filename] = source;
let ast = parse( source, rel, options );
let ast = parse( source, rel, parseOptions );
a.sources[filename] = ast;
ast.filename = rel;
ast.dirname = path.dirname( filename );
assertConsistency( ast, options );
assertConsistency( ast, parseOptions );
fulfill( ast );

@@ -439,3 +433,5 @@ }

let sources = Object.create(null);
var messagesArray = [];
let model = { sources, options };
getMessageFunction( model ); // make sure that we have a "central" messages array
const parseOptions = optionsWithMessages( options, model );

@@ -445,8 +441,6 @@ for (let filename in content) {

if (typeof source === 'string') {
let ast = parse( source, filename, options );
let ast = parse( source, filename, parseOptions );
sources[filename] = ast;
ast.filename = filename;
assertConsistency( ast, options );
if (ast && ast.messages)
messagesArray.push( ast.messages );
assertConsistency( ast, parseOptions );
}

@@ -471,7 +465,15 @@ // else

return compileDo( sources, messagesArray, options, sourcesDict );
return compileDo( model );
}
function compileDo( sources, messagesArray, options = {}, fileContentDict ) {
var model = { sources, options, messages: [].concat( ...messagesArray ) }; // flatten
// Make sure to use a "central" messages array during parsing:
function optionsWithMessages( options, model ) {
return (options.messages)
? options
// : node 8.3 / elint-5+: { messages: model.messages, ...options };
: Object.assign( { messages: model.messages }, options );
}
function compileDo( model ) {
let options = model.options;
if (!options.testMode) {

@@ -481,8 +483,7 @@ model.version = versionObject( options );//TODO remove

}
// if (!options.parseOnly) return define(model);
if (!options.parseOnly) {
define(model);
resolve(model);
}
if (options.parseOnly)
return handleMessages( model );
define( model );
resolve( model );
assertConsistency( model );

@@ -492,17 +493,6 @@ handleMessages( model ); // stop compilation with errors

return model;
semanticChecks(model);
handleMessages( model );
model = propagator.propagate( model );
if (!options.modelExtender || '$draft.cds' in sources)
return model;
let draft = options.modelExtender( model );
if (!draft)
return model;
if (typeof draft === 'string')
return compileSources( Object.assign( { '$draft.cds': draft }, fileContentDict ),
options );
handleMessages( draft );
throw new Error( 'Option `modelExtender` returns illegal value' );
return propagator.propagate( model );
}

@@ -537,88 +527,2 @@

function toSwagger(model) {
return backends.toSwagger(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 }'
// containing
// - 'annotations': (for backward compatibility only): the 'annotations' property of the first entry in 'services'
// - 'metadata': (for backward compatibility only): the 'metadata' property of the first entry in 'services'
// - 'csn': a transformed model
// - 'services': a dictionary of service names, containing for each 'service' from 'model' an object with
// - 'annotations': an XML string with EDMX for ODATA v4 annotations for 'service'
// - 'metadata': an XML string with EDMX for ODATA v2 metadata for 'service'
// The optional 'options' may contain TNT-specific settings to control
// the behavior of 'tntFlavor'. Default is to perform all TNT-specific post-processings,
// which equals to { tntFlavor: true }.
// FIXME: All transformations involved are not yet properly defined and are currently
// specific to the requirements of the TNT project (subject to change).
// Throws a CompilationError on errors.
function toTntSpecificOutput(model, options) {
// If no 'options' are given, the default is to do full 'tntFlavor' without
// skipping anything (for downward compatibility, behaves like {tntFlavor: true} ).
// Selected aspects of 'tntFlavor' can be skipped by setting single options to 'true'.
// Everything can be skipped by setting options to { tntFlavor: false }
// Merge options from model (generate for ODATA what is required by TNT)
options = mergeOptions({ toOdata : { version : 'v2', xml: true, separate : true, csn : true } }, model.options, options);
// Whatever we now have as options, tntFlavor is now set simply because we are in this function
// (just don't overwrite options.tntFlavor with defaults if we already got an object)
if (!options.tntFlavor || typeof options.tntFlavor != 'object') {
options.tntFlavor = getDefaultTntFlavorOptions();
}
// Delegate to new API
let odataResult = backends.toOdata(model, options);
// Assemble result object
let result = {
csn: odataResult.csn,
services: odataResult.services,
alerts: odataResult.messages,
}
// FIXME: For backward compatibility, also transfer messages (is that really required?)
setProp(result.csn, 'messages', odataResult._augmentedCsn.messages);
if (!options.tntFlavor.skipPropagatingIncludes) {
propagateIncludesForTnt(result.csn);
}
// FIXME: For backward compatibility, replace the 'annotations.xml' in all services with the V4 version
// (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 l_annotations_edm = csn2edm(compactedModel, serviceName, mergeOptions(options, { toOdata : { version : 'v4' }}));
result.services[serviceName].annotations = l_annotations_edm.toXML('annotations');
}
// For backward compatibility, fill 'annotations' and 'metadata'
// if there is exactly one service
// FIXME: For backward compatibility only, should be removed soon
let serviceNames = Object.keys(result.services);
if (serviceNames.length == 1) {
let firstService = result.services[serviceNames[0]];
result.annotations = firstService.annotations;
result.metadata = firstService.metadata;
}
return result;
}
// Return the 'version' object that should appear in CSNs generated by this compiler.

@@ -681,6 +585,8 @@ function versionObject( options ) { // TODO remove

toOdata : backends.toOdata,
// TODO: Expose when transformers are switched to CSN
// toOdataWithCsn: backends.toOdataWithCsn,
preparedCsnToEdmx : backends.preparedCsnToEdmx,
preparedCsnToEdm : backends.preparedCsnToEdm,
toCdl : backends.toCdl,
toSwagger,
toSwagger : backends.toSwagger,
toSql : backends.toSql,

@@ -693,5 +599,2 @@ toCsn : backends.toCsn,

parseToExpr,
// Everything below is for backward compatibility only and should no longer be used
toTntSpecificOutput, // FIXME: Temporary, subject to change
}
};

@@ -14,2 +14,11 @@ 'use strict'

// Return true if 'type' is an association or composition
function isAssocOrComposition(type) {
if (!type)
return type;
if (type._artifact)
type = type._artifact.name;
return type.absolute == 'cds.Association' || type.absolute == 'cds.Composition';
}
// Return true if 'type' is an association type

@@ -21,4 +30,12 @@ function isAssociation(type) {

type = type._artifact.name;
return type.absolute == 'cds.Association' || type.absolute == 'cds.Composition';
return type.absolute == 'cds.Association';
}
// Return true if 'type' is a composition type
function isComposition(type) {
if (!type)
return type;
if (type._artifact)
type = type._artifact.name;
return type.absolute == 'cds.Composition';
}

@@ -147,8 +164,28 @@ // Return true if 'elem' is an element (plus sanity check: must have _finalType)

absolute: absoluteName.substring(1),
location: node.location, // inherit location from main element
},
val: theValue,
literal: 'boolean',
location: node.location, // inherit location from main element
};
}
// Add an annotation with absolute name 'absoluteName' (including '@') and path ref 'theValue' to 'node'
function addRefAnnotationTo(absoluteName, theValue, node) {
// Sanity check
if (!absoluteName.startsWith('@')) {
throw Error('Annotation name should start with "@": ' + absoluteName);
}
// Assemble the annotation
//member['@'+vlAnno] = { name: { path: vlAnno.split('.') }, path: [ { id: member.name.id } ] };
node[absoluteName] = {
name: {
absolute: absoluteName.substring(1),
},
path: theValue
};
}
// Rename annotation 'fromName' in 'node' to 'toName' (both names including '@')

@@ -189,4 +226,5 @@ function renameAnnotation(node, fromName, toName) {

// Return true if 'node' has a bool annotation 'name' with (explicit or implicit) value 'true'
function hasBoolAnnotation(node, name) {
// Return true if 'node' has a bool annotation 'name' with explicit value 'val'. If 'val' is omitted, it checks for
// the implicit value 'true'
function hasBoolAnnotation(node, name, val = true) {
// Sanity check

@@ -196,3 +234,3 @@ if (!name.startsWith('@')) {

}
return node[name] && (node[name].val === undefined || node[name].val === true);
return node[name] && ( val === true ? node[name].val === undefined || node[name].val === true : node[name].val === val);
}

@@ -408,3 +446,5 @@

isManagedAssociationElement,
isAssocOrComposition,
isAssociation,
isComposition,
isStructuredElement,

@@ -420,2 +460,3 @@ isArrayElement,

addBoolAnnotationTo,
addRefAnnotationTo,
renameAnnotation,

@@ -433,2 +474,2 @@ copyAnnotations,

getElementDatabaseNameOf,
}
};

@@ -9,2 +9,5 @@ "use strict";

const version = require('../../package.json').version;
const alerts = require('../base/alerts');
const { transformLocation } = require('./renderUtil');
const DuplicateChecker = require('./DuplicateChecker');

@@ -21,2 +24,3 @@ // Render the CSN model 'model' to CDS source text. One source is created per

function toCdsSource(model, options) {
// Merge options (arguments first, then model options)

@@ -26,7 +30,10 @@ options = mergeOptions(model.options, options);

let hdbcdsNames = options.forHana && options.forHana.names == 'hdbcds';
// Skip compactModel if already using CSN
const csn = (options.newTransformers) ? model : compactModel(model);
const { signal, warning, error } = alerts(csn);
let result = Object.create(null);
// 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.

@@ -45,2 +52,6 @@ // FIXME: This should actually only be necessary for toHana (because that wants hierarchical

let globalDuplicateChecker;
if(plainNames) // enable duplicates check only for plain mode
globalDuplicateChecker = new DuplicateChecker(); // registry for all artifact names and element names
// Render each top-level artifact on its own

@@ -53,3 +64,3 @@ for (let artifactName in getTopLevelArtifacts()) {

if (sourceStr != '') {
result[plainNames ? uppercaseAndUnderscore(artifactName) : artifactName]
result[plainNames ? artifactName.replace(/\./g, '_').toUpperCase() : artifactName]
= `${options.testMode ? '' : `// generated by cds-compiler version ${version} \n`}`

@@ -59,2 +70,4 @@ + renderNamespaceDeclaration(artifactName, env) + renderUsings(artifactName, env) + sourceStr;

}
globalDuplicateChecker && globalDuplicateChecker.check(signal, error); // perform duplicates check

@@ -69,5 +82,5 @@ // If there are unapplied 'extend' and 'annotate' statements, render them separately

// Throw up if we have errors
if (hasErrors(model.messages)) {
throw new CompilationError(sortMessages(model.messages), model);
// Throw exception in case of errors
if (hasErrors(csn.messages)) {
throw new CompilationError(sortMessages(csn.messages), csn);
}

@@ -229,9 +242,2 @@ return result;

// Return a string array with the names of the magic variables defined in the
// compiler (e.g. CURRENT_DATE, $now, depending on options also CURRENT_CONNECTION etc for HANA)
function getMagicVariables() {
// FIXME: Retrieve directly from compiler instead, or from the list used to decide quoting-necesity
return Object.keys(model.$magicVariables.artifacts);
}
// Render a context or service. Return the resulting source string.

@@ -271,3 +277,5 @@ function renderContext(artifactName, art, env) {

let childEnv = increaseIndent(env);
result += env.indent + (art.abstract ? 'abstract ' : '') + 'entity ' + renderArtifactName(artifactName, env);
let normalizedArtifactName = renderArtifactName(artifactName, env);
globalDuplicateChecker && globalDuplicateChecker.addArtifact(normalizedArtifactName, art && art.$location);
result += env.indent + (art.abstract ? 'abstract ' : '') + 'entity ' + normalizedArtifactName;
let parameters = Object.keys(art.params || []).map(name => renderParameter(name, art.params[name], childEnv)).join(',\n');

@@ -280,5 +288,8 @@ result += (parameters == '') ? '' : ' (\n' + parameters + '\n' + env.indent + ')';

result += ' {\n';
let duplicateChecker = new DuplicateChecker(); // registry for all artifact names and element names
duplicateChecker.addArtifact(artifactName, art && art.$location)
for (let name in art.elements) {
result += renderElement(name, art.elements[name], childEnv);
result += renderElement(name, art.elements[name], childEnv, duplicateChecker);
}
duplicateChecker.check(signal, error);
result += env.indent + '}';

@@ -368,4 +379,6 @@ result += renderActionsAndFunctions(art, env) + renderTechnicalConfiguration(art.technicalConfig, env) + ';\n';

// Return the resulting source string.
function renderElement(elementName, elm, env) {
function renderElement(elementName, elm, env, duplicateChecker) {
// Ignore if toHana says so
if (options.toHana && elm.virtual)
elm._ignore = true;
if (elm._ignore) {

@@ -377,2 +390,3 @@ return '';

let result = renderAnnotationAssignments(elm, env);
duplicateChecker && elm && duplicateChecker.addElement(quoteOrUppercaseId(elementName), elm && elm.$location);
result += env.indent + (elm.virtual ? 'virtual ' : '')

@@ -435,3 +449,3 @@ + (elm.key ? 'key ' : '')

// Column must have an alias or be a path - take last part of that as element name
columnMap[col.as || getLastPartOfRef(col.ref)] = col;
columnMap[col.as || col.func || getLastPartOfRef(col.ref)] = col;
}

@@ -475,3 +489,3 @@ // Now iterate elements - render an annotation if it is different from the column's

if (source.as) {
result += ` as ${quoteId(source.as)}`;
result += ` as ${quoteOrUppercaseId(source.as)}`;
}

@@ -563,22 +577,35 @@ return result;

let result = renderAnnotationAssignments(col, env);
result += env.indent + (col.key ? 'key ' : '') + renderExpr(col, env, true);
let alias = col.as;
// HANA requires an alias for 'key' columns just for syntactical reasons
// FIXME: This will not complain for non-refs (but that should be checked in forHana)
if (options.forHana && col.key && !alias) {
alias = col.ref && col.ref[col.ref.length - 1];
}
// Explicit or implicit alias?
if (alias) {
result += ' as ' + quoteOrUppercaseId(alias);
}
// Explicit type provided for the view element?
if (col.cast) {
// Special case: Explicit association type is actually a redirect
if (col.cast.target) {
// Redirections are never flattened (don't exist in HANA)
result += ' : redirected to ' + renderAbsoluteNameWithQuotes(col.cast.target, env);
} else {
result += ' : ' + renderTypeReference(col.cast, env);
let leaf = col.as || col.ref && col.ref[col.ref.length-1];
// Render 'null as <alias>' only for database and if element is virtual
if(options.forHana && leaf && env._artifact.elements[leaf] && env._artifact.elements[leaf].virtual) {
result += env.indent + 'null as ' + quoteOrUppercaseId(leaf);
} else {
// If key is explicitly set in a not-first query of a UNION, issue an error.
if(col.key && env.skipKeys){
signal(error`KEY must only be added in the first query of a UNION`, transformLocation(col.$location));
}
const key = (!env.skipKeys && (col.key || (options.forHana && leaf && env._artifact.elements[leaf] && env._artifact.elements[leaf].key)) ? 'key ' : '');
result += env.indent + key + renderExpr(col, env, true);
let alias = col.as;
// HANA requires an alias for 'key' columns just for syntactical reasons
// FIXME: This will not complain for non-refs (but that should be checked in forHana)
// Explicit or implicit alias?
// Shouldn't we simply generate an alias all the time?
if (options.forHana && (key || col.cast) && !alias) {
alias = leaf;
}
if (alias) {
result += ' as ' + quoteOrUppercaseId(alias);
}
// Explicit type provided for the view element?
if (col.cast) {
// Special case: Explicit association type is actually a redirect
if (col.cast.target) {
// Redirections are never flattened (don't exist in HANA)
result += ' : redirected to ' + renderAbsoluteNameWithQuotes(col.cast.target, env);
} else {
result += ' : ' + renderTypeReference(col.cast, env);
}
}
}

@@ -594,3 +621,3 @@ return result;

let result = renderAnnotationAssignments(art, env);
result += `${env.indent}${syntax == 'projection' ? 'entity' : syntax} ${renderArtifactName(artifactName, env)}`;
result += `${env.indent}${art.abstract ? 'abstract ' : ''}${syntax == 'projection' ? 'entity' : syntax} ${renderArtifactName(artifactName, env)}`;
if (art.params) {

@@ -611,2 +638,3 @@ let childEnv = increaseIndent(env);

}
env._artifact = art;
result += renderQuery(art.query, true, syntax, env);

@@ -631,3 +659,12 @@ result += ';\n';

if (query.SET.op) {
result += `\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} ${renderQuery(query.SET.args[1], false, 'view', env)}`;
if(query.SET.op === 'union'){
// For UNION, don't render "key X as X" in the second SELECT
env.skipKeys = true;
}
// Loop over all other arguments, i.e. for A UNION B UNION C UNION D ...
for(let i = 1; i < query.SET.args.length; i++){
result += `\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} ${renderQuery(query.SET.args[i], false, 'view', env)}`;
}
// Reset afterwards
env.skipKeys = false;
}

@@ -651,2 +688,4 @@ result += ')';

let childEnv = increaseIndent(env);
if (options.forHana)
childEnv.currentArtifactName = '$projection'; // $self to be replaced by $projection
if (syntax == 'projection') {

@@ -659,8 +698,13 @@ result += `projection on ${renderViewSource(select.from, env)}`;

}
if (isLeadingQuery && select.mixin) {
result += ' mixin {\n'
if (select.mixin) {
let elems = '';
for (let name in select.mixin) {
result += renderElement(name, select.mixin[name], childEnv);
if (!select.mixin[name]._ignore)
elems += renderElement(name, select.mixin[name], childEnv);
}
result += env.indent + '} into'
if (elems) {
result += ' mixin {\n';
result += elems;
result += env.indent + '} into';
}
}

@@ -772,3 +816,3 @@ result += select.distinct ? ' distinct' : '';

function renderParameter(parName, par, env) {
let result = renderAnnotationAssignments(par, env) + env.indent + quoteId(parName) + ' : ' + renderTypeReference(par, env);
let result = renderAnnotationAssignments(par, env) + env.indent + quoteOrUppercaseId(parName) + ' : ' + renderTypeReference(par, env);
result += renderNullability(par);

@@ -807,3 +851,3 @@ return result;

// Array type: Render items instead
if (elm.items) {
if (elm.items && !elm.type) {
return 'array of ' + renderTypeReference(elm.items, env);

@@ -962,8 +1006,18 @@ }

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')
return "SESSION_CONTEXT('XS_APPLICATIONUSER')";
else if (x.ref[1] === 'locale')
return "SESSION_CONTEXT('LOCALE')";
if (options.forHana && !x.param && !x.global) {
if(x.ref[0] === '$user') {
// FIXME: this is all not enough: we might need an explicit select item alias
if (x.ref[1] === 'id')
return "SESSION_CONTEXT('XS_APPLICATIONUSER')";
else if (x.ref[1] === 'locale')
return "SESSION_CONTEXT('LOCALE')";
}
else if(x.ref[0] === '$at') {
if(x.ref[1] === 'from') {
return "SESSION_CONTEXT('VALID-FROM')";
}
else if(x.ref[1] === 'to') {
return "SESSION_CONTEXT('VALID-TO')";
}
}
}

@@ -1029,4 +1083,3 @@ // FIXME: no extra magic with x.param or x.global

if (idx == 0
&& (['$projection', '$self', '$parameters'].includes(s)
|| getMagicVariables().map(id => id.toLowerCase()).includes(s.toLowerCase()))) {
&& s.startsWith("$")){
return s;

@@ -1127,2 +1180,5 @@ }

}
if (elm.srid !== undefined) {
params.push(elm.srid);
}
// Additional type parameters

@@ -1361,4 +1417,2 @@ // FIXME: Not yet clear how that looks in new CSN

// Return an id 'id' with appropriate "-quotes
// FIXME: Should only quote where necessary (examining the id for magic characters and reserved
// keywords) - for now, simply quote everything
function quoteId(id) {

@@ -1375,20 +1429,29 @@ // Should only ever be called for real IDs (i.e. no dots inside)

}
// Returns quoted name if:
// 1. starts with a digit
// 2. it contains chars different than:
// - uppercase letters
// - lowercase letters
// - digits
// - underscore
if (id.match(/^\d/) || id.match(/\W/g))
return '"' + id.replace(/"/g, '""') + '"';
if (keywords.cdl.includes(id.toUpperCase()))
return '"' + id.replace(/"/g, '""') + '"';
if (options.toHana && keywords.sql92.includes(id.toUpperCase()))
return '"' + id.replace(/"/g, '""') + '"';
if (keywords.functions.includes(id.toUpperCase()))
return '"' + id.replace(/"/g, '""') + '"';
// Quote if required for CDL or (if rendering for toHana) always
if (requiresQuotingForCdl(id) || options.toHana) {
// Sanity check
if (plainNames) {
throw new Error('Not expecting quotes in plain mode');
}
return `"${id.replace(/"/g, '""')}"`;
}
return id;
}
// Returns true if 'id' requires quotes for CDL, i.e. if 'id'
// 1. starts with a digit
// 2. contains chars different than:
// - uppercase letters
// - lowercase letters
// - digits
// - underscore
// 3. is a CDL keyword or a CDL function without parentheses (CURRENT_*, SYSUUID, ...)
function requiresQuotingForCdl(id) {
return id.match(/^\d/)
|| id.match(/\W/g)
|| keywords.cdl.includes(id.toUpperCase())
|| keywords.cdl_functions.includes(id.toUpperCase());
}
// Return an absolute name 'absname', with '::' inserted if required by naming strategy 'hdbcds', quoted

@@ -1410,3 +1473,12 @@ // as if it was a single identifier (required only for native USINGs)

if (plainNames) {
return id.replace(/\./g, '_').toUpperCase();
// Sanity check
if (!options.toHana) {
throw new Error('Not expecting uppercase names in non-HANA mode');
}
let result = uppercaseAndUnderscore(id);
// Warn if colliding with HANA keyword
if (keywords.hana.includes(result)) {
signal(warning`The identifier "${id}" is a HANA keyword.`);
}
return result;
} else {

@@ -1421,9 +1493,13 @@ return quoteId(id);

function renderArtifactName(artifactName, env) {
return (plainNames) ? quoteOrUppercaseId(artifactName)
return (plainNames) ? uppercaseAndUnderscore(artifactName)
: env.namePrefix + quoteId(getLastPartOf(artifactName));
}
// replace . by _
// convert to uppercase
// For 'name', replace '.' by '_', convert to uppercase, and add double-quotes if
// required because of non-leading '$' (but do not consider leading '$', other special
// characters, or SQL keywords/functions - somewhat weird but this retains maximum
// compatibility with a future hdbtable-based solution and with sqlite, where non-leading
// '$' is legal again but nothing else)
function uppercaseAndUnderscore(name) {
// Always replace '.' by '_' and uppercase
return name.replace(/\./g, '_').toUpperCase();

@@ -1433,4 +1509,2 @@ }

module.exports = {
toCdsSource,
};
module.exports = { toCdsSource };

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

}
// Throw up if we have errors
// Throw exception in case of errors
if (hasErrors(model.messages)) {

@@ -71,3 +71,3 @@ throw new CompilationError(sortMessages(model.messages), model);

if (beforeTableName != afterTableName) {
resultStr += 'RENAME TABLE ' + beforeTableName + ' TO ' + afterTableName + ';\n';
resultStr += " EXEC 'RENAME TABLE " + beforeTableName + " TO " + afterTableName + "';\n";
}

@@ -84,6 +84,8 @@

if (e.target) {
result = 'ALTER TABLE ' + afterTableName + ' DROP ASSOCIATION ' + beforeColumnName + ';\n'
resultStr += ' ';
result = " EXEC 'ALTER TABLE " + afterTableName + " DROP ASSOCIATION " + beforeColumnName + "';\n";
}
else if (beforeColumnName != afterColumnName) {
result = 'RENAME COLUMN ' + afterTableName + '.' + beforeColumnName + ' TO ' + afterColumnName + ';\n';
resultStr += ' ';
result = " EXEC 'RENAME COLUMN " + afterTableName + "." + beforeColumnName + " TO " + afterColumnName + "';\n";
}

@@ -90,0 +92,0 @@ }

@@ -10,3 +10,51 @@

const version = require('../../package.json').version;
const DuplicateChecker = require("./DuplicateChecker");
// Type mapping from cds type names to DB type names:
// (in the future, we would introduce an option for the mapping table)
const cdsToSqlTypes = {
standard: {
// characters and binaries
'cds.String': 'NVARCHAR',
'cds.hana.NCHAR': 'NCHAR',
'cds.LargeString' : 'NCLOB',
'cds.hana.VARCHAR': 'VARCHAR',
'cds.hana.CHAR': 'CHAR',
'cds.hana.CLOB': 'CLOB',
'cds.Binary': 'VARBINARY', // not a Standard SQL type, but HANA and MS SQL Server
'cds.hana.BINARY': 'BINARY',
'cds.LargeBinary': 'BLOB',
// numbers: exact and approximate
'cds.Decimal': 'DECIMAL',
'cds.DecimalFloat': 'DECIMAL',
'cds.Integer64': 'BIGINT',
'cds.Integer': 'INTEGER',
'cds.hana.SMALLINT': 'SMALLINT',
'cds.hana.TINYINT': 'TINYINT', // not a Standard SQL type
'cds.Double': 'DOUBLE',
'cds.hana.REAL': 'REAL',
// other: date/time, boolean
'cds.Date': 'DATE',
'cds.Time': 'TIME',
'cds.DateTime': 'TIMESTAMP', // https://github.wdf.sap.corp/cdx/cds-compiler/issues/2758
'cds.Timestamp': 'TIMESTAMP',
'cds.Boolean': 'BOOLEAN',
'cds.UUID': 'NVARCHAR', // changed to cds.String earlier
// (TODO: do it later; TODO: why not CHAR or at least VARCHAR?)
},
hana: {
'cds.hana.SMALLDECIMAL': 'SMALLDECIMAL',
'cds.DateTime': 'SECONDDATE',
'cds.hana.ST_POINT': 'ST_POINT',
'cds.hana.ST_GEOMETRY': 'ST_GEOMETRY',
},
sqlite: {
'cds.Binary': 'CHAR',
'cds.hana.BINARY': 'CHAR',
'cds.hana.SMALLDECIMAL': 'DECIMAL',
},
};
// Render the CSN model 'model' to SQL DDL statements. One statement is created

@@ -55,3 +103,4 @@ // per top-level artifact into dictionaries 'hdbtable', 'hdbview', ..., without

// The final result in hdb-kind-specific form, without leading CREATE, without trailing newlines
// (note that the order here is relevant for transmission into 'resultObj.sql' below)
// (note that the order here is relevant for transmission into 'resultObj.sql' below and that
// the attribute names must be the HDI plugin names for --src hdi)
let resultObj = {

@@ -65,2 +114,4 @@ hdbtabletype: Object.create(null),

let duplicateChecker = new DuplicateChecker(); // registry for all artifact names and element names
// Render each artifact on its own

@@ -77,3 +128,5 @@ for (let artifactName in csn.definitions) {

// Throw up if we have errors
duplicateChecker.check(signal, error); // trigger artifact and element name checks
// Throw exception in case of errors
if (hasErrors(model.messages)) {

@@ -83,3 +136,3 @@ throw new CompilationError(sortMessages(model.messages), model);

// Transfer results from hdb-specific dictionaries into 'sql' dictionary in proper order
// Transfer results from hdb-specific dictionaries into 'sql' dictionary in proper order if toSql.src == 'sql'
// (relying on the order of dictionaries above)

@@ -91,16 +144,23 @@ // FIXME: Should consider inter-view dependencies, too

for (let name in resultObj[hdbKind]) {
let sourceString = resultObj[hdbKind][name];
// Hack: Other than in 'hdbtable' files, in HANA SQL COLUMN is not mandatory but default.
if (options.toSql.dialect == 'hana' && hdbKind == 'hdbtable' && sourceString.startsWith('COLUMN ')) {
sourceString = sourceString.slice('COLUMN '.length);
if (options.toSql.src == 'sql') {
let sourceString = resultObj[hdbKind][name];
// Hack: Other than in 'hdbtable' files, in HANA SQL COLUMN is not mandatory but default.
if (options.toSql.dialect == 'hana' && hdbKind == 'hdbtable' && sourceString.startsWith('COLUMN ')) {
sourceString = sourceString.slice('COLUMN '.length);
}
sql[name] = `${options.testMode ? '' : sqlVersionLine}CREATE ${sourceString};`;
}
sql[name] = `${options.testMode ? '' : sqlVersionLine}CREATE ${sourceString};`;
if (!options.testMode) {
resultObj[hdbKind][name] = sqlVersionLine + resultObj[hdbKind][name];
else {
if (!options.testMode) {
resultObj[hdbKind][name] = sqlVersionLine + resultObj[hdbKind][name];
}
}
}
if (options.toSql.src == 'sql') {
delete resultObj[hdbKind];
}
}
resultObj.sql = sql;
if (options.toSql.src == 'sql') {
resultObj.sql = sql;
}
return resultObj;

@@ -157,4 +217,3 @@

result += art.technicalConfig.hana.storeType.toUpperCase() + ' ';
}
else if (options.toSql.dialect == 'hana') {
} else {
// in 'hdbtable' files, COLUMN or ROW is mandatory, and COLUMN is the default

@@ -164,7 +223,14 @@ result += 'COLUMN ';

}
result += 'TABLE ' + quoteSqlId(absoluteCdsName(artifactName));
let tableName = quoteSqlId(absoluteCdsName(artifactName));
duplicateChecker.addArtifact(tableName, art && art.$location)
result += 'TABLE ' + tableName;
result += ' (\n';
result += Object.keys(art.elements).map(name => renderElement(artifactName, name, art.elements[name], getFzIndex(name, hanaTc), childEnv))
let elements = Object.keys(art.elements).map(name => renderElement(artifactName, art, name, art.elements[name], getFzIndex(name, hanaTc), childEnv))
.filter(s => s != '')
.join(',\n');
if (elements != '') {
result += elements;
} else {
signal(error`"${artifactName}": Entity must have at least one element that is non-virtual`, art.location);
}
let primaryKeys = Object.keys(art.elements).filter(name => art.elements[name].key)

@@ -174,2 +240,8 @@ .filter(name => !art.elements[name]._ignore)

.join(', ');
let uniqueFields = Object.keys(art.elements).filter(name => art.elements[name].unique && !art.elements[name]._ignore)
.map(quoteSqlId)
.join(', ');
if (uniqueFields != '') {
result += ',\n' + childEnv.indent + 'UNIQUE(' + uniqueFields + ')'
}
if (primaryKeys != '') {

@@ -222,10 +294,14 @@ result += ',\n' + childEnv.indent + 'PRIMARY KEY(' + primaryKeys + ')\n'

// Return the resulting source string (no trailing LF).
function renderElement(artifactName, elementName, elm, fzindex, env) {
function renderElement(artifactName, art, elementName, elm, fzindex, env) {
// Ignore if forHana says so, or if it is an association
if (elm.virtual)
elm._ignore = true; // this has the side effect, that it's also ignored in the primary key generation
if (elm._ignore || elm.target) {
return '';
}
let result = env.indent + quoteSqlId(elementName) + ' '
let quotedElementName = quoteSqlId(elementName);
duplicateChecker.addElement(quotedElementName, elm && elm.$location);
let result = env.indent + quotedElementName + ' '
+ renderTypeReference(artifactName, elementName, elm)
+ renderNullability(elm);
+ renderNullability(elm, true);
if (elm.default) {

@@ -247,3 +323,3 @@ result += ' DEFAULT ' + renderExpr(elm.default, env);

let result = '';
if (elm.target) {
if (elm.target && !elm._ignore) {
result += env.indent + 'MANY TO ';

@@ -468,7 +544,14 @@ if (elm.cardinality && elm.cardinality.max && (elm.cardinality.max == '*' || Number(elm.cardinality.max) > 1)) {

}
// 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 result = '';
let leaf = col.as || col.ref && col.ref[col.ref.length-1];
if(leaf && env._artifact.elements[leaf] && env._artifact.elements[leaf].virtual) {
// render a virtual column 'null as <alias>'
result += env.indent + 'NULL AS ' + quoteSqlId(col.as || leaf);
} else {
// FIXME: We may want to wrap a cast around 'col' if it has an explicit type?
result = env.indent + renderExpr(col, env, true);
// Explicit or implicit alias?
if (col.as) {
result += ' AS ' + quoteSqlId(col.as);
}
}

@@ -480,3 +563,6 @@ return result;

function renderView(artifactName, art, env) {
let result = 'VIEW ' + quoteSqlId(absoluteCdsName(artifactName));
env._artifact = art;
let viewName = quoteSqlId(absoluteCdsName(artifactName));
duplicateChecker.addArtifact(viewName, art && art.$location)
let result = 'VIEW ' + viewName;
result += renderParameterDefinitions(artifactName, art.params);

@@ -545,3 +631,4 @@ result += ' AS ' + renderQuery(artifactName, art.query, env);

result += '\n' +
(select.columns||['*']).filter(col => !(select.mixin || {})[firstPathStepId(col.ref)]) // No mixin columns
(select.columns||['*']).filter(s => !s._ignore)
.filter(col => !(select.mixin || {})[firstPathStepId(col.ref)]) // No mixin columns
.map(col => renderViewColumn(col, childEnv))

@@ -611,2 +698,4 @@ .filter(s => s != '')

}
let typeName = quoteSqlId(absoluteCdsName(artifactName));
duplicateChecker.addArtifact(typeName, art && art.$location)
let result = 'TYPE ' + quoteSqlId(absoluteCdsName(artifactName)) + ' AS TABLE (\n';

@@ -616,6 +705,11 @@ let childEnv = increaseIndent(env);

// Structured type
result += Object.keys(art.elements).map(name => renderElement(artifactName, name, art.elements[name], null, childEnv))
let elements = Object.keys(art.elements).map(name => renderElement(artifactName, art, name, art.elements[name], null, childEnv))
.filter(s => s != '')
.join(',\n') + '\n';
result += env.indent + ')';
if (elements != '') {
result += elements;
result += env.indent + ')';
} else {
signal(error`"${artifactName}": HANA table type must have at least one element that is non-virtual`, art.location);
}
} else {

@@ -633,8 +727,2 @@ // Non-structured HANA table type

// Array type: Not supported with SQL
if (elm.items) {
signal(error`"${artifactName}.${elementName}": Array types are not supported for conversion to SQL`, elm.location);
return result;
}
// Anonymous structured type: Not supported with SQL (but shouldn't happen anyway after forHana flattened them)

@@ -669,44 +757,11 @@ if (!elm.type) {

function renderBuiltinType(typeName) {
const cdsToSql = {
// CDS builtin types
'cds.String': 'NVARCHAR',
'cds.LargeString' : 'NCLOB',
'cds.Binary': 'VARBINARY',
'cds.LargeBinary': 'BLOB',
'cds.Decimal': 'DECIMAL',
'cds.DecimalFloat': 'DECIMAL',
'cds.Integer64': 'BIGINT',
'cds.Integer': 'INTEGER',
'cds.Double': 'DOUBLE',
'cds.Date': 'DATE',
'cds.Time': 'TIME',
'cds.DateTime': 'SECONDDATE',
'cds.Timestamp': 'TIMESTAMP',
'cds.Boolean': 'BOOLEAN',
// HANA specific builtin types
'hana.ALPHANUM': 'ALPHANUM',
'hana.SMALLINT': 'SMALLINT',
'hana.TINYINT': 'TINYINT',
'hana.SMALLDECIMAL': 'SMALLDECIMAL',
'hana.REAL': 'REAL',
'hana.CHAR': 'CHAR',
'hana.NCHAR': 'NCHAR',
'hana.VARCHAR': 'VARCHAR',
'hana.CLOB': 'CLOB',
'hana.BINARY': 'BINARY',
'hana.ST_POINT': 'ST_POINT',
'hana.ST_GEOMETRY': 'ST_GEOMETRY',
// Obsolete names from HANA CDS, for downward compatibility
'cds.BinaryFloat': 'DOUBLE',
'cds.LocalDate': 'DATE',
'cds.LocalTime': 'TIME',
'cds.UTCDateTime': 'SECONDDATE',
'cds.UTCTimestamp': 'TIMESTAMP',
const forHanaRenamesToEarly = {
'cds.UTCDateTime' : 'cds.DateTime',
'cds.UTCTimestamp' : 'cds.Timestamp',
'cds.LocalDate' : 'cds.Date',
'cds.LocalTime' : 'cds.Time',
};
let result = cdsToSql[typeName];
if (!result) {
throw Error('Unknown primitive type: ' + typeName);
}
return result;
const tName = forHanaRenamesToEarly[typeName] || typeName;
const types = cdsToSqlTypes[options.toSql.dialect];
return types && types[tName] || cdsToSqlTypes.standard[tName] || 'CHAR';
}

@@ -723,8 +778,8 @@

// Render the nullability of an element or parameter (can be unset, true, or false)
function renderNullability(obj /* , env */) {
if (obj.notNull === undefined) {
function renderNullability(obj, treatKeyAsNotNull = false) {
if (obj.notNull === undefined && !(obj.key && treatKeyAsNotNull)) {
// Attribute not set at all
return '';
}
return obj.notNull ? ' NOT NULL' : ' NULL';
return obj.notNull || obj.key ? ' NOT NULL' : ' NULL';
}

@@ -746,2 +801,9 @@

}
if (elm.srid !== undefined) {
// Geometry types translate into CHAR on Sqlite (give them the default length of 5000)
if (options.toSql.dialect == 'sqlite')
params.push(5000);
else
params.push(elm.srid);
}
// Additional type parameters

@@ -820,23 +882,41 @@ // FIXME: Not yet clear how that looks in new CSN

else if (x.ref) {
if (options.forHana && !x.param && !x.global && x.ref[0] === '$user') {
if (options.forHana && !x.param && !x.global) {
if(x.ref[0] === '$user') {
// FIXME: this is all not enough: we might need an explicit select item alias
if (x.ref[1] === 'id') {
if (options.forHana.dialect === 'sqlite') {
if (options.toSql.user && typeof options.toSql.user === 'string' || options.toSql.user instanceof String) {
return `'${options.toSql.user}'`;
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'`;
}
}
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'`;
else {
return "SESSION_CONTEXT('XS_APPLICATIONUSER')";
}
}
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')";
}
}
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')";
else if(x.ref[0] === '$at') {
// return current_time for all $at
if(options.forHana.dialect === 'sqlite') {
return "current_time";
}
else if(options.forHana.dialect === 'hana') {
if(x.ref[1] === 'from') {
return "SESSION_CONTEXT('VALID-FROM')";
}
else if(x.ref[1] === 'to') {
return "SESSION_CONTEXT('VALID-TO')";
}
}
}

@@ -962,22 +1042,26 @@ }

// If 'options.toSql.names' is 'plain'
// - replace '.' or '::' by '_' and convert to uppercase
// - replace '.' or '::' by '_'
// else if 'options.toSql.names' is 'quoted'
// - replace '::' by '.'
// Complain about names that collide with known SQL keywords or functions
function quoteSqlId(name) {
if (options.toSql.dialect === 'sqlite' && keywords.sqlite.includes(name.toUpperCase())){
if (options.toSql.dialect === 'sqlite' && keywords.sqlite.includes(name.toUpperCase())) {
// Sanity check
if (options.toSql.names != 'plain') {
throw new Error(`Not expecting ${options.toSql.names} names for 'sqlite' dialect`);
}
signal(warning`The identifier "${name}" is a SQLite keyword.`);
}
}
if (options.toSql.names == 'plain') {
if (options.toSql.dialect == 'hana') {
if (keywords.hana.includes(name.toUpperCase())) {
signal(warning`The identifier "${name}" is a HANA keyword.`);
}
}
name = name.replace(/(\.|::)/g, '_');
if (name.match(/\W/g)
|| name.match(/^\d/) || name.match(/^_/)
|| keywords.sql92.includes(name.toUpperCase())
|| keywords.functions.includes(name.toUpperCase()))
return `"${name.replace(/"/g, '""').toUpperCase()}"`
else
return name;
return name;
}
else if (options.toSql.names == 'quoted') {
name = name.replace(/::/g, '.');
}
}
return `"${name.replace(/"/g, '""')}"`;

@@ -984,0 +1068,0 @@ }

const schemaObjects = require('./swaggerSchemaObjects');
const { forEachDefinition } = require('../base/model');
const preprocessModel = require('../transform/preprocessModelForSwagger');
const { transformTntExtensions } = require('../transform/tntSpecific');
const { compactSorted } = require('../json/compactor');

@@ -10,5 +9,3 @@ const { compactModel } = require('../json/to-csn')

function csnToSwagger(model, options) {
// Perform TNT-specific magic (only if tntFlavor ist set) and deep-copy the model (always)
model = transformTntExtensions(model);
preprocessModel(model, model.options.tntFlavor);
preprocessModel(model);

@@ -19,3 +16,3 @@ let result = {};

if (options.toSwagger.csn) {
result.csn = options.newCsn ? compactModel(model) : compactSorted(model);
result.csn = options.newCsn === false ? compactSorted(model) : compactModel(model);
result._augmentedCsn = model;

@@ -46,3 +43,3 @@ }

// create schema for the object
swaggerJson.components.schemas[art.name.id /* tntFlavor specific magic*/ || art.name.absolute.split('.').pop()] = createSchemaObjectForArt(art);
swaggerJson.components.schemas[art.name.id] = createSchemaObjectForArt(art);

@@ -216,3 +213,3 @@ // add the default schema for an error

type = type.type._artifact;
if (type.name.absolute.startsWith('cds.')) {
if (type.name.absolute.startsWith('cds.') && !type.name.absolute.startsWith('cds.foundation.')) {
// representing the length specified for strings or binary

@@ -230,5 +227,3 @@ if (art.length)

if (type.name.absolute === "cds.Association" || art.type._artifact.name.absolute === "cds.Composition" && art.target) {
let resultSchema = art._swaggerTntString ?
{ type: 'string', maxLength: 255 }
: schemaObjects.referenceObject('#/components/schemas', art._swaggerTarget || art.target._artifact.name.id /* tntFlavor specific magic*/ || art.target._artifact.name.absolute.split('.').pop());
let resultSchema = schemaObjects.referenceObject('#/components/schemas', art._swaggerTarget || art.target._artifact.name.id);
if (art.cardinality && (art.cardinality.targetMax.val === '*' || art.cardinality.targetMax.val > 1))

@@ -296,3 +291,3 @@ return Object.assign(result, { type: 'array', items: resultSchema });

// if it is not a built-in
if (!(type.name.absolute && type.name.absolute.startsWith('cds.')))
if (!(type.name.absolute && type.name.absolute.startsWith('cds.') && !type.name.absolute.startsWith('cds.foundation.')))
return undefined;

@@ -299,0 +294,0 @@

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

const { CompilationError, hasErrors, sortMessages } = require('../base/messages');
const { isManagedAssociationElement, isStructuredElement, isAssociation, isElementWithType,
renameAnnotation, addBoolAnnotationTo, addStringAnnotationTo, copyAnnotations,
const { isManagedAssociationElement, isStructuredElement, isAssociation, isComposition, isAssocOrComposition, isElementWithType,
renameAnnotation, addBoolAnnotationTo, addStringAnnotationTo, addRefAnnotationTo, copyAnnotations,
foreachPath, hasBoolAnnotation, getElementDatabaseNameOf, getArtifactDatabaseNameOf } = require('../model/modelUtils');

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

const { error, warning, signal } = alerts(inputModel);
let model = deepCopy(inputModel);
let model;
model = deepCopy(inputModel);
model.messages = inputModel.messages;
options = mergeOptions(inputModel.options, options);
model.options = options;
const { flattenForeignKeys, createForeignKeyElement, checkForeignKeys,
flattenStructuredElement, flattenStructStepsInPath,
checkExposedAssoc, toFinalBaseType,
addImplicitRedirections, createAndAddDraftAdminDataProjection,
createAndAddDraftAdminDataProjection,
createScalarElement, createAssociationElement, createAssociationPathComparison,
addElement, createAction, addAction, copyAndAddElement } = transformUtils.getTransformers(model, '_');
addElement, createAction, addAction, copyAndAddElement,
extractValidFromToKeyElement, checkAssignment, checkMultipleAssignments } = transformUtils.getTransformers(model, '_');
// semantic checks before flattening
forEachDefinition(model, artifact => {
// Gather all elements with @cds.valid.from/to/key
let validFrom = [], validTo = [], validKey = [];
forEachMemberRecursively(artifact, member => {
let [f, t, k] = extractValidFromToKeyElement(member);
validFrom.push(...f);
validTo.push(...t);
validKey.push(...k);
});
// Check that @cds.valid.from/to/key is only in valid places
validFrom.forEach(e => checkAssignment('@cds.valid.from', e, artifact));
validTo.forEach(e => checkAssignment('@cds.valid.to', e, artifact));
validKey.forEach(e => checkAssignment('@cds.valid.key', e, artifact));
checkMultipleAssignments(validFrom, '@cds.valid.from', artifact);
checkMultipleAssignments(validTo, '@cds.valid.to', artifact);
checkMultipleAssignments(validKey, '@cds.valid.key', artifact);
if(validKey.length && !(validFrom.length && validTo.length)){
signal(error`@cds.valid.key was used but @cds.valid.from and @cds.valid.to are missing`, artifact.location);
}
});
// Second walk: Flatten structs, unravel derived types, deal with annotations

@@ -84,2 +113,5 @@ forEachDefinition(model, (artifact) => {

toFinalBaseType(member.returns);
toFinalBaseType(member.returns && member.returns.items);
// Mark fields with @odata.on.insert/update as @Core.Computed
annotateCoreComputed(member);
// Resolve annotation shorthands for elements, actions, action parameters

@@ -102,2 +134,3 @@ renameShorthandAnnotations(member);

toFinalBaseType(artifact.returns);
toFinalBaseType(artifact.returns && artifact.returns.items);
}

@@ -118,18 +151,14 @@ // If the artifact is a derived structured type, unravel that as well

// Perform implicit redirection of non-exposed association targets
// Note that this can only happen after struct flattening has been performed, because
// the current implementation relies on modifying associations in place, which would
// not be possible when an entity has an element typed with a named struct containing
// an association (we would modify the type instead).
addImplicitRedirections(model);
// 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);
if (artifact._service) {
if (artifact.kind == 'action' || artifact.kind == 'function') {
exposeStructTypesForAction(artifact, artifact._service);
}
for (let actionName in artifact.actions || {}) {
exposeStructTypesForAction(artifact.actions[actionName], artifact._service);
}
}
for (let actionName in artifact.actions || {}) {
exposeStructTypesForAction(artifact.actions[actionName], artifact._service);
}
});

@@ -143,3 +172,3 @@

// Generate foreign key elements for managed associations
if (isManagedAssociationElement(member)) {
if (isManagedAssociationElement(member) && !member._ignore) {
// Flatten foreign keys (replacing foreign keys that are managed associations by their respective foreign keys)

@@ -158,3 +187,3 @@ member.foreignKeys = flattenForeignKeys(member.foreignKeys);

// (No need to check again for min <= max cardinality, because max has already been checked to be > 0)
if (member.notNull) {
if (member.notNull && member.notNull.val===true) {
if (!member.cardinality) {

@@ -175,2 +204,3 @@ member.cardinality = {};

// Fourth walk through the model: Now all artificially generated things are in place
let visitedArtifacts = {};
forEachDefinition(model, artifact => {

@@ -189,3 +219,3 @@ if (artifact.kind == 'entity' || artifact.kind == 'view') {

else {
generateDraftForOdata(artifact, artifact);
generateDraftForOdata(artifact, artifact, visitedArtifacts);
}

@@ -197,72 +227,10 @@ }

// Check for valid foreign keys
if (isAssociation(elem.type)) {
if (isAssocOrComposition(elem.type)) {
checkForeignKeys(elem);
if(options.betaMode && isComposition(elem.type) &&
options.toOdata.version == 'v4' &&
!hasBoolAnnotation(elem, '@odata.contained'))
elem['@odata.contained'] = { val: true };
}
// Perform checks and add attributes for "contained" sub-entities:
// - A container is recognized by having an association annotated with '@odata.contained'.
// - All targets of such associations ("containees") are marked with a property
// '_containerEntity: []', having as value an array of container names (i.e. of entities
// that have a '@odata.contained' association pointing to the containee). Note that this
// may be multiple entities, possibly including the container itself.
// - All associations in the containee pointing back to the container are marked with
// a boolean property '_isToContainerEntity : true', except if the association itself
// has the annotation '@odata.contained' (indicating the top-down link in a hierarchy).
// - All associations in the containee pointing back to the container must have a specific
// setting for 'NOT NULL': Required if container and containee are different entities
// (header/item scenario), not allowed if they are identical (hierarchy scenario).
// FIXME: This currently requires that only managed associations can point from containee
// back to container in the header/item scenario, because CDS syntax does not allow 'NOT NULL'
// for unmanaged associations). If unmanaged associations are also wanted, we would have to
// omit this check for them.
if (hasBoolAnnotation(elem, '@odata.contained') && elem.target) {
// Retrieve the containee artifact
let containedArtifact = elem.target._artifact;
// Sanity check
if (!containedArtifact) {
throw new Error('Expected target artifact of @odata.contained assoc to be resolved: ' + JSON.stringify(elem));
}
// Let the containee know its container (array because their may be more than one)
if (!containedArtifact._containerEntity) {
containedArtifact._containerEntity = [];
}
// add container only once
if (containedArtifact._containerEntity.includes(artifact.name.absolute)) {
signal(error`"${containedArtifact.name.absolute}": Entity is already recursively contained`, artifact.location);
continue;
}
containedArtifact._containerEntity.push(artifact.name.absolute);
// Mark associations in the containee pointing to the container (i.e. to this entity)
for (let containedElemName in containedArtifact.elements) {
let containedElem = containedArtifact.elements[containedElemName];
if (isAssociation(containedElem.type)) {
// Sanity check
if (!containedElem.target || !containedElem.target._artifact) {
throw new Error('Expected target artifact of assoc in container to be resolved: ' + JSON.stringify(containedElem));
}
// If this is an association that points to a container (but is not by itself contained,
// which would indicate the top role in a hierarchy) mark it with '_isToContainer'
if (containedElem.target._artifact.name.absolute == artifact.name.absolute
&& !hasBoolAnnotation(containedElem, '@odata.contained')) {
containedElem._isToContainer = true;
// If this is really a header/item relationship (i.e. two different entities involved), the
// association to the container must be NOT NULL
if (containedArtifact.name.absolute != artifact.name.absolute) {
// FIXME: See above: We could also omit the check for unmanaged assocs
// if (!containedElem.onCond && !containedElem.notNull)
if (!containedElem.notNull) {
signal(error`"${containedArtifact.name.absolute}.${containedElemName}": Association to container entity must have "NOT NULL"`, containedElem.location);
}
}
// Otherwise (recursive hierarchy relationship, only one entity involved), it must not be NOT NULL
else {
if (containedElem.notNull) {
signal(error`"${containedArtifact.name.absolute}.${containedElemName}": Association to container entity in a recursive hierarchy must not have "NOT NULL`, containedElem.location);
}
}
}
}
}
}
// Remove '$projection' from paths in the element's ON-condition

@@ -279,6 +247,7 @@ // FIXME: Hack - should actually be done by the compiler, and should only

}
visitedArtifacts[artifact.name.absolute] = true;
}
forEachMemberRecursively(artifact, (member, memberName) => {
if (artifact._service) {
if (isAssociation(member.type)) {
if (isAssocOrComposition(member.type)) {
// Check that exposed associations do not point to non-exposed targets

@@ -296,2 +265,20 @@ checkExposedAssoc(artifact, member);

}
// If the association target is annotated with @cds.odata.valuelist, annotate the
// association with @Common.ValueList.viaAssociation (but only for service member artifacts
// to avoid CSN bloating). The propatation of the @Common.ValueList.viaAssociation annotation
// to the foreign keys is done very late in edmPreprocessor.initializeAssociation()
addCommonValueListviaAssociation(member);
// https://github.wdf.sap.corp/cdx/cds-compiler/issues/837
// add check here for @Analytics.Measure and @Aggregation.default
// @Analytics has scope element
if (member.kind && member.kind === 'element'
&& member['@Analytics.Measure'] && !member['@Aggregation.default']) {
signal(
warning`'@Analytics.Measure' expects '@Aggregation.default' to be assigned as well in element '${member.name.absolute}.${member.name.id}'`,
member.location
);
}
}

@@ -311,5 +298,6 @@ });

}
forEachMemberRecursively(artifact, (member, memberName) => {
// Only these are actually required
if (['element', 'key', 'param'].includes(member.kind)) {
forEachMemberRecursively(artifact, (member, memberName) => {
// Only these are actually required and don't annotate virtual elements in entities or types
// as the have no DB representation (although in views)
if (['element', 'key', 'param'].includes(member.kind) && (!member.virtual || artifact.query)) {
// If we have a 'preserved dotted name' (i.e. we are a result of flattening), use that for the @cds.persistence.name annotation

@@ -329,2 +317,3 @@ if (member._flatElementNameWithDots) {

// Walk the elements
let eltCount = 0;
for (let elemName in artifact.elements) {

@@ -341,4 +330,8 @@ let elem = artifact.elements[elemName];

// Count keys and elements annotated with @Core.MediaType
if (elem.key && elem.key.val) {
keyCount++;
let ignore = hasBoolAnnotation(elem, '@cds.api.ignore', true);
if(!elem._ignore) {
eltCount++;
if (elem.key && elem.key.val && !ignore) {
keyCount++;
}
}

@@ -350,4 +343,7 @@ if (elem['@Core.MediaType']) {

// Exposed non-abstract entities in non-contained artifacts must have a key
if (keyCount == 0 && !artifact._containerEntity) {
if(eltCount == 0) {
signal(error`Entity "${artifact.name.absolute}" must have at least one element`, artifact.location);
}
// Exposed non-abstract entities must have a key
if (keyCount == 0) {
signal(error`Entity "${artifact.name.absolute}" does not have a key: ODATA entities must have a key`, artifact.location);

@@ -380,3 +376,3 @@ }

// Throw up if we have errors
// Throw exception in case of errors
if (hasErrors(model.messages)) {

@@ -404,3 +400,3 @@ throw new CompilationError(sortMessages(model.messages), model);

// 'rootArtifact' is the root artifact where composition traversal started.
function generateDraftForOdata(artifact, rootArtifact) {
function generateDraftForOdata(artifact, rootArtifact, visitedArtifacts) {
// Sanity check

@@ -517,3 +513,3 @@ if (!artifact._service) {

// Make all non-key elements nullable
if (elem.notNull && !(elem.key && elem.key.val)) {
if (elem.notNull && elem.notNull.val===true && !(elem.key && elem.key.val)) {
elem.notNull.val = false;

@@ -536,5 +532,9 @@ }

}
// if odata.draft.enabled is explicitly set to false, we ignore it when linked via composition
else if (hasBoolAnnotation(draftNode, '@odata.draft.enabled', false)) {
continue;
}
else {
// Generate draft stuff into the target
generateDraftForOdata(draftNode, rootArtifact);
generateDraftForOdata(draftNode, rootArtifact, visitedArtifacts);
}

@@ -648,2 +648,44 @@ }

/**
* Mark elements that are annotated with @odata.on.insert/update with the annotation @Core.Computed.
*
* Implements: CDXCORE-62
*
* @param {any} node The node to check
*/
function annotateCoreComputed(node){
// If @Core.Computed is explicitly set, don't overwrite it!
if (node['@Core.Computed'] || node.kind !== 'element')
return;
// For @odata.on.insert/update, also add @Core.Computed
if(node['@odata.on.insert'] || node['@odata.on.update']){
addBoolAnnotationTo('@Core.Computed', true, node);
}
}
//
/**
* Mark association whose target has @cds.odata.valuelist with @Common.ValueList.viaAssociation
* => This must be done before foreign keys are calculated and the annotations are propagated
* to them. This will make sure that association and all its foreing keys are annotated with
* Common.ValueList in the final EDM.
*
* Do this only if the association is navigable and the enclosing artifact is
* a service member (don't pollute the CSN with unnecessary annotations).
*
* Implements: CDXCORE-481
*
* @param {any} artifact The parent artifact
* @param {any} node The node to check
*/
function addCommonValueListviaAssociation(member) {
let vlAnno = '@Common.ValueList.viaAssociation';
if(isAssociation(member.type)) {
let navigable = hasBoolAnnotation(member, '@odata.navigable') !== false;
if(navigable && member.target._artifact['@cds.odata.valuelist'] && !member[vlAnno]) {
addRefAnnotationTo(vlAnno, [ { id: member.name.id } ], member);
}
}
}
// Rename shorthand annotations within artifact or element 'node' according to a builtin

@@ -691,3 +733,3 @@ // list.

else {
renameAnnotation(node, name, '@Core.Immutable');
renameAnnotation(node, name, '@Core.Computed');
}

@@ -694,0 +736,0 @@ }

const { setProp, forEachDefinition, forEachGeneric, forEachMemberRecursively } = require('../base/model');
const { CompilationError, hasErrors, sortMessages } = require('../base/messages');
const transformUtils = require('./transformUtils');
const { isAssociation } = require('../model/modelUtils');
const { isAssocOrComposition } = require('../model/modelUtils');
const alerts = require('../base/alerts');
function preprocessModel(model, tntFlavor) {
function isValidAssocOrComposition(elem) {
return isAssocOrComposition(elem.type) && !elem._ignore
}
function preprocessModel(model) {
// e.g. signal(error`...`, <artifact>.location);

@@ -24,3 +28,3 @@ const { error, signal } = alerts(model);

// Throw up if we have errors
// Throw exception in case of errors
if (hasErrors(model.messages)) {

@@ -46,7 +50,8 @@ throw new CompilationError(sortMessages(model.messages));

return;
if (!memberType._artifact.name.absolute.startsWith('cds.')) {
if (!memberType._artifact.name.absolute.startsWith('cds.') || memberType._artifact.name.absolute.startsWith('cds.foundation.')) {
let memberTypeService = memberType._artifact._service || memberType._artifact._main && memberType._artifact._main._service;
if (memberTypeService !== currentService) {
// if the type is builtin and not from the current service -> expand it
if (!art.projection && memberType._artifact.type && memberType._artifact.type._artifact.name.absolute.startsWith('cds.'))
if (!art.projection && memberType._artifact.type && (memberType._artifact.type._artifact.name.absolute.startsWith('cds.')
&& !memberType._artifact.type._artifact.name.absolute.startsWith('cds.foundation')))
member.items ? setProp(member.items, '_swaggerExpandType', true) : setProp(member, '_swaggerExpandType', true);

@@ -59,8 +64,4 @@ else if (/* the projection case is handle in processProjection */!art.projection)// structured type not from the current service

// 2. check if the target of the association is part of the current service
if (!art.projection && isAssociation(member.type)) {
if (tntFlavor) { /* ugly tnt magic -> to be removed */
if (member.target && member.target._artifact && member.target._artifact._service != art._service)
setProp(member, '_swaggerTntString', true);
} else
checkExposedAssoc(art, member);
if (!art.projection && isValidAssocOrComposition(member)) {
checkExposedAssoc(art, member);
}

@@ -82,3 +83,3 @@ });

// association redirecting
if (elem.type && isAssociation(elem.type)) {
if (isValidAssocOrComposition(elem)) {
// try to find representation of the association target in the current service

@@ -93,6 +94,3 @@ let targetFromCurrectService = Object.keys(parent.artifacts).filter(artName => parent.artifacts[artName].projection)

else {
/* ugly tnt magic -> to be removed*/
tntFlavor ?
setProp(elem, '_swaggerTntString', true)
: signal(error`Association ${elem.name.absolute}.${elem.name.id} cannot be implicitly redirected: Target ${elem.target._artifact.name.absolute} is not exposed in service ${parent.name.absolute} by any projection`, elem.location);
signal(error`Association ${elem.name.absolute}.${elem.name.id} cannot be implicitly redirected: Target ${elem.target._artifact.name.absolute} is not exposed in service ${parent.name.absolute} by any projection`, elem.location);
}

@@ -104,3 +102,3 @@ } else {

return;
if (elemType._artifact.name.absolute.startsWith('cds.'))
if (elemType._artifact.name.absolute.startsWith('cds.') && !elemType._artifact.name.absolute.startsWith('cds.foundation.'))
return;

@@ -117,3 +115,3 @@ let elemTypeService = elemType._artifact._service || elemType._artifact._main && elemType._artifact._main._service;

elem.items ? setProp(elem.items, '_swaggerType', typeFromCurrectService) : setProp(elem, '_swaggerType', typeFromCurrectService);
else if (elemType._artifact.type && elemType._artifact.type._artifact.name.absolute.startsWith('cds.'))
else if (elemType._artifact.type && elemType._artifact.type._artifact.name.absolute.startsWith('cds.') && !elemType._artifact.type._artifact.name.absolute.startsWith('cds.foundation.'))
// if the type is builtin and not from the current service -> expand it

@@ -133,10 +131,7 @@ elem.items ? setProp(elem.items, '_swaggerExpandType', true) : setProp(elem, '_swaggerExpandType', true);

return;
// ugly tnt magic -> to be removed
if (art.type && art.type.builtin)
return;
forEachMemberRecursively(art._finalType /* ugly tnt magic -> to be removed once @extends is resolved correctly */ || art.type._artifact, member => {
forEachMemberRecursively(art._finalType, member => {
let memberType = member.items ? member.items.type : member.type;
if (!memberType || !memberType._artifact)
return;
if (memberType._artifact.name.absolute.startsWith('cds.'))
if (memberType._artifact.name.absolute.startsWith('cds.') && !memberType._artifact.name.absolute.startsWith('cds.foundation.'))
return;

@@ -143,0 +138,0 @@ // try to find the exposition of the type in the current service

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

const { setProp, cloneWithTransformations, forEachDefinition,
forEachMemberRecursively } = require('../base/model');
const { setProp, cloneWithTransformations } = require('../base/model');
const { addStringAnnotationTo, printableName,

@@ -16,3 +15,3 @@ copyAnnotations, isStructuredElement, hasBoolAnnotation } = require('../model/modelUtils');

function getTransformers(model, pathDelimiter) {
const { error, signal } = alerts(model);
const { error, warning, signal } = alerts(model);
let options = model.options;

@@ -24,2 +23,5 @@

checkForeignKeys,
checkMultipleAssignments,
extractValidFromToKeyElement,
checkAssignment,
flattenStructuredElement,

@@ -29,3 +31,2 @@ flattenStructStepsInPath,

toFinalBaseType,
addImplicitRedirections,
isAssociationOperand,

@@ -50,4 +51,3 @@ isDollarSelfOperand,

function flattenForeignKeys(foreignKeys) {
// (TNT only): Use <assoc><key> instead of <assoc>_key>
let fkSeparator = (options.tntFlavor && !options.tntFlavor.skipGeneratedFKsWithout_) ? '' : pathDelimiter;
let fkSeparator = pathDelimiter;

@@ -151,4 +151,3 @@ let result = Object.create(null);

// (TNT only): Use <assoc><key> instead of <assoc>_key>
let fkSeparator = (options.tntFlavor && !options.tntFlavor.skipGeneratedFKsWithout_) ? '' : pathDelimiter;
let fkSeparator = pathDelimiter;

@@ -174,3 +173,3 @@ // Assemble foreign key element name from assoc name, '_' and foreign key name/alias

// FIXME: There is currently no other way but to treat the annotation '@odata.Type' as a type property.
for (let prop of ['length', 'scale', 'precision', 'default', '@odata.Type']) {
for (let prop of ['length', 'scale', 'precision', 'srid', 'default', '@odata.Type']) {
if (fkArtifact[prop]) {

@@ -409,4 +408,7 @@ foreignKeyElement[prop] = fkArtifact[prop];

function checkExposedAssoc(artifact, association) {
if (association.target && association.target._artifact && association.target._artifact._service != artifact._service)
signal(error`Association "${artifact.name.absolute}.${association.name.id}" must be redirected: Target "${association.target._artifact.name.absolute}" is not exposed by service "${artifact._service.name.absolute}"`, association.location);
if (!association._ignore && association.target && association.target._artifact && association.target._artifact._service != artifact._service) {
// If we have a 'preserved dotted name' -> a result of flattening -> This scenario is not supported yet
if (association._flatElementNameWithDots)
signal(error`Redirection for sub elements not supported yet - association "${artifact.name.absolute}.${association.name.id}"`, association.location);
}
}

@@ -443,3 +445,3 @@

// Make sure all propagated type properties are taken as non-inferred
for (let prop of ['length', 'precision', 'scale', 'enum', 'target', 'foreignKeys']) {
for (let prop of ['length', 'precision', 'scale', 'srid', 'enum', 'target', 'foreignKeys']) {
if (node[prop] != undefined && node[prop].$inferred == 'prop') {

@@ -453,152 +455,2 @@ delete node[prop].$inferred;

// Examine augmented 'model' for exposed associations/compositions having a non-exposed
// target, and supply appropriate implicit redirections in-place.
// Expects model to already have '_service' entries as provided by the first step of 'transform4oData'.
// Report errors in 'model.messages'.
function addImplicitRedirections(model) {
// Create a backward exposure mapping for implicit re-targeting of association targets
let exposedByProjection = createExposedByMapping(model);
// Walk all artifacts of the model
forEachDefinition(model, (artifact, artifactName) => {
// Only affects exposed artifacts
if (!artifact._service) {
return;
}
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:
forEachMemberRecursively(artifact, (member, memberName) => {
// If this is an association or composition element that is not explicitly redirected,
// and its target is not exposed in the same service as this artifact
if (member.kind == 'element' && member.target && !member.redirected
&& member.target._artifact && member.target._artifact._service != artifact._service) {
// If no candidate for implicit redirection can be found so far, and if requested by annotation: auto-expose the target
let implicitlyRedirected = (exposedByProjection[member.target._artifact.name.absolute] || []).filter(p => p._service == artifact._service);
if (implicitlyRedirected.length == 0 && hasBoolAnnotation(member.target._artifact, '@cds.autoexpose')) {
let projectionId = member.target._artifact.name.absolute.replace(/\./g, '_').replace(/::/g, '__');
let projection = createExposingProjection(member.target._artifact, projectionId, artifact._service, member.location, 'auto-exposure');
if (projection) {
// Take the just-created projection as the (only!) projection exposing the target in this service
exposedByProjection[member.target._artifact.name.absolute] = (exposedByProjection[member.target._artifact.name.absolute] || []).filter(p => p._service != artifact._service);
exposedByProjection[member.target._artifact.name.absolute].push(projection);
// 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);
}
}
// Attempt implicit redirection
redirectAssociationImplicitly(member, memberName, artifact, artifactName);
}
// FIXME: Should we also rewrite '@Common.ValueList.entity' annotations based on auto-exposure? If so, how?
// (not clear whether they have a path or a string as value, and how to resolve the name - TNT seems to have service-local
});
}
// Redirect element 'assoc' with name 'assocName' in 'artifact' with name 'artifactName' implicitly
// by modifying its target and adding a 'redirected' property. Complain if not unique or not exposed
// in this service.
function redirectAssociationImplicitly(assoc, assocName, artifact, artifactName) {
// Only consider exposures in the same service for implicit redirection
let implicitlyRedirected = (exposedByProjection[assoc.target._artifact.name.absolute] || []).filter(p => p._service == artifact._service);
// Complain if no implicit redirection or if not unique
if (implicitlyRedirected.length == 0) {
signal(error`Association "${artifactName}.${assocName}" cannot be implicitly redirected: Target "${assoc.target._artifact.name.absolute}" is not exposed in service "${artifact._service.name.absolute}" by any projection`, assoc.location);
return;
} else if (implicitlyRedirected.length > 1) {
signal(error`Association "${artifactName}.${assocName}" cannot be implicitly redirected: Target "${assoc.target._artifact.name.absolute}" is exposed in service "${artifact._service.name.absolute}" by multiple projections: ${implicitlyRedirected.map(p => '"' + p.name.absolute + '"').join(', ')}`, assoc.location);
return;
}
// Perform implicit redirection
// console.log(`Redirecting target ${assoc.target._artifact.name.absolute} of association ${artifactName}.${assoc.name.element} to ${implicitlyRedirected[0].name.absolute}`);
assoc.redirected = { val: true };
assoc.target = {
absolute: implicitlyRedirected[0].name.absolute,
path: [ { id : implicitlyRedirected[0].name.absolute } ],
};
setProp(assoc.target, '_artifact', implicitlyRedirected[0]);
setProp(assoc.target.path[0], '_artifact', implicitlyRedirected[0]);
}
}
// Create an "exposed by" artifact name -> [artifact, ...] mapping in augmented 'model'
// with the following semantics:
// For each exposed projection P or view V that exposes an otherwise not-exposed "source"
// S (where "source" means the single artifact in the projection's or view's simple
// FROM-clause), we add a mapping from the non-exposed source artifact to the exposed
// projection artifact, i.e. exposedByMapping(S) == [P] resp. [V].
// Even more magically, we also add mappings for each 'include' of S or P resp. V, i.e.
// an entity or projection/view also exposes everything it resp. its source inherits from,
// transitively.
// For convenience, the mapping keeps all values of P/V in an array (even if there is only
// one P/V).
// Returns a dictionary with mappings from names to artifact arrays.
function createExposedByMapping(model) {
let result = Object.create(null);
// FIXME: If requested with 'newRedirectImpl', the compiler already performs implicit redirection.
// Only the auto-exposure part needs to be done here, which can easily be achieved by disabling
// the search for redirection candidates.
if (options.newRedirectImpl) {
return result;
}
// For all artifacts, if they are exposed, consider their query source and includes for implicit exposure
forEachDefinition(model, artifact => {
if (artifact._service) {
addToResult(artifact, artifact);
}
});
// Consider 'exposedArtifact', its simple query source (if any) and its includes (if any) as
// something that is implicitly exposed by 'artifactInService'. Do not take artifacts that
// are part of a service itself.
// Add a mapping of name => [artifact]
function addToResult(exposedArtifact, artifactInService) {
// 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;
// Sanity check
if (!from[0]._artifact) {
throw new Error(`Unresolved query source in ${exposedArtifact.name.absolute}`);
}
// FIXME: We would rather use `from[0]._artifact` here, but the stupid TNT-specific @extends-magic
// does not preserve all _artifact links in queries ...
let sourceArtifact = from[0]._artifact;
// Sanity check
if (!sourceArtifact) {
throw new Error(`Non-existing query source ${from[0]._artifact.name.absolute} in ${exposedArtifact.name.absolute}`);
}
addToResult(sourceArtifact, artifactInService);
}
// An artifact also exposes everything it includes, transitively
if (exposedArtifact.includes != undefined) {
for (let includedArtifact of exposedArtifact.includes.map(include => include._artifact)) {
addToResult(includedArtifact, artifactInService);
}
}
// Ignore if exposed artifact is part of a service by itself
if (exposedArtifact._service) {
return;
}
// Always keep right hand side as an array (more convenient if multiple services are involved)
if (result[exposedArtifact.name.absolute]) {
// Only add if not yet there
if (result[exposedArtifact.name.absolute].every(entry => entry.name.absolute != artifactInService.name.absolute)) {
result[exposedArtifact.name.absolute].push(artifactInService);
}
} else {
result[exposedArtifact.name.absolute] = [artifactInService];
}
// console.log(`Artifact "${artifactInService.name.absolute}" exposes "${exposedArtifact.name.absolute}`);
}
return result;
}
// Return a full projection 'projectionId' of artifact 'art' for exposure in 'service', where the name of the projection

@@ -637,2 +489,7 @@ // is the fully qualified name of 'art', replacing '.' and '::' by '_' resp. '__'.

}
// Transfer xrefs, that are redirected to the projection
// TODO: shall we remove the transfered elements from the original?
if (artElem._xref) {
setProp(elem, '_xref', artElem._xref.filter(xref => xref.user && xref.user._main && xref.user._main._service == service));
}
// FIXME: Remove once the compactor no longer renders 'origin'

@@ -654,3 +511,3 @@ elem.origin = elem.value;

// Add an artifact link for the last path step (for some reason, the original path does not seem to have it)
if (!source.path[source.path.length - 1]._artifact) {
if (source.path && !source.path[source.path.length - 1]._artifact) {
setProp(source.path[source.path.length - 1], '_artifact', art);

@@ -1004,3 +861,3 @@ }

for (let prop in elem) {
if (['type', 'key', 'notNull', 'length', 'precision', 'scale', 'localized', 'onCond', 'foreignKeys', 'location', 'cardinality'].includes(prop)
if (['type', 'key', 'notNull', 'length', 'precision', 'scale', 'srid', 'localized', 'onCond', 'foreignKeys', 'location', 'cardinality', 'items'].includes(prop)
|| prop.startsWith('@')) {

@@ -1100,2 +957,67 @@ result[prop] = elem[prop];

/**
* If the element has annotation @cds.valid.from or @cds.valid.to, return it.
*
* @param {any} element Element to check
* @param {boolean} [to=true] Wether to check for @cds.valid to or not
* @returns {Array[]} Array of arrays, first filed has an array with the element if it has @cds.valid.from, second field if it has @cds.valid.to. Default value is [] for each field.
*/
function extractValidFromToKeyElement(element) {
let validFroms = [], validTos = [], validKeys = [];
if(hasBoolAnnotation(element, '@cds.valid.from')) {
validFroms.push(element);
}
if(hasBoolAnnotation(element, '@cds.valid.to')) {
validTos.push(element);
}
if(hasBoolAnnotation(element, '@cds.valid.key')) {
validKeys.push(element);
}
return [validFroms, validTos, validKeys];
}
/**
* Check if the element can be annotated with the given annotation.
* Only runs the check if:
* - The artifact is not a type
* - The artifact is not a view
*
* Signals an error, if:
* - The element is structured
* - Has a target
* - Has an element as _parent.kind
*
* @param {any} annoName Annotation name
* @param {any} element Element to check
* @param {any} artifact Artifact
* @returns {Boolean} True if no errors
*/
function checkAssignment(annoName, element, artifact) {
if(artifact.kind !== 'type' && !artifact.query) {
if(isStructuredElement(element) || element.target || element._parent.kind === 'element') {
signal(error`Element cannot be annotated with "${annoName}"`, element.location);
return false;
}
}
return true;
}
/**
* Signals an error/warning if an annotation has been assigned more than once
*
* @param {any} array Array of elements that have the annotation
* @param {any} annoName Name of the annotation
* @param {any} artifact Root artifact containing the elements
* @param {boolean} [err=true] Down-grade to a warning if set to false
*/
function checkMultipleAssignments(array, annoName, artifact, err=true) {
if(array.length > 1) {
if(err == true) {
signal(error`"${annoName}" must be assigned only once`, artifact.location);
}
else {
signal(warning`"${annoName}" must be assigned only once`, artifact.location);
}
}
}
}

@@ -1102,0 +1024,0 @@

'use strict'
var { CompilationError, hasErrors, sortMessages } = require('../base/messages');
const { setProp, forEachGeneric, forEachDefinition } = require('../base/model');

@@ -10,8 +9,8 @@ const modelUtils = require('../model/modelUtils.js');

// 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'];
function translateAssocsToJoins(model)
{
// 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'];
const { error, signal } = alerts(model);

@@ -31,6 +30,2 @@

// Throw up if we have errors
if (hasErrors(model.messages)) {
throw new CompilationError( sortMessages(model.messages), model)
}
return model;

@@ -66,12 +61,2 @@

}
else
{
let env = {
tableAliases: [ art.name.id ],
art,
location: 'onCondAssoc',
callback: [ flyTrap ]
};
walk(art.onCond || art.on, env);
}
}

@@ -85,13 +70,28 @@ // drill into structures

let queries = art.$queries;
let fullJoins = fullJoinOption;
if(queries && queries.length > 0)
{
let fullJoins = fullJoinOption;
let env = {
fullJoins,
aliasCount: 0,
walkover: { from: true, onCondFrom:true, select:true, filter: true },
};
/*
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'.
Setup QAs for mixins
Mark all mixin assoc definitions with a pseudo QA that points to the assoc target.
This QA is required to detect mixin assoc usages to decide wether a minimum or full
join needs to be done
*/
queries.forEach(q => createQAForMixinAssoc(q, env));
/*
HANA cannot process associations with parameters, filters in FROM paths
Filters will lead to a minimum join translation (only FROM clause and MIXIN usages)
A full join conversion is required if an assoc path step meets the following conditions:
1) Argument List && path step is an association
2) Argument List && path step is not at leaf if path step is not an association
3) Filter has cardinality ':1'
4) Association is a mixin usage and not at leaf position (no exposure of the mixin but acutal usage)
*/
if(!fullJoinOption) {

@@ -105,7 +105,16 @@ let env = {

env.minimum = env.minimum ||
(env.location == 'from' ? pathDict.path.some(p => p.where) //!!pathDict.path.filter(p => isEntityOrView(p._artifact))[0].where
(env.location == 'from' ? pathDict.path.some(p => p.where /*filter*/)
: (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) || e.cardinality })
env.full =
env.full
|| pathDict.path.some((e, i, a) => {
let hasArgs = !!e.namedArgs;
let hasCardinality = e.cardinality;
let isAssocStep = e._artifact.target;
let isNotTail = a.length-1 > i;
let isMixinUsage = e._navigation && e._navigation.$QA && e._navigation.$QA.mixin;
return (hasArgs && (isAssocStep || isNotTail)) || hasCardinality || (isMixinUsage && isNotTail);
});
}

@@ -119,18 +128,13 @@ }

}
env.fullJoins = fullJoins;
/*
Setup QATs and leaf QAs (mixins, query, subqueries in from clause)
1a) Mark all mixin assoc definitions with a pseudo QA that points to the assoc target.
1b) For all paths in a query do flytrap checks and create the path prefix trees aka QATs.
Setup QATs and leaf QAs (@ query and subqueries in from clause)
a) For all paths in a query create the path prefix trees aka QATs.
Paths that start with a mixin assoc are Qat'ed into the mixin definition.
If a mixin assoc is published, its leaf Qat receives the pseudo QA(view) from the rootQat,
which is the mixin definition itself. See 1a)
1c) Create QAs for FROM clause subqueries, as they are not yet swept by the path walk
b) Create QAs for FROM clause subqueries, as they are not yet swept by the path walk
*/
let env = {
fullJoins,
aliasCount: 0,
walkover: { from: true, onCondFrom:true, select:true, filter: true },
callback: [ flyTrap, mergePathIntoQAT ]
};
queries.forEach(q => createQAForMixinAssoc(q, env));
env.callback = mergePathIntoQAT;
queries.forEach(q => walkQuery(q, env));

@@ -196,3 +200,3 @@ queries.forEach(q => createQAForFromClauseSubQuery(q, env));

{
if(tan !== '$projection') // don't drive into $projection tableAlias (yet)
if(!['$projection', '$self'].includes(tan)) // don't drive into $projection/$self tableAlias (yet)
{

@@ -221,3 +225,3 @@ let ta = query.$tableAliases[tan];

if(!ta.$QA) {
ta.$QA = createQA(env, ta._finalType, undefined, taName);
ta.$QA = createQA(env, ta._finalType, taName, undefined);
incAliasCount(env, ta.$QA);

@@ -255,3 +259,3 @@ if(ta.name && ta.name.id) {

{
art.$QA = createQA(env, art.target._artifact);
art.$QA = createQA(env, art.target._artifact, art.name.id );
art.$QA.mixin = true;

@@ -276,32 +280,52 @@ }

let path;
let pathValue;
if(env.location === 'onCondFrom')
{
let [ tableAlias, tail ] = constructTableAliasAndTailPath(pathNode.path);
let pathStr = translateONCondPath(tail).map(ps => ps.id).join(pathDelimiter);
path = [ tableAlias, { id: pathStr, _artifact: pathNode._artifact } ];
if(checkPathDictionary(pathNode, env)) {
let [ tableAlias, tail ] = constructTableAliasAndTailPath(pathNode.path);
let pathStr = translateONCondPath(tail).map(ps => ps.id).join(pathDelimiter);
pathValue = constructPathNode([ tableAlias, { id: pathStr, _artifact: pathNode._artifact } ]);
replaceNodeContent(pathNode, pathValue);
}
}
else
else
{
// Paths without _navigation in ORDER BY are select item aliases, they must
// be rendered verbatim
if(env.location === 'OrderBy' && !pathNode.path[0]._navigation)
if((env.location === 'OrderBy' && !pathNode.path[0]._navigation))
return;
// path outside ON cond, follow QAT path backwards until QA is found
let pl = pathNode.path.length-1;
let ps = pathNode.path[pl--];
let QA = ps._navigation._parent.$QA;
let path = pathNode.path;
let [head, ...tail] = pathNode.path;
if(['$projection', '$self'].includes(head.id) && tail.length) {
path = tail;
}
// iterate over the path and search for first QA, this is the table alias
let QA = undefined;
let pl = path.length-1;
let ps = path[pl--];
if(ps._navigation && ps._navigation._parent)
QA = ps._navigation._parent.$QA;
while(!QA && pl >= 0)
{
ps = pathNode.path[pl--];
QA = ps._navigation._parent.$QA;
ps = path[pl--];
if(ps._navigation && ps._navigation._parent)
QA = ps._navigation._parent.$QA;
}
if(QA)
path = [ { id: QA.name.id, _artifact: QA._artifact }, ...pathNode.path.slice(pathNode.path.indexOf(ps)) ];
else
throw Error('No QA found for path ' + pathAsStr(pathNode.path, '"'));
if(QA) {
pathValue = constructPathNode([ { id: QA.name.id, _artifact: QA._artifact }, ...path.slice(path.indexOf(ps)) ]);
}
else {
// it's a reference back into the select clause, substitute against original value
pathValue = env.lead.elements[path[0].id].value;
}
if(pathValue === undefined) {
throw Error('No path/value found for ' + pathAsStr(pathNode.path, '"'));
}
replaceNodeContent(pathNode, pathValue);
}
replaceNodeContent(pathNode, constructPathNode(path));
}

@@ -352,3 +376,3 @@

if(!childQat.$QA)
childQat.$QA = createQA(env, art, childQat._namedArgs);
childQat.$QA = createQA(env, art, art.name.absolute.split('.').pop(), childQat._namedArgs);
incAliasCount(env, childQat.$QA);

@@ -368,3 +392,3 @@ newAssocLHS = childQat.$QA;

/* filterPath=> */ pathNode.path ]; // eslint-disable-line indent-legacy
});
}, env);

@@ -384,5 +408,4 @@ if(!env.lead.$startFilters)

//if(!(mixinJoins && env.location !== 'from') && !childQat.QA)
if((env.fullJoins || env.location === 'from') && !childQat.$QA)
childQat.$QA = createQA(env, art.target._artifact, childQat._namedArgs);
childQat.$QA = createQA(env, art.target._artifact, art.name.id, childQat._namedArgs);

@@ -394,3 +417,3 @@ // Do not create a JOIN for that assoc if it has no subsequent path steps

incAliasCount(env, childQat.$QA);
joinTree = createJoinQA(joinType, joinTree, childQat.$QA, childQat, lastAssocQA);
joinTree = createJoinQA(joinType, joinTree, childQat.$QA, childQat, lastAssocQA, env);
newAssocLHS = childQat.$QA;

@@ -417,3 +440,3 @@ }

function createJoinQA(joinType, lhs, rhs, assocQAT, assocSourceQA)
function createJoinQA(joinType, lhs, rhs, assocQAT, assocSourceQA, env)
{

@@ -434,3 +457,3 @@ let node = { op: { val: 'join' }, join: joinType, args: [lhs, rhs] };

return [ tgtTableAlias, pathNode.path ];
});
}, env);

@@ -494,4 +517,6 @@ // If toplevel ON cond op is AND add filter condition to the args array,

}
else
else {
env.firstAssocPathStep = assoc.name.id;
return cloneOnCondition(assoc.onCond || assoc.on);
}

@@ -523,2 +548,3 @@ // clone ON condition with rewritten paths and substituted backlink conditions

{
env.firstAssocPathStep = fwdAssoc.name.id;
result.args.push(createOnCondition(fwdAssoc, tgtAlias, srcAlias));

@@ -542,9 +568,37 @@ i += 2; // skip next two tokens and continue with loop

let fwdAssoc = getForwardAssociationExpr(expr);
if(fwdAssoc)
return createOnCondition(fwdAssoc, tgtAlias, srcAlias);
if(fwdAssoc) {
env.firstAssocPathStep = fwdAssoc.name.id;
let newSrcAlias = tgtAlias;
let newTgtAlias = {};
// first try to identify table alias for complex views or
// redirected associations
if(fwdAssoc._redirected) {
newTgtAlias.id = fwdAssoc._redirected[fwdAssoc._redirected.length-1].$QA.name.id;
newTgtAlias._artifact = fwdAssoc._redirected[fwdAssoc._redirected.length-1]._finalType;
}
else {
// if target was used as part of complex query (join in FROM clause) use
// ta.QA
let ta = env.lead.$tableAliases[fwdAssoc.target._artifact.name.id];
if(ta) {
let srcQA = ta.$QA;
newTgtAlias.id = srcQA.name.id;
newTgtAlias._artifact = srcQA._artifact;
}
else {
// last resort, use last original srcAlias
newTgtAlias = Object.assign(newTgtAlias, srcAlias);
}
}
let oc = createOnCondition(fwdAssoc, newSrcAlias, newTgtAlias);
return oc;
}
// 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(expr.op) {
let x = clone(expr);
x.args = expr.args.map(cloneOnCondition);
return x;
}

@@ -559,3 +613,2 @@ // If this is a regular path, rewrite it

let path = pathNode.path;
if(!path) // it's not a path return it

@@ -565,2 +618,7 @@ return pathNode;

let [head, ...tail] = path;
if(rhs.mixin)
env.firstAssocPathStep = assocQAT.name.id;
if(!checkPathDictionary(pathNode, env)) {
return pathNode;
}

@@ -570,8 +628,6 @@ if(internalArtifactKinds.includes(head._artifact.kind)) // don't rewrite path

if(assocSourceQA.mixin)
if(rhs.mixin)
{
if(head.id === '$projection')
throw Error('Following mix-in association "' + assoc.name.id +
'" in defining view is not allowed with ON condition from projection: ' +
pathAsStr(pathNode.path, '"'));
if(['$projection', '$self'].includes(head.id) && tail.length)
path = env.lead.elements[tail[0].id].value.path;

@@ -609,2 +665,10 @@ /*

/* if the $projection path has association path steps make sure to address the
element by its last table alias name. Search from the end upwards
to the top for the first association path step and cut off path here.
*/
let i = path.length-1;
while(i >= 0 && !path[i--]._artifact.target);
// slice doesn't respect last element, use length instead of length-1 ;)
path = path.slice(i+1, path.length);
[ tableAlias, path ] = constructTableAliasAndTailPath(path);

@@ -660,8 +724,9 @@ }

*/
function createQA(env, artifact, namedArgs, alias)
function createQA(env, artifact, alias=undefined, namedArgs=undefined)
{
if(alias === undefined)
alias = artifact.name.id;
if(alias === undefined) {
throw Error('no alias provided');
}
let node = constructPathNode([ { id: artifact.name.absolute, _artifact: artifact, namedArgs } ], alias);
let node = constructPathNode([ { id: artifact.name.absolute, _artifact: artifact, namedArgs } ], alias);
return node;

@@ -674,3 +739,3 @@ }

{
QA.name.id += '_$' + env.aliasCount++;
QA.name.id += '_' + env.aliasCount++;
QA.numberedAlias = true;

@@ -691,47 +756,25 @@ }

*/
function rewritePathsInExpression(node, getTableAliasAndPathSteps)
function rewritePathsInExpression(node, getTableAliasAndPathSteps, env)
{
let env = {
let innerEnv = {
lead: env.lead,
location: env.location,
position: env.position,
aliasCount: env.aliasCount,
walkover: {},
callback: [
function(pathNode) {
let [ tableAlias, path ] = getTableAliasAndPathSteps(pathNode);
let pathStr = path.map(ps => ps.id).join(pathDelimiter);
replaceNodeContent(pathNode, constructPathNode([ tableAlias, { id: pathStr, _artifact: pathNode._artifact } ]));
if(checkPathDictionary(pathNode, env)) {
let [ tableAlias, path ] = getTableAliasAndPathSteps(pathNode);
let pathStr = path.map(ps => ps.id).join(pathDelimiter);
replaceNodeContent(pathNode, constructPathNode([ tableAlias, { id: pathStr, _artifact: pathNode._artifact } ]));
}
} ]
};
walk(node, env);
walk(node, innerEnv);
}
/*
Return a new CSN path object constructed from an array of pathSteps
path steps is an array of [ 'pathStep id', _artifact reference, namedArgs (optional) ]
Alias is optional
The final _artifact ref is set as _artifact ref to the path
*/
function constructPathNode(pathSteps, alias)
{
let node = {
$rewritten: true,
path : pathSteps.map(p => {
let o = {};
Object.keys(p).forEach(k => {
if(!['_'].includes(k[0]))
o[k] = p[k];
});
setProp(o, '_artifact', p._artifact );
return o; })
};
if(alias)
node.name = { id: alias }
// set the leaf artifact
setProp(node, '_artifact', pathSteps[pathSteps.length-1]._artifact);
return node;
}
/*
Replace the content of the old node with the new one.
If newNode is a join tree (rewritten FROM path), oldPath must be cleared first.
If newNode is a not a path (expression or constant/literal value), oldPath must be cleared first.
If newNode is a path => oldNode._artifact === newNode._artifact, no need to

@@ -742,4 +785,3 @@ exchange _artifact (as non-iterable property it is not assigned).

{
// If newNode is a join tree, throw away old path content
if(newNode.op) {
if(!newNode.path) {
Object.keys(oldNode).forEach(k => {

@@ -865,80 +907,115 @@ delete oldNode[k] });

{
let ppt = assocStep._artifact.$fkPathPrefixTree.children;
let fk = undefined; // last found FK
let fkPs = undefined; // last path step that found FK
path.forEach(ps => {
if(ppt[ps.id])
{
if(ppt[ps.id]._fk)
if(assocStep && assocStep._artifact && assocStep._artifact.$fkPathPrefixTree) {
let ppt = assocStep._artifact.$fkPathPrefixTree.children;
let fk = undefined; // last found FK
let fkPs = undefined; // last path step that found FK
path.forEach(ps => {
if(ppt[ps.id])
{
fk = ppt[ps.id]._fk;
fkPs = ps;
if(ppt[ps.id]._fk)
{
fk = ppt[ps.id]._fk;
fkPs = ps;
}
ppt = ppt[ps.id].children;
}
ppt = ppt[ps.id].children;
});
if(fk)
{
if(pathStr.length)
pathStr += pathDelimiter;
pathStr += fk.name.id;
}
else if(!fk)
// this should never happen if foreign key path could be resolved fully... Anyway, just in case
throw Error('Could not find fkPrefixTree for ' + ps.id + ' from path ' + pathAsStr(path, '"'));
});
if(fk)
{
if(pathStr.length)
pathStr += pathDelimiter;
pathStr += fk.name.id;
let tail = path.slice(path.indexOf(fkPs)+1);
// If foreign key is an association itself, apply substituteFKAliasForPath on tail
if(fk && fk.targetElement._artifact.target && tail.length)
return substituteFKAliasForPath(fk.targetElement, tail, pathStr);
else
return [ tail, pathStr ];
}
let tail = path.slice(path.indexOf(fkPs)+1);
// If foreign key is an association itself, apply substituteFKAliasForPath on tail
if(fk && fk.targetElement._artifact.target && tail.length)
return substituteFKAliasForPath(fk.targetElement, tail, pathStr);
else
return [ tail, pathStr ];
else {
//signal(error`No fkPrefixTree for association, please report this error`, assocStep.location);
return [ path, pathStr ];
}
}
/*
Catch the most basic path constraint violations that are not yet covered elsewhere
/*
checkPathDictionary performs these checks (for ON condition and filter paths)
1) all paths must end on a scalar type
2) Unmanaged associations are not allowed
Exception: The first path step of a mixin assoc is allowed (to identify the
target side of the mixin ON condition) and with backlink associations
3) Managed associations are only allowed to access a foreign key element
Returns true on success, false otherwise
*/
function flyTrap(pathDict, env)
function checkPathDictionary(pathDict, env)
{
// all leaf type must be scalar in a query
if(pathDict.$check !== undefined)
return pathDict.$check;
pathDict.$check = true;
// all leaf types must be scalar in a query
let path = pathDict.path;
if(env.location === 'filter' || env.location === 'onCondFrom' || env.location === 'onCondAssoc')
{
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, "'")}`);
let [head, ...tail] = path;
// pop head if it is a table alias or $projection
let [head, ...tail] = path;
// pop head if it is a table alias or $projection
if(env.tableAliases && env.tableAliases.includes(head.id) || head.id === '$projection')
path = tail;
if(['$projection', '$self'].includes(head.id)) {
path = tail;
[head, ...tail] = path;
}
if(head && env.tableAliases && env.tableAliases.includes(head.id)) {
path = tail;
[head, ...tail] = path;
}
path.forEach(ps => {
if(ps._artifact.target)
{
if(ps._artifact.onCond || ps._artifact.on)
let headIsAssoc = head.id == env.firstAssocPathStep;
let lead = env.art || env.lead;
path.forEach((ps, idx) => {
/* checks for all path steps */
if(ps.namedArgs) {
signal(error`"${lead.name.absolute}": "${ps.id}" must not have parameters`, pathDict.location);
pathDict.$check = false;
}
if(ps.where) {
signal(error`"${lead.name.absolute}": "${ps.id}" must not have a filter`, pathDict.location);
pathDict.$check = false;
}
// checks for all path steps except the first one (if it is the name of the defining association)
if((idx > 0 || !headIsAssoc) && ps._artifact && ps._artifact.target)
{
// if this is not the last path step, complain
let la1 = pathDict.path[pathDict.path.indexOf(ps)+1];
if(la1) {
if((ps._artifact.onCond || ps._artifact.on))
{
if(env.location !== 'onCondAssoc')
signal(error`Unmanaged associations not allowed in path: ${pathAsStr(pathDict.path)}`);
signal(error`"${lead.name.absolute}": "${ps.id}" in path "${pathAsStr(pathDict.path)}" must not be an unmanaged association`, pathDict.location);
pathDict.$check = false;
}
else // must be managed
else if(ps._artifact.$fkPathPrefixTree)// must be managed
{
let la1 = pathDict.path[pathDict.path.indexOf(ps)+1];
if(la1 && !ps._artifact.$fkPathPrefixTree.children[la1.id])
signal(error`Pathstep ' + la1.id + ' is not foreign key of association ' +
ps.id + ' in ON condition path: ' + pathAsStr(pathDict.path)`);
if(!ps._artifact.$fkPathPrefixTree.children[la1.id]) {
signal(error`"${lead.name.absolute}": "${la1.id}" is not foreign key of managed association "${ps.id}" in path "${pathAsStr(pathDict.path)}"` , pathDict.location);
pathDict.$check = false;
}
}
}
});
else {
// it is the last path step => no association
signal(error`"${lead.name.absolute}": "${ps.id}" in path "${pathAsStr(pathDict.path)}" must not be an association`, pathDict.location);
pathDict.$check = false;
}
}
});
if(path[path.length-1]._artifact.elements) {
signal(error`${lead.name.absolute}": "${path[path.length-1].id}" must have scalar type`, pathDict.location);
pathDict.$check = false;
}
if(env.location !== 'onCondFrom' && env.location !== 'onCondAssoc')
{
if(pathDict.path[0].id === '$projection')
signal(error`'$projection' outside of ON conditions not supported: " +
pathAsStr(pathDict.path)`);
}
return pathDict.$check;
}
/*

@@ -978,2 +1055,21 @@ Create path prefix trees and merge paths into the trees depending on the path location.

if(['$projection', '$self'].includes(head.id) && tail.length) {
[head, ...tail] = tail;
/*
if the head is a path (it better be;) then use it as
anchor for _navigation and just merge the tail into that QAT
example:
entity E { key id: Integer; toE: associaton to E; toF: association to F;}
entity F { key id: Integer; toE: associaton to E; }
view V as select from E { toE, $projection.toE.toF.id };
*/
let value = env.lead.elements[head.id].value;
if(value.path) {
head = value.path[0];
}
else // value is another expression, don't consider it
return;
}
// qatParent is the node where the starting qat is attached to

@@ -1080,2 +1176,4 @@ let qatParent = undefined;

if(qat.origin._artifact.$QA) {
// mark mixin assoc definition to be ignored in later rendering step
qat.origin._artifact._ignore = true;
qat.$QA = clone(qat.origin._artifact.$QA);

@@ -1119,3 +1217,3 @@ if(qat._namedArgs)

let alias = pathDict.name ? pathDict.name.id : undefined;
rootQat.$QA = qat.$QA = createQA(env, art, qat._namedArgs, alias);
rootQat.$QA = qat.$QA = createQA(env, art, alias, qat._namedArgs);
}

@@ -1134,148 +1232,5 @@ }

// Crawl all relevant sections of the query AST for paths
function walkQuery(query, env)
{
if(!query)
return;
if(!env.walkover)
env.walkover = {};
env.location = query.op;
if(query.op.val === 'query')
{
env.lead = query;
env.location = 'from';
walkFrom(query.from);
env.location = 'select';
if(env.walkover[env.location])
{
for(let alias in query.elements)
walk(query.elements[alias].value, env);
env.location = 'Where';
walk(query.where, env);
env.location = 'GroupBy';
walk(query.groupBy, env);
env.location = 'Having';
walk(query.having, env);
env.location = 'OrderBy';
walk(query.orderBy, env);
// outer orderBy's of anonymous union
walk(query.$orderBy, env);
env.location = 'Limit';
walk(query.limit, env);
env.location = 'Offset';
walk(query.offset, env);
}
}
function walkFrom(query)
{
let aliases = [];
if(query)
{
if(Array.isArray(query))
{
for(let q of query)
aliases = aliases.concat(walkFrom(q));
}
else if(env.walkover[env.location] && walkPath(query, env))
{
if(query.name)
aliases.push(query.name.id);
}
else
{
aliases = walkFrom(query.args);
env.location = 'onCondFrom';
if(env.walkover[env.location])
{
env.tableAliases = aliases;
walk(query.onCond || query.on, env)
delete env.tableAliases;
}
env.location = 'from';
}
}
return aliases;
}
}
/* node: any
env: { callback: (array of) callback methods with signature(thing, env)
...: any additional payload for the callback
}
*/
function walk(node, env)
{
// In some expressions queries can occur, do not follow them as they
// are walked as member of the queries array
if(!env || !node || (node && node.op && node.op.val == 'query'))
return;
if(typeof node === 'object' && !Array.isArray(node))
if(walkPath(node, env))
return;
// Ask for Array before typeof object (which would also be true for Array)
if(Array.isArray(node))
node.map(n => walk(n, env));
// instanceof Object doesn't respect dictionaries...
else if(typeof node === 'object')
for(let n in node)
walk(node[n], env);
}
function walkPath(node, env)
{
let path = node['path'];
// Ignore paths that that have no artifact (function calls etc) or that are builtins ($now, $user)
// or that are parameters ($parameters or escaped paths (':')
//path.length && path[ path.length-1 ]._artifact
let art = path && path.length && path[path.length-1]._artifact;
if(art && !internalArtifactKinds.includes(art.kind))
{
if(env.callback)
{
// an array of callbacks applied to the node
if(Array.isArray(env.callback))
env.callback.forEach(cb => cb(node, env));
else
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);
filterEnv.location = 'filter';
if(filterEnv.walkover[filterEnv.location])
{
// Walk over all filter expressions (not JOIN relevant,
// cannot be detected in generic walk. Store path step
// to which this filter was attached to in filterEnt.pathStep
path.filter(pathStep=>pathStep.where).forEach(pathStep => {
filterEnv.pathStep = pathStep;
walk(pathStep.where, filterEnv) });
}
*/
// TODO: Parameter expressions!
}
return path;
}
function pathAsStr(p, delim='')
{
return p.map(p => delim + p.id + delim).join(pathDelimiter);
return p.map(p => delim + p.id + delim).join('.');
}

@@ -1353,3 +1308,179 @@

/**
* Return a new CSN path object constructed from an array of pathSteps
* The final _artifact ref is set as _artifact ref to the path
*
* @param {Objects[]} pathSteps Array of [ 'pathStep id', _artifact reference, namedArgs (optional) ]
* @param {any} alias Alias to set as the name property -> { id: <alias> }
* @param {boolean} [rewritten=true] If true, mark the objects with $rewritten
* @returns {Object} CSN path
*/
function constructPathNode(pathSteps, alias, rewritten=true)
{
let node = {
$rewritten: rewritten,
path : pathSteps.map(p => {
let o = {};
Object.keys(p).forEach(k => {
if(!(rewritten && ['_'].includes(k[0])))
o[k] = p[k];
});
setProp(o, '_artifact', p._artifact );
return o; })
};
module.exports = { translateAssocsToJoins };
if(alias)
node.name = { id: alias }
// set the leaf artifact
setProp(node, '_artifact', pathSteps[pathSteps.length-1]._artifact);
return node;
}
// Crawl all relevant sections of the query AST for paths
function walkQuery(query, env)
{
if(!query)
return;
if(!env.walkover)
env.walkover = {};
env.location = query.op;
env.position = query;
if(query.op.val === 'query')
{
env.lead = query;
env.location = 'from';
walkFrom(query.from);
env.location = 'select';
if(env.walkover[env.location])
{
for(let alias in query.elements)
walk(query.elements[alias].value, env);
env.location = 'Where';
walk(query.where, env);
env.location = 'GroupBy';
walk(query.groupBy, env);
env.location = 'Having';
walk(query.having, env);
env.location = 'OrderBy';
walk(query.orderBy, env);
// outer orderBy's of anonymous union
walk(query.$orderBy, env);
env.location = 'Limit';
walk(query.limit, env);
env.location = 'Offset';
walk(query.offset, env);
}
}
function walkFrom(fromBlock)
{
let aliases = [];
env.position = fromBlock;
if(fromBlock)
{
if(Array.isArray(fromBlock))
{
for(let q of fromBlock)
aliases = aliases.concat(walkFrom(q));
}
else if(env.walkover[env.location] && walkPath(fromBlock, env))
{
if(fromBlock.name)
aliases.push(fromBlock.name.id);
}
else
{
aliases = walkFrom(fromBlock.args);
env.location = 'onCondFrom';
if(env.walkover[env.location])
{
env.tableAliases = aliases;
walk(fromBlock.onCond || fromBlock.on, env)
delete env.tableAliases;
}
env.location = 'from';
}
}
return aliases;
}
}
/* node: any
env: { callback: (array of) callback methods with signature(thing, env)
...: any additional payload for the callback
}
*/
function walk(node, env)
{
env.position = node;
// In some expressions queries can occur, do not follow them as they
// are walked as member of the queries array
if(!env || !node || (node && node.op && node.op.val == 'query'))
return;
if(typeof node === 'object' && !Array.isArray(node))
if(walkPath(node, env))
return;
// Ask for Array before typeof object (which would also be true for Array)
if(Array.isArray(node))
node.map(n => walk(n, env));
// instanceof Object doesn't respect dictionaries...
else if(typeof node === 'object')
for(let n in node)
walk(node[n], env);
}
function walkPath(node, env)
{
let path = node['path'];
// Ignore paths that that have no artifact (function calls etc) or that are builtins ($now, $user)
// or that are parameters ($parameters or escaped paths (':')
//path.length && path[ path.length-1 ]._artifact
let art = path && path.length && path[path.length-1]._artifact;
if(art && !internalArtifactKinds.includes(art.kind))
{
if(env.callback)
{
// an array of callbacks applied to the node
if(Array.isArray(env.callback))
env.callback.forEach(cb => cb(node, env));
else
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);
filterEnv.location = 'filter';
if(filterEnv.walkover[filterEnv.location])
{
// Walk over all filter expressions (not JOIN relevant,
// cannot be detected in generic walk. Store path step
// to which this filter was attached to in filterEnt.pathStep
path.filter(pathStep=>pathStep.where).forEach(pathStep => {
filterEnv.pathStep = pathStep;
walk(pathStep.where, filterEnv) });
}
*/
// TODO: Parameter expressions!
}
return path;
}
module.exports = { translateAssocsToJoins, constructPathNode, walkQuery };
{
"name": "@sap/cds-compiler",
"version": "1.8.1",
"version": "1.17.1",
"lockfileVersion": 1,
"requires": true,
"dependencies": {
"antlr4": {
"version": "4.7.1"
"version": "4.7.1",
"resolved": "http://nexus.wdf.sap.corp:8081/nexus/repository/build.releases.npm/antlr4/-/antlr4-4.7.1.tgz",
"integrity": "sha512-haHyTW7Y9joE5MVs37P2lNYfU2RWBLfcRDD8OWldcdZm5TiCE91B5Xl1oWSwiDUSd4rlExpt2pu1fksYQjRBYQ=="
},
"resolve": {
"version": "1.8.1",
"resolved": "http://nexus.wdf.sap.corp:8081/nexus/repository/build.releases.npm/resolve/-/resolve-1.8.1.tgz",
"integrity": "sha512-AicPrAC7Qu1JxPCZ9ZgCZlY35QgFnNqc+0LtbRNxnVw4TXvjQ72wnuL9JQcEBgXkI9JM8MsT9kaQoHcpCRJOYA==",
"requires": {
"path-parse": "1.0.5"
},
"dependencies": {
"path-parse": {
"version": "1.0.5"
"version": "1.0.5",
"resolved": "http://nexus.wdf.sap.corp:8081/nexus/repository/build.releases.npm/path-parse/-/path-parse-1.0.5.tgz",
"integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME="
}

@@ -17,5 +28,7 @@ }

"sax": {
"version": "1.2.4"
"version": "1.2.4",
"resolved": "http://nexus.wdf.sap.corp:8081/nexus/repository/build.releases.npm/sax/-/sax-1.2.4.tgz",
"integrity": "sha512-NqVDv9TpANUjFm0N8uM5GxL36UgKi9/atZw+x7YFnQ8ckwFGKrl4xX4yWtrey3UJm5nP1kUbnYgLopqWNSRhWw=="
}
}
}

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

{"bin":{"cdsc":"bin/cdsc.js","cdshi":"bin/cdshi.js","cdsse":"bin/cdsse.js"},"bundleDependencies":false,"dependencies":{"antlr4":"4.7.1","resolve":"1.8.1","sax":"1.2.4"},"deprecated":false,"description":"CDS (Core Data Services) compiler and backends","keywords":["CDS"],"main":"lib/main.js","name":"@sap/cds-compiler","version":"1.8.1","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.17.1","license":"SEE LICENSE IN developer-license-3.1.txt"}

@@ -35,3 +35,3 @@ # Getting started

The exit code is similar to [`grep` and other commands](http://stackoverflow.com/questions/1101957/are-there-any-standard-exit-status-codes-in-linux):
The exit code of the process is:

@@ -38,0 +38,0 @@ * `0`: successful compilation

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

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

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc