@sap/cds-compiler
Advanced tools
Comparing version 1.5.0 to 1.8.0
517
bin/cdsc.js
@@ -15,5 +15,3 @@ #!/usr/bin/env node | ||
const program = require('commander'); | ||
const compiler = require('../lib/main'); | ||
const { getDefaultTntFlavorOptions } = require('../lib/transform/tntSpecific'); | ||
var util = require('util'); | ||
@@ -23,137 +21,4 @@ var fs = require('fs'); | ||
var reveal = require('./raw-output'); | ||
const { optionProcessor } = require('../lib/backends'); | ||
program | ||
.usage('[options] <file> ...') | ||
.description(`Compile a CDS model given from the input files. Input files may be CDS source files (.cds), CSN | ||
model files (.json) and XML files (.xml) | ||
for pre-processed ODATA annotations.`) | ||
// Uncomment this to make it easier to check for the 100 columns limitation in the '--help' output | ||
// .option('@@', '34567890123456789012345678901234567890123456789012345678901234567890123456789012345678901234567890') | ||
.option('@@', 'General Options') | ||
.option('-h, --help', 'Display this help text and exit') | ||
.option('-w, --warning <level>', 'Show warnings up to <level> (0: Error, 1: Warnings, 2: Info (default))', | ||
verifyWarningOption) | ||
.option(' --show-message-id', 'Show message ID in error, warning and info messages') | ||
.option('-v, --version', 'Display version number and exit') | ||
.option('@@', 'Generation options (can be combined, default if none given is "--to-csn client --out -")') | ||
.option('-o, --out <dir>', 'Place generated files in directory <dir>, default is "-" for <stdout>') | ||
.option('-H, --to-hana <flags>', `Generate HANA CDS source files, <flags> can be a comma-separated | ||
combination of either "plain" (default), "quoted" or "hdbcds" for | ||
entity names, either "assocs" (default) or "joins" for associations | ||
and any of "src,csn" | ||
plain : Produce HANA entity and element names in uppercase and | ||
flattened with underscores. Do not generate structured | ||
types. | ||
quoted : Produce HANA entity and element names in original case | ||
as in CDL. Keep nested contexts (resulting in entity names | ||
with dots), but flatten element names with underscores. | ||
Generate structured types, too. | ||
hdbcds : Produce HANA entity end element names as HANA CDS would | ||
generate them from the same CDS source (like "quoted", but | ||
using element names with dots). | ||
assocs : Keep associations in HANA CDS as far as possible | ||
joins : Transform associations to HANA CDS joins | ||
src : Generate HANA CDS source files "<artifact>.hdbcds" | ||
csn : Generate "hana_csn.json" with HANA-preprocessed model`, verifyToHanaOption) | ||
.option('-O, --to-odata <flags>', `Generate ODATA metadata and annotations, <flags> can be a comma- | ||
separated combination of "xml,json,separate,combined,csn" and either | ||
"v2" (default) or "v4" version. Additionally, one of | ||
"plain,quoted,hdbcds" can be specified to annotate resulting DB names | ||
in the generated CSN. | ||
v2 : Generate ODATA V2 output | ||
v4 : Generate ODATA V4 output | ||
xml : Generate XML output (separate or combined) | ||
json : Generate JSON output (not available for V2) | ||
separate: Generate "<svc>_metadata.xml" and "<svc>_annotations.xml" | ||
combined: Generate "<svc>.xml" | ||
csn : Generate "odata_csn.json" with ODATA-preprocessed model | ||
plain : Annotate DB names in "plain" style (see --to-hana, | ||
--to-sql) | ||
quoted : Annotate DB names in "quoted" style (see --to-hana, | ||
--to-sql) | ||
hdbcds : Annotate DB names in "hdbcds" style (see --to-hana, | ||
--to-sql)`, verifyToOdataOption) | ||
.option('-C, --to-cdl', 'Generate CDS source files "<artifact>.cds"') | ||
.option('-S, --to-swagger <flags>', `Generate Swagger (OpenAPI) JSON, <flags> can be a comma-separated | ||
combination of "json" and "csn" | ||
json : Generate OpenAPI JSON output for each service as | ||
"<svc>_swagger.json" | ||
csn : Generate "swagger_csn.json" with Swagger-preprocessed | ||
model`, verifyToSwaggerOption) | ||
.option('-Q, --to-sql <flags>', `Generate SQL DDL statements to create tables and views, <flags> can be | ||
a comma-separated combination of either "plain" (default), "quoted" or | ||
"hdbcds" for entity names, either "assocs" (default) or "joins" for | ||
associations, either "hana" or "sqlite" for the SQL dialect and any | ||
of "src,csn" | ||
plain : Produce SQL table and view names in uppercase and | ||
flattened with underscores (no quotes required) | ||
quoted : Produce SQL table and view names in original case | ||
as in CDL (with dots), but flatten element names with | ||
underscores (requires quotes) | ||
hdbcds : Produce SQL table, view and column names as HANA CDS | ||
would generate them from the same CDS source (like | ||
"quoted", but using element names with dots). | ||
assocs : Keep associations as far as possible (only usable for | ||
HANA SQL) | ||
joins : Transform associations to SQL joins | ||
src : Generate SQL source files as "<artifact>.sql" | ||
hana : Generate HANA specific SQL | ||
sqlite : Generate SQLite specific SQL | ||
csn : Generate "sql_csn.json" with SQL-preprocessed model`, verifyToSqlOption) | ||
.option(' --to-csn <flavor>', `Generate original model as CSN to "csn.json", <flavor> can be either | ||
"client" (default) or "gensrc" | ||
client : Generate standard CSN consumable by clients and backends | ||
gensrc : Generate CSN specifically for use as a source, e.g. for | ||
combination with additional extend/annotate statements, | ||
but not suitable for consumption by clients or backends`, | ||
verifyToCsnOption) | ||
.option('-l, --lint-mode', `Generate nothing, just produce single-file error messages if any (for | ||
use by editors)`) | ||
.option('@@', 'Backward compatibility options (deprecated, do not use)') | ||
.option(' --oldstyle-self', 'Allow "self" alternatively to "$self" (implied by --tnt-flavor)') // FIXME: We should get rid of that | ||
.option(' --tnt-flavor', 'Compile with backward compatibility for the "TNT" project') | ||
.option(' --tnt-csn', 'Generate TNT-specific post-processed CSN') // FIXME: Should migrate towards --to-odata v2,xml,separate,csn' | ||
.option('@@', 'Diagnostic options') | ||
.option(' --trace-parser', 'Trace parser') // FIXME: Previously --trace-parse | ||
.option(' --trace-parser-amb', 'Trace parser ambiguities') // FIXME: Previously --ambig-detection | ||
.option(' --trace-fs', 'Trace file system access caused by "using from"') | ||
.option('@@', 'Validation options') | ||
.option(' --fuzzy-csn', 'Allow free-style CSN properties by disabling CSN input validation') | ||
.option('@@', 'Internal options (for testing only, may be changed/removed at any time)') | ||
.option('-R, --raw-output', 'Write raw augmented CSN and error output to stdout') | ||
.option(' --beta-mode', 'Enable unsupported, incomplete (beta) features') | ||
.option(' --new-csn', 'Produce new-style CSN (preview of planned future CSN format)') | ||
.option(' --hana-flavor', 'Compile with backward compatibility for HANA CDS (incomplete)') | ||
.option(' --parse-only', 'Stop compilation after parsing and write result to stdout') | ||
.option(' --to-extensions', 'Generate augmented CSN for extensions from properties file') // FIXME: Previously 'generate-extensions', should later become part of normal compilation, producing plain CSN | ||
.option(' --test-mode', `Produce extra-stable output for automated tests (normalize | ||
filenames in errors, sort properties in CSN, omit version in CSN)`) | ||
.option(' --extra-augment', 'Compile to plain CSN, augment and compile again, augmentation tests') | ||
.option(' --re-augmented', 'Re-augmented CSN and error output') // FIXME: What does that mean/do? Isn't that what --extra-augment is supposed to do? | ||
.option(' --old-sql-impl', `Use old "toSql" implementation (deprecated, only as fallback)`) | ||
.option('@@', 'Table renaming (tentative, subject to change, requires "--beta-mode")') | ||
.option(' --to-rename <flags>', `Generate SQL DDL statements to rename existing tables and their | ||
columns so that they match the result of "--to-hana" or "--to-sql" | ||
with the "plain" option. Possible values for <flags> are either | ||
"quoted" or "hdbcds" (default) for the names of the existing tables. | ||
Results are generated as "rename_<artifact>.sql" | ||
quoted : Assume existing SQL tables and views were named in | ||
original case as in CDL (with dots), but element names | ||
were flattened with underscores (as a result e.g. from | ||
"cdsc --to-hana src,quoted") | ||
hdbcds : Assume existing SQL tables, views and columns were | ||
generated by HANA CDS from the same CDS source (or from | ||
"cdsc --to-sql src,hdbcds")`, verifyToRenameOption) | ||
; | ||
// Note: When adding any options where the '--foo-bar' part becomes longer than the longest existing one, | ||
// you will need to adapt the indentation of those having multiple lines, so that '--help' comes out aligned. | ||
// Generally, the alignment of the lines above needs a bit of care - the multi-line ones need to be just so... | ||
// Also, we try to keep the output below 100 characters per line. | ||
// Note: Instead of throwing ProcessExitError, we would rather just call process.exit(exitCode), | ||
@@ -169,64 +34,41 @@ // but that might truncate the output of stdout and stderr, both of which are async (or rather, | ||
// Deal with '--version' explicitly (so that it appears in the proper place in '--help') | ||
program.on('option:version', function() { | ||
process.stdout.write(compiler.version() + '\n'); | ||
throw new ProcessExitError(0); | ||
}); | ||
// Deal with '--help' explicitly (needs some tweaking for the text formatting) | ||
program.on('option:help', function() { | ||
displayUsage(null, 0); | ||
}); | ||
// Report unknown options explicitly (so that it looks like the other error messages) | ||
program.unknownOption = (option) => { | ||
displayUsage(`Unknown option "${option}"`, 2); | ||
} | ||
// Parse the command line and translate it into options | ||
try { | ||
program.parse(process.argv); | ||
// Complain if no files given | ||
// FIXME: Might later read from stdin instead | ||
if (!program.args.length) { | ||
displayUsage('Missing <file> ... arguments', 2); | ||
let cmdLine = optionProcessor.processCmdLine(process.argv); | ||
// Deal with '--version' explicitly | ||
if (cmdLine.options.version) { | ||
process.stdout.write(compiler.version() + '\n'); | ||
throw new ProcessExitError(0); | ||
} | ||
// Please keep these in the same order as the --help output | ||
var options = { | ||
// Default warning level is 2 (info) | ||
warning : program.warning === undefined ? 2 : program.warning, | ||
showMessageId : program.showMessageId, | ||
// Default output goes to stdout | ||
out : program.out || '-', | ||
toHana: program.toHana, | ||
toOdata : program.toOdata, | ||
toCdl : program.toCdl, | ||
toSwagger : program.toSwagger, | ||
toSql : program.toSql, | ||
lintMode : program.lintMode, | ||
// By default, toCsn is on if no other to-option is set | ||
toCsn : program.toCsn || (!program.toHana && !program.toOdata && !program.toCdl && !program.toSwagger && !program.toSql | ||
&& !program.lintMode && !program.toRename), | ||
oldstyleSelf : program.oldstyleSelf, | ||
tntFlavor : program.tntFlavor && getDefaultTntFlavorOptions(), | ||
tntCsn : program.tntCsn, | ||
traceParser : program.traceParser, | ||
traceParserAmb : program.traceParserAmb, | ||
traceFs : program.traceFs, | ||
fuzzyCsn: program.fuzzyCsn, | ||
rawOutput : program.rawOutput, | ||
betaMode : program.betaMode, | ||
newCsn : program.newCsn, // TEMP | ||
hanaFlavor : program.hanaFlavor, | ||
parseOnly : program.parseOnly, | ||
toExtensions : program.toExtensions, | ||
testMode : program.testMode, | ||
extraAugment : program.extraAugment, | ||
reAugmented : program.reAugmented, | ||
oldSqlImpl : program.oldSqlImpl, | ||
toRename : program.toRename, | ||
// Deal with '--help' explicitly | ||
if (cmdLine.command) { | ||
// Command specific help | ||
if (cmdLine.options.help || cmdLine.options[cmdLine.command] && cmdLine.options[cmdLine.command].help) { | ||
displayUsage(null, optionProcessor.commands[cmdLine.command].helpText, 0); | ||
} | ||
} else if (cmdLine.options.help) { | ||
// General help | ||
displayUsage(null, optionProcessor.helpText, 0); | ||
} | ||
// Report complaints if any | ||
if (cmdLine.cmdErrors.length > 0) { | ||
// Command specific errors | ||
displayUsage(cmdLine.cmdErrors, optionProcessor.commands[cmdLine.command].helpText, 2); | ||
} else if (cmdLine.errors.length > 0) { | ||
// General errors | ||
displayUsage(cmdLine.errors, optionProcessor.helpText, 2); | ||
} | ||
// Do the work for all options at once | ||
executeCommandLine(options, program.args) | ||
// Default warning level is 2 (info) | ||
// FIXME: Is that not set anywhere in the API? | ||
if (!cmdLine.options.warning) { | ||
cmdLine.options.warning = 2; | ||
} | ||
// Default output goes to stdout | ||
if (!cmdLine.options.out) { | ||
cmdLine.options.out = '-'; | ||
} | ||
// Do the work for the selected command (default 'toCsn' | ||
executeCommandLine(cmdLine.command || 'toCsn', cmdLine.options, cmdLine.args); | ||
} catch (err) { | ||
@@ -242,13 +84,14 @@ // This whole try/catch is only here because process.exit does not work in combination with | ||
// Display 'error' (if any) and the program's usage help text, then exit with exit code <code> | ||
function displayUsage(error, code) { | ||
program.outputHelp(helpText => { | ||
return helpText | ||
.replace(/\n\n {2}Options:\n\n/, '') // Get rid of the extra 'Options:' headline with many newlines | ||
.replace(/ {4}@@ */g, '\n ') // Convert the '@@' fake-options to subsections | ||
.replace(/ {4}-h, --help *output usage information\n/g, ''); // Get rid of auto-generated '--help' | ||
}); | ||
// Display error at the end (more readable, no scrolling) | ||
// Display help text 'helpText' and 'error' (if any), then exit with exit code <code> | ||
function displayUsage(error, helpText, code) { | ||
// Display non-error output (like help) to stdout | ||
let out = (code == 0 && !error) ? process.stdout : process.stderr; | ||
// Display help text first, error at the end (more readable, no scrolling) | ||
out.write(`${helpText}\n`); | ||
if (error) { | ||
process.stderr.write(`\n ERROR: ${error}\n`); | ||
if (error instanceof Array) { | ||
out.write(error.map(error => `cdsc: ERROR: ${error}`).join('\n') + '\n'); | ||
} else { | ||
out.write(`cdsc: ERROR: ${error}\n`); | ||
} | ||
} | ||
@@ -258,198 +101,4 @@ throw new ProcessExitError(code); | ||
// Check the value of --warning for legal values. Return value as an int. | ||
function verifyWarningOption(value) { | ||
let result = parseInt(value); | ||
if (Number.isNaN(result) || result < 0 || result > 2) { | ||
displayUsage(`Illegal <level> "${value}" for option "-w, --warning"`, 2); | ||
} | ||
return result; | ||
} | ||
// Check whether all 'flags' occur in array 'allowedFlags'. Complain if one doesn't, mentioning | ||
// the option name 'optStr' and optionally the flag name 'flagId'. Return an object with each of 'flags' | ||
// set to 'true' | ||
function verifyFlags(allowedFlags, flags, optStr, flagId='flag') { | ||
let result = {}; | ||
for (let flag of flags.split(',')) { | ||
if (!allowedFlags.includes(flag)) { | ||
displayUsage(`Illegal <${flagId}> "${flag}" for option "${optStr}", allowed are: "${allowedFlags.join(',')}"`, 2); | ||
} | ||
result[flag] = true; | ||
} | ||
return result; | ||
} | ||
// Check the value of --to-odata for legal values. Return the value as an object with sub-options. | ||
function verifyToOdataOption(value) { | ||
let result = verifyFlags([ | ||
'v4', 'v2', | ||
'xml', 'json', | ||
'separate', 'combined', | ||
'csn', | ||
'plain', 'quoted', 'hdbcds'], value, '-O, --to-odata'); | ||
if (result.v2) { | ||
result.version = 'v2' | ||
} | ||
if (result.v4) { | ||
result.version = 'v4' | ||
} | ||
if (result.v2 && result.v4) { | ||
displayUsage(`Either "v2" (default) or "v4" can be specified for version with option "-O, --to-odata", but not both`, 2); | ||
} | ||
delete result.v2; | ||
delete result.v4; | ||
let nrOfNameFlags = 0; | ||
if (result.plain) { | ||
result.names = 'plain' | ||
nrOfNameFlags++; | ||
} | ||
if (result.quoted) { | ||
result.names = 'quoted' | ||
nrOfNameFlags++; | ||
} | ||
if (result.hdbcds) { | ||
result.names = 'hdbcds' | ||
nrOfNameFlags++; | ||
} | ||
if (nrOfNameFlags > 1) { | ||
displayUsage(`Only one of "plain", "quoted" or "hdbcds" can be specified for names with option "-O, --to-odata", but not combinations`, 2); | ||
} | ||
delete result.plain; | ||
delete result.quoted; | ||
delete result.hdbcds; | ||
return result; | ||
} | ||
// Check the value of --to-swagger for legal values. Return the value as an object with sub-options. | ||
function verifyToSwaggerOption(value) { | ||
return verifyFlags(['json','csn'], value, '-S, --to-swagger'); | ||
} | ||
// Check the value of --to-hana for legal values. Return the value as an object with sub-options. | ||
function verifyToHanaOption(value) { | ||
let result = verifyFlags([ | ||
'plain', 'quoted', 'hdbcds', | ||
'assocs', 'joins', | ||
'src', 'csn'], value, '-H, --to-hana'); | ||
let flagCount = 0; | ||
if (result.plain) { | ||
result.names = 'plain' | ||
flagCount++; | ||
} | ||
if (result.quoted) { | ||
result.names = 'quoted' | ||
flagCount++; | ||
} | ||
if (result.hdbcds) { | ||
result.names = 'hdbcds' | ||
flagCount++; | ||
} | ||
if (flagCount > 1) { | ||
displayUsage(`Only one of "plain" (default), "quoted" or "hdbcds" can be specified for names with option "-H, --to-hana", but not combinations`, 2); | ||
} | ||
delete result.plain; | ||
delete result.quoted; | ||
delete result.hdbcds; | ||
if (result.assocs) { | ||
result.associations = 'assocs' | ||
} | ||
if (result.joins) { | ||
result.associations = 'joins' | ||
} | ||
if (result.assocs && result.joins) { | ||
displayUsage(`Either "assocs" (default) or "joins" can be specified for associations with option "-H, --to-hana", but not both`, 2); | ||
} | ||
delete result.assocs; | ||
delete result.joins; | ||
return result; | ||
} | ||
// Check the value of --to-sql for legal values. Return the value as an object with sub-options. | ||
function verifyToSqlOption(value) { | ||
let result = verifyFlags([ | ||
'plain', 'quoted', 'hdbcds', | ||
'assocs', 'joins', | ||
'src', 'csn', | ||
'hana', 'sqlite'], value, '-Q, --to-sql'); | ||
let flagCount = 0; | ||
if (result.plain) { | ||
result.names = 'plain' | ||
flagCount++; | ||
} | ||
if (result.quoted) { | ||
result.names = 'quoted' | ||
flagCount++; | ||
} | ||
if (result.hdbcds) { | ||
result.names = 'hdbcds' | ||
flagCount++; | ||
} | ||
if (flagCount > 1) { | ||
displayUsage(`Only one of "quoted" (default), "quoted" or "hdbcds" can be specified for names with option "-Q, --to-sql", but not combinations`, 2); | ||
} | ||
delete result.quoted; | ||
delete result.quoted; | ||
delete result.hdbcds; | ||
flagCount = 0; | ||
if (result.assocs) { | ||
result.associations = 'assocs' | ||
flagCount++; | ||
} | ||
if (result.joins) { | ||
result.associations = 'joins' | ||
flagCount++; | ||
} | ||
if (flagCount > 1) { | ||
displayUsage(`Only one of "assocs" (default) or "joins" can be specified for associations with option "-Q, --to-sql", but not both`, 2); | ||
} | ||
delete result.assocs; | ||
delete result.joins; | ||
if (result.hana) { | ||
result.dialect = 'hana'; | ||
} | ||
if (result.sqlite) { | ||
result.dialect = 'sqlite'; | ||
} | ||
if (result.hana && result.sqlite) { | ||
displayUsage(`Either "hana" or "sqlite" (default) can be specified for a dialect with option "-Q, --to-sql", but not both`, 2); | ||
} | ||
delete result.hana; | ||
delete result.sqlite; | ||
return result; | ||
} | ||
// Check the value of --to-rename for legal values. Return the value as an object with sub-options. | ||
function verifyToRenameOption(value) { | ||
let result = verifyFlags(['quoted','hdbcds'], value, '--to-rename'); | ||
if (result.quoted) { | ||
result.names = 'quoted' | ||
} | ||
if (result.hdbcds) { | ||
result.names = 'hdbcds' | ||
} | ||
if (result.quoted && result.hdbcds) { | ||
displayUsage(`Either "quoted" or "hdbcds" (default) can be specified for names with option "--to-rename", but not both`, 2); | ||
} | ||
delete result.quoted; | ||
delete result.hdbcds; | ||
return result; | ||
} | ||
// Check the value of --to-csn for legal values. Return the value as an object with sub-options. | ||
function verifyToCsnOption(value) { | ||
verifyFlags(['client', 'gensrc'], value, '--to-csn', 'flavor'); | ||
return (value == 'gensrc') ? { gensrc: true } : undefined; | ||
} | ||
// Executes a command line that has been translated to 'options' (what to do) and 'args' (which files) | ||
function executeCommandLine(options, args) { | ||
// Executes a command line that has been translated to 'command' (what to do), 'options' (how) and 'args' (which files) | ||
function executeCommandLine(command, options, args) { | ||
const normalizeFilename = options.testMode && process.platform === 'win32'; | ||
@@ -465,56 +114,22 @@ const messageLevels = { Error: 0, Warning: 1, Info: 2, None: 3 }; | ||
var run; | ||
if (options.toExtensions) { | ||
// This option can disappear when each extension is tagged whether is has | ||
// been applied - the compactor could then list the non-applied extensions | ||
try { | ||
console.log(JSON.stringify(compiler.generateExtensions(args[0], options), null, 2)); | ||
} catch (err) { | ||
console.error(err.message); | ||
process.exit(1); | ||
} | ||
// Add implementation functions corresponding to commands here | ||
const commands = { | ||
toCdl, | ||
toCsn, | ||
toHana, | ||
toOdata, | ||
toRename, | ||
toSql, | ||
toSwagger, | ||
toTntSpecificOutput, | ||
} | ||
else { | ||
run = compiler.compile( args, undefined, options ); | ||
} | ||
// The backends (should be changed to async where necessary): ---------------- | ||
if (options.toExtensions) { | ||
run = null; // already done (option to be deleted) | ||
if (!commands[command]) { | ||
throw new Error(`Missing implementation for command ${command}`); | ||
} | ||
else if (options.tntCsn) { | ||
// FIXME: This is the only one that is not actually combinable with others | ||
run = run.then( tntOutput ); | ||
} else { | ||
// Backend options (in alphabetical order) | ||
if (options.toCdl) { | ||
run = run.then( toCdl ); | ||
} | ||
if (options.toCsn) { // standard: output CSN | ||
run = run.then( toCsn ); | ||
} | ||
if (options.toHana) { | ||
run = run.then( toHana ); | ||
} | ||
if (options.toOdata) { | ||
run = run.then( toOdata ); | ||
} | ||
if (options.toRename) { | ||
run = run.then( toRename ); | ||
} | ||
if (options.toSql) { | ||
run = run.then( toSql ); | ||
} | ||
if (options.toSwagger) { | ||
run = run.then( toSwagger ); | ||
} | ||
} | ||
if(run) { | ||
run = run.then( displayMessages, displayErrors ); | ||
} | ||
compiler.compile( args, undefined, options ) | ||
.then( commands[command] ) | ||
.then( displayMessages, displayErrors ) | ||
.catch( catchErrors ); | ||
if (run) { | ||
run.catch( catchErrors ); | ||
} | ||
// Execute the command line option '--to-cdl' and display the results. | ||
@@ -619,3 +234,3 @@ // Return the original model (for chaining) | ||
// Return the original model (for chaining) | ||
function tntOutput( model ) { | ||
function toTntSpecificOutput( model ) { | ||
// TODO: use async file-system API | ||
@@ -670,3 +285,3 @@ // Perform TNT-specific post-processing | ||
if (messageLevels[ msg.severity ] <= options.warning) | ||
console.error( compiler.messageString( msg, normalizeFilename, !options.showMessageId ) ); | ||
console.error( compiler.messageString( msg, normalizeFilename, !options.showMessageId, !options.testMode ) ); | ||
} | ||
@@ -673,0 +288,0 @@ } |
@@ -32,2 +32,3 @@ // Make internal properties of the augmented CSN visible | ||
$layerNumber: n => n, | ||
$extra: e => e, | ||
_layerRepresentative: s => s.realname, | ||
@@ -41,4 +42,9 @@ _layerExtends: layerExtends, | ||
_navigation: artifactIdentifier, | ||
_origTarget: artifactIdentifier, | ||
_tableAlias: artifactIdentifier, | ||
_projections: artifactIdentifier, // array | ||
_redirected: artifactIdentifier, // array | ||
_entities: artifactIdentifier, // array | ||
_ancestors: artifactIdentifier, // array | ||
_descendants: artifactDictionary, // dict of array | ||
_$queryNode: n => (n.location && { location: locationString( n.location ) }), | ||
@@ -73,2 +79,8 @@ _$next: artifactIdentifier, | ||
return 'this'; | ||
if (node.kind === 'source') | ||
return 'source:' + quoted( node.filename ); | ||
if (node.kind === '$magicVariables') | ||
return '$magicVariables'; | ||
if (!node.name) | ||
return JSON.stringify(node); | ||
switch (node.kind) { | ||
@@ -78,28 +90,18 @@ case undefined: // TODO: remove this `returns` property for actions | ||
? artifactIdentifier( node._artifact ) | ||
: JSON.stringify(node); | ||
: JSON.stringify(node.name); | ||
case 'builtin': | ||
return 'builtin("cds")'; | ||
return '$magicVariables/' + msg.artName(node); | ||
case 'source': | ||
return (node.name) | ||
? 'source(' + quoted( node.filename ) + ',using:' + quoted( node.name.id ) + ')' | ||
: 'source(' + quoted( node.filename ) + ')'; | ||
case 'using': | ||
return 'using(' + quoted( node.name && node.name.id ) + | ||
',source:' + quoted( node.location && node.location.filename ) + ')'; | ||
return 'source:' + quoted( node.location && node.location.filename ) + | ||
'/using:' + quoted( node.name.id ) | ||
default: { | ||
let names = []; | ||
if (node.name) { | ||
[ 'element', 'alias', 'query', 'param', 'action', 'absolute' ].forEach( function( prop ) { | ||
if (prop in node.name) | ||
names.push( (names.length || node.kind === 'annotate' ? prop + ':' : '') + quoted( node.name[prop] ) ); | ||
}); | ||
} | ||
else | ||
names = [ quoted(undefined) ]; | ||
if (!names.length) // TODO: should only be necessary temporarily | ||
names = [JSON.stringify(node.name)]; | ||
// TODO: as long as we have no kind='mixin': | ||
let kind = (node.kind === 'element' && !node.name.element) ? 'mixin' : node.kind; | ||
return (kind || '<kind>') + outer + '(' + names.join(',') + | ||
( node.name && node.name.$renamed ? ', orig:"' + node.name.$renamed + '")' : ')' ); | ||
let kind = (node._main) | ||
? node._main.kind | ||
: (node.kind === 'block') | ||
? node._parent && node._parent.kind | ||
: node.kind; | ||
return (kind || '<kind>') + ':' + msg.artName( node ) + outer + | ||
// todo: get rid of $renamed / do the oData renaming late on the CSN | ||
(node.name.$renamed ? '-original:' + quoted( node.name.$renamed ) : ''); | ||
} | ||
@@ -106,0 +108,0 @@ } |
103
CHANGELOG.md
@@ -1,3 +0,102 @@ | ||
# ChangeLog for cdx compiler and backends | ||
# ChangeLog for cdx compiler and backends | ||
## Version 1.8.0 | ||
Features | ||
* Support the OData annotation vocabularies `PersonalData` and `Aggregation`. | ||
The vocabulary for `PersonalData` contains a number of annotations that are flagged | ||
as "experimental". Their usage will result in a warning. | ||
* New option for specifying the locale in SQLite dialect. As part of the `toSql` | ||
command is now available the options `'-l, --locale <locale>'` for specifying | ||
value for the "$user.locale" variable. | ||
Changes | ||
* Entity definitions with elements of type `array` and structure type definitions with | ||
association elements will now lead to an error message when generating edmx for OData v2. | ||
These constructs are not allowed in OData v2, but there was no corresponding check in the | ||
cds compiler yet. | ||
## Version 1.7.1 | ||
Fixes | ||
* Restore version function which was deleted by accident | ||
## Version 1.7.0 | ||
Features | ||
* Allow entities to have parameters. They can be referred to inside the query with | ||
`:Param`. Entites with parameters are not allowed in `toSql` for dialect "sqlite". | ||
When generating for HANA, parameters cannot be used in combination with associations: | ||
an entity with parameters cannot have associations, and an association must not point | ||
to an entity with parameters. | ||
* The parameters and return value of actions and functions can now have structured types. | ||
* In the annotation translation for OData, falsy values of the special variable `$value` | ||
(that is used to provide nested annotations for scalar values) are correctly handled. | ||
* When (new-style) csn is used as input, the compiler ignores unknown attributes. | ||
* Implicit redirection and auto-exposure are now applied recursively, i.e. the associations | ||
of an auto-exposed entity are considered for implicit redirection and auto-exposure, | ||
if necessary. | ||
Changes: | ||
* With `--new-csn`, consider `redirected to` on projected associations and | ||
adapt the `on` condition and the `keys` specification accordingly. There are | ||
also Info messages if an element referred to in the `on` condition or `keys` | ||
specification has not been projected to the new association target. | ||
_The severity of these messages will be increased if implicit redirections | ||
will have been performed by the core compiler._ | ||
* `toHana` and `toSql` now reject entities that only contain unmanaged associations. | ||
Such entites would lead to a deployment error later. | ||
* SQL name mapping modes `quoted` and `hdbcds` are only allowed when generating for HANA. | ||
* In the csn, the csn language version is now stored in the top level attribute `$version`. | ||
The version information via `version.csn` is deprecated and will be removed in a future | ||
release. The information about the creator of the csn has been moved inside the new | ||
top level attribute `meta`. | ||
Fixes | ||
* Provide code completion for references in complex select item expressions not | ||
(yet) having an alias (complex = not consisting of just a reference). | ||
* With `--new-csn`, avoid internal error while rewriting the `on` condition | ||
from an element of a source entity which refers to a `mixin` definition with | ||
an `on` condition containing a reference like `$projection.<elem>`. | ||
* OData, edmx generation: correctly escape the characters `<`, `>`, `&`, and `"`. | ||
* When an entity is auto-exposed, it's annotations are transferred to the generated | ||
projection. | ||
## Version 1.6.0 | ||
Features | ||
* Provide code completion for `using` declarations. | ||
* Support the OData annotation vocabulary "Validation". | ||
* For compositions in EDM, add `<OnDelete Action="Cascade"/>` to the navigation | ||
property where required. | ||
Changes | ||
* With `--new-csn`, complain more often about projected associations whose `on` | ||
condition could not be rewritten correctly. | ||
* Make `associations: 'joins'` the default for `toSql` (because the default for | ||
`dialect` is already `sqlite`, which requires joins). | ||
* Adapt the command line interface to use commands instead of the `--to...` generation | ||
options (e.g. `cdsc toHana --src --names plain` instead of `cdsc --toHana src,plain`). | ||
Please see the [Command Line Migration guide](doc/CommandLineMigration.md) | ||
for details. | ||
* Add a `generated by cds-compiler version x.y.z` comment to all generated SQL and `hdbcds` | ||
sources. | ||
* Replace the CSN validator (formerly `ajv`) with a new own implementation. | ||
Fixes | ||
* With `--new-csn`, do not change references to magic variables like `$user.id` | ||
while rewriting the `on` conditition of a projected association. | ||
* Apply OData specific checks (e.g. that all elements of an entity must have a type) | ||
applied only to objects that are exposed in a service. | ||
* When generating SQL for SQLite, replace the the special variables `$now`, `$user.id` | ||
and `$user.locale` by `CURRENT_TIMESTAMP`, `'$user.id'`, and `'EN'`, respectively. | ||
* Issue a warning for conflicting cardinality declarations (e.g. `association[1] to many ...`). | ||
* Handle filters with cardinality correctly when translating associations to joins. | ||
* Avoid crash when checking structured action parameters. | ||
* Handle `$self` as the first of multiple path steps correctly in `toOdata`. | ||
* In `toHana`, render the combination of enums and `type of` correctly. | ||
* In mixins generated by `toHana`, handle special variables starting with `$` correctly. | ||
## Version 1.5.0 | ||
@@ -878,3 +977,3 @@ | ||
Features | ||
* New implementation of name resolution (according to [spec](https://github.wdf.sap.corp/CDS/cdsv/blob/master/doc/NameResolution.md)) | ||
* New implementation of name resolution (according to [spec](doc/NameResolution.md) | ||
* Support for bound and unbound actions and functions | ||
@@ -881,0 +980,0 @@ * More semantic checks |
# Command Line Migration | ||
With revision 1.0.24, the CDS compiler offers a new command line interface `cdsc`. The old `cdsv` command line is | ||
deprecated, will not be extended with new features, and will be removed in a subsequent release. | ||
With revision 1.5.1, the `cdsc` command line interface has been adapted to use commands with | ||
options. | ||
Please see `cdsc --help` for a description of the new command line options (snapshot of current version below). | ||
Usage is now `cdsc <command> [options] <file...>` instead of `cdsc [options] <file...>`. | ||
``` | ||
$ cdsc --help | ||
The generation options (`--toHana`, `--toSql`, ...) have been replaced by commands | ||
(`toHana`, `toSql`, ...). This allows for better per-command options, which can now be optional, | ||
can use more single-letter abbreviations, and now match those from the `options` object in the API. | ||
Usage: cdsc [options] <file> ... | ||
Some examples: | ||
Compile a CDS model given from the input files. Input files may be CDS source files (.cds), CSN | ||
model files (.json) and XML files (.xml) | ||
for pre-processed ODATA annotations. | ||
| Old command line | New command line | | ||
| -------------------------- | --------------------------------------------- | | ||
| `cdsc --new-csn --toHana csn,plain foo.cds` | `cdsc --new-csn toHana --csn --names plain foo.cds` | | ||
| `cdsc -R --H csn,plain foo.cds` | `cdsc -R H -c -n plain foo.cds` | | ||
| `cdsc --toOdata xml,v2,separate foo.cds` | `cdsc toOdata --xml --version v2 --separate foo.cds` | | ||
| `cdsc --toSql src foo.cds` | `cdsc toSql foo.cds` | | ||
| `cdsc foo.cds` | `cdsc foo.cds` | | ||
General Options | ||
-h, --help Display this help text and exit | ||
-w, --warning <level> Show warnings up to <level> (0: Error, 1: Warnings (default), 2: Info) | ||
-v, --version Display version number and exit | ||
List of commands (as of v1.5.1): | ||
Generation options (default if none given: generate original model as CSN to <stdout>) | ||
-o, --out <dir> Place generated files in directory <dir>, default is "-" for <stdout> | ||
-H, --to-hana <flags> Generate HANA CDS source, <flags> can be a comma-separated combination | ||
of either "plain" or "quoted" (default) for entity names, either "assocs" | ||
(default) or "joins" for associations and any of "src,csn" | ||
plain : produce uppercased flattened HANA entity names with | ||
underscores | ||
quoted : produce HANA entity names with dots and nested contexts | ||
as in CDL | ||
assocs : keep associations in HANA CDS as far as possible | ||
joins : transform associations to HANA CDS joins | ||
src : generate HANA CDS source files | ||
csn : generate "hana_csn.json" with HANA-preprocessed model | ||
-O, --to-odata <flags> Generate ODATA metadata and annotations, <flags> can be a comma- | ||
separated combination of "xml,json,separate,combined,csn" and either | ||
"v2" (default) or "v4" version | ||
v2 : Generate ODATA V2 output | ||
v4 : Generate ODATA V4 output | ||
xml : generate XML output (separate or combined) | ||
json : generate JSON output (not available for V2) | ||
separate: generate "<svc>_metadata.xml" and "<svc>_annotations.xml" | ||
combined: generate "<svc>.xml" | ||
csn : generate "odata_csn.json" with ODATA-preprocessed model | ||
-C, --to-cdl Generate CDS source | ||
-S, --to-swagger Generate Swagger (OpenAPI) JSON | ||
-Q, --to-sql <flags> Generate SQL DDL statements, <flags> can be a comma-separated | ||
combination of either "plain" or "quoted" (default) for entity names, | ||
either "assocs" (default) or "joins" for associations and any of | ||
"src,csn" | ||
plain : produce uppercased flattened table/view names with | ||
underscores | ||
quoted : produce quoted table/view names with dots | ||
assocs : keep associations as far as possible (only usable for | ||
HANA SQL) | ||
joins : transform associations to SQL joins | ||
src : generate SQL source files | ||
csn : generate "sql_csn.json" with SQL-preprocessed model | ||
-l, --lint-mode Generate nothing, just produce single-file error messages if any (for | ||
use by editors) | ||
--to-csn Generate original model as CSN to "csn.json" | ||
``` | ||
Commands | ||
H, toHana [options] <file...> Generate HANA CDS source files | ||
O, toOdata [options] <file...> Generate ODATA metadata and annotations | ||
C, toCdl <file...> Generate CDS source files | ||
S, toSwagger [options] <file...> Generate Swagger (OpenAPI) JSON | ||
Q, toSql [options] <file...> Generate SQL DDL statements | ||
toCsn [options] <file...> (default) Generate original model as CSN | ||
toTntSpecificOutput <file...> (internal) Generate TNT-specific post-processed CSN | ||
toRename [options] <file...> (internal) Generate SQL DDL rename statements | ||
``` | ||
Backward compatibility options (deprecated, do not use) | ||
--check-model Perform extra checks on the model | ||
--oldstyle-self Allow "self" alternatively to "$self" (implied by --tnt-flavor) | ||
--tnt-flavor Compile with backward compatibility for the "TNT" project | ||
--tnt-csn Generate TNT-specific post-processed CSN | ||
Please see `cdsc --help` for the list of commands and general options, or `cdsc <command> --help` | ||
for help regarding a specific command. | ||
Diagnostic options | ||
--trace-parser Trace parser | ||
--trace-parser-amb Trace parser ambiguities | ||
--trace-fs Trace file system access caused by "using from" | ||
Internal options (for testing only, may be changed/removed at any time) | ||
-R, --raw-output Write raw augmented CSN and error output to stdout | ||
--beta-mode Enable unsupported, incomplete (beta) features | ||
--hana-flavor Compile with backward compatibility for HANA CDS (incomplete) | ||
--parse-only Stop compilation after parsing and write result to stdout | ||
--to-extensions Generate augmented CSN for extensions from properties file | ||
--test-mode Produce extra-stable output for automated tests (normalize filenames | ||
in errors, sort properties in CSN, omit version in CSN) | ||
--extra-augment Compile to plain CSN, augment and compile again, augmentation tests | ||
--re-augmented Re-augmented CSN and error output | ||
--omit-record-type Omit unnecessary type attribute for ODATA records | ||
``` | ||
## Some helpful hints | ||
Please note the following general concepts regarding the new command line | ||
- All `--to-...` options are orthogonal, i.e. they may be freely combined to generate multiple kinds of output with one invocation. | ||
- When no `--to-...` options at all are specified, the behavior is still the same as `cdsv` (generate CSN output to `stdout`). | ||
- When no `--out` option is provided or if `-` is specified as output directory , all output will go to `<stdout>` instead of | ||
being written to files - this is helpful for manual testing. | ||
- The `--raw-output` option also affects all `--to-...` options where a CSN file is generated. Instead of `csn.json`, a `csn_raw.txt` | ||
will be produced. | ||
## Migration guide | ||
The following table shows replacements for some common `cdsv` options | ||
| Old command line | New command line | | ||
| -------------------------- | --------------------------------------------- | | ||
| `cdsv --odata-and-hana-output ...` | `cdsc --to-odata xml,json,separate,combined,csn --to-hana src ...` | | ||
| `cdsv --odata-and-hana-output --odata-only ...` | `cdsc --to-odata xml,json,separate,combined,csn ...` | | ||
| `cdsv --odata-and-hana-output --odata-only --odatav4 ...` | `cdsc --to-odata v4,xml,json,separate,combined,csn src ...` | | ||
| `cdsv --odata-preprocess ...` | `cdsc --to-odata csn ...` | | ||
| `cdsv --odata-preprocess --odatav4 ...` | `cdsc --to-odata csn,v4 ...` | | ||
| `cdsv --to-hana ...` | `cdsc --to-hana src --out - ...` | | ||
| `cdsv --hana-preprocess ...` | `cdsc --to-hana csn ...` | | ||
| `cdsv --cdl-output ...` | `cdsc --to-cdl ...` | | ||
| `cdsv --to-sql ...` | `cdsc --to-sql src ...` | | ||
| `cdsv --tnt-output foo ...` | `cdsc --out foo --tnt-flavor --to-odata v2,xml,separate --tnt-csn ...` | | ||
## Changes in behavior | ||
The following changes have been made to `cdsc` (all affecting ODATA output). | ||
- ODATA output is generated either for V2 or for V4, with identical filenames (i.e. the suffix `_v4` no longer appears in filenames). | ||
- The ODATA files `metadata.xml` and `annotations.xml` are no longer produced (`cdsv` only produced them if there was exactly one | ||
service in the model). Note that you can still produce `<service>_metadata.xml` and `<service>_annotations.xml` per service by | ||
specifiying the flag `separate` for `--to-odata` . | ||
- The former ODATA output file `csn.json` is now named `odata_csn.json`, to avoid the name clash with the output of `--to-csn`, | ||
which is now `csn.json`. | ||
- The content of ODATA output files for V2 is now equivalent to the new API results for V2 (`cdsv` produced the separate `annotations.xml` | ||
file with `V4` even if `V2` was specified, resulting in slightly different output. The `combined.xml` file always had the correct versioning). | ||
Please note the following general concepts regarding the new command line: | ||
- General options can be placed anywhere, command specific options must appear after the command. | ||
- In the unlikely case that a file name starts with `-`, please use `--` to indicate the end of options. | ||
- The `src` argument of `toHana`, `toCdl`, `toSql` is now optional (and it would now be `--src`). | ||
- If no command is specified, the default is `toCsn --flavor client` (as before). | ||
- When no `--out` option is provided or if `-` is specified as output directory , all output will | ||
go to `<stdout>` instead of being written to files (like before). | ||
- The `--raw-output` option also affects all commands where a CSN file is generated. | ||
Instead of `...csn.json`, a `...csn_raw.txt` will be produced (like before). |
@@ -6,4 +6,2 @@ 'use strict'; | ||
// FIXME: Adapt and unify all API docus. | ||
const csnToSwagger = require('./render/toSwagger'); | ||
@@ -15,5 +13,4 @@ const { transformForHana } = require('./transform/forHana'); | ||
const { toSqlDdl } = require('./render/toSql'); | ||
const { toSqlDdlNew } = require('./render/toSqlNew'); | ||
const { toRenameDdl } = require('./render/toRename'); | ||
const { transform4odata, getServiceNames, compactForService } = require('./transform/forOdata'); | ||
const { transform4odata, getServiceNames } = require('./transform/forOdata'); | ||
const csn2edm = require('./edm/csn2edm'); | ||
@@ -23,22 +20,121 @@ const { mergeOptions } = require('./model/modelUtils'); | ||
const alerts = require('./base/alerts'); | ||
const { setProp } = require('./base/model'); | ||
var { CompilationError, sortMessages } = require('./base/messages'); | ||
const { createOptionProcessor } = require('./base/optionProcessor'); | ||
// This option processor is used both by the command line parser (to translate cmd line options | ||
// into an options object) and by the API functions (to verify options) | ||
let optionProcessor = createOptionProcessor(); | ||
// General options | ||
// FIXME: Since they mainly affect the compiler, they could also live near main.compile | ||
optionProcessor | ||
.option('-h, --help') | ||
.option('-v, --version') | ||
.option('-w, --warning <level>', ['0', '1', '2']) | ||
.option(' --show-message-id') | ||
.option('-o, --out <dir>') | ||
.option('-l, --lint-mode') | ||
.option(' --fuzzy-csn-error') | ||
.option(' --trace-parser') | ||
.option(' --trace-parser-amb') | ||
.option(' --trace-fs') | ||
.option('-R, --raw-output') | ||
.option(' --beta-mode') | ||
.option(' --new-csn') | ||
.option(' --hana-flavor') | ||
.option(' --parse-only') | ||
.option(' --test-mode') | ||
.option(' --tnt-flavor') | ||
.help(` | ||
Usage: cdsc <command> [options] <file...> | ||
Compile a CDS model given from input <file...>s and generate results according to <command>. | ||
Input files may be CDS source files (.cds), CSN model files (.json) or pre-processed ODATA | ||
annotation XML files (.xml). Output depends on <command>, see below. If no command is given, | ||
"toCsn" is used by default. | ||
Use "cdsc <command> --help" to get more detailed help for each command. | ||
General options | ||
-h, --help Show this help text | ||
-v, --version Display version number and exit | ||
-w, --warning <level> Show warnings up to <level> | ||
0: Error | ||
1: Warnings | ||
2: (default) Info | ||
--show-message-id Show message ID in error, warning and info messages | ||
-o, --out <dir> Place generated files in directory <dir>, default is "-" for <stdout> | ||
-l, --lint-mode Generate nothing, just produce single-file error messages if any (for | ||
use by editors) | ||
--fuzzy-csn-error Report free-style CSN properties as errors | ||
-- Indicate the end of options (helpful if source names start with "-") | ||
Diagnostic options | ||
--trace-parser Trace parser | ||
--trace-parser-amb Trace parser ambiguities | ||
--trace-fs Trace file system access caused by "using from" | ||
Internal options (for testing only, may be changed/removed at any time) | ||
-R, --raw-output Write raw augmented CSN and error output to <stdout> | ||
--beta-mode Enable unsupported, incomplete (beta) features | ||
--new-csn Produce new-style CSN (preview of planned future CSN format) | ||
--hana-flavor Compile with backward compatibility for HANA CDS (incomplete) | ||
--parse-only Stop compilation after parsing and write result to <stdout> | ||
--test-mode Produce extra-stable output for automated tests (normalize filenames | ||
in errors, sort properties in CSN, omit version in CSN) | ||
Backward compatibility options (deprecated, do not use) | ||
--tnt-flavor Compile with backward compatibility for the "TNT" project | ||
Commands | ||
H, toHana [options] <file...> Generate HANA CDS source files | ||
O, toOdata [options] <file...> Generate ODATA metadata and annotations | ||
C, toCdl <file...> Generate CDS source files | ||
S, toSwagger [options] <file...> Generate Swagger (OpenAPI) JSON | ||
Q, toSql [options] <file...> Generate SQL DDL statements | ||
toCsn [options] <file...> (default) Generate original model as CSN | ||
toTntSpecificOutput <file...> (internal) Generate TNT-specific post-processed CSN | ||
toRename [options] <file...> (internal) Generate SQL DDL rename statements | ||
`); | ||
// ----------- toHana ----------- | ||
optionProcessor.command('H, toHana') | ||
.option('-h, --help') | ||
.option('-n, --names <style>', ['plain', 'quoted', 'hdbcds']) | ||
.option('-a, --associations <proc>', ['assocs', 'joins']) | ||
.option('-s, --src') | ||
.option('-c, --csn') | ||
.help(` | ||
Usage: cdsc toHana [options] <file...> | ||
Generate HANA CDS source files, or CSN. | ||
Options | ||
-h, --help Show this help text | ||
-n, --names <style> Naming style for generated entity and element names: | ||
plain : (default) Produce HANA entity and element names in | ||
uppercase and flattened with underscores. Do not generate | ||
structured types. | ||
quoted : Produce HANA entity and element names in original case as | ||
in CDL. Keep nested contexts (resulting in entity names | ||
with dots), but flatten element names with underscores. | ||
Generate structured types, too. | ||
hdbcds : Produce HANA entity end element names as HANA CDS would | ||
generate them from the same CDS source (like "quoted", but | ||
using element names with dots). | ||
-a, --associations <proc> Processing of associations: | ||
assocs : (default) Keep associations in HANA CDS as far as possible | ||
joins : Transform associations to joins | ||
-s, --src (default) Generate HANA CDS source files "<artifact>.hdbcds" | ||
-c, --csn Generate "hana_csn.json" with HANA-preprocessed model | ||
`); | ||
// Transform an augmented CSN 'model' into HANA-compatible CDS source. | ||
// The following options control what is actually generated: | ||
// The following options control what is actually generated (see help above): | ||
// options : { | ||
// toHana.names : either 'plain', 'quoted' (default) or 'hdbcds' | ||
// 'plain': Produce HANA entity and element names in uppercase and | ||
// flattened with underscores. Do not generate structured | ||
// types. | ||
// 'quoted': Produce HANA entity and element names in original case | ||
// as in CDL. Keep nested contexts (resulting in entity names | ||
// with dots), but flatten element names with underscores. | ||
// Generate structured types, too. | ||
// 'hdbcds: Produce HANA entity and element as HANA CDS would generate | ||
// them from the same CDS source (like "quoted", but using | ||
// element names with dots). | ||
// toHana.associations : either 'assocs' (default, keep associations as they are if possible) | ||
// or 'joins' (replace associations by joins) | ||
// toHana.src : if true, generate HANA CDS source files (default) | ||
// toHana.csn : if true, generate the transformed CSN model | ||
// toHana.names | ||
// toHana.associations | ||
// toHana.src | ||
// toHana.csn | ||
// } | ||
@@ -88,2 +184,5 @@ // Options provided here are merged with (and take precedence over) options from 'model'. | ||
// Verify options | ||
optionProcessor.verifyOptions(options, 'toHana').map(complaint => signal(warning`${complaint}`)); | ||
// Special case: For naming variant 'hdbcds' in combination with 'toHana' (and only there!), 'forHana' | ||
@@ -95,4 +194,4 @@ // must leave namespaces, structs and associations alone. | ||
// Prepare model for HANA (transferring the options to forHana) | ||
let forHanaAugmented = transformForHana(model, mergeOptions(options, { forHana : options.toHana } )); | ||
// Prepare model for HANA (transferring the options to forHana, and setting 'dialect' to 'hana', because 'toHana' is only used for that) | ||
let forHanaAugmented = transformForHana(model, mergeOptions(options, { forHana: { dialect: 'hana' } }, { forHana : options.toHana } )); | ||
@@ -116,2 +215,37 @@ // Assemble result | ||
// ----------- toOdata ----------- | ||
optionProcessor.command('O, toOdata') | ||
.option('-h, --help') | ||
.option('-v, --version <version>', ['v2', 'v4']) | ||
.option('-x, --xml') | ||
.option('-j, --json') | ||
.option(' --separate') | ||
.option(' --combined') | ||
.option('-c, --csn') | ||
.option('-n, --names <style>', ['plain', 'quoted', 'hdbcds']) | ||
.help(` | ||
Usage: cdsc toOdata [options] <file...> | ||
Generate ODATA metadata and annotations, or CSN. | ||
Options | ||
-h, --help Show this help text | ||
-v, --version <version> ODATA version | ||
v2: (default) ODATA V2 | ||
v4: ODATA V4 | ||
-x, --xml (default) Generate XML output (separate or combined) | ||
-j, --json Generate JSON output as "<svc>.json" (not available for v2) | ||
--separate Generate "<svc>_metadata.xml" and "<svc>_annotations.xml" | ||
--combined (default) Generate "<svc>.xml" | ||
-c, --csn Generate "odata_csn.json" with ODATA-preprocessed model | ||
-n, --names <style> Annotate artifacts and elements with "@cds.persistence.name", which is | ||
the corresponding database name (see "--names" for "toHana or "toSql") | ||
plain : (default) Names in uppercase and flattened with underscores | ||
quoted : Names in original case as in CDL. Entity names with dots, | ||
but element names flattened with underscores | ||
hdbcds : Names as HANA CDS would generate them from the same CDS | ||
source (like "quoted", but using element names with dots) | ||
`); | ||
// Generate ODATA for augmented CSN `model` using `options`. | ||
@@ -131,14 +265,11 @@ // Before anything is generated, the following transformations are applied to 'model': | ||
// - Rename annotations according to a fixed list of short-hands | ||
// The following options control what is actually generated: | ||
// The following options control what is actually generated (see help above): | ||
// options : { | ||
// toOdata.version : either 'v2' or 'v4' (default) | ||
// toOdata.xml : if true, generate XML output (default) | ||
// toOdata.json : if true, generate JSON output (not available for ODATA V2) | ||
// toOdata.separate : if true, generate XML 'metadata' and XML 'annotations' separately | ||
// toOdata.combined : if true, generate XML metadata and XML annotations together as | ||
// 'combined' (default) | ||
// toOdata.csn : if true, generate the transformed CSN model | ||
// toOdata.names : either 'plain', 'quoted' or 'hdbcds'. If set, artifacts and elements | ||
// are annotated with "@cds.persistence.name" containing the | ||
// corresponding database name (see "toHana.names" or "toSql.names") | ||
// toOdata.version | ||
// toOdata.xml | ||
// toOdata.json | ||
// toOdata.separate | ||
// toOdata.combined | ||
// toOdata.csn | ||
// toOdata.names | ||
// } | ||
@@ -197,2 +328,5 @@ // Options provided here are merged with (and take precedence over) options from 'model'. | ||
// Verify options | ||
optionProcessor.verifyOptions(options, 'toOdata').map(complaint => signal(warning`${complaint}`)); | ||
// Perform extra-magic for TNT if requested | ||
@@ -220,7 +354,9 @@ if (model.options.tntFlavor) { | ||
if (options.toOdata.xml || options.toOdata.json) { | ||
// Compact the model | ||
let compactedModel = compactModel(forOdataAugmented); | ||
setProp(compactedModel, 'messages', forOdataAugmented.messages); | ||
for (let serviceName of getServiceNames(model)) | ||
{ | ||
let forOdata = compactForService(forOdataAugmented, serviceName); | ||
// FIXME: Unify handling of version and tntFlavor (use original options) | ||
let l_edm = csn2edm(forOdata, { version: options.toOdata.version, tntFlavor : options.tntFlavor }); | ||
let l_edm = csn2edm(compactedModel, serviceName, options); | ||
@@ -255,2 +391,33 @@ result.services[serviceName] = {}; | ||
// Generate edmx for given 'service' based on 'csn' (new-style compact, already prepared for OData) | ||
// using 'options' | ||
function preparedCsnToEdmx(csn, service, options) { | ||
// Merge options with those from model | ||
options = mergeOptions(csn.options, options); | ||
let edmx = csn2edm(csn, service, options).toXML('all'); | ||
return edmx; | ||
} | ||
// Generate edm-json for given 'service' based on 'csn' (new-style compact, already prepared for OData) | ||
// using 'options' | ||
function preparedCsnToEdm(csn, service, options) { | ||
// Merge options with those from model, override OData version as edm json is always v4 | ||
options = mergeOptions(csn.options, options, { toOdata : { version : 'v4' }}); | ||
let edmj = csn2edm(csn, service, options).toJSON(); | ||
return edmj; | ||
} | ||
// ----------- toCdl ----------- | ||
optionProcessor.command('C, toCdl') | ||
.option('-h, --help') | ||
.help(` | ||
Usage: cdsc toCdl [options] <file...> | ||
Generate CDS source files "<artifact>.cds". | ||
Options | ||
-h, --help Show this help text | ||
`); | ||
// Generate CDS source text for augmented CSN model 'model'. | ||
@@ -272,7 +439,27 @@ // The following options control what is actually generated: | ||
function toCdl(model, options) { | ||
const { warning, signal } = alerts(model); | ||
// Merge options with those from model | ||
options = mergeOptions({ toCdl : true }, model.options, options); | ||
// Verify options | ||
optionProcessor.verifyOptions(options, 'toCdl').map(complaint => signal(warning`${complaint}`)); | ||
return toCdsSource(model, options); | ||
} | ||
// ----------- toSwagger ----------- | ||
optionProcessor.command('S, toSwagger') | ||
.option('-h, --help') | ||
.option('-j, --json') | ||
.option('-c, --csn') | ||
.help(` | ||
Usage: cdsc toSwagger [options] <file...> | ||
Generate Swagger (OpenAPI) JSON, or CSN | ||
Options | ||
-h, --help Show this help text | ||
-j, --json (default) Generate OpenAPI JSON output for each service as "<svc>_swagger.json | ||
-c, --csn Generate "swagger_csn.json" with Swagger-preprocessed model | ||
`); | ||
// Generate OpenAPI JSON version 3 for the augmented CSN 'model'. | ||
@@ -307,2 +494,3 @@ // The following options control what is actually generated: | ||
function toSwagger(model, options) { | ||
const { warning, signal } = alerts(model); | ||
// Optional wrapper? | ||
@@ -318,2 +506,4 @@ if (options && !options.toSwagger) { | ||
} | ||
// Verify options | ||
optionProcessor.verifyOptions(options, 'toSwagger').map(complaint => signal(warning`${complaint}`)); | ||
// Actual implementation | ||
@@ -323,21 +513,57 @@ return csnToSwagger(model, options); | ||
// ----------- toSql ----------- | ||
optionProcessor.command('Q, toSql') | ||
.option('-h, --help') | ||
.option('-n, --names <style>', ['plain', 'quoted', 'hdbcds']) | ||
.option('-a, --associations <proc>', ['assocs', 'joins']) | ||
.option('-d, --dialect <dialect>', ['hana', 'sqlite']) | ||
.option('-u, --user <user>') | ||
.option('-l, --locale <locale>') | ||
.option('-s, --src') | ||
.option('-c, --csn') | ||
.help(` | ||
Usage: cdsc toSql [options] <file...> | ||
Generate SQL DDL statements to create tables and views, or CSN | ||
Options | ||
-h, --help Show this help text | ||
-n, --names <style> Naming style for generated entity and element names: | ||
plain : (default) Produce SQL table and view names in uppercase | ||
and flattened with underscores (no quotes required) | ||
quoted : Produce SQL table and view names in original case as in | ||
CDL (with dots), but flatten element names with | ||
underscores (requires quotes). Can only be used in | ||
combination with "hana" dialect. | ||
hdbcds : Produce SQL table, view and column names as HANA CDS would | ||
generate them from the same CDS source (like "quoted", but | ||
using element names with dots). Can only be used in | ||
combination with "hana" dialect. | ||
-a, --associations <proc> Processing of associations: | ||
assocs : Keep associations as far as possible. Note that some | ||
associations (e.g. those defined in a mixin and used in | ||
the same view) must always be replaced by joins because of | ||
SQL limitations, and that "assocs" should only be used | ||
with "hana" dialect. | ||
joins : (default) Transform associations to joins | ||
-d, --dialect <dialect> SQL dialect to be generated: | ||
hana : SQL with HANA specific language features | ||
sqlite : (default) Common SQL for sqlite | ||
-u, --user <user> Value for the "$user" variable in "sqlite" dialect | ||
-l, --locale <locale> Value for the "$user.locale" variable in "sqlite" dialect | ||
-s, --src (default) Generate SQL source files as "<artifact>.sql" | ||
-c, --csn Generate "sql_csn.json" with SQL-preprocessed model | ||
`); | ||
// Generate SQL DDL statements for augmented CSN 'model'. | ||
// The following options control what is actually generated: | ||
// The following options control what is actually generated (see help above): | ||
// options : { | ||
// toSql.names : either 'plain' (default), 'quoted' or 'hdbcds' | ||
// 'plain': Produce SQL table and view names in uppercase and | ||
// flattened with underscores (no quotes required) | ||
// 'quoted': Produce SQL table and view names in original case | ||
// as in CDL (with dots), but flatten element names with | ||
// underscores (requires quotes). | ||
// 'hdbcds: Produce SQL table, view and column names as HANA CDS | ||
// would generate them from the same CDS source (like "quoted", | ||
// but using element names with dots). | ||
// toSql.associations : either 'assocs' (default, keep associations as they are if possible) | ||
// or 'joins' (replace associations by joins). Note that some associations | ||
// (e.g. those defined in a mixin and used in the same view) must always be | ||
// replaced by joins because of SQL limitations. | ||
// toSql.dialect : either 'hana' or 'sqlite' (default) | ||
// toSql.src : if true, generate SQL DDL source files (default) | ||
// toSql.csn : if true, generate the transformed CSN model | ||
// toSql.names | ||
// toSql.associations | ||
// toSql.dialect | ||
// toSql.user.id | ||
// toSql.user.locale | ||
// toSql.src | ||
// toSql.csn | ||
// } | ||
@@ -360,2 +586,10 @@ // Options provided here are merged with (and take precedence over) options from 'model'. | ||
function toSql(model, options) { | ||
const { warning, error, signal } = alerts(model); | ||
// when toSql is invoked via the CLI - toSql options are under model.options | ||
// ensure the desired format of the user option | ||
if (model.options && model.options.toSql &&(model.options.toSql.user || model.options.toSql.locale)) { | ||
transforUserOption(model.options.toSql); | ||
} | ||
// Optional wrapper? | ||
@@ -365,2 +599,9 @@ if (options && !options.toSql) { | ||
} | ||
// when the API funtion is used directly - toSql options are in options | ||
// ensure the desired format of the user option | ||
if (options && (options.toSql.user || options.toSql.locale)){ | ||
transforUserOption(options.toSql); | ||
} | ||
// Provide defaults and merge options with those from model | ||
@@ -376,3 +617,2 @@ options = mergeOptions({ toSql : getDefaultBackendOptions().toSql }, model.options, options); | ||
// FIXME: Remove after a few releases | ||
const { warning, signal } = alerts(model); | ||
if (options.toSql.names == 'flat') { | ||
@@ -387,2 +627,5 @@ signal(warning`Option "{ toSql.names: 'flat' }" is deprecated, use "{ toSql.names: 'plain' }" instead`); | ||
// Verify options | ||
optionProcessor.verifyOptions(options, 'toSql').map(complaint => signal(warning`${complaint}`)); | ||
// FIXME: Currently, '--to-sql' implies transformation for HANA (transferring the options to forHana) | ||
@@ -397,4 +640,14 @@ let forHanaOptions = options.toSql; | ||
// It doesn't make much sense to use 'sqlite' dialect with associations | ||
if (options.toSql.dialect == 'sqlite' && options.toSql.associations != 'joins') { | ||
signal(warning`Option "{ toSql.dialect: 'sqlite' }" should always be combined with "{ toSql.assocs: 'joins' }"`); | ||
} | ||
// CDXCORE-465, 'quoted' and 'hdbcds' are to be used in combination with dialect 'hana' only | ||
if(options.toSql.dialect != 'hana' && ['quoted', 'hdbcds'].includes(options.toSql.names)) { | ||
signal(error`Option "{ toSql.dialect: '${options.toSql.dialect}' }" cannot be combined with "{ toSql.names: '${options.toSql.names}' }"`); | ||
} | ||
// Because (even HANA) SQL cannot deal with associations in mixins that are published in the same view, | ||
// the association processing must at least be 'mixin', even if callers specified 'assocs' or nothing | ||
// the association processing must at least be 'mixin', even if callers specified 'assocs' | ||
if (forHanaOptions.associations == 'assocs') { | ||
@@ -412,3 +665,3 @@ forHanaOptions.associations = 'mixin'; | ||
if (options.toSql.src) { | ||
result = options.oldSqlImpl ? toSqlDdl(forSqlAugmented) : toSqlDdlNew(forSqlAugmented); | ||
result = toSqlDdl(forSqlAugmented); | ||
} | ||
@@ -425,4 +678,46 @@ if (options.toSql.csn) { | ||
return result; | ||
// If among the options user, user.id or user.locale are specified via the CLI or | ||
// via the API, then ensure that at the end there is a user option, which is an object and has(have) | ||
// "id" and/or "locale" prop(s) | ||
function transforUserOption(options) { | ||
// move the user option value under user.id if specified as a string | ||
if (options.user && typeof options.user === 'string' || options.user instanceof String) { | ||
options.user = { id: options.user }; | ||
} | ||
// move the locale option(if provided) under user.locale | ||
if (options.locale) { | ||
options.user | ||
? Object.assign(options.user, { locale: options.locale }) | ||
: options.user = { locale: options.locale }; | ||
delete options.locale; | ||
} | ||
} | ||
} | ||
// ----------- toRename ----------- | ||
optionProcessor.command('toRename') | ||
.option('-h, --help') | ||
.option('-n, --names <style>', ['quoted', 'hdbcds']) | ||
.help(` | ||
Usage: cdsc toRename [options] <file...> | ||
(internal, subject to change): Generate SQL DDL statements to "rename_<artifact>.sql" that | ||
rename existing tables and their columns so that they match the result of "toHana" or "toSql" | ||
with the "--names plain" option. | ||
Options | ||
-h, --help Display this help text | ||
-n, --names <style> Assume existing tables were generated with "--names <style>": | ||
quoted : Assume existing SQL tables and views were named in original | ||
case as in CDL (with dots), but column names were flattened | ||
with underscores (e.g. resulting from "toHana --names quoted") | ||
hdbcds : (default) Assume existing SQL tables, views and columns were | ||
generated by HANA CDS from the same CDS source (or resulting | ||
from "toHana --names hdbcds") | ||
`); | ||
// FIXME: Not yet supported, only in beta mode | ||
@@ -433,5 +728,5 @@ // Generate SQL DDL rename statements for a migration, renaming existing tables and their | ||
// Expects the naming convention of the existing tables to be either 'quoted' or 'hdbcds' (default). | ||
// The following options control what is actually generated: | ||
// The following options control what is actually generated (see help above): | ||
// options : { | ||
// toRename.names : existing names, either 'quoted' or 'hdbcds' (default) | ||
// toRename.names | ||
// } | ||
@@ -474,2 +769,5 @@ // Return a dictionary of top-level artifacts by their names, like this: | ||
// Verify options | ||
optionProcessor.verifyOptions(options, 'toRename').map(complaint => signal(warning`${complaint}`)); | ||
// Requires beta mode | ||
@@ -501,2 +799,22 @@ if (!options.betaMode) { | ||
// ----------- toCsn ----------- | ||
optionProcessor.command('toCsn') | ||
.option('-h, --help') | ||
.option('-f, --flavor <flavor>', ['client', 'gensrc']) | ||
.help(` | ||
Usage: cdsc toCsn [options] <file...> | ||
Generate original model as CSN to "csn.json" | ||
Options | ||
-h, --help Show this help text | ||
-f, --flavor <flavor> Generate CSN in one of two flavors: | ||
client : (default) Standard CSN consumable by clients and backends | ||
gensrc : CSN specifically for use as a source, e.g. for | ||
combination with additional "extend" or "annotate" | ||
statements, but not suitable for consumption by clients or | ||
backends | ||
`); | ||
// Generate compact CSN for augmented CSN 'model' | ||
@@ -516,3 +834,3 @@ // The following options control what is actually generated: | ||
function toCsn(model, options) { | ||
const { error, signal } = alerts(model); | ||
const { error, warning, signal } = alerts(model); | ||
// Can't have an optional wrapper here because 'testMode' and 'newCsn' are global options | ||
@@ -523,2 +841,5 @@ | ||
// Verify options | ||
optionProcessor.verifyOptions(options, 'toCsn').map(complaint => signal(warning`${complaint}`)); | ||
if (options.toCsn.gensrc && !options.newCsn) { | ||
@@ -550,3 +871,3 @@ signal(error`CSN in "gensrc" flavor can only be generated as new-style CSN (option "newCsn" required)`); | ||
names : 'plain', | ||
associations: 'assocs', | ||
associations: 'joins', | ||
dialect: 'sqlite', | ||
@@ -558,4 +879,7 @@ }, | ||
module.exports = { | ||
optionProcessor, | ||
toHana, | ||
toOdata, | ||
preparedCsnToEdmx, | ||
preparedCsnToEdm, | ||
toCdl, | ||
@@ -562,0 +886,0 @@ toSwagger, |
@@ -23,3 +23,3 @@ // Functions for dictionaries (Objects without prototype) | ||
// Redefinitions from second source -> also complain in first source | ||
if (messageCallback) | ||
if (messageCallback && name) | ||
messageCallback( name, found.name.location, found ); | ||
@@ -38,3 +38,3 @@ if (messageCallback !== null) | ||
// TODO: with packages, we could also use the package hierarchy | ||
if (messageCallback) | ||
if (messageCallback && name) | ||
messageCallback( name, found.name.location, found ); | ||
@@ -44,3 +44,3 @@ if (messageCallback !== null) | ||
} | ||
if (messageCallback) | ||
if (messageCallback && name) | ||
messageCallback( name, entry.name.location, entry ); | ||
@@ -47,0 +47,0 @@ if (messageCallback !== null) |
@@ -49,3 +49,3 @@ // Functions and classes for syntax messages | ||
'expected-const': 'A constant value is expected here', | ||
'expected-struct': 'A structured type or a non-query entity is expected here', | ||
'expected-struct': 'A structured type or a non-query entity without parameters is expected here', | ||
'expected-context': 'A context or service is expected here', | ||
@@ -86,3 +86,3 @@ 'expected-type': 'A type or an element of a type is expected here', | ||
constructor(errs, model, text, ...args) { | ||
super( text || 'CDS compilation failed\n' + errs.map( m => m.toString() ), | ||
super( text || 'CDS compilation failed\n' + errs.map( m => m.toString() ).join('\n'), | ||
...args ); | ||
@@ -95,3 +95,3 @@ this.errors = errs; // TODO: rename to messages | ||
? this.message | ||
: this.message + '\n' + this.errors.map( m => m.toString() ); | ||
: this.message + '\n' + this.errors.map( m => m.toString() ).join('\n'); | ||
} | ||
@@ -103,5 +103,7 @@ } | ||
class CompileMessage extends Error { | ||
constructor(location, msg, severity = 'Error', id) { | ||
constructor(location, msg, severity = 'Error', id, home) { | ||
super(msg); | ||
this.location = location; | ||
if (home) // semantic location, e.g. 'entity:"E"/element:"x"' | ||
this.home = home; | ||
this.severity = severity; | ||
@@ -144,3 +146,3 @@ if (id) | ||
return function message( id, location, params = {}, severity = undefined, texts = undefined ) { | ||
return function message( id, location, home, params = {}, severity = undefined, texts = undefined ) { | ||
if (!severity) // TODO: check that they are always eq per messageId | ||
@@ -154,3 +156,4 @@ severity = standardSeverities[id]; | ||
: messageText( texts || standardTexts[id], params ); | ||
let msg = new CompileMessage( location, text, s, id ); | ||
let msg = new CompileMessage( location, text, s, id, | ||
(typeof home === 'string' ? home : homeName(home)) ); | ||
model.messages.push( msg ); | ||
@@ -162,9 +165,12 @@ return msg; | ||
const paramsTransform = { | ||
alias: msgName, | ||
alias: quoted, | ||
anno: transformAnno, | ||
art: transformArg, | ||
target: transformArg, | ||
token: t => t.match( /^[a-zA-Z]+$/ ) ? t.toUpperCase() : "'" + t + "'", | ||
code: n => '`' + n + '`', | ||
name: msgName, | ||
names: transformManyWith( msgName ), | ||
id: msgName, | ||
newcode: n => '`' + n + '`', | ||
name: quoted, | ||
names: transformManyWith( quoted ), | ||
id: quoted, | ||
file: s => "'" + s.replace( /'/g, "''" ) + "'", // sync ; | ||
@@ -174,3 +180,3 @@ }; | ||
function transformAnno( anno ) { | ||
return (anno.charAt() === '@') ? msgName( anno ) : msgName( '@' + anno ); | ||
return (anno.charAt() === '@') ? quoted( anno ) : quoted( '@' + anno ); | ||
// if (anno.charAt() === '@') | ||
@@ -182,14 +188,17 @@ // anno = anno.slice(1); | ||
function transformArg( arg, r, args, texts ) { | ||
if (!arg || typeof arg !== 'object' || args['#'] || args.member ) | ||
return msgName( arg ); | ||
if (!arg || typeof arg !== 'object') | ||
return quoted( arg ); | ||
if (args['#'] || args.member ) | ||
return artName( arg ); | ||
if (arg._artifact) | ||
arg = arg._artifact; | ||
if (arg.name) | ||
arg = arg.name; | ||
let prop = ['element','param','action','alias'].find( p => arg[p] ); | ||
let name = arg.name; | ||
if (!name) | ||
return quoted( name ); | ||
let prop = ['element','param','action','alias'].find( p => name[p] ); | ||
if (!prop || !texts[prop] ) | ||
return msgName( arg ); | ||
r['#'] = texts[ arg.$variant ] && arg.$variant || prop; // text variant (set by searchName) | ||
r.member = msgName( arg[prop] ); | ||
return msgName( arg, prop ); | ||
return artName( arg ); | ||
r['#'] = texts[ name.$variant ] && name.$variant || prop; // text variant (set by searchName) | ||
r.member = quoted( name[prop] ); | ||
return artName( arg, prop ); | ||
} | ||
@@ -215,3 +224,2 @@ | ||
if (!variant) { | ||
// TODO: probably use null instead undefinedArtifact in resolver.js | ||
let type = art._finalType && art._finalType.kind !== 'undefined' ? art._finalType : art; | ||
@@ -222,6 +230,5 @@ art = type.target && type.target._artifact || type; | ||
let prop = nameProp[variant] || variant; | ||
let name = Object.assign( {}, (art._artifact||art).name ); | ||
name[prop] = (name[prop]) ? name[prop] + '.' + id : id; | ||
name.$variant = variant; | ||
return name; | ||
let name = Object.assign( { $variant: variant }, (art._artifact||art).name ); | ||
name[prop] = (name[prop]) ? name[prop] + '.' + id : id || '?'; | ||
return { name, kind: art.kind }; | ||
} | ||
@@ -263,7 +270,8 @@ | ||
// Return message string with location if present | ||
function messageString( err, normalizeFilename, noMessageId ) { | ||
function messageString( err, normalizeFilename, noMessageId, noHome ) { | ||
return (err.location ? locationString( err.location, normalizeFilename ) + ': ' : '') + | ||
(err.severity||'Error') + | ||
(err.messageId && !noMessageId ? ' ' + err.messageId + ': ' : ': ') + | ||
err.message; | ||
err.message + | ||
(err.home && !noHome ? ' (in ' + err.home + ')' : ''); | ||
} | ||
@@ -294,31 +302,46 @@ | ||
// Return string for complete reference | ||
function refString( name, omit ) { | ||
// prepare that resolvePath does not set ref.absolute etc: | ||
if (name._artifact) | ||
name = name._artifact; | ||
if (name.name) | ||
name = name.name; | ||
let compact = ''; | ||
if (name.alias) | ||
compact = '.$alias.' + name.alias; | ||
function artName( art, omit ) { | ||
let name = art.name; | ||
let r = (name.absolute) ? [ quoted( name.absolute ) ] : []; | ||
if (name.query || art.kind === 'block') // Yes, omit $query.0 - TODO: rename to block | ||
r.push( (art.kind === 'block' ? 'block:' : 'query:') + name.query ); | ||
if (name.action && omit !== 'action') | ||
compact = '.$action.' + name.action; | ||
if (name.param && omit !== 'param') | ||
compact += '.$param.' + name.param; | ||
r.push( memberActionName(art) + ':' + quoted( name.action ) ); | ||
if (name.param && omit !== 'param') // TODO: also use for alias/mixin | ||
r.push( 'param:' + quoted( name.param ) ); | ||
else if (name.alias) // TODO: use 'param' | ||
r.push( (name.$mixin ? 'mixin:' : 'alias:') + quoted( name.alias ) ) | ||
if (name.element && omit !== 'element') | ||
compact += (compact ? '.' : '..') + name.element; | ||
// Yes, omit $query.0 -> test is (name.query), not (name.query != null) | ||
return name.absolute + | ||
(name.query ? '.$query.' + name.query : '') + compact; | ||
r.push( (art.kind === 'enum' ? 'enum:' : 'element:') + quoted( name.element ) ); | ||
return r.join('/'); | ||
} | ||
// TODO: create error tag function which automatically calls msgName() on args, | ||
// or better: named message parameters and having a name-dependent toString() | ||
// function | ||
function msgName( ref, omit ) { | ||
let name = (typeof ref === 'string') ? ref : refString( ref, omit ); | ||
return '"' + name.replace( /"/g, '""' ) + '"'; // sync "; | ||
function memberActionName( art ) { | ||
while (art && art._main) { | ||
if (art.kind === 'action' || art.kind === 'function') | ||
return art.kind; | ||
art = art._parent; | ||
} | ||
return 'action'; | ||
} | ||
function homeName( art ) { | ||
if (!art) | ||
return art; | ||
if (art._outer) // in returns / items property | ||
return homeName( art._outer ); | ||
else if (art.kind === 'source' || !art.name) // error reported in parser or on source level | ||
return null; | ||
else if (art.kind === 'using') | ||
return 'using:' + quoted( art.name.id ); | ||
else if (art.name._artifact) // block, extend, annotate | ||
return homeName( art.name._artifact ); // use corresponding definition | ||
else | ||
return (art._main ? art._main.kind : art.kind) + ':' + artName( art ); | ||
} | ||
function quoted( name ) { | ||
return (name) ? '"' + name.replace( /"/g, '""' ) + '"' : '<?>'; // sync "; | ||
} | ||
module.exports = { | ||
@@ -328,6 +351,5 @@ hasErrors, | ||
messageString, | ||
refString, | ||
searchName, | ||
getMessageFunction, | ||
msgName, | ||
artName, | ||
handleMessages, | ||
@@ -334,0 +356,0 @@ sortMessages: (m => m.sort(compareMessage)), |
@@ -41,7 +41,7 @@ // | ||
// Descend into nested members, too | ||
forEachMember( member, callback ); | ||
forEachMemberRecursively( member, callback ); | ||
}); | ||
// If 'construct' has more than one query, descend into the elements of the remaining ones, too | ||
if (construct.queries && construct.queries.length > 1) { | ||
construct.queries.slice(1).forEach(query => forEachMemberRecursively(query, callback)); | ||
if (construct.$queries && construct.$queries.length > 1) { | ||
construct.$queries.slice(1).forEach(query => forEachMemberRecursively(query, callback)); | ||
} | ||
@@ -48,0 +48,0 @@ } |
@@ -33,3 +33,3 @@ 'use strict'; | ||
const { error, signal } = alerts(model); | ||
for (let query of art.queries || []) { | ||
for (let query of art.$queries || []) { | ||
for (let groupByEntry of query.groupBy || []) { | ||
@@ -36,0 +36,0 @@ if (groupByEntry._artifact && groupByEntry._artifact._finalType && groupByEntry._artifact._finalType.onCond) { |
@@ -14,4 +14,4 @@ 'use strict'; | ||
let target = elem.target; | ||
// Not a managed assoc at all or not inferred => nothing to check | ||
if (!target || elem.on || elem.onCond || elem.$inferred) { | ||
// Not a managed assoc at all, inferred elem or redirected => nothing to check | ||
if (!target || elem.on || elem.onCond || elem.$inferred || !elem.type || elem.type.$inferred) { | ||
return; | ||
@@ -18,0 +18,0 @@ } |
@@ -40,5 +40,2 @@ 'use strict'; | ||
} | ||
// anonymous types not supported yet by OData processor | ||
if (act.returns._finalType.elements && !act.returns._finalType.kind) | ||
signal(warning`Anonymous types not supported`, location); | ||
// check array return type | ||
@@ -57,6 +54,2 @@ if (act.returns.items) | ||
return; | ||
// anonymous types not supported yet by OData processor | ||
if (act.returns._finalType.items.elements && !act.returns._finalType.kind) | ||
signal(warning`Anonymous types not supported`, location); | ||
// array of array is not allowed | ||
@@ -98,7 +91,2 @@ if (act.returns._finalType.items._finalType.items) | ||
} | ||
// check if user defined structured type is from the current service | ||
let actReturnTypeServiceName = getAbsNameWithoutId(act.returns._finalType.name.absolute); | ||
if (actReturnTypeServiceName !== serviceName) | ||
signal(warning`The user defined return type of action '${act.name.id}' must be from the current service '${serviceName}'`, act.location); | ||
} | ||
@@ -110,10 +98,6 @@ } | ||
let paramTypeArtifact = param.type._artifact; | ||
let paramTypeArtifact = param.type && param.type._artifact || {}; | ||
// check if the entity type is from the current service | ||
if (paramTypeArtifact.kind === 'entity') | ||
checkEntityParam(paramTypeArtifact); | ||
// check when the parameter is of user defined type | ||
if (paramTypeArtifact.kind === 'type') { | ||
checkUserDefinedTypeParam(param, paramTypeArtifact); | ||
} | ||
@@ -124,15 +108,2 @@ function checkEntityParam(paramTypeArtifact) { | ||
} | ||
function checkUserDefinedTypeParam(param, paramTypeArtifact) { | ||
// if the type is resolved to a builtin | ||
if (paramTypeArtifact._finalType.builtin) | ||
return; | ||
// user defined type resolved to builtin | ||
if (paramTypeArtifact._finalType.type && paramTypeArtifact._finalType.type._artifact.builtin) | ||
return; | ||
if (getAbsNameWithoutId(paramTypeArtifact.name.absolute) !== serviceName) | ||
signal(warning`The type of input parameter '${param.name.id}' must be from the current service`, location); | ||
} | ||
} | ||
@@ -139,0 +110,0 @@ } |
@@ -134,3 +134,3 @@ 'use strict'; | ||
function checkEntity(entity) { | ||
if (entity.source) { // projection | ||
if (source(entity)) { // projection | ||
checkProjection(entity, model); | ||
@@ -145,6 +145,6 @@ } | ||
function checkProjection(entity) { | ||
// TODO: check FROM clauses of queries instead | ||
let sourceEntity = entity.source._artifact; | ||
// TODO: check too simple (just one source), as most of those in this file | ||
let sourceEntity = source(entity)._artifact; | ||
if(sourceEntity && isAbstractEntity(sourceEntity)) { | ||
signal(error`Projection ${entity.name.absolute} on abstract entity ${sourceEntity.name.absolute}`, entity.source.location); | ||
signal(error`Projection ${entity.name.absolute} on abstract entity ${sourceEntity.name.absolute}`, source(entity).location); | ||
} | ||
@@ -154,7 +154,7 @@ } | ||
function checkView(view) { | ||
// TODO: check FROM clauses of queries instead | ||
if (view.source) { | ||
let sourceEntity = view.source._artifact; | ||
// TODO: check too simple (just one source), as most of those in this file | ||
if (source(view)) { | ||
let sourceEntity = source(view)._artifact; | ||
if(sourceEntity && isAbstractEntity(sourceEntity)) { | ||
signal(error`View ${view.name.absolute} on abstract entity ${sourceEntity.name.absolute}`, view.source.location); | ||
signal(error`View ${view.name.absolute} on abstract entity ${sourceEntity.name.absolute}`, source(view).location); | ||
} | ||
@@ -164,3 +164,3 @@ } | ||
// Check expressions in the various places where they may occur | ||
for (let query of view.queries || []) { | ||
for (let query of view.$queries || []) { | ||
if (query.from) { | ||
@@ -247,2 +247,8 @@ checkExpressionsInPaths(query.from); | ||
// TODO: checks on one "source" are incomplete! | ||
function source( view ) { | ||
let from = view.query && view.query.from; | ||
return from && from.length === 1 && from[0] && from[0].path && from[0]; | ||
} | ||
module.exports = semanticCheck; |
@@ -24,3 +24,3 @@ // Consistency checker on model (XSN = augmented CSN) | ||
// - Standard object: object with `Object.prototype` as prototype - its | ||
// property names are predefined (or at least their first: `@` in case of | ||
// property names are predefined (or at least their first char: `@` for | ||
// annotation assignments) and the value type depends on the property name. | ||
@@ -47,3 +47,3 @@ // - Special object: currently just for the messages. | ||
// - Optional sub properties are listed in `optional`, which can also be a | ||
// - function returning true if property is allowed. | ||
// function returning true if the property is allowed. | ||
// | ||
@@ -87,3 +87,3 @@ // The above mentioned restriction of the value space in certain contexts can | ||
requires: ['messages','options','definitions','sources'], | ||
optional: ['extensions','version','$magicVariables','$builtins','$internal'] // version without --test-mode | ||
optional: ['extensions','version','$version','meta','$magicVariables','$builtins','$internal','_entities'] // version without --test-mode | ||
}, | ||
@@ -99,2 +99,4 @@ ':parser': { // top-level from parser | ||
'version', // TODO: do not set in parser | ||
'$version', | ||
'meta', | ||
'@sql_mapping', // TODO: it is time that a 'header' attribute replaces 'version' | ||
@@ -142,2 +144,4 @@ ] | ||
version: { test: TODO }, // TODO: describe - better: 'header' | ||
$version: { test: TODO, parser: true}, | ||
meta: { test: TODO }, | ||
namespace: { | ||
@@ -180,3 +184,6 @@ test: (model.$frontend !== 'json') ? standard : TODO, | ||
requires: ['op','location','args'], | ||
optional: ['name','quantifier','orderBy','limit','offset','_leadingQuery'] | ||
optional: [ | ||
'name','quantifier','orderBy','limit','offset','_leadingQuery', | ||
'name','kind','_parent','_main','_finalType','$navigation' // in FROM | ||
] | ||
}, | ||
@@ -265,4 +272,10 @@ select: { // sub query | ||
'source','namespace','using', | ||
'$tableAlias' | ||
] | ||
}, | ||
$syntax: { | ||
parser: true, | ||
kind: ['entity','view'], | ||
test: isString // CSN parser should check for 'entity', 'view', 'projection' | ||
}, | ||
value: { | ||
@@ -282,3 +295,4 @@ kind: true, | ||
requires: ['op','location'], | ||
optional: ['args','func','quantifier','$inferred','augmented'] | ||
optional: ['args','func','quantifier','$inferred','augmented','_artifact'] | ||
// _artifact with "localized data"s 'coalesce' | ||
}, | ||
@@ -354,3 +368,3 @@ query: { inherits: 'query' } | ||
dbType: { kind: true, test: locationVal() }, | ||
source: { kind: true, test: TODO }, // TODO: remove in JSON/CDL parser | ||
source: { kind: true, test: TODO }, // TODO: remove in JSON/CDL parser - only in old | ||
projection: { kind: true, test: TODO }, // TODO: remove in JSON/CDL parser | ||
@@ -382,2 +396,3 @@ technicalConfig: { kind: ['entity'], test: TODO }, // TODO: some spec | ||
_parent: { kind: true, test: TODO }, | ||
_service: { kind: true, test: TODO }, | ||
_main: { kind: true, test: TODO }, | ||
@@ -391,7 +406,9 @@ _artifact: { test: TODO }, | ||
queries: { kind: true, test: TODO }, // TODO: $queries with other structure | ||
$queries: { kind: ['entity','view'], test: TODO }, | ||
_leadingQuery: { kind: true, test: TODO }, | ||
$navigation: { kind: true, test: TODO }, | ||
$replacement: { kind: true, test: TODO }, // for smart * in queries | ||
origin: { kind: true, test: TODO }, // TODO: define some _origin | ||
$from: { kind: true, test: TODO }, // all table refs necesary to compute elements | ||
_origTarget: { kind: true, test: TODO }, // for REDIRECTED TO | ||
_redirected: { kind: true, test: TODO }, // for REDIRECTED TO | ||
_$next: { kind: true, test: TODO }, // next lexical search environment for values | ||
@@ -406,2 +423,5 @@ _extend: { kind: true, test: TODO }, // for collecting extend/annotate on artifact | ||
_projections: { kind: true, test: TODO }, // for mixin definitions | ||
_entities: { test: TODO }, | ||
_ancestors: { kind: ['type','entity','view'], test: isArray( TODO ) }, | ||
_descendants: { kind: ['entity','view'], test: isDictionary( isArray( TODO ) ) }, | ||
$duplicate: { parser: true, kind: true, test: isBoolean }, | ||
@@ -414,2 +434,3 @@ $extension: { kind: true, test: TODO }, // TODO: introduce $applied instead or $status | ||
redirected: { kind: true, test: TODO }, // TODO: do it with not-$inferred | ||
$extra: { parser: true, test: TODO }, // for unexpectex properties in CSN | ||
} | ||
@@ -454,3 +475,4 @@ var _noSyntaxErrors = null; | ||
} | ||
(spec.test||standard)( node, parent, prop, spec ); | ||
(spec.test||standard)( node, parent, prop, spec, | ||
typeof noPropertyTest === 'string' && noPropertyTest ); | ||
} | ||
@@ -501,3 +523,3 @@ | ||
: optional( n, spec ); | ||
if (!(opt || requires.includes( n ))) { | ||
if (!(opt || requires.includes( n ) || n === '$extra')) { | ||
throw new Error( `Property '${n}' is not expected${at( [node[n], node, parent], prop, name )}` ); | ||
@@ -527,3 +549,3 @@ } | ||
if (spec[op]) | ||
assertProp( node, parent, prop, spec[op], true ); | ||
assertProp( node, parent, prop, spec[op], op ); | ||
else { | ||
@@ -530,0 +552,0 @@ throw new Error( `No specification for computed variant '${op}'${at( [node, parent], prop, idx )}` ); |
@@ -108,3 +108,3 @@ // Detect cycles in the dependencies between nodes (artifacts and elements) | ||
if (dep.art._scc.lowlink == w._scc.lowlink) // in same SCC | ||
reportCycle( dep.art, dep.location ); | ||
reportCycle( dep.art, dep.location, w ); | ||
} | ||
@@ -111,0 +111,0 @@ return r; |
@@ -82,3 +82,3 @@ // Compiler phase "define": transform dictionary of AST-like CSNs into augmented CSN | ||
const { msgName, getMessageFunction, searchName } = require('../base/messages'); | ||
const { getMessageFunction, searchName } = require('../base/messages'); | ||
const { queryOps, setProp, forEachGeneric, forEachInOrder, forEachMember } | ||
@@ -88,3 +88,3 @@ = require('../base/model'); | ||
= require('../base/dictionaries'); | ||
const { dictKinds, kindProperties, fns, linkToOrigin, setMemberParent, storeExtension, combinedLocation } = require('./shared'); | ||
const { dictKinds, kindProperties, fns, setLink, linkToOrigin, setMemberParent, storeExtension, combinedLocation } = require('./shared'); | ||
const { compareLayer, layer } = require('./moduleLayers'); | ||
@@ -110,6 +110,6 @@ var initBuiltins = require('./builtins'); | ||
model.definitions = Object.create(null); | ||
// model.annotations = Object.create(null); | ||
setProp( model, '_entities', [] ); // for entities with includes | ||
var extensionsDict = Object.create(null); | ||
var lateExtensionsDict = Object.create(null); | ||
var lateExtensionsDict = Object.create(null); // for generated artifacts | ||
initBuiltins( model ); | ||
@@ -120,5 +120,8 @@ for (let name in model.sources) { | ||
applyExtensions(); | ||
forEachGeneric( model, 'definitions', checkRedefinitions ); | ||
processLocalizedData(); | ||
forEachGeneric( model, 'definitions', processArtifact ); | ||
lateExtensions(); | ||
// Set _service link (sorted to set it on parent first). Could be set | ||
// directly, but beware a namespace becoming a service later. | ||
Object.keys( model.definitions ).sort().forEach( setAncestorsAndService ); | ||
forEachGeneric( model, 'definitions', postProcessArtifact ); | ||
return model; | ||
@@ -128,5 +131,5 @@ | ||
forEachMember( obj, checkRedefinitions ); | ||
if (i == undefined) | ||
if (i == null) | ||
return; | ||
message( 'duplicate-definition', obj.name.location, | ||
message( 'duplicate-definition', obj.name.location, obj, | ||
{ name, '#': (obj.kind === 'namespace') ? 'namespace' : dictKinds[prop] }, | ||
@@ -178,3 +181,3 @@ 'Error', { | ||
if (builtin && !builtin.internal) { | ||
message( 'ref-shadowed-builtin', src.namespace.location, | ||
message( 'ref-shadowed-builtin', src.namespace.location, null, // no home artifact | ||
{ id: last.id, art: src.namespace, code: `using ${builtin.name.absolute};` }, | ||
@@ -225,2 +228,3 @@ 'Warning', '$(ID) now refers to $(ART) - consider $(CODE)' ); | ||
addToDefinitions( context, absolute ); | ||
setProp( context, '_parent', parent || null ); | ||
} | ||
@@ -248,9 +252,14 @@ setProp( item, '_artifact', context ); | ||
addToDict( context.artifacts, id.id, art ); | ||
setProp( art, '_parent', context ); | ||
} | ||
else if (parent && art.name.path) { | ||
addToDict( parent.artifacts, art.name.path[0].id, art ); | ||
setProp( art, '_parent', parent ); | ||
} | ||
else if (!('_parent' in art)) { | ||
setProp( art, '_parent', null ); | ||
} | ||
if (absolute === 'cds') { | ||
// TODO: move all 'cds' prefix checks into compiler | ||
message( null, art.name.location, | ||
message( null, art.name.location, parent, | ||
`The namespace "cds" is reserved for CDS builtins` ); | ||
@@ -295,2 +304,3 @@ } | ||
addToDict( context.artifacts, id, art ); | ||
setProp( art, '_parent', context ); | ||
} | ||
@@ -323,4 +333,4 @@ | ||
if (art.kind === 'using') // repeated defs would be shown repeatedly otherwise | ||
message( 'duplicate-using', loc, { name }, 'Error', | ||
`Duplicate definition of top-level name ${msgName( name )}` ); | ||
message( 'duplicate-using', loc, null, { name }, 'Error', | ||
'Duplicate definition of top-level name $(NAME)' ); | ||
} ); | ||
@@ -397,2 +407,3 @@ } | ||
setProp( env.name, '_artifact', art ); | ||
setProp( env, '_parent', art ); | ||
art.blocks.push( env ); | ||
@@ -416,16 +427,11 @@ defineAnnotations( env, art, block ); // requires name.absolute of siblings! | ||
defineAnnotations( art, art, block ); | ||
if (defProp && !options.newCsn) { | ||
initMembers( art, art, block ); // old augmentor bug workaround | ||
if (art.source) // from old-style CSN - TODO: error!? | ||
return; | ||
} | ||
initDollarSelf( art ); // to allow extend projection with auto-mixin assoc, see #924 | ||
initParams( art ); | ||
art.$from = []; // for sequence of resolve steps | ||
art.$queries = []; | ||
art.queries = []; | ||
setProp( art, '_leadingQuery', initQueryExpression( art, art.query ) ); | ||
setProp( art._leadingQuery, '_$next', art ); | ||
art.queries.forEach( initSubQuery ); // TODO: per art.query | ||
// TODO: simplified for simple views = just one query with one table source | ||
if (art.query.from && art.query.from.length === 1 && art.query.from[0] && art.query.from[0].path) | ||
art.source = art.query.from[0]; | ||
// resolve parameters and actions: | ||
@@ -440,3 +446,3 @@ initMembers( art, art, block ); // before setting art.elements! | ||
if (art.dbType && !options.hanaFlavor) | ||
message( null, art.dbType.location, `TABLE TYPE is not supported yet` ); | ||
message( null, art.dbType.location, art, `TABLE TYPE is not supported yet` ); | ||
defineAnnotations( art, art, block ); | ||
@@ -464,4 +470,2 @@ initMembers( art, art, block ); | ||
art.$tableAliases[selfname] = self; | ||
if (options.oldstyleSelf) | ||
art.$tableAliases.self = art.$tableAliases[selfname]; | ||
setProp( art, '_$next', model.$magicVariables ); | ||
@@ -494,3 +498,3 @@ } | ||
initExprForQuery( query.on, query ); | ||
// TODO: MIXIN with name = ...subquery | ||
// TODO: MIXIN with name = ...subquery (not yet supported anyway) | ||
for (let elem of query.columns || []) { | ||
@@ -507,6 +511,2 @@ if (elem && elem.value) { | ||
initExprForQuery( query.having, query ); | ||
for (let sub of query.queries) { | ||
initSubQuery( sub ); | ||
setProp( sub, '_$next', query ); // for name resolution | ||
} | ||
initMembers( query, query, query._block ); | ||
@@ -552,2 +552,6 @@ } | ||
addQuery(); | ||
// TODO: use first tabalias name on the right side of the join (if much | ||
// easier: last of left side) as 'param' in name of this "query" (do | ||
// not use that for user msg, only for --raw-output) | ||
initSubQuery( query ); | ||
parents = [...parents, query]; | ||
@@ -560,2 +564,3 @@ } | ||
addQuery(); | ||
query._main.$queries.push( query ); // TODO: set number with it | ||
if (parents.length) | ||
@@ -584,6 +589,7 @@ addAlias( {}, query ); | ||
}; | ||
setProp( query.$tableAliases.$self, '_parent', query ); | ||
setProp( query.$tableAliases.$self, '_main', query._main ); | ||
setProp( query.$tableAliases.$self, '_finalType', query ); | ||
setProp( query.$tableAliases.$self, '_finalType', query ); | ||
} | ||
initSubQuery( query ); // after from / mixin | ||
} | ||
@@ -604,3 +610,3 @@ else if (query.args) { // UNION, INTERSECT, ..., sub query | ||
if (parents.length) | ||
addAlias( {}, leading ); | ||
addAlias( query, leading ); | ||
} | ||
@@ -611,3 +617,3 @@ // else: with parse error (`select from <EOF>`) | ||
function signalDuplicate( name, loc ) { | ||
message( 'duplicate-definition', loc, { name, '#': '$tableAlias' }, | ||
message( 'duplicate-definition', loc, query, { name, '#': '$tableAlias' }, | ||
'Error', | ||
@@ -621,10 +627,13 @@ { '$tableAlias': 'Duplicate definition of table alias or mixin $(NAME)' } ); | ||
if (!query.name || !query.name.id) { | ||
message( 'query-req-alias', query.location, {}, | ||
message( 'query-req-alias', query.location, query, {}, // TODO: not subquery.location ? | ||
'Error', 'Table alias is required for this subquery' ); | ||
return; | ||
} | ||
let name = { id: query.name.id, location: query.name.location }; | ||
Object.assign( alias, { name, kind: '$tableAlias', location: query.location } ); | ||
if (alias !== query) { | ||
alias.name = { id: query.name.id, location: query.name.location }; | ||
alias.location = query.location; | ||
} | ||
alias.kind = '$tableAlias'; | ||
let parent = parents[0]; | ||
setMemberParent( alias, name.id, parent ); | ||
setMemberParent( alias, alias.name.id, parent ); | ||
if (!parent._firstAliasInFrom) | ||
@@ -659,3 +668,3 @@ setProp( parent, '_firstAliasInFrom', alias ); | ||
function addQuery() { | ||
// TODO: set $next | ||
setProp( query, '_$next', art ); | ||
setProp( query, '_block', art._block ); | ||
@@ -735,2 +744,3 @@ query.kind = 'query'; | ||
} | ||
// if (!kindProperties[ elem.kind ]) console.log(elem.kind,elem.name) | ||
if (kindProperties[ elem.kind ].isExtension) { | ||
@@ -740,3 +750,4 @@ storeExtension( elem, name, prop, parent, block ); | ||
else if (isQueryExtension && elem.kind === 'element') { | ||
message( 'extend-query', elem.location, { art: parent._main||parent }, | ||
message( 'extend-query', elem.location, construct, // TODO: searchName ? | ||
{ art: parent._main||parent }, | ||
'Error', 'Query entity $(ART) can only be extended with actions' ); | ||
@@ -764,32 +775,27 @@ } | ||
if (prop === 'actions') { | ||
message( 'unexpected-actions', location, {}, 'Error', | ||
message( 'unexpected-actions', location, {}, construct, 'Error', | ||
'Actions and functions only exist top-level and for entities' ); | ||
} | ||
else if (parent.kind === 'action' || parent.kind === 'function') { | ||
message( 'extend-action', construct.location, {}, 'Error', | ||
message( 'extend-action', construct.location, construct, {}, 'Error', | ||
'Actions and functions cannot be extended, only annotated' ); | ||
} | ||
else if (prop === 'params') { | ||
if (!feature) { | ||
if (!['entity','view'].includes(parent.kind) || | ||
!options.betaMode && !options.hanaFlavor && construct.kind !== 'annotate') | ||
message( 'unexpected-params', location, {}, 'Error', | ||
'Parameters only exist for actions or functions' ); | ||
else if (construct.kind === 'annotate') | ||
return true; | ||
} | ||
if (!feature) | ||
message( 'unexpected-params', location, construct, {}, 'Error', | ||
'Parameters only exist for entities, actions or functions' ); | ||
else | ||
message( 'extend-with-params', location, {}, 'Error', // remark: we could allow this | ||
message( 'extend-with-params', location, construct, {}, 'Error', // remark: we could allow this | ||
'Extending artifacts with parameters is not supported' ); | ||
} | ||
else if (feature) { // allowed in principle, but not with extend | ||
message( 'extend-type', location, {}, 'Error', | ||
message( 'extend-type', location, construct, {}, 'Error', | ||
'Only structures or enum types can be extended with elements/enums' ); | ||
} | ||
else if (prop === 'elements') { | ||
message( 'unexpected-elements', location, {}, 'Error', | ||
message( 'unexpected-elements', location, construct, {}, 'Error', | ||
'Elements only exist in entities, types or typed constructs' ); | ||
} | ||
else { // if (prop === 'enum') { | ||
message( 'unexpected-enum', location, {}, 'Error', | ||
message( 'unexpected-enum', location, construct, {}, 'Error', | ||
'Enum symbols can only be defined for types or typed constructs' ); | ||
@@ -800,2 +806,74 @@ } | ||
// Set projection ancestors, and _service link for artifact with absolute name 'name': | ||
// - not set: internal artifact | ||
// - null: not within service | ||
// - false: within abstract service | ||
// - service: the artifact of the embedding service | ||
// This function must be called ordered: parent first | ||
function setAncestorsAndService( name ) { | ||
let art = model.definitions[ name ]; | ||
if (!('_parent' in art)) | ||
return; // nothing to do for builtins and redefinitions | ||
if (art.$from && !('_ancestors' in art)) { | ||
setProjectionAncestors( art ); | ||
} | ||
let parent = art._parent; | ||
let service = (parent && parent.kind !== 'service') ? parent._service : parent; | ||
setProp( art, '_service', service && !service.abstract && service ); | ||
if (service == null) // do not return on false (= in abstract service) | ||
return; | ||
// reconstruct service (in parent) as the value is false for abstract service | ||
while (parent.kind !== 'service') | ||
parent = parent._parent; | ||
if (art.kind === 'service') | ||
message( 'service-nested-service', art.name.location, art, { art: parent }, | ||
['Error'], 'A service cannot be nested within a service $(ART)' ); | ||
else if (art.kind === 'context') | ||
message( 'service-nested-context', art.name.location, art, { art: parent }, | ||
['Error'], 'A context cannot be nested within a service $(ART)' ); | ||
} | ||
function setProjectionAncestors( art ) { | ||
// Must be run after processLocalizedData() as we could have a projection | ||
// on a generated entity. | ||
let chain = []; | ||
while (art && !('_ancestors' in art) && | ||
art.$from && art.$from.length === 1 && | ||
art.query.op && art.query.op.val === 'query') { | ||
chain.push( art ); | ||
setProp( art, '_ancestors', null ); // avoid infloop with cyclic from | ||
let name = resolveUncheckedPath( art.$from[0], 'include', art ); | ||
// TODO: do not set _ancestors if params change | ||
art = name && model.definitions[ name ]; | ||
} | ||
let ancestors = art && (art._ancestors || []); | ||
for (let a of chain.reverse()) { | ||
ancestors = (ancestors ? [...ancestors, art] : []); | ||
setProp( a, '_ancestors', ancestors ); | ||
art = a; | ||
} | ||
} | ||
function postProcessArtifact( art ) { | ||
if (!art._ancestors || art.kind === 'type') | ||
return; | ||
let service = art._service; | ||
if (!service) | ||
return; | ||
let sname = service.name.absolute; | ||
art._ancestors.forEach( expose ); | ||
return; | ||
function expose( ancestor ) { | ||
if (ancestor._service === service) | ||
return; | ||
let desc = ancestor._descendants || | ||
setLink( ancestor, Object.create(null), '_descendants' ); | ||
if (!desc[ sname ]) | ||
desc[ sname ] = [art]; | ||
else | ||
desc[ sname ].push( art ); | ||
} | ||
} | ||
// Collect all artifact extensions | ||
@@ -828,2 +906,3 @@ function collectArtifactExtensions( construct, block ) { | ||
for (let ext of exts) { | ||
delete ext.name.path[0]._artifact; // get message for root | ||
resolvePath( ext.name, ext.kind, ext ); // should issue error/info | ||
@@ -914,3 +993,3 @@ if (ext.kind === 'annotate') | ||
if (!options.tntFlavor) | ||
message( null, dictLocation( art.includes ), | ||
message( null, dictLocation( art.includes ), art, | ||
'Service includes are not supported yet' ); | ||
@@ -933,2 +1012,3 @@ for (let ref of art.includes) { | ||
} | ||
model._entities.push( art ); // add structure with includes in dep order | ||
includeMembers( art, 'elements', forEachInOrder ); | ||
@@ -949,3 +1029,3 @@ includeMembers( art, 'actions', forEachGeneric ); | ||
if (noExtend && ext.kind === 'extend') { | ||
message( 'extend-for-generated', ext.name.location, { art }, | ||
message( 'extend-for-generated', ext.name.location, ext, { art }, | ||
'Error', 'You cannot use EXTEND on the generated $(ART)' ); | ||
@@ -989,3 +1069,3 @@ continue; | ||
for (let ext of extensions) { | ||
let extLayer = layer( ext ); | ||
let extLayer = layer( ext ) || { realname: '', _layerExtends: Object.create(null) }; | ||
if (!open.length) { | ||
@@ -997,7 +1077,7 @@ lastExt = ext; | ||
if (lastExt) { | ||
message( 'extend-repeated-intralayer', lastExt.location, {}, 'Warning', | ||
message( 'extend-repeated-intralayer', lastExt.location, lastExt, {}, 'Warning', | ||
'Unstable element order due to repeated extensions in same layer' ); | ||
lastExt = null; | ||
} | ||
message( 'extend-repeated-intralayer', ext.location, {}, 'Warning', | ||
message( 'extend-repeated-intralayer', ext.location, ext, {}, 'Warning', | ||
'Unstable element order due to repeated extensions in same layer' ); | ||
@@ -1008,3 +1088,3 @@ } | ||
// report for lastExt if that is unrelated to other open exts or current ext | ||
message( 'extend-unrelated-layer', lastExt.location, {}, 'Warning', | ||
message( 'extend-unrelated-layer', lastExt.location, lastExt, {}, 'Warning', | ||
'Unstable element order due to other extension in unrelated layer' ); | ||
@@ -1021,3 +1101,3 @@ } | ||
for (let ext of extensions) { | ||
message( 'extend-undefined', ext.name.location, | ||
message( 'extend-undefined', ext.name.location, ext, | ||
{ art: searchName( art, name, dictKinds[prop] ) }, | ||
@@ -1033,3 +1113,2 @@ 'Error', { | ||
function includeMembers( art, prop, forEach ) { | ||
// TODO: a projection cannot be used as include, right? | ||
// TODO two kind of messages: | ||
@@ -1040,5 +1119,9 @@ // Error 'More than one include defines element "A"' (at include ref) | ||
clearDict( art, prop ); // TODO: do not set actions property if there are none | ||
setProp( art, '_ancestors', [] ); // recursive array of includes | ||
for (let ref of art.includes) { | ||
let template = resolvePath( ref, 'include', art ); | ||
if (template) { // be robust | ||
if (template._ancestors) | ||
art._ancestors.push( ...template._ancestors ); | ||
art._ancestors.push( template ); | ||
forEach( template, prop, function( origin, name ) { | ||
@@ -1065,21 +1148,27 @@ if (members && name in members) | ||
function processLocalizedData() { | ||
function processArtifact( art, name, prop, i ) { | ||
checkRedefinitions( art, name, prop, i ); | ||
if (i != null) | ||
return; | ||
if (art.kind === 'entity' && art.elements && // check potential entity parse error | ||
(!art.abstract || !art.abstract.val)) | ||
processLocalizedData( art ); | ||
} | ||
function processLocalizedData( art ) { | ||
if (!options.betaMode) | ||
return; | ||
for (let absolute in model.definitions) { | ||
let art = model.definitions[absolute]; | ||
if (art instanceof Array || // redefininition | ||
!art.elements || // potential entity parse error | ||
art.kind !== 'entity' || art.query) // not non-query entity | ||
continue; | ||
let textsName = art.name.absolute + '_txts'; | ||
let localized = localizedData( art, textsName ); | ||
if (localized) { | ||
createTextsEntity( art, textsName, localized ); | ||
addTextsAssociations( art, textsName, absolute ); | ||
} | ||
let textsName = art.name.absolute + '_txts'; | ||
// If we re-introduce '::', search for '.' after '::'... | ||
let dot = textsName.lastIndexOf('.') + 1; | ||
let viewName = textsName.substring( 0, dot ) + 'localized_' + art.name.absolute.substring( dot ); | ||
let localized = localizedData( art, textsName, viewName ); | ||
if (localized) { | ||
createTextsEntity( art, textsName, localized ); | ||
createLocalizedDataView( art, viewName, localized ); | ||
addTextsAssociations( art, textsName, localized ); | ||
} | ||
} | ||
function localizedData( art, textsName ) { | ||
function localizedData( art, textsName, viewName ) { | ||
let keys = 0; | ||
@@ -1100,3 +1189,3 @@ let textElems = []; | ||
} | ||
else if (elem.localized && elem.localized.val) | ||
else if (hasTruthyProp( elem, 'localized' )) | ||
textElems.push( elem ); | ||
@@ -1108,22 +1197,36 @@ } | ||
if (!keys) { | ||
message( null, art.name.location, {}, 'Warning', | ||
message( null, art.name.location, art, {}, 'Warning', | ||
'No texts entity can be created when no key element exists' ); | ||
} | ||
for (let elem of protectedElems) { | ||
message( null, elem.name.location, { name: elem.name.id }, 'Warning', | ||
message( null, elem.name.location, art, { name: elem.name.id }, 'Warning', | ||
'No texts entity can be created when element $(NAME) exists' ); | ||
} | ||
let textsEntity = model.definitions[ textsName ]; | ||
if (!textsEntity) | ||
let viewEntity = model.definitions[ viewName ]; | ||
let names = []; | ||
if (textsEntity) { | ||
if (!(textsEntity instanceof Array)) | ||
message( null, textsEntity.name.location, textsEntity, { art }, 'Info', | ||
'No texts entity for $(ART) can be created with this definition' ); | ||
names.push( textsName ); | ||
} | ||
if (viewEntity) { | ||
if (!(viewEntity instanceof Array)) | ||
message( null, viewEntity.name.location, viewEntity, { art }, 'Info', | ||
'No texts entity for $(ART) can be created with this definition' ); | ||
names.push( viewName ); | ||
} | ||
if (!names.length) | ||
return !protectedElems.length && keys && textElems; | ||
message( null, art.name.location, { art: textsName }, 'Warning', | ||
'Name $(ART) for localized texts entity used by other definition' ); | ||
if (!(textsEntity instanceof Array)) { | ||
message( null, textsEntity.name.location, { art }, 'Info', | ||
'No texts entity for $(ART) can be created with this definition' ); | ||
} | ||
message( null, art.name.location, art, { names }, 'Warning', { | ||
std: 'Names $(NAMES) for localized data entities used by other definitions', | ||
one: 'Name $(NAMES) for localized data entity used by other definition' | ||
} ); | ||
return false; | ||
} | ||
// TODO: set _parent also for main artifacts! | ||
function createTextsEntity( base, absolute, textElems ) { | ||
@@ -1134,30 +1237,71 @@ let elements = Object.create(null); | ||
kind: 'entity', | ||
name: { absolute, location }, | ||
name: { path: splitIntoPath( location, absolute ), location }, | ||
location: base.location, | ||
elements, | ||
'@cds.autoexpose': { name: augmentGlobal( location, '@cds.autoexpose' ), location }, | ||
'@cds.autoexpose': { name: augmentPath( location, '@cds.autoexpose' ), location }, | ||
$inferred: 'localized' | ||
} | ||
setProp( art, '_block', model.$internal ); | ||
model.definitions[absolute] = art; | ||
let locale = { | ||
name: { location, id: 'locale' }, | ||
kind: 'element', | ||
type: augmentGlobal( location, 'cds.String' ), | ||
key: { val: true, location }, | ||
type: augmentPath( location, 'cds.String' ), | ||
length: { literal: 'number', val: 5, location }, | ||
location | ||
}; | ||
setMemberParent( locale, 'locale', art, 'elements' ); | ||
setProp( locale, '_block', model.$internal ); | ||
addToDictWithIndexNo( art, 'elements', 'locale', locale ); | ||
let artifacts = Object.create(null); | ||
artifacts[ absolute ] = art; | ||
initArtifacts( { artifacts }, null, model.$internal, false, '' ); | ||
for (let orig of textElems) { | ||
let elem = linkToOrigin( orig, orig.name.id, art, 'elements' ); | ||
if (!orig.key || !orig.key.val) // use location of LOCALIZED keyword | ||
elem.localized = { val: false, $inferred: 'localized', location: orig.localized.location }; | ||
if (orig.key && orig.key.val) | ||
elem.key = { val: true, $inferred: 'localized', location }; | ||
else { // use location of LOCALIZED keyword | ||
let localized = orig.localized || orig.type || orig.name; | ||
elem.localized = { val: false, $inferred: 'localized', location: localized.location }; | ||
} | ||
} | ||
} | ||
function addTextsAssociations( art, textsName ) { | ||
function createLocalizedDataView( base, absolute, textElems ) { | ||
let location = base.name.location; | ||
let columns = [ { location, val: '*' } ]; | ||
let artifacts = Object.create(null); | ||
let from = augmentPath( location, base.name.absolute ); | ||
from.name = { id: 'L', location }; | ||
artifacts[ absolute ] = { | ||
kind: 'entity', | ||
name: { location, path: splitIntoPath( location, absolute ) }, | ||
location: base.location, | ||
query: { location, op: { val: 'query', location }, from: [ from ], columns }, | ||
$inferred: 'localized' | ||
}; | ||
for (let orig of textElems) { | ||
if (!orig.key || !orig.key.val) { | ||
let location = orig.name.location; | ||
let id = orig.name.id; | ||
let value = { // TODO: special to allow different code in HANA? | ||
op: { location, val: 'call' }, | ||
func: augmentPath( location, 'coalesce' ), | ||
args: [ | ||
augmentPath( location, 'L', 'localized', id ), | ||
augmentPath( location, 'L', id ) ], | ||
location | ||
}; | ||
setProp( value, '_artifact', orig ); | ||
let origin = {}; | ||
setProp( origin, '_artifact', orig ); | ||
// TODO: stay automatically silent if "shadowed" source element appears in expression | ||
columns.push( { name: { id, location }, location: orig.location, value, origin, $replacement: 'silent' } ); | ||
} | ||
} | ||
// TODO: support for name.space::Base ? | ||
initArtifacts( { artifacts }, null, model.$internal, false, '' ); | ||
} | ||
function addTextsAssociations( art, textsName, textElems ) { | ||
// texts : Composition of many Books_txts on texts.ID=ID; | ||
let keys = textElems.filter( e => e.key && e.key.val ); | ||
let location = art.name.location; | ||
@@ -1169,6 +1313,6 @@ let texts = { | ||
$inferred: 'localized', | ||
type: augmentGlobal( location, 'cds.Composition' ), | ||
type: augmentPath( location, 'cds.Composition' ), | ||
cardinality: { targetMax: { literal: 'string', val: '*', location }, location }, | ||
target: augmentGlobal( location, textsName ), | ||
onCond: augmentEqual( location, ['texts.ID', 'ID'] ) | ||
target: augmentPath( location, textsName ), | ||
onCond: augmentEqual( location, 'texts', keys ) | ||
} | ||
@@ -1179,2 +1323,3 @@ setMemberParent( texts, 'texts', art, 'elements' ); | ||
// localized.ID=ID and localized.locale = $user.locale; | ||
keys.push( ['localized.locale', '$user.locale'] ); | ||
let localized = { | ||
@@ -1185,6 +1330,5 @@ name: { location, id: 'localized' }, | ||
$inferred: 'localized', | ||
type: augmentGlobal( location, 'cds.Association' ), | ||
target: augmentGlobal( location, textsName ), | ||
onCond: augmentEqual( location, | ||
['localized.ID', 'ID'], ['localized.locale', '$user.locale'] ) | ||
type: augmentPath( location, 'cds.Association' ), | ||
target: augmentPath( location, textsName ), | ||
onCond: augmentEqual( location, 'localized', keys ) | ||
} | ||
@@ -1194,2 +1338,27 @@ setMemberParent( localized, 'localized', art, 'elements' ); | ||
} | ||
function hasTruthyProp( art, prop ) { | ||
// Returns whether art directly or indirectly has the property 'prop', | ||
// following the 'origin' and the 'type' (not involving elements). | ||
// | ||
// TODO: we should issue a warning if we get localized via TYPE OF | ||
let processed = Object.create(null); // avoid infloops with circular refs | ||
let name = art.name.absolute; // is ok, since no recursive type possible | ||
while (art && !processed[name]) { | ||
if (art[prop]) | ||
return art[prop].val; | ||
processed[name] = art; | ||
if (art.origin && art.origin._artifact) { | ||
art = art.origin._artifact; | ||
name = art && art.name.absolute; | ||
} | ||
else if (art.type && art._block) { // TODO: not TYPE OF | ||
name = resolveUncheckedPath( art.type, 'type', art ); | ||
art = name && model.definitions[ name ]; | ||
} | ||
else | ||
return false; | ||
} | ||
return false; | ||
} | ||
} | ||
@@ -1203,7 +1372,15 @@ | ||
function augmentGlobal( location, id ) { | ||
return { path: [{ id, location }], location }; | ||
function splitIntoPath( location, name ) { | ||
// TODO: is currently needed to add the artifact into parent | ||
// TODO: make it also work with path = [{id:absolute}] | ||
let colons = name.indexOf('::'); | ||
let items = (colons < 0) ? name.split('.') : name.substring(colons).split('.'); | ||
return items.map( id => ({ id, location }) ); | ||
} | ||
function augmentEqual( location, ...relations ) { | ||
function augmentPath( location, ...args ) { | ||
return { path: args.map( id => ({ id, location }) ), location }; | ||
} | ||
function augmentEqual( location, prefix, relations ) { | ||
let args = relations.map( eq ); | ||
@@ -1214,4 +1391,15 @@ return (args.length === 1) | ||
function eq( args ) { | ||
return { op: { val: '=', location }, args: args.map( ref ), location }; | ||
function eq( refs ) { | ||
if (refs instanceof Array) | ||
return { op: { val: '=', location }, args: refs.map( ref ), location }; | ||
else { | ||
let id = refs.name.id; | ||
return { | ||
op: { val: '=', location }, | ||
args: [ | ||
{ path: [ { id: prefix, location }, { id, location } ], location }, | ||
{ path: [ { id, location } ], location } ], | ||
location | ||
}; | ||
} | ||
} | ||
@@ -1218,0 +1406,0 @@ function ref( path ) { |
@@ -15,2 +15,4 @@ // | ||
'@cds.persistence.table': never, | ||
'@Analytics.hidden': never, | ||
'@Analytics.visible': never, | ||
'@': withKind, // always except in 'returns' and 'items' | ||
@@ -20,3 +22,3 @@ default: withKind, // always except in 'returns' and 'items' | ||
notNull, // a variant of notViaType() | ||
targetElement: onlyViaParent, | ||
targetElement: onlyViaParent, // in foreign keys | ||
value: onlyViaParent, // enum symbol value | ||
@@ -152,3 +154,3 @@ // masked: special = done in definer | ||
function expensive( prop, target, source ) { | ||
// console.log(prop,refString(source),'->',target.kind,refString(target),refString(type)); | ||
// console.log(prop,refString(source),'->',target.kind,refString(target)); | ||
if (prop !== 'foreignKeys' && availableAtType( prop, target, source )) | ||
@@ -158,4 +160,7 @@ // foreignKeys must always be copied with target to avoid any confusion | ||
return; | ||
if (prop === 'params' && target.$inferred !== 'proxy' && target.$inferred !== 'include') | ||
return; | ||
let location = target.type && !target.type.$inferred && target.type.location | ||
|| target.location; | ||
|| target.location | ||
|| target._outer && target._outer.location; | ||
let dict = source[prop]; | ||
@@ -176,3 +181,3 @@ for (let name in dict) { | ||
function onlyViaParent( prop, target, source ) { | ||
if (target.$inferred === 'proxy') | ||
if (target.$inferred === 'proxy') // assocs and enums do not have 'include' | ||
always( prop, target, source ); | ||
@@ -179,0 +184,0 @@ } |
@@ -23,4 +23,4 @@ // Compiler functions and utilities shared across all phases | ||
service: { artifacts: true, normalized: 'namespace' }, // actions: true with "service-bound" actions | ||
entity: { elements: true, actions: true }, // beta-mode - params: () => false | ||
view: { elements: true, actions: true }, // beta-mode - params: () => false | ||
entity: { elements: true, actions: true, params: () => false }, | ||
view: { elements: true, actions: true, params: () => false }, | ||
query: { elements: true }, | ||
@@ -75,5 +75,8 @@ $tableAlias: { normalized: 'alias', $navigation: true }, // table alias in select | ||
expr: { next: '_$next', escape: 'param', noDep: true }, | ||
rewrite: { next: '_$next', escape: 'param', noDep: true, rewrite: true }, // TODO: assertion that there is no next/escape used | ||
'order-by-union': { next: '_$next', escape: 'param', noDep: true, noExt: true }, | ||
// expr TODO: better - on condition for assoc, other on | ||
// expr TODO: write dependency, but care for $self | ||
param: { reject: rejectNonConst }, | ||
global: { useDefinitions: true, global: true }, // for using declaration | ||
} | ||
@@ -92,3 +95,3 @@ | ||
function rejectNonStruct( art ) { | ||
return (['type', 'entity'].includes( art.kind ) && art.elements && !art.query) | ||
return (['type', 'entity'].includes( art.kind ) && art.elements && !art.query && !art.params) | ||
? undefined | ||
@@ -141,3 +144,5 @@ : 'expected-struct'; | ||
let spec = specExpected[expected]; | ||
let art = getPathRoot( ref.path, spec, user._block, null, true ); | ||
let art = (ref.scope === 'global' || spec.global) | ||
? getPathRoot( ref.path, spec, user, {}, model.definitions ) | ||
: getPathRoot( ref.path, spec, user, user._block, null, true ); | ||
if (art === false) // redefinitions | ||
@@ -175,3 +180,3 @@ art = ref.path[0]._artifact[0]; // array stored in head's _artifact | ||
let variant = (env.$frontend && env.$frontend !== 'cdl') ? 'std' : 'cdl'; | ||
message( 'ref-unexpected-scope', head.location, { name: head.id, '#': variant }, | ||
message( 'ref-unexpected-scope', head.location, user, { name: head.id, '#': variant }, | ||
'Error', { | ||
@@ -201,10 +206,11 @@ std: 'Unexpected parameter scope for name $(NAME)', | ||
// queries: first tabaliases, then $magic - value refs: first $self, then $magic | ||
// TODO: set extDict to query.$combined if extDict is not set | ||
if (!extDict && !spec.noExt) | ||
extDict = query && query.$combined || | ||
environment( user._main ? user._parent : user ); | ||
} | ||
// if (!head) console.error(ref) | ||
// 'global' for CSN later in value paths, CDL for Association/Composition: | ||
let art = (ref.scope === 'global') | ||
? getPathRoot( path, spec, {}, model.definitions ) | ||
: getPathRoot( path, spec, env, extDict, msgArt || 0 ); | ||
let art = (ref.scope === 'global' || spec.global) | ||
? getPathRoot( path, spec, user, {}, model.definitions ) | ||
: getPathRoot( path, spec, user, env, extDict, msgArt || 0 ); | ||
if (!art) | ||
@@ -235,3 +241,3 @@ return setLink( ref, art ); | ||
} | ||
else { // FROM subquery, $projection | ||
else { // FROM subquery, $projection, $self | ||
setLink( head, art._finalType ); // the query (sub or self) | ||
@@ -241,3 +247,3 @@ } | ||
art = getPathItem( path, spec ); | ||
art = getPathItem( path, spec, user ); | ||
if (!art) | ||
@@ -256,3 +262,3 @@ return setLink( ref, art ); | ||
if (msg) { | ||
signalNotFound( msg, ref.location ); | ||
signalNotFound( msg, ref.location, user ); | ||
return setLink( ref, false ); | ||
@@ -282,4 +288,6 @@ } | ||
// non-existing external using references if `unchecked` is truthy. | ||
function getPathRoot( path, spec, env, extDict, msgArt ) { | ||
function getPathRoot( path, spec, user, env, extDict, msgArt ) { | ||
let head = path[0]; | ||
if (!head || !head.id) | ||
return undefined; // parse error | ||
if ('_artifact' in head) | ||
@@ -309,4 +317,9 @@ return (head._artifact instanceof Array) ? false : head._artifact; | ||
else if (r.kind === '$parameters') { | ||
if (!head.quoted && path.length > 1) | ||
if (!head.quoted && path.length > 1) { | ||
message( 'ref-obsolete-parameters', head.location, user, | ||
{ code: '$parameters.' + path[1].id, newcode: ':' + path[1].id }, | ||
['Error'], 'Obsolete $(CODE) - replace by $(NEWCODE)' ); | ||
// TODO: replace it in to-csn correspondingly | ||
return setLink( head, r ); | ||
} | ||
} | ||
@@ -327,3 +340,3 @@ else if (r.kind !== '$tableAlias' || | ||
if (names.length) | ||
message( 'ref-ambiguous', head.location, { id: head.id, names }, | ||
message( 'ref-ambiguous', head.location, user, { id: head.id, names }, | ||
'Error', 'Ambiguous $(ID), replace by $(NAMES)' ); | ||
@@ -349,6 +362,15 @@ } | ||
// navigation elements (for which you should use a table alias) | ||
for (let name in extDict) { | ||
if (!(extDict[name] instanceof Array && extDict[name][0].kind === '$navElement')) | ||
e[name] = extDict[name]; | ||
if (extDict !== model.definitions) { | ||
for (let name in extDict) { | ||
let def = extDict[name]; | ||
if (!(def instanceof Array && def[0].kind === '$navElement')) | ||
e[name] = def; | ||
} | ||
} | ||
else { | ||
for (let name in extDict) { | ||
if (!name.includes('.')) | ||
e[name] = extDict[name]; | ||
} | ||
} | ||
valid.push( e ); | ||
@@ -361,14 +383,14 @@ } | ||
if (msgArt) | ||
signalNotFound( 'ref-undefined-element', head.location, valid, | ||
signalNotFound( 'ref-undefined-element', head.location, user, valid, | ||
{ art: searchName( msgArt, head.id, 'element' ) } ); | ||
else | ||
signalNotFound( 'ref-undefined-var', head.location, valid, { id: head.id }, | ||
signalNotFound( 'ref-undefined-var', head.location, user, valid, { id: head.id }, | ||
'Error', 'Element or variable $(ID) has not been found' ); | ||
} | ||
else if (env.$frontend && env.$frontend !== 'cdl') | ||
else if (env.$frontend && env.$frontend !== 'cdl' || spec.global) | ||
// IDE can inspect <model>.definitions - provide null for valid | ||
signalNotFound( spec.undefinedDef || 'ref-undefined-def', head.location, null, | ||
signalNotFound( spec.undefinedDef || 'ref-undefined-def', head.location, user, valid, | ||
{ art: head.id } ); | ||
else | ||
signalNotFound( spec.undefinedArt || 'ref-undefined-art', head.location, valid, | ||
signalNotFound( spec.undefinedArt || 'ref-undefined-art', head.location, user, valid, | ||
{ name: head.id } ); | ||
@@ -382,6 +404,6 @@ return setLink( head, null ); | ||
// element item in the path) | ||
function getPathItem( path, spec ) { | ||
function getPathItem( path, spec, user ) { | ||
var art; | ||
for (let item of path) { | ||
if (!item) // incomplete AST due to parse error | ||
if (!item || !item.id) // incomplete AST due to parse error | ||
return undefined; | ||
@@ -408,9 +430,9 @@ if (item._artifact) { // should be there on first path element | ||
// TODO: better for TYPE OF, FROM e.Assoc (even disallow for other refs) | ||
signalNotFound( spec.undefinedDef || 'ref-undefined-def', item.location, [env], | ||
{ art: searchName( art, item.id ) } ); | ||
signalNotFound( spec.undefinedDef || 'ref-undefined-def', item.location, user, | ||
[env], { art: searchName( art, item.id ) } ); | ||
} | ||
else if (art.name.query != null) { | ||
// TODO: probably not extra messageId, but text variant | ||
signalNotFound( 'query-undefined-element', item.location, [env], | ||
{ id: item.id }, 'Error', | ||
signalNotFound( 'query-undefined-element', item.location, user, | ||
[env], { id: item.id }, 'Error', | ||
'Element $(ID) has not been found in the elements of the query' ); | ||
@@ -421,9 +443,9 @@ // TODO: 'The current query has no element $(MEMBER)' with name.self | ||
else if (art.kind === '$parameters') { | ||
signalNotFound( 'ref-undefined-param', item.location, [env], | ||
{ art: searchName( art._main, item.id, 'param' ) }, | ||
signalNotFound( 'ref-undefined-param', item.location, user, | ||
[env], { art: searchName( art._main, item.id, 'param' ) }, | ||
'Error', { param: 'Entity $(ART) has no parameter $(MEMBER)' } ); | ||
} | ||
else { | ||
signalNotFound( 'ref-undefined-element', item.location, [env], | ||
{ art: searchName( art, item.id ) } ); | ||
signalNotFound( 'ref-undefined-element', item.location, user, | ||
[env], { art: searchName( art, item.id ) } ); | ||
} | ||
@@ -434,7 +456,8 @@ return null; | ||
function signalNotFound( msgId, location, valid, ...args ) { | ||
function signalNotFound( msgId, location, home, valid, ...args ) { | ||
// if (!location) console.log(msgId, valid, ...args) | ||
if (location.$notFound) | ||
return; | ||
location.$notFound = true; | ||
let err = message( msgId, location, ...args ); | ||
let err = message( msgId, location, home, ...args ); | ||
// console.log( Object.keys( Object.assign( Object.create(null), ...valid.reverse() ) ) ) | ||
@@ -445,3 +468,3 @@ if (valid && (options.attachValidNames || options.testMode)) | ||
let names = Object.keys( err.validNames ); | ||
message( null, location, | ||
message( null, location, null, | ||
names.length ? 'Valid: ' + names.sort().join(', ') : 'No valid names', | ||
@@ -497,3 +520,3 @@ 'Info' ); | ||
if (iHaveVariant) | ||
message( 'anno-duplicate-variant', item.name.variant.location, {}, // TODO: params | ||
message( 'anno-duplicate-variant', item.name.variant.location, construct, {}, // TODO: params | ||
'Error', 'Annotation variant has been already provided' ); | ||
@@ -572,2 +595,3 @@ prop = prop + '#' + item.name.variant.id; // TODO: check for double variants | ||
setProp( elem.origin, '_artifact', origin ); | ||
// TODO: make this just elem._origin, remove elem.origin | ||
return elem; | ||
@@ -631,2 +655,3 @@ } | ||
fns, | ||
setLink, | ||
linkToOrigin, setMemberParent, | ||
@@ -633,0 +658,0 @@ storeExtension, |
@@ -699,2 +699,91 @@ { | ||
"AppliesTo": "Annotation" | ||
}, | ||
"Validation.Pattern": { | ||
"Type": "Edm.String", | ||
"AppliesTo": "Property Parameter Term" | ||
}, | ||
"Validation.Minimum": { | ||
"Type": "Edm.Decimal", | ||
"Scale": "variable", | ||
"AppliesTo": "Property Parameter Term" | ||
}, | ||
"Validation.Maximum": { | ||
"Type": "Edm.Decimal", | ||
"Scale": "variable", | ||
"AppliesTo": "Property Parameter Term" | ||
}, | ||
"Validation.Exclusive": { | ||
"Type": "Core.Tag", | ||
"AppliesTo": "Annotation" | ||
}, | ||
"Validation.AllowedValues": { | ||
"Type": "Collection(Validation.AllowedValue)", | ||
"AppliesTo": "Property Parameter TypeDefinition" | ||
}, | ||
"Validation.MultipleOf": { | ||
"Type": "Edm.Decimal", | ||
"Scale": "variable", | ||
"AppliesTo": "Property Parameter Term" | ||
}, | ||
"Validation.Constraint": { | ||
"Type": "Validation.ConstraintType", | ||
"AppliesTo": "Property EntityType ComplexType" | ||
}, | ||
"Validation.ItemsOf": { | ||
"Type": "Collection(Validation.ItemsOfType)", | ||
"AppliesTo": "EntityType ComplexType" | ||
}, | ||
"Validation.OpenPropertyTypeConstraint": { | ||
"Type": "Collection(Core.QualifiedTypeName)", | ||
"AppliesTo": "ComplexType EntityType" | ||
}, | ||
"Validation.DerivedTypeConstraint": { | ||
"Type": "Collection(Core.QualifiedTypeName)", | ||
"AppliesTo": "EntitySet Singleton NavigationProperty Property TypeDefinition Parameter ReturnType" | ||
}, | ||
"Validation.AllowedTerms": { | ||
"Type": "Collection(Core.QualifiedTermName)", | ||
"AppliesTo": "Term Property" | ||
}, | ||
"Validation.MaxItems": { | ||
"Type": "Edm.Int64", | ||
"AppliesTo": "Collection" | ||
}, | ||
"Validation.MinItems": { | ||
"Type": "Edm.Int64", | ||
"AppliesTo": "Collection" | ||
}, | ||
"PersonalData.EntitySemantics": { | ||
"Type": "PersonalData.EntitySemanticsType", | ||
"AppliesTo": "EntitySet", | ||
"$experimental": true | ||
}, | ||
"PersonalData.DataSubjectRole": { | ||
"Type": "Edm.String", | ||
"AppliesTo": "EntitySet", | ||
"$experimental": true | ||
}, | ||
"PersonalData.DataSubjectRoleDescription": { | ||
"Type": "Edm.String", | ||
"AppliesTo": "EntitySet", | ||
"$experimental": true | ||
}, | ||
"PersonalData.FieldSemantics": { | ||
"Type": "PersonalData.FieldSemanticsType", | ||
"AppliesTo": "Property", | ||
"$experimental": true | ||
}, | ||
"PersonalData.IsPotentiallyPersonal": { | ||
"Type": "Core.Tag", | ||
"AppliesTo": "Property", | ||
"$experimental": true | ||
}, | ||
"PersonalData.IsPotentiallySensitive": { | ||
"Type": "Core.Tag", | ||
"AppliesTo": "Property" | ||
}, | ||
"PersonalData.IsUserID": { | ||
"Type": "Core.Tag", | ||
"AppliesTo": "Property", | ||
"$experimental": true | ||
} | ||
@@ -1932,4 +2021,32 @@ }, | ||
] | ||
}, | ||
"Validation.AllowedValue": { | ||
"$kind": "ComplexType", | ||
"Properties": { | ||
"Value": "Edm.PrimitiveType" | ||
} | ||
}, | ||
"Validation.ConstraintType": { | ||
"$kind": "ComplexType", | ||
"Properties": { | ||
"FailureMessage": "Edm.String", | ||
"Condition": "Edm.Boolean" | ||
} | ||
}, | ||
"Validation.ItemsOfType": { | ||
"$kind": "ComplexType", | ||
"Properties": { | ||
"path": "Edm.NavigationPropertyPath", | ||
"target": "Edm.NavigationPropertyPath" | ||
} | ||
}, | ||
"PersonalData.EntitySemanticsType": { | ||
"$kind": "TypeDefinition", | ||
"UnderlyingType": "Edm.String" | ||
}, | ||
"PersonalData.FieldSemanticsType": { | ||
"$kind": "TypeDefinition", | ||
"UnderlyingType": "Edm.String" | ||
} | ||
} | ||
} |
@@ -144,4 +144,4 @@ const parseXml = require('./xmlParserWithLocations'); | ||
return { | ||
literal: 'decimal', | ||
val: Number(val) | ||
literal: 'number', | ||
val: Number.isSafeInteger(Number.parseFloat(val)) ? Number(val) : val | ||
} | ||
@@ -151,4 +151,4 @@ }, | ||
return { | ||
literal: 'decimal', | ||
val: Number(val) | ||
literal: 'number', | ||
val: Number.isSafeInteger(Number.parseFloat(val)) ? Number(val) : val | ||
} | ||
@@ -158,8 +158,14 @@ }, | ||
return { | ||
literal: 'integer', | ||
val: Number(val) | ||
literal: 'number', | ||
val: Number.isSafeInteger(Number.parseInt(val)) ? Number(val) : val | ||
} | ||
}, | ||
EnumMember: obj => handleEnum(obj), | ||
Path: obj => handlePath(obj, false), | ||
Binary: val => edmxAttrs.String(val), | ||
Date: val => edmxAttrs.String(val), | ||
DateTimeOffset: val => edmxAttrs.String(val), | ||
Guid: val => edmxAttrs.String(val), | ||
TimeOfDay: val => edmxAttrs.String(val), | ||
Duration: val => edmxAttrs.String(val), | ||
EnumMember: (obj, location) => handleEnum(obj, location), | ||
Path: (obj, location) => handlePath(obj, false, location), | ||
NavigationPropertyPath: obj => handlePath(obj, false), | ||
@@ -179,4 +185,2 @@ PropertyPath: obj => handlePath(obj, false) | ||
return handlePropValue(obj.PropertyValue, isCollection); | ||
if (obj.String) | ||
return handleString(obj.String, isCollection); | ||
else | ||
@@ -247,7 +251,7 @@ return handleValue(obj, isCollection); | ||
function handlePath(path, isCollection) { | ||
function handlePath(path, isCollection, location = obj._location) { | ||
if (isCollection) | ||
return path.map(e => handlePath(e, false)) | ||
return path.map(e => handlePath(e, false, location)) | ||
return { | ||
path: (path._text || path).split('/').map(p => { return { id: p, location: obj._location } }), | ||
path: (path._text || path).split('/').map(p => { return { id: p, location: location } }), | ||
location: obj._location | ||
@@ -265,8 +269,2 @@ }; | ||
function handleString(obj, isCollection) { | ||
if (isCollection) | ||
return obj.map(e => handleString(e, false)); | ||
return Object.assign(edmxAttrs.String(obj._text), { location: obj._location }); | ||
} | ||
function handleValue(obj, isCollection) { | ||
@@ -278,2 +276,10 @@ // this prevents the map execution of not an array, valid only for the collection tag | ||
// handle the case of builtin as tag and collection of builtins | ||
let builtinTag = Object.keys(edmxAttrs).find(atr => Object.keys(obj).includes(atr) || atr === obj._name); | ||
if (builtinTag) { | ||
if (isCollection) | ||
return obj[builtinTag].map(e => handleValue(e, false)); | ||
return Object.assign(edmxAttrs[builtinTag](obj._text), { location: obj._location }); | ||
} | ||
if (isCollection) | ||
@@ -288,3 +294,3 @@ return obj.map(e => handleValue(e, false)); | ||
if (bltnAttr) | ||
return Object.assign(edmxAttrs[bltnAttr](obj._attributes[bltnAttr]), { location: obj._location }); | ||
return Object.assign(edmxAttrs[bltnAttr](obj._attributes[bltnAttr], obj._location), { location: obj._location }); | ||
return {}; | ||
@@ -291,0 +297,0 @@ } |
@@ -9,2 +9,4 @@ 'use strict'; | ||
const knownVocabularies = ['Aggregation', 'Analytics', 'Core', 'Common', 'UI', 'Communication', 'Capabilities', 'Measures', 'Validation', 'PersonalData']; | ||
/************************************************************************************************** | ||
@@ -33,2 +35,3 @@ * csn2annotationEdm | ||
} | ||
let g_experimental_terms = {}; // take note of all experimental annos that have been used | ||
@@ -67,5 +70,5 @@ let v = options.v; | ||
let schema = Edm.Schema.create(v, serviceName, serviceName, g_annosArray, false); | ||
let service = Edm.DataServices.create(v, schema); | ||
let edm = Edm.create(v, service); | ||
let schema = new Edm.Schema(v, serviceName, serviceName, g_annosArray, false); | ||
let service = new Edm.DataServices(v, schema); | ||
let edm = new Edm(v, service); | ||
return edm; | ||
@@ -270,3 +273,3 @@ | ||
// result objects that holds all the annotation objects to be created | ||
let newAnnosStd = Edm.Annotations.create(v, stdName); // used in closure | ||
let newAnnosStd = new Edm.Annotations(v, stdName); // used in closure | ||
g_annosArray.push(newAnnosStd); | ||
@@ -282,3 +285,3 @@ let newAnnosAlt = null; // used in closure | ||
if (!newAnnosAlt) { // only create upon insertion of first anno | ||
newAnnosAlt = Edm.Annotations.create(v, altName); | ||
newAnnosAlt = new Edm.Annotations(v, altName); | ||
g_annosArray.push(newAnnosAlt); | ||
@@ -366,3 +369,3 @@ } | ||
function handleTerm(termName, annoValue, context) { | ||
let newAnno = Edm.Annotation.create(v, termName); | ||
let newAnno = new Edm.Annotation(v, termName); | ||
@@ -386,2 +389,7 @@ // termName may contain a qualifier: @UI.FieldGroup#shippingStatus | ||
termTypeName = dictTerm.Type; | ||
// issue warning for usage of experimental Terms, but only once per Term | ||
if (!options.betaMode && dictTerm["$experimental"] && !g_experimental_terms[termNameWithoutQualifiers]) { | ||
warningMessage(context, termNameWithoutQualifiers + " is experimental and can be changed or removed at any time, do not use productively!"); | ||
g_experimental_terms[termNameWithoutQualifiers] = true; | ||
} | ||
} | ||
@@ -420,7 +428,4 @@ else { | ||
let typeName = "Path"; | ||
if (dTypeName == "Edm.PropertyPath" || | ||
dTypeName == "Edm.AnnotationPath" || | ||
dTypeName == "Edm.NavigationPropertyPath") { | ||
if( ['Edm.AnnotationPath', 'Edm.ModelElementPath', 'Edm.NavigationPropertyPath', 'Edm.PropertyPath', 'Edm.Path' ].includes(dTypeName) ) | ||
typeName = dTypeName.split('.')[1]; | ||
} | ||
@@ -456,76 +461,159 @@ let val = expr; | ||
if(dTypeName == 'Edm.PrimitiveType') | ||
dTypeName = undefined; | ||
if (typeof val === 'string') { | ||
if (dTypeName == "Edm.Boolean") { | ||
if (val == "true" || val == "false") { | ||
typeName = "Bool"; | ||
} | ||
else { | ||
warningMessage(context, "found String, but expected type " + dTypeName); | ||
} | ||
// https://github.com/oasis-tcs/odata-abnf/blob/master/abnf/odata-abnf-construction-rules.txt#L923 | ||
let isBool = ['true', 'false'].includes(val); | ||
let isNum = /^((\+|-)?\d+(.\d+)?(e(\+|-)?\d+)?[fFmMdD]?|NaN|-?INF)$/.test(val); | ||
switch(dTypeName) { | ||
case 'Edm.Boolean': | ||
typeName = 'Bool'; | ||
if(!isBool) { | ||
warningMessage(context, "found String, but expected type " + dTypeName); | ||
} | ||
break; | ||
case 'Edm.Double': | ||
case 'Edm.Single': | ||
typeName = 'Float'; | ||
if(!isNum) { | ||
warningMessage(context, "found non-numeric string, but expected type " + dTypeName); | ||
} | ||
break; | ||
case 'Edm.Decimal': | ||
typeName = 'Decimal'; | ||
if(!isNum) { | ||
warningMessage(context, "found non-numeric string, but expected type " + dTypeName); | ||
} | ||
break; | ||
default: | ||
if(dTypeName) { | ||
if(isComplexType(dTypeName)) { | ||
warningMessage(context, "found String, but expected complex type " + dTypeName); | ||
} | ||
else if(isEnumType(dTypeName)) { | ||
warningMessage(context, "found String, but expected enum type " + dTypeName); | ||
typeName = "EnumMember"; | ||
} | ||
else if(dTypeName.startsWith('Edm.')) { | ||
typeName = dTypeName.substring(4); | ||
} | ||
else { | ||
// TODO | ||
//warningMessage(context, "type is not yet handled: found String, expected type: " + dTypeName); | ||
} | ||
} | ||
else { // no dTypeName, best guessing | ||
if(isBool) { | ||
typeName = 'Bool'; | ||
dTypeName = 'Edm.Boolean'; | ||
} | ||
else if(isNum) { | ||
typeName = 'Int'; | ||
if(/^(\+|-)?\d{1,19}$/.test(val)) { | ||
let n = parseInt(val); | ||
if (n >= -32768 && n <= 32767) { | ||
dTypeName = 'Edm.Int16'; | ||
} else if(n >= -2147483648 && n <= 2147483647) { | ||
dTypeName = 'Edm.Int32'; | ||
} else { | ||
dTypeName = 'Edm.Int64'; | ||
} | ||
} | ||
else { | ||
typeName = 'Decimal'; | ||
dTypeName = 'Edm.Decimal'; | ||
} | ||
} | ||
else { | ||
//8HEXDIG "-" 4HEXDIG "-" 4HEXDIG "-" 4HEXDIG "-" 12HEXDIG | ||
if(/^[a-fA-F\d]{8}-[a-fA-F\d]{4}-[a-fA-F\d]{4}-[a-fA-F\d]{4}-[a-fA-F\d]{12}$/.test(val)) { | ||
typeName = 'Guid'; | ||
dTypeName = 'Edm.Guid'; | ||
} | ||
else if(/^(\+|-)?P(\d+D)?(T(\d+H)?(\d+M)?(\d+(\.\d+)?S)?)?$/.test(val)) { | ||
typeName = 'Duration'; | ||
dTypeName = 'Edm.Duration'; | ||
} | ||
// Date Matchgroup 1 ^((\d{1,4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01])) | ||
// TimeOffset Matchgroup 5 (T([0-1]\d|2[[0-3])(:|%3[aA])[05]\d(:|%3[aA])[05]\d(\.\d{1,12})?(Z|-?([0-1]\d|2[[0-3])(:|%3[aA])[05]\d)?)?)$ | ||
else { | ||
let m = val.match(/^((\d{1,4}-(0[1-9]|1[0-2])-(0[1-9]|[12]\d|3[01]))(T([0-1]\d|2[[0-3])(:|%3[aA])[05]\d(:|%3[aA])[05]\d(\.\d{1,12})?(Z|-?([0-1]\d|2[[0-3])(:|%3[aA])[05]\d)?)?)$/); | ||
if(m) { | ||
if(m[5]) { | ||
typeName = 'DateTimeOffset'; | ||
dTypeName = 'Edm.DateTimeOffset'; | ||
} | ||
else { | ||
typeName = 'Date'; | ||
dTypeName = 'Edm.Date' | ||
} | ||
} | ||
else { | ||
typeName = 'String'; | ||
dTypeName = 'Edm.String'; | ||
} | ||
} | ||
} | ||
} | ||
} | ||
else if (dTypeName == "Edm.Decimal") { | ||
if (isNaN(val) || isNaN(parseFloat(val))) { | ||
warningMessage(context, "found non-numeric string, but expected type " + dTypeName); | ||
} | ||
else { | ||
typeName = "Decimal"; | ||
} | ||
} | ||
else if (dTypeName == "Edm.Double" || dTypeName == "Edm.Single") { | ||
if (isNaN(val) || isNaN(parseFloat(val))) { | ||
warningMessage(context, "found non-numeric string, but expected type " + dTypeName); | ||
} | ||
else { | ||
typeName = "Float"; | ||
} | ||
} | ||
else if (isComplexType(dTypeName)) { | ||
warningMessage(context, "found String, but expected complex type " + dTypeName); | ||
} | ||
else if (isEnumType(dTypeName)) { | ||
warningMessage(context, "found String, but expected enum type " + dTypeName); | ||
typeName = "EnumMember"; | ||
} | ||
else if (dTypeName && dTypeName.startsWith("Edm.") && dTypeName !== "Edm.PrimitiveType") { | ||
typeName = dTypeName.substring(4); | ||
} | ||
else { | ||
// TODO | ||
//warningMessage(context, "type is not yet handled: found String, expected type: " + dTypeName); | ||
} | ||
} | ||
else if (typeof val === 'boolean') { | ||
typeName = "Bool"; | ||
if (dTypeName == "Edm.Boolean") { | ||
val = val ? "true" : "false"; | ||
if(dTypeName == undefined) | ||
dTypeName = 'Edm.Boolean'; | ||
switch(dTypeName) { | ||
case 'Edm.Boolean': | ||
typeName = 'Bool'; | ||
break; | ||
case 'Edm.String': | ||
val = val.toString(); | ||
break; | ||
default: | ||
warningMessage(context, "found Boolean, but expected type " + dTypeName); | ||
break; | ||
} | ||
else if (dTypeName == "Edm.String") { | ||
typeName = "String"; | ||
} | ||
else { | ||
warningMessage(context, "found Boolean, but expected type " + dTypeName); | ||
} | ||
} | ||
else if (typeof val === 'number') { | ||
if (isComplexType(dTypeName)) { | ||
warningMessage(context, "found number, but expected complex type " + dTypeName); | ||
if(dTypeName == undefined) { | ||
if(Number.isInteger(val)) { | ||
if (val >= -32768 && val <= 32767) { | ||
dTypeName = 'Edm.Int16'; | ||
} else if(val >= -2147483648 && val <= 2147483647) { | ||
dTypeName = 'Edm.Int32'; | ||
} else { | ||
dTypeName = 'Edm.Int64'; | ||
} | ||
} | ||
else { | ||
dTypeName = 'Edm.Decimal'; | ||
} | ||
} | ||
else if (dTypeName === 'Edm.String') { | ||
typeName = "String"; | ||
switch(dTypeName) { | ||
case 'Edm.String': | ||
typeName = 'String'; | ||
break; | ||
case 'Edm.Single': | ||
case 'Edm.Double': | ||
typeName = 'Float'; | ||
break; | ||
case 'Edm.Decimal': | ||
typeName = 'Decimal'; | ||
break; | ||
case 'Edm.Int16': | ||
case 'Edm.Int32': | ||
case 'Edm.Int64': | ||
case 'Edm.Byte': | ||
case 'Edm.SByte': | ||
typeName = 'Int'; | ||
break; | ||
default: | ||
if(isComplexType(dTypeName)) { | ||
warningMessage(context, "found number, but expected complex type " + dTypeName); | ||
} | ||
else { | ||
// all others (Paths, Enums) | ||
warningMessage(context, "found number, but expected type " + dTypeName); | ||
} | ||
} | ||
else if (dTypeName == "Edm.PropertyPath") { | ||
warningMessage(context, "found number, but expected " + dTypeName); | ||
} | ||
else if (dTypeName == "Edm.Boolean") { | ||
warningMessage(context, "found number, but expected type " + dTypeName); | ||
} | ||
else if (dTypeName == "Edm.Decimal") { | ||
typeName = "Decimal"; | ||
} | ||
else if (dTypeName == "Edm.Double") { | ||
typeName = "Float"; | ||
} | ||
else { | ||
typeName = Number.isInteger(val) ? 'Int' : 'Float'; | ||
} | ||
} | ||
@@ -536,4 +624,13 @@ else { | ||
if(typeName == undefined) | ||
throw Error('Please debug me: typeName unset for value: ' + val); | ||
if(dTypeName == undefined) | ||
throw Error('Please debug me: dtypeName unset for value: ' + val); | ||
if( ['Edm.AnnotationPath', 'Edm.ModelElementPath', 'Edm.NavigationPropertyPath', 'Edm.PropertyPath', 'Edm.Path' ].includes(dTypeName) ) | ||
dTypeName = dTypeName.split('.')[1]; | ||
return { | ||
name : typeName, | ||
jsonName: dTypeName, | ||
value : val | ||
@@ -558,4 +655,4 @@ }; | ||
checkMultiEnumValue(cAnnoValue, dTypeName, context); | ||
oTarget.setJSON({ "EnumMember@odata.type" : '#'+dTypeName, EnumMember: generateMultiEnumValue(cAnnoValue, dTypeName, false) }); | ||
oTarget.setXml({ "EnumMember": generateMultiEnumValue(cAnnoValue, dTypeName, true) }); | ||
oTarget.setJSON({ "EnumMember": generateMultiEnumValue(cAnnoValue, dTypeName, false), "EnumMember@odata.type" : '#'+dTypeName }); | ||
oTarget.setXml( { "EnumMember": generateMultiEnumValue(cAnnoValue, dTypeName, true) }); | ||
} | ||
@@ -573,5 +670,6 @@ else | ||
let res = handleExpression(cAnnoValue["="], dTypeName, context); | ||
oTarget[res.name] = res.value; | ||
oTarget.setXml( { [res.name] : res.value }); | ||
oTarget.setJSON( { [res.name] : res.value }); | ||
} | ||
else if (cAnnoValue["#"] != undefined) | ||
else if (cAnnoValue["#"] !== undefined) | ||
{ | ||
@@ -581,12 +679,12 @@ if (dTypeName) | ||
checkEnumValue(cAnnoValue["#"], dTypeName, context); | ||
oTarget.setJSON({ "EnumMember@odata.type" : '#'+dTypeName, EnumMember: cAnnoValue["#"] }); | ||
oTarget.setXml({ "EnumMember": dTypeName + "/" + cAnnoValue["#"] }); | ||
oTarget.setJSON({ "EnumMember": cAnnoValue["#"], "EnumMember@odata.type" : '#'+dTypeName, }); | ||
oTarget.setXml( { "EnumMember": dTypeName + "/" + cAnnoValue["#"] }); | ||
} | ||
else | ||
{ | ||
oTarget.setJSON({ "EnumMember@odata.type" : '#'+oTermName + "Type/", EnumMember: cAnnoValue["#"] }); | ||
oTarget.setXml({ "EnumMember": oTermName + "Type/" + "/" + cAnnoValue["#"] }); | ||
oTarget.setJSON({ "EnumMember": cAnnoValue["#"], "EnumMember@odata.type" : '#'+oTermName + "Type/" }); | ||
oTarget.setXml( { "EnumMember": oTermName + "Type/" + "/" + cAnnoValue["#"] }); | ||
} | ||
} | ||
else if (cAnnoValue["$value"]) { | ||
else if (cAnnoValue["$value"] !== undefined) { | ||
// "pseudo-structure" used for annotating scalar annotations | ||
@@ -615,3 +713,4 @@ handleValue(cAnnoValue["$value"], oTarget, oTermName, dTypeName, context); | ||
let res = handleSimpleValue(cAnnoValue, dTypeName, context); | ||
oTarget[res.name] = res.value; | ||
oTarget.setXml( { [res.name] : res.value }); | ||
oTarget.setJSON( { [res.jsonName] : res.value }); | ||
} | ||
@@ -662,3 +761,3 @@ } | ||
function generateRecord(obj, termName, dictRecordTypeName, context) { | ||
let newRecord = Edm.Record.create(v); | ||
let newRecord = new Edm.Record(v); | ||
let actualTypeName = null; | ||
@@ -738,3 +837,3 @@ | ||
let newPropertyValue = Edm.PropertyValue.create(v, i); | ||
let newPropertyValue = new Edm.PropertyValue(v, i); | ||
handleValue(obj[i], newPropertyValue, termName, dictPropertyTypeName, context); | ||
@@ -755,3 +854,3 @@ newRecord.append(newPropertyValue); | ||
function generateCollection(annoValue, termName, dTypeName, context) { | ||
let newCollection = Edm.Collection.create(v); | ||
let newCollection = new Edm.Collection(v); | ||
@@ -779,3 +878,4 @@ let innerTypeName = null; | ||
let res = handleExpression(value["="], innerTypeName, context); | ||
let newPropertyPath = Edm.ValueThing.create(v, res.name, res.value ); | ||
let newPropertyPath = new Edm.ValueThing(v, res.name, res.value ); | ||
newPropertyPath.setJSON( { [res.name] : res.value } ); | ||
newCollection.append(newPropertyPath); | ||
@@ -793,3 +893,4 @@ } | ||
let res = handleSimpleValue(value, innerTypeName, context); | ||
let newThing = Edm.ValueThing.create(v, res.name, value ); | ||
let newThing = new Edm.ValueThing(v, res.name, value ); | ||
newThing.setJSON( { [res.jsonName] : res.value }); | ||
newCollection.append(newThing); | ||
@@ -817,3 +918,3 @@ } | ||
let k = Object.keys(obj)[0]; | ||
return Edm.ValueThing.create(v, k.slice(1), obj[k] ); | ||
return new Edm.ValueThing(v, k.slice(1), obj[k] ); | ||
} | ||
@@ -825,3 +926,3 @@ warningMessage(context, "edmJson code contains no special property out of: " + specialProperties); | ||
// name of special property determines element kind | ||
let newElem = Edm.Thing.create(v, subset[0].slice(1)); | ||
let newElem = new Edm.Thing(v, subset[0].slice(1)); | ||
let mainAttribute = null; | ||
@@ -857,3 +958,3 @@ | ||
} | ||
newElem.append(Edm.ValueThing.create(v, getTypeName(a), a)); | ||
newElem.append(new Edm.ValueThing(v, getTypeName(a), a)); | ||
} | ||
@@ -903,5 +1004,4 @@ } | ||
// filter function, assumed to be used for array of string | ||
// accepts those strings that start with a knwon vocabulary name | ||
// accepts those strings that start with a known vocabulary name | ||
function filterKnownVocabularies(name) { | ||
let knownVocabularies = ['Analytics', 'Core', 'Common', 'UI', 'Communication', 'Capabilities', 'Measures']; | ||
var match = name.match(/^(@)(\w+)/); | ||
@@ -973,2 +1073,2 @@ if (match == null) return false; | ||
module.exports = { csn2annotationEdm }; | ||
module.exports = { knownVocabularies, csn2annotationEdm }; |
@@ -276,4 +276,14 @@ 'use strict'; | ||
// expand shortcut form of ValueList annotation | ||
if (aNameWithoutQualifier == "@Common.ValueList.entity") { | ||
if (aNameWithoutQualifier == "@Common.ValueList.entity" || | ||
aNameWithoutQualifier == "@Common.ValueList.viaAssociation") { | ||
try { | ||
// note: we loop over all annotations that were originally present, even if they are | ||
// removed from the carrier via this handler | ||
// we don't remove anything from the array "annoNames" | ||
if (aNameWithoutQualifier == "@Common.ValueList.viaAssociation" && !options.betaMode) { | ||
signal(warning`annotation preprocessing: shortcut annotation ${aNameWithoutQualifier} is only available in beta-mode, ${ctx}`); | ||
throw 'leave'; | ||
} | ||
// if CollectionPath is explicitly given, no shortcut expansion is made | ||
@@ -284,14 +294,50 @@ if (carrier["@Common.ValueList.CollectionPath"]) { | ||
if (carrier.kind === 'entity' || carrier.kind === 'view') { | ||
signal(warning`annotation preprocessing/${aNameWithoutQualifier}: annotation must not be used for an entity, ${ctx}`); | ||
throw 'leave'; | ||
} | ||
// check on "type"? e.g. if present, it must be #fixed ... ? | ||
// the value list entity | ||
let entityName = carrier["@Common.ValueList.entity"]; // name of value list entity | ||
if (entityName["="]) { | ||
signal(warning`in annotation preprocessing/value help shortcut: 'entity' must be a string, ${ctx}`); | ||
// value list entity | ||
let enameShort = null; // (string) name of value list entity, short (i.e. name within service) | ||
let enameFull = null; // (string) name of value list entity, fully qualified name | ||
if (aNameWithoutQualifier == "@Common.ValueList.viaAssociation") { | ||
// value is expected to be an expression, namely the path to an association of the carrier entity | ||
let assocName = carrier["@Common.ValueList.viaAssociation"]['=']; | ||
if (!assocName) { | ||
signal(warning`in annotation preprocessing/${aNameWithoutQualifier}: value of 'viaAssociation' must be a path, ${ctx}`); | ||
throw 'leave'; | ||
} | ||
let assoc = csn.definitions[art].elements[assocName]; | ||
if (!assoc || !(assoc.type === 'cds.Association' || assoc.type === 'cds.Composition')) { | ||
signal(warning`in annotation preprocessing/${aNameWithoutQualifier}: there is no association "${assocName}", ${ctx}`); | ||
throw 'leave'; | ||
} | ||
enameFull = assoc.target.name || assoc.target; // full name | ||
enameShort = enameFull.split('.').pop(); | ||
} | ||
let ename = entityName["="] || entityName; | ||
let nameprefix = art.replace(/.[^.]+$/, ''); // better way of getting the service name? | ||
let vlEntity = csn.definitions[nameprefix + '.' + ename]; | ||
else if (aNameWithoutQualifier == "@Common.ValueList.entity") { | ||
// if both annotations are present, ignore 'entity' and raise a message | ||
if (annoNames.map(x=>x.split("#")[0]).find(x=>(x=="@Common.ValueList.viaAssociation"))) { | ||
signal(warning`in annotation preprocessing/@Common.ValueList: 'entity' is ignored, as 'viaAssociation' is present, ${ctx}`); | ||
throw "leave"; | ||
} | ||
let annoVal = carrier["@Common.ValueList.entity"]; // name of value list entity | ||
if (annoVal["="]) { | ||
signal(warning`in annotation preprocessing/${aNameWithoutQualifier}: annotation value must be a string, ${ctx}`); | ||
} | ||
let nameprefix = art.replace(/.[^.]+$/, ''); // better way of getting the service name? | ||
enameShort = annoVal["="] || annoVal; | ||
enameFull = nameprefix + '.' + enameShort; | ||
} | ||
let vlEntity = csn.definitions[enameFull]; // (object) value list entity | ||
if (!vlEntity) { | ||
signal(warning`in annotation preprocessing/value help shortcut: entity ${ename} does not exist, ${ctx}`); | ||
signal(warning`in annotation preprocessing/${aNameWithoutQualifier}: entity "${enameFull}" does not exist, ${ctx}`); | ||
throw "leave"; | ||
@@ -302,5 +348,5 @@ } | ||
// explicitly provided label wins | ||
// TODO: once TnT excpetion for nonexisting vlEntity is removed, simplify condition | ||
// TODO: once TnT exception for nonexisting vlEntity is removed, simplify condition | ||
let label = carrier["@Common.ValueList.Label"] || | ||
carrier["@Common.Label"] || (vlEntity && vlEntity["@Common.Label"]) || ename; | ||
carrier["@Common.Label"] || (vlEntity && vlEntity["@Common.Label"]) || enameShort; | ||
@@ -320,7 +366,7 @@ // localDataProp | ||
if (keys.length == 0) { | ||
signal(warning`in annotation preprocessing/value help shortcut: entity ${ename} has no key, ${ctx}`); | ||
signal(warning`in annotation preprocessing/value help shortcut: entity "${enameFull}" has no key, ${ctx}`); | ||
throw "leave"; | ||
} | ||
else if (keys.length > 1) | ||
signal(warning`in annotation preprocessing/value help shortcut: entity ${ename} has more than one key, ${ctx}`); | ||
signal(warning`in annotation preprocessing/value help shortcut: entity "${enameFull}" has more than one key, ${ctx}`); | ||
valueListProp = keys[0]; | ||
@@ -366,5 +412,5 @@ | ||
for (let e in carrier) { | ||
if (e == "@Common.ValueList.entity") { | ||
if (e == "@Common.ValueList.entity" || e == "@Common.ValueList.viaAssociation") { | ||
newObj["@Common.ValueList.Label"] = label; | ||
newObj["@Common.ValueList.CollectionPath"] = entityName; | ||
newObj["@Common.ValueList.CollectionPath"] = enameShort; | ||
newObj["@Common.ValueList.Parameters"] = parameters; | ||
@@ -393,3 +439,3 @@ if (textField && options && options.tntFlavor) { | ||
// avoid subsequent warnings | ||
delete carrier["@Common.ValueList.entity"]; | ||
delete carrier[aNameWithoutQualifier]; | ||
delete carrier["@Common.ValueList.type"]; | ||
@@ -396,0 +442,0 @@ } |
@@ -25,3 +25,3 @@ 'use strict'; | ||
module.exports = function csn2edm(csn, _options) { | ||
module.exports = function csn2edm(csn, serviceName, _options) { | ||
@@ -36,15 +36,18 @@ const options = glue.validateOptions(_options); | ||
// Remove from the model all definitions that do not belong to the specified service | ||
// TODO maybe this can be combined with initializeModel | ||
if (serviceName) { | ||
let allDefinitions = model.definitions; | ||
let namesNotInService = Object.keys(allDefinitions).filter(n => !n.startsWith(serviceName + '.') && n != serviceName); | ||
for (let name of namesNotInService) { | ||
delete model.definitions[name]; | ||
} | ||
} | ||
let serviceCsn = glue.initializeModel(model, options); | ||
if(serviceCsn == undefined) | ||
throw "No Service found in model" | ||
throw Error('No Service found in model'); | ||
let navigationProperties = []; | ||
// FIXME: Temporary special handling for TNT | ||
if (options && options.tntFlavor) | ||
{ | ||
if(options.oldstyleSelf) | ||
Edm.NavigationProperty.OLDSTYLE_SELF='self'; | ||
} | ||
function baseName(str, del) { let l = str.lastIndexOf(del); // eslint-disable-line no-unused-vars | ||
@@ -59,3 +62,3 @@ return (l >= 0) ? str.slice(l+del.length, str.length) : str; } | ||
let Schema = Edm.Schema.create(v, serviceCsn.name, undefined /* unset alias */); | ||
let Schema = new Edm.Schema(v, serviceCsn.name, undefined /* unset alias */); | ||
@@ -66,4 +69,4 @@ // now namespace and alias are used to create the fullQualified(name) | ||
let service = Edm.DataServices.create(v, Schema); | ||
let edm = Edm.create(v, service); | ||
let service = new Edm.DataServices(v, Schema); | ||
let edm = new Edm(v, service); | ||
@@ -109,3 +112,3 @@ /* create the entitytypes and sets | ||
if(Schema._ec._children.length == 0) | ||
throw "EntityContainer must contain at least one EntitySet" | ||
throw Error('EntityContainer must contain at least one EntitySet'); | ||
@@ -136,7 +139,7 @@ return edm | ||
Schema.append(Edm.EntityType.create(v, attributes, properties, entityCsn)); | ||
Schema.append(new Edm.EntityType(v, attributes, properties, entityCsn)); | ||
if (createEntitySet) | ||
{ | ||
let entitySet = Edm.EntitySet.create(v, { Name: EntitySetName, EntityType: fqEntityTypeName }, entityCsn); | ||
let entitySet = new Edm.EntitySet(v, { Name: EntitySetName, EntityType: fqEntityTypeName }, entityCsn); | ||
@@ -170,4 +173,4 @@ // V4: Create NavigationPropertyBinding in EntitySet if NavigationProperty is not a Containment | ||
let actionNode = (iAmAnAction) ? Edm.Action.create(v, attributes) | ||
: Edm.FunctionDefinition.create(v, attributes); | ||
let actionNode = (iAmAnAction) ? new Edm.Action(v, attributes) | ||
: new Edm.FunctionDefinition(v, attributes); | ||
@@ -178,3 +181,3 @@ if(entityCsn != undefined) | ||
// Binding Parameter: 'in' at first position in sequence, this is decisive! | ||
actionNode.append(Edm.Parameter.create(v, { Name: "in", Type: fullQualified(entityCsn.name) }, {} )); | ||
actionNode.append(new Edm.Parameter(v, { Name: "in", Type: fullQualified(entityCsn.name) }, {} )); | ||
} | ||
@@ -184,4 +187,4 @@ else // unbound => produce Action/FunctionImport | ||
let actionImport = iAmAnAction | ||
? Edm.ActionImport.create(v, { Name: actionName, Action : fullQualified(actionName) }) | ||
: Edm.FunctionImport.create(v, { Name: actionName, Function : fullQualified(actionName) }); | ||
? new Edm.ActionImport(v, { Name: actionName, Action : fullQualified(actionName) }) | ||
: new Edm.FunctionImport(v, { Name: actionName, Function : fullQualified(actionName) }); | ||
@@ -202,3 +205,3 @@ let rt = actionCsn.returns && (actionCsn.returns.type || actionCsn.returns.items.type); | ||
glue.forAll(actionCsn.params, (parameterCsn, parameterName) => { | ||
actionNode.append(Edm.Parameter.create(v, { Name: parameterName }, parameterCsn )); | ||
actionNode.append(new Edm.Parameter(v, { Name: parameterName }, parameterCsn )); | ||
}); | ||
@@ -208,3 +211,3 @@ | ||
if(actionCsn.returns) { | ||
actionNode._returnType = Edm.ReturnType.create(v, actionCsn.returns, fullQualified); | ||
actionNode._returnType = new Edm.ReturnType(v, actionCsn.returns, fullQualified); | ||
// if binding type matches return type add attribute EntitySetPath | ||
@@ -221,3 +224,3 @@ if(entityCsn && fullQualified(entityCsn.name) === actionNode._returnType._type) { | ||
{ | ||
let functionImport = Edm.FunctionImport.create(v, { Name: name.replace(namespace, '') } ); | ||
let functionImport = new Edm.FunctionImport(v, { Name: name.replace(namespace, '') } ); | ||
@@ -253,3 +256,3 @@ // inserted now to maintain attribute order with old odata generator... | ||
else | ||
throw "Please debug me: Neither function nor action"; | ||
throw Error('Please debug me: Neither function nor action'); | ||
@@ -267,3 +270,3 @@ if(entityCsn != undefined) | ||
(elementCsn, elementName) => { | ||
functionImport.append(Edm.Parameter.create(v, { Name: elementName }, elementCsn, 'In' )); | ||
functionImport.append(new Edm.Parameter(v, { Name: elementName }, elementCsn, 'In' )); | ||
} | ||
@@ -281,5 +284,5 @@ ); | ||
// V2 XML spec does only mention default Nullable=true for Properties not for Parameters so omitting Nullable=true let | ||
// the client assume that Nullable is false.... Correct Nullable Handling is done inside Parameter.create() | ||
// the client assume that Nullable is false.... Correct Nullable Handling is done inside Parameter constructor | ||
glue.forAll(actionCsn.params, (parameterCsn, parameterName) => { | ||
functionImport.append(Edm.Parameter.create(v, { Name: parameterName }, parameterCsn, 'In' )); | ||
functionImport.append(new Edm.Parameter(v, { Name: parameterName }, parameterCsn, 'In' )); | ||
}); | ||
@@ -312,3 +315,4 @@ | ||
{ | ||
setProp(elementCsn, '_parent', parentCsn); | ||
if(elementCsn._parent == undefined) | ||
setProp(elementCsn, '_parent', parentCsn); | ||
@@ -326,3 +330,3 @@ if(glue.isAssociationOrComposition(elementCsn)) | ||
{ | ||
let navProp = Edm.NavigationProperty.create(v, { | ||
let navProp = new Edm.NavigationProperty(v, { | ||
Name: elementName, | ||
@@ -341,3 +345,3 @@ Type: fullQualified(elementCsn.target.name) | ||
let anonymousComplexType = createAnonymousComplexType(elementCsn, prefix); | ||
props.push(Edm.Property.create(v, { | ||
props.push(new Edm.Property(v, { | ||
Name: elementCsn.name, | ||
@@ -360,3 +364,3 @@ Type: fullQualified(anonymousComplexType.Name) | ||
!(options.isV4() && isContainerAssoc)) | ||
props.push(Edm.Property.create(v, { Name: elementName }, elementCsn)); | ||
props.push(new Edm.Property(v, { Name: elementName }, elementCsn)); | ||
else | ||
@@ -399,3 +403,3 @@ { | ||
let complexType = Edm.ComplexType.create(v, attributes, structuredTypeCsn); | ||
let complexType = new Edm.ComplexType(v, attributes, structuredTypeCsn); | ||
let elementsCsn = structuredTypeCsn.items || structuredTypeCsn; | ||
@@ -419,5 +423,5 @@ complexType.append(...(createProperties(elementsCsn)[0])); | ||
if((typeCsn.items && typeCsn.items.enum) || typeCsn.enum) | ||
typeDef = Edm.EnumType.create(v, props, typeCsn); | ||
typeDef = new Edm.EnumType(v, props, typeCsn); | ||
else | ||
typeDef = Edm.TypeDefinition.create(v, props, typeCsn ); | ||
typeDef = new Edm.TypeDefinition(v, props, typeCsn ); | ||
Schema.append(typeDef); | ||
@@ -481,3 +485,3 @@ } | ||
1) Counterpart NavigationProperty exists and is responsible to create the edm:Association element which needs to | ||
be reused by this backlink association. | ||
be reused by this backlink association. This is save because at this point of the processing all NavProps are created. | ||
2) Counterpart NavigationProperty does not exist (@odata.navigable:false), then the missing edm:Association element | ||
@@ -488,3 +492,4 @@ of the origin association needs to be created as if it would have been already available in case (1). | ||
let reuseAssoc = false; | ||
if(constraints._originAssocCsn) | ||
let forwardAssocCsn = constraints._originAssocCsn; | ||
if(forwardAssocCsn) | ||
{ | ||
@@ -495,14 +500,36 @@ // This is a backlink, swap the roles and types, rewrite assocName | ||
parentName = constraints._originAssocCsn._parent.name.replace(namespace, ''); | ||
assocName = parentName + NAVPROP_TRENNER + constraints._originAssocCsn.name.replace(VALUELIST_NAVPROP_PREFIX, ''); | ||
parentName = forwardAssocCsn._parent.name.replace(namespace, ''); | ||
assocName = parentName + NAVPROP_TRENNER + forwardAssocCsn.name.replace(VALUELIST_NAVPROP_PREFIX, ''); | ||
navigationProperty.Relationship = fullQualified(assocName) | ||
reuseAssoc = !!constraints._originAssocCsn._NavigationProperty; | ||
constraints = navigationProperty.getReferentialConstraints(constraints._originAssocCsn); | ||
reuseAssoc = !!forwardAssocCsn._NavigationProperty; | ||
constraints = navigationProperty.getReferentialConstraints(forwardAssocCsn); | ||
} | ||
if(reuseAssoc) | ||
{ | ||
// Example: | ||
// entity E { key id: Integer; toF: association to F; }; | ||
// entity F { key id: Integer; toE: composition of E on toE.toF = $self; }; | ||
// | ||
// Consider we're in NavigationProperty 'toE' which is the backlink to F. | ||
// Then forwardAssocCsn is 'E_toF' with two Ends: E, F. | ||
// Backlink F.toE is a composition, making E existentially dependant on F. | ||
// So End E of Association E_toF (which is End[0]) receives Edm.OnDelete. | ||
// Depending on the order of the navigation properties it might be that the | ||
// forward Edm.Association has not yet been produced. In this case Edm.OnDelete | ||
// is parked at the forward NavigationProperty. | ||
if(!forwardAssocCsn._NavigationProperty._edmAssociation && navigationProperty._csn.type == 'cds.Composition') | ||
{ | ||
// TODO: to be specified via @sap.on.delete | ||
forwardAssocCsn._NavigationProperty.set( { _OnDeleteSrcEnd: new Edm.OnDelete(v, { Action: 'Cascade' }) } ); | ||
} | ||
return; | ||
} | ||
// create Association and AssociationSet if this is not a backlink association | ||
let association = Edm.Association.create(v, { Name: assocName }, | ||
// Create Association and AssociationSet if this is not a backlink association. | ||
// Store association at navigation property because in case the Ends must be modified | ||
// later by the partner (backlink) association | ||
navigationProperty._edmAssociation = new Edm.Association(v, { Name: assocName }, navigationProperty, | ||
[ fromRole, fullQualified(fromEntityType) ], | ||
@@ -514,8 +541,8 @@ [ toRole, fullQualified(toEntityType) ], | ||
if(Object.keys(constraints.constraints).length > 0) | ||
association.append(Edm.ReferentialConstraint.createV2(v, | ||
navigationProperty._edmAssociation.append(Edm.ReferentialConstraint.createV2(v, | ||
fromRole, toRole, constraints.constraints)); | ||
Schema.append(association); | ||
Schema.append(navigationProperty._edmAssociation); | ||
let assocSet = Edm.AssociationSet.create(v, { Name: assocName, Association: fullQualified(assocName) }, | ||
let assocSet = new Edm.AssociationSet(v, { Name: assocName, Association: fullQualified(assocName) }, | ||
fromRole, toRole, fromEntityType, toEntityType); | ||
@@ -522,0 +549,0 @@ Schema._ec.append(assocSet); |
@@ -5,17 +5,12 @@ 'use strict' | ||
class Node { | ||
static create(v, details=Object.create(null), csn=undefined) | ||
{ | ||
return new this(v, details, csn); | ||
} | ||
class Node | ||
{ | ||
constructor(v, attributes=Object.create(null), csn=undefined) | ||
{ | ||
if(!attributes || typeof attributes !== 'object') | ||
throw "Please debug me: attributes must be a dictionary" | ||
throw Error('Please debug me: attributes must be a dictionary'); | ||
if(!(v instanceof Array)) | ||
throw "Please debug me: v is either undefined or not an array: " + v; | ||
throw Error('Please debug me: v is either undefined or not an array: ' + v); | ||
if(v.filter(v=>v).length != 1) | ||
throw "Please debug me: exactly one version must be set" | ||
throw Error('Please debug me: exactly one version must be set'); | ||
Object.assign(this, attributes); | ||
@@ -39,3 +34,3 @@ this.set({ _children: [], _xmlOnlyAttributes: Object.create(null), _jsonOnlyAttributes: Object.create(null), _v: v }); | ||
if(!attributes || typeof attributes !== 'object') | ||
throw "Please debug me: attributes must be a dictionary" | ||
throw Error('Please debug me: attributes must be a dictionary'); | ||
let newAttributes = Object.create(null); | ||
@@ -55,3 +50,3 @@ for (let p in attributes) newAttributes[p] = { | ||
if(!attributes || typeof attributes !== 'object') | ||
throw "Please debug me: attributes must be a dictionary" | ||
throw Error('Please debug me: attributes must be a dictionary'); | ||
return Object.assign(this._xmlOnlyAttributes, attributes); | ||
@@ -65,3 +60,3 @@ } | ||
if(!attributes || typeof attributes !== 'object') | ||
throw "Please debug me: attributes must be a dictionary" | ||
throw Error('Please debug me: attributes must be a dictionary'); | ||
return Object.assign(this._jsonOnlyAttributes, attributes); | ||
@@ -82,3 +77,3 @@ } | ||
// $kind Property MAY be omitted in JSON for performance reasons | ||
if(this.kind != 'Property') | ||
if(![ 'Property', 'EntitySet', 'ActionImport', 'FunctionImport', 'Singleton', 'Schema' ].includes(this.kind)) | ||
json['$Kind'] = this.kind; | ||
@@ -145,4 +140,7 @@ | ||
function escapeString(s) { | ||
return (typeof s === 'string') ? s.replace(/"/g, '"') : s; | ||
function escapeString(s) | ||
{ | ||
// first regex: replace & if not followed by apos; or quot; or gt; or lt; or amp; or # | ||
// Do not escape > as it is a marker for {bi18n>...} translated string values | ||
return (typeof s === 'string') ? s.replace(/&(?!(?:apos|quot|[gl]t|amp);|#)/g, '&')./*.replace(/>/g, '>').*/replace(/</g, '<').replace(/"/g, '"') : s; | ||
} | ||
@@ -174,10 +172,9 @@ } | ||
{ | ||
static | ||
create(v, details) | ||
constructor(v, details) | ||
{ | ||
let node = super.create(v, details); | ||
if(node.v2) | ||
node['xmlns:edmx'] = 'http://docs.oasis-open.org/odata/ns/edmx'; | ||
return node; | ||
super(v, details); | ||
if(this.v2) | ||
this['xmlns:edmx'] = 'http://docs.oasis-open.org/odata/ns/edmx'; | ||
} | ||
get kind() { return 'edmx:Reference' } | ||
@@ -208,4 +205,3 @@ | ||
{ | ||
static | ||
create(v, ns, alias=undefined, annotations=[], withEntityContainer=true) | ||
constructor(v, ns, alias=undefined, annotations=[], withEntityContainer=true) | ||
{ | ||
@@ -216,5 +212,5 @@ let props = Object.create(null); | ||
props.Alias = alias; | ||
let schema = super.create(v, props); | ||
schema.set( { _annotations: annotations, _actions: {} } ); | ||
schema.setXml( { xmlns: (schema.v2) ? "http://schemas.microsoft.com/ado/2008/09/edm" : "http://docs.oasis-open.org/odata/ns/edm" } ); | ||
super(v, props); | ||
this.set( { _annotations: annotations, _actions: {} } ); | ||
this.setXml( { xmlns: (this.v2) ? "http://schemas.microsoft.com/ado/2008/09/edm" : "http://docs.oasis-open.org/odata/ns/edm" } ); | ||
@@ -224,11 +220,10 @@ if(withEntityContainer) | ||
let ecprops = { Name: 'EntityContainer' }; | ||
let ec = EntityContainer.create(v, ecprops ); | ||
if(schema.v2) | ||
let ec = new EntityContainer(v, ecprops ); | ||
if(this.v2) | ||
ec.setXml( { 'm:IsDefaultEntityContainer': true } ); | ||
// append for rendering, ok ec has Name | ||
schema.append(ec); | ||
this.append(ec); | ||
// set as attribute for later access... | ||
schema.set({ _ec : ec }) | ||
this.set({ _ec : ec }) | ||
} | ||
return schema | ||
} | ||
@@ -272,17 +267,2 @@ | ||
// no $Kind | ||
toJSON() | ||
{ | ||
let json = Object.create(null); | ||
this.toJSONattributes(json); | ||
this.toJSONchildren(json); | ||
glue.forAll(this._actions, (actionArray, actionName) => { | ||
json[actionName] = []; | ||
actionArray.forEach(action => { | ||
json[actionName].push(action.toJSON()); | ||
}); | ||
}); | ||
return json; | ||
} | ||
// no $Namespace | ||
@@ -309,2 +289,9 @@ toJSONattributes(json) | ||
} | ||
glue.forAll(this._actions, (actionArray, actionName) => { | ||
json[actionName] = []; | ||
actionArray.forEach(action => { | ||
json[actionName].push(action.toJSON()); | ||
}); | ||
}); | ||
return json; | ||
@@ -317,10 +304,8 @@ } | ||
{ | ||
static | ||
create(v, schema) | ||
constructor(v, schema) | ||
{ | ||
let node = super.create(v); | ||
node.append(schema); | ||
if(node.v2) | ||
node.setXml( { 'm:DataServiceVersion': '2.0' } ) | ||
return node; | ||
super(v); | ||
this.append(schema); | ||
if(this.v2) | ||
this.setXml( { 'm:DataServiceVersion': '2.0' } ) | ||
} | ||
@@ -350,10 +335,9 @@ | ||
{ | ||
static | ||
create(v, service) | ||
constructor(v, service) | ||
{ | ||
let edm = super.create(v, { Version : (v[1]) ? '4.0' : '1.0' }); | ||
edm.set( { _service: service, _defaultRefs: [] } ); | ||
super(v, { Version : (v[1]) ? '4.0' : '1.0' }); | ||
this.set( { _service: service, _defaultRefs: [] } ); | ||
let xmlProps = Object.create(null); | ||
if(edm.v4) | ||
if(this.v4) | ||
{ | ||
@@ -368,5 +352,3 @@ xmlProps['xmlns:edmx'] = "http://docs.oasis-open.org/odata/ns/edmx"; | ||
} | ||
edm.setXml(xmlProps); | ||
return edm; | ||
this.setXml(xmlProps); | ||
} | ||
@@ -381,33 +363,41 @@ | ||
{ | ||
let r = Reference.create(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1.xml" }); | ||
r.append(Include.create(this._v, {Alias : "Core", Namespace : "Org.OData.Core.V1"} )) | ||
let r = new Reference(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Core.V1.xml" }); | ||
r.append(new Include(this._v, {Alias : "Core", Namespace : "Org.OData.Core.V1"} )) | ||
this._defaultRefs.push(r); | ||
r = Reference.create(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Measures.V1.xml" }); | ||
r.append(Include.create(this._v, {Alias : "Measures", Namespace : "Org.OData.Measures.V1"} )) | ||
r = new Reference(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Measures.V1.xml" }); | ||
r.append(new Include(this._v, {Alias : "Measures", Namespace : "Org.OData.Measures.V1"} )) | ||
this._defaultRefs.push(r); | ||
r = Reference.create(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1.xml" }); | ||
r.append(Include.create(this._v, {Alias : "Capabilities", Namespace : "Org.OData.Capabilities.V1"} )) | ||
r = new Reference(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Capabilities.V1.xml" }); | ||
r.append(new Include(this._v, {Alias : "Capabilities", Namespace : "Org.OData.Capabilities.V1"} )) | ||
this._defaultRefs.push(r); | ||
r = Reference.create(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Aggregation.V1.xml" }); | ||
r.append(Include.create(this._v, {Alias : "Aggregation", Namespace : "Org.OData.Aggregation.V1"} )) | ||
r = new Reference(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Aggregation.V1.xml" }); | ||
r.append(new Include(this._v, {Alias : "Aggregation", Namespace : "Org.OData.Aggregation.V1"} )) | ||
this._defaultRefs.push(r); | ||
r = Reference.create(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/462030211/Analytics.xml?api=v2" }); | ||
r.append(Include.create(this._v, {Alias : "Analytics", Namespace : "com.sap.vocabularies.Analytics.v1"} )) | ||
r = new Reference(this._v, { Uri : "https://oasis-tcs.github.io/odata-vocabularies/vocabularies/Org.OData.Validation.V1.xml" }); | ||
r.append(new Include(this._v, {Alias : "Validation", Namespace : "Org.OData.Validation.V1"} )) | ||
this._defaultRefs.push(r); | ||
r = Reference.create(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/448470974/Common.xml?api=v2" }); | ||
r.append(Include.create(this._v, {Alias : "Common", Namespace : "com.sap.vocabularies.Common.v1"} )) | ||
r = new Reference(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/462030211/Analytics.xml?api=v2" }); | ||
r.append(new Include(this._v, {Alias : "Analytics", Namespace : "com.sap.vocabularies.Analytics.v1"} )) | ||
this._defaultRefs.push(r); | ||
r = Reference.create(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/448470971/Communication.xml?api=v2" }); | ||
r.append(Include.create(this._v, {Alias : "Communication", Namespace : "com.sap.vocabularies.Communication.v1"} )) | ||
r = new Reference(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/448470974/Common.xml?api=v2" }); | ||
r.append(new Include(this._v, {Alias : "Common", Namespace : "com.sap.vocabularies.Common.v1"} )) | ||
this._defaultRefs.push(r); | ||
r = Reference.create(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/448470968/UI.xml?api=v2" }); | ||
r.append(Include.create(this._v, {Alias : "UI", Namespace : "com.sap.vocabularies.UI.v1"} )) | ||
r = new Reference(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/448470971/Communication.xml?api=v2" }); | ||
r.append(new Include(this._v, {Alias : "Communication", Namespace : "com.sap.vocabularies.Communication.v1"} )) | ||
this._defaultRefs.push(r); | ||
r = new Reference(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/448470968/UI.xml?api=v2" }); | ||
r.append(new Include(this._v, {Alias : "UI", Namespace : "com.sap.vocabularies.UI.v1"} )) | ||
this._defaultRefs.push(r); | ||
r = new Reference(this._v, { Uri : "https://wiki.scn.sap.com/wiki/download/attachments/496435637/PersonalData.xml?api=v2" }); | ||
r.append(new Include(this._v, {Alias : "PersonalData", Namespace : "com.sap.vocabularies.PersonalData.v1"} )) | ||
this._defaultRefs.push(r); | ||
} | ||
@@ -487,12 +477,8 @@ } | ||
class EntityContainer extends Node {} | ||
class EntitySet extends Node | ||
{ | ||
static | ||
create(v, details, csn) | ||
{ | ||
let node = super.create(v, details, csn); | ||
return node; | ||
} | ||
// virtual | ||
// virtual | ||
setSapVocabularyAsAttributes(csn) | ||
@@ -512,2 +498,4 @@ { | ||
{ | ||
// OASIS ODATA-1231 $Collection=true | ||
json['$Collection']=true; | ||
for (let p in this) | ||
@@ -539,11 +527,9 @@ { | ||
{ | ||
static create(v, keys) | ||
constructor(v, keys) | ||
{ | ||
let node = undefined; | ||
super(v); | ||
if (keys && keys.length > 0) | ||
{ | ||
node = super.create(v); | ||
keys.forEach(k => node.append(PropertyRef.create(v, k))); | ||
keys.forEach(k => this.append(new PropertyRef(v, k))); | ||
} | ||
return node; | ||
} | ||
@@ -569,8 +555,6 @@ | ||
{ | ||
static | ||
create(v, details) | ||
constructor(v, details) | ||
{ | ||
let node = super.create(v, details); | ||
node.set( { _returnType: undefined }); | ||
return node; | ||
super(v, details); | ||
this.set( { _returnType: undefined }); | ||
} | ||
@@ -619,4 +603,3 @@ | ||
{ | ||
static | ||
create(v, csn, fullQualified) | ||
constructor(v, csn, fullQualified) | ||
{ | ||
@@ -628,10 +611,9 @@ let type = csn.type || csn.items.type; | ||
type = fullQualified(type); | ||
let node = super.create(v, { Type: type }); | ||
glue.addTypeFacets(node, csn); | ||
super(v, { Type: type }); | ||
glue.addTypeFacets(this, csn); | ||
node.set( { _type: type, _isCollection: csn.items != undefined, _nullable: true }); | ||
this.set( { _type: type, _isCollection: csn.items != undefined, _nullable: true }); | ||
if(node._isCollection) | ||
node.Type = `Collection(${node.Type})` | ||
return node; | ||
if(this._isCollection) | ||
this.Type = `Collection(${this.Type})` | ||
} | ||
@@ -658,13 +640,12 @@ | ||
{ | ||
static | ||
create(v, attributes, csn, typeName='Type') | ||
constructor(v, attributes, csn, typeName='Type') | ||
{ | ||
if(!(csn instanceof Object)) | ||
throw "Please debug me: csn must be an object" | ||
throw Error('Please debug me: csn must be an object'); | ||
// ??? Is CSN still required? NavProp? | ||
let node = super.create(v, attributes, csn); | ||
node.set({ _isCollection: csn.items != undefined, _typeName: typeName }); | ||
super(v, attributes, csn); | ||
this.set({ _isCollection: csn.items != undefined, _typeName: typeName }); | ||
if(node[typeName] == undefined) | ||
if(this[typeName] == undefined) | ||
{ | ||
@@ -678,9 +659,9 @@ let typecsn = csn.items || csn; | ||
{ | ||
node[typeName] = glue.mapCdsToEdmType(typecsn.type, node.v2, csn['@Core.MediaType']); | ||
this[typeName] = glue.mapCdsToEdmType(typecsn.type, this.v2, csn['@Core.MediaType']); | ||
// CDXCORE-CDXCORE-173 ignore type facets for Edm.Stream | ||
if(node[typeName] != 'Edm.Stream') | ||
glue.addTypeFacets(node, typecsn); | ||
if(this[typeName] != 'Edm.Stream') | ||
glue.addTypeFacets(this, typecsn); | ||
} | ||
else | ||
node[typeName] = typecsn.type; | ||
this[typeName] = typecsn.type; | ||
@@ -696,16 +677,15 @@ // CDXCORE-245: | ||
{ | ||
node[typeName] = odataType; | ||
this[typeName] = odataType; | ||
if(odataTypeMaxLen) | ||
node['MaxLength'] = odataTypeMaxLen; | ||
this['MaxLength'] = odataTypeMaxLen; | ||
} | ||
// store undecorated type for JSON | ||
node.set( { _type : node[typeName] }); | ||
this.set( { _type : this[typeName] }); | ||
// decorate for XML (not for Complex/EntityType) | ||
if(node._isCollection) | ||
node[typeName] = `Collection(${node[typeName]})` | ||
if(this._isCollection) | ||
this[typeName] = `Collection(${this[typeName]})` | ||
} | ||
} | ||
return node; | ||
} | ||
@@ -741,10 +721,9 @@ | ||
{ | ||
static | ||
create(v, details, properties, csn) | ||
constructor(v, details, properties, csn) | ||
{ | ||
let node = super.create(v, details, csn); | ||
node.append(...properties); | ||
let keys = Key.create(v, properties.filter(c => c.isKey).map(c => c.Name)); | ||
node.set( { _keys: keys } ); | ||
return node; | ||
super(v, details, csn); | ||
this.append(...properties); | ||
let keys = properties.filter(c => c.isKey).map(c => c.Name); | ||
if(keys.length > 0) | ||
this.set( { _keys: new Key(v, keys) } ); | ||
} | ||
@@ -773,7 +752,5 @@ | ||
{ | ||
static | ||
create(v, attributes, csn) | ||
constructor(v, attributes, csn) | ||
{ | ||
let node = super.create(v, attributes, csn, 'UnderlyingType'); | ||
return node | ||
super(v, attributes, csn, 'UnderlyingType'); | ||
} | ||
@@ -791,6 +768,5 @@ | ||
{ | ||
static | ||
create(v, attributes, csn) | ||
constructor(v, attributes, csn) | ||
{ | ||
let node = super.create(v, attributes, csn); | ||
super(v, attributes, csn); | ||
@@ -800,5 +776,4 @@ // array of enum not yet allowed | ||
glue.forAll(enumValues, (e, en) => { | ||
node.append(Member.create(v, { Name: en, Value: e.val } )); | ||
this.append(new Member(v, { Name: en, Value: e.val } )); | ||
}); | ||
return node | ||
} | ||
@@ -821,5 +796,2 @@ | ||
{ | ||
static create(v, attributes) | ||
{ return super.create(v, attributes); } | ||
toJSONattributes(json) | ||
@@ -832,9 +804,9 @@ { | ||
class PropertyBase extends TypeBase { | ||
static | ||
create(v, attributes, csn) | ||
class PropertyBase extends TypeBase | ||
{ | ||
constructor(v, attributes, csn) | ||
{ | ||
let node = super.create(v, attributes, csn); | ||
node.set({ _csn: csn }); | ||
if(node.v2) | ||
super(v, attributes, csn); | ||
this.set({ _csn: csn }); | ||
if(this.v2) | ||
{ | ||
@@ -846,9 +818,8 @@ let typecsn = csn.items || csn; | ||
// but not if Edm.DateTime is the result of a regular cds type mapping | ||
if(node.Type == 'Edm.DateTime' | ||
if(this.Type == 'Edm.DateTime' | ||
&& (typecsn.type != 'cds.DateTime' && typecsn.type != 'cds.Timestamp')) | ||
node.setXml( { 'sap:display-format' : "Date" } ); | ||
this.setXml( { 'sap:display-format' : "Date" } ); | ||
} | ||
node.setNullable(); | ||
return node | ||
this.setNullable(); | ||
} | ||
@@ -887,5 +858,5 @@ | ||
class Property extends PropertyBase { | ||
static | ||
create(v, attributes, csn) | ||
class Property extends PropertyBase | ||
{ | ||
constructor(v, attributes, csn) | ||
{ | ||
@@ -902,8 +873,8 @@ // the annotations in this array shall become exposed as Property attributes in | ||
let node = super.create(v, attributes, csn); | ||
super(v, attributes, csn); | ||
// TIPHANACDS-4180 | ||
if(node.v2) | ||
if(this.v2) | ||
{ | ||
if(csn['@odata.etag'] == true) | ||
node.ConcurrencyMode='Fixed' | ||
this.ConcurrencyMode='Fixed' | ||
@@ -914,7 +885,6 @@ // translate the following @sap annos as xml attributes to the Property | ||
if (Property.SAP_Annotation_Attribute_WhiteList.includes(p)) | ||
node.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn[p] }); | ||
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : csn[p] }); | ||
} | ||
} | ||
node.set({isKey: csn.key != undefined }); | ||
return node; | ||
this.set({isKey: csn.key != undefined }); | ||
} | ||
@@ -928,3 +898,3 @@ | ||
{ | ||
static create(v, Name) { return super.create(v, { Name }); } | ||
constructor(v, Name) { super(v, { Name }); } | ||
} | ||
@@ -934,19 +904,16 @@ | ||
{ | ||
static | ||
create(v, attributes, csn={}, mode=null) | ||
{ | ||
let node = super.create(v, attributes, csn); | ||
constructor(v, attributes, csn={}, mode=null) | ||
{ | ||
super(v, attributes, csn); | ||
if(mode != null) | ||
node.Mode = mode; | ||
if(mode != null) | ||
this.Mode = mode; | ||
// V2 XML: Parameters that are not explicitly marked as Nullable or NotNullable in the CSN must become Nullable=true | ||
// V2 XML Spec does only mention default Nullable=true for Properties not for Parameters so omitting Nullable=true let | ||
// the client assume that Nullable is false.... Correct Nullable Handling is done inside Parameter.create() | ||
if(node.v2 && node.Nullable === undefined) | ||
node.setXml({Nullable: true}); | ||
// V2 XML: Parameters that are not explicitly marked as Nullable or NotNullable in the CSN must become Nullable=true | ||
// V2 XML Spec does only mention default Nullable=true for Properties not for Parameters so omitting Nullable=true let | ||
// the client assume that Nullable is false.... Correct Nullable Handling is done inside Parameter constructor | ||
if(this.v2 && this.Nullable === undefined) | ||
this.setXml({Nullable: true}); | ||
} | ||
return node; | ||
} | ||
toJSON() | ||
@@ -964,22 +931,30 @@ { | ||
class NavigationProperty extends Property { | ||
static | ||
create(v, attributes, csn) | ||
class NavigationProperty extends Property | ||
{ | ||
constructor(v, attributes, csn) | ||
{ | ||
NavigationProperty.DOLLAR_SELF = '$self' | ||
let navProp = super.create(v, attributes, csn); | ||
navProp.set( { | ||
super(v, attributes, csn); | ||
this.set( { | ||
_type: attributes.Type, | ||
_isCollection: glue.isToMany(csn), | ||
_referentialConstraints: navProp.getReferentialConstraints(), | ||
_referentialConstraints: this.getReferentialConstraints(), | ||
_targetCsn: csn.target } ); | ||
if (navProp.v4) { | ||
if (this.v4) | ||
{ | ||
// either csn has multiplicity or we have to use the multiplicity of the backlink | ||
if(navProp._isCollection || navProp._referentialConstraints.multiplicity[1] == '*') { | ||
navProp.Type = `Collection(${attributes.Type})` | ||
if(this._isCollection || this._referentialConstraints.multiplicity[1] == '*') { | ||
this.Type = `Collection(${attributes.Type})` | ||
// attribute Nullable is not allowed in combination with Collection (see Spec) | ||
delete navProp.Nullable; | ||
delete this.Nullable; | ||
} | ||
if(csn.type === 'cds.Composition') | ||
{ | ||
// TODO: to be specified via @sap.on.delete | ||
this.append(new OnDelete(v, { Action: 'Cascade' } ) ); | ||
} | ||
/* | ||
@@ -995,23 +970,20 @@ 1) If this navigation property belongs to an EntityType for a parameterized entity | ||
if(csn['@odata.contained'] == true || csn.containsTarget) | ||
navProp.ContainsTarget = true; | ||
this.ContainsTarget = true; | ||
// if backlink has established partner before this underlying NavProp is created, use it | ||
if(csn.$Partner) | ||
navProp.Partner = csn.$Partner; | ||
if(csn._partnerCsn) | ||
this.Partner = csn._partnerCsn.name; | ||
} | ||
if (navProp.v2 && navProp.isNotNullable()) { | ||
if (this.v2 && this.isNotNullable()) { | ||
// in V2 not null must be expressed with target cardinality of 1 or more, | ||
// store Nullable=false and evaluate in determineMultiplicity() | ||
delete navProp.Nullable; | ||
delete this.Nullable; | ||
} | ||
// store NavProp reference in the model for bidirectional $Partner tagging (done in getReferentialConstraints()) | ||
csn._NavigationProperty = navProp; | ||
csn._NavigationProperty = this; | ||
// we don't want NavProps in the <KEY> list | ||
delete navProp.isKey; | ||
// TODO: Possible V4 Attributes: Partner, ContainsTarget | ||
// Possible subelement <OnDelete>, may be specified via @odata.OnDelete annotation... | ||
return navProp; | ||
delete this.isKey; | ||
} | ||
@@ -1047,3 +1019,16 @@ | ||
let json_constraints = Object.create(null); | ||
this._children.forEach(c => json_constraints[c.Property] = c.ReferencedProperty); | ||
this._children.forEach(c => { | ||
switch(c.kind) { | ||
case 'ReferentialConstraint': | ||
// collect ref constraints in dictionary | ||
json_constraints[c.Property] = c.ReferencedProperty; | ||
break; | ||
case 'OnDelete': | ||
json['$OnDelete'] = c.Action; | ||
break; | ||
default: | ||
throw Error('Unhandled NavProp child: ' + c.kind); | ||
} | ||
}); | ||
// TODO Annotations | ||
@@ -1057,3 +1042,3 @@ if(Object.keys(json_constraints).length > 0) | ||
{ | ||
return NavigationPropertyBinding.create(this._v, | ||
return new NavigationPropertyBinding(this._v, | ||
{ Path: this.Name, Target: this._csn.target.name.replace(namespace, '') } | ||
@@ -1066,3 +1051,3 @@ ); | ||
glue.forAll(this._referentialConstraints.constraints, | ||
c => this.append(ReferentialConstraint.create(this._v, | ||
c => this.append(new ReferentialConstraint(this._v, | ||
{ Property: c[0], ReferencedProperty: c[1] } | ||
@@ -1074,3 +1059,3 @@ ) ) ); | ||
{ | ||
let result = { multiplicity: [ ], constraints: Object.create(null), selfs: [] }; | ||
let result = { multiplicity: [ ], constraints: Object.create(null), selfs: [], termCount: 0 }; | ||
@@ -1085,3 +1070,3 @@ assocCsn = assocCsn || this._csn; | ||
// for all $self conditions, fill constraints of partner (if any) | ||
let isBacklink = result.selfs.length == 1; | ||
let isBacklink = result.selfs.length == 1 && result.termCount == 1; | ||
@@ -1100,3 +1085,10 @@ /* example for originalTarget: | ||
result.selfs.forEach(partner => { | ||
let originAssocCsn = assocCsn.target.elements[partner] || assocCsn.originalTarget.elements[partner]; | ||
let originAssocCsn = assocCsn.target.elements[partner]; | ||
if(originAssocCsn == undefined && assocCsn.originalTarget) | ||
originAssocCsn = assocCsn.originalTarget.elements[partner]; | ||
if(originAssocCsn == undefined) | ||
throw Error('Could not find forward association "' + assocCsn.target.name + '.' + partner + | ||
' for backlink association "' + assocCsn._parent.name + '.' + assocCsn.name + '", maybe forward is not published in service?'); | ||
// let originAssocCsn = assocCsn.target.elements[partner] || assocCsn.originalTarget.elements[partner]; | ||
if(glue.isAssociationOrComposition(originAssocCsn)) { | ||
@@ -1121,11 +1113,17 @@ // if the assoc is marked as primary key, add all its foreign keys as constraint | ||
// association to the two corresponding csn's and to this NavProp | ||
// (but only if originAssoc is navigable (undefined !== false) still evaluates to true) | ||
if(this.v4 && originAssocCsn['@odata.navigable'] !== false) | ||
// (but only if originAssoc is navigable (undefined !== false) still evaluates to true AND if the original assoc has no parter yet) | ||
if(this.v4 && originAssocCsn['@odata.navigable'] !== false && originAssocCsn._partnerCsn === undefined) | ||
{ | ||
this.Partner = assocCsn.$Partner = originAssocCsn.name; | ||
// set the Partner attribute to this (backlink) NavProp | ||
this.Partner = originAssocCsn.name; | ||
// link the two CSNs with each other | ||
assocCsn._partnerCsn = originAssocCsn; | ||
originAssocCsn._partnerCsn = assocCsn; | ||
// if the other NavProp has been created already, set NavProp.Partner | ||
// if not, Partner will be set during creation of other NavProp | ||
if(originAssocCsn._NavigationProperty) | ||
originAssocCsn._NavigationProperty.Partner = assocCsn.name; | ||
// if not, Partner will be set during creation of other NavProp | ||
originAssocCsn.$Partner = assocCsn.name; | ||
//console.log('NavigationProperty "' + assocCsn._parent.name + '.' + assocCsn.name + '" wants to establish partnership with NavigationProperty "' + originAssocCsn._parent.name + '.' + originAssocCsn.name + '" which is already partnered with NavigationProperty "' + originAssocCsn._partnerCsn._parent.name + '.' + originAssocCsn._partnerCsn.name + '"'); | ||
} | ||
@@ -1140,3 +1138,3 @@ } | ||
*/ | ||
throw "Backlink association element is not an association or composition: " + originAssocCsn.name; | ||
throw Error('Backlink association element is not an association or composition: "' + originAssocCsn.name); | ||
} | ||
@@ -1188,3 +1186,3 @@ | ||
if(!expr.some(isNotAConstraintTerm)) | ||
expr.map(fillConstraints) | ||
expr.forEach(fillConstraints) | ||
@@ -1198,3 +1196,3 @@ // return true if token is not one of '=', 'and', '(', ')' or object | ||
return tok.some(isNotAConstraintTerm); | ||
return !(typeof tok === 'object' && tok !== null || allowedTokens.includes(tok)); | ||
return !(typeof tok === 'object' && tok != null || allowedTokens.includes(tok)); | ||
} | ||
@@ -1210,25 +1208,28 @@ | ||
let rhs = expr[pos+1]; | ||
if(arg === '=' && lhs.ref && rhs.ref) | ||
if(['='].includes(arg)) | ||
{ | ||
lhs = lhs.ref; | ||
rhs = rhs.ref; | ||
// if exactly one operand starts with the prefix then this is potentially a constraint | ||
if((lhs[0] === assocCsn.name && rhs[0] !== assocCsn.name) || | ||
(lhs[0] !== assocCsn.name && rhs[0] === assocCsn.name)) | ||
result.termCount++; | ||
if(lhs.ref && rhs.ref) // ref is a path | ||
{ | ||
// order is always [ property, referencedProperty ] | ||
//backlink [ self, assocName ] | ||
let c; | ||
if(lhs[0] === assocCsn.name) | ||
c = [ rhs[0], lhs[1] ]; | ||
else | ||
c = [ lhs[0], rhs[1] ]; | ||
lhs = lhs.ref; | ||
rhs = rhs.ref; | ||
// if exactly one operand starts with the prefix then this is potentially a constraint | ||
if((lhs[0] === assocCsn.name && rhs[0] !== assocCsn.name) || | ||
(lhs[0] !== assocCsn.name && rhs[0] === assocCsn.name)) | ||
{ | ||
// order is always [ property, referencedProperty ] | ||
//backlink [ self, assocName ] | ||
let c; | ||
if(lhs[0] === assocCsn.name) | ||
c = [rhs[0], lhs[1]]; | ||
else | ||
c = [lhs[0], rhs[1]]; | ||
// do we have a $self or optionally a 'self' id? | ||
// if so, store partner in selfs array | ||
if(c[0] === NavigationProperty.DOLLAR_SELF || | ||
(NavigationProperty.OLDSTYLE_SELF && c[0] === NavigationProperty.OLDSTYLE_SELF)) | ||
result.selfs.push(c[1]); | ||
else | ||
result.constraints[c] = c; | ||
// do we have a $self id? | ||
// if so, store partner in selfs array | ||
if(c[0] === NavigationProperty.DOLLAR_SELF) | ||
result.selfs.push(c[1]); | ||
else | ||
result.constraints[c] = c; | ||
} | ||
} | ||
@@ -1307,6 +1308,11 @@ } | ||
class OnDelete extends Node {} | ||
// Annotations below | ||
class AnnotationBase extends Node | ||
{ | ||
toJSON() // no $Kind | ||
// No Kind: AnnotationBase is base class for Thing and ValueThing with dynamic kinds, | ||
// this requires an explicit constructor as the kinds cannot be blacklisted in | ||
// Node.toJSON() | ||
toJSON() | ||
{ | ||
@@ -1322,33 +1328,24 @@ let json = Object.create(null); | ||
// short form: key: value | ||
let inlineConstExpr = [ 'Bool', 'Float', 'String' ]; | ||
// if not inline, represented as object (exceptions apply (INF/NAN)) | ||
let constExpr = [ 'Binary', 'Bool', 'Date', 'DateTimeOffset', | ||
'Decimal', 'Duration', 'EnumMember', 'EnumMember@odata.type', 'Float', | ||
'Guid', 'Int', 'String', 'TimeOfDay', | ||
'Path', 'AnnotationPath', 'ModelElementPath', | ||
'NavigationPropertyPath', 'PropertyPath' ]; | ||
let inlineConstExpr = | ||
[ 'Edm.Binary', 'Edm.Boolean', 'Edm.Byte', 'Edm.Date', 'Edm.DateTimeOffset', 'Edm.Decimal', 'Edm.Double', 'Edm.Duration', 'Edm.Guid', | ||
'Edm.Int16', 'Edm.Int32', 'Edm.Int64', 'Edm.SByte','Edm.Single', /*'Edm.Stream',*/ 'Edm.String', 'Edm.TimeOfDay', | ||
/* UI.xml: defines Annotations with generic type 'Edm.PrimitiveType' */ | ||
'Edm.PrimitiveType' ]; | ||
// call this for 'all' properties and for JSON only properties | ||
// eihter 'all' props or JSON only props must be filled but not both! | ||
let constExpr = [ ...inlineConstExpr, | ||
'AnnotationPath', 'ModelElementPath', 'NavigationPropertyPath', 'PropertyPath', 'Path', | ||
'EnumMember', 'EnumMember@odata.type' ]; | ||
let expr = glue.intersect(constExpr, Object.keys(this)); | ||
let jsonOnlyExpr = glue.intersect(constExpr, Object.keys(this._jsonOnlyAttributes)) | ||
if(expr.length + jsonOnlyExpr.length == 0) | ||
throw "Please debug me: neither child nor constant expression found on annotation"; | ||
let expr = glue.intersect(constExpr, Object.keys(this._jsonOnlyAttributes)) | ||
if(expr.length > 0 && jsonOnlyExpr.length > 0) | ||
throw "Please debug me: either expr or jsonOnlyExpr must be used but not together" + expr + " " + jsonOnlyExpr; | ||
if(expr.length == 0) | ||
throw Error('Please debug me: neither child nor constant expression found on annotation'); | ||
return addExpressions(expr, this._jsonOnlyAttributes); | ||
if(expr.length > 0) | ||
return addExpressions(expr, this); | ||
if(jsonOnlyExpr.length > 0) | ||
return addExpressions(jsonOnlyExpr, this._jsonOnlyAttributes); | ||
return undefined; | ||
function addExpressions(expr, dict) | ||
{ | ||
let json = Object.create(null); | ||
let inline = glue.intersect(expr, inlineConstExpr); | ||
if(inline.length > 1) | ||
throw Error('Please debug me: more than one inline constant expression found on annotation ' + inline.join(', ')); | ||
if(inline.length==1) | ||
@@ -1359,16 +1356,14 @@ { | ||
{ | ||
case 'Bool': | ||
return (v=='true'?true:(v=='false'?false:v)); | ||
case 'Float': | ||
if(v=='INF'||v=='-INF'||v=='NaN') | ||
{ | ||
json['$Float'] = v; | ||
return json; | ||
} | ||
else | ||
return v; | ||
case 'String': | ||
/* short notation for Edm.Boolean, Edm.String and Edm.Float, see: | ||
https://github.wdf.sap.corp/edmx2csn-npm/edm-converters/blob/835d92a1aa6b0be25c56cef85e260c9188187429/lib/edmxV40ToJsonV40/README.md | ||
*/ | ||
case 'Edm.Boolean': | ||
v = (v=='true'?true:(v=='false'?false:v)); | ||
// eslint-no-fallthrough | ||
case 'Edm.String': | ||
// eslint-no-fallthrough | ||
case 'Edm.Float': | ||
return v; | ||
default: | ||
throw "Please debug me: default not reachable"; | ||
return { '$Cast': v, '$Type': inline[0] }; | ||
} | ||
@@ -1378,2 +1373,3 @@ } | ||
{ | ||
let json = Object.create(null); | ||
for(let i = 0; i < expr.length; i++) | ||
@@ -1389,9 +1385,7 @@ json['$' + expr[i]] = dict[expr[i]] | ||
{ | ||
static | ||
create(v, target) | ||
constructor(v, target) | ||
{ | ||
let node = super.create(v, { Target: target }); | ||
if (node.v2) | ||
node.setXml( { xmlns : "http://docs.oasis-open.org/odata/ns/edm" } ); | ||
return node; | ||
super(v, { Target: target }); | ||
if (this.v2) | ||
this.setXml( { xmlns : "http://docs.oasis-open.org/odata/ns/edm" } ); | ||
} | ||
@@ -1427,7 +1421,5 @@ | ||
{ | ||
static | ||
create(v, termName) | ||
constructor(v, termName) | ||
{ | ||
let node = super.create(v, { Term: termName } ); | ||
return node; | ||
super(v, { Term: termName } ); | ||
} | ||
@@ -1449,5 +1441,3 @@ | ||
{ | ||
let json = []; | ||
this._children.forEach(a => json.push(a.toJSON())); | ||
return json; | ||
return this._children.map(a => a.toJSON()); | ||
} | ||
@@ -1478,3 +1468,3 @@ } | ||
default: | ||
throw "Please debug me: default not reachable"; | ||
throw Error('Please debug me: default not reachable'); | ||
} | ||
@@ -1488,7 +1478,6 @@ json[name] = a.toJSON() | ||
{ | ||
static create(v, property) | ||
constructor(v, property) | ||
{ | ||
let node = super.create(v); | ||
node.Property = property; | ||
return node; | ||
super(v); | ||
this.Property = property; | ||
} | ||
@@ -1509,7 +1498,6 @@ | ||
{ | ||
static create(v, kind, details) | ||
constructor(v, kind, details) | ||
{ | ||
let node = super.create(v, details); | ||
node.setKind(kind); | ||
return node; | ||
super(v, details); | ||
this.setKind(kind); | ||
} | ||
@@ -1526,7 +1514,6 @@ | ||
{ | ||
static create(v, kind, value) | ||
constructor(v, kind, value) | ||
{ | ||
let node = super.create(v, kind); | ||
node.set( { _value : value }); | ||
return node; | ||
super(v, kind, undefined); | ||
this.set( { _value : value }); | ||
} | ||
@@ -1538,14 +1525,19 @@ | ||
let xml = indent + "<" + kind + this.toXMLattributes(); | ||
xml += (this._value ? ">" + escapeString(this._value) + "</" + kind + ">" : + "/>"); | ||
xml += (this._value !== undefined ? ">" + escapeString(this._value) + "</" + kind + ">" : "/>"); | ||
return xml; | ||
function escapeString(s) { | ||
function escapeString(s) | ||
{ | ||
// first regex: replace & if not followed by apos; or quot; or gt; or lt; or amp; or # | ||
return (typeof s === 'string') ? s.replace(/&(?!(?:apos|quot|[gl]t|amp);|#)/g, '&').replace(/>/g, '>').replace(/</g, '<') : s; | ||
return (typeof s === 'string') ? s.replace(/&(?!(?:apos|quot|[gl]t|amp);|#)/g, '&').replace(/>/g, '>').replace(/</g, '<').replace(/"/g, '"') : s; | ||
} | ||
} | ||
toJSONattributes(json) | ||
toJSON() | ||
{ | ||
json['$'+this.kind] = this._value; | ||
if(this._children.length == 0) // must be a constant expression | ||
return this.getConstantExpressionValue(); | ||
else | ||
// annotation must have exactly one child (=record or collection) | ||
return this._children[0].toJSON(); | ||
} | ||
@@ -1559,12 +1551,21 @@ } | ||
{ | ||
static | ||
create(v, details, f, t, m) | ||
constructor(v, details, navProp, fromRole, toRole, multiplicity) | ||
{ | ||
let node = super.create(v, details); | ||
node.set( { _end: [] }); | ||
node._end.push( | ||
End.create(v, { Role: f[0], Type: f[1], Multiplicity: m[0] } ), | ||
End.create(v, { Role: t[0], Type: t[1], Multiplicity: m[1] } ) | ||
); | ||
return node; | ||
super(v, details); | ||
this.set( { _end: [] }); | ||
this._end.push( | ||
new End(v, { Role: fromRole[0], Type: fromRole[1], Multiplicity: multiplicity[0] } ), | ||
new End(v, { Role: toRole[0], Type: toRole[1], Multiplicity: multiplicity[1] } ) ); | ||
// if eventually a backlink is a composition and this (Forward-)Association has not yet been | ||
// produced, add the OnDelete property now to the source end. | ||
// undefined values are not appended | ||
this._end[0].append(navProp._OnDeleteSrcEnd); | ||
// if the NavProp is a composition, add the OnDelete to the target end | ||
if(navProp._csn.type == 'cds.Composition') { | ||
// TODO: to be specified via @sap.on.delete | ||
this._end[1].append(new OnDelete(v, { Action: 'Cascade' } ) ); | ||
} | ||
} | ||
@@ -1583,11 +1584,9 @@ | ||
{ | ||
static | ||
create(v, details, fromRole, toRole, fromEntitySet, toEntitySet) | ||
constructor(v, details, fromRole, toRole, fromEntitySet, toEntitySet) | ||
{ | ||
let node = super.create(v, details); | ||
node.append( | ||
End.create(v, { Role: fromRole, EntitySet: fromEntitySet } ), | ||
End.create(v, { Role: toRole, EntitySet: toEntitySet } ) | ||
super(v, details); | ||
this.append( | ||
new End(v, { Role: fromRole, EntitySet: fromEntitySet } ), | ||
new End(v, { Role: toRole, EntitySet: toEntitySet } ) | ||
); | ||
return node; | ||
} | ||
@@ -1598,12 +1597,13 @@ } | ||
class Principal extends Node {} | ||
ReferentialConstraint.createV2 = | ||
function(v, from, to, c) | ||
{ | ||
let node = ReferentialConstraint.create(v, {}); | ||
node.set({ _d: Dependent.create(v, { Role: from } ) }); | ||
node.set({ _p: Principal.create(v, { Role: to } ) }); | ||
let node = new ReferentialConstraint(v, {}); | ||
node.set({ _d: new Dependent(v, { Role: from } ) }); | ||
node.set({ _p: new Principal(v, { Role: to } ) }); | ||
glue.forAll(c, cv => { | ||
node._d.append(PropertyRef.create(v, cv[0])); | ||
node._p.append(PropertyRef.create(v, cv[1])); | ||
node._d.append(new PropertyRef(v, cv[0])); | ||
node._p.append(new PropertyRef(v, cv[1])); | ||
}); | ||
@@ -1638,2 +1638,3 @@ return node; | ||
ReferentialConstraint, | ||
OnDelete, | ||
// Annotations | ||
@@ -1640,0 +1641,0 @@ Annotations, |
'use strict'; | ||
/* eslint max-statements-per-line:off */ | ||
const { setProp } = require('../base/model'); | ||
@@ -8,3 +9,8 @@ function validateOptions(_options) | ||
{ | ||
// csn2edm expects "version" to be a top-level property of options | ||
// set to 'v4' as default, override with value from incoming options | ||
// (here version comes inside "toOdata") | ||
const options = Object.assign({ version: 'v4'}, _options); | ||
if (options.toOdata && options.toOdata.version) | ||
options.version = options.toOdata.version; | ||
@@ -17,3 +23,3 @@ const v2 = options.version.match(/v2/i) != undefined; | ||
if(options.v.filter(v=>v).length != 1) | ||
throw `Please debug me: EDM V2:${v2}, V4:${v4}` | ||
throw Error(`Please debug me: EDM V2:${v2}, V4:${v4}`); | ||
@@ -126,3 +132,3 @@ options.isV2 = function() { return this.v[0] == true; } | ||
if(options == undefined) | ||
throw "Please debug me: initializeModel must be invoked with options" | ||
throw Error('Please debug me: initializeModel must be invoked with options'); | ||
@@ -154,3 +160,2 @@ // make sure options are complete | ||
foreach(model.definitions, isStructuredArtifact, initializeAssociation); | ||
// Attach name to actions and their parameters | ||
return service; | ||
@@ -242,2 +247,3 @@ | ||
element.name = element.Name = elementName; | ||
setProp(element, '_parent', struct); | ||
@@ -255,12 +261,3 @@ // Collect keys | ||
// create dictionary to hold attributes that should belong to the | ||
// resulting EntitySet | ||
let newAttributes = Object.create(null); | ||
newAttributes['_EntitySetAttributes'] = { | ||
value: Object.create(null), | ||
configurable: true, | ||
enumerable: false, | ||
writable: true | ||
} | ||
Object.defineProperties(struct, newAttributes) | ||
setProp(struct, '_EntitySetAttributes', Object.create(null)); | ||
@@ -349,3 +346,3 @@ appSpecificLateCsnTranformations.atStructure(options, struct); | ||
// add the value list association as new element into struct.elements | ||
struct.elements[assocName] = { | ||
let assoc = { | ||
name: assocName, | ||
@@ -356,2 +353,4 @@ target: valueListEntity, | ||
} | ||
setProp(assoc, '_parent', struct); | ||
struct.elements[assocName] = assoc; | ||
} | ||
@@ -384,5 +383,23 @@ } | ||
'cds.UUID': 'Edm.Guid', | ||
/* unused but EDM defined | ||
Edm.Geography | ||
Edm.GeographyPoint | ||
Edm.GeographyLineString | ||
Edm.GeographyPolygon | ||
Edm.GeographyMultiPoint | ||
Edm.GeographyMultiLineString | ||
Edm.GeographyMultiPolygon | ||
Edm.GeographyCollection | ||
Edm.Geometry | ||
Edm.GeometryPoint | ||
Edm.GeometryLineString | ||
Edm.GeometryPolygon | ||
Edm.GeometryMultiPoint | ||
Edm.GeometryMultiLineString | ||
Edm.GeometryMultiPolygon | ||
Edm.GeometryCollection | ||
*/ | ||
}[cdsType]; | ||
if (edmType == undefined) | ||
throw "No edm type found for " + cdsType; | ||
throw Error('No edm type found for ' + cdsType); | ||
if(isV2) | ||
@@ -389,0 +406,0 @@ { |
@@ -111,2 +111,3 @@ let W = require("./walker"); | ||
location(node[name], path.concat(name)) | ||
throw Error("augmentor: Queries not supported"); | ||
} | ||
@@ -363,15 +364,2 @@ | ||
function modifySource(node, name, path) { | ||
/* TODO uncomment and test | ||
node.query ={ | ||
op: | ||
{ | ||
val: 'query' }, | ||
from: | ||
[ { path: | ||
[ { id: node.source } ] } ], | ||
all: | ||
{ value: true }, | ||
elements: {} | ||
} | ||
*/ | ||
if(options && options.augmentor && options.augmentor.oldProjections) { | ||
@@ -378,0 +366,0 @@ node.source = {absolute:node.source, path:[{id:node.source}]} |
@@ -54,2 +54,4 @@ let W = require("./walker"); | ||
let le = U.getLastElement(path) | ||
if(le==="$extra") // skip $extra from validator | ||
return false; | ||
if(le[0]==="@") | ||
@@ -56,0 +58,0 @@ return false; // do not walk annotations |
@@ -240,2 +240,9 @@ // contains query relevant augmentor functions | ||
return W.dmap(args, (arg,obj) => { | ||
if(obj.param === true) | ||
return { | ||
name: { id: arg, location: U.newLocation(path.concat(arg), U.WILO_FULL) }, | ||
path: refAsPath(obj.ref, path.concat(arg,"ref")), | ||
scope: "param", | ||
location: U.newLocation(path.concat(arg), U.WILO_FULL) | ||
} | ||
return newValue(obj.val, path.concat(arg), arg); | ||
@@ -301,2 +308,6 @@ }); | ||
E.value = {path, location: U.newLocation(refPath, U.WILO_FULL)}; | ||
if(C.param) | ||
E.value.scope="param"; | ||
if(C.global) | ||
E.value.scope="global"; | ||
} | ||
@@ -303,0 +314,0 @@ if(C.as!==undefined) { |
@@ -227,3 +227,3 @@ let W = require("./walker"); | ||
if(node.returns.items!==undefined) { | ||
U.setLocation(node.returns.items, path.concat(["returns","items"])); | ||
augmentItems(path.concat(["returns","items"]), node.returns.items) | ||
} | ||
@@ -273,2 +273,4 @@ } | ||
} | ||
if(node.items) | ||
augmentItems(path.concat("items"),node.items) | ||
} | ||
@@ -299,3 +301,3 @@ | ||
if(def.returns.items !== undefined) { | ||
U.setLocation(def.returns.items, path.concat("returns","items")); | ||
augmentItems(path.concat("returns","items"), def.returns.items); | ||
} | ||
@@ -302,0 +304,0 @@ } |
@@ -21,3 +21,2 @@ // Transform augmented CSN into compact "official" CSN | ||
let cloneWithTransformations = require('../base/model').cloneWithTransformations; | ||
const { refString } = require('../base/messages'); | ||
const { queryOps } = require('../base/model'); | ||
@@ -35,3 +34,3 @@ const { mergeOptions } = require('../model/modelUtils'); | ||
blocks: ignore, | ||
columns: ( c, node, r ) => { if (c[0] && c[0].val === '*') r.all = true; }, | ||
columns: ( c, node, r ) => { if (c && c[0] && c[0].val === '*') r.all = true; }, | ||
annotationAssignments: ignore, // original with structure values | ||
@@ -50,3 +49,2 @@ kind: filterKind, | ||
target: compactName, | ||
source: compactName, | ||
scope: ignore, | ||
@@ -134,3 +132,3 @@ query: compactQuery, | ||
// We always filter these | ||
if (kind !== 'element' && kind !== 'key' && kind !== 'enum') | ||
if (!['element', 'key', 'enum', 'annotate', '$tableAlias'].includes(kind)) | ||
return kind; | ||
@@ -218,3 +216,5 @@ return undefined; | ||
function compactQuery( query, node ) { // not for projections - TEMP? | ||
function compactQuery( query, node, result ) { // not for projections - TEMP? | ||
if (query.from && query.from.length === 1 && query.from[0] && query.from[0].path) | ||
result.source = compactName( query.from[0] ); | ||
return (node.projection) ? undefined : compactCondOrExpr( query ); | ||
@@ -287,22 +287,2 @@ } | ||
//check if environment variable VALIDATE_CSN is set and if so perform CSN validation | ||
let VALIDATE_CSN = "VALIDATE_CSN" in process.env; | ||
if(VALIDATE_CSN) { | ||
let validateCSN = require("./schema/validateCSN.js"); | ||
let errors = validateCSN(newModel, {ajv:{useDefaults: false}}); | ||
if(errors.length>0) { | ||
// persist the results in files if environment variable VALIDATE_CSN_PERSIST_ERROR is set | ||
let VALIDATE_CSN_PERSIST_ERROR = "VALIDATE_CSN_PERSIST_ERROR" in process.env; | ||
if(VALIDATE_CSN_PERSIST_ERROR) { | ||
let fs = require("fs"); | ||
fs.writeFileSync("lasterror.json", JSON.stringify(newModel,null,2)); | ||
fs.writeFileSync("lasterror.txt", errors.toString()); | ||
throw Error("Invalid CSN: see lasterror.json and lasterror.txt"); | ||
} else { | ||
throw Error("Invalid CSN:" | ||
+ JSON.stringify(newModel,null,2) | ||
+ errors); | ||
} | ||
} | ||
} | ||
return newModel; | ||
@@ -357,2 +337,22 @@ } | ||
// Return string for complete reference | ||
function refString( name ) { | ||
// prepare that resolvePath does not set ref.absolute etc: | ||
if (name._artifact) | ||
name = name._artifact; | ||
if (name.name) | ||
name = name.name; | ||
let compact = ''; | ||
if (name.alias) | ||
compact = '.$alias.' + name.alias; | ||
if (name.action) | ||
compact = '.$action.' + name.action; | ||
if (name.param) | ||
compact += '.$param.' + name.param; | ||
if (name.element) | ||
compact += (compact ? '.' : '..') + name.element; | ||
// Yes, omit $query.0 -> test is (name.query), not (name.query != null) | ||
return name.absolute + | ||
(name.query ? '.$query.' + name.query : '') + compact; | ||
} | ||
@@ -359,0 +359,0 @@ module.exports = { |
@@ -18,11 +18,13 @@ /** | ||
// CSN validation | ||
let validateCSN = require("./schema/validateCSN.js"); | ||
validateCSN(model, options); | ||
let newCsn=require("./csnVersion").isNewCSN(model,options); | ||
// augment CSN | ||
if(newCsn) { | ||
if(model.version && model.version.csn === "0.1.99" || options && options.newCsn) | ||
// CSN validation | ||
let validateCSN = require("./validator/validateCSN.js"); | ||
validateCSN(model, options); | ||
augmentor3.augment(model); | ||
else | ||
} else | ||
augmentor.augment(model , filename, options ); | ||
@@ -29,0 +31,0 @@ delete model["locations"]; |
@@ -40,5 +40,6 @@ let W = require("./walker"); | ||
function cbParam(/*O*/) { | ||
function cbParam(O) { | ||
return [ | ||
] | ||
nullProto(O.elements, cbElement), | ||
].concat(directItems(O.items)) | ||
} | ||
@@ -51,3 +52,3 @@ | ||
nullProto(O.returns && O.returns.enum, cbEnum) | ||
] | ||
].concat(directItems(O.returns && O.returns.items)) | ||
} | ||
@@ -64,2 +65,3 @@ | ||
].concat(directItems(O.items)) | ||
.concat(directItems(O.returns && O.returns.items)) | ||
} | ||
@@ -66,0 +68,0 @@ |
@@ -8,78 +8,80 @@ // Transform augmented CSN into compact "official" CSN | ||
// in main: | ||
// const { compactModel: compactSortedJson } = require('./json/to-csn') | ||
//var strict = false; | ||
// dictionary: | ||
// exclude | ||
// namedArgs/arrowedArgs: insertOrderDict | ||
// struct: insertOrderDict | ||
var csn_gensrc = true; // good enough here... | ||
var mode_strict = false; // whether to dump with unknown properties (in standard) | ||
// IMPORTANT: the order of these properties determine the order of properties | ||
// in the resulting CSN !!! | ||
const transformers = { | ||
// early and modifiers (without null / not null) ------------------------------------- | ||
kind, | ||
name: ignore, // as is provided extra | ||
id: n => n, // in path item | ||
'@': value, | ||
// definitions, extensions, members ---------------------------------------- | ||
definitions: sortedDict, | ||
extensions: standard, // is array - TODO: sort | ||
kind, | ||
name: ignore, | ||
actions: nonEmptyDict, | ||
elements, | ||
enum: insertOrderDict, | ||
foreignKeys: renameTo( 'keys', dictAsArray ), // XSN: rename? | ||
mixin: insertOrderDict, // only in queries with special handling | ||
abstract: value, | ||
dbType: value, // TODO: currently with --hana-flavor only | ||
virtual: value, | ||
key: value, | ||
masked: value, | ||
params: insertOrderDict, | ||
returns: standard, // storing the return type of actions | ||
// type properties --------------------------------------------------------- | ||
cardinality: standard, // sub: src, min, max | ||
includes: arrayOf( artifactRef ), // also entities | ||
items: standard, | ||
// early expression / query properties ------------------------------------- | ||
op: o => (o.val !== 'query') ? o.val : undefined, | ||
from: fromOld, // before elements! XSN TODO just one (cross if necessary) | ||
// join done in from() | ||
// func // in expression() | ||
quantifier: ( q, csn ) => { csn[ q.val ] = true; }, | ||
all: ignore, // XSN TODO use quantifier | ||
// type properties (without 'elements') ------------------------------------ | ||
localized: value, | ||
type: artifactRef, | ||
length: value, | ||
on: (cond) => (typeof cond === 'string' ? undefined : condition( cond )), | ||
onCond : renameTo( 'on', condition ), // XSN TODO: onCond -> on | ||
precision: value, | ||
scale: value, | ||
cardinality: standard, // also for pathItem: after 'id', before 'where' | ||
target: artifactRef, | ||
type: artifactRef, | ||
sourceMax: renameTo( 'src', value ), // TODO XSN: rename? | ||
targetMin: renameTo( 'min', value ), | ||
targetMax: renameTo( 'max', value ), | ||
// general properties of constructs ---------------------------------------- | ||
abstract: value, | ||
dbType: value, // TODO: currently with --hana-flavor only | ||
default: expression, | ||
key: value, | ||
localized: value, | ||
masked: value, | ||
notNull: value, | ||
// targetElement: ignore, // special display of foreign key, renameTo: select | ||
value: enumValue, // do not list for select items as elements | ||
virtual: value, | ||
// queries, expressions ---------------------------------------------------- | ||
query, | ||
from: fromOld, // XSN TODO just one (cross if necessary) | ||
quantifier: ( q, csn ) => { csn[ q.val ] = true; }, | ||
foreignKeys: renameTo( 'keys', dictAsArray ), // XSN: rename? | ||
on: (cond) => (typeof cond === 'string' ? undefined : condition( cond )), // also for join | ||
onCond : renameTo( 'on', condition ), // XSN TODO: onCond -> on | ||
enum: insertOrderDict, | ||
items: standard, | ||
includes: arrayOf( artifactRef ), // also entities | ||
// late expressions / query properties ------------------------------------- | ||
mixin: insertOrderDict, // only in queries with special handling | ||
columns, | ||
exclude: renameTo( 'excluding', Object.keys ), // XSN TODO: exclude->excluding | ||
groupBy: arrayOf( expression ), | ||
where: condition, // also pathItem after 'cardinality' before 'args' | ||
having: condition, | ||
limit, // TODO XSN: include offset | ||
offset: ignore, // TODO XSN: move into `limit` | ||
offset: ignore, // TODO XSN: move into `limit` - see limit | ||
orderBy: arrayOf( orderBy ), // TODO XSN: make `sort` and `nulls` sibling properties | ||
where: condition, | ||
// special HANA CDS featues ------------------------------------------------ | ||
args, // also pathItem after 'where' | ||
namedArgs: renameTo( 'args', args ), // XSN TODO - use args | ||
// definitions, extensions, members ---------------------------------------- | ||
returns: standard, // storing the return type of actions | ||
notNull: value, | ||
default: expression, | ||
// targetElement: ignore, // special display of foreign key, renameTo: select | ||
value: enumValue, // do not list for select items as elements | ||
query, | ||
elements, | ||
sequenceOptions: ignore, // TODO: currently not in the JSON by HANA | ||
actions: nonEmptyDict, | ||
technicalConfig, // TODO: spec, re-check | ||
// Old-style XSN/CSN ------------------------------------------------------- | ||
indexNo: ignore, // TODO XSN: remove | ||
origin: ignore, // remove (introduce non-enum _origin link) | ||
projection: ignore, // later in entity: $syntax: 'projection' | ||
source: ignore, // remove | ||
// protected (non-public) -------------------------------------------------- | ||
//'_' not here, as non-enumerable properties are not transformed anyway | ||
'$': ignore, | ||
// special: top-level, cardinality ----------------------------------------- | ||
definitions: sortedDict, | ||
extensions: standard, // is array - TODO: sort | ||
messages: ignore, // consider compactQuery / compactExpr | ||
sourceMax: renameTo( 'src', value ), // TODO XSN: rename? | ||
targetMin: renameTo( 'min', value ), | ||
targetMax: renameTo( 'max', value ), | ||
// late protected ---------------------------------------------------------- | ||
viaTransform: standard, // FIXME: not a standard prop, start with $ | ||
generatedFieldName: renameTo( '$generatedFieldName', n => n ), // TODO: XSN name | ||
$syntax: s => s, | ||
_containerEntity: n => n, // FIXME: prop starting with _ is link and non-enumerable | ||
_ignore: a => a, // not yet obsolete - still required by toHana (FIXME: maybe rename to $ignore, or use an annotation instead?) | ||
_ignoreMasked: standard, // FIXME: prop starting with _ is link and non-enumerable | ||
_isToContainer: standard, // FIXME: prop starting with _ is link and non-enumerable | ||
$extra: (e, csn) => { Object.assign( csn, e ); }, | ||
// IGNORED ----------------------------------------------------------------- | ||
artifacts: ignore, // well-introduced, hence not $artifacts | ||
@@ -91,18 +93,24 @@ location: ignore, // TODO: think about $location with flat struct (w/o offset) | ||
typeArguments: ignore, // FIXME: make it $typeArgs | ||
// protected - to be subsumed by $inferred --------------------------------- | ||
calculated: ignore, // TODO remove ($inferred: 'as') | ||
implicitForeignKeys: ignore, // later in assoc: $inferred: { foreignKeys: 'fk' } or $inferred on each fk | ||
indexNo: ignore, // TODO XSN: remove | ||
origin: ignore, // TODO remove (introduce non-enum _origin link) | ||
projection: ignore, // TODO remove | ||
redirected: ignore, // TODO remove: no need for this | ||
source: ignore, // TODO remove | ||
viaAll: ignore, // TODO remove, later in elem: $inferred: '*' | ||
'$': ignore, | ||
//'_' not here, as non-enumerable properties are not transformed anyway | ||
_typeIsExplicit: ignore, | ||
calculated: ignore, // later in name: $inferred: 'as' | ||
implicitForeignKeys: ignore, // later in assoc: $inferred: { foreignKeys: 'fk' } or $inferred on each fk | ||
redirected: ignore, // TODO: no need for this | ||
viaAll: ignore, // TODO remove, later in elem: $inferred: '*' | ||
// protected created by transformers --------------------------------------- | ||
_containerEntity: n => n, // FIXME: prop starting with _ is link and non-enumerable | ||
_ignore: a => a, // not yet obsolete - still required by toHana (FIXME: maybe rename to $ignore, or use an annotation instead?) | ||
_ignoreMasked: standard, // FIXME: prop starting with _ is link and non-enumerable | ||
_isToContainer: standard, // FIXME: prop starting with _ is link and non-enumerable | ||
generatedFieldName: renameTo( '$generatedFieldName', n => n ), // TODO: XSN name | ||
viaTransform: standard, // FIXME: not a standard prop, start with $ | ||
} | ||
const typeProperties = [ // currently just for select items | ||
const propertyOrder = (function () { | ||
let r = {}; | ||
let i = 0; | ||
for (let n in transformers) | ||
r[n] = ++i; | ||
return r; | ||
})(); | ||
const typeProperties = [ // just for `cast` in select items | ||
'type', 'length', 'precision', 'scale', 'items', 'target', 'elements', 'enum' | ||
@@ -136,3 +144,12 @@ ]; | ||
if (model.version) | ||
csn.version = model.version; | ||
csn.version = model.version; // TODO remove | ||
if(!options.testMode) { | ||
setMetaProperty(csn, model); | ||
setCsnVersion(csn); | ||
} | ||
// Use $extra properties of first source as resulting $extra properties | ||
for (let f in model.sources) { | ||
set( '$extra', csn, model.sources[f] ); | ||
break; | ||
} | ||
return csn; | ||
@@ -206,9 +223,2 @@ } | ||
function elements( dict, csn, node ) { | ||
if (!csn_gensrc || !node.query && !node.type) | ||
return insertOrderDict( dict ); | ||
else | ||
return undefined; | ||
} | ||
function set( prop, csn, node ) { | ||
@@ -223,2 +233,11 @@ let val = node[prop]; | ||
function elements( dict, csn, node ) { | ||
if (csn.from) // with SELECT | ||
return undefined; | ||
if (!csn_gensrc || !node.query && !node.type) | ||
return insertOrderDict( dict ); | ||
else | ||
return undefined; | ||
} | ||
// for csn_gensrc: return annotations from definition (annotated==false) | ||
@@ -289,5 +308,6 @@ // or annotations (annotated==true) | ||
if (art.kind === 'key') { // foreignkey | ||
let key = addExplicitAs( expression( art.targetElement ), art.name ); | ||
let key = addExplicitAs( expression( art.targetElement ), | ||
art.name, neqPath( art.targetElement ) ); | ||
set( 'generatedFieldName', key, art ); | ||
return key; | ||
return extra( key, art ); | ||
} | ||
@@ -305,5 +325,5 @@ else | ||
} | ||
if (k === 'view') // XSN TODO: kind: 'entity', $syntax: 'view' | ||
if (k === 'view') // XSN TODO: kind: 'entity' | ||
return 'entity'; | ||
if (['element', 'key', 'enum', 'annotate'].includes(k)) | ||
if (['element', 'key', 'enum', 'annotate', 'query', '$tableAlias'].includes(k)) | ||
return undefined; | ||
@@ -352,10 +372,6 @@ return k; | ||
function pathItem( item ) { | ||
if (!item.args && !item.namedArgs && !item.where && !item.cardinality) | ||
if (!item.args && !item.namedArgs && !item.where && !item.cardinality && !item.$extra) | ||
return item.id; | ||
let r = { id: item.id }; | ||
if (item.args || item.namedArgs) // XSN TODO: namedArgs -> args | ||
r.args = args( item.args || item.namedArgs ); | ||
set( 'cardinality', r, item ); | ||
set( 'where', r, item ); | ||
return r; | ||
else | ||
return standard( item ); | ||
} | ||
@@ -372,2 +388,3 @@ | ||
// "Short" value form, e.g. for annotation assignments | ||
function value( node ) { | ||
@@ -379,9 +396,9 @@ if (!node) | ||
if (node.path) | ||
return { '=': node.path.map( id => id.id ).join('.') }; | ||
return extra( { '=': node.path.map( id => id.id ).join('.') }, node ); | ||
if (node.literal == 'enum') | ||
return { "#" : node.symbol.id }; | ||
return extra( { "#" : node.symbol.id }, node ); | ||
if (node.literal == 'array') | ||
return node.val.map( value ); | ||
if (node.literal != 'struct') | ||
// no val (undefined( as true only for annotation values (and struct elem values) | ||
// no val (undefined) as true only for annotation values (and struct elem values) | ||
return node.name && !('val' in node) || node.val; | ||
@@ -408,3 +425,4 @@ let r = Object.create( null ); | ||
function expression( node ) { | ||
function expression( node, withExtra ) { | ||
let en = withExtra != null && node; | ||
if (typeof node === 'string') | ||
@@ -421,5 +439,5 @@ return node; | ||
if (node.path) | ||
return { param: true, ref: node.path.map( pathItem ) }; | ||
return extra( { ref: node.path.map( pathItem ), param: true }, en ); | ||
else | ||
return { param: true, ref: [ node.param.val ] }; | ||
return extra( { ref: [ node.param.val ], param: true }, en ); | ||
} | ||
@@ -429,17 +447,19 @@ if (node.path) { | ||
if (node.path.length !== 1) | ||
return { ref: node.path.map( pathItem ) }; | ||
return extra( { ref: node.path.map( pathItem ) }, en ); | ||
let item = pathItem( node.path[0] ); | ||
if (typeof item === 'string' && !node.path[0].quoted && | ||
// TODO: use _artifact if available | ||
magicFunctions.includes( item.toUpperCase() )) { | ||
return { func: item }; | ||
return extra( { func: item }, en ); | ||
} | ||
return { ref: [item] }; | ||
return extra( { ref: [item] }, en ); | ||
} | ||
if (node.literal) { | ||
if (typeof node.val === node.literal || node.val === null) | ||
return { val: node.val }; | ||
return extra( { val: node.val }, en ); | ||
else if (node.literal === 'enum') | ||
return { "#" : node.symbol.id }; | ||
return extra( { "#" : node.symbol.id }, en ); | ||
else // TODO XSN: literal 'hex'->'x' | ||
return { val: node.val, literal: (node.literal==='hex') ? 'x' : node.literal }; | ||
return extra( { val: node.val, literal: (node.literal==='hex') ? 'x' : node.literal }, | ||
en ); | ||
} | ||
@@ -450,8 +470,11 @@ if (node.func) { // TODO XSN: remove op: 'call', func is no path | ||
call.args = args( node.args || node.namedArgs ); | ||
return call; | ||
return extra( call, en ); | ||
} | ||
if (queryOps[ node.op.val ]) | ||
return query( node ); | ||
else // do not use xpr() for xpr, as it would flatten inner xpr's (semantically ok) | ||
return { xpr: (node.op.val === 'xpr') ? node.args.map( expression ) : xpr( node ) }; | ||
else if (node.op.val === 'xpr') | ||
// do not use xpr() for xpr, as it would flatten inner xpr's (semantically ok) | ||
return extra( { xpr: node.args.map( expression ) }, node ); | ||
else // other ops have no $extra | ||
return { xpr: xpr( node ) }; | ||
} | ||
@@ -487,9 +510,10 @@ | ||
function query( node ) { | ||
// TODO: add "inferred" elements for leading query | ||
while (node instanceof Array) // in parentheses -> remove | ||
node = node[0]; | ||
if (node.op.val === 'query') | ||
return { SELECT: standard( node ) }; | ||
let csn = {}; | ||
// for UNION, ... ---------------------------------------------------------- | ||
if (!['query', 'subquery'].includes( node.op.val )) { | ||
if (node.op.val !== 'unionAll') // CSN TODO: quantifier: 'all'|'distinct' | ||
if (node.op.val !== 'subquery') { | ||
if (node.op.val !== 'unionAll') // XSN TODO: quantifier: 'all'|'distinct' | ||
csn.op = node.op.val; | ||
@@ -510,15 +534,6 @@ else | ||
} | ||
// for SELECT -------------------------------------------------------------- | ||
set( 'from', csn, node ); | ||
set( 'mixin', csn, node ); | ||
set( 'columns', csn, node ); | ||
set( 'quantifier', csn, node ); | ||
set( 'exclude', csn, node ); // XSN TODO: exclude->excluding | ||
set( 'where', csn, node ); | ||
set( 'groupBy', csn, node ); | ||
set( 'having', csn, node ); | ||
// for both ---------------------------------------------------------------- | ||
set( 'orderBy', csn, node ); | ||
set( 'limit', csn, node ); | ||
return (node.op.val === 'query') ? { SELECT: csn } : { SET: csn }; | ||
set( 'limit', csn, node ); // TODO XSN: also offset | ||
set( '$extra', csn, node ); | ||
return { SET: csn }; | ||
} | ||
@@ -558,3 +573,3 @@ | ||
// TODO: CSN: FROM ((SELECT...)) as -> also add 'subquery' op? - Together | ||
// with []-elimination in FROM... | ||
// with []-elimination in FROM... -> normal standard() | ||
if (node.join) { // XSN TODO: remove '…Outer' | ||
@@ -568,16 +583,16 @@ // binary (without additions) -> n-ary - the while loop should be done in | ||
set( 'on', join, node ); | ||
return join; | ||
return extra( join, node ); | ||
} | ||
else if (!node.path) { | ||
return addExplicitAs( query( node ), node.name ); | ||
return addExplicitAs( query( node ), node.name ); // $extra inside SELECT/SET | ||
} | ||
else if (!node._artifact || node._artifact.main) { | ||
return addExplicitAs( artifactRef( node, null ), node.name ); | ||
return extra( addExplicitAs( artifactRef( node, null ), node.name ), node ); | ||
} | ||
else | ||
return addExplicitAs( artifactRef( node, null ), node.name, function(id) { | ||
return extra( addExplicitAs( artifactRef( node, null ), node.name, function(id) { | ||
let name = node._artifact.name.absolute; | ||
let dot = name.lastIndexOf('.'); | ||
return name.substring( dot+1 ) !== id; | ||
}); | ||
}), node ); | ||
} | ||
@@ -595,11 +610,7 @@ | ||
csn_gensrc = true; | ||
addExplicitAs( Object.assign( col, expression(elem.value) ), elem.name, function(id) { | ||
// $user should be rendered as { ref: ['$user','id'], as: '$user' } | ||
let path = elem.value && elem.value.path; | ||
let last = path[ path.length-1 ]; | ||
return (last && last.id) !== id; | ||
}); | ||
set( 'key', col, elem ); | ||
addExplicitAs( Object.assign( col, expression(elem.value) ), | ||
elem.name, neqPath( elem.value ) ); | ||
if (elem._typeIsExplicit || elem.redirected) { // TODO XSN: introduce $inferred | ||
col.cast = {}; | ||
col.cast = {}; // TODO: what about $extra in cast? | ||
for (let prop of typeProperties) | ||
@@ -616,6 +627,6 @@ set( prop, col.cast, elem ); | ||
} | ||
columns.push( col ); | ||
columns.push( extra( col, elem ) ); | ||
} | ||
function orderBy( node ) { // TODO XSN: flatten (no extra 'value') | ||
function orderBy( node ) { // TODO XSN: flatten (no extra 'value'), part of expression | ||
let expr = expression( node.value ); | ||
@@ -626,6 +637,6 @@ if (node.sort) | ||
expr.nulls = node.nulls.val; | ||
return expr; | ||
return extra( expr, node ); | ||
} | ||
function limit( limit, csn, node ) { // XSN TODO: use same structure | ||
function limit( limit, csn, node ) { // XSN TODO: use same structure, $extra | ||
let rows = expression( limit ); | ||
@@ -637,2 +648,13 @@ return (node.offset) | ||
function $extra( obj, csn ) { | ||
for (let prop of Object.keys( obj ).sort()) | ||
csn[prop] = obj[prop]; | ||
} | ||
function extra( csn, node ) { | ||
if (node && node.$extra) | ||
$extra( node.$extra, csn ); | ||
return csn; | ||
} | ||
function addExplicitAs( node, name, implicit ) { | ||
@@ -644,5 +666,9 @@ if (name && (!name.calculated && !name.$inferred || implicit && implicit(name.id) )) | ||
// normalize CSN: sort properties alphabetically if no prototype (also sort model.definitions) | ||
// for "niceness", put the following properties first: op, kind, name | ||
const earlyProperties = { op: '\x01', kind: '\x02', name: '\x03' }; | ||
function neqPath( ref ) { | ||
let path = ref && ref.path; | ||
return path && function( id ) { | ||
let last = path[ path.length-1 ]; | ||
return (last && last.id) !== id; | ||
}; | ||
} | ||
@@ -652,6 +678,5 @@ function compareProperties( a, b ) { | ||
return 0; | ||
else if ((earlyProperties[a] || a) < (earlyProperties[b] || b)) | ||
return -1; | ||
else | ||
return 1; | ||
let oa = propertyOrder[a] || propertyOrder[a.charAt()]; | ||
let ob = propertyOrder[b] || propertyOrder[b.charAt()]; | ||
return oa - ob || (a < b ? -1 : 1); | ||
} | ||
@@ -938,3 +963,16 @@ | ||
function getCompilerVersion() { | ||
return require('../../package.json').version; | ||
} | ||
function setMetaProperty( csn, model ) { | ||
csn.meta = model.meta || {}; | ||
csn.meta.creator = 'CDS Compiler v' + getCompilerVersion(); | ||
} | ||
// 0.2 - pre-release version | ||
function setCsnVersion( csn ) { | ||
csn.$version = "0.2" | ||
} | ||
module.exports = { compactModel, compactQuery, compactExpr }; |
@@ -486,3 +486,3 @@ /** | ||
walkNodesExFn, | ||
walkNodesExFnPath, | ||
walkNodesExFnPath, | ||
walkWithPath, | ||
@@ -489,0 +489,0 @@ walkNodesExWithPath, |
@@ -149,2 +149,3 @@ // Error strategy with special handling for (non-reserved) keywords | ||
case ATNState.STAR_LOOP_BACK: | ||
// TODO: do not delete a '}' | ||
this.reportUnwantedToken(recognizer); | ||
@@ -212,3 +213,3 @@ var expecting = new IntervalSet.IntervalSet(); | ||
'Error', 'Extraneous $(OFFENDING), expecting $(EXPECTING)' ); | ||
err.expectedTokens = expecting; | ||
err.expectedTokens = expecting; // TODO: remove next token? | ||
if (!recognizer.avoidErrorListeners) // with --trace-parser or --trace-parser-ambig | ||
@@ -327,4 +328,4 @@ recognizer.notifyErrorListeners( err.message, token, err ); | ||
} | ||
if (recognizer.$nextTokensToken === recognizer.$removeSemiFor) | ||
names = names.filter( n => n !== "';'" && n !== "'}'" ); | ||
if (recognizer.$adaptExpectedToken && recognizer.$nextTokensToken === recognizer.$adaptExpectedToken) | ||
names = names.filter( n => !recognizer.$adaptExpectedExcludes.includes( n ) ); | ||
else if (names.includes("';'")) | ||
@@ -389,2 +390,3 @@ names = names.filter( n => n !== "'}'" ); | ||
var identType = recognizer.constructor.Identifier; | ||
var hideAltsType = recognizer.constructor.HideAlternatives; | ||
var beforeUnreserved = recognizer.constructor.Number; | ||
@@ -397,3 +399,5 @@ if (!identType || !beforeUnreserved || beforeUnreserved + 2 > identType) | ||
var orig_addInterval = expected.addInterval; | ||
var orig_addSet = expected.addSet; | ||
expected.addInterval = addInterval; | ||
expected.addSet = addSet; | ||
let lookBusy = new antlr4.Utils.Set(); | ||
@@ -424,2 +428,7 @@ let calledRules = new antlr4.Utils.BitSet(); | ||
function addSet(other) { | ||
if (!other.contains( hideAltsType )) | ||
orig_addSet.call( this, other ); | ||
} | ||
// Add an interval `v` to the IntervalSet `this`. If `v` contains the token | ||
@@ -426,0 +435,0 @@ // type `Identifier`, do not add non-reserved keywords in `v`. |
@@ -47,2 +47,3 @@ // Generic ANTLR parser class with AST-building functions | ||
setOnce, | ||
setMaxCardinality, | ||
hanaFlavorOnly, | ||
@@ -52,2 +53,3 @@ csnParseOnly, | ||
noSemicolonHere, | ||
excludeExpected, | ||
isStraightBefore, | ||
@@ -96,3 +98,3 @@ constructor: GenericAntlrParser // keep this last | ||
(loc instanceof antlr4.CommonToken) ? this.tokenLocation(loc) : loc, | ||
...args ); | ||
null, ...args ); | ||
} | ||
@@ -132,3 +134,4 @@ | ||
var t = this.getCurrentToken(); | ||
this.$removeSemiFor = t; | ||
this.$adaptExpectedToken = t; | ||
this.$adaptExpectedExcludes = ["';'", "'}'"]; | ||
this.$nextTokensToken = t; | ||
@@ -142,2 +145,11 @@ this.$nextTokensContext = null; // match() of WITH does not reset | ||
function excludeExpected( excludes ) { | ||
if (excludes) { | ||
var t = this.getCurrentToken(); | ||
this.$adaptExpectedToken = t; | ||
this.$adaptExpectedExcludes = (excludes instanceof Array) ? excludes : [excludes]; | ||
this.$nextTokensToken = t; | ||
} | ||
} | ||
// // Special function for rule `requiredSemi` before return $ctx | ||
@@ -235,3 +247,4 @@ // function braceForSemi() { | ||
// identifer (TODO: check for dots etc/ here). | ||
function identAst( token ) { | ||
function identAst( token, category ) { | ||
token.isIdentifier = category; | ||
var id = token.text; | ||
@@ -448,5 +461,16 @@ if (token.type !== this.constructor.Identifier && !/^[a-zA-Z]+$/.test( id )) | ||
function setMaxCardinality( art, token, max ) { | ||
let location = this.tokenLocation( token ); | ||
if (art.cardinality) { | ||
this.message( 'syntax-repeated-cardinality', location, { token: token.text }, | ||
'Warning', 'The target cardinality has already been specified - ignored $(TOKEN)' ); | ||
} | ||
else { | ||
art.cardinality = { targetMax: Object.assign( {location}, max ), location }; | ||
} | ||
} | ||
module.exports = { | ||
genericAntlrParser: GenericAntlrParser | ||
}; |
@@ -35,2 +35,4 @@ // Main entry point for the Research Vanilla CDS Compiler | ||
// 0.1.99 : Like 0.1.0, but with new-style CSN | ||
// 0.2 : same as 0.1.99, but with new top-level properties: $version, meta | ||
// TODO: move csn versioning in to-csn.js | ||
function csnVersion( options ) { | ||
@@ -40,3 +42,3 @@ // Merge default options | ||
// New-style CSN vs old-style CSN | ||
return options.newCsn ? '0.1.99' : '0.1.0'; | ||
return options.newCsn ? '0.2' : '0.1.0'; | ||
} | ||
@@ -59,3 +61,2 @@ | ||
var fs = require('fs'); | ||
var { compactForService } = require('./transform/forOdata'); | ||
var { getDefaultTntFlavorOptions, propagateIncludesForTnt } = require('./transform/tntSpecific'); | ||
@@ -84,5 +85,2 @@ var csn2edm = require('./edm/csn2edm'); | ||
model.$frontend = 'json'; | ||
// TODO: I (CW) do not think that the following is a good idea... | ||
if (model.version && model.version.csn === "0.1.99") | ||
options.newCsn = true; // force new version TODO optimize? | ||
return model; | ||
@@ -95,3 +93,3 @@ } else if (options.fallbackParser || ['.cds', '.hdbcds', '.hdbdd'].includes(ext)) | ||
message( 'file-unknown-ext', | ||
{ filename, start: { offset: 0, line: 1, column: 1 } }, | ||
{ filename, start: { offset: 0, line: 1, column: 1 } }, null, | ||
{ file: ext && ext.slice(1), '#': !ext && 'none' }, | ||
@@ -374,3 +372,3 @@ 'Error', { | ||
for (let from of dep.usingFroms) | ||
message( 'file-not-readable', from.location, { file: resolved }, | ||
message( 'file-not-readable', from.location, null, { file: resolved }, | ||
'Error', 'Cannot read file $(FILE)' ); | ||
@@ -380,3 +378,3 @@ } | ||
for (let from of dep.usingFroms) | ||
message( 'file-unknown-local', from.location, { file: dep.module }, | ||
message( 'file-unknown-local', from.location, null, { file: dep.module }, | ||
'Error', 'Cannot find local module $(FILE)' ); | ||
@@ -387,3 +385,3 @@ } | ||
for (let from of dep.usingFroms) | ||
message( 'file-unknown-package', from.location, | ||
message( 'file-unknown-package', from.location, null, | ||
{ file: dep.module, '#': internal }, | ||
@@ -475,18 +473,11 @@ 'Error', { | ||
var model = { sources, options, messages: [].concat( ...messagesArray ) }; // flatten | ||
if (!options.testMode) | ||
model.version = versionObject( options ); | ||
if (!options.parseOnly) | ||
model = resolve( define(model) ); | ||
if (options.reAugmented) { // for testing | ||
let filename = Object.keys(sources)[0]; | ||
let source = options.newCsn ? compactModel( model, options ) : compactJson( model, options ); | ||
let parseCSNfromJson = require("./json/from-json"); | ||
source = parseCSNfromJson( JSON.stringify(source), filename, options ); | ||
source.$frontend = 'json'; | ||
model = { sources: Object.create(null), options, messages: [] }; | ||
model.sources[filename] = source; | ||
if (options.reAugmented === 'recompile') | ||
model = resolve( define(model) ); | ||
if (!options.testMode) { | ||
model.version = versionObject( options );//TODO remove | ||
model.meta = {}; // provide initial central meta object | ||
} | ||
// if (!options.parseOnly) return define(model); | ||
if (!options.parseOnly) { | ||
define(model); | ||
resolve(model); | ||
} | ||
@@ -545,2 +536,16 @@ assertConsistency( model ); | ||
backends.optionProcessor.command('toTntSpecificOutput') | ||
.option('-h, --help') | ||
.help(` | ||
Usage: cdsc toTntSpecificOutput [options] <file...> | ||
(internal, subject to change): Generate backward-compatible output for the TNT project. Should | ||
ultimately be replaced by "cdsc toOdata --version v2 --xml --separate --csn". | ||
Options | ||
-h, --help Display this help text | ||
`) | ||
; | ||
// TNT-specific, temporary: Transforms augmented CSN 'model' into an object '{ annotations, metadata, csn, services }' | ||
@@ -594,5 +599,7 @@ // containing | ||
// (unfortunately we used to deliver this really as a V4 version, which was probably unnecessary ...) | ||
// Compact the model | ||
let compactedModel = compactModel(odataResult._augmentedCsn); | ||
setProp(compactedModel, 'messages', odataResult._augmentedCsn.messages); | ||
for (let serviceName in result.services) { | ||
let forOdata = compactForService(odataResult._augmentedCsn, serviceName); | ||
let l_annotations_edm = csn2edm(forOdata, mergeOptions(options, { toOdata : { version : 'v4' }})); | ||
let l_annotations_edm = csn2edm(compactedModel, serviceName, mergeOptions(options, { toOdata : { version : 'v4' }})); | ||
result.services[serviceName].annotations = l_annotations_edm.toXML('annotations'); | ||
@@ -614,5 +621,4 @@ } | ||
// Return the 'version' object that should appear in CSNs generated by this compiler. | ||
function versionObject( options ) { | ||
function versionObject( options ) { // TODO remove | ||
return { | ||
creator: 'CDS Compiler v' + version(), | ||
csn: csnVersion( options ), | ||
@@ -672,2 +678,4 @@ } | ||
toOdata : backends.toOdata, | ||
preparedCsnToEdmx : backends.preparedCsnToEdmx, | ||
preparedCsnToEdm : backends.preparedCsnToEdm, | ||
toCdl : backends.toCdl, | ||
@@ -674,0 +682,0 @@ toSwagger, |
@@ -5,15 +5,7 @@ 'use strict' | ||
// Return true if 'node' is a projection entity | ||
function isProjection(node) { | ||
// FIXME: Looking at 'source' is probably not correct here? | ||
return node.kind == 'entity' && node.source != undefined; | ||
} | ||
// TODO: Are these functions written with derived types in mind (or even that | ||
// an element definition uses an association type)? Some are, others not... | ||
// Return true if 'node' is a view entity | ||
function isView(node) { | ||
// FIXME: Do we really have to consider 'kind' or is it sufficient to look at 'queries'? | ||
return node.kind == 'view' && node.queries != undefined; | ||
} | ||
// Return true if 'node is a managed association element | ||
// TODO: what about elements having a type, which (finally) is a assoc? | ||
function isManagedAssociationElement(node) { | ||
@@ -411,4 +403,2 @@ return node.kind == 'element' && node.target != undefined && node.onCond == undefined; | ||
module.exports = { | ||
isProjection, | ||
isView, | ||
isManagedAssociationElement, | ||
@@ -415,0 +405,0 @@ isAssociation, |
@@ -8,2 +8,3 @@ "use strict"; | ||
const keywords = require('../base/keywords'); | ||
const version = require('../../package.json').version; | ||
@@ -50,3 +51,4 @@ // Render the CSN model 'model' to CDS source text. One source is created per | ||
result[plainNames ? uppercaseAndUnderscore(artifactName) : artifactName] | ||
= renderNamespaceDeclaration(artifactName, env) + renderUsings(artifactName, env) + sourceStr; | ||
= `${options.testMode ? '' : `// generated by cds-compiler version ${version} \n`}` | ||
+ renderNamespaceDeclaration(artifactName, env) + renderUsings(artifactName, env) + sourceStr; | ||
} | ||
@@ -136,3 +138,3 @@ } | ||
// somewhat difficult because this kind of absolute path is quite unusual). In order not to have to pass | ||
// the current artifact name down through the stack to renderExpr, we just put it nto the env. | ||
// the current artifact name down through the stack to renderExpr, we just put it into the env. | ||
env.currentArtifactName = artifactName; | ||
@@ -461,3 +463,3 @@ if (art.query) { | ||
if (source.SELECT || source.SET) { | ||
let result = `(${renderQuery(source, false, increaseIndent(env))})`; | ||
let result = `(${renderQuery(source, false, 'view', increaseIndent(env))})`; | ||
if (source.as) { | ||
@@ -575,10 +577,20 @@ result += ` as ${quoteId(source.as)}`; | ||
// Render a view | ||
// Render a view. If '$syntax' is set (to 'projection', 'view', 'entity'), | ||
// the view query is rendered in the requested syntax style, otherwise it | ||
// is rendered as a view. | ||
function renderView(artifactName, art, env) { | ||
let syntax = art.$syntax || 'view'; | ||
let result = renderAnnotationAssignments(art, env); | ||
result += env.indent + 'view ' + renderArtifactName(artifactName, env); | ||
result += `${env.indent}${syntax == 'projection' ? 'entity' : syntax} ${renderArtifactName(artifactName, env)}`; | ||
if (art.params) { | ||
let childEnv = increaseIndent(env); | ||
result += ' with parameters\n' + Object.keys(art.params).map(name => renderParameter(name, art.params[name], childEnv)).join(',\n') + '\n'; | ||
result += env.indent + 'as '; | ||
let parameters = Object.keys(art.params).map(name => renderParameter(name, art.params[name], childEnv)).join(',\n'); | ||
// HANA only understands the 'with parameters' syntax' | ||
if (options.forHana) { | ||
result += ` with parameters\n${parameters}\n${env.indent}as `; | ||
} | ||
// Otherwise prefer simple parentheses | ||
else { | ||
result += `(\n${parameters}\n${env.indent}) as `; | ||
} | ||
} | ||
@@ -588,3 +600,3 @@ else { | ||
} | ||
result += renderQuery(art.query, true, env); | ||
result += renderQuery(art.query, true, syntax, env); | ||
result += ';\n'; | ||
@@ -598,4 +610,5 @@ result += renderQueryElementAnnotations(artifactName, art, env); | ||
// If 'isLeadingQuery' is true, mixins, actions and functions of 'art' are | ||
// also rendered into the query. | ||
function renderQuery(query, isLeadingQuery, env) { | ||
// also rendered into the query. Use 'syntax'style ('projection', 'view', | ||
// or 'entity') | ||
function renderQuery(query, isLeadingQuery, syntax, env) { | ||
let result = ''; | ||
@@ -605,6 +618,6 @@ // Set operator, like UNION, INTERSECT, ... | ||
// First arg may be leading query | ||
result += `(${renderQuery(query.SET.args[0], isLeadingQuery, env)}` | ||
result += `(${renderQuery(query.SET.args[0], isLeadingQuery, 'view', env)}` | ||
// FIXME: Clarify if set operators can be n-ary (assuming binary here) | ||
if (query.SET.op) { | ||
result += `\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} ${renderQuery(query.SET.args[1], false, env)}`; | ||
result += `\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} ${renderQuery(query.SET.args[1], false, 'view', env)}`; | ||
} | ||
@@ -628,3 +641,9 @@ result += ')'; | ||
let childEnv = increaseIndent(env); | ||
result += `select from ${renderViewSource(select.from, env)}`; | ||
if (syntax == 'projection') { | ||
result += `projection on ${renderViewSource(select.from, env)}`; | ||
} else if (syntax == 'view' || syntax == 'entity') { | ||
result += `select from ${renderViewSource(select.from, env)}`; | ||
} else { | ||
throw new Error(`Unknown query syntax: ${syntax}`); | ||
} | ||
if (isLeadingQuery && select.mixin) { | ||
@@ -820,3 +839,7 @@ result += ' mixin {\n' | ||
// For HANA CDS, we need a 'type of' | ||
return (options.forHana ? 'type of ' : '') + renderAbsolutePath(elm.type, env); | ||
result += (options.forHana ? 'type of ' : '') + renderAbsolutePath(elm.type, env); | ||
if (elm.enum) { | ||
result += renderEnum(elm.enum, env); | ||
} | ||
return result; | ||
} | ||
@@ -835,11 +858,3 @@ | ||
if (elm.enum) { | ||
result += ' enum {\n'; | ||
let childEnv = increaseIndent(env); | ||
for (let name in elm.enum) { | ||
let enumConst = elm.enum[name]; | ||
result += renderAnnotationAssignments(enumConst, childEnv); | ||
let enumValue = { val: enumConst.val === undefined ? name : enumConst.val }; | ||
result += childEnv.indent + quoteId(name) + ' = ' + renderExpr(enumValue, childEnv) + ';\n'; | ||
} | ||
result += env.indent + '}'; | ||
result += renderEnum(elm.enum, env); | ||
} | ||
@@ -849,2 +864,16 @@ return result; | ||
// Render the 'enum { ... } part of a type declaration | ||
function renderEnum(enumPart, env) { | ||
let result = ' enum {\n'; | ||
let childEnv = increaseIndent(env); | ||
for (let name in enumPart) { | ||
let enumConst = enumPart[name]; | ||
result += renderAnnotationAssignments(enumConst, childEnv); | ||
let enumValue = { val: enumConst.val === undefined ? name : enumConst.val }; | ||
result += childEnv.indent + quoteId(name) + ' = ' + renderExpr(enumValue, childEnv) + ';\n'; | ||
} | ||
result += env.indent + '}'; | ||
return result; | ||
} | ||
// Render an annotation value (somewhat like a simplified expression, with slightly different | ||
@@ -947,7 +976,7 @@ // representation) | ||
// renderQuery for SELECT does not bring its own parentheses (because it is also used in renderView) | ||
return `(${renderQuery(x, false, increaseIndent(env))})`; | ||
return `(${renderQuery(x, false, 'view', increaseIndent(env))})`; | ||
} | ||
else if (x.SET) { | ||
// renderQuery for SET always brings its own parentheses (because it is also used in renderViewSource) | ||
return `${renderQuery(x, false, increaseIndent(env))}`; | ||
return `${renderQuery(x, false, 'view', increaseIndent(env))}`; | ||
} | ||
@@ -991,4 +1020,3 @@ else { | ||
&& (['$projection', '$self', '$parameters'].includes(s) | ||
|| getMagicVariables().map(id => id.toLowerCase()).includes(s.toLowerCase()) | ||
|| s == 'self' && options.oldstyleSelf)) { | ||
|| getMagicVariables().map(id => id.toLowerCase()).includes(s.toLowerCase()))) { | ||
return s; | ||
@@ -1332,3 +1360,2 @@ } | ||
if (id == '$projection' || id == '$self' || | ||
id == 'self' && options.oldstyleSelf || | ||
id == '$user' || id == '$now') { | ||
@@ -1335,0 +1362,0 @@ return id; |
@@ -5,3 +5,4 @@ | ||
const { CompilationError, hasErrors, sortMessages } = require('../base/messages'); | ||
const { mergeOptions, isAssociation, getTopLevelArtifactNameOf, getParentNameOf } = require('../model/modelUtils'); | ||
const { mergeOptions, getTopLevelArtifactNameOf, getParentNameOf, getParentNamesOf } = require('../model/modelUtils'); | ||
const { compactModel } = require('../json/to-csn'); | ||
@@ -29,5 +30,20 @@ // Render the augmented CSN 'model' to SQL DDL statements renaming existing tables and their | ||
// FIXME: This should happen in the caller | ||
let csn = compactModel(model); | ||
// Create artificial namespace objects, so that each artifact has parents up to top-level. | ||
// FIXME: This is actually only necessary to make 'getParentNameOf' work - should be reworked | ||
for (let artifactName in csn.definitions) { | ||
for (let parentName of getParentNamesOf(artifactName)) { | ||
if (!csn.definitions[parentName]) { | ||
csn.definitions[parentName] = { | ||
kind : 'namespace', | ||
}; | ||
} | ||
} | ||
} | ||
// Render each artifact on its own | ||
for (let artifactName in model.definitions) { | ||
let sourceStr = renameTableAndColumns(model.definitions[artifactName]); | ||
for (let artifactName in csn.definitions) { | ||
let sourceStr = renameTableAndColumns(artifactName, csn.definitions[artifactName]); | ||
@@ -49,7 +65,7 @@ if (sourceStr != '') { | ||
// Do not rename anything if the names are identical. | ||
function renameTableAndColumns(art) { | ||
function renameTableAndColumns(artifactName, art) { | ||
let resultStr = ''; | ||
if (art.kind == 'entity' && !art.source) { | ||
let beforeTableName = quoteSqlId(absoluteCdsName(art.name.absolute)); | ||
let afterTableName = plainSqlId(art.name.absolute); | ||
if (art.kind == 'entity' && !art.query) { | ||
let beforeTableName = quoteSqlId(absoluteCdsName(artifactName)); | ||
let afterTableName = plainSqlId(artifactName); | ||
@@ -64,7 +80,7 @@ if (beforeTableName != afterTableName) { | ||
let beforeColumnName = quoteSqlId(e.name.id); | ||
let afterColumnName = plainSqlId(e.name.id); | ||
let beforeColumnName = quoteSqlId(name); | ||
let afterColumnName = plainSqlId(name); | ||
if (!e._ignore) { | ||
if (isAssociation(e._finalType.type)) { | ||
if (e.target) { | ||
result = 'ALTER TABLE ' + afterTableName + ' DROP ASSOCIATION ' + beforeColumnName + ';\n' | ||
@@ -89,3 +105,3 @@ } | ||
} | ||
let topLevelName = getTopLevelArtifactNameOf(name, model); | ||
let topLevelName = getTopLevelArtifactNameOf(name, csn); | ||
let namespaceName = getParentNameOf(topLevelName); | ||
@@ -116,2 +132,2 @@ if (namespaceName) { | ||
toRenameDdl, | ||
}; | ||
}; |
@@ -5,6 +5,7 @@ | ||
const { CompilationError, hasErrors, sortMessages } = require('../base/messages'); | ||
const { isAssociation, getTopLevelArtifactNameOf, getParentNameOf } = require('../model/modelUtils'); | ||
const renderUtils = require('./renderUtils'); | ||
const { getTopLevelArtifactNameOf, getParentNameOf, getParentNamesOf, getLastPartOf, getLastPartOfRef } = require('../model/modelUtils'); | ||
const keywords = require('../base/keywords'); | ||
const alerts = require('../base/alerts'); | ||
const { compactModel } = require('../json/to-csn'); | ||
const version = require('../../package.json').version; | ||
@@ -28,3 +29,3 @@ // Render the CSN model 'model' to SQL DDL statements. One statement is created | ||
function toSqlDdl(model) { | ||
const { error, signal } = alerts(model); | ||
const { error, signal, warning, info } = alerts(model); | ||
@@ -34,9 +35,2 @@ // Use model options | ||
const { renderExpressionOrCondition, renderJoinOp } = renderUtils.getRenderUtils(model, options, { | ||
renderPathOrValue, | ||
renderQuery, | ||
increaseIndent, | ||
decreaseIndent, | ||
}); | ||
// FIXME: Currently requires 'options.forHana', because it can only render HANA-ish SQL dialect | ||
@@ -47,2 +41,17 @@ if (!options.forHana) { | ||
// FIXME: This should happen in the caller | ||
let csn = compactModel(model); | ||
// Create artificial namespace objects, so that each artifact has parents up to top-level. | ||
// FIXME: This is actually only necessary to make 'getParentNameOf' work - should be reworked | ||
for (let artifactName in csn.definitions) { | ||
for (let parentName of getParentNamesOf(artifactName)) { | ||
if (!csn.definitions[parentName]) { | ||
csn.definitions[parentName] = { | ||
kind : 'namespace', | ||
}; | ||
} | ||
} | ||
} | ||
// The final result in hdb-kind-specific form, without leading CREATE, without trailing newlines | ||
@@ -59,6 +68,3 @@ // (note that the order here is relevant for transmission into 'resultObj.sql' below) | ||
// Render each artifact on its own | ||
let artifactNames = Object.keys(model.definitions); | ||
// Note: Sorting here just to minimize the diff with the new version based on new-style compact CSN, which is always sorted | ||
artifactNames.sort(); | ||
for (let artifactName of artifactNames) { | ||
for (let artifactName in csn.definitions) { | ||
// This environment is passed down the call hierarchy, for dealing with | ||
@@ -70,4 +76,5 @@ // indentation issues | ||
} | ||
renderArtifactInto(model.definitions[artifactName], resultObj, env); | ||
renderArtifactInto(artifactName, csn.definitions[artifactName], resultObj, env); | ||
} | ||
// Throw up if we have errors | ||
@@ -82,2 +89,3 @@ if (hasErrors(model.messages)) { | ||
let sql = Object.create(null); | ||
let sqlVersionLine = `-- generated by cds-compiler version ${version}\n`; | ||
for (let hdbKind of Object.keys(resultObj)) { | ||
@@ -90,11 +98,15 @@ for (let name in resultObj[hdbKind]) { | ||
} | ||
sql[name] = `CREATE ${sourceString};`; | ||
sql[name] = `${options.testMode ? '' : sqlVersionLine}CREATE ${sourceString};`; | ||
if (!options.testMode) { | ||
resultObj[hdbKind][name] = sqlVersionLine + resultObj[hdbKind][name]; | ||
} | ||
} | ||
} | ||
resultObj.sql = sql; | ||
return resultObj; | ||
// Render an artifact into the appropriate dictionary of 'resultObj'. | ||
function renderArtifactInto(art, resultObj, env) { | ||
function renderArtifactInto(artifactName, art, resultObj, env) { | ||
// Ignore whole artifacts if forHana says so | ||
@@ -106,25 +118,16 @@ if (art._ignore) { | ||
case 'entity': | ||
if (art.source) { | ||
let result = renderProjection(art, env); | ||
case 'view': | ||
if (art.query) { | ||
let result = renderView(artifactName, art, env); | ||
if (result) { | ||
resultObj.hdbview[art.name.absolute] = result; | ||
resultObj.hdbview[artifactName] = result; | ||
} | ||
} else { | ||
let result = renderEntityInto(art, resultObj, env); | ||
if (result) { | ||
resultObj.hdbentity[art.name.absolute] = result; | ||
} | ||
renderEntityInto(artifactName, art, resultObj, env); | ||
} | ||
break; | ||
case 'view': { | ||
let result = renderView(art, env); | ||
if (result) { | ||
resultObj.hdbview[art.name.absolute] = result; | ||
} | ||
break; | ||
} | ||
case 'type': { | ||
let result = renderType(art, env); | ||
let result = renderType(artifactName, art, env); | ||
if (result) { | ||
resultObj.hdbtabletype[art.name.absolute] = result; | ||
resultObj.hdbview[artifactName] = result; | ||
} | ||
@@ -148,23 +151,23 @@ break; | ||
// dictionaries of 'resultObj'. | ||
function renderEntityInto(art, resultObj, env) { | ||
// FIXME: Took this from toCdl, but do entities have parameters yet at all? Views apparently don't ... I am confused | ||
if (art.params && Object.keys(art.params)[0]) { | ||
let firstParam = art.params[Object.keys(art.params)[0]]; | ||
signal(error`"${art.name.absolute}": Entities with parameters are not supported for conversion to SQL`, firstParam.location); | ||
} | ||
function renderEntityInto(artifactName, art, resultObj, env) { | ||
let childEnv = increaseIndent(env); | ||
let tc = art.technicalConfig; | ||
let storeType = (tc && tc.storeType) ? renderPathOrValue(tc.storeType) + ' ': ''; | ||
// in 'hdbtable' files, COLUMN or ROW is mandatory, and COLUMN is the default | ||
if (options.toSql.dialect == 'hana' && storeType == '') { | ||
storeType += 'COLUMN '; | ||
let hanaTc = art.technicalConfig && art.technicalConfig.hana; | ||
let result = ''; | ||
// Only HANA has row/column tables | ||
if (options.toSql.dialect == 'hana') { | ||
if (hanaTc && hanaTc.storeType) { | ||
// Explicitly specified | ||
result += art.technicalConfig.hana.storeType.toUpperCase() + ' '; | ||
} | ||
else if (options.toSql.dialect == 'hana') { | ||
// in 'hdbtable' files, COLUMN or ROW is mandatory, and COLUMN is the default | ||
result += 'COLUMN '; | ||
} | ||
} | ||
let result = storeType + 'TABLE ' + quoteSqlId(absoluteCdsName(art.name.absolute)); | ||
result += 'TABLE ' + quoteSqlId(absoluteCdsName(artifactName)); | ||
result += ' (\n'; | ||
result += Object.keys(art.elements).map(name => renderElement(art.elements[name], childEnv)) | ||
result += Object.keys(art.elements).map(name => renderElement(artifactName, name, art.elements[name], getFzIndex(name, hanaTc), childEnv)) | ||
.filter(s => s != '') | ||
.join(',\n'); | ||
let primaryKeys = Object.keys(art.elements).filter(name => art.elements[name].key && art.elements[name].key.val) | ||
let primaryKeys = Object.keys(art.elements).filter(name => art.elements[name].key) | ||
.filter(name => !art.elements[name]._ignore) | ||
@@ -178,5 +181,9 @@ .map(name => quoteSqlId(name)) | ||
} | ||
result += env.indent + ')' + renderTechnicalConfiguration(tc, childEnv); | ||
result += env.indent + ')'; | ||
let associations = Object.keys(art.elements).map(name => renderAssociationElement(art.elements[name], childEnv)) | ||
if (options.toSql.dialect === 'hana') { | ||
result += renderTechnicalConfiguration(art.technicalConfig, childEnv); | ||
} | ||
let associations = Object.keys(art.elements).map(name => renderAssociationElement(name, art.elements[name], childEnv)) | ||
.filter(s => s != '') | ||
@@ -188,34 +195,45 @@ .join(',\n'); | ||
} | ||
// Only HANA has indices | ||
// FIXME: Really? We should provide a DB-agnostic way to specify that | ||
if (options.toSql.dialect === 'hana') { | ||
renderIndexesInto(art.technicalConfig && art.technicalConfig.hana.indexes, artifactName, resultObj, env); | ||
} | ||
resultObj.hdbtable[artifactName] = result; | ||
} | ||
result += renderIndexesInto(tc, art.name.absolute, resultObj, childEnv); | ||
resultObj.hdbtable[art.name.absolute] = result; | ||
// Retrieve the 'fzindex' (fuzzy index) property (if any) for element 'elemName' from hanaTc (if defined) | ||
function getFzIndex(elemName, hanaTc) { | ||
if (!hanaTc || !hanaTc.fzindexes || !hanaTc.fzindexes[elemName]) { | ||
return undefined; | ||
} | ||
if (hanaTc.fzindexes[elemName][0] instanceof Array) { | ||
// FIXME: Should we allow multiple fuzzy search indices on the same column at all? | ||
// And if not, why do we wrap this into an array? | ||
return hanaTc.fzindexes[elemName][hanaTc.fzindexes[elemName].length - 1]; | ||
} | ||
else { | ||
return hanaTc.fzindexes[elemName]; | ||
} | ||
} | ||
// Render an element (of an entity or type, not a projection or view). | ||
// Ignore association elements (those are rendered later by renderAssociationElement) | ||
// Render an element 'elm' with name 'elementName' (of an entity or type, not of a | ||
// projection or view), optionally with corresponding fuzzy index 'fzindex' from the | ||
// technical configuration. | ||
// Ignore association elements (those are rendered later by renderAssociationElement). | ||
// Use 'artifactName' only for error output. | ||
// Return the resulting source string (no trailing LF). | ||
function renderElement(elm, env) { | ||
function renderElement(artifactName, elementName, elm, fzindex, env) { | ||
// Ignore if forHana says so, or if it is an association | ||
if (elm._ignore) { | ||
if (elm._ignore || elm.target) { | ||
return ''; | ||
} | ||
if (isAssociation(elm._finalType.type)) { | ||
return ''; | ||
} | ||
let result = env.indent + quoteSqlId(elm.name.id) + ' ' | ||
+ renderTypeReference(elm) | ||
let result = env.indent + quoteSqlId(elementName) + ' ' | ||
+ renderTypeReference(artifactName, elementName, elm) | ||
+ renderNullability(elm); | ||
if (elm.default) { | ||
result += ' DEFAULT ' + renderPathOrValue(elm.default, env); | ||
result += ' DEFAULT ' + renderExpr(elm.default, env); | ||
} | ||
if(elm._fzindex) { | ||
result += ' FUZZY SEARCH INDEX ON'; | ||
if(elm._fzindex.fuzzy) { | ||
result += ' FUZZY SEARCH MODE'; | ||
if(elm._fzindex.fuzzy.mode) { | ||
result += ' ' + renderPathOrValue(elm._fzindex.fuzzy.mode); | ||
} | ||
} | ||
// Only HANA has fuzzy indizes | ||
if (fzindex && options.toSql.dialect === 'hana') { | ||
result += ' ' + renderExpr(fzindex, env); | ||
} | ||
@@ -225,10 +243,11 @@ return result; | ||
// Render those elements from 'elements' that are associations, in the style required for | ||
// Render an element 'elm' with name 'elementName' if it is an association, in the style required for | ||
// HANA native associations (e.g. 'MANY TO ONE JOIN "source" AS "assoc" ON (condition)'). | ||
// Return a string with one line per association, or an empty string if there are none | ||
function renderAssociationElement(elm, env) { | ||
// Return a string with one line per association element, or an empty string if the element | ||
// is not an association. | ||
function renderAssociationElement(elementName, elm, env) { | ||
let result = ''; | ||
if (isAssociation(elm._finalType.type)) { | ||
if (elm.target) { | ||
result += env.indent + 'MANY TO '; | ||
if (elm.cardinality && elm.cardinality.targetMax && (elm.cardinality.targetMax.val == '*' || Number(elm.cardinality.targetMax.val) > 1)) { | ||
if (elm.cardinality && elm.cardinality.max && (elm.cardinality.max == '*' || Number(elm.cardinality.max) > 1)) { | ||
result += 'MANY'; | ||
@@ -239,4 +258,4 @@ } else { | ||
result += ' JOIN '; | ||
result += quoteSqlId(absoluteCdsName(elm.target._artifact.name.absolute)) + ' AS ' + quoteSqlId(elm.name.id) + ' ON ('; | ||
result += renderExpressionOrCondition(elm.onCond, env, true) + ')'; | ||
result += quoteSqlId(absoluteCdsName(elm.target)) + ' AS ' + quoteSqlId(elementName) + ' ON ('; | ||
result += renderExpr(elm.on, env) + ')'; | ||
} | ||
@@ -246,405 +265,214 @@ return result; | ||
// Render the 'technical configuration { ... }' section of an entity. | ||
// Render the 'technical configuration { ... }' section of an entity that comes as a suffix | ||
// to the CREATE TABLE statement (includes migration, unload prio, extended storage, | ||
// auto merge, partitioning, ...). | ||
// Return the resulting source string. | ||
function renderTechnicalConfiguration(tc, env) { | ||
let result = ''; | ||
if (!tc) { | ||
return result; | ||
} | ||
if (tc.migration) { | ||
result += '\n' + env.indent + 'MIGRATION ' + renderPathOrValue(tc.migration, env); | ||
} | ||
if (tc.extendedStorage) { | ||
result += '\n' + env.indent + 'USING EXTENDED STORAGE'; | ||
} | ||
if (tc.unloadPrio) { | ||
result += '\n' + env.indent + 'UNLOAD PRIORITY ' + renderPathOrValue(tc.unloadPrio, env); | ||
} | ||
if (tc.autoMerge) { | ||
result += '\n' + env.indent + (tc.autoMerge.val == false ? 'NO ' : '') + 'AUTO MERGE'; | ||
} | ||
if (tc.group) { | ||
result += '\n' + env.indent; | ||
if (tc.group.name) { | ||
result += 'GROUP NAME ' + quoteSqlId(tc.group.name.id); | ||
} | ||
if (tc.group.name && tc.group.type) { | ||
result += ' '; | ||
} | ||
if (tc.group.type) { | ||
result += 'GROUP TYPE ' + quoteSqlId(tc.group.type.id); | ||
} | ||
if ((tc.group.name || tc.group.type) && tc.group.subType) { | ||
result += ' '; | ||
} | ||
if (tc.group.subType) { | ||
result += 'GROUP SUBTYPE ' + quoteSqlId(tc.group.subType.id); | ||
} | ||
// FIXME: How to deal with non-HANA technical configurations? | ||
// This also affects renderIndexes | ||
tc = tc.hana; | ||
if (!tc) { | ||
throw new Error('Expecting a HANA technical configuration'); | ||
} | ||
if (tc.tableSuffix) { | ||
// Although we could just render the whole bandwurm as one stream of tokens, the | ||
// compactor has kindly stored each part (e.g. `migration enabled` `row store`, ...) | ||
// in its own `xpr` (for the benefit of the `toCdl` renderer, which needs semicolons | ||
// between parts). We use this here for putting each one one line) | ||
if (tc.partition) { | ||
result += '\n' + env.indent + 'PARTITION BY '; | ||
let i = 0; | ||
tc.partition.specs.forEach(p => { | ||
if (i > 0) { | ||
result += ', '; | ||
// The ignore array contains technical configurations that are illegal in HANA SQL | ||
let ignore = [ | ||
'PARTITION BY KEEPING EXISTING LAYOUT', | ||
'ROW STORE', | ||
'COLUMN STORE', | ||
'MIGRATION ENABLED', | ||
'MIGRATION DISABLED' | ||
]; | ||
for (let xpr of tc.tableSuffix) { | ||
let clause = renderExpr(xpr, env); | ||
if(!ignore.includes(clause.toUpperCase())) { | ||
result += '\n' + env.indent + clause; | ||
} | ||
result += renderPartition(p, env); | ||
i++; | ||
}); | ||
if (tc.partition.wpoac) { | ||
result += ' ' + 'WITH PARTITIONING ON ANY COLUMNS ' + renderPathOrValue(tc.partition.wpoac, env); | ||
} | ||
} | ||
return result; | ||
} | ||
// Render a partition spec of a technical configuration. | ||
function renderPartition(partition, env) { | ||
let result = renderPathOrValue(partition.scheme, env); | ||
if (partition.columns) { | ||
result += ' ('; | ||
let i = 0; | ||
partition.columns.filter(column => !column._ignore).forEach(column => { | ||
if (i > 0) { | ||
result += ', '; | ||
} | ||
if (column.unit) { | ||
result += renderPathOrValue(column.unit, env) + '('; | ||
} | ||
result += renderPathOrValue(column, env); | ||
if (column.unit) { | ||
result += ')'; | ||
} | ||
i++; | ||
}); | ||
result += ')'; | ||
// Render the array `indexes` from the technical configuration of an entity 'artifactName' | ||
function renderIndexesInto(indexes, artifactName, resultObj, env) { | ||
// Indices and full-text indices | ||
for (let idxName in indexes || {}) { | ||
let result = ''; | ||
if (indexes[idxName][0] instanceof Array) { | ||
// FIXME: Should we allow multiple indices with the same name at all? (last one wins) | ||
for (let index of indexes[idxName]) { | ||
result = renderExpr(insertTableName(index), env); | ||
} | ||
} | ||
if (partition.partitions) { | ||
result += ' PARTITIONS ' + renderPathOrValue(partition.partitions, env); | ||
else { | ||
result = renderExpr(insertTableName(indexes[idxName]), env); | ||
} | ||
if (partition.ranges) { | ||
result += ' ('; | ||
let oppStore = (partition.ranges[0].store == 'default' ? 'extended' : 'default'); | ||
let delimiter = false; | ||
partition.ranges.forEach((range, env) => { | ||
if (range.store != oppStore) { | ||
if (partition.withStorageSpec) { | ||
if (delimiter) { | ||
result += ') '; | ||
} | ||
result += 'USING ' + range.store.toUpperCase() + ' STORAGE ('; | ||
} | ||
delimiter = false; | ||
oppStore = range.store; | ||
} | ||
if (delimiter) { | ||
result += ', '; | ||
} | ||
result += renderPartitionRange(range, env); | ||
delimiter = true; | ||
}); | ||
if (partition.withStorageSpec) { | ||
result += ')'; | ||
} | ||
result += ')'; | ||
// FIXME: Full text index should already be different in compact CSN | ||
if (result.startsWith('FULLTEXT')) { | ||
resultObj.hdbfulltextindex[`${artifactName}.${idxName}`] = result; | ||
} | ||
return result; | ||
function renderPartitionRange(range, env) { | ||
let result = 'PARTITION '; | ||
if (range.others) { | ||
return result + renderPathOrValue(range.others, env); | ||
} | ||
if (!range.max) { | ||
result += 'VALUE = ' | ||
} | ||
result += renderPathOrValue(range.min, env); | ||
if (range.isCurrent) { | ||
result += ' IS CURRENT'; | ||
} | ||
if (range.max) { | ||
result += ' <= VALUES < ' + renderPathOrValue(range.max, env); | ||
} | ||
return result; | ||
else { | ||
resultObj.hdbindex[`${artifactName}.${idxName}`] = result; | ||
} | ||
} | ||
} | ||
// Render the indices belonging to 'artifactName' into the appropriate | ||
// dictionary of 'resultObj'. | ||
function renderIndexesInto(tc, artifactName, resultObj, env) { | ||
let result = ''; | ||
if (tc && tc.indexes) { | ||
for (let idxName in tc.indexes) { | ||
let idx = tc.indexes[idxName]; | ||
if (Array.isArray(idx)) { | ||
idx.forEach(i => renderIndexInto(i, artifactName, resultObj, env)); | ||
} | ||
else { | ||
renderIndexInto(idx, artifactName, resultObj, env); | ||
} | ||
// Insert 'artifactName' (quoted according to naming style) into the index | ||
// definition 'index' in two places: | ||
// CDS: unique index "foo" on (x, y) | ||
// becomes | ||
// SQL: unique index "<artifact>.foo" on "<artifact>"(x, y) | ||
// CDS does not need this because the index lives inside the artifact, but SQL does. | ||
function insertTableName(index) { | ||
let i = index.indexOf('index'); | ||
let j = index.indexOf('('); | ||
if (i > index.length - 2 || !index[i + 1].ref || j < i || j > index.length - 2) { | ||
throw new Error(`Unexpected form of index: "${index}"`); | ||
} | ||
} | ||
return result; | ||
function renderIndexInto(idx, artifactName, resultObj, env) { | ||
let result = ''; | ||
if (idx.kind === 'index') { | ||
if (idx.unique) { | ||
result += 'UNIQUE '; | ||
} | ||
result += 'INDEX ' + quoteSqlId(idx.name.id) + ' ON (' + renderArray(idx.columns) + ')'; | ||
if (idx.sort) { | ||
result += ' ' + renderPathOrValue(idx.sort, env); | ||
} | ||
resultObj.hdbindex[`${artifactName}.${idx.name.id}`] = result; | ||
let indexName = `${absoluteCdsName(artifactName)}.${index[i + 1].ref}`; | ||
if (options.toSql.names == 'plain') { | ||
indexName = indexName.replace(/(\.|::)/g, '_'); | ||
} | ||
else if (idx.kind === 'fulltextindex') { | ||
result += 'FULLTEXT INDEX ' + quoteSqlId(idx.name.id) + ' ON (' + renderArray(idx.columns) + ')'; | ||
if (idx.language) { | ||
if (idx.language.column) { | ||
result += ' ' + 'LANGUAGE COLUMN ' + renderPathOrValue(idx.language.column, env); | ||
} | ||
if (idx.language.detection) { | ||
result += ' ' + 'LANGUAGE DETECTION (' + renderArray(idx.language.detection) + ')' | ||
} | ||
} | ||
if (idx.mimeTypeColumn) { | ||
result += ' ' + 'MIME TYPE COLUMN ' + renderPathOrValue(idx.mimeTypeColumn, env); | ||
} | ||
if (idx.fuzzySearchIndex) { | ||
result += ' ' + 'FUZZY SEARCH INDEX ' + renderPathOrValue(idx.fuzzySearchIndex, env); | ||
} | ||
if (idx.phraseIndexRatio) { | ||
result += ' ' + 'PHRASE INDEX RATIO ' + renderPathOrValue(idx.phraseIndexRatio, env); | ||
} | ||
if (idx.configuration) { | ||
result += ' ' + 'CONFIGURATION ' + renderPathOrValue(idx.configuration, env); | ||
} | ||
if (idx.textAnalysis) { | ||
result += ' ' + 'TEXT ANALYSIS ' + renderPathOrValue(idx.textAnalysis, env); | ||
} | ||
if (idx.searchOnly) { | ||
result += ' ' + 'SEARCH ONLY ' + renderPathOrValue(idx.searchOnly, env); | ||
} | ||
if (idx.fastPreprocess) { | ||
result += ' ' + 'FAST PREPROCESS ' + renderPathOrValue(idx.fastPreprocess, env); | ||
} | ||
if (idx.mimeType) { | ||
result += ' ' + 'MIME TYPE ' + renderPathOrValue(idx.mimeType, env); | ||
} | ||
if (idx.tokenSeparators) { | ||
result += ' ' + 'TOKEN SEPARATORS ' + renderPathOrValue(idx.tokenSeparators, env); | ||
} | ||
if (idx.textMining) { | ||
if (idx.textMining.state) { | ||
result += ' ' + 'TEXT MINING ' + renderPathOrValue(idx.textMining.state, env); | ||
} | ||
if (idx.textMining.config) { | ||
result += ' ' + 'TEXT MINING CONFIGURATION ' + renderPathOrValue(idx.textMining.config, env); | ||
} | ||
if (idx.textMining.overlay) { | ||
result += ' ' + 'TEXT MINING CONFIGURATION OVERLAY ' + renderPathOrValue(idx.textMining.overlay, env); | ||
} | ||
} | ||
if (idx.changeTracking) { | ||
let ct = idx.changeTracking; | ||
result += ' ' + renderPathOrValue(ct.mode); | ||
if (ct.asyncSpec) { | ||
let asp = ct.asyncSpec; | ||
result += ' FLUSH '; | ||
if (asp.queue) { | ||
result += renderPathOrValue(asp.queue, env) + ' '; | ||
} | ||
if (asp.minutes) { | ||
result += 'EVERY ' + renderPathOrValue(asp.minutes, env) + ' MINUTES'; | ||
if (asp.documents) { | ||
result += ' OR '; | ||
} | ||
} | ||
if (asp.documents) { | ||
result += 'AFTER ' + renderPathOrValue(asp.documents, env) + ' DOCUMENTS'; | ||
} | ||
} | ||
} | ||
resultObj.hdbfulltextindex[`${artifactName}.${idx.name.id}`] = result; | ||
} | ||
return; | ||
function renderArray(arr) { | ||
let r = '', i = 0; | ||
arr.filter(v => !v._ignore).forEach(v => { | ||
if (i > 0) { | ||
r += ', '; | ||
} | ||
r += renderPathOrValue(v, env); | ||
if (v.sort) { | ||
r += ' ' + renderPathOrValue(v.sort, env); | ||
} | ||
i++; | ||
}); | ||
return r; | ||
} | ||
let result = index.slice(0, i + 1); // CREATE UNIQUE INDEX | ||
result.push({ ref: [indexName] }); // "<artifact>.foo" | ||
result.push(...index.slice(i + 2, j)); // ON | ||
result.push({ ref: [absoluteCdsName(artifactName)] }); // <artifact> | ||
result.push(...index.slice(j)); // (x, y) | ||
return result; | ||
} | ||
} | ||
// Render a projection entity. Return the resulting source string. | ||
// NOTE: Not actually used for now, because forHana is always applied first, converting projections to views. | ||
function renderProjection(art, env) { | ||
let childEnv = increaseIndent(env); | ||
let result = 'VIEW ' + quoteSqlId(absoluteCdsName(art.name.absolute)); | ||
result += renderParameterDefinitions(art.params); | ||
result += ' AS SELECT\n'; | ||
result += Object.keys(art.elements).map(name => renderViewOrProjectionElement(art.elements[name], childEnv)) | ||
.filter(s => s != '') | ||
.join(',\n') + '\n'; | ||
result += env.indent + 'FROM ' + renderSourcePathWithAlias(art.source, art); | ||
return result; | ||
} | ||
// Render the source of a query, which may either be a path with an alias or a join operation, | ||
// as seen from artifact 'art'. | ||
// Render the source of a query, which may be a path reference, possibly with an alias, | ||
// or a subselect, or a join operation. Use 'artifactName' only for error output. | ||
// FIXME: Misleading name, should be something like 'renderQueryFrom'. All the query | ||
// parts should probably also be rearranged. | ||
// Returns the source as a string. | ||
function renderViewSource(source, art, env) { | ||
if (source instanceof Array) { | ||
// Join operations in parentheses | ||
if (source.length != 1) { | ||
throw new Error('Expecting only one join operation: ' + source); | ||
function renderViewSource(artifactName, source, env) { | ||
// Sub-SELECT | ||
if (source.SELECT || source.SET) { | ||
let result = `(${renderQuery(artifactName, source, increaseIndent(env))})`; | ||
if (source.as) { | ||
result += ` AS ${quoteSqlId(source.as)}`; | ||
} | ||
return `${renderViewSource(source[0], art, env)}`; | ||
} else if (source.op && source.op.val == 'join') { | ||
return result; | ||
} | ||
// JOIN | ||
else if (source.join) { | ||
// One join operation, possibly with ON-condition | ||
let result = `${renderViewSource(source.args[0], art, env)} ${renderJoinOp(source.join).toUpperCase()} ${renderViewSource(source.args[1], art, env)}`; | ||
// FIXME: Clarify if join operators can be n-ary (assuming binary here) | ||
let result = `(${renderViewSource(artifactName, source.args[0], env)} ${source.join.toUpperCase()} JOIN ${renderViewSource(artifactName, source.args[1], env)}`; | ||
if (source.on) { | ||
result += ` ON ${renderExpressionOrCondition(source.on)}`; | ||
result += ` ON ${renderExpr(source.on, env)}`; | ||
} | ||
// Always put parentheses around joins for readability (other than toCdl) | ||
return `(${result})`; | ||
} else if (source.op && source.op.val == 'query') { | ||
// sub-select | ||
let result = `(${renderQuery(source, art, false, increaseIndent(env))})`; | ||
if (source.name && !source.name.calculated) { | ||
// Source had an alias - render it | ||
result += ' AS ' + quoteSqlId(source.name.id); | ||
} | ||
result += `)`; | ||
return result; | ||
} else { | ||
// Ordinary path, possibly with an alias | ||
return renderSourcePathWithAlias(source, art); | ||
} | ||
// Ordinary path, possibly with an alias | ||
else { | ||
// Sanity check | ||
if (!source.ref) { | ||
throw new Error(`Expecting ref in ${JSON.stringify(source)}`); | ||
} | ||
return renderAbsolutePathWithAlias(artifactName, source, env); | ||
} | ||
} | ||
// Render the source path of a projection or query, possibly with an alias, as seen from artifact 'art'. | ||
// Expects an object 'source' that has a 'path' and (in case of an alias) a 'name'. | ||
// Render a path that starts with an absolute name (as used for the source of a query), | ||
// possibly with an alias, with plain or quoted names, depending on options. Expects an object 'path' that has a | ||
// 'ref' and (in case of an alias) an 'as'. If necessary, an artificial alias | ||
// is created to the original implicit name. Use 'artifactName' only for error output. | ||
// Returns the name and alias as a string. | ||
function renderSourcePathWithAlias(source, art) { | ||
// Sanity checks | ||
if (!source.path || source.path.length < 1) { | ||
throw new Error('Expecting path in source of ' + art.name.absolute + ': ' + JSON.stringify(source, null, 2)); | ||
function renderAbsolutePathWithAlias(artifactName, path, env) { | ||
// This actually can't happen anymore because assoc2joins should have taken care of it | ||
if (path.ref[0].where) { | ||
throw new Error(`"${artifactName}": Filters in FROM are not supported for conversion to SQL`); | ||
} | ||
if (!source.path[0]._artifact) { | ||
throw new Error('Expecting first path step in source of ' + art.name.absolute + ' to be resolved: ' + JSON.stringify(source, null, 2)); | ||
} | ||
// FIXME: This is all very ugly (will likely improve once we consume new-style CSN here) | ||
// Determine the index of the first entity on the path (skipping over contexts etc) - after that we may have assoc elements | ||
let firstEntityIdx = 0; | ||
for (let i = 0; i < source.path.length; i++) { | ||
if (source.path[i]._artifact && source.path[i]._artifact.kind != 'element') { | ||
// Still within the entity name | ||
firstEntityIdx = i; | ||
} | ||
// SQL needs a ':' after path.ref[0] to separate associations | ||
let result = renderAbsolutePath(path, ':', env); | ||
// Take care of aliases | ||
let implicitAlias = getLastPartOfRef(path.ref); | ||
if (path.as) { | ||
// Source had an alias - render it | ||
result += ' AS ' + quoteSqlId(path.as); | ||
} | ||
if (source.path[firstEntityIdx].where) { | ||
signal(error`"${art.name.absolute}": Filters in FROM are not supported for conversion to SQL`, source.location); | ||
return ''; | ||
else if (getLastPartOf(result) != quoteSqlId(implicitAlias)) { | ||
// Render an artificial alias if the result would produce a different one | ||
result += ' AS ' + quoteSqlId(implicitAlias); | ||
} | ||
let firstEntity = source.path[firstEntityIdx]._artifact; | ||
return result; | ||
} | ||
// Start with the absolute name of the first path step | ||
let result = quoteSqlId(absoluteCdsName(firstEntity.name.absolute)); | ||
// Even the first step might have parameters | ||
if (source.path[firstEntityIdx].namedArgs) { | ||
result += renderNamedArgs(source.path[firstEntityIdx].namedArgs); | ||
// Render a path that starts with an absolute name (as used e.g. for the source of a query), | ||
// with plain or quoted names, depending on options. Expects an object 'path' that has a 'ref'. | ||
// Uses <seperator> (typically ':': or '.') to separate the first artifact name from any | ||
// subsequent associations. | ||
// Returns the name as a string. | ||
function renderAbsolutePath(path, sep, env) { | ||
// Sanity checks | ||
if (!path.ref) { | ||
throw new Error('Expecting ref in path: ' + JSON.stringify(path)); | ||
} | ||
// Add any paths that may follow after that (separating the artifact name from the rest by ':' !) | ||
for (let i = firstEntityIdx + 1; i < source.path.length; i++) { | ||
// Sanity check | ||
if (source.path[i]._artifact && source.path[i]._artifact.kind != 'element') { | ||
throw new Error('Expecting an element here: ' + JSON.stringify(source.path[i], null, 2)); | ||
} | ||
// Path continues with elements - complain if it has a filter | ||
if (source.path[i].where) { | ||
signal(error`"${art.name.absolute}": Filters in FROM are not supported for conversion to SQL`, source.location); | ||
return ''; | ||
} | ||
if (i > firstEntityIdx + 1) { | ||
// We are already in the elements part of a path - append '.' and quoted id | ||
result += '.' + quoteSqlId(source.path[i].id); | ||
// Append view params if any | ||
if (source.path[i].namedArgs) { | ||
result += renderNamedArgs(source.path[i].namedArgs); | ||
} | ||
} else { | ||
// Sanity check | ||
if (i < 1) { | ||
throw new Error('Cannot be the first path step: ' + JSON.stringify(source.path[i], null, 2)); | ||
} | ||
// We have just left the artifact and entered elements - append ':' and the quoted id of the current path step | ||
result += ':' + quoteSqlId(source.path[i].id); | ||
if (source.path[i].namedArgs) { | ||
result += renderNamedArgs(source.path[i].namedArgs); | ||
} | ||
} | ||
// Determine the absolute name of the first artifact on the path (before any associations or element traversals) | ||
let firstArtifactName = path.ref[0].id || path.ref[0]; | ||
let firstArtifact = csn.definitions[firstArtifactName]; | ||
if (!firstArtifact) { | ||
throw new Error('Expecting first path step in path to be resolvable: ' + JSON.stringify(path)); | ||
} | ||
// Sanity check: must have a name | ||
if (!source.name) { | ||
throw new Error('Expecting source to have a name: ' + JSON.stringify(source)); | ||
let result = quoteSqlId(absoluteCdsName(firstArtifactName)); | ||
// Even the first step might have parameters and/or a filter | ||
if (path.ref[0].args) { | ||
result += `(${renderArgs(path.ref[0].args, '=>', env)})`; | ||
} | ||
// Render an alias unless it is redundant | ||
if (quoteSqlId(source.name.id) != result) { | ||
result += ' AS ' + quoteSqlId(source.name.id); | ||
if (path.ref[0].where) { | ||
result += `[${path.ref[0].cardinality ? (path.ref[0].cardinality.max + ': ') : ''}${renderExpr(path.ref[0].where, env)}]`; | ||
} | ||
// Add any path steps (possibly with parameters and filters) that may follow after that | ||
if (path.ref.length > 1) { | ||
result += `${sep}${renderExpr({ref: path.ref.slice(1)}, env)}`; | ||
} | ||
return result; | ||
} | ||
function renderNamedArgs(args) { | ||
let result = '('; | ||
let i = 0; | ||
for (let argName in args) { | ||
let arg = args[argName]; | ||
if (i > 0) { | ||
result += ', '; | ||
} | ||
result += quoteSqlId(arg.name.id) + ' => ' + renderPathOrValue(arg, { indent: '' }); | ||
i++; | ||
// Render function arguments or view parameters (positional if array, named if object/dict), | ||
// using 'sep' as separator for positional parameters | ||
function renderArgs(args, sep, env) { | ||
// Positional arguments | ||
if (args instanceof Array) { | ||
return args.map(arg => renderExpr(arg, env)).join(', '); | ||
} | ||
return result + ')'; | ||
// Named arguments (object/dict) | ||
else if (typeof args == 'object') { | ||
return Object.keys(args).map(key => `${quoteSqlId(key)} ${sep} ${renderExpr(args[key], env)}`).join(', '); | ||
} | ||
else { | ||
throw new Error('Unknown args: ' + JSON.stringify(args)); | ||
} | ||
} | ||
// Render a single view or projection element 'elm', as it occurs in a select list or projection list, | ||
// possibly with annotations. Return the resulting source string (no trailing LF). | ||
function renderViewOrProjectionElement(elm, env) { | ||
// Ignore if forHana says so, or if it is an association | ||
if (elm._ignore) { | ||
// Render a single view column 'col', as it occurs in a select list or projection list. | ||
// Return the resulting source string (one line per column item, no CR). | ||
function renderViewColumn(col, env) { | ||
// Ignore if forHana says so | ||
// FIXME: Have we already filtered out associations here? | ||
if (col._ignore) { | ||
return ''; | ||
} | ||
if (isAssociation((elm._finalType || elm).type)) { | ||
return ''; | ||
// FIXME: We may want to wrap a cast around 'col' if it has an explicit type? | ||
let result = env.indent + renderExpr(col, env, true); | ||
// Explicit or implicit alias? | ||
if (col.as) { | ||
result += ' AS ' + quoteSqlId(col.as); | ||
} | ||
let elementValue = renderExpressionOrCondition(elm.value, env, true); | ||
if (elm._typeIsExplicit) { | ||
// FIXME: We may want to wrap a cast around 'elementValue' in this case? | ||
} | ||
let result = env.indent + elementValue; | ||
// Render an alias unless it is redundant | ||
let path = elm.value.path; | ||
if (!path || elm.name.id != path[path.length-1].id) { | ||
result += ' AS ' + quoteSqlId(elm.name.id); | ||
} | ||
return result; | ||
@@ -654,9 +482,9 @@ } | ||
// Render a view | ||
function renderView(art, env) { | ||
let result = 'VIEW ' + quoteSqlId(absoluteCdsName(art.name.absolute)); | ||
result += renderParameterDefinitions(art.params); | ||
result += ' AS ' + renderQuery(art.query, art, true, env); | ||
function renderView(artifactName, art, env) { | ||
let result = 'VIEW ' + quoteSqlId(absoluteCdsName(artifactName)); | ||
result += renderParameterDefinitions(artifactName, art.params); | ||
result += ' AS ' + renderQuery(artifactName, art.query, env); | ||
let childEnv = increaseIndent(env); | ||
let associations = Object.keys(art.elements).filter(name => isAssociation(art.elements[name].type)) | ||
.map(name => renderAssociationElement(art.elements[name], childEnv)) | ||
let associations = Object.keys(art.elements).filter(name => !!art.elements[name].target) | ||
.map(name => renderAssociationElement(name, art.elements[name], childEnv)) | ||
.filter(s => s != '') | ||
@@ -671,54 +499,88 @@ .join(',\n'); | ||
function renderParameterDefinitions(params) { | ||
let paramDefs = Object.keys(params || []).map(name => | ||
{ return 'IN ' + quoteSqlId(name) + ' ' + renderTypeReference(params[name])}).join(', '); | ||
return (paramDefs == '') ? paramDefs : '(' + paramDefs + ')'; | ||
// Render the parameter definition of a view if any. Return the parameters in parentheses, or an empty string | ||
function renderParameterDefinitions(artifactName, params) { | ||
let result = Object.keys(params || {}).map(name => 'IN ' + quoteSqlId(name) + ' ' + renderTypeReference(artifactName, name, params[name])) | ||
.join(', '); | ||
if (result != '') { | ||
result = '(' + result + ')'; | ||
} | ||
return result; | ||
} | ||
// Render a query 'query', i.e. a select statement with where-condition etc, possibly as part of artifact 'art'. | ||
// If 'isLeadingQuery' is true, mixins of 'art' are also rendered into the query. | ||
// FIXME: No support for MIXINs yet. | ||
// FIXME: currently only selection from multiple sources is supported, no JOIN yet, no UNION yet | ||
function renderQuery(query, art, isLeadingQuery, env) { | ||
// Render a query 'query', i.e. a select statement with where-condition etc. Use 'artifactName' only for error messages. | ||
function renderQuery(artifactName, query, env) { | ||
let result = ''; | ||
if(Array.isArray(query)) { | ||
result = `(${renderQuery(query[0], art, isLeadingQuery, env)})`; | ||
} else if (query.op && query.op.val == 'query') { | ||
let childEnv = increaseIndent(env); | ||
result += 'SELECT' + ((query.quantifier && query.quantifier.val) ? ` ${ query.quantifier.val.toUpperCase() }\n` : '\n'); | ||
result += Object.keys(query.elements).map(name => renderViewOrProjectionElement(query.elements[name], childEnv)) | ||
.filter(s => s != '') | ||
.join(',\n') + '\n'; | ||
result += `${env.indent}FROM ${query.from.map(source => renderViewSource(source, art, env)).join(', ')}`; | ||
} else if (query.op && query.op.val == 'subquery') { | ||
// Magic special case: Subquery in parentheses can have ORDER BY, LIMIT and OFFSET outside the parentheses. | ||
result += `(${renderQuery(query.args[0][0], art, isLeadingQuery, env)})`; | ||
} else if (query.op && ['union', 'unionAll', 'intersect', 'except'].includes(query.op.val)) { | ||
// Ordinary query operators (first may be leading query) | ||
result += `(${renderQuery(query.args[0], art, isLeadingQuery, env)}` | ||
result += `\n${env.indent}${query.op.val.replace('All', ' all').toUpperCase()} ${renderQuery(query.args[1], art, false, env)})`; | ||
} else { | ||
throw new Error('Unexpected query operation ' + query.op.val); | ||
// Set operator, like UNION, INTERSECT, ... | ||
if (query.SET) { | ||
result += query.SET.args | ||
.map(arg => { | ||
// Wrap each query in the SET in parentheses that | ||
// - is a SET itself (to preserve precedence between the different SET operations), | ||
// - has an ORDER BY/LIMIT (because UNION etc. can't stand directly behind an ORDER BY) | ||
let queryString = renderQuery(artifactName, arg, env); | ||
return (arg.SET || arg.SELECT && (arg.SELECT.orderBy || arg.SELECT.limit)) ? `(${queryString})` : queryString; | ||
}) | ||
.join(`\n${env.indent}${query.SET.op && query.SET.op.toUpperCase()}${query.SET.all ? ' ALL ' : ' '}`); | ||
// Set operation may also have an ORDER BY and LIMIT/OFFSET (in contrast to the ones belonging to | ||
// each SELECT) | ||
// If the whole SET has an ORDER BY/LIMIT, wrap the part before that in parentheses | ||
// (otherwise some SQL implementations (e.g. sqlite) would interpret the ORDER BY/LIMIT as belonging | ||
// to the last SET argument, not to the whole SET) | ||
if (query.SET.orderBy || query.SET.limit) { | ||
result = `(${result})`; | ||
if (query.SET.orderBy) { | ||
result += `\n${env.indent}ORDER BY ${query.SET.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`; | ||
} | ||
if (query.SET.limit) { | ||
result += `\n${env.indent}${renderLimit(query.SET.limit, env)}`; | ||
} | ||
} | ||
return result; | ||
} | ||
if (query.where) { | ||
result += `\n${env.indent}WHERE ${renderExpressionOrCondition(query.where, env)}`; | ||
// Otherwise must have a SELECT | ||
else if (!query.SELECT) { | ||
throw new Error('Unexpected query operation ' + JSON.stringify(query)); | ||
} | ||
if (query.groupBy) { | ||
result += `\n${env.indent}GROUP BY ${query.groupBy.map(exprOrCond => renderExpressionOrCondition(exprOrCond, env)).join(', ')}`; | ||
let select = query.SELECT; | ||
let childEnv = increaseIndent(env); | ||
result += 'SELECT' + (select.distinct ? ' DISTINCT' : ''); | ||
// FIXME: We probably also need to consider `excluding` here ? | ||
result += '\n' + | ||
(select.columns||['*']).filter(col => !(select.mixin || {})[firstPathStepId(col.ref)]) // No mixin columns | ||
.map(col => renderViewColumn(col, childEnv)) | ||
.filter(s => s != '') | ||
.join(',\n') + '\n'; | ||
result += `${env.indent}FROM ${renderViewSource(artifactName, select.from, env)}`; | ||
if (select.where) { | ||
result += `\n${env.indent}WHERE ${renderExpr(select.where, env)}`; | ||
} | ||
if (query.having) { | ||
result += `\n${env.indent}HAVING ${renderExpressionOrCondition(query.having, env)}`; | ||
if (select.groupBy) { | ||
result += `\n${env.indent}GROUP BY ${select.groupBy.map(expr => renderExpr(expr, env)).join(', ')}`; | ||
} | ||
// Need extra parentheses if ORDER BY or LIMIT is involved, because they have strange precedence in relation to UNION, INTERSECT, ... | ||
if (query.orderBy || query.limit) { | ||
result = `(${result})`; | ||
if (select.having) { | ||
result += `\n${env.indent}HAVING ${renderExpr(select.having, env)}`; | ||
} | ||
if (query.orderBy) { | ||
result += `\n${env.indent}ORDER BY ${query.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`; | ||
if (select.orderBy) { | ||
result += `\n${env.indent}ORDER BY ${select.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`; | ||
} | ||
if (query.limit) { | ||
result += `\n${env.indent}LIMIT ${renderPathOrValue(query.limit, env)}`; | ||
if (select.limit) { | ||
result += `\n${env.indent}${renderLimit(select.limit, env)}`; | ||
} | ||
if (query.offset) { | ||
result += `\n${env.indent}OFFSET ${renderPathOrValue(query.offset, env)}`; | ||
return result; | ||
} | ||
// Returns the id of the first path step in 'ref' if any, otherwise undefined | ||
function firstPathStepId(ref) { | ||
return ref && ref[0] && (ref[0].id || ref[0]); | ||
} | ||
// Render a query's LIMIT clause, which may have also have OFFSET. | ||
function renderLimit(limit, env) { | ||
let result = ''; | ||
if (limit.rows !== undefined) { | ||
result += `LIMIT ${renderExpr(limit.rows, env)}`; | ||
} | ||
if (limit.offset !== undefined) { | ||
result += `${result != '' ? '\n' + env.indent : ''}OFFSET ${renderExpr(limit.offset, env)}`; | ||
} | ||
return result; | ||
@@ -730,8 +592,8 @@ } | ||
function renderOrderByEntry(entry, env) { | ||
let result = renderExpressionOrCondition(entry.value, env); | ||
let result = renderExpr(entry, env); | ||
if (entry.sort) { | ||
result += ` ${entry.sort.val.toUpperCase()}`; | ||
result += ` ${entry.sort.toUpperCase()}`; | ||
} | ||
if (entry.nulls) { | ||
result += ` NULLS ${entry.nulls.val.toUpperCase()}`; | ||
result += ` NULLS ${entry.nulls.toUpperCase()}`; | ||
} | ||
@@ -743,3 +605,3 @@ return result; | ||
// Return the resulting source string. | ||
function renderType(art, env) { | ||
function renderType(artifactName, art, env) { | ||
// Only HANA table types are SQL-relevant | ||
@@ -749,13 +611,18 @@ if (!art.dbType) { | ||
} | ||
let result ='TYPE ' + quoteSqlId(absoluteCdsName(art.name.absolute)) + ' AS TABLE (\n'; | ||
// In Sqlite dialect do not generate table type and throw an info | ||
if (options.toSql.dialect === 'sqlite') { | ||
signal(info`"${artifactName}": HANA table types are not supported in SQLite`, art.location); | ||
return ''; | ||
} | ||
let result = 'TYPE ' + quoteSqlId(absoluteCdsName(artifactName)) + ' AS TABLE (\n'; | ||
let childEnv = increaseIndent(env); | ||
if (art._finalType.elements) { | ||
// Structured type or annotation with anonymous struct type | ||
result += Object.keys(art._finalType.elements).map(name => renderElement(art._finalType.elements[name], childEnv)) | ||
.filter(s => s != '') | ||
.join(',\n') + '\n'; | ||
if (art.elements) { | ||
// Structured type | ||
result += Object.keys(art.elements).map(name => renderElement(artifactName, name, art.elements[name], null, childEnv)) | ||
.filter(s => s != '') | ||
.join(',\n') + '\n'; | ||
result += env.indent + ')'; | ||
} else { | ||
// Non-structured HANA table type | ||
signal(error`"${art.name.absolute}": HANA table types must have structured types for conversion to SQL`, art.location); | ||
signal(error`"${artifactName}": HANA table types must have structured types for conversion to SQL`, art.location); | ||
return ''; | ||
@@ -766,9 +633,9 @@ } | ||
// Render a reference to the final type used by 'elm' (named or inline) | ||
function renderTypeReference(elm) { | ||
// Render a reference to the type used by 'elm' (with name 'elementName' in 'artifactName', both used only for error messages). | ||
function renderTypeReference(artifactName, elementName, elm) { | ||
let result = ''; | ||
// Array type: Not supported with SQL | ||
if (elm._finalType.items) { | ||
signal(error`"${elm._main.name.absolute}.${elm.name.element}": Array types are not supported for conversion to SQL`, elm.location); | ||
if (elm.items) { | ||
signal(error`"${artifactName}.${elementName}": Array types are not supported for conversion to SQL`, elm.location); | ||
return result; | ||
@@ -778,7 +645,7 @@ } | ||
// Anonymous structured type: Not supported with SQL (but shouldn't happen anyway after forHana flattened them) | ||
if (!elm._finalType.type) { | ||
if (!elm._finalType.elements) { | ||
throw new Error('Missing type of: ' + elm.name.id); | ||
if (!elm.type) { | ||
if (!elm.elements) { | ||
throw new Error('Missing type of: ' + elementName); | ||
} | ||
signal(error`"${elm._main.name.absolute}.${elm.name.element}": Anonymous structured types are not supported for conversion to SQL`, elm.location); | ||
signal(error`"${artifactName}.${elementName}": Anonymous structured types are not supported for conversion to SQL`, elm.location); | ||
return result; | ||
@@ -788,5 +655,5 @@ } | ||
// Association type | ||
if (elm._finalType.target) { | ||
if (elm.target) { | ||
// We can't do associations yet | ||
signal(error`"${elm._main.name.absolute}.${elm.name.element}": Association and composition types are not yet supported for conversion to SQL`, elm.location); | ||
signal(error`"${artifactName}.${elementName}": Association and composition types are not yet supported for conversion to SQL`, elm.location); | ||
return result; | ||
@@ -796,13 +663,9 @@ } | ||
// If we get here, it must be a primitive (i.e. builtin) type | ||
if (elm._finalType.type._artifact.builtin) { | ||
if (isBuiltinType(elm.type)) { | ||
// cds.Integer => render as INTEGER (no quotes) | ||
result += renderBuiltinType(elm._finalType.type._artifact); | ||
result += renderBuiltinType(elm.type); | ||
} else { | ||
throw new Error('Unexpected non-primitive type of: ' + elm._main.name.absolute + '.' + elm.name.element); | ||
throw new Error('Unexpected non-primitive type of: ' + artifactName + '.' + elementName); | ||
} | ||
result += renderTypeParameters(elm._finalType); | ||
// FIXME: Quickhack: Apparently we sometimes omit the default length for strings | ||
if (result == 'NVARCHAR') { | ||
result += '(5000)'; | ||
} | ||
result += renderTypeParameters(elm); | ||
return result; | ||
@@ -812,3 +675,3 @@ } | ||
// Render the name of a builtin CDS type | ||
function renderBuiltinType(type) { | ||
function renderBuiltinType(typeName) { | ||
const cdsToSql = { | ||
@@ -851,5 +714,5 @@ // CDS builtin types | ||
let result = cdsToSql[type.name.absolute]; | ||
let result = cdsToSql[typeName]; | ||
if (!result) { | ||
throw Error('Unknown primitive type: ' + JSON.stringify(type)); | ||
throw Error('Unknown primitive type: ' + typeName); | ||
} | ||
@@ -859,9 +722,17 @@ return result; | ||
// Return true if 'typeName' is the name of a builtin type | ||
function isBuiltinType(typeName) { | ||
// FIXME: Rather than checking a list of builtin types, we just rely on the fact | ||
// that in a valid model, the only types that do not occur in 'definitions' are | ||
// the builtin ones. | ||
return !csn.definitions[typeName]; | ||
} | ||
// Render the nullability of an element or parameter (can be unset, true, or false) | ||
function renderNullability(obj /* , env */) { | ||
if (!obj.notNull) { | ||
if (obj.notNull === undefined) { | ||
// Attribute not set at all | ||
return ''; | ||
} | ||
return obj.notNull.val ? ' NOT NULL' : ' NULL'; | ||
return obj.notNull ? ' NOT NULL' : ' NULL'; | ||
} | ||
@@ -874,14 +745,15 @@ | ||
// Length, precision and scale (even if incomplete) | ||
if (elm.length) { | ||
params.push(elm.length.val); | ||
if (elm.length !== undefined) { | ||
params.push(elm.length); | ||
} | ||
if (elm.precision) { | ||
params.push(elm.precision.val); | ||
if (elm.precision !== undefined) { | ||
params.push(elm.precision); | ||
} | ||
if (elm.scale) { | ||
params.push(elm.scale.val); | ||
if (elm.scale !== undefined) { | ||
params.push(elm.scale); | ||
} | ||
// Additional type parameters | ||
// FIXME: Not yet clear how that looks in new CSN | ||
for (let arg of elm.typeArguments || []) { | ||
params.push(arg.val); | ||
params.push(arg); | ||
} | ||
@@ -891,97 +763,183 @@ return params.length == 0 ? '' : '(' + params.join(', ') + ')'; | ||
// Render a single value (i.e. something that has 'path' or 'literal' and 'val') | ||
// FIXME: Reuse this together with `toCdl`. | ||
// Render an expression (including paths and values) or condition 'x'. | ||
// (no trailing LF, don't indent if inline) | ||
function renderPathOrValue(v, env, inline=true) { | ||
let result = inline ? '' : env.indent; | ||
if (v.path) { | ||
// E.i | ||
return result + renderPath(v.path, env, v); | ||
} else if (v.literal == 'string') { | ||
// 'foo', with proper escaping | ||
return result + "'" + v.val.replace(/'/g, "''") + "'"; | ||
} else if (v.literal == 'enum') { | ||
// #foo | ||
// FIXME: We can't do enums yet because they are not resolved (and we don't bother finding their value by hand) | ||
signal(error`Enum values are not yet supported for conversion to SQL`, v.location); | ||
return ''; | ||
} else if (v.literal == 'hex') { | ||
// x'f000' | ||
return result + "x'" + v.val + "'"; | ||
} else if (v.literal == 'date' || v.literal == 'time' || v.literal == 'timestamp') { | ||
// date'2017-11-02' | ||
// date('2017-11-02') if sqlite | ||
return result + v.literal + `${options.toSql.dialect === 'sqlite' ? | ||
"('" : "'"}` + v.val + `${options.toSql.dialect === 'sqlite' ? "')" : "'"}`; | ||
} else if (v.literal == 'struct') { | ||
// { foo: 1 } | ||
// We can't do structs yet | ||
// FIXME: Can that happen at all outside of annotations? | ||
signal(error`: Struct values are not supported for conversion to SQL`, v.location); | ||
return ''; | ||
} else if (v.literal == 'array') { | ||
// [ 'foo', 'bar' ] | ||
// We can't do arrays yet | ||
// FIXME: Can that happen at all outside of annotations? | ||
signal(error`: Array values are not supported for conversion to SQL`, v.location); | ||
return ''; | ||
} else { | ||
// 17.42, null, true | ||
return result + String(v.val).toUpperCase(); | ||
function renderExpr(x, env, inline=true) { | ||
// Compound expression | ||
if (x instanceof Array) { | ||
// Simply concatenate array parts with spaces (with a tiny bit of beautification) | ||
// FIXME: Take this for `toCdl`, too | ||
let tokens = x.map(item => renderExpr(item, env, inline)); | ||
let result = ''; | ||
for (let i = 0; i < tokens.length; i++) { | ||
result += tokens[i]; | ||
// No space after last token, after opening parentheses, before closing parentheses, before comma | ||
if (i != tokens.length - 1 && tokens[i] != '(' && ![')', ','].includes(tokens[i + 1])) { | ||
result += ' '; | ||
} | ||
} | ||
return result; | ||
// return x.map(item => renderExpr(item, env, inline)).join(' '); | ||
} | ||
} | ||
// Render a path or query path (provided as an array of path steps, possibly with filters) | ||
function renderPath(path, env) { | ||
// Magic special case: SQL functions that have no parentheses (CURRENT_*) are not recognized as | ||
// function expressions by the parser - instead they appear here as paths of length 1 with a | ||
// 'builtin' artifact. | ||
const magicForHana = { | ||
'$now': 'CURRENT_TIMESTAMP', | ||
'$user.id': "SESSION_CONTEXT('XS_APPLICATIONUSER')", | ||
'$user.locale': "SESSION_CONTEXT('LOCALE')", | ||
} | ||
let ref = path.length && path[ path.length-1 ]._artifact; | ||
if (ref) { | ||
if(ref.kind == 'builtin') { | ||
if (options.forHana) { | ||
// HANA-specific translation of '$now' and '$user' | ||
// FIXME: This should rather happen in forHana, but it is non-trivial to catch all the different | ||
// flavors in which a path can be used there (e.g. for 'foo.origin.path', we would have to modify | ||
// 'foo' to have a 'foo.value'). So much easier to do it here... | ||
let impl = magicForHana[ ref.name.element ]; | ||
// FIXME: this is all not enough: we might need an explicit select item alias | ||
if (impl) | ||
return impl; | ||
// Various special cases represented as objects | ||
else if (typeof x == 'object' && x !== null) { | ||
// Literal value, possibly with explicit 'literal' property | ||
if (x.val !== undefined) { | ||
switch (x.literal || typeof x.val) { | ||
case 'number': | ||
case 'boolean': | ||
case 'null': | ||
// 17.42, NULL, TRUE | ||
return String(x.val).toUpperCase(); | ||
case 'x': | ||
// x'f000' | ||
return `${x.literal}'${x.val}'`; | ||
case 'date': | ||
case 'time': | ||
case 'timestamp': | ||
if (options.toSql.dialect === 'sqlite') { | ||
// date('2017-11-02') | ||
return `${x.literal}('${x.val}')`; | ||
} else { | ||
// date'2017-11-02' | ||
return `${x.literal}'${x.val}'`; | ||
} | ||
case 'string': | ||
// 'foo', with proper escaping | ||
return `'${x.val.replace(/'/g, "''")}'`; | ||
case 'object': | ||
if (x.val === null) { | ||
return 'NULL'; | ||
} | ||
// otherwise fall through to | ||
default: | ||
throw new Error('Unknown literal or type: ' + JSON.stringify(x)); | ||
} | ||
// TODO: really toUpperCase() if not forHana - even the $now etc? | ||
let start = String(path[0].id).toUpperCase(); | ||
return (path.length > 1) ? start + renderPath(path.slice(1), env) : start; | ||
} | ||
if(ref.kind == 'param') { | ||
let [ head, ...tail] = path; | ||
if(head.id == '$parameters') { | ||
path = tail; | ||
// Enum symbol | ||
else if (x['#']) { | ||
// #foo | ||
// FIXME: We can't do enums yet because they are not resolved (and we don't bother finding their value by hand) | ||
signal(error`Enum values are not yet supported for conversion to SQL`, x.location); | ||
return ''; | ||
} | ||
// Reference: Array of path steps, possibly preceded by ':' | ||
else if (x.ref) { | ||
if (options.forHana && !x.param && !x.global && x.ref[0] === '$user') { | ||
// FIXME: this is all not enough: we might need an explicit select item alias | ||
if (x.ref[1] === 'id') { | ||
if (options.forHana.dialect === 'sqlite') { | ||
if (options.toSql.user && typeof options.toSql.user === 'string' || options.toSql.user instanceof String) { | ||
return `'${options.toSql.user}'`; | ||
} | ||
else if ((options.toSql.user && options.toSql.user.id) && (typeof options.toSql.user.id === 'string' || options.toSql.user.id instanceof String)) { | ||
return `'${options.toSql.user.id}'`; | ||
} else { | ||
signal(warning`The "$user" variable is not supported by SQLite. Use the "toSql.user" option to set a value for "$user.id"`); | ||
return `'$user.id'`; | ||
} | ||
} | ||
return "SESSION_CONTEXT('XS_APPLICATIONUSER')"; | ||
} | ||
else if (x.ref[1] === 'locale') { | ||
return options.forHana.dialect === 'sqlite' | ||
? options.toSql.user && options.toSql.user.locale | ||
? `'${options.toSql.user && options.toSql.user.locale}'` : `'EN'` | ||
: "SESSION_CONTEXT('LOCALE')"; | ||
} | ||
} | ||
path[0].id = ':' + path[0].id; | ||
// Note: parameters should be of path length 1 | ||
return path.map(p => p.id.toUpperCase()).join('.') | ||
// FIXME: We currently cannot distinguish whether '$parameters' was quoted or not - we | ||
// assume that it was not if the path has length 2 ( | ||
if (firstPathStepId(x.ref) == '$parameters' && x.ref.length == 2) { | ||
// Parameters must be uppercased and unquoted in SQL | ||
return `:${x.ref[1].toUpperCase()}`; | ||
} | ||
if (x.param) { | ||
return `:${x.ref[0].toUpperCase()}`; | ||
} | ||
return x.ref.map(renderPathStep) | ||
.filter(s => s != '') | ||
.join('.'); | ||
} | ||
// Function call, possibly with args (use '=>' for named args) | ||
else if (x.func) { | ||
if (x.args) | ||
return `${x.func.toUpperCase()}(${renderArgs(x.args, '=>', env)})`; | ||
else | ||
return x.func; | ||
} | ||
// Nested expression | ||
else if (x.xpr) { | ||
return renderExpr(x.xpr, env); | ||
} | ||
// Sub-select | ||
else if (x.SELECT) { | ||
// renderQuery for SELECT does not bring its own parentheses (because it is also used in renderView) | ||
return `(${renderQuery('<subselect>', x, increaseIndent(env))})`; | ||
} | ||
else if (x.SET) { | ||
// renderQuery for SET always brings its own parentheses (because it is also used in renderViewSource) | ||
return `${renderQuery('<union>', x, increaseIndent(env))}`; | ||
} | ||
else { | ||
throw new Error('Unknown expression: ' + JSON.stringify(x)); | ||
} | ||
} | ||
// FIXME: Not the most elegant solution to do that here: filter out initial '$projection' and '$self' (because SQL | ||
// neither understands nor needs it). | ||
if (options.forHana && (path[0].id === '$projection' || path[0].id === '$self')) { | ||
return renderPath(path.slice(1), env); | ||
// Not a literal value but part of an operator, function etc - just leave as it is | ||
// FIXME: For the sake of simplicity, we should get away from all this uppercasing in toSql | ||
else { | ||
return String(x).toUpperCase(); | ||
} | ||
return path.map(step => { | ||
let result = quoteSqlId(step.id); | ||
if(step.namedArgs) { | ||
result += renderNamedArgs(step.namedArgs); | ||
// Render a single path step 's' at path position 'idx', which can have filters or parameters or be a function | ||
function renderPathStep(s, idx) { | ||
// Simple id or absolute name | ||
if (typeof(s) == 'string') { | ||
const magicForHana = { | ||
'$now': 'CURRENT_TIMESTAMP', | ||
'$user.id': "SESSION_CONTEXT('XS_APPLICATIONUSER')", | ||
'$user.locale': "SESSION_CONTEXT('LOCALE')", | ||
} | ||
// Some magic for first path steps | ||
if (idx == 0) { | ||
// HANA-specific translation of '$now' and '$user' | ||
// FIXME: this is all not enough: we might need an explicit select item alias | ||
if (magicForHana[s]) { | ||
return magicForHana[s]; | ||
} | ||
// Ignore initial $projection and initial $self | ||
if (s == '$projection' || s == '$self') { | ||
return ''; | ||
} | ||
} | ||
return quoteSqlId(s); | ||
} | ||
if (step.where) { | ||
result += '[' + (step.cardinality ? step.cardinality.targetMax.val + ': ' : '') + renderExpressionOrCondition(step.where, env, true) + ']'; | ||
// ID with filters or parameters | ||
else if (typeof s == 'object') { | ||
// Sanity check | ||
if (!s.func && !s.id) { | ||
throw new Error('Unknown path step object: ' + JSON.stringify(s)); | ||
} | ||
// Not really a path step but an object-like function call | ||
if (s.func) { | ||
return `${s.func}(${renderArgs(s.args, '=>', env)})`; | ||
} | ||
// Path step, possibly with view parameters and/or filters | ||
let result = `${quoteSqlId(s.id)}`; | ||
if (s.args) { | ||
// View parameters | ||
result += `(${renderArgs(s.args, '=>', env)})`; | ||
} | ||
if (s.where) { | ||
// Filter, possibly with cardinality | ||
// FIXME: Does SQL understand filter cardinalities? | ||
result += `[${s.cardinality ? (s.cardinality.max + ': ') : ''}${renderExpr(s.where, env)}]`; | ||
} | ||
return result; | ||
} | ||
return result; | ||
}).join('.'); | ||
else { | ||
throw new Error('Unknown path step: ' + JSON.stringify(s)); | ||
} | ||
} | ||
} | ||
@@ -994,7 +952,2 @@ | ||
// Returns a copy of 'env' with decreased indentation | ||
function decreaseIndent(env) { | ||
return Object.assign({}, env, { indent: env.indent.substring(2) }); | ||
} | ||
// Return 'name' in the form of an absolute CDS name - for the 'hdbcds' naming convention, | ||
@@ -1007,3 +960,3 @@ // this means converting '.' to '::' on the border between namespace and top-level artifact. | ||
} | ||
let topLevelName = getTopLevelArtifactNameOf(name, model); | ||
let topLevelName = getTopLevelArtifactNameOf(name, csn); | ||
let namespaceName = getParentNameOf(topLevelName); | ||
@@ -1017,4 +970,2 @@ if (namespaceName) { | ||
// Return 'name' with appropriate "-quotes. | ||
// FIXME: Should only quote where necessary (examining the id for magic characters and reserved | ||
// keywords) - for now, simply quote everything | ||
// Additionally perform the following conversions on 'name' | ||
@@ -1021,0 +972,0 @@ // If 'options.toSql.names' is 'plain' |
@@ -26,6 +26,3 @@ const schemaObjects = require('./swaggerSchemaObjects'); | ||
forEachDefinition(model, obj => { | ||
if (!obj._service) | ||
return; | ||
if (obj.kind === 'service') { | ||
if (obj.kind === 'service' && !obj.abstract) { | ||
swaggerJson = schemaObjects.openAPIObject(); | ||
@@ -32,0 +29,0 @@ swaggerJson.info.title = obj.name.absolute; |
'use strict'; | ||
const { forEachDefinition, forEachMemberRecursively, setProp } = require('../base/model'); | ||
const { compactModel } = require('../json/to-csn'); | ||
const deepCopy = require('../base/deepCopy'); | ||
@@ -40,10 +39,7 @@ const { CompilationError, hasErrors, sortMessages } = require('../base/messages'); | ||
const { flattenForeignKeys, createForeignKeyElement, checkForeignKeys, | ||
flattenStructuredElement, flattenStructStepsInPath, preprocessAction, | ||
setServiceProperty, checkExposedAssoc, toFinalBaseType, | ||
flattenStructuredElement, flattenStructStepsInPath, | ||
checkExposedAssoc, toFinalBaseType, | ||
addImplicitRedirections, createAndAddDraftAdminDataProjection, | ||
createScalarElement, createAssociationElement, createAssociationPathComparison, | ||
addElement, createAction, addAction } = transformUtils.getTransformers(model, '_'); | ||
// First walk through the model: perform preparations only | ||
// Set '_service' property for all artifacts and sub-artifacts, for each service in the model | ||
setServiceProperty(model); | ||
@@ -69,3 +65,3 @@ // Second walk: Flatten structs, unravel derived types, deal with annotations | ||
} | ||
if (!isElementWithType(elem)) { | ||
if (artifact._service && !isElementWithType(elem)) { | ||
signal(error`Element "${artifact.name.absolute}.${elemName}" does not have a type: Elements of ODATA entities must have a type`, elem.location); | ||
@@ -76,3 +72,3 @@ } | ||
// Types must not have anonymous structured elements | ||
else if (artifact.kind == 'type') { | ||
else if (artifact._service && artifact.kind == 'type') { | ||
for (let elemName in artifact.elements) { | ||
@@ -95,4 +91,4 @@ let elem = artifact.elements[elemName]; | ||
// Entities only: Flatten structs used in paths | ||
if (artifact.kind == 'entity') { | ||
// Entities and views only: Flatten structs used in paths | ||
if (artifact.kind == 'entity' || artifact.kind == 'view') { | ||
foreachPath(member, (path, pathOwner) => { | ||
@@ -107,2 +103,3 @@ pathOwner.path = flattenStructStepsInPath(path); | ||
toFinalBaseType(artifact.items); | ||
toFinalBaseType(artifact.returns); | ||
} | ||
@@ -130,2 +127,13 @@ // If the artifact is a derived structured type, unravel that as well | ||
// For exposed actions and functions that use non-exposed or anonymous structured types, create | ||
// artificial exposing types | ||
forEachDefinition(model, (artifact) => { | ||
if (artifact._service && (artifact.kind == 'action' || artifact.kind == 'function')) { | ||
exposeStructTypesForAction(artifact, artifact._service); | ||
} | ||
for (let actionName in artifact.actions || {}) { | ||
exposeStructTypesForAction(artifact.actions[actionName], artifact._service); | ||
} | ||
}); | ||
// Third walk: Generate foreign key fields for managed associations (must be done | ||
@@ -243,3 +251,3 @@ // after struct flattening, otherwise we might encounter already generated foreign | ||
if (!containedElem.notNull) { | ||
signal(error`"${containedArtifact.name.absolute}.${containedElemName}": Association to container entity must have "NOT NULL`, containedElem.location); | ||
signal(error`"${containedArtifact.name.absolute}.${containedElemName}": Association to container entity must have "NOT NULL"`, containedElem.location); | ||
} | ||
@@ -270,18 +278,19 @@ } | ||
} | ||
forEachMemberRecursively(artifact, member => { | ||
// Check that exposed associations do not point to non-exposed targets | ||
if (artifact._service && isAssociation(member.type)) { | ||
checkExposedAssoc(artifact, member); | ||
forEachMemberRecursively(artifact, (member, memberName) => { | ||
if (artifact._service) { | ||
if (isAssociation(member.type)) { | ||
// Check that exposed associations do not point to non-exposed targets | ||
checkExposedAssoc(artifact, member); | ||
// CDXCORE-457 | ||
if (artifact.kind === 'type' && options.toOdata.version == 'v2') { | ||
signal(warning`"${artifact.name.absolute}.${memberName}": Structured types must not contain associations for OData V2`, member.location); | ||
} | ||
} | ||
// CDXCORE-458 | ||
else if (member.kind == 'element' && member.items && options.toOdata.version == 'v2') { | ||
signal(error`"${artifact.name.absolute}.${memberName}": Element must not be an "array of" for OData V2`, member.location); | ||
} | ||
} | ||
}); | ||
// Preprocess bound actions/functions | ||
if (artifact.kind === 'entity' && artifact.actions && artifact._service) { | ||
Object.keys(artifact.actions).forEach(actName => preprocessAction(artifact.actions[actName])); | ||
} | ||
// Preprocess unbound actions/function | ||
if ((artifact.kind === 'action' || artifact.kind === 'function') && artifact._service) { | ||
preprocessAction(artifact); | ||
} | ||
}); | ||
@@ -541,2 +550,78 @@ | ||
} | ||
// If 'action' uses structured types as parameters or return values that are not exposed in 'service' | ||
// (because the types are anonymous or have a definition outside of 'service'), create equivalent types | ||
// in 'service' and make 'action' use them instead | ||
function exposeStructTypesForAction(action, service) { | ||
exposeStructTypeOf(action.returns, service, `__return_${actionNameNoDots(action)}`); | ||
for (let paramName in action.params || {}) { | ||
exposeStructTypeOf(action.params[paramName], service, `__param_${actionNameNoDots(action)}_${paramName}`); | ||
} | ||
} | ||
// Return the absolute name of an action/function, with all dots replaced by underscores | ||
function actionNameNoDots(action) { | ||
return action.name.absolute.replace(/\./g, '_') + (action.name.action ? `_${action.name.action}` : ''); | ||
} | ||
// If 'node' exists and has a structured type that is not exposed in 'service', (because the type is | ||
// anonymous or has a definition outside of 'service'), create an equivalent type in 'service', either | ||
// using the type's name or (if anonymous) 'artificialName', and make 'node' use that type instead. | ||
// If there is an error, complain on 'node.location'. | ||
function exposeStructTypeOf(node, service, artificialName) { | ||
if (!node) { | ||
return; | ||
} | ||
if (node.items) { | ||
exposeStructTypeOf(node.items, service, artificialName); | ||
} | ||
if ((node._finalType || node.type).elements && (!node.type || node.type._artifact._service != service)) { | ||
let typeId = node.type ? `__${node.type._artifact.name.absolute.replace(/\./g, '_')}` | ||
: artificialName; | ||
let type = exposeStructType(typeId, node._finalType.elements, service, node.location); | ||
if (!type) { | ||
// Error already reported | ||
return; | ||
} | ||
node.type = { | ||
path : [ { id: type.name.absolute } ], | ||
}; | ||
setProp(node.type, '_artifact', type); | ||
setProp(node.type.path[0], '_artifact', type); | ||
setProp(node, '_finalType', type); | ||
} | ||
} | ||
// Expose an artificial structured type with ID 'typeId' with 'elements' in 'service' (reusing such a type | ||
// if it already exists). | ||
// Return the exposed type. Report any errors on 'location' | ||
function exposeStructType(typeId, elements, service, location) { | ||
let typeName = `${service.name.absolute}.${typeId}`; | ||
// If type already exists, reuse it (complain if not created here) | ||
let type = model.definitions[typeName]; | ||
if (type) { | ||
if (type.$inferred == 'actionType') { | ||
return type; | ||
} else { | ||
signal(error`Cannot create artificial type "${typeName}" for an action or function because the name is already used`, location); | ||
return null; | ||
} | ||
} | ||
// Create type (use elements as they are) | ||
type = { | ||
name: { | ||
path: [ { id: typeName }], | ||
id: typeId, | ||
absolute: typeName, | ||
}, | ||
kind: 'type', | ||
elements: elements, // FIXME: Actually, we should deep-copy here and adapt _parent, _main, name , ... | ||
$inferred: 'actionType', | ||
}; | ||
setProp(type, '_finalType', type); | ||
service.artifacts[typeId] = type; | ||
setProp(type, '_parent', service); | ||
model.definitions[typeName] = type; | ||
return type; | ||
} | ||
} | ||
@@ -603,24 +688,5 @@ | ||
// Compact model and remove everything that that does not belong to the specified service | ||
// model: augmented csn, prepared for OData | ||
function compactForService(model, serviceName) { | ||
// Compact the model | ||
let compactedModel = compactModel(model); | ||
setProp(compactedModel, 'messages', model.messages); | ||
// Remove definitions that don't belong to given service | ||
if (serviceName) { | ||
let dict = compactedModel.definitions; | ||
let namesNotInService = Object.keys(dict).filter(n => !n.startsWith(serviceName + '.') && n != serviceName); | ||
for (let name of namesNotInService) { | ||
delete compactedModel.definitions[name]; | ||
} | ||
} | ||
return compactedModel; | ||
} | ||
module.exports = { | ||
transform4odata, | ||
getServiceNames, | ||
compactForService | ||
getServiceNames | ||
} |
@@ -10,5 +10,3 @@ const { setProp, forEachDefinition, forEachGeneric, forEachMemberRecursively } = require('../base/model'); | ||
const { error, signal } = alerts(model); | ||
const { preprocessAction, setServiceProperty, checkExposedAssoc } = transformUtils.getTransformers(model, '_'); | ||
// Set '_service' property for all artifacts and sub-artifacts, for each service in the model | ||
setServiceProperty(model); | ||
const { checkExposedAssoc } = transformUtils.getTransformers(model, '_'); | ||
@@ -20,8 +18,4 @@ forEachDefinition(model, obj => { | ||
processElements(art); | ||
if (art.projection) | ||
if (art.projection) // TODO: use art.query | ||
processProjection(obj, art); | ||
if (art.kind === 'entity' && art.actions) | ||
Object.keys(art.actions).forEach(a => preprocessAction(art.actions[a])); | ||
if (art.kind === 'action' || art.kind === 'function') | ||
preprocessAction(art); | ||
if (art.kind === 'type') | ||
@@ -82,3 +76,3 @@ processType(obj, art); | ||
function processProjection(parent, art) { | ||
if (parent._service !== art.source._artifact._service) { | ||
if (parent !== source(art)._service) { // parent is the service | ||
forEachMemberRecursively(art, elem => { | ||
@@ -93,3 +87,3 @@ if (elem.kind !== 'element') | ||
// the projection source with which the reference to be replaced | ||
parent.artifacts[projName].source._artifact.name.absolute === elem.target._artifact.name.absolute | ||
source(parent.artifacts[projName]).name.absolute === elem.target._artifact.name.absolute | ||
); | ||
@@ -158,2 +152,10 @@ if (targetFromCurrectService) | ||
} | ||
// TODO: consider making the above functionality work with more than one source | ||
function source( view ) { | ||
let from = view.query && view.query.from; | ||
return from && from[0] && from[0]._artifact; | ||
} | ||
module.exports = preprocessModel; |
@@ -131,2 +131,3 @@ 'use strict'; | ||
setProp(targetArtifact, '_parent', artifact); | ||
setProp(targetArtifact, '_service', artifact); | ||
if (sourceArtifact.source != targetArtifact.source) { | ||
@@ -133,0 +134,0 @@ targetArtifact.source = sourceArtifact.source; |
@@ -6,3 +6,3 @@ 'use strict'; | ||
const { setProp, cloneWithTransformations, forEachDefinition, forEachGeneric, | ||
const { setProp, cloneWithTransformations, forEachDefinition, | ||
forEachMemberRecursively } = require('../base/model'); | ||
@@ -25,4 +25,2 @@ const { addStringAnnotationTo, printableName, | ||
flattenStructStepsInPath, | ||
preprocessAction, | ||
setServiceProperty, | ||
checkExposedAssoc, | ||
@@ -396,84 +394,10 @@ toFinalBaseType, | ||
} | ||
// If the path starts with '$self', this is now redundant (because of flattening) and can be omitted, | ||
// making life easier for consumers | ||
if (result[0].id == '$self' && result.length > 1) { | ||
result = result.slice(1); | ||
} | ||
return result; | ||
} | ||
// Takes an augmented action/function object and checks if a defined type is used in the declaration, | ||
// if yes, then checks if it is from the same service as the action/function | ||
// used for OData and Swagger transformation | ||
function preprocessAction(action) { | ||
// A bound action has a parent - the corresponding entity | ||
// An unbound action does not have a parent, but a '_service' | ||
let actionBlock = action._parent ? action._parent._service : action._service; | ||
// an action can return a builtin, a defined type, an entity, an array of one of the first three or an inline structured type | ||
// if the action has a return declaration | ||
// and the returned artifact is not a builtin - check if it is from the current service | ||
if (action.returns) { | ||
let returnedTypes = action.returns.elements ? | ||
Object.keys(action.returns.elements).map(e => action.returns.elements[e].type) : | ||
action.returns.items | ||
? [action.returns.items.type] | ||
: [action.returns.type]; | ||
returnedTypes.forEach(returnType => { | ||
if (!returnType._artifact.name.absolute.startsWith('cds.')) { | ||
let returnTypeBlock = obtainTypesService(returnType); | ||
if (returnTypeBlock !== actionBlock) | ||
signal(error`The defined return type ${returnType._artifact.name.absolute} of action ${action.name.absolute}${action.name.action ? '.' + action.name.action : ''} is not from the current service ${actionBlock.name.absolute}`, action.location); | ||
} | ||
}); | ||
} | ||
// if the action has parameters and a paremeter is not a builtin - check if the used type is from the current service | ||
if (action.params) { | ||
for (let p in action.params) { | ||
let param = action.params[p]; | ||
if (param.type._artifact.name.absolute.startsWith('cds.')) | ||
continue; | ||
let paramTypeBlock = obtainTypesService(param.type); | ||
// this can happens if the special magic for @extends is used | ||
// ugly tnt magic -> to be removed | ||
if (!actionBlock && paramTypeBlock === 'not in a service') { | ||
if (/* actionBlock */ action.name.absolute.split('.').slice(0, -1).join('.') !== /* paramTypeBlock */ param.type._artifact.name.absolute.split('.').slice(0, -1).join('.')) | ||
signal(error`The type ${param.type._artifact.name.absolute} of parameter ${param.name.absolute}.${param.name.id} in action ${action.name.absolute}${action.name.action ? '.' + action.name.action : ''} is not from the current service`, param.location); | ||
} else if (paramTypeBlock !== actionBlock) | ||
signal(error`The type ${param.type._artifact.name.absolute} of parameter ${param.name.absolute}.${param.name.id} in action ${action.name.absolute}${action.name.action ? '.' + action.name.action : ''} is not from the current service ${actionBlock && actionBlock.name.absolute}`, param.location); | ||
} | ||
} | ||
// Returns the service where the type is declared or 'not in a service' if the type is outside of such. | ||
// Covers also the case when an element of defined type is used, for specifying a type. | ||
function obtainTypesService(type) { | ||
if (type._artifact._service) | ||
return type._artifact._service; | ||
if (type._artifact.kind === 'element' && type._artifact._main._service) | ||
return type._artifact._main._service; | ||
else | ||
return 'not in a service'; | ||
} | ||
} | ||
// Takes a model and looks for services inside it. | ||
// When a non-abstract service is found - adds an attribute '_service' to all artifacts that are exposed | ||
// in a service (pointing to the service artifact). | ||
function setServiceProperty(model) { | ||
forEachDefinition(model, artifact => { | ||
// If this is a non-abstract service, let all its (recursive) artifacts know | ||
if (artifact.kind == 'service' && !artifact.abstract) { | ||
setService(artifact, artifact); | ||
} | ||
// Set '_service' for 'artifact' and its sub-artifacts to 'service'. | ||
function setService(artifact, service) { | ||
// Services must not be nested | ||
if (artifact.kind == 'service' && artifact._service != undefined) { | ||
signal(error`Services cannot be nested: Service "${artifact.name.absolute}" is nested within service "${artifact._service.name.absolute}"`, artifact.location); | ||
} | ||
// Contexts cannot be defined within services | ||
if (artifact.kind == 'context') { | ||
signal(error`Contexts cannot be defined within services: Context "${artifact.name.absolute}" is defined within service "${service.name.absolute}"`, artifact.location); | ||
} | ||
setProp(artifact, '_service', service); | ||
forEachGeneric(artifact, 'artifacts', subartifact => setService(subartifact, service)); | ||
} | ||
}); | ||
} | ||
// Check that exposed associations do not point to non-exposed targets | ||
@@ -486,3 +410,4 @@ function checkExposedAssoc(artifact, association) { | ||
// Replace the type of 'node' with its final base type (in contrast to the compiler, | ||
// also unravel derived enum types, i.e. take the final base type of the enum's base type. | ||
// also unravel derived enum types, i.e. take the final base type of the enum's base type. | ||
// Similar with associations and compositions (we probably need a _baseType link) | ||
function toFinalBaseType(node) { | ||
@@ -504,2 +429,5 @@ // Nothing to do if no type (or if array/struct type) | ||
} | ||
// If that is an e | ||
while (baseType._artifact.target) | ||
baseType = baseType._artifact.type; | ||
node.type = { | ||
@@ -529,2 +457,9 @@ path: [ { id: baseType._artifact.name.absolute } ], | ||
} | ||
considerForImplicitRedirectionOrAutoExposure(artifact, artifactName); | ||
}); | ||
// For all elements of 'artifact', see whether they require implicit redirection or the creation of an | ||
// artificial exposing projection (via `@cds.autoexpose`). Do this recursively for artificially generated | ||
// projections, too | ||
function considerForImplicitRedirectionOrAutoExposure(artifact, artifactName) { | ||
// Perform implicit re-targeting of association elements based on exposure: | ||
@@ -546,2 +481,4 @@ forEachMemberRecursively(artifact, (member, memberName) => { | ||
// console.log(`Auto-exposing target ${member.target._artifact.name.absolute} of association ${artifactName}.${memberName} as ${projection.name.absolute}`); | ||
// The newly created exposing projection may in turn contain associations to non-exposed artifacts | ||
considerForImplicitRedirectionOrAutoExposure(projection, projection.name.absolute); | ||
} | ||
@@ -555,3 +492,3 @@ } | ||
}); | ||
}); | ||
} | ||
@@ -611,5 +548,5 @@ // Redirect element 'assoc' with name 'assocName' in 'artifact' with name 'artifactName' implicitly | ||
// A projection or view with a single query and a single, simple source also exposes the source | ||
if (exposedArtifact.queries && exposedArtifact.queries.length == 1 | ||
&& exposedArtifact.queries[0].from.length == 1 && exposedArtifact.queries[0].from[0].path) { | ||
let from = exposedArtifact.queries[0].from; | ||
if (exposedArtifact.$queries && exposedArtifact.$queries.length == 1 | ||
&& exposedArtifact.$queries[0].from.length == 1 && exposedArtifact.$queries[0].from[0].path) { | ||
let from = exposedArtifact.$queries[0].from; | ||
// Sanity check | ||
@@ -676,4 +613,4 @@ if (!from[0]._artifact) { | ||
id : elemName, | ||
absolute: projectionAbsoluteName, | ||
element: elemName, | ||
// absolute: projectionAbsoluteName, | ||
// element: elemName, | ||
path: [ { id : art.name.id }, { id : elemName } ] | ||
@@ -723,6 +660,11 @@ } | ||
queries: [ query ], | ||
source: source, | ||
$queries: [ query ], | ||
// source: source, | ||
elements: query.elements, | ||
$generatedByAutoExposure: true, | ||
}; | ||
// copy annotations from art to projection | ||
for (let a of Object.keys(art).filter(x => x.startsWith('@'))) { | ||
projection[a] = art[a]; | ||
} | ||
setProp(projection, '_service', service); | ||
@@ -758,3 +700,3 @@ // Sanity check: Can't already be there (checked above) | ||
function isDollarSelfOperand(arg) { | ||
return arg.path && arg.path.length == 1 && (arg.path[0].id == '$self' || model.options.oldstyleSelf && arg.path[0].id == 'self'); | ||
return arg.path && arg.path.length == 1 && (arg.path[0].id == '$self'); | ||
} | ||
@@ -761,0 +703,0 @@ |
@@ -12,2 +12,5 @@ 'use strict' | ||
{ | ||
// Paths that start with an artifact of protected kind are special | ||
// either ignore them in QAT building or in path rewriting | ||
const internalArtifactKinds = ['builtin'/*, '$parameters'*/, 'param']; | ||
@@ -36,4 +39,3 @@ const { error, signal } = alerts(model); | ||
{ | ||
let type = art._finalType; | ||
if(art.kind === 'element' && type && type.target) // contexts have no type | ||
if(art.kind === 'element' && art.target) | ||
{ | ||
@@ -50,7 +52,7 @@ /* Create the prefix string up to the main artifact which is | ||
*/ | ||
if(type.foreignKeys && !type.$fkPathPrefixTree) | ||
if(art.foreignKeys && !art.$fkPathPrefixTree) | ||
{ | ||
type.$fkPathPrefixTree = { children: Object.create(null) }; | ||
forEachGeneric(type, 'foreignKeys', fk => { | ||
let ppt = type.$fkPathPrefixTree; | ||
art.$fkPathPrefixTree = { children: Object.create(null) }; | ||
forEachGeneric(art, 'foreignKeys', fk => { | ||
let ppt = art.$fkPathPrefixTree; | ||
fk.targetElement.path.forEach(ps => { | ||
@@ -69,7 +71,7 @@ if(!ppt.children[ps.id]) | ||
tableAliases: [ art.name.id ], | ||
type, | ||
art, | ||
location: 'onCondAssoc', | ||
callback: [ flyTrap ] | ||
}; | ||
walk(type.onCond || type.on, env); | ||
walk(art.onCond || art.on, env); | ||
} | ||
@@ -83,3 +85,3 @@ } | ||
{ | ||
let queries = art.queries; | ||
let queries = art.$queries; | ||
let fullJoins = fullJoinOption; | ||
@@ -90,6 +92,7 @@ | ||
/* | ||
HANA cannot process associations with parameters, filters on first FROM path step and mixin-assoc usages | ||
Filters and mixin usages will lead to a minimum join translation (only FROM clause and MIXINs) | ||
An association path step with parameters requires a full join conversion or if parameter path step is | ||
not at leaf position (entity/view with parameters followed by another association in FROM clause) | ||
HANA cannot process associations with parameters, filters in FROM paths and mixin-assoc usages | ||
Filters and mixin usages will lead to a minimum join translation (only FROM clause and MIXIN usages) | ||
A full join conversion is required if an assoc path step has parameters or if parameter path step is | ||
not at leaf position (entity/view with parameters followed by another association in FROM clause), or | ||
if a filter has a cardinality ':1'. | ||
*/ | ||
@@ -104,10 +107,10 @@ if(!fullJoinOption) { | ||
env.minimum = env.minimum || | ||
(env.location == 'from' ? !!pathDict.path.filter(p => isEntityOrView(p._artifact))[0].where | ||
(env.location == 'from' ? pathDict.path.some(p => p.where) //!!pathDict.path.filter(p => isEntityOrView(p._artifact))[0].where | ||
: (pathDict.path[0]._navigation && pathDict.path[0]._navigation.kind == 'element' && pathDict.path.length > 1)); | ||
env.full = env.full || pathDict.path.some((e, i, a) => { | ||
return !!e.namedArgs && (e._artifact.target || i < a.length-1) }) | ||
return !!e.namedArgs && (e._artifact.target || i < a.length-1) || e.cardinality }) | ||
} | ||
} | ||
queries.map(q => walkQuery(q, env)); | ||
queries.forEach(q => walkQuery(q, env)); | ||
@@ -133,5 +136,5 @@ if(!env.minimum && !env.full) | ||
}; | ||
queries.map(q => createQAForMixinAssoc(q, env)); | ||
queries.map(q => walkQuery(q, env)); | ||
queries.map(q => createQAForFromClauseSubQuery(q, env)); | ||
queries.forEach(q => createQAForMixinAssoc(q, env)); | ||
queries.forEach(q => walkQuery(q, env)); | ||
queries.forEach(q => createQAForFromClauseSubQuery(q, env)); | ||
@@ -141,6 +144,6 @@ // 2) Walk over each from table path, transform it into a join tree | ||
env.callback = createInnerJoins; | ||
queries.map(q => walkQuery(q, env)); | ||
queries.forEach(q => walkQuery(q, env)); | ||
// 3) Transform toplevel FROM block into cross join | ||
queries.map(q => createCrossJoins(q)); | ||
queries.forEach(q => createCrossJoins(q)); | ||
@@ -150,3 +153,3 @@ // 4) Transform all remaining join relevant paths into left outer joins and connect with | ||
// of each $tableAlias. | ||
queries.map(q => createLeftOuterJoins(q, env)); | ||
queries.forEach(q => createLeftOuterJoins(q, env)); | ||
@@ -158,6 +161,6 @@ // 5) Rewrite ON condition paths that are part of the original FROM block | ||
env.callback = rewriteGenericPaths; | ||
queries.map(q => walkQuery(q, env)); | ||
queries.forEach(q => walkQuery(q, env)); | ||
// 7) Attach firstFilterConds to Where Condition. | ||
queries.map(q => attachFirstFilterConditions(q)); | ||
queries.forEach(q => attachFirstFilterConditions(q)); | ||
@@ -177,5 +180,2 @@ // TODO: support parameters | ||
query.from.splice(0, query.from.length, { op: { val: 'join' }, join: 'cross', args: [...query.from ] }); | ||
// Recurse into sub queries | ||
query.queries.map(q => createCrossJoins(q)); | ||
} | ||
@@ -209,5 +209,2 @@ } | ||
query.from = [ joinTree ]; | ||
// Recurse into sub queries | ||
query.queries.map(q => createLeftOuterJoins(q, env)); | ||
} | ||
@@ -226,11 +223,18 @@ } | ||
{ | ||
for (let taName in query.$tableAliases) { | ||
if (!['$self', '$projection'].includes(taName)) { | ||
let ta = query.$tableAliases[taName]; | ||
if(!ta.$QA) { | ||
ta.$QA = createQA(env, ta._finalType, undefined, taName); | ||
incAliasCount(env, ta.$QA); | ||
if(ta.name && ta.name.id) { | ||
ta.name.id = ta.$QA.name.id; | ||
} | ||
} | ||
} | ||
} | ||
// Only subqueries of the FROM clause have a name (which is the alias) | ||
if(query.op.val === 'query' && query.name.id) | ||
if(query.op.val === 'query' && query.name.id && query._tableAlias) | ||
{ | ||
// Set the QA for the outer ON cond paths and rename the query name itself | ||
let QA = query._tableAlias._parent.$tableAliases[query.name.id].$QA = createQA(env, query); | ||
incAliasCount(env, QA); | ||
query.name.id = QA.name.id; | ||
query.queries.map(q => createQAForFromClauseSubQuery(q, env)); | ||
query.name.id = query._tableAlias._parent.$tableAliases[query.name.id].$QA.name.id; | ||
} | ||
@@ -261,4 +265,2 @@ } | ||
}); | ||
query.queries.map(q => createQAForMixinAssoc(q, env)); | ||
} | ||
@@ -277,3 +279,3 @@ } | ||
{ | ||
if(pathNode.rewritten) | ||
if(pathNode.$rewritten) | ||
return; | ||
@@ -336,4 +338,2 @@ | ||
} | ||
// Recurse into sub queries | ||
query.queries.map(q => attachFirstFilterConditions(q)); | ||
} | ||
@@ -426,51 +426,146 @@ } | ||
let srcTableAlias = { id: assocSourceQA.name.id, _artifact: assocSourceQA._artifact }; | ||
let tgtTableAlias = { id: assocQAT.$QA.name.id, _artifact: assocQAT.$QA._artifact }; | ||
let tgtTableAlias = { id: assocQAT.$QA.name.id, _artifact: assocQAT.$QA._artifact }; | ||
let assocElt = assocQAT.origin._artifact; | ||
node.on = createOnCondition(assocQAT.origin._artifact, srcTableAlias, tgtTableAlias); | ||
// Inject the ON condition of the managed association | ||
if(assocElt._finalType.foreignKeys) | ||
if(assocQAT._filter) | ||
{ | ||
/* | ||
Get both the source and the target column names for the EQ term. | ||
For the src side provide a path prefix for all paths that is the assocElement name itself preceded by | ||
the path up to the first lead artifact (usually the entity or view) (or in QAT speak: follow the parent | ||
QATs until a QA has been found). | ||
*/ | ||
let srcPaths = flattenElement(assocElt, true, assocElt.name.element.replace(/\./g, pathDelimiter)); | ||
let tgtPaths = flattenElement(assocElt, false); | ||
// Filter conditions are unique for each JOIN, they don't need to be copied | ||
let filter = assocQAT._filter; | ||
rewritePathsInExpression(filter, function(pathNode) { | ||
return [ tgtTableAlias, pathNode.path ]; | ||
}); | ||
if(srcPaths.length != tgtPaths.length) | ||
throw Error('srcPaths length ['+srcPaths.length+'] != tgtPaths length ['+tgtPaths.length+']'); | ||
// If toplevel ON cond op is AND add filter condition to the args array, | ||
// create a new toplevel AND op otherwise | ||
let onCond = (Array.isArray(node.on) ? node.on[0] : node.on); | ||
if(onCond.op.val == 'and') | ||
// parenthesize filter | ||
onCond.args.push( [ filter ] ); | ||
else | ||
// parenthesize onCond and filter | ||
node.on = [ { op: { val: 'and' }, args: [ [ onCond ], [ filter ] ] } ]; | ||
} | ||
return node; | ||
/* | ||
Put all src/tgt path siblings into the EQ term and create the proper path objects | ||
with the src/tgt table alias path steps in front. | ||
// produce the ON condition for a given association | ||
function createOnCondition(assoc, srcAlias, tgtAlias) | ||
{ | ||
let prefixes = [ assoc.name.id ]; | ||
/* This is no art and can be removed once ON cond for published | ||
and renamed backlink assocs are publicly available. Example: | ||
entity E { ...; toE: association to E; toEb: association to E on $self = toEb.toE; }; | ||
entity EP as projection on E { *, toEb as foo }; | ||
This requires ON cond rewritten to: $self = foo.toE but instead its still $self = toEb.toE, | ||
so prefix 'foo' won't match.... | ||
*/ | ||
let args = []; | ||
for(let i = 0; i < srcPaths.length; i++) | ||
if(assoc.origin && assoc.origin._artifact && !prefixes.includes(assoc.origin._artifact.name.id)) | ||
prefixes.push(assoc.origin._artifact.name.id); | ||
// produce the ON condition of the managed association | ||
if(assoc.foreignKeys) | ||
{ | ||
args.push({op: {val: '=' }, | ||
args: [ constructPathNode( [ srcTableAlias, srcPaths[i] ] ), | ||
constructPathNode( [ tgtTableAlias, tgtPaths[i] ] ) ] }); // eslint-disable-line indent-legacy | ||
/* | ||
Get both the source and the target column names for the EQ term. | ||
For the src side provide a path prefix for all paths that is the assocElement name itself preceded by | ||
the path up to the first lead artifact (usually the entity or view) (or in QAT speak: follow the parent | ||
QATs until a QA has been found). | ||
*/ | ||
let srcPaths = flattenElement(assoc, true, assoc.name.element.replace(/\./g, pathDelimiter)); | ||
let tgtPaths = flattenElement(assoc, false); | ||
if(srcPaths.length != tgtPaths.length) | ||
throw Error('srcPaths length ['+srcPaths.length+'] != tgtPaths length ['+tgtPaths.length+']'); | ||
/* | ||
Put all src/tgt path siblings into the EQ term and create the proper path objects | ||
with the src/tgt table alias path steps in front. | ||
*/ | ||
let args = []; | ||
for(let i = 0; i < srcPaths.length; i++) | ||
{ | ||
args.push({op: {val: '=' }, | ||
args: [ constructPathNode( [ srcAlias, srcPaths[i] ] ), | ||
constructPathNode( [ tgtAlias, tgtPaths[i] ] ) ] }); // eslint-disable-line indent-legacy | ||
} | ||
// Parenthesize each AND term | ||
return [ (args.length > 1 ? { op: { val: 'and' }, args: [ ...args.map(a=>[a]) ] } : args[0] ) ]; | ||
} | ||
// Parenthesize each AND term | ||
node.on = [ (args.length > 1 ? { op: { val: 'and' }, args: [ ...args.map(a=>[a]) ] } : args[0] ) ]; | ||
else | ||
return cloneOnCondition(assoc.onCond || assoc.on); | ||
} | ||
// Inject the ON condition of the unmanaged association | ||
else if (assocElt.onCond || assocElt.on) | ||
{ | ||
node.on = clone(assocElt.onCond || assocElt.on); | ||
rewritePathsInExpression(node.on, function(pathNode) | ||
// clone ON condition with rewritten paths and substituted backlink conditions | ||
function cloneOnCondition(expr) | ||
{ | ||
if(expr.op && expr.op.val === 'xpr') | ||
return cloneOnCondExprStream(expr); | ||
else | ||
return cloneOnCondExprTree(expr); | ||
} | ||
function cloneOnCondExprStream(expr) { | ||
let args = expr.args; | ||
let result = { op: { val: expr.op.val }, args: [] }; | ||
for(let i = 0; i < args.length; i++) | ||
{ | ||
if(args[i].op && args[i].op.val === 'xpr') | ||
{ | ||
result.args.push(cloneOnCondition(args[i])); | ||
} | ||
// If this is a backlink condition, produce the | ||
// ON cond of the forward assoc with swapped src/tgt aliases | ||
else if(i < args.length-2 && args[i].path && args[i+1] == '=' && args[i+2].path) | ||
{ | ||
let fwdAssoc = getForwardAssociation(args[i].path, args[i+2].path); | ||
if(fwdAssoc) | ||
{ | ||
result.args.push(createOnCondition(fwdAssoc, tgtAlias, srcAlias)); | ||
i += 2; // skip next two tokens and continue with loop | ||
continue; | ||
} | ||
} | ||
result.args.push(rewritePathNode(args[i])); | ||
} | ||
return result; | ||
} | ||
function cloneOnCondExprTree(expr) { | ||
// keep parentheses intact | ||
if(Array.isArray(expr)) | ||
return expr.map(cloneOnCondition); | ||
// If this is a backlink condition, produce the | ||
// ON cond of the forward assoc with swapped src/tgt aliases | ||
let fwdAssoc = getForwardAssociationExpr(expr); | ||
if(fwdAssoc) | ||
return createOnCondition(fwdAssoc, tgtAlias, srcAlias); | ||
// If this is an ordinary expression, clone it and mangle its arguments | ||
// this will substitute multiple backlink conditions ($self = ... AND $self = ...AND ...) | ||
if(expr.op) | ||
return { op: { val: expr.op.val }, args: expr.args.map(cloneOnCondition) }; | ||
// If this is a regular path, rewrite it | ||
return rewritePathNode(expr); | ||
} | ||
function rewritePathNode(pathNode) | ||
{ | ||
let tableAlias; | ||
let path = pathNode.path; | ||
if(!path) // it's not a path return it | ||
return pathNode; | ||
let [head, ...tail] = path; | ||
if(internalArtifactKinds.includes(head._artifact.kind)) // don't rewrite path | ||
return pathNode; | ||
if(assocSourceQA.mixin) | ||
{ | ||
if(head.id === '$projection') | ||
throw Error('Following mix-in association "' + assocElt.name.id + | ||
throw Error('Following mix-in association "' + assoc.name.id + | ||
'" in defining view is not allowed with ON condition from projection: ' + | ||
@@ -510,11 +605,11 @@ pathAsStr(pathNode.path, '"')); | ||
return constructTableAliasAndTailPath(path); | ||
[ tableAlias, path ] = constructTableAliasAndTailPath(path); | ||
} | ||
else // ON condition of non-mixin association | ||
{ | ||
if(head.id === assocQAT.name.id) // target side | ||
if(prefixes.includes(head.id)) // target side | ||
{ | ||
// no element prefix on target side | ||
path = translateONCondPath(tail); | ||
tableAlias = tgtTableAlias; | ||
tableAlias = tgtAlias; | ||
} | ||
@@ -528,39 +623,31 @@ else // source side | ||
tableAlias = srcTableAlias; | ||
tableAlias = srcAlias; | ||
// if path is not an absolute path, prepend element prefix | ||
path = translateONCondPath(path, !isAbsolutePath ? assocElt.$elementPrefix : undefined); | ||
path = translateONCondPath(path, !isAbsolutePath ? assoc.$elementPrefix : undefined); | ||
} | ||
} | ||
return [ tableAlias, path ]; | ||
}); | ||
} | ||
else | ||
{ | ||
throw Error('assocQAT has neither foreign keys nor an on condition:' + | ||
assocElt.name.absolute + pathDelimiter + assocElt.name.element); | ||
} | ||
let pathStr = path.map(ps => ps.id).join(pathDelimiter); | ||
return constructPathNode([ tableAlias, { id: pathStr, _artifact: pathNode._artifact } ]); | ||
} | ||
if(assocQAT._filter) | ||
{ | ||
// Filter conditions are unique for each JOIN, they don't need to be copied | ||
let filter = assocQAT._filter; | ||
rewritePathsInExpression(filter, function(pathNode) { | ||
return [ tgtTableAlias, pathNode.path ]; | ||
}); | ||
// Return the original association if expr is a backlink term, undefined otherwise | ||
function getForwardAssociationExpr(expr) { | ||
if(expr.op && expr.op.val == '=' && expr.args.length == 2) { | ||
return getForwardAssociation(expr.args[0].path, expr.args[1].path); | ||
} | ||
return undefined; | ||
} | ||
// If toplevel ON cond op is AND add filter condition to the args array, | ||
// create a new toplevel AND op otherwise | ||
let onCond = (Array.isArray(node.on) ? node.on[0] : node.on); | ||
function getForwardAssociation(lhs, rhs) { | ||
if(lhs && rhs) { | ||
if(rhs.length == 1 && rhs[0].id == '$self' && lhs.length > 1 && prefixes.includes(lhs[lhs.length-2].id)) | ||
return lhs[lhs.length-1]._artifact; | ||
if(lhs.length == 1 && lhs[0].id == '$self' && rhs.length > 1 && prefixes.includes(rhs[rhs.length-2].id)) | ||
return rhs[rhs.length-1]._artifact; | ||
} | ||
return undefined; | ||
} | ||
} // createOnCondition | ||
} // createJoinQA | ||
if(onCond.op.val == 'and') | ||
// parenthesize filter | ||
onCond.args.push( [ filter ] ); | ||
else | ||
// parenthesize onCond and filter | ||
node.on = [ { op: { val: 'and' }, args: [ [ onCond ], [ filter ] ] } ]; | ||
} | ||
return node; | ||
} | ||
/* | ||
@@ -622,12 +709,10 @@ A QA (QueryArtifact) is a representative for a table/view that must appear | ||
let node = { | ||
rewritten: true, | ||
$rewritten: true, | ||
path : pathSteps.map(p => { | ||
let o = Object.assign({}, { id: p.id }); | ||
let o = {}; | ||
Object.keys(p).forEach(k => { | ||
if(!['_'].includes(k[0])) | ||
o[k] = p[k]; | ||
}); | ||
setProp(o, '_artifact', p._artifact ); | ||
if(p.where) | ||
o.where = p.where; | ||
if(p.namedArgs) | ||
o.namedArgs = p.namedArgs; | ||
if(p.args) | ||
o.args = p.args; | ||
return o; }) | ||
@@ -669,3 +754,3 @@ }; | ||
// terminate if element is unstructured | ||
if(!element._finalType.foreignKeys && !element.elements) | ||
if(!element.foreignKeys && !element.elements) | ||
return [ { id: prefix, _artifact: element } ]; | ||
@@ -675,7 +760,7 @@ | ||
// get paths of managed assocs (unmanaged assocs are not allowed in FK paths) | ||
if(element._finalType.foreignKeys) | ||
if(element.foreignKeys) | ||
{ | ||
for(let fkn in element._finalType.foreignKeys) | ||
for(let fkn in element.foreignKeys) | ||
{ | ||
let fk = element._finalType.foreignKeys[fkn]; | ||
let fk = element.foreignKeys[fkn]; | ||
// once a fk is to be followed, treat all sub patsh as srcSide, this will add fk.name.id only | ||
@@ -757,3 +842,3 @@ if(srcSide) | ||
pathStr += ps.id; | ||
return (ps._artifact._finalType.target); // true if it has a target => is assoc => terminate find | ||
return (ps._artifact.target); // true if it has a target => is assoc => terminate find | ||
}); | ||
@@ -820,3 +905,3 @@ return [ assocStep, path.slice(path.indexOf(assocStep)+1), pathStr ]; | ||
{ | ||
if(path[path.length-1].id != '$self' && pathDict._artifact._finalType.elements) | ||
if(path[path.length-1].id != '$self' && pathDict._artifact.elements) | ||
signal(error`${'Only scalar types allowed in this location of a query: ' + pathAsStr(pathDict.path, "'")}`); | ||
@@ -831,5 +916,5 @@ | ||
path.forEach(ps => { | ||
if(ps._artifact._finalType.target) | ||
if(ps._artifact.target) | ||
{ | ||
if(ps._artifact._finalType.on) | ||
if(ps._artifact.onCond || ps._artifact.on) | ||
{ | ||
@@ -842,3 +927,3 @@ if(env.location !== 'onCondAssoc') | ||
let la1 = pathDict.path[pathDict.path.indexOf(ps)+1]; | ||
if(la1 && !ps._artifact._finalType.$fkPathPrefixTree.children[la1.id]) | ||
if(la1 && !ps._artifact.$fkPathPrefixTree.children[la1.id]) | ||
signal(error`Pathstep ' + la1.id + ' is not foreign key of association ' + | ||
@@ -975,4 +1060,10 @@ ps.id + ' in ON condition path: ' + pathAsStr(pathDict.path)`); | ||
qat = linkToOrigin(pathStep._artifact, pathStep.id, qatParent, undefined, pathStep.location); | ||
if(pathStep.where) | ||
qat._filter = pathStep.where; | ||
/* | ||
Query filter have precedence over default filters. | ||
Clone default filter for each usage to avoid path rewriting of the definition. | ||
TODO: If Filter become JOIN relevant, default filters MUST BE cloned before starting the transformation | ||
or the paths won't be added to the QAT and the rewriting would be done on the filter definition. | ||
*/ | ||
if(pathStep.where /*|| pathStep._artifact.where*/) | ||
qat._filter = pathStep.where /*|| clone(pathStep._artifact.where)*/; | ||
if(pathStep.namedArgs) | ||
@@ -1080,5 +1171,2 @@ qat._namedArgs= pathStep.namedArgs; | ||
// walk all subqueries of this query | ||
query.queries.map(q => walkQuery(q, env)); | ||
function walkFrom(query) | ||
@@ -1107,3 +1195,3 @@ { | ||
env.tableAliases = aliases; | ||
walk(query.on, env) | ||
walk(query.onCond || query.on, env) | ||
delete env.tableAliases; | ||
@@ -1150,3 +1238,3 @@ } | ||
let art = path && path.length && path[path.length-1]._artifact; | ||
if(art && !['$builtin', '$parameters', 'param'].includes(art.kind)) | ||
if(art && !internalArtifactKinds.includes(art.kind)) | ||
{ | ||
@@ -1157,3 +1245,3 @@ if(env.callback) | ||
if(Array.isArray(env.callback)) | ||
env.callback.map(cb => cb(node, env)); | ||
env.callback.forEach(cb => cb(node, env)); | ||
else | ||
@@ -1163,2 +1251,12 @@ env.callback(node, env); | ||
/* | ||
NOTE: As long as association path steps are not allowed in filters, | ||
it is not required to walk over filter expressions. | ||
Simple filter paths are rewritten inin createJoinTree (first filter) | ||
and createJoinQA (subsequent one that belong to the ON condition). | ||
If the filter becomes JOIN relevant, default FILTERS (part of the | ||
association definition) MUST be CLONED to each assoc path step | ||
BEFORE resolution. | ||
let filterEnv = Object.assign({walkover: {} }, env); | ||
@@ -1175,2 +1273,3 @@ filterEnv.location = 'filter'; | ||
} | ||
*/ | ||
// TODO: Parameter expressions! | ||
@@ -1228,3 +1327,2 @@ } | ||
function clone(obj) { | ||
@@ -1231,0 +1329,0 @@ let newObj; |
{ | ||
"name": "@sap/cds-compiler", | ||
"version": "1.5.0", | ||
"version": "1.8.0", | ||
"dependencies": { | ||
"ajv": { | ||
"version": "6.1.1", | ||
"dependencies": { | ||
"json-schema-traverse": { | ||
"version": "0.3.1" | ||
}, | ||
"fast-deep-equal": { | ||
"version": "1.0.0" | ||
}, | ||
"fast-json-stable-stringify": { | ||
"version": "2.0.0" | ||
} | ||
} | ||
}, | ||
"antlr4": { | ||
"version": "4.7.1" | ||
}, | ||
"commander": { | ||
"version": "2.17.1" | ||
}, | ||
"fs-extra": { | ||
"version": "7.0.0", | ||
"dependencies": { | ||
"universalify": { | ||
"version": "0.1.2" | ||
}, | ||
"graceful-fs": { | ||
"version": "4.1.15" | ||
}, | ||
"jsonfile": { | ||
"version": "4.0.0", | ||
"dependencies": { | ||
"graceful-fs": { | ||
"version": "4.1.15" | ||
} | ||
} | ||
} | ||
} | ||
}, | ||
"resolve": { | ||
"version": "1.5.0", | ||
"version": "1.8.1", | ||
"dependencies": { | ||
@@ -47,0 +11,0 @@ "path-parse": { |
@@ -1,1 +0,1 @@ | ||
{"bin":{"cdsc":"bin/cdsc.js"},"bundleDependencies":false,"dependencies":{"ajv":"6.1.1","antlr4":"4.7.1","commander":"2.17.1","fs-extra":"7.0.0","resolve":"1.5.0","sax":"1.2.4"},"deprecated":false,"description":"Standard-Feature-Set Vanilla-CDS in Product Quality","keywords":["CDS"],"main":"lib/main.js","name":"@sap/cds-compiler","repository":{"type":"git","url":"git@github.wdf.sap.corp/CDS/cds-compiler.git"},"version":"1.5.0","license":"SEE LICENSE IN developer-license-3.1.txt"} | ||
{"bin":{"cdsc":"bin/cdsc.js","cdshi":"bin/cdshi.js","cdsse":"bin/cdsse.js"},"bundleDependencies":false,"dependencies":{"antlr4":"4.7.1","resolve":"1.8.1","sax":"1.2.4"},"deprecated":false,"description":"CDS (Core Data Services) compiler and backends","keywords":["CDS"],"main":"lib/main.js","name":"@sap/cds-compiler","version":"1.8.0","license":"SEE LICENSE IN developer-license-3.1.txt"} |
@@ -8,33 +8,6 @@ # Getting started | ||
[Installation and Usage](#installation-and-usage) | ||
[Documentation](#documentation) | ||
[Fiori annotations](doc/FioriAnnotations.md) | ||
[Command invocation](#command-invocation) | ||
## Installation and Usage | ||
### github | ||
***Do not add direct dependency to cdsv's github project!*** | ||
### Snapshots/Milestones/Releases | ||
Configure Nexus registry: | ||
* snapshots | ||
``` | ||
npm config set registry "http://nexus.wdf.sap.corp:8081/nexus/content/groups/build.snapshots.npm" | ||
``` | ||
* milestones | ||
``` | ||
npm config set registry "http://nexus.wdf.sap.corp:8081/nexus/content/groups/build.milestones.npm" | ||
``` | ||
* releases | ||
``` | ||
npm config set registry "http://nexus.wdf.sap.corp:8081/nexus/content/groups/build.releases.npm" | ||
``` | ||
Install via npm: | ||
@@ -59,5 +32,5 @@ | ||
```bash | ||
cdsc [options] <file...> | ||
cdsc <command> [options] <file...> | ||
``` | ||
See `cdsc --help` for the options. | ||
See `cdsc --help` for commands and options. | ||
@@ -69,5 +42,1 @@ The exit code is similar to [`grep` and other commands](http://stackoverflow.com/questions/1101957/are-there-any-standard-exit-status-codes-in-linux): | ||
* `2`: commmand invocation error (invalid options, repeated file name) | ||
## Documentation | ||
See <https://github.wdf.sap.corp/pages/cap/CDS>. |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
No repository
Supply chain riskPackage does not have a linked source code repository. Without this field, a package will have no reference to the location of the source code use to generate the package.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Dynamic require
Supply chain riskDynamic require can indicate the package is performing dangerous or unsafe dynamic code execution.
Found 1 instance in 1 package
2934321
3
105
57369
9
40
+ Addedresolve@1.8.1(transitive)
- Removedajv@6.1.1
- Removedcommander@2.17.1
- Removedfs-extra@7.0.0
- Removedajv@6.1.1(transitive)
- Removedcommander@2.17.1(transitive)
- Removedfast-deep-equal@1.1.0(transitive)
- Removedfast-json-stable-stringify@2.1.0(transitive)
- Removedfs-extra@7.0.0(transitive)
- Removedgraceful-fs@4.2.11(transitive)
- Removedjson-schema-traverse@0.3.1(transitive)
- Removedjsonfile@4.0.0(transitive)
- Removedresolve@1.5.0(transitive)
- Removeduniversalify@0.1.2(transitive)
Updatedresolve@1.8.1