@sap/cds-compiler
Advanced tools
Comparing version 2.4.4 to 2.5.0
@@ -23,2 +23,3 @@ #!/usr/bin/env node | ||
// | ||
'use strict'; | ||
@@ -34,3 +35,3 @@ | ||
if (cliArgs.length != 1) | ||
if (cliArgs.length !== 1) | ||
exitError(`Expected exactly one argument, ${cliArgs.length} given`); | ||
@@ -63,3 +64,3 @@ | ||
const errors = options.messages | ||
.filter(msg => (msg.severity === 'Error' && msg.messageId !== 'syntax-deprecated-ident')); | ||
.filter(msg => (msg.severity === 'Error' && msg.messageId !== 'syntax-deprecated-ident')); | ||
if (errors.length > 0) { | ||
@@ -75,3 +76,3 @@ errors.forEach((msg) => { | ||
const tokens = ast.tokenStream.tokens; | ||
const { tokens } = ast.tokenStream; | ||
for (const token of tokens) { | ||
@@ -90,6 +91,6 @@ if (token.type === ast.tokenStream.Identifier && token.text.startsWith('"')) | ||
if (!identToken.stop) | ||
throw new Error(`INTERNAL ERROR: Identifier at ${ identToken.start } has no end!`); | ||
throw new Error(`INTERNAL ERROR: Identifier at ${identToken.start} has no end!`); | ||
const start = identToken.start + currentOffset; | ||
const end = identToken.stop + currentOffset + 1; // end points at the position *before* the character | ||
const end = identToken.stop + currentOffset + 1; // end points at the position *before* the character | ||
@@ -108,3 +109,3 @@ source = replaceSliceInSource(source, start, end, newIdentText); | ||
return `![${ ident }]`; | ||
return `![${ident}]`; | ||
} | ||
@@ -138,4 +139,4 @@ } | ||
function usage() { | ||
console.error('') | ||
console.error(''); | ||
console.error(`usage: ${path.basename(process.argv[1])} <filename>`); | ||
} |
310
bin/cdsc.js
@@ -20,11 +20,12 @@ #!/usr/bin/env node | ||
const main = require('../lib/main'); | ||
const { for_sql, for_hdi, for_hdbcds } = require('../lib/api/main'); | ||
const { compactModel } = require('../lib/json/to-csn'); | ||
const { toOdataWithCsn, toHanaWithCsn, toSqlWithCsn, toCdlWithCsn, toRenameWithCsn, alterConstraintsWithCsn } = require('../lib/backends'); | ||
var util = require('util'); | ||
var fs = require('fs'); | ||
var path = require('path'); | ||
var { reveal } = require('../lib/model/revealInternalProperties'); | ||
const { toRenameWithCsn, alterConstraintsWithCsn } = require('../lib/backends'); | ||
const util = require('util'); | ||
const fs = require('fs'); | ||
const path = require('path'); | ||
const { reveal } = require('../lib/model/revealInternalProperties'); | ||
const enrichCsn = require('../lib/model/enrichCsn'); | ||
const { optionProcessor } = require('../lib/optionProcessor'); | ||
const { explainMessage, hasMessageExplanation, sortMessages } = require('../lib/base/messages') | ||
const { explainMessage, hasMessageExplanation, sortMessages } = require('../lib/base/messages'); | ||
const term = require('../lib/utils/term'); | ||
@@ -39,3 +40,3 @@ const { splitLines } = require('../lib/utils/file'); | ||
class ProcessExitError extends Error { | ||
constructor(exitCode,...args) { | ||
constructor(exitCode, ...args) { | ||
super(...args); | ||
@@ -46,8 +47,40 @@ this.exitCode = exitCode; | ||
function remapCmdOptions(options, cmdOptions) { | ||
if (!cmdOptions) | ||
return options; | ||
for (const [ key, value ] of Object.entries(cmdOptions)) { | ||
switch (key) { | ||
case 'names': | ||
options.sqlMapping = value; | ||
break; | ||
case 'user': | ||
if (!options.magicVars) | ||
options.magicVars = {}; | ||
options.magicVars.user = value; | ||
break; | ||
case 'dialect': | ||
options.sqlDialect = value; | ||
break; | ||
case 'version': | ||
options.odataVersion = value; | ||
break; | ||
case 'locale': | ||
if (!options.magicVars) | ||
options.magicVars = {}; | ||
options.magicVars.locale = value; | ||
break; | ||
default: | ||
options[key] = value; | ||
} | ||
} | ||
return options; | ||
} | ||
// Parse the command line and translate it into options | ||
try { | ||
let cmdLine = optionProcessor.processCmdLine(process.argv); | ||
const cmdLine = optionProcessor.processCmdLine(process.argv); | ||
// Deal with '--version' explicitly | ||
if (cmdLine.options.version) { | ||
process.stdout.write(main.version() + '\n'); | ||
process.stdout.write(`${main.version()}\n`); | ||
throw new ProcessExitError(0); | ||
@@ -58,6 +91,6 @@ } | ||
// Command specific help | ||
if (cmdLine.options.help || cmdLine.options[cmdLine.command] && cmdLine.options[cmdLine.command].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) { | ||
} | ||
else if (cmdLine.options.help) { | ||
// General help | ||
@@ -77,3 +110,4 @@ displayUsage(null, optionProcessor.helpText, 0); | ||
displayUsage(cmdLine.cmdErrors, optionProcessor.commands[cmdLine.command].helpText, 2); | ||
} else if (cmdLine.errors.length > 0) { | ||
} | ||
else if (cmdLine.errors.length > 0) { | ||
// General errors | ||
@@ -85,9 +119,9 @@ displayUsage(cmdLine.errors, optionProcessor.helpText, 2); | ||
// FIXME: Is that not set anywhere in the API? | ||
if (!cmdLine.options.warning) { | ||
if (!cmdLine.options.warning) | ||
cmdLine.options.warning = 2; | ||
} | ||
// Default output goes to stdout | ||
if (!cmdLine.options.out) { | ||
if (!cmdLine.options.out) | ||
cmdLine.options.out = '-'; | ||
} | ||
// --cds-home <dir>: modules starting with '@sap/cds/' are searched in <dir> | ||
@@ -114,35 +148,39 @@ if (cmdLine.options.cdsHome) { | ||
const err = `'parseCdl' expects exactly one file! ${cmdLine.args.files.length} provided.`; | ||
displayUsage(err, optionProcessor.commands['parseCdl'].helpText, 2); | ||
displayUsage(err, optionProcessor.commands.parseCdl.helpText, 2); | ||
} | ||
} | ||
if (cmdLine.options.directBackend) { | ||
if (cmdLine.options.directBackend) | ||
validateDirectBackendOption(cmdLine.command, cmdLine.options, cmdLine.args); | ||
} | ||
if (cmdLine.options.beta) { | ||
const features = cmdLine.options.beta.split(','); | ||
cmdLine.options.beta = {}; | ||
features.forEach((val) => cmdLine.options.beta[val] = true); | ||
features.forEach((val) => { | ||
cmdLine.options.beta[val] = true; | ||
}); | ||
} | ||
// Enable all beta-flags if betaMode is set to true | ||
if(cmdLine.options.betaMode) { | ||
if (cmdLine.options.betaMode) | ||
cmdLine.options.beta = availableBetaFlags; | ||
} | ||
if (cmdLine.options.deprecated) { | ||
const features = cmdLine.options.deprecated.split(','); | ||
cmdLine.options.deprecated = {}; | ||
features.forEach((val) => cmdLine.options.deprecated[val] = true); | ||
features.forEach((val) => { | ||
cmdLine.options.deprecated[val] = true; | ||
}); | ||
} | ||
// Do the work for the selected command | ||
executeCommandLine(cmdLine.command, cmdLine.options, cmdLine.args); | ||
} catch (err) { | ||
} | ||
catch (err) { | ||
// This whole try/catch is only here because process.exit does not work in combination with | ||
// stdout/err - see comment at ProcessExitError | ||
if (err instanceof ProcessExitError) { | ||
if (err instanceof ProcessExitError) | ||
process.exitCode = err.exitCode; | ||
} else { | ||
else | ||
throw err; | ||
} | ||
} | ||
@@ -160,14 +198,14 @@ | ||
function validateDirectBackendOption(command, options, args) { | ||
if (!['toCdl', 'toOdata', 'toHana', 'toCsn', 'toSql'].includes(command)) { | ||
if (![ 'toCdl', 'toOdata', 'toHana', 'toCsn', 'toSql' ].includes(command)) { | ||
displayUsage(`Option '--direct-backend' can't be used with command '${command}'`, | ||
optionProcessor.helpText, 2); | ||
optionProcessor.helpText, 2); | ||
} | ||
if (!args.files || args.files.length !== 1) { | ||
displayUsage(`Option '--direct-backend' expects exactly one JSON file, but ${args.files.length} given`, | ||
optionProcessor.helpText, 2); | ||
optionProcessor.helpText, 2); | ||
} | ||
const filename = args.files[0]; | ||
if (!filename.endsWith('.csn') && !filename.endsWith('.json')) { | ||
displayUsage(`Option '--direct-backend' expects a filename with a *.csn or *.json suffix`, | ||
optionProcessor.helpText, 2); | ||
displayUsage('Option \'--direct-backend\' expects a filename with a *.csn or *.json suffix', | ||
optionProcessor.helpText, 2); | ||
} | ||
@@ -179,11 +217,10 @@ } | ||
// Display non-error output (like help) to stdout | ||
let out = (code === 0 && !error) ? process.stdout : process.stderr; | ||
const 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) { | ||
if (error instanceof Array) { | ||
out.write(error.map(error => `cdsc: ERROR: ${error}`).join('\n') + '\n'); | ||
} else { | ||
if (error instanceof Array) | ||
out.write(`${error.map(error => `cdsc: ERROR: ${error}`).join('\n')}\n`); | ||
else | ||
out.write(`cdsc: ERROR: ${error}\n`); | ||
} | ||
} | ||
@@ -196,3 +233,5 @@ throw new ProcessExitError(code); | ||
const normalizeFilename = options.testMode && process.platform === 'win32'; | ||
const messageLevels = { Error: 0, Warning: 1, Info: 2, None: 3 }; | ||
const messageLevels = { | ||
Error: 0, Warning: 1, Info: 2, None: 3, | ||
}; | ||
// All messages are put into the message array, even those which should not | ||
@@ -202,6 +241,6 @@ // been displayed (severity 'None') | ||
// Create output directory if necessary | ||
if (options.out && options.out !== '-' && !fs.existsSync(options.out)) { | ||
if (options.out && options.out !== '-' && !fs.existsSync(options.out)) | ||
fs.mkdirSync(options.out); | ||
} | ||
// Add implementation functions corresponding to commands here | ||
@@ -221,6 +260,6 @@ const commands = { | ||
if (!commands[command] && !commandsWithoutCompilation[command]) { | ||
if (!commands[command] && !commandsWithoutCompilation[command]) | ||
throw new Error(`Missing implementation for command ${command}`); | ||
} | ||
if (commandsWithoutCompilation[command]) { | ||
@@ -233,10 +272,10 @@ commandsWithoutCompilation[command](); | ||
const fileCache = Object.create(null) | ||
const compiled = options.directBackend ? | ||
util.promisify(fs.readFile)( args.files[0], 'utf-8' ).then((str) => JSON.parse( str )) : | ||
compiler.compileX( args.files, undefined, options, fileCache ); | ||
const fileCache = Object.create(null); | ||
const compiled = options.directBackend | ||
? util.promisify(fs.readFile)( args.files[0], 'utf-8' ).then(str => JSON.parse( str )) | ||
: compiler.compileX( args.files, undefined, options, fileCache ); | ||
compiled.then( commands[command] ) | ||
.then( displayMessages, displayErrors ) | ||
.catch( catchErrors ); | ||
.then( displayMessages, displayErrors ) | ||
.catch( catchErrors ); | ||
@@ -249,6 +288,6 @@ return; // below are only command implementations. | ||
const csn = options.directBackend ? model : compactModel(model, options); | ||
const cdlResult = toCdlWithCsn(csn, options).result; | ||
for (const name in cdlResult) { | ||
writeToFileOrDisplay(options.out, name + '.cds', cdlResult[name]); | ||
} | ||
const cdlResult = main.to.cdl(csn, remapCmdOptions(options)); | ||
for (const name in cdlResult) | ||
writeToFileOrDisplay(options.out, `${name}.cds`, cdlResult[name]); | ||
return model; | ||
@@ -262,3 +301,4 @@ } | ||
displayNamedCsn(model, 'csn', options); | ||
} else { | ||
} | ||
else { | ||
// Result already provided by caller | ||
@@ -274,9 +314,12 @@ displayNamedXsn(model, 'csn', options); | ||
const csn = options.directBackend ? model : compactModel(model, options); | ||
const hanaResult = toHanaWithCsn(csn, options); | ||
for( const pluginName of ['hdbcds', 'hdbconstraint']){ | ||
for(const name in hanaResult[pluginName]){ | ||
writeToFileOrDisplay(options.out, `${name}.${pluginName}`, hanaResult[pluginName][name]); | ||
} | ||
if (options.toHana && options.toHana.csn) { | ||
displayNamedCsn(for_hdbcds(csn, remapCmdOptions(options, options.toHana)), 'hana_csn', options); | ||
} | ||
displayNamedCsn(hanaResult.csn, 'hana_csn', options); | ||
else { | ||
const hanaResult = main.to.hdbcds(csn, remapCmdOptions(options, options.toHana)); | ||
for (const name in hanaResult) | ||
writeToFileOrDisplay(options.out, name, hanaResult[name]); | ||
} | ||
return model; | ||
@@ -288,3 +331,3 @@ } | ||
function toOdata( model ) { | ||
if(options.toOdata && | ||
if (options.toOdata && | ||
options.toOdata.version === 'v4x') { | ||
@@ -295,23 +338,17 @@ options.toOdata.version = 'v4'; | ||
} | ||
let csn = options.directBackend ? model : compactModel(model, options); | ||
const odataResult = toOdataWithCsn(csn, options); | ||
for (let serviceName in odataResult.services) { | ||
// <service>_metadata.xml (metadata) | ||
if (odataResult.services[serviceName].metadata) { | ||
writeToFileOrDisplay(options.out, serviceName + '_metadata.xml', odataResult.services[serviceName].metadata); | ||
} | ||
// <service>_annotations.xml (annotations) | ||
if (odataResult.services[serviceName].annotations) { | ||
writeToFileOrDisplay(options.out, serviceName + '_annotations.xml', odataResult.services[serviceName].annotations); | ||
} | ||
// <service>.xml (combined) | ||
if (odataResult.services[serviceName].combined) { | ||
writeToFileOrDisplay(options.out, serviceName + '.xml', odataResult.services[serviceName].combined); | ||
} | ||
// <service>.json (metadata_json) | ||
if (odataResult.services[serviceName].metadata_json) { | ||
writeToFileOrDisplay(options.out, serviceName + '.json', odataResult.services[serviceName].metadata_json); | ||
} | ||
const csn = options.directBackend ? model : compactModel(model, options); | ||
const odataCsn = main.for.odata(csn, remapCmdOptions(options, options.toOdata)); | ||
if (options.toOdata && options.toOdata.csn) { | ||
displayNamedCsn(odataCsn, 'odata_csn', options); | ||
} | ||
displayNamedCsn(odataResult.csn, 'odata_csn', options); | ||
else if (options.toOdata && options.toOdata.json) { | ||
const result = main.to.edm.all(odataCsn, options); | ||
for (const serviceName in result) | ||
writeToFileOrDisplay(options.out, `${serviceName}.json`, result[serviceName]); | ||
} | ||
else { | ||
const result = main.to.edmx.all(odataCsn, options); | ||
for (const serviceName in result) | ||
writeToFileOrDisplay(options.out, `${serviceName}.xml`, result[serviceName]); | ||
} | ||
return model; | ||
@@ -323,18 +360,18 @@ } | ||
// | ||
/// THIS MUST SURVIVE IF WE REMOVE THE OLD API | ||
/// DO NOT DELETE THIS TORENAME FUNCTIONALITY!! | ||
// / THIS MUST SURVIVE IF WE REMOVE THE OLD API | ||
// / DO NOT DELETE THIS TORENAME FUNCTIONALITY!! | ||
function toRename( model ) { | ||
let csn = options.directBackend ? model : compactModel(model, options); | ||
let renameResult = toRenameWithCsn(csn, options); | ||
let storedProcedure = 'PROCEDURE RENAME_' + renameResult.options.toRename.names.toUpperCase() + '_TO_PLAIN LANGUAGE SQLSCRIPT AS BEGIN\n'; | ||
for (let name in renameResult.rename) { | ||
storedProcedure += ' --\n -- ' + name + '\n --\n'; | ||
const csn = options.directBackend ? model : compactModel(model, options); | ||
const renameResult = toRenameWithCsn(csn, options); | ||
let storedProcedure = `PROCEDURE RENAME_${renameResult.options.toRename.names.toUpperCase()}_TO_PLAIN LANGUAGE SQLSCRIPT AS BEGIN\n`; | ||
for (const name in renameResult.rename) { | ||
storedProcedure += ` --\n -- ${name}\n --\n`; | ||
storedProcedure += renameResult.rename[name]; | ||
} | ||
storedProcedure += "END;\n"; | ||
writeToFileOrDisplay(options.out, 'storedProcedure_' + renameResult.options.toRename.names + '_to_plain.sql', storedProcedure, true); | ||
storedProcedure += 'END;\n'; | ||
writeToFileOrDisplay(options.out, `storedProcedure_${renameResult.options.toRename.names}_to_plain.sql`, storedProcedure, true); | ||
return model; | ||
} | ||
// Execute the command line option 'manageConstraints' and display the results. | ||
// Execute the command line option 'manageConstraints' and display the results. | ||
function manageConstraints( model ) { | ||
@@ -346,7 +383,7 @@ const csn = options.directBackend ? model : compactModel(model, options); | ||
const renderedConstraintStatement = alterConstraintsResult[id]; | ||
if(src === 'hdi') | ||
if (src === 'hdi') | ||
writeToFileOrDisplay(options.out, `${id}.hdbconstraint`, renderedConstraintStatement); | ||
else | ||
writeToFileOrDisplay(options.out, `${id}.sql`, renderedConstraintStatement); | ||
}) | ||
}); | ||
} | ||
@@ -358,10 +395,19 @@ | ||
const csn = options.directBackend ? model : compactModel(model, options); | ||
const sqlResult = toSqlWithCsn(csn, options); | ||
['hdbtabletype', 'hdbtable', 'hdbindex', 'hdbfulltextindex', 'hdbview', 'hdbconstraint', 'sql'].forEach(pluginName => { | ||
for(let name in sqlResult[pluginName]) { | ||
writeToFileOrDisplay(options.out, name + '.' + pluginName, sqlResult[pluginName][name] + '\n', true); | ||
if (options.toSql && options.toSql.src === 'hdi') { | ||
if (options.toSql.csn) { | ||
displayNamedCsn(for_hdi(csn, remapCmdOptions(options, options.toSql)), 'hdi_csn', options); | ||
} | ||
}); | ||
displayNamedCsn(sqlResult.csn, 'sql_csn', options); | ||
else { | ||
const hdiResult = main.to.hdi(csn, remapCmdOptions(options, options.toSql)); | ||
for (const name in hdiResult) | ||
writeToFileOrDisplay(options.out, name, hdiResult[name]); | ||
} | ||
} | ||
else if (options.toSql && options.toSql.csn) { | ||
displayNamedCsn(for_sql(csn, remapCmdOptions(options, options.toSql)), 'sql_csn', options); | ||
} | ||
else { | ||
const sqlResult = main.to.sql(csn, remapCmdOptions(options, options.toSql)); | ||
writeToFileOrDisplay(options.out, 'model.sql', sqlResult.join('\n'), true); | ||
} | ||
return model; | ||
@@ -372,3 +418,3 @@ } | ||
if (args.length !== 1) | ||
displayUsage(`Command 'explain' expects exactly one message-id.`, optionProcessor.commands['explain'].helpText, 2); | ||
displayUsage('Command \'explain\' expects exactly one message-id.', optionProcessor.commands.explain.helpText, 2); | ||
@@ -385,3 +431,3 @@ const id = args.files[0]; | ||
// as possible = is problematic, since console.error() might be asynchronous | ||
function displayErrors (err) { | ||
function displayErrors(err) { | ||
if (err instanceof main.CompilationError) { | ||
@@ -396,3 +442,3 @@ if (options.rawOutput) | ||
console.error( '' ); | ||
for (let sub of err.errors) | ||
for (const sub of err.errors) | ||
console.error( sub.message ); | ||
@@ -402,4 +448,5 @@ console.error( '' ); | ||
} | ||
else | ||
else { | ||
throw err; | ||
} | ||
@@ -425,7 +472,7 @@ err.hasBeenReported = true; | ||
if (options.internalMsg) { | ||
messages.map(msg => util.inspect( msg, { depth: null, maxArrayLength: null} ) ) | ||
messages.map(msg => util.inspect( msg, { depth: null, maxArrayLength: null } ) ) | ||
.forEach(msg => log(msg)); | ||
} | ||
else if (options.noMessageContext) { | ||
messages.filter(msg => (messageLevels[ msg.severity ] <= options.warning)) | ||
messages.filter(msg => (messageLevels[msg.severity] <= options.warning)) | ||
.forEach(msg => log(main.messageString(msg, normalizeFilename, !options.showMessageId))); | ||
@@ -442,15 +489,16 @@ } | ||
let hasAtLeastOneExplanation = false; | ||
messages.filter(msg => messageLevels[ msg.severity ] <= options.warning).forEach(msg => { | ||
hasAtLeastOneExplanation = hasAtLeastOneExplanation || main.hasMessageExplanation(msg.messageId) | ||
messages.filter(msg => messageLevels[msg.severity] <= options.warning).forEach((msg) => { | ||
hasAtLeastOneExplanation = hasAtLeastOneExplanation || main.hasMessageExplanation(msg.messageId); | ||
const name = msg.location && msg.location.file; | ||
const fullFilePath = name ? path.resolve('', name) : undefined; | ||
const context = fullFilePath && sourceLines(fullFilePath); | ||
log(main.messageStringMultiline(msg, { normalizeFilename, noMessageId: !options.showMessageId, withLineSpacer: true, hintExplanation: true })); | ||
log(main.messageStringMultiline(msg, { | ||
normalizeFilename, noMessageId: !options.showMessageId, withLineSpacer: true, hintExplanation: true, | ||
})); | ||
if (context) | ||
log(main.messageContext(context, msg)); | ||
log() // newline | ||
log(); // newline | ||
}); | ||
if (options.showMessageId && hasAtLeastOneExplanation) { | ||
log(`${term.help('help')}: Messages marked with '…' have an explanation text. Use \`cdsc explain <message-id>\` for a more detailed error description.`) | ||
} | ||
if (options.showMessageId && hasAtLeastOneExplanation) | ||
log(`${term.help('help')}: Messages marked with '…' have an explanation text. Use \`cdsc explain <message-id>\` for a more detailed error description.`); | ||
} | ||
@@ -466,9 +514,9 @@ return model; | ||
if (options.rawOutput) { | ||
writeToFileOrDisplay(options.out, name + '_raw.txt', util.inspect(reveal(xsn, options.rawOutput), false, null), true); | ||
writeToFileOrDisplay(options.out, `${name}_raw.txt`, util.inspect(reveal(xsn, options.rawOutput), false, null), true); | ||
} | ||
else if (options.internalMsg) { | ||
writeToFileOrDisplay(options.out, name + '_raw.txt', util.inspect(reveal(xsn).messages, { depth: null, maxArrayLength: null}), true); | ||
writeToFileOrDisplay(options.out, `${name}_raw.txt`, util.inspect(reveal(xsn).messages, { depth: null, maxArrayLength: null }), true); | ||
} | ||
else if (!options.lintMode) { | ||
let csn = compactModel(xsn, options); | ||
const csn = compactModel(xsn, options); | ||
if (command === 'toCsn' && options.toCsn && options.toCsn.withLocalized) | ||
@@ -478,3 +526,3 @@ addLocalizationViews(csn, options); | ||
enrichCsn( csn, options ); | ||
writeToFileOrDisplay(options.out, name + '.json', csn, true); | ||
writeToFileOrDisplay(options.out, `${name}.json`, csn, true); | ||
} | ||
@@ -492,3 +540,3 @@ } | ||
if (options.internalMsg) { | ||
writeToFileOrDisplay(options.out, name + '_raw.txt', options.messages, true); | ||
writeToFileOrDisplay(options.out, `${name}_raw.txt`, options.messages, true); | ||
} | ||
@@ -498,3 +546,3 @@ else if (!options.lintMode && !options.internalMsg) { | ||
addLocalizationViews(csn, options); | ||
writeToFileOrDisplay(options.out, name + '.json', csn, true); | ||
writeToFileOrDisplay(options.out, `${name}.json`, csn, true); | ||
} | ||
@@ -514,17 +562,17 @@ } | ||
// replace all dots with underscore to get deployable .hdbcds sources (except the one before the file extension) | ||
if(options.toHana) | ||
fileName = fileName.replace(/\.(?=.*?\.)/g, '_') | ||
if (options.toHana) | ||
fileName = fileName.replace(/\.(?=.*?\.)/g, '_'); | ||
if (!(content instanceof String || typeof content === 'string')) { | ||
if (!(content instanceof String || typeof content === 'string')) | ||
content = JSON.stringify(content, null, 2); | ||
} | ||
if (dir === '-') { | ||
if (!omitHeadline) { | ||
if (!omitHeadline) | ||
process.stdout.write(`// ------------------- ${fileName} -------------------\n`); | ||
} | ||
process.stdout.write(`${content}\n`); | ||
if (!omitHeadline) { | ||
process.stdout.write(`\n`); | ||
} | ||
} else { | ||
if (!omitHeadline) | ||
process.stdout.write('\n'); | ||
} | ||
else { | ||
// TODO: We might consider using async file-system API ... | ||
@@ -535,4 +583,4 @@ fs.writeFileSync(path.join(dir, fileName), content); | ||
function catchErrors (err) { | ||
if (err instanceof Error && err['hasBeenReported']) | ||
function catchErrors(err) { | ||
if (err instanceof Error && err.hasBeenReported) | ||
return; | ||
@@ -539,0 +587,0 @@ console.error( '' ); |
@@ -19,5 +19,12 @@ #!/usr/bin/env node | ||
const categoryChars = { | ||
artref: 'm', paramname: 'b', | ||
Entity: 'D', Enum: 'H', Index: 'J', AnnoDef: 'V', Extend: 'Z', Annotate: 'Z', Event: 'Y' | ||
} | ||
artref: 'm', | ||
paramname: 'b', | ||
Entity: 'D', | ||
Enum: 'H', | ||
Index: 'J', | ||
AnnoDef: 'V', | ||
Extend: 'Z', | ||
Annotate: 'Z', | ||
Event: 'Y', | ||
}; | ||
@@ -29,17 +36,17 @@ function highlight( err, buf ) { | ||
} | ||
let ts = compiler.parseX( buf, 'hi.cds', { attachTokens: true, messages: [] } ).tokenStream; | ||
const ts = compiler.parseX( buf, 'hi.cds', { attachTokens: true, messages: [] } ).tokenStream; | ||
if (!buf.length || !ts.tokens || !ts.tokens.length) | ||
return; | ||
let chars = [...buf]; | ||
for (let tok of ts.tokens) { | ||
let cat = tok.isIdentifier; | ||
const chars = [ ...buf ]; | ||
for (const tok of ts.tokens) { | ||
const cat = tok.isIdentifier; | ||
if (cat && tok.start >= 0) { | ||
if (cat !== 'ref' || chars[ tok.start ] !== '$') | ||
chars[ tok.start ] = categoryChars[ cat ] || cat.charAt(0); | ||
if (cat !== 'ref' || chars[tok.start] !== '$') | ||
chars[tok.start] = categoryChars[cat] || cat.charAt(0); | ||
if (tok.stop > tok.start) // stop in ANTLR at last char, not behind | ||
chars[ tok.start+1 ] = '_'; | ||
chars[tok.start + 1] = '_'; | ||
} | ||
} | ||
for (let c of chars) | ||
for (const c of chars) | ||
process.stdout.write( c ); | ||
} |
@@ -18,4 +18,4 @@ #!/usr/bin/env node | ||
const commands = { | ||
complete, find, lint | ||
} | ||
complete, find, lint, | ||
}; | ||
@@ -29,8 +29,8 @@ const fs = require('fs'); | ||
let argv = process.argv; | ||
let cmd = commands[ argv[2] ]; | ||
var line = Number.parseInt( argv[3] ); | ||
let column = Number.parseInt( argv[4] ); | ||
let file = argv[5]; | ||
let frel = path.relative( '', file||'' ); | ||
const { argv } = process; | ||
const cmd = commands[argv[2]]; | ||
const line = Number.parseInt( argv[3] ); | ||
const column = Number.parseInt( argv[4] ); | ||
const file = argv[5]; | ||
const frel = path.relative( '', file || '' ); | ||
// TODO: proper realname | ||
@@ -67,13 +67,15 @@ | ||
else { | ||
let charBefore = buf[ off.prefix-1 ]; | ||
if ([':', '<', '.', '>', '!', '|', '='].includes( charBefore )) | ||
const charBefore = buf[off.prefix - 1]; | ||
if ([ ':', '<', '.', '>', '!', '|', '=' ].includes( charBefore )) | ||
// If first of multi-char symbols from 'literalNames' in | ||
// gen/languageParser, calculate "symbol continuation" | ||
tokensAt( buf, off.prefix-1, off.col-1, charBefore ); | ||
tokensAt( buf, off.prefix - 1, off.col - 1, charBefore ); | ||
hasId = tokensAt( buf, off.prefix, off.col, true ); | ||
} | ||
if (hasId) { | ||
let src = buf.substring( 0, off.prefix ) + '__NO_SUCH_ID__' + buf.substring( off.cursor ); | ||
let fname = path.resolve( '', file ); | ||
compiler.compileX( [file], '', { attachValidNames: true, lintMode: true, beta, messages: [] } , { [fname]: src } ) | ||
const src = `${buf.substring( 0, off.prefix )}__NO_SUCH_ID__${buf.substring( off.cursor )}`; | ||
const fname = path.resolve( '', file ); | ||
compiler.compileX( [ file ], '', { | ||
attachValidNames: true, lintMode: true, beta, messages: [], | ||
}, { [fname]: src } ) | ||
.then( ident, ident ); | ||
@@ -86,7 +88,7 @@ } | ||
return usage( xsnOrErr ); | ||
let vn = messageAt( xsnOrErr, 'validNames', off.col ) || Object.create(null); | ||
const vn = messageAt( xsnOrErr, 'validNames', off.col ) || Object.create(null); | ||
// TODO: if there is no such message, use console.log( 'arbitrary identifier' ) | ||
// if we want to avoid that the editor switches to fuzzy completion match | ||
// against the prefix (not yet done anyway) | ||
for (let n in vn) | ||
for (const n in vn) | ||
console.log( n, vn[n].kind ); | ||
@@ -111,15 +113,17 @@ if (!Object.keys( vn ).length) | ||
return true; | ||
const src = buf.substring( 0, off.prefix ) + '__NO_SUCH_ID__' + buf.substring( off.cursor ); | ||
const src = `${buf.substring( 0, off.prefix )}__NO_SUCH_ID__${buf.substring( off.cursor )}`; | ||
const fname = path.resolve( '', file ); | ||
compiler.compileX( [file], '', { attachValidNames: true, lintMode: true, beta, messages: [] } , { [fname]: src } ) | ||
compiler.compileX( [ file ], '', { | ||
attachValidNames: true, lintMode: true, beta, messages: [], | ||
}, { [fname]: src } ) | ||
.then( show, show ); | ||
return true; | ||
function show( xsnOrErr ) { | ||
function show( xsnOrErr ) { | ||
if (!(xsnOrErr.messages || xsnOrErr.options && xsnOrErr.options.messages)) | ||
return usage( xsnOrErr ); | ||
let vn = messageAt( xsnOrErr, 'validNames', off.col ) || Object.create(null); | ||
const vn = messageAt( xsnOrErr, 'validNames', off.col ) || Object.create(null); | ||
const art = vn[buf.substring( off.prefix, off.cursor )]; | ||
if (art) | ||
console.log( locationString( art.name.location || art.location ) + ': Definition' ); | ||
console.log( `${locationString( art.name.location || art.location )}: Definition` ); | ||
return true; | ||
@@ -132,4 +136,4 @@ } | ||
return usage( err ); | ||
let fname = path.resolve( '', file ); | ||
compiler.compileX( [file], '', { lintMode: true, beta, messages: [] }, { [fname]: buf } ) | ||
const fname = path.resolve( '', file ); | ||
compiler.compileX( [ file ], '', { lintMode: true, beta, messages: [] }, { [fname]: buf } ) | ||
.then( display, display ); | ||
@@ -139,6 +143,6 @@ return true; | ||
function display( xsnOrErr ) { | ||
let messages = xsnOrErr.messages || xsnOrErr.options && xsnOrErr.options.messages; | ||
const messages = xsnOrErr.messages || xsnOrErr.options && xsnOrErr.options.messages; | ||
if (!messages) | ||
return usage( xsnOrErr ); | ||
for (let msg of messages) | ||
for (const msg of messages) | ||
console.log( main.messageString( msg ) ); | ||
@@ -150,5 +154,5 @@ return true; | ||
function tokensAt( buf, offset, col, symbol ) { | ||
let src = buf.substring( 0, offset ) + '≠' + buf.substring( offset ); | ||
let et = messageAt( compiler.parseX( src, frel, { messages: [] } ), 'expectedTokens', col ) || []; | ||
for (let n of et) { | ||
const src = `${buf.substring( 0, offset )}≠${buf.substring( offset )}`; | ||
const et = messageAt( compiler.parseX( src, frel, { messages: [] } ), 'expectedTokens', col ) || []; | ||
for (const n of et) { | ||
if (typeof symbol === 'string') { | ||
@@ -165,4 +169,5 @@ if (n.length > 3 && n.charAt(0) === "'" && n.charAt(1) === symbol) | ||
} | ||
else if (n !== 'Identifier') | ||
else if (n !== 'Identifier') { | ||
console.log( n, 'unknown' ); | ||
} | ||
} | ||
@@ -173,4 +178,5 @@ return et.includes( 'Identifier' ); | ||
function messageAt( model, prop, col ) { | ||
let msg = (model.messages || model.options && model.options.messages).find( | ||
m => m[prop] && m.location.line === line && m.location.col === col && m.location.file === frel ); | ||
const msg = (model.messages || model.options && model.options.messages).find( | ||
m => m[prop] && m.location.line === line && m.location.col === col && m.location.file === frel | ||
); | ||
return msg && msg[prop]; | ||
@@ -186,3 +192,3 @@ } | ||
let pos = 0; | ||
for (let l = line-1; l; --l) { | ||
for (let l = line - 1; l; --l) { | ||
pos = buf.indexOf( '\n', pos ) + 1; | ||
@@ -189,0 +195,0 @@ if (!pos) |
#!/usr/bin/env node | ||
'use strict'; | ||
// Very simple command-line interface to support model migration from compiler | ||
@@ -7,10 +9,9 @@ // v1 to v2. (If our client command processor would have been not as difficult | ||
const commands = { | ||
ria | ||
} | ||
ria, | ||
}; | ||
const compiler = require('../lib/compiler'); | ||
const argv = process.argv; | ||
const cmd = commands[ argv[2] ]; | ||
const { argv } = process; | ||
const cmd = commands[argv[2]]; | ||
const files = argv.slice(3); | ||
@@ -40,4 +41,4 @@ const options = { messages: [] }; | ||
const matches = msgObj.message.match( /["“][^"”]+["”]/g ); | ||
matches.slice(2).forEach( name => { | ||
annotates[ name.slice( 1, -1 ) ] = true; | ||
matches.slice(2).forEach( (name) => { | ||
annotates[name.slice( 1, -1 )] = true; | ||
} ); | ||
@@ -44,0 +45,0 @@ } |
@@ -10,2 +10,56 @@ # ChangeLog for cds compiler and backends | ||
## Version 2.5.0 - 2021-07-28 | ||
### Added | ||
- Allow to extend existing array annotation values via the ellipsis operator `...`. | ||
An ellipsis may appear exactly once at an arbitrary position in the top level array | ||
of an `annotate` directive. Only array values can be merged into arrays and unapplied | ||
ellipses are removed from the final array value. Annotation layering rules remain unaffected. | ||
- to.sql/hdi/hdbcds: | ||
+ Doc comments are translated into HANA comments (or into `@Comment` annotation for `to.hdbcds`). | ||
Such comments are possible on entities, views, elements of entities and `to.hdbcds` also supports comments on view columns. | ||
Generation can be disabled via option `disableHanaComments`. Entites/views (and their elements/columns) | ||
annotated with `@cds.persistence.journal` for `to.hdi`/`to.sql` will not have comments rendered. | ||
+ Generation of temporal `WHERE` clause can be suppressed by annotating the `validFrom`/`validTo` elements of the projection with `false` or `null`. | ||
- to.sql/hdi/hdbcds/edm(x)/for.odata: Structure/managed association comparisons (tuple comparisons) are now | ||
also expanded in `WHERE` and `HAVING` - this was previously only supported in on-conditions. | ||
- `cdsc` now internally uses SNAPI. | ||
- to.hdi.migration: | ||
+ Validate that the two supplied CSNs are compatible. | ||
+ Improve delta-mechanism to not render superflous [ALTER|DROP|ADD] statements for unchanged SQL. | ||
### Changed | ||
- If the first source provided to the compile command has a `$sources` property | ||
(whether enumerable or not) which is an array of strings, | ||
use that instead of calculating one. | ||
- Updated OData vocabularies 'Aggregation', 'Analytics', 'Authorization', 'Capabilities', | ||
'CodeList', 'Common', 'Communication', 'Core', 'Graph', 'HTML5', 'Measures', 'ODM', 'PersonalData', | ||
'Repeatability', 'Session', 'UI', 'Validation' | ||
### Removed | ||
- Removed internal property `$viaTransform` from CSN produced by OData/HANA transformation | ||
### Fixed | ||
- Remove warnings 'Ignoring annotation “@odata.draft.enabled” as the artifact is not part of a service' | ||
and 'Ignoring draft node for composition target ... because it is not part of a service' | ||
- Doc comments are no longer ignored after enum values and on view columns in parseCdl mode. | ||
- to.cdl: | ||
- Doc comments for enum values are correctly rendered. | ||
- Enum value and doc comments are now correctly rendered if the enum is called `doc`. | ||
- Doc comments at type references are correctly rendered. | ||
- Empty doc comments are correctly rendered and not left out. | ||
- Doc comments on view columns are correctly rendered. | ||
- to.edm(x): | ||
- OData V2: Ignore `@odata.singleton`. | ||
- OData V4: Do not render an `edm:NavigationPropertyBinding` to a singleton if the association has | ||
cardinality 'to-many'. | ||
- forOData: | ||
- Fix automatic renaming of shortcut annotation (eg. `@label`) with value `null`. | ||
- CSN parser: | ||
- Empty doc comments are correctly parsed and not complained about. | ||
## Version 2.4.4 - 2021-07-02 | ||
@@ -16,3 +70,2 @@ | ||
- Do not remove parentheses around single literals and references on the right-hand side of an `in` and `not in` operator. | ||
of an `in` and `not in` operator. | ||
@@ -118,2 +171,7 @@ ## Version 2.4.2 - 2021-07-01 | ||
### 2.5.0 Addendum to Changed | ||
- Replace outdated option `length` with `defaultStringLength` which is usable in `for.*` and `to.*` APIs. | ||
## Version 2.2.4 - 2021-05-06 | ||
@@ -425,2 +483,8 @@ | ||
## Version 1.50.8 - 2021-07-01 | ||
### Fixed | ||
- to.hdi.migration: Don't generate `ALTER` for type change from association to composition or vice versa (if the rest stays the same), as the resulting SQL is identical. | ||
## Version 1.50.6 - 2021-05-05 | ||
@@ -427,0 +491,0 @@ |
@@ -10,2 +10,13 @@ # ChangeLog of Beta Features for cdx compiler and backends | ||
## Version 2.4.4 | ||
### Added `nestedProjections` | ||
- Support `expand`: columns can look like `assoc_or_struct_or_tabalias { col_expression1, … }`, | ||
`longer.ref as name { *, … } excluding { … }`, `{ col_expression1 as sub1, … } as name`, etc. | ||
- Support `inline`: columns can look like `assoc_or_struct_or_tabalias.{ col_expression1, … }`, | ||
`longer.ref[filter = condition].{ *, … } excluding { … }`, `assoc_or_struct_or_tabalias.*`, etc. | ||
- _Some checks are missing and will be added! Minor changes might occur._ | ||
- __The SQL backends might not work properly yet if nested projections are used!__ | ||
## Version 2.4.2 | ||
@@ -12,0 +23,0 @@ |
@@ -156,2 +156,42 @@ /** @module API */ | ||
/** | ||
* Transform a CSN like to.sql | ||
* | ||
* @param {CSN.Model} csn Plain input CSN | ||
* @param {sqlOptions} [options={}] Options | ||
* @returns {CSN.Model} CSN transformed like to.sql | ||
* @private | ||
*/ | ||
function forSql(csn, options = {}) { | ||
const internalOptions = prepareOptions.to.sql(options); | ||
internalOptions.toSql.csn = true; | ||
return backends.toSqlWithCsn(csn, internalOptions).csn; | ||
} | ||
/** | ||
* Transform a CSN like to.hdi | ||
* | ||
* @param {CSN.Model} csn Plain input CSN | ||
* @param {hdiOptions} [options={}] Options | ||
* @returns {CSN.Model} CSN transformed like to.hdi | ||
* @private | ||
*/ | ||
function forHdi(csn, options = {}) { | ||
const internalOptions = prepareOptions.to.hdi(options); | ||
internalOptions.toSql.csn = true; | ||
return backends.toSqlWithCsn(csn, internalOptions).csn; | ||
} | ||
/** | ||
* Transform a CSN like to.hdbcds | ||
* | ||
* @param {CSN.Model} csn Plain input CSN | ||
* @param {hdbcdsOptions} [options={}] Options | ||
* @returns {CSN.Model} CSN transformed like to.hdbcds | ||
* @private | ||
*/ | ||
function forHdbcds(csn, options = {}) { | ||
const internalOptions = prepareOptions.to.hdbcds(options); | ||
internalOptions.toHana.csn = true; | ||
return backends.toHanaWithCsn(csn, internalOptions).csn; | ||
} | ||
/** | ||
* Process the given CSN into SQL. | ||
@@ -342,3 +382,3 @@ * | ||
// Compare both images. | ||
const diff = compareModels(beforeImage || afterImage, afterImage); | ||
const diff = compareModels(beforeImage || afterImage, afterImage, internalOptions); | ||
@@ -603,4 +643,9 @@ // Convert the diff to SQL. | ||
edmx: publishCsnProcessor(edmx, 'to.edmx'), | ||
/** Internal only */ | ||
for_sql: publishCsnProcessor(forSql, 'for.sql'), | ||
for_hdi: publishCsnProcessor(forHdi, 'for.hdi'), | ||
for_hdbcds: publishCsnProcessor(forHdbcds, 'for.hdbcds'), | ||
renderSQL, | ||
undeploy, | ||
/** */ | ||
}; | ||
@@ -714,2 +759,8 @@ | ||
/** | ||
* Available SQL change modes | ||
* | ||
* @typedef {'alter' | 'drop' } SqlChangeMode | ||
*/ | ||
/** | ||
* Available oData versions | ||
@@ -756,2 +807,4 @@ * | ||
* @property {NamingMode} [sqlMapping='plain'] Naming mode to use | ||
* @property {SqlChangeMode} [sqlChangeMode='alter'] SQL change mode to use (for changed columns) | ||
* @property {boolean} [allowCsnDowngrade=false] Allow downgrades of CSN major version (for modelCompare) | ||
* @property {object} [beta] Enable experimental features - not for productive use! | ||
@@ -758,0 +811,0 @@ * @property {boolean} [longAutoexposed=false] Deprecated: Produce long names (with underscores) for autoexposed entities |
@@ -22,2 +22,3 @@ 'use strict'; | ||
'sqlChangeMode', | ||
'allowCsnDowngrade', | ||
'joinfk', | ||
@@ -53,2 +54,3 @@ 'magicVars', | ||
'internalMsg', | ||
'disableHanaComments', // in case of issues with hana comment rendering | ||
'dependentAutoexposed', // deprecated, no effect - TODO: safe to remove? | ||
@@ -55,0 +57,0 @@ 'longAutoexposed', // deprecated, no effect - TODO: safe to remove? |
@@ -17,5 +17,5 @@ 'use strict'; | ||
function combinedLocation( start, end ) { | ||
if (!start) | ||
if (!start || !start.location) | ||
return end.location; | ||
else if (!end) | ||
else if (!end || !end.location) | ||
return start.location; | ||
@@ -22,0 +22,0 @@ const loc = { |
@@ -103,2 +103,3 @@ // Central registry for messages. | ||
'composition-as-key': { severity: 'Error', configurableFor: 'deprecated' }, // is confusing and not supported | ||
'odata-spec-violation-array-of-v2': { severity: 'Warning' }, // more than 30 chars | ||
'odata-spec-violation-constraints': { severity: 'Warning' }, // more than 30 chars | ||
@@ -111,2 +112,5 @@ 'odata-spec-violation-property-name': { severity: 'Warning' }, // more than 30 chars | ||
const centralMessageTexts = { | ||
'anno-mismatched-ellipsis': 'An array with $(CODE) can only be used if there is an assignment below with an array value', | ||
'anno-unexpected-ellipsis': 'Unexpected $(CODE) in annotation assignment', | ||
'missing-type-parameter': 'Missing value for type parameter $(NAME) in reference to type $(ID)', | ||
'syntax-csn-expected-object': 'Expected object for property $(PROP)', | ||
@@ -205,2 +209,6 @@ 'syntax-csn-expected-column': 'Expected object or string \'*\' for property $(PROP)', | ||
'i18n-different-value': 'Different translation for key $(PROP) of language $(OTHERPROP) in unrelated layers', | ||
'odata-spec-violation-array-of-v2': 'EDM $(LITERAL) must not have an attribute $(PROP) with the value $(NAME)', | ||
'odata-spec-violation-param-v2' : 'Type of EDM FunctionImport Parameter must be an EDM SimpleType or ComplexType', | ||
'odata-spec-violation-returns-v2': 'EDM FunctionImport must return EDM SimpleType, ComplexType, EntityType or a collection of one of these', | ||
'odata-spec-violation-assoc-v2': 'EDM ComplexType $(NAME) must not contain an EDM NavigationProperty', | ||
'odata-spec-violation-attribute': 'EDM Property $(NAME) has no attribute $(PROP)', | ||
@@ -207,0 +215,0 @@ 'odata-spec-violation-property-name': 'EDM Property $(NAME) must not have the same name as the declaring $(TYPE)', |
@@ -615,2 +615,4 @@ // Functions and classes for syntax messages | ||
names: transformManyWith( quoted ), | ||
number: n => n, | ||
literal: l => l, | ||
art: transformArg, | ||
@@ -625,4 +627,9 @@ service: transformArg, | ||
// msg: m => m, | ||
$reviewed: ignoreTextTransform, | ||
}; | ||
function ignoreTextTransform() { | ||
return null; | ||
} | ||
function transformManyWith( t, sorted ) { | ||
@@ -771,3 +778,3 @@ return function transformMany( many, r, args, texts ) { | ||
* Example: | ||
* <source>.cds:3:11: Error: cannot find value `nu` in this scope * | ||
* <source>.cds:3:11: Error message-id: Can't find type `nu` in this scope (in entity:“E”/element:“e”) | ||
* | ||
@@ -809,8 +816,11 @@ * @param {CSN.Message} err | ||
/** | ||
* Return message string with location if present. | ||
* Returns a message string with file- and semantic location if present | ||
* in multiline form. | ||
* | ||
* Example: | ||
* Error: cannot find value `nu` in this scope | ||
* <source>.cds:3:11, at entity:“E” | ||
* | ||
* ```txt | ||
* Error[message-id]: Can't find type `nu` in this scope (in entity:“E”/element:“e”) | ||
* | | ||
* <source>.cds:3:11, at entity:“E” | ||
* ``` | ||
* @param {CSN.Message} err | ||
@@ -1206,2 +1216,7 @@ * @param {object} [config = {}] | ||
} | ||
else if (inElement) { | ||
result += select(); | ||
elements.push(step); | ||
inQuery = false; | ||
} | ||
} | ||
@@ -1318,3 +1333,3 @@ else if ( inMixin ) { | ||
let isFound = false; | ||
traverseQuery(rootQuery, null, (q, querySelect) => { | ||
traverseQuery(rootQuery, null, null, (q, querySelect) => { | ||
if ( querySelect ) | ||
@@ -1321,0 +1336,0 @@ totalQueryDepth += 1; |
@@ -184,60 +184,3 @@ 'use strict'; | ||
// FIXME: this function is only used by old tests -- REMOVE! | ||
// | ||
// Clone 'node', transforming nodes therein recursively. Object 'transformers' is expected | ||
// to contain a mapping of property 'key' names to transformer functions. The node's properties | ||
// are walked recursively, calling each transformer function on its corresponding property | ||
// 'key' of 'node', replacing 'value' in 'resultNode' with the function's return value | ||
// (returning 'undefined' will delete the property). | ||
// If no transformation function is found for 'key', the first letter of 'key' is tried | ||
// instead (this seems to be intended for handling annotations that start with '@' ?) | ||
// FIXME: Do we really need this ? | ||
// If `withArtifactLink` is set, the `_artifact` links of `node` are copied too (not cloned, | ||
// of course). | ||
// Regardless of their names, transformers are never applied to dictionary elements. | ||
// | ||
// The transformer functions are called with the following signature: | ||
// transformer(value, node, resultNode, key) | ||
function cloneWithTransformations(node, transformers, withArtifactLink) { | ||
return transformNode(node); | ||
// This general transformation function will be applied to each node recursively | ||
function transformNode(node) { | ||
// Return primitive values and null unchanged, but let objects and dictionaries through | ||
// (Note that 'node instanceof Object' would be false for dictionaries). | ||
if (node === null || typeof node !== 'object') { | ||
return node | ||
} | ||
// Simply return if node is to be ignored | ||
if (node._ignore) | ||
return undefined; | ||
// Transform arrays element-wise | ||
if (Array.isArray(node)) { | ||
return node.map(transformNode); | ||
} | ||
// Things not having 'proto' are dictionaries | ||
const proto = Object.getPrototypeOf(node); | ||
// Iterate own properties of 'node' and transform them into 'resultNode' | ||
const resultNode = Object.create(proto); | ||
for (let key of Object.keys(node)) { | ||
// Dictionary always use transformNode(), other objects their transformer according to key | ||
const transformer = !proto ? transformNode : transformers[key] || transformers[key.charAt(0)]; | ||
// Apply transformer, or use transformNode() if there is none | ||
const resultValue = (transformer || transformNode)(node[key], node, resultNode, key); | ||
if (resultValue !== undefined) { | ||
resultNode[key] = resultValue; | ||
} | ||
} | ||
// For non-dictionaries, take over `_artifact` if requested | ||
if (withArtifactLink && proto) { | ||
let pd = Object.getOwnPropertyDescriptor(node, '_artifact') | ||
if (pd) | ||
Object.defineProperty(resultNode, '_artifact', pd); | ||
} | ||
return resultNode; | ||
} | ||
} | ||
module.exports = { | ||
@@ -256,3 +199,2 @@ isBetaEnabled, | ||
setProp, | ||
cloneWithTransformations, | ||
}; |
@@ -51,3 +51,2 @@ 'use strict'; | ||
* @param {string} artName Name of the artifact | ||
* | ||
*/ | ||
@@ -54,0 +53,0 @@ function checkChainedArray(art, artName) { |
@@ -34,3 +34,3 @@ // This is very similar to lib/model/enrichCsn - but the goal and the execution differ a bit: | ||
// Annotations are ignored. | ||
'@': () => {}, | ||
'@': () => { /* ignore annotations */ }, | ||
}; | ||
@@ -107,3 +107,5 @@ let cleanupCallbacks = []; | ||
function pathRef( node, prop, path ) { | ||
const { links, art, scope } = inspectRef( csnPath ); | ||
const { | ||
links, art, scope, $env, | ||
} = inspectRef( csnPath ); | ||
if (links) { | ||
@@ -117,2 +119,6 @@ setProp(node, '_links', links); | ||
} | ||
if ($env) { | ||
setProp(node, '$env', $env ); | ||
cleanupCallbacks.push(() => delete node.$env); | ||
} | ||
setProp(node, '$scope', scope); | ||
@@ -119,0 +125,0 @@ cleanupCallbacks.push(() => delete node.$scope); |
'use strict'; | ||
const { hasBoolAnnotation, isPersistedOnDatabase, isBuiltinType } = require('../model/csnUtils'); | ||
const { hasAnnotationValue, isPersistedOnDatabase, isBuiltinType } = require('../model/csnUtils'); | ||
/** | ||
@@ -72,3 +72,3 @@ * Make sure that all source artifacts and association targets reach the database | ||
if (!isPersistedOnDatabase(endArtifact)) { | ||
const cdsPersistenceSkipped = hasBoolAnnotation(endArtifact, '@cds.persistence.skip'); | ||
const cdsPersistenceSkipped = hasAnnotationValue(endArtifact, '@cds.persistence.skip'); | ||
this.error( null, obj.$path, { | ||
@@ -105,3 +105,3 @@ id: pathStep, elemref: obj, name, '#': cdsPersistenceSkipped ? 'std' : 'abstract', | ||
if (isPersistedOnDatabase(this.artifact) && !hasBoolAnnotation(this.artifact, '@cds.persistence.table')) { | ||
if (isPersistedOnDatabase(this.artifact) && !hasAnnotationValue(this.artifact, '@cds.persistence.table')) { | ||
const generalQueryProperties = [ 'from', 'columns', 'where', 'groupBy', 'orderBy', 'having', 'limit' ]; | ||
@@ -108,0 +108,0 @@ for (const prop of generalQueryProperties) { |
@@ -5,3 +5,3 @@ 'use strict'; | ||
forEachDefinition, forEachMemberRecursively, forAllQueries, | ||
forEachMember, getNormalizedQuery, hasBoolAnnotation, | ||
forEachMember, getNormalizedQuery, hasAnnotationValue, | ||
} = require('../model/csnUtils'); | ||
@@ -113,4 +113,3 @@ const enrich = require('./enricher'); | ||
that.artifact = artifact; | ||
if (memberValidators.length && | ||
(!iterateOptions.filterArtifact || iterateOptions.filterArtifact(artifact))) { | ||
if (memberValidators.length) { | ||
forEachMemberRecursively( artifact, | ||
@@ -133,3 +132,2 @@ memberValidators.map(v => v.bind(that)), | ||
* @param {object} that Will be provided to the validators via "this" | ||
* | ||
* @returns {Function} the validator function with the respective checks for the HANA backend | ||
@@ -152,3 +150,3 @@ */ | ||
{ | ||
filterArtifact: artifact => !artifact.abstract && !hasBoolAnnotation(artifact, '@cds.persistence.skip'), | ||
skipArtifact: artifact => artifact.abstract || hasAnnotationValue(artifact, '@cds.persistence.skip'), | ||
skip: [ | ||
@@ -165,3 +163,2 @@ 'action', | ||
* @param {object} that Will be provided to the validators via "this" | ||
* | ||
* @returns {Function} the validator function with the respective checks for the OData backend | ||
@@ -184,5 +181,8 @@ */ | ||
), | ||
forOdataQueryValidators.concat(commonQueryValidators)); | ||
forOdataQueryValidators.concat(commonQueryValidators), | ||
{ | ||
skipArtifact: this.isExternalServiceMember, | ||
}); | ||
} | ||
module.exports = { forHana, forOdata }; |
@@ -114,2 +114,3 @@ // Consistency checker on model (XSN = augmented CSN) | ||
'$withLocalized', | ||
'$sources', | ||
], | ||
@@ -125,4 +126,4 @@ }, | ||
col: { test: isNumber }, | ||
endLine: { test: isNumber }, | ||
endCol: { test: isNumber }, | ||
endLine: { test: isNumber, also: [ undefined ] }, | ||
endCol: { test: isNumber, also: [ undefined ] }, | ||
$notFound: { test: isBoolean }, | ||
@@ -291,4 +292,4 @@ }, | ||
kind: [ 'extend' ], | ||
inherits: 'definitions', | ||
test: isArray( column ), | ||
optional: thoseWithKind, | ||
enum: [ '*' ], | ||
@@ -301,2 +302,3 @@ requires: [ 'location' ], | ||
excludingDict: { | ||
kind: 'element', | ||
test: isDictionary( definition ), // definition since redef | ||
@@ -546,3 +548,3 @@ requires: [ 'location', 'name' ], | ||
_origin: { kind: [ 'entity' ], test: TODO }, | ||
_pathHead: { kind: [ 'element' ], test: TODO }, | ||
_pathHead: { kind: [ 'element', undefined ], test: TODO }, // column or * (wildcard) | ||
_from: { kind: true, test: TODO }, // all table refs necessary to compute elements | ||
@@ -583,2 +585,3 @@ // array of $tableAlias (or includes) for explicit and implicit redirection: | ||
$withLocalized: { test: isBoolean }, | ||
$sources: { parser: true, test: isArray( isString ) }, | ||
$expected: { parser: true, test: isString }, | ||
@@ -815,3 +818,3 @@ }; | ||
const requires = [ 'val', 'location' ]; | ||
const optional = [ 'literal', '$inferred' ]; | ||
const optional = [ 'literal', '$inferred', '_pathHead' ]; | ||
standard( node, parent, prop, { schema: valSchema, requires, optional }, name ); | ||
@@ -818,0 +821,0 @@ }; |
@@ -56,21 +56,19 @@ // Main XSN-based compiler functions | ||
const ext = path.extname( filename ).toLowerCase(); | ||
if ([ '.json', '.csn' ].includes(ext) || (options.fallbackParser === 'csn')) { | ||
if ([ '.json', '.csn' ].includes(ext) || options.fallbackParser === 'csn!') | ||
return parseCsn.parse( source, filename, options ); | ||
} | ||
else if ([ '.cds', '.hdbcds', '.hdbdd', '.cdl' ].includes(ext) || options.fallbackParser) { | ||
// Note: Historically, all truthy values for options.fallbackParser were interpreted as 'cdl'. | ||
// To not break existing programs, we do the same if it's not set to `csn`. | ||
if ([ '.cds', '.hdbcds', '.hdbdd', '.cdl' ].includes(ext)) | ||
return parseLanguage( source, filename, options ); | ||
} | ||
// eslint-disable-next-line no-else-return | ||
else { | ||
const model = { location: { file: filename } }; | ||
const { error } = makeMessageFunction( model, options, 'compile' ); | ||
error( 'file-unknown-ext', emptyWeakLocation(filename), | ||
{ file: ext && ext.slice(1), '#': !ext && 'none' }, { | ||
std: 'Unknown file extension $(FILE)', | ||
none: 'No file extension', | ||
} ); | ||
return model; | ||
} | ||
if (options.fallbackParser === 'csn') | ||
return parseCsn.parse( source, filename, options ); | ||
if (options.fallbackParser) // any other value: like 'cdl' (historic reasons) | ||
return parseLanguage( source, filename, options ); | ||
const model = { location: { file: filename } }; | ||
const { error } = makeMessageFunction( model, options, 'compile' ); | ||
error( 'file-unknown-ext', emptyWeakLocation(filename), | ||
{ file: ext && ext.slice(1), '#': !ext && 'none' }, { | ||
std: 'Unknown file extension $(FILE)', | ||
none: 'No file extension', | ||
} ); | ||
return model; | ||
} | ||
@@ -87,3 +85,4 @@ | ||
// This function returns a Promise. See ../bin/cdsv.js for an example usage. | ||
// This function returns a Promise and can be used with `await`. For an example | ||
// see `examples/api-usage/`. | ||
// See function `compileSyncX` or `compileSourcesX` for alternative compile | ||
@@ -336,2 +335,4 @@ // functions. | ||
* | ||
* TODO: re-check `using from` dependencies. | ||
* | ||
* @param {string|object} sourcesDict Files to compile. | ||
@@ -342,12 +343,9 @@ * @param {object} [options={}] Compilation options. | ||
function compileSourcesX( sourcesDict, options = {} ) { | ||
// eslint-disable-next-line no-nested-ternary | ||
const content = sourcesDict.sources | ||
? sourcesDict.sources | ||
: (typeof sourcesDict === 'string') ? { '<stdin>.cds': sourcesDict } : sourcesDict; | ||
if (typeof sourcesDict === 'string') | ||
sourcesDict = { '<stdin>.cds': sourcesDict }; | ||
const sources = Object.create(null); | ||
const model = { sources, options }; | ||
for (const filename in content) { | ||
const source = content[filename]; | ||
for (const filename in sourcesDict) { | ||
const source = sourcesDict[filename]; | ||
if (typeof source === 'string') { | ||
@@ -363,17 +361,2 @@ const ast = parseX( source, filename, options ); | ||
} | ||
// add dependencies to AST | ||
for (const filename in sourcesDict.dependencies) { | ||
const dependency = sourcesDict.dependencies[filename]; | ||
for (const val in dependency) { | ||
const dep = { | ||
literal: 'string', val, realname: dependency[val], // location ? | ||
}; | ||
const arr = sources[filename].dependencies; | ||
if (arr) | ||
arr.push( dep ); | ||
else | ||
sources[filename].dependencies = [ dep ]; | ||
} | ||
} | ||
moduleLayers.setLayers( sources ); | ||
@@ -380,0 +363,0 @@ |
@@ -287,2 +287,12 @@ // Compiler functions and utilities shared across all phases | ||
function userQuery( user ) { | ||
// TODO: we need _query links set by the definer | ||
while (user._main) { | ||
if (user.kind === 'select' || user.kind === '$join') | ||
return user; | ||
user = user._parent; | ||
} | ||
return null; | ||
} | ||
// Return artifact or element referred by the path in `ref`. The first | ||
@@ -329,8 +339,4 @@ // environment we search in is `env`. If no such artifact or element exist, | ||
// TODO: SIMPLIFY this function | ||
// eslint-disable-next-line no-nested-ternary | ||
const query = (spec.lexical === 'main') | ||
? user._main // in path filter, just $magic (and $parameters) | ||
: (user.kind === 'select' || user.kind === '$join') | ||
? user | ||
: user._parent && user._parent.kind === 'select' && user._parent; | ||
const query = (spec.lexical === 'main') ? user._main : userQuery( user ); | ||
// in path filter, just $magic (and $parameters) | ||
env = (spec.lexical === 'from') ? query._parent : query || user._main || user; | ||
@@ -355,3 +361,3 @@ // queries: first tabaliases, then $magic - value refs: first $self, then $magic | ||
} | ||
else if (user._pathHead) { | ||
else if (!spec.envFn && user._pathHead) { | ||
// eslint-disable-next-line no-empty | ||
@@ -403,3 +409,3 @@ } | ||
// console.log(expected, ref.path.map(a=>a.id),artItemsCount) | ||
art = getPathItem( path, spec, user, artItemsCount, user._pathHead && art); | ||
art = getPathItem( path, spec, user, artItemsCount, !spec.envFn && user._pathHead && art); | ||
if (!art) | ||
@@ -512,3 +518,4 @@ return setLink( ref, art ); | ||
function getPathRoot( path, spec, user, env, extDict, msgArt ) { | ||
if (user._pathHead) { // TODO: not necessarily for explicit ON condition in expand | ||
if (!spec.envFn && user._pathHead) { | ||
// TODO: not necessarily for explicit ON condition in expand | ||
environment( user._pathHead ); // make sure _origin is set | ||
@@ -813,3 +820,3 @@ return user._pathHead._origin; | ||
// annotation definition is missing | ||
function defineAnnotations( construct, art, block, priority ) { | ||
function defineAnnotations( construct, art, block, priority = 'define' ) { | ||
if (!options.parseCdl && construct.kind === 'annotate') { | ||
@@ -835,7 +842,6 @@ // Namespaces cannot be annotated in CSN but because they exist as XSN artifacts | ||
art.doc = construct.$annotations.doc; | ||
if (!construct.$annotations || !construct.$annotations.length) { | ||
if (construct === art) | ||
return; | ||
// $annotations is set if parsed from CDL but may not be set if | ||
// the input is CSN so we extract them. | ||
if (!construct.$annotations) { | ||
if (!block || block.$frontend !== 'json') | ||
return; // namespace, or in CDL source without @annos: | ||
// CSN input: set _block and $priority, shallow-copy from extension | ||
for (const annoProp in construct) { | ||
@@ -848,3 +854,5 @@ if (annoProp.charAt(0) === '@') { | ||
setProp( a, '_block', block ); | ||
setAnnotation(art, annoProp, a, priority); | ||
a.$priority = priority; | ||
if (construct !== art) | ||
dictAddArray( art, annoProp, a ); | ||
} | ||
@@ -899,10 +907,6 @@ } | ||
// set _artifact? | ||
setAnnotation( art, annoProp, anno, priority ); | ||
anno.$priority = priority; | ||
dictAddArray( art, annoProp, anno ); | ||
} | ||
} | ||
function setAnnotation( art, annoProp, anno, priority = 'define') { | ||
anno.$priority = priority; | ||
dictAddArray( art, annoProp, anno ); | ||
} | ||
} | ||
@@ -909,0 +913,0 @@ |
@@ -137,3 +137,3 @@ 'use strict'; | ||
if(!options) | ||
throw 'Please debug me: csn2annotationsEdm must be invoked with options'; | ||
throw new Error('Please debug me: csn2annotationsEdm must be invoked with options'); | ||
@@ -140,0 +140,0 @@ const messageFunctions = makeMessageFunction(csn, options); |
@@ -13,3 +13,3 @@ 'use strict'; | ||
const { setProp } = require('../base/model'); | ||
const { cloneCsn, isEdmPropertyRendered } = require('../model/csnUtils'); | ||
const { cloneCsn, isEdmPropertyRendered, isBuiltinType } = require('../model/csnUtils'); | ||
const { checkCSNVersion } = require('../json/csnVersion'); | ||
@@ -396,8 +396,12 @@ const { makeMessageFunction } = require('../base/messages'); | ||
properties.forEach(p => { | ||
const pLoc = [...loc, 'elements', p.Name]; | ||
if(!p[p._typeName]) { | ||
warning('odata-spec-violation-attribute', loc, { name: p.Name, prop: p._typeName }); | ||
warning('odata-spec-violation-attribute', pLoc, { name: p.Name, prop: p._typeName }); | ||
} | ||
if(p.Name === EntityTypeName) { | ||
warning('odata-spec-violation-property-name', loc, { name: p.Name, type: 'EntityType' }); | ||
warning('odata-spec-violation-property-name', pLoc, { name: p.Name, type: 'EntityType' }); | ||
} | ||
if(options.isV2() && p._isCollection && !edmUtils.isAssociationOrComposition(p._csn)) { | ||
warning('odata-spec-violation-array-of-v2', pLoc, { literal: 'Property', prop: 'Type', name: 'Collection' }); | ||
} | ||
}); | ||
@@ -418,7 +422,4 @@ | ||
let containerEntry; | ||
let singleton = entityCsn['@odata.singleton']; | ||
let hasNullable = entityCsn['@odata.singleton.nullable'] !== undefined && | ||
entityCsn['@odata.singleton.nullable'] !== null; | ||
if(singleton || ((singleton === undefined || singleton === null) && hasNullable)) { | ||
if(isSingleton(entityCsn)) { | ||
containerEntry = new Edm.Singleton(v, { Name: EntitySetName, Type: fullQualified(EntityTypeName) }, entityCsn); | ||
@@ -433,3 +434,2 @@ if(entityCsn['@odata.singleton.nullable']) | ||
// V4: Create NavigationPropertyBinding in EntitySet | ||
// if NavigationProperty is not a Containment and if the target is not a containee | ||
if(options.isV4()) | ||
@@ -439,3 +439,7 @@ properties.filter(np => | ||
// @ts-ignore TypeScript does not recognize these properties on type NavigationProperty | ||
!np.isContainment() && !edmUtils.isContainee(np._targetCsn) && !np._targetCsn.$proxy && !np._targetCsn.$externalRef | ||
!np.isContainment() && | ||
!edmUtils.isContainee(np._targetCsn) && | ||
!np._targetCsn.$proxy && | ||
!np._targetCsn.$externalRef && | ||
!(np._isCollection && isSingleton(np._targetCsn)) | ||
). forEach(np => | ||
@@ -452,2 +456,8 @@ containerEntry.append(np.createNavigationPropertyBinding(schemaNamePrefix))); | ||
}); | ||
function isSingleton(entityCsn) { | ||
const singleton = entityCsn['@odata.singleton']; | ||
const hasNullable = entityCsn['@odata.singleton.nullable'] !== undefined && entityCsn['@odata.singleton.nullable'] !== null; | ||
return options.isV4() && singleton || ((singleton === undefined || singleton === null) && hasNullable); | ||
} | ||
} | ||
@@ -537,2 +547,3 @@ | ||
const actLoc = ['definitions', ...(entityCsn ? [entityCsn.name, 'actions', actionCsn.name] : [actionCsn.name])]; | ||
let rt = actionCsn.returns && ((actionCsn.returns.items && actionCsn.returns.items.type) || actionCsn.returns.type); | ||
@@ -582,3 +593,11 @@ if(rt) // add EntitySet attribute only if return type is an entity | ||
edmUtils.forAll(actionCsn.params, (parameterCsn, parameterName) => { | ||
functionImport.append(new Edm.Parameter(v, { Name: parameterName }, parameterCsn, 'In' )); | ||
const paramLoc = [...actLoc, 'params', parameterName]; | ||
const param = new Edm.Parameter(v, { Name: parameterName }, parameterCsn, 'In' ); | ||
if(!param._type.startsWith('Edm.') && !edmUtils.isStructuredType(csn.definitions[param._type])) { | ||
warning('odata-spec-violation-param-v2', paramLoc); | ||
} | ||
if(param._isCollection) { | ||
warning('odata-spec-violation-array-of-v2', paramLoc, { literal: 'FunctionImport Parameter', prop: 'Type', name: 'Collection' }); | ||
} | ||
functionImport.append(param); | ||
}); | ||
@@ -594,4 +613,9 @@ | ||
let type = returns.type; | ||
if(type) | ||
if(type){ | ||
if(!isBuiltinType(type) && !['entity', 'view', 'type'].includes(csn.definitions[type].kind)){ | ||
const returnsLoc = [ ...actLoc, 'returns']; | ||
warning('odata-spec-violation-returns-v2', returnsLoc); | ||
} | ||
type = edmUtils.mapCdsToEdmType(returns, messageFunctions, options.isV2()); | ||
} | ||
@@ -676,2 +700,3 @@ if(action.returns._isCollection) | ||
let properties = createProperties(elementsCsn)[0]; | ||
const loc = ['definitions', structuredTypeCsn.name]; | ||
@@ -683,8 +708,17 @@ if(properties.length === 0) { | ||
properties.forEach(p => { | ||
const pLoc = [ ...loc, ...(structuredTypeCsn.items ? ['items', 'elements'] : [ 'elements' ]), p.Name ]; | ||
if(!p[p._typeName]) { | ||
warning('odata-spec-violation-attribute', ['definitions', structuredTypeCsn.name], { name: p.Name, prop: p._typeName }); | ||
warning('odata-spec-violation-attribute', pLoc, { name: p.Name, prop: p._typeName }); | ||
} | ||
if(p.Name === complexType.Name) { | ||
warning('odata-spec-violation-property-name', ['definitions', structuredTypeCsn.name], { name: p.Name, type: complexType.kind }); | ||
warning('odata-spec-violation-property-name', pLoc, { name: p.Name, type: complexType.kind }); | ||
} | ||
if(options.isV2()) { | ||
if(p._isCollection && !edmUtils.isAssociationOrComposition(p._csn)) { | ||
warning('odata-spec-violation-array-of-v2', pLoc, { literal: 'Property' ,prop: 'Type', name: 'Collection' }); | ||
} | ||
if(edmUtils.isAssociationOrComposition(p._csn)) { | ||
warning('odata-spec-violation-assoc-v2', pLoc, { name: structuredTypeCsn.name }); | ||
} | ||
} | ||
}); | ||
@@ -691,0 +725,0 @@ |
@@ -148,2 +148,17 @@ // CSN frontend - transform CSN into XSN | ||
}, | ||
column: { | ||
arrayOf: selectItem, | ||
msgId: 'syntax-csn-expected-column', | ||
defaultKind: '$column', | ||
validKinds: [], // pseudo kind '$column' | ||
requires: [ 'ref', 'xpr', 'val', '#', 'func', 'list', 'SELECT', 'SET', 'expand' ], | ||
schema: { | ||
xpr: { | ||
class: 'condition', | ||
type: xprInValue, | ||
inKind: [ '$column' ], | ||
inValue: true, | ||
}, | ||
}, | ||
}, | ||
}; | ||
@@ -227,31 +242,11 @@ | ||
columns: { | ||
arrayOf: selectItem, | ||
msgId: 'syntax-csn-expected-column', | ||
defaultKind: '$column', | ||
validKinds: [], // pseudo kind '$column' | ||
requires: [ 'ref', 'xpr', 'val', '#', 'func', 'list', 'SELECT', 'SET' ], // requires one of... | ||
inKind: [ 'extend' ], // only valid in extend and SELECT | ||
schema: { | ||
xpr: { | ||
class: 'condition', | ||
type: xprInValue, | ||
inKind: [ '$column' ], | ||
inValue: true, | ||
}, | ||
}, | ||
class: 'column', | ||
inKind: [ 'extend' ], // only valid in extend and SELECT/projection | ||
}, | ||
expand: { | ||
arrayOf: selectItem, // TODO: more specific | ||
msgId: 'syntax-csn-expected-column', | ||
defaultKind: '$column', | ||
validKinds: [], // pseudo kind '$column' | ||
requires: [ 'ref' ], // requires one of... | ||
class: 'column', | ||
inKind: [ '$column' ], // only valid in $column | ||
}, | ||
inline: { | ||
arrayOf: selectItem, // TODO: more specific | ||
msgId: 'syntax-csn-expected-column', | ||
defaultKind: '$column', | ||
validKinds: [], // pseudo kind '$column' | ||
requires: [ 'ref' ], // requires one of... | ||
class: 'column', | ||
inKind: [ '$column' ], // only valid in $column | ||
@@ -518,2 +513,3 @@ }, | ||
excluding: { | ||
inKind: [ '$column' ], | ||
arrayOf: string, | ||
@@ -562,3 +558,3 @@ type: excluding, | ||
doc: { | ||
type: stringVal, | ||
type: stringValOrNull, | ||
inKind: () => true, // allowed in all definitions (including columns and extensions) | ||
@@ -705,2 +701,3 @@ }, | ||
let dollarLocations = []; | ||
let arrayLvlCnt = 0; | ||
@@ -1106,2 +1103,9 @@ /** | ||
function stringValOrNull( val, spec ) { | ||
if (val === null) | ||
return { val, location: location() }; | ||
return stringVal(val, spec); | ||
} | ||
function natnum( val, spec ) { | ||
@@ -1137,3 +1141,13 @@ if (typeof val === 'number' && val >= 0) | ||
if (Array.isArray( val )) { | ||
return { | ||
const ec = val.reduce((c, v) => ((v && v['...'] && Object.keys(v).length === 1) ? ++c : c), 0); | ||
if (arrayLvlCnt === 0 && ec > 1) { | ||
error( 'syntax-csn-duplicate-ellipsis', location(true), { code: '...' }, | ||
'Expected no more than one $(CODE)' ); | ||
} | ||
if (arrayLvlCnt > 0 && ec > 0) { | ||
error( 'syntax-csn-unexpected-ellipsis', location(true), { code: '...' }, | ||
'Unexpected $(CODE) in nested array' ); | ||
} | ||
arrayLvlCnt++; | ||
const retval = { | ||
location: location(), | ||
@@ -1143,2 +1157,4 @@ val: arrayOf( annoValue )( val, spec ), | ||
}; | ||
arrayLvlCnt--; | ||
return retval; | ||
} | ||
@@ -1161,2 +1177,9 @@ if (typeof val['#'] === 'string') { | ||
} | ||
else if (val['...'] && Object.keys(val).length === 1) { | ||
return { | ||
val: '...', | ||
literal: 'token', | ||
location: location(), | ||
}; | ||
} | ||
const struct = Object.create(null); | ||
@@ -1656,2 +1679,6 @@ ++virtualLine; | ||
attachVocabInDefinitions( r ); | ||
if (csn.$sources && Array.isArray( csn.$sources ) && | ||
csn.$sources.every( fname => typeof fname === 'string' )) | ||
// non-enumerable or enumerable, ignore with wrong value | ||
r.$sources = csn.$sources; | ||
return Object.assign( xsn, r ); | ||
@@ -1658,0 +1685,0 @@ } |
@@ -70,3 +70,3 @@ // Transform XSN (augmented CSN) into CSN | ||
inline: ignore, // do not list for select items as elements | ||
excludingDict: renameTo( 'excluding', Object.keys ), | ||
excludingDict, | ||
groupBy: arrayOf( expression ), | ||
@@ -287,2 +287,3 @@ where: condition, // also pathItem after 'cardinality' before 'args' | ||
// 'namespace' for complete model is 'namespace' of first source | ||
// (not a really useful property at all, avoids XSN inspection by Umbrella) | ||
for (const first in srcDict) { | ||
@@ -377,3 +378,3 @@ const { namespace } = srcDict[first]; | ||
(art.query || art.includes || art.$inferred)) { | ||
const annos = art.$inferred && annotations( art, true ); | ||
const annos = art.$inferred && annotationsAndDocComment( art, true ); | ||
const elems = inferred( art.elements, art.$inferred ); | ||
@@ -399,3 +400,3 @@ /** @type {object} */ | ||
// happen through extensions. | ||
const annos = annotations( art, true ); | ||
const annos = annotationsAndDocComment( art, true ); | ||
const annotate = Object.assign( { annotate: name }, annos ); | ||
@@ -440,4 +441,9 @@ if (Object.keys( annotate ).length > 1) { | ||
function sources( srcDict, csn ) { | ||
const names = Object.keys( srcDict ); | ||
const $sources = names.length && srcDict[names[0]].$sources; | ||
if ($sources) { | ||
setHidden( csn, '$sources', $sources ); | ||
return undefined; | ||
} | ||
// TODO: sort according to some layering order, see #6368 | ||
const names = Object.keys( srcDict); | ||
setHidden( csn, '$sources', (!strictMode) ? names : names.map( relativeName ) ); | ||
@@ -458,3 +464,3 @@ return undefined; | ||
continue; | ||
const csn = annotations( elem, true ); | ||
const csn = annotationsAndDocComment( elem, true ); | ||
if (Object.keys(csn).length) | ||
@@ -549,3 +555,3 @@ ext[name] = csn; | ||
// or annotations (annotated==true) | ||
function annotations( node, annotated ) { | ||
function annotationsAndDocComment( node, annotated ) { | ||
const csn = {}; | ||
@@ -567,2 +573,4 @@ const transformer = transformers['@']; | ||
} | ||
if (node.doc) | ||
csn.doc = transformers.doc(node.doc); | ||
return csn; | ||
@@ -803,2 +811,4 @@ } | ||
return node.val.map( value ); | ||
if (node.literal === 'token' && node.val === '...') | ||
return extra( { '...': true } ); | ||
if (node.literal !== 'struct') | ||
@@ -833,2 +843,3 @@ // no val (undefined) as true only for annotation values (and struct elem values) | ||
const expr = expression( node ); | ||
// we do not set a hidden $parens on array - we could still do it if requested | ||
return !expr.cast && expr.xpr || [ expr ]; | ||
@@ -893,3 +904,3 @@ } | ||
if (node.query) | ||
return query( node.query ); | ||
return query( node.query, null, null, 1 ); | ||
if (!node.op) // parse error | ||
@@ -899,3 +910,3 @@ return { xpr: [] }; | ||
// do not use xpr() for xpr, as it would flatten inner xpr's | ||
return extra({ xpr: node.args.map( expression ) }, dollarExtraNode ); | ||
return extra({ xpr: node.args.map( expression ) }, dollarExtraNode, 1 ); | ||
else if (node.op.val === 'cast') | ||
@@ -905,6 +916,7 @@ return cast( expression( node.args[0] ), dollarExtraNode ); | ||
else if (node.op.val !== ',') | ||
return extra( { xpr: xpr( node ) }, dollarExtraNode ); | ||
return extra( { xpr: xpr( node ) }, dollarExtraNode, 1 ); | ||
return (parensAsStrings) | ||
? { xpr: [ '(', ...xpr( node ), ')' ] } | ||
: extra( { list: node.args.map( expression ) } ); | ||
// the inner parens belong to the tuple construct, i.e. won't count as parens | ||
: extra( { list: node.args.map( expression ) }, dollarExtraNode, 0 ); | ||
} | ||
@@ -956,5 +968,3 @@ | ||
function query( node, csn, xsn ) { | ||
while (Array.isArray(node)) // in parentheses -> remove | ||
node = node[0]; | ||
function query( node, csn, xsn, expectedParens = 0 ) { | ||
if (node.op.val === 'SELECT') { | ||
@@ -966,3 +976,4 @@ if (xsn && xsn.query === node && xsn.$syntax === 'projection' && | ||
} | ||
const select = { SELECT: standard( node ) }; | ||
const select = { SELECT: extra( standard( node ), node, expectedParens ) }; | ||
// one paren pair is not put into XSN - TODO: change that? | ||
const elems = node.elements; | ||
@@ -1013,7 +1024,9 @@ if (elems && node._main && node !== node._main._leadingQuery && gensrcFlavor !== true) { | ||
function excludingDict( xsnDict, csn, xsn ) { | ||
if (xsn.kind !== 'element') | ||
csn.excluding = Object.keys( xsnDict ); | ||
} | ||
function from( node ) { | ||
while (Array.isArray(node)) // TODO: old-style parentheses - keep tmp for A2J | ||
node = node[0]; | ||
// TODO: CSN: FROM ((SELECT...)) as -> also add 'subquery' op? - Together | ||
// with []-elimination in FROM... -> normal standard() | ||
// TODO: can we use the normal standard(), at least with JOIN? | ||
if (node.join) { | ||
@@ -1027,3 +1040,3 @@ const join = { join: node.join.val }; | ||
else if (node.query) { | ||
return addExplicitAs( query( node.query ), node.name ); // $extra inside SELECT/SET | ||
return addExplicitAs( query( node.query, null, null, 1 ), node.name ); | ||
} | ||
@@ -1044,3 +1057,3 @@ else if (!node._artifact || node._artifact._main) { // CQL or follow assoc | ||
// only list annotations here which are provided directly with definition | ||
const col = (gensrcFlavor) ? annotations( elem, false ) : {}; | ||
const col = (gensrcFlavor) ? annotationsAndDocComment( elem, false ) : {}; | ||
// with `client` flavor, assignments are available at the element | ||
@@ -1055,2 +1068,3 @@ const gensrcSaved = gensrcFlavor; | ||
Object.assign( col, (expr.cast ? { xpr: [ expr ] } : expr) ); | ||
gensrcFlavor = gensrcSaved; // for not having annotations in inline etc | ||
if (elem.expand) | ||
@@ -1060,2 +1074,5 @@ col.expand = columns( elem.expand ); | ||
col.inline = columns( elem.inline ); | ||
gensrcFlavor = gensrcFlavor || 'column'; | ||
if (elem.excludingDict) | ||
col.excluding = Object.keys( elem.excludingDict ); | ||
// yes, the AS comes after the EXPAND | ||
@@ -1084,2 +1101,4 @@ addExplicitAs( col, elem.name, neqPath( elem.value ) ); | ||
const parens = elem.value.$parens; | ||
if (parens) | ||
setHidden( col, '$parens', parens.length ); | ||
addLocation( (parens ? parens[parens.length - 1] : elem.value.location), col ); | ||
@@ -1099,8 +1118,9 @@ } | ||
function extra( csn, node ) { | ||
function extra( csn, node, expectedParens = 0 ) { | ||
if (node) { | ||
if (node.$extra) | ||
Object.assign( csn, node.$extra ); | ||
if (node.$parens) | ||
setHidden( csn, '$parens', node.$parens.length ); | ||
const parens = (node.$parens ? node.$parens.length : 0); | ||
if (parens !== expectedParens) | ||
setHidden( csn, '$parens', parens ); | ||
} | ||
@@ -1179,3 +1199,3 @@ return csn; | ||
module.exports = { | ||
cloneCsnDictionary: csnDictionary, | ||
cloneCsnDictionary: (csn, options) => csnDictionary(csn, false, options), | ||
compactModel, | ||
@@ -1182,0 +1202,0 @@ compactQuery, |
@@ -308,3 +308,3 @@ // Generic ANTLR parser class with AST-building functions | ||
function surroundByParens( expr, open, close ) { | ||
function surroundByParens( expr, open, close, asQuery = false ) { | ||
if (!expr) | ||
@@ -317,3 +317,3 @@ return expr; | ||
expr.$parens = [ location ]; | ||
return expr; | ||
return (asQuery) ? { query: expr, location } : expr; | ||
} | ||
@@ -367,2 +367,3 @@ | ||
'Please add the keyword $(KEYWORD) in front of the alias name' ); | ||
return ast; | ||
} | ||
@@ -369,0 +370,0 @@ |
@@ -55,2 +55,3 @@ // Main entry point for the CDS Compiler | ||
// FIXME: The implementation of those functions that delegate to 'backends' should probably move here | ||
// ATTENTION: Keep in sync with main.d.ts! | ||
module.exports = { | ||
@@ -57,0 +58,0 @@ // Compiler |
@@ -33,6 +33,26 @@ // CSN functionality for resolving references | ||
// "csnPath" is the name or index of that property; 'args' (its value can be a | ||
// dictionary) is handled extra here. | ||
// dictionary) is handled extra here, also 'expand' and 'inline' | ||
const artifactProperties = [ 'elements', 'columns', 'keys', 'mixin', 'enum', | ||
'params', 'actions', 'definitions', 'extensions' ]; | ||
'params', 'actions', 'definitions', 'extensions' ]; // + 'args', see above | ||
const scopeSpecs = { | ||
type: { global: true }, | ||
includes: { global: true }, | ||
target: { global: true }, | ||
from: { global: true }, | ||
keys: { lexical: false, dynamic: 'target' }, | ||
excluding: { lexical: false }, | ||
expand: { lexical: justDollar }, // dynamic: 'baseEnv-or-source' | ||
inline: { lexical: justDollar }, // dynamic: 'baseEnv' | ||
'ref-target': { lexical: justDollar }, // dynamic: 'baseEnv' | ||
on: { lexical: justDollar, dynamic: 'query' }, // assoc defs, redirected to | ||
orderBy: { dynamic: 'query' }, | ||
'orderBy-set': { lexical: query => query.$next, dynamic: 'query' }, // to outer SELECT (from UNION) | ||
// default: { lexical: query => query, dynamic: 'source' } | ||
} | ||
function justDollar() { | ||
return null; | ||
} | ||
/** | ||
@@ -56,2 +76,3 @@ * @param {CSN.Model} csn | ||
effectiveType, artifactRef, inspectRef, queryOrMain, | ||
__getCache_forEnrichCsnDebugging: obj => cache.get( obj ), | ||
}; | ||
@@ -63,3 +84,3 @@ | ||
* confusion with the "base type", we do not use the term "final type". | ||
* (This function could be omitted if we would use JS prototypes for type refs.) | ||
* (This function could be simplified if we would use JS prototypes for type refs.) | ||
* | ||
@@ -114,3 +135,3 @@ * @param {CSN.ArtifactWithRefs} art | ||
? csn.definitions[ref] | ||
: cached( ref, 'ref', artifactPathRef ); | ||
: cached( ref, '_ref', artifactPathRef ); | ||
if (art) | ||
@@ -143,2 +164,19 @@ return art; | ||
* @param {CSN.Path} csnPath | ||
* | ||
* - return value `scope` | ||
* global: first item is name of definition | ||
* param: first item is parameter of definition (with param: true) | ||
* parent: first item is elem of parent (definition or outer elem) | ||
* target: first item is elem in target (for keys of assocs) | ||
* $magic: magic variable (path starts with $magic, see also $self) | ||
* // now values only in queries: | ||
* mixin: first item is mixin | ||
* alias: first item is table alias | ||
* $self: first item is $self or $projection | ||
* source: first item is element in a query source | ||
* query: first item is element of current query | ||
* ref-target: first item is element of target of outer ref item | ||
* (used for filter condition) | ||
* expand: ref is "path continuation" of an EXPAND | ||
* inline: ref is "path continuation" of an INLINE | ||
*/ | ||
@@ -150,88 +188,66 @@ function inspectRef( csnPath ) { | ||
function resolveRef( obj, parent, query, scope, baseEnv, main ) { | ||
const queries = cached( main, '$queries', allQueries ); | ||
const path = (typeof obj === 'string') ? [ obj ] : obj.ref; | ||
if (!Array.isArray( path )) | ||
throw new Error( 'Value references must look like {ref:[...]}' ); | ||
let head = pathId( path[0] ); | ||
// 1,2: with 'param' or 'global' property, in `keys` | ||
if (obj.param) { | ||
const head = pathId( path[0] ); | ||
if (obj.param) | ||
return expandRefPath( path, main.params[head], 'param' ); | ||
const spec = scopeSpecs[scope] || {}; | ||
if (spec.global || obj.global) | ||
return expandRefPath( path, csn.definitions[head], 'global', scope === 'from' ); | ||
cached( main, '$queries', allQueries ); | ||
let qenv = query && cache.get( query.projection || query ); | ||
// BACKEND ISSUE: you cannot call csnRefs(), inspect some refs, change the | ||
// CSN and again inspect some refs without calling csnRefs() before! | ||
// WORKAROUND: if no cached query, a backend has changed the CSN - re-eval cache | ||
if (query && !qenv) { | ||
setCache( main, '$queries', allQueries( main ) ); | ||
qenv = cache.get( query.projection || query ); | ||
} | ||
else if (obj.global || [ 'type', 'includes', 'target', 'from' ].includes( scope )) { | ||
return expandRefPath( path, csn.definitions[head], scope ); | ||
// first the lexical scopes (due to query hierarchy) and $magic: --------- | ||
if (spec.lexical !== false) { | ||
const tryAlias = path.length > 1 || obj.expand || obj.inline; | ||
let env = (qenv && spec.lexical) ? spec.lexical( qenv ) : qenv; | ||
while (env) { | ||
const alias = tryAlias && env.aliases[head]; | ||
if (alias) | ||
return expandRefPath( path, alias._select || alias, 'alias', env.$queryNumber ); | ||
const mixin = env._select.mixin && env._select.mixin[head]; | ||
if (mixin && {}.hasOwnProperty.call( env._select.mixin, head )) | ||
return expandRefPath( path, mixin, 'mixin', env.$queryNumber ); | ||
env = env.$next; | ||
} | ||
if (head.charAt(0) === '$') { | ||
if (head !== '$self' && head !== '$projection') | ||
return { scope: '$magic' }; | ||
const self = qenv && qenv.$queryNumber > 1 ? qenv._select : main; | ||
return expandRefPath( path, self, '$self' ); | ||
} | ||
} | ||
else if (scope === 'keys') { | ||
// now the dynamic environment: ------------------------------------------ | ||
if (spec.dynamic === 'target') { // ref in keys | ||
// not selecting the corresponding element for a select column works, | ||
// because explicit keys can only be provided with explicit redirection | ||
// target | ||
const target = csn.definitions[parent.target || parent.cast.target]; | ||
return expandRefPath( path, target.elements[head], 'keys' ); | ||
return expandRefPath( path, target.elements[head], 'target' ); | ||
} | ||
// 3: $magic | ||
if (head.charAt(0) === '$') { | ||
if (head === '$self' || head === '$projection') { | ||
const self = query ? queryOrMain( query, main ) : main; | ||
return expandRefPath( path, self, '$self' ); | ||
} | ||
return { scope: '$magic' }; | ||
} | ||
// 4: where inside ref, expand, inline | ||
if (baseEnv) { | ||
if (baseEnv) // ref-target (filter condition), expand, inline | ||
return expandRefPath( path, baseEnv.elements[head], scope ); | ||
} | ||
// 5,6,7: outside queries, in queries where inferred elements are referred to | ||
if (!query) | ||
return expandRefPath( path, (parent || main).elements[head], scope ); | ||
const select = query.SELECT || query.projection; | ||
const obj$env = obj.$env; | ||
if (!select || obj$env === true) | ||
// TODO: do not do this if current query has a parent query (except with obj.$env) | ||
// TODO: also consider expand/inline | ||
return expandRefPath( path, queryOrMain( query, main ).elements[head] ); | ||
if (!query) // outside queries - TODO: items? | ||
return expandRefPath( path, parent.elements[head], 'parent' ); | ||
// With explicitly provided $env: | ||
if (typeof obj$env === 'number') { // head is mixin or table alias name | ||
const s = (obj$env) ? queries[obj$env - 1] : select; | ||
const m = s.mixin && s.mixin[head]; | ||
return expandRefPath( path, m || getCache( s, '_sources' )[head], (m ? 'mixin' : 'alias') ); | ||
if (spec.dynamic === 'query') | ||
// TODO: for ON condition in expand, would need to use cached _element | ||
return expandRefPath( path, qenv.elements[head], 'query' ); | ||
for (const name in qenv.aliases) { | ||
const found = qenv.aliases[name].elements[head]; | ||
if (found) | ||
return expandRefPath( path, found, 'source', name ) | ||
} | ||
else if (typeof obj$env === 'string') { | ||
const source = getCache( select, '_sources' )[obj$env]; | ||
// Had a case where a obj.$env was the name of a mixin - TODO: should not be - example? | ||
if (source) | ||
return expandRefPath( path, source.elements[head], 'source' ); | ||
else if (select.mixin && select.mixin[obj$env]) | ||
return expandRefPath( path, select.mixin[head], 'source' ); | ||
throw new Error('No source found!'); | ||
} | ||
// ON ref is to be searched only in the query elements | ||
if (scope === 'on') // TODO: ok with expand/inline? Probably not with the latter | ||
return expandRefPath( path, queryOrMain( query, main ).elements[head], scope ); | ||
// 8: try to search in MIXIN section (not in ON of JOINs) | ||
if (scope !== 'from-on' && scope !== 'orderBy' && select.mixin) { | ||
const art = select.mixin[head]; | ||
if (art) | ||
return expandRefPath( path, art, 'mixin' ); | ||
} | ||
// 9: try to search for table aliases (partially in ON of JOINs) | ||
const alias = getCache( select, '$alias' ); | ||
if (path.length > 1 && (alias || scope !== 'from-on')) { | ||
const art = getCache( select, '_sources' )[head]; | ||
if (art) | ||
return expandRefPath( path, art, 'alias' ); | ||
} | ||
// ORDER BY ref might have been a table alias (CSN not always has an $env), | ||
// otherwise query elements | ||
if (scope === 'orderBy') | ||
return expandRefPath( path, queryOrMain( query, main ).elements[head], scope ); | ||
// 10: search in elements of source entity | ||
// TODO: do not do this if current query has a parent query !!! | ||
if (typeof alias === 'string') { // with unique source | ||
const source = getCache( select, '_sources' )[alias]; | ||
return expandRefPath( path, source.elements[head], 'source' ); | ||
} | ||
throw new Error( `Missing helper property $env: ${ scope }` ); | ||
// console.log(query.SELECT,qenv,qenv.$next,main) | ||
throw new Error ( `Path item ${ 0 }=${ head } refers to nothing, scope: ${ scope }` ); | ||
} | ||
@@ -244,5 +260,8 @@ | ||
*/ | ||
function expandRefPath( path, art, scope = null ) { | ||
function expandRefPath( path, art, scope, extraInfo ) { | ||
/** @type {{idx, art?, env?}[]} */ | ||
const links = path.map( (_v, idx) => ({ idx }) ); | ||
// TODO: backends should be changed to enable uncommenting: | ||
// if (!art) // does not work with test3/Associations/KeylessManagedAssociation/ | ||
// throw new Error ( `Path item ${ 0 }=${ pathId( path[0] ) } refers to nothing, scope: ${ scope }`); | ||
links[0].art = art; | ||
@@ -262,7 +281,8 @@ for (let i = 1; i < links.length; ++i) { // yes, starting at 1, links[0] is set above | ||
} | ||
const last = path[path.length - 1] | ||
if (scope === 'from' || typeof last !== 'string') { | ||
const last = path[path.length - 1]; | ||
const fromRef = scope === 'global' && extraInfo; | ||
if (fromRef || typeof last !== 'string') { | ||
const env = navigationEnv( art ); | ||
links[links.length - 1].env = env; | ||
if (scope === 'from') | ||
if (fromRef) | ||
art = env; | ||
@@ -272,3 +292,5 @@ if (typeof last !== 'string') | ||
} | ||
return { links, art, scope }; | ||
return (extraInfo && !fromRef) | ||
? { links, art, scope, $env: extraInfo } | ||
: { links, art, scope }; | ||
} | ||
@@ -287,25 +309,72 @@ | ||
if (!projection) | ||
return all; | ||
traverseQuery( projection, null, function memorize( query, select ) { | ||
return null; | ||
traverseQuery( projection, null, null, function memorize( query, fromSelect, parentQuery ) { | ||
if (query.ref) { // ref in from | ||
// console.log('SQ:',query,cache.get(query)) | ||
const as = query.as || implicitAs( query.ref ); | ||
getCache( select, '_sources' )[as] = fromRef( query ); | ||
const alias = getCache( select, '$alias' ) // alias of unique source | ||
setCache( select, '$alias', (alias != null) ? typeof alias === 'string' : as); | ||
getCache( fromSelect, 'aliases' )[as] = fromRef( query ); | ||
} | ||
else if (select && query.as) { // sub query in FROM | ||
const { as } = query; | ||
getCache( select, '_sources' )[as] = queryOrMain( query, main ); | ||
const alias = getCache( select, '$alias' ) // alias of unique source | ||
setCache( select, '$alias', (alias != null) ? typeof alias === 'string' : as); } | ||
const proj = query.SELECT || query.projection; | ||
if (proj) { // every SELECT query -- TODO: remember number? | ||
setCache( proj, '_sources', Object.create(null) ); | ||
setCache( proj, '$alias', null ); | ||
all.push( proj ); | ||
else { | ||
const qenv = initQueryEnv( query !== main && query, parentQuery ); | ||
if (fromSelect) | ||
getCache( fromSelect, 'aliases' )[query.as] = qenv; | ||
const select = query.SELECT || query.projection; | ||
if (select) { | ||
cache.set( select, qenv ); // query and query.SELECT have the same cache qenv | ||
qenv._select = select; | ||
all.push( qenv ); | ||
} | ||
} | ||
} ); | ||
all.forEach( function initElements( qenv, index ) { | ||
qenv.$queryNumber = index + 1; | ||
qenv.elements = (index ? qenv._select : main).elements; | ||
const columns = qenv._select.columns; | ||
if (qenv.elements && columns) | ||
columns.map( c => initColumnElement( c, qenv ) ); | ||
} ); | ||
return all; | ||
} | ||
function initQueryEnv( query, parentQuery ) { | ||
let qenv; // = query && cache.get( query ) | ||
// if (qenv) | ||
// return qenv; | ||
const penv = parentQuery && parentQuery.SET && cache.get( parentQuery ); | ||
if (penv && !penv._select) { // SELECT not yet reached | ||
qenv = penv; // for leading query | ||
} | ||
else { | ||
qenv = { aliases: Object.create(null) }; | ||
if (parentQuery) | ||
qenv.$next = cache.get( parentQuery ); | ||
} | ||
if (query) | ||
cache.set( query, qenv ); | ||
return qenv; | ||
} | ||
function initColumnElement( col, parentElementOrQueryEnv ) { | ||
if (col === '*') | ||
return; | ||
if (col.inline) { | ||
col.inline.map( c => initColumnElement( c, parentElementOrQueryEnv ) ); | ||
return; | ||
} | ||
setCache( col, '_parent', // not set for query (has property _select) | ||
!parentElementOrQueryEnv._select && parentElementOrQueryEnv ); | ||
const as = col.as || col.func || implicitAs( col.ref ); | ||
let type = parentElementOrQueryEnv; | ||
while (type.items) | ||
type = type.items; | ||
const elem = setCache( col, '_element', type.elements[as] ); | ||
// if requested, we could set a _column link in element | ||
if (col.expand) | ||
col.expand.map( c => initColumnElement( c, elem ) ); | ||
} | ||
// property name convention in cache: | ||
// - $name: to other cache object (with proto), dictionary (w/o proto), or scalar | ||
// - _name, name: to CSN object value (_name) or dictionary (name) | ||
function setCache( obj, prop, val ) { | ||
@@ -340,7 +409,6 @@ let hidden = cache.get( obj ); | ||
// Return value of a query SELECT for the query, or the main artifact, | ||
// Return value of a query SELECT for the query node, or the main artifact, | ||
// i.e. a value with an `elements` property. | ||
// TODO: avoid the term Query, use QuerySelect or QueryNode | ||
/** | ||
* @param {CSN.Query} query | ||
* @param {CSN.Query} query node (object with SET or SELECT property) | ||
* @param {CSN.Definition} main | ||
@@ -365,22 +433,28 @@ */ | ||
/** | ||
* Traverse query in pre-order | ||
* | ||
* @param {CSN.Query} query | ||
* @param {CSN.QuerySelect} select | ||
* @param {CSN.QuerySelect} fromSelect | ||
* @param {(query: CSN.Query&CSN.QueryFrom, select: CSN.QuerySelectEnriched) => void} callback | ||
*/ | ||
function traverseQuery( query, select, callback ) { | ||
function traverseQuery( query, fromSelect, parentQuery, callback ) { | ||
if (query.SELECT || query.projection) { | ||
callback( query, select ); | ||
query = query.SELECT || query.projection; | ||
traverseFrom( query.from, query, callback ); | ||
callback( query, fromSelect, parentQuery ); | ||
const select = query.SELECT || query.projection; | ||
traverseFrom( select.from, select, parentQuery, callback ); | ||
for (const prop of [ 'columns', 'where', 'having' ]) { | ||
// all properties which could have sub queries | ||
const expr = select[prop]; | ||
if (expr) | ||
expr.forEach( q => traverseExpr( q, query, callback ) ); | ||
} | ||
} | ||
else if (query.SET) { | ||
callback( query, select ); | ||
query = query.SET; | ||
} | ||
for (const prop of [ 'args', 'xpr', 'columns', 'where', 'having' ]) { | ||
// all properties which could have sub queries (directly or indirectly) | ||
const expr = query[prop]; | ||
if (expr && typeof expr === 'object') { | ||
const args = Array.isArray( expr ) ? expr : Object.values( expr ); | ||
args.forEach( q => traverseQuery( q, null, callback ) ); | ||
callback( query, fromSelect, parentQuery ); | ||
const { args } = query.SET; | ||
for (const q of args || []) { | ||
if (q === args[0]) // leading query | ||
traverseQuery( q, fromSelect, query, callback ); | ||
else | ||
traverseQuery( q, null, query, callback ); | ||
} | ||
@@ -395,16 +469,29 @@ } | ||
*/ | ||
function traverseFrom( from, select, callback ) { | ||
function traverseFrom( from, fromSelect, parentQuery, callback ) { | ||
if (from.ref) { | ||
callback( from, select ); | ||
callback( from, fromSelect, parentQuery ); | ||
} | ||
else if (from.args) { // join | ||
from.args.forEach( arg => traverseFrom( arg, select, callback ) ); | ||
if (from.on) // join | ||
from.on.forEach( arg => traverseQuery( arg, select, callback ) ); | ||
from.args.forEach( arg => traverseFrom( arg, fromSelect, parentQuery, callback ) ); | ||
if (from.on) // join-on, potentially having a sub query | ||
from.on.forEach( arg => traverseQuery( arg, null, fromSelect, callback ) ); | ||
} | ||
else { // sub query in FROM | ||
traverseQuery( from, select, callback ); | ||
traverseQuery( from, fromSelect, parentQuery, callback ); | ||
} | ||
} | ||
function traverseExpr( expr, parentQuery, callback ) { | ||
if (expr.SELECT || expr.SET) | ||
traverseQuery( expr, null, parentQuery, callback ) | ||
for (const prop of [ 'args', 'xpr' ]) { | ||
// all properties which could have sub queries (directly or indirectly), | ||
const val = expr[prop]; | ||
if (val && typeof val === 'object') { | ||
const args = Array.isArray( val ) ? val : Object.values( val ); | ||
args.forEach( e => traverseExpr( e, parentQuery, callback ) ); | ||
} | ||
} | ||
} | ||
function pathId( item ) { | ||
@@ -457,7 +544,6 @@ return (typeof item === 'string') ? item : item.id; | ||
isName = true; // for named arguments | ||
if (scope === 'orderBy') | ||
scope = 'orderBy-xpr'; // no need to extra 'orderBy-args' | ||
} | ||
else if (prop === 'SELECT' || prop === 'SET' || prop === 'projection') { | ||
query = obj; | ||
parent = null; | ||
scope = prop; | ||
@@ -469,18 +555,21 @@ } | ||
csn.definitions[csnPath[1]] ); | ||
scope = 'ref-where'; | ||
scope = 'ref-target'; | ||
} | ||
else if ((prop === 'expand' || prop === 'inline') && obj.ref) { | ||
if (obj.ref && resolve) { | ||
baseEnv = resolve.expand( obj, parent, query, scope, baseEnv, | ||
csn.definitions[csnPath[1]] ); | ||
else if (prop === 'expand' || prop === 'inline') { | ||
if (obj.ref) { | ||
if (resolve) | ||
baseEnv = resolve.expand( obj, parent, query, scope, baseEnv, | ||
csn.definitions[csnPath[1]] ); | ||
scope = prop; | ||
} | ||
scope = prop; | ||
if (prop === 'expand') | ||
isName = prop; | ||
} | ||
else if (prop === 'on') { | ||
if (scope === 'from') | ||
scope = 'from-on'; | ||
scope = 'join-on'; | ||
else if (scope === 'mixin') | ||
scope = 'mixin-on'; | ||
else | ||
scope = 'on'; | ||
scope = 'on'; // will use query elements with REDIRECTED TO | ||
} | ||
@@ -491,8 +580,8 @@ else if (prop === 'ref') { | ||
} | ||
else if (prop === 'orderBy') { | ||
scope = (query.SET ? 'orderBy-set' : 'orderBy'); | ||
} | ||
else if (prop !== 'xpr') { | ||
scope = prop; | ||
} | ||
else if (scope === 'orderBy') { | ||
scope = 'orderBy-xpr'; | ||
} | ||
@@ -499,0 +588,0 @@ obj = obj[prop]; |
@@ -5,3 +5,3 @@ 'use strict'; | ||
const { csnRefs } = require('../model/csnRefs'); | ||
const { sortCsn } = require('../json/to-csn'); | ||
const { sortCsn, cloneCsnDictionary: _cloneCsnDictionary } = require('../json/to-csn'); | ||
const version = require('../../package.json').version; | ||
@@ -56,3 +56,3 @@ | ||
getServiceName, | ||
hasBoolAnnotation, | ||
hasAnnotationValue, | ||
cloneWithTransformations, | ||
@@ -431,5 +431,6 @@ getFinalBaseType, | ||
* Deeply clone the given CSN model and return it. | ||
* In testMode, definitions are sorted. | ||
* In testMode (or with testSortCsn), definitions are sorted. | ||
* Note that annotations are only copied shallowly. | ||
* | ||
* @param {object} csn | ||
* @param {object} csn Top-level CSN. You can pass non-dictionary values. | ||
* @param {CSN.Options} options CSN Options, only used for `dictionaryPrototype`, `testMode`, and `testSortCsn` | ||
@@ -442,2 +443,16 @@ */ | ||
/** | ||
* Deeply clone the given CSN dictionary and return it. | ||
* Note that annotations are only copied shallowly. | ||
* This function does _not_ sort the given dictionary. | ||
* See cloneCsn() if you want sorted definitions. | ||
* | ||
* @param {object} csn | ||
* @param {CSN.Options} options Only cloneOptions.dictionaryPrototype is | ||
* used and cloneOptions are passed to sort(). | ||
*/ | ||
function cloneCsnDictionary(csn, options) { | ||
return _cloneCsnDictionary(csn, options); | ||
} | ||
/** | ||
* Apply function `callback` to all artifacts in dictionary | ||
@@ -533,3 +548,5 @@ * `model.definitions`. See function `forEachGeneric` for details. | ||
const dictObj = dict[name]; | ||
if(iterateOptions.skip && iterateOptions.skip.includes(dictObj.kind)) | ||
if((iterateOptions.skip && iterateOptions.skip.includes(dictObj.kind)) | ||
|| (iterateOptions.skipArtifact && typeof iterateOptions.skipArtifact === 'function' | ||
&& iterateOptions.skipArtifact(dictObj, name))) | ||
continue; | ||
@@ -690,11 +707,26 @@ cb( dictObj, name ); | ||
/** | ||
* Returns true if the element has a specific annotation set to the given value. | ||
* Compare a given annotation value with an expectation value and return | ||
* | ||
* | Expected | ||
* | true | false | null | arb val | ||
* Anno Val |-------|-------|-------|-------- | ||
* true | true | false | false | false | ||
* false | false | true | false | false | ||
* null | false | true | true | false | ||
* arb val | false | false | false | true/false | ||
* | ||
* If the annotation value is 'null', 'true' is returned for the expectation values | ||
* 'null' and 'false'. Expecting 'null' for an annotation value 'false' returns | ||
* 'false'. | ||
* | ||
* @param {CSN.Artifact} artifact | ||
* @param {string} annotationName Name of the annotation (including the at-sign) | ||
* @param {any} value | ||
* @param {any} expected | ||
* @returns {boolean} | ||
*/ | ||
function hasBoolAnnotation(artifact, annotationName, value = true) { | ||
return artifact[annotationName] === value; | ||
function hasAnnotationValue(artifact, annotationName, expected = true) { | ||
if(expected === false) | ||
return artifact[annotationName] === expected || artifact[annotationName] === null; | ||
else | ||
return artifact[annotationName] === expected; | ||
} | ||
@@ -1056,3 +1088,3 @@ | ||
function isPersistedOnDatabase(art) { | ||
return !([ 'entity', 'view' ].includes(art.kind) && (art.abstract || hasBoolAnnotation(art, '@cds.persistence.skip'))); | ||
return !([ 'entity', 'view' ].includes(art.kind) && (art.abstract || hasAnnotationValue(art, '@cds.persistence.skip'))); | ||
} | ||
@@ -1170,2 +1202,3 @@ | ||
// TODO: to be checked by author: still intended behaviour with 'cds' prefix? | ||
// TODO: Can this be replaced by getRootArtifactName? Or maybe not rely on namespace-hacking... | ||
// FIXME: This only works with namespace-hacking, i.e. adding them as artifacts... | ||
@@ -1278,3 +1311,3 @@ function getTopLevelArtifactNameOf(name, model) { | ||
if (prop.startsWith('@')) { | ||
if (toNode[prop] == undefined || overwrite) { | ||
if (toNode[prop] === undefined || overwrite) { | ||
toNode[prop] = fromNode[prop]; | ||
@@ -1286,11 +1319,2 @@ } | ||
// Return true if 'type' is a composition type | ||
function isComposition(type) { | ||
if (!type) | ||
return type; | ||
if (type._artifact) | ||
type = type._artifact.name; | ||
return type.absolute === 'cds.Composition'; | ||
} | ||
function isAspect(node) { | ||
@@ -1318,56 +1342,4 @@ return node && node.kind === 'aspect'; | ||
// Add an annotation with absolute name 'absoluteName' (including '@') and string value 'theValue' to 'node' | ||
function addBoolAnnotationTo(absoluteName, theValue, node) { | ||
// Sanity check | ||
if (!absoluteName.startsWith('@')) { | ||
throw Error('Annotation name should start with "@": ' + absoluteName); | ||
} | ||
// Assemble the annotation | ||
if(isAnnotationAssignable(node, absoluteName)) { | ||
node[absoluteName] = { | ||
name: { | ||
absolute: absoluteName.substring(1), | ||
location: node.location, // inherit location from main element | ||
}, | ||
val: theValue, | ||
literal: 'boolean', | ||
location: node.location, // inherit location from main element | ||
}; | ||
} | ||
} | ||
// Add an annotation with absolute name 'absoluteName' (including '@') and array 'theValue' to 'node' | ||
function addArrayAnnotationTo(absoluteName, theValue, node) { | ||
// Sanity check | ||
if (!absoluteName.startsWith('@')) { | ||
throw Error(`Annotation name should start with "@": ${ absoluteName}`); | ||
} | ||
// Assemble the annotation | ||
if(isAnnotationAssignable(node, absoluteName)) { | ||
node[absoluteName] = { | ||
name: { | ||
absolute: absoluteName.substring(1), | ||
location: node.location, // inherit location from main element | ||
}, | ||
val: theValue, | ||
literal: 'array', | ||
location: node.location, // inherit location from main element | ||
}; | ||
} | ||
} | ||
/** | ||
* Check wether an annotation can be assigned or not | ||
* (must be either undefined or null value) | ||
* @param {object} node Assignee | ||
* @param {string} name Annotation name | ||
* @returns {boolean} | ||
*/ | ||
function isAnnotationAssignable(node, name) { | ||
if (!name.startsWith('@')) { | ||
throw Error(`Annotation name should start with "@": ${ name}`); | ||
} | ||
return (node[name] === undefined || node[name] && node[name].val === null); | ||
} | ||
/** | ||
* Return true if the artifact has a valid, truthy persistence.exists/skip annotation | ||
@@ -1380,3 +1352,3 @@ * | ||
return (artifact.kind === 'entity' || artifact.kind === 'view') && | ||
(hasBoolAnnotation(artifact, '@cds.persistence.exists', true) || hasBoolAnnotation(artifact, '@cds.persistence.skip', true)) | ||
(hasAnnotationValue(artifact, '@cds.persistence.exists', true) || hasAnnotationValue(artifact, '@cds.persistence.skip', true)) | ||
@@ -1435,3 +1407,3 @@ } | ||
* | ||
* @param {CSN.Model} csn | ||
* @param {CSN.Model} csn | ||
* @returns {CSN.Service[]} | ||
@@ -1453,2 +1425,3 @@ */ | ||
cloneCsn, | ||
cloneCsnDictionary, | ||
isBuiltinType, | ||
@@ -1463,3 +1436,3 @@ assignAll, | ||
forAllElements, | ||
hasBoolAnnotation, | ||
hasAnnotationValue, | ||
isEdmPropertyRendered, | ||
@@ -1483,7 +1456,4 @@ getArtifactDatabaseNameOf, | ||
copyAnnotations, | ||
isComposition, | ||
isAspect, | ||
forEachPath, | ||
addBoolAnnotationTo, | ||
addArrayAnnotationTo, | ||
hasValidSkipOrExists, | ||
@@ -1490,0 +1460,0 @@ getNamespace, |
@@ -35,3 +35,3 @@ // For testing: reveal non-enumerable properties in CSN, display result of csnRefs | ||
const transformers = { | ||
$env: reveal, | ||
// $env: reveal, | ||
elements: dictionary, | ||
@@ -48,6 +48,8 @@ definitions: dictionary, | ||
includes: simpleRef, | ||
'@': () => {}, | ||
// TODO: excluding | ||
'@': () => { /* ignore annotations */ }, | ||
} | ||
setLocations( csn, false, null ); | ||
const { inspectRef, artifactRef } = csnRefs( csn ); | ||
const { inspectRef, artifactRef, __getCache_forEnrichCsnDebugging } = csnRefs( csn ); | ||
let $$cacheObjectNumber = 0; // for debugging | ||
const csnPath = []; | ||
@@ -58,2 +60,4 @@ if (csn.definitions) | ||
reveal( csn, '$location', locationString( csn.$location ) ); | ||
if (csn.$sources) | ||
reveal( csn, '$sources', csn.$sources ); | ||
return csn; | ||
@@ -74,2 +78,5 @@ | ||
} | ||
if (obj.$parens) | ||
reveal( obj, '$parens', obj.$parens ); | ||
_cache_debug( obj ); | ||
} | ||
@@ -120,3 +127,3 @@ csnPath.pop(); | ||
function pathRef( parent, prop, path ) { | ||
const { links, art, scope } = (() => { | ||
const { links, art, scope, $env } = (() => { | ||
if (options.testMode) | ||
@@ -138,2 +145,4 @@ return inspectRef( csnPath ); | ||
parent._scope = scope; | ||
if ($env) | ||
parent._env = $env; | ||
@@ -153,2 +162,47 @@ csnPath.push( prop ); | ||
} | ||
function _cache_debug( obj ) { | ||
if (options.enrichCsn !== 'DEBUG') | ||
return; | ||
const cache = __getCache_forEnrichCsnDebugging( obj ); | ||
if (!cache) | ||
return; | ||
if (cache.$$objectNumber > 0) { | ||
obj.$$cacheObjectNumber = cache.$$objectNumber; | ||
} | ||
else { | ||
cache.$$objectNumber = (cache.$$objectNumber) | ||
? -cache.$$objectNumber | ||
: ++$$cacheObjectNumber; | ||
obj.$$cacheObject = {}; | ||
for (const name of Object.keys( cache )) { | ||
const val = cache[name]; | ||
if (val === null || typeof val !== 'object') { | ||
obj.$$cacheObject[name] = val; | ||
} | ||
else if (name[0] === '_') { | ||
// _‹name›: link to CSN node, usually with kind & location | ||
obj.$$cacheObject[name] | ||
= (val.$location) ? locationString( val.$location ) : 'CSN node'; | ||
} | ||
else if (name[0] !== '$' || !Object.getPrototypeOf( val )) { | ||
// ‹name›: dictionary of CSN nodes, | ||
// ‹$name›: dictionary of cache values if no prototype, | ||
obj.$$cacheObject[name] = Object.keys( val ); // TODO: or dict? | ||
} | ||
else if (Array.isArray( val )) { | ||
obj.$$cacheObject[name] = val.map( item => { | ||
if (!item.$$objectNumber) | ||
item.$$objectNumber = -(++$$cacheObjectNumber); | ||
return item.$$objectNumber; | ||
} ); | ||
} | ||
else { | ||
if (!val.$$objectNumber) | ||
val.$$objectNumber = -(++$$cacheObjectNumber); | ||
obj.$$cacheObject[name] = val.$$objectNumber || -(++$$cacheObjectNumber); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
@@ -155,0 +209,0 @@ |
'use strict'; | ||
const { makeMessageFunction } = require('../base/messages'); | ||
const { | ||
forEachDefinition, | ||
forEachMember, | ||
hasBoolAnnotation | ||
hasAnnotationValue | ||
} = require('../model/csnUtils'); | ||
@@ -14,6 +15,10 @@ | ||
* @param afterModel the after-model | ||
* @param {hdiOptions|false} options | ||
* @returns {object} the sets of deletions, extensions, and migrations of entities necessary to transform the before-model | ||
* to the after-model, together with all the definitions of the after-model | ||
*/ | ||
function compareModels(beforeModel, afterModel) { | ||
function compareModels(beforeModel, afterModel, options) { | ||
if(!(options && options.testMode)) // no $version with testMode | ||
validateCsnVersions(beforeModel, afterModel, options); | ||
const deletedEntities = Object.create(null); | ||
@@ -35,2 +40,25 @@ const elementAdditions = []; | ||
function validateCsnVersions(beforeModel, afterModel, options) { | ||
const beforeVersion = beforeModel.$version; | ||
const afterVersion = afterModel.$version; | ||
let beforeVersionParts = beforeVersion && beforeVersion.split('.'); | ||
let afterVersionParts = afterVersion && afterVersion.split('.'); | ||
if (!beforeVersionParts || beforeVersionParts.length < 2) { | ||
const { error, throwWithError } = makeMessageFunction(beforeModel, options, 'modelCompare'); | ||
error(null, null, `Invalid CSN version: ${beforeVersion}`); | ||
throwWithError(); | ||
} | ||
if (!afterVersionParts || afterVersionParts.length < 2) { | ||
const { error, throwWithError } = makeMessageFunction(afterModel, options, 'modelCompare'); | ||
error(null, null, `Invalid CSN version: ${afterVersion}`); | ||
throwWithError(); | ||
} | ||
if (beforeVersionParts[0] > afterVersionParts[0] && !(options && options.allowCsnDowngrade)) { | ||
const { error, throwWithError } = makeMessageFunction(afterModel, options, 'modelCompare'); | ||
error(null, null, `Incompatible CSN versions: ${afterVersion} is a major downgrade from ${beforeVersion}. Is @sap/cds-compiler version ${require('../../package.json').version} outdated?`); | ||
throwWithError(); | ||
} | ||
} | ||
function getArtifactComparator(otherModel, addedEntities, deletedEntities, elementAdditions, elementChanges) { // (, alerts) | ||
@@ -111,5 +139,5 @@ return function compareArtifacts(artifact, name) { // (, topKey, path) topKey == 'definitions' | ||
&& !artifact.abstract | ||
&& (!artifact.query && !artifact.projection || hasBoolAnnotation(artifact, '@cds.persistence.table')) | ||
&& !hasBoolAnnotation(artifact, '@cds.persistence.skip') | ||
&& !hasBoolAnnotation(artifact, '@cds.persistence.exists'); | ||
&& (!artifact.query && !artifact.projection || hasAnnotationValue(artifact, '@cds.persistence.table')) | ||
&& !hasAnnotationValue(artifact, '@cds.persistence.skip') | ||
&& !hasAnnotationValue(artifact, '@cds.persistence.exists'); | ||
} | ||
@@ -116,0 +144,0 @@ |
@@ -36,3 +36,3 @@ 'use strict'; | ||
.option(' --parse-only') | ||
.option(' --fallback-parser <type>', ['cdl', 'csn']) | ||
.option(' --fallback-parser <type>', ['cdl', 'csn', 'csn!']) | ||
.option(' --test-mode') | ||
@@ -115,4 +115,5 @@ .option(' --test-sort-csn') | ||
parser as a fallback. Valid values are: | ||
cdl : Use CDL parser | ||
csn : Use CSN parser | ||
cdl : Use CDL parser | ||
csn : Use CSN parser | ||
csn! : Use CSN parser even with extension cds, cdl, hdbcds and hdbdd | ||
--direct-backend Do not compile the given CSN but directly pass it to the backend. | ||
@@ -184,4 +185,2 @@ Can only be used with certain new CSN based backends. Combination with | ||
.option('-j, --json') | ||
.option(' --separate') | ||
.option(' --combined') | ||
.option(' --odata-containment') | ||
@@ -208,4 +207,2 @@ .option(' --odata-proxies') | ||
-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 | ||
@@ -212,0 +209,0 @@ -f, --odata-format <format> Set the format of the identifier rendering |
@@ -10,3 +10,3 @@ | ||
const { | ||
renderFunc, processExprArray, cdsToSqlTypes, | ||
renderFunc, processExprArray, cdsToSqlTypes, getHanaComment, hasHanaComment, | ||
} = require('./utils/common'); | ||
@@ -399,2 +399,12 @@ const { | ||
changeElementsDuplicateChecker.addElement(sqlId, undefined, eltName); | ||
const eltStrOld = def.old.target | ||
? renderAssociationElement(eltName, def.old, env) | ||
: renderElement(artifactName, eltName, def.old, null, null, env); | ||
const eltStrNew = def.new.target | ||
? renderAssociationElement(eltName, def.new, env) | ||
: renderElement(artifactName, eltName, def.new, null, null, env); | ||
if (eltStrNew === eltStrOld) | ||
return; // Prevent spurious migrations, where the column DDL does not change. | ||
if (options.sqlChangeMode === 'drop' || def.old.target || def.new.target || reducesTypeSize(def)) { | ||
@@ -413,4 +423,3 @@ // Lossy change because either an association is removed and/or added, or the type size is reduced. | ||
// Lossless change: no associations directly affected, no size reduction. | ||
const eltStr = renderElement(artifactName, eltName, def.new, null, null, env); | ||
addMigration(resultObj, artifactName, false, render.alterColumns(tableName, eltStr)); | ||
addMigration(resultObj, artifactName, false, render.alterColumns(tableName, eltStrNew)); | ||
} | ||
@@ -431,2 +440,3 @@ } | ||
function renderEntityInto(artifactName, art, resultObj, env) { | ||
env._artifact = art; | ||
const childEnv = increaseIndent(env); | ||
@@ -519,2 +529,5 @@ const hanaTc = art.technicalConfig && art.technicalConfig.hana; | ||
if (options.toSql.dialect === 'hana' && hasHanaComment(art, options, art)) | ||
result += ` COMMENT '${getHanaComment(art)}'`; | ||
resultObj.hdbtable[artifactName] = result; | ||
@@ -618,2 +631,5 @@ } | ||
if (options.toSql.dialect === 'hana' && hasHanaComment(elm, options, env._artifact)) | ||
result += ` COMMENT '${getHanaComment(elm)}'`; | ||
return result; | ||
@@ -1000,4 +1016,9 @@ } | ||
let result = `VIEW ${viewName}`; | ||
if (options.toSql.dialect === 'hana' && hasHanaComment(art, options, art)) | ||
result += ` COMMENT '${getHanaComment(art)}'`; | ||
result += renderParameterDefinitions(artifactName, art.params); | ||
result += ` AS ${renderQuery(artifactName, getNormalizedQuery(art).query, env)}`; | ||
const childEnv = increaseIndent(env); | ||
@@ -1012,2 +1033,3 @@ const associations = Object.keys(art.elements).filter(name => !!art.elements[name].target) | ||
} | ||
return result; | ||
@@ -1014,0 +1036,0 @@ } |
@@ -21,2 +21,5 @@ // Common render functions for toCdl.js, toHdbcds.js and toSql.js | ||
const { implicitAs } = require('../../model/csnRefs'); | ||
/** | ||
@@ -288,2 +291,22 @@ * Render the given function | ||
/** | ||
* Get the element matching the column | ||
* | ||
* @param {CSN.Elements} elements Elements of a query | ||
* @param {CSN.Column} column Column from the same query | ||
* @returns {CSN.Element} | ||
*/ | ||
function findElement(elements, column) { | ||
if (!elements) | ||
return undefined; | ||
if (column.as) | ||
return elements[column.as]; | ||
else if (column.ref) | ||
return elements[implicitAs(column.ref)]; | ||
else if (column.func) | ||
return elements[column.func]; | ||
return undefined; | ||
} | ||
/** | ||
* If there is a context A and a context A.B.C without a definition A.B, create an | ||
@@ -319,2 +342,27 @@ * intermediate context A.B to keep the context hierarchy intact. | ||
/** | ||
* Check wether the given artifact or element has a comment that needs to be rendered. | ||
* Things annotated with @cds.persistence.journal (for HANA SQL), should not get a comment. | ||
* | ||
* @param {CSN.Artifact} obj | ||
* @param {CSN.Options} options To check for `disableHanaComments` | ||
* @param {CSN.Artifact} artifact Artifact containing obj (in case of elem or columns) | ||
* @returns {boolean} | ||
*/ | ||
function hasHanaComment(obj, options, artifact = {}) { | ||
return !artifact['@cds.persistence.journal'] && !options.disableHanaComments && typeof obj.doc === 'string'; | ||
} | ||
/** | ||
* Return the comment of the given artifact or element. | ||
* Uses the first block (everything up to the first empty line (double \n)). | ||
* Remove leading/trailing whitespace. | ||
* | ||
* @param {CSN.Artifact|CSN.Element} obj | ||
* @returns {string} | ||
* @todo Warning/info to user? | ||
*/ | ||
function getHanaComment(obj) { | ||
return obj.doc.split('\n\n')[0].trim().replace(/'/g, "''"); | ||
} | ||
/** | ||
* @typedef CdlRenderEnvironment Rendering environment used throughout the render process. | ||
@@ -343,2 +391,5 @@ * | ||
cdsToSqlTypes, | ||
hasHanaComment, | ||
getHanaComment, | ||
findElement, | ||
}; |
'use strict'; | ||
const { handleMessages, makeMessageFunction } = require('../base/messages'); | ||
const { makeMessageFunction } = require('../base/messages'); | ||
const { isDeprecatedEnabled } = require('../base/model'); | ||
@@ -80,11 +80,2 @@ const transformUtils = require('./transformUtilsNew'); | ||
addLocalizationViews(csn, options); | ||
const keepLocalizedViews = isDeprecatedEnabled(options, 'createLocalizedViews'); | ||
forEachDefinition(csn, (artifact, artifactName) => { | ||
if (hasLocalizedConvenienceView(csn, artifactName)) | ||
artifact.$localized = true; | ||
else if (!keepLocalizedViews && isInLocalizedNamespace(artifactName)) | ||
delete csn.definitions[artifactName]; | ||
}); | ||
// the new transformer works only with new CSN | ||
@@ -103,3 +94,3 @@ checkCSNVersion(csn, options); | ||
recurseElements, setAnnotation, renameAnnotation, | ||
expandStructsInOnConditions | ||
expandStructsInExpression | ||
} = transformers; | ||
@@ -112,3 +103,3 @@ | ||
getServiceName, | ||
hasBoolAnnotation, | ||
hasAnnotationValue, | ||
isAssocOrComposition, | ||
@@ -128,6 +119,21 @@ isAssociation, | ||
// use the array when there is a need to identify if an artifact is in a service or not | ||
let services = getServiceNames(csn); | ||
const services = getServiceNames(csn); | ||
// @ts-ignore | ||
const externalServices = services.filter(serviceName => csn.definitions[serviceName]['@cds.external']); | ||
// @ts-ignore | ||
const isExternalServiceMember = (_art, name) => externalServices.includes(getServiceName(name)); | ||
addLocalizationViews(csn, options); | ||
const keepLocalizedViews = isDeprecatedEnabled(options, 'createLocalizedViews'); | ||
forEachDefinition(csn, (artifact, artifactName) => { | ||
if (hasLocalizedConvenienceView(csn, artifactName)) | ||
artifact.$localized = true; | ||
else if (!keepLocalizedViews && isInLocalizedNamespace(artifactName)) | ||
delete csn.definitions[artifactName]; | ||
}, { skipArtifact: isExternalServiceMember }); | ||
validate.forOdata(csn, { | ||
error, warning, info, inspectRef, effectiveType, artifactRef, csn, options, csnUtils, services, getFinalBaseType, isAspect | ||
error, warning, info, inspectRef, effectiveType, artifactRef, csn, options, csnUtils, services, getFinalBaseType, isAspect, isExternalServiceMember | ||
}); | ||
@@ -137,3 +143,3 @@ | ||
// Throw exception in case of errors | ||
handleMessages(csn, options); | ||
throwWithError(); | ||
@@ -151,12 +157,13 @@ // Semantic checks before flattening regarding temporal data | ||
} | ||
}] | ||
}], | ||
{ skipArtifact: isExternalServiceMember } | ||
); | ||
expandToFinalBaseType(csn, transformers, csnUtils, services, options); | ||
expandToFinalBaseType(csn, transformers, csnUtils, services, options, isExternalServiceMember); | ||
// Check if structured elements and managed associations are compared in an ON condition | ||
// Check if structured elements and managed associations are compared in an expression | ||
// and expand these structured elements. This tuple expansion allows all other | ||
// subsequent procession steps (especially a2j) to see plain paths in ON conditions. | ||
// If errors are detected, handleMessages will return from further processing | ||
forEachDefinition(csn, expandStructsInOnConditions); | ||
// subsequent procession steps (especially a2j) to see plain paths in expressions. | ||
// If errors are detected, throwWithError() will return from further processing | ||
forEachDefinition(csn, expandStructsInExpression, { skipArtifact: isExternalServiceMember }); | ||
@@ -172,3 +179,4 @@ // handles reference flattening | ||
// flatten structures | ||
flattenCSN(csn, csnUtils, options, referenceFlattener, error); | ||
// @ts-ignore | ||
flattenCSN(csn, csnUtils, options, referenceFlattener, error, isExternalServiceMember); | ||
// flatten references | ||
@@ -179,3 +187,3 @@ referenceFlattener.flattenAllReferences(csn); | ||
// structure flattener reports errors, further processing is not safe -> throw exception in case of errors | ||
handleMessages(inputModel, options); | ||
throwWithError(); | ||
@@ -187,5 +195,5 @@ // Process associations | ||
if (!structuredOData) | ||
expandStructKeysInAssociations(csn, referenceFlattener, csnUtils); | ||
expandStructKeysInAssociations(csn, referenceFlattener, csnUtils, isExternalServiceMember); | ||
// 2. generate foreign keys for managed associations | ||
generateForeignKeys(csn, options, referenceFlattener, csnUtils, error); | ||
generateForeignKeys(csn, options, referenceFlattener, csnUtils, error, isExternalServiceMember); | ||
@@ -196,3 +204,3 @@ // Apply default type facets as set by options | ||
// composition targets are annotated with @odata.draft.enabled in this step | ||
forEachDefinition(csn, [ setDefaultTypeFacets, processOnCond ]); | ||
forEachDefinition(csn, [ setDefaultTypeFacets, processOnCond ], { skipArtifact: isExternalServiceMember }); | ||
@@ -214,7 +222,3 @@ // Now all artificially generated things are in place | ||
// Ignore if not part of a service | ||
if (!isArtifactInSomeService(defName, services)) { | ||
warning(null, ['definitions', defName], { art: defName }, | ||
'Ignoring annotation “@odata.draft.enabled” because artifact $(ART) is not part of a service'); | ||
} | ||
else { | ||
if (isArtifactInSomeService(defName, services)) { | ||
generateDraftForOdata(def, defName, def, visitedArtifacts); | ||
@@ -225,3 +229,3 @@ } | ||
visitedArtifacts[defName] = true; | ||
}); | ||
}, { skipArtifact: isExternalServiceMember }); | ||
@@ -259,2 +263,3 @@ // Deal with all kind of annotations manipulations here | ||
// - Check for @Analytics.Measure and @Aggregation.default | ||
// @ts-ignore | ||
if (isArtifactInSomeService(defName, services) || isLocalizedArtifactInService(defName, services)) { | ||
@@ -274,6 +279,6 @@ // If the member is an association and the target is annotated with @cds.odata.valuelist, | ||
} | ||
}) | ||
}, { skipArtifact: isExternalServiceMember }) | ||
// Throw exception in case of errors | ||
handleMessages(csn, options); | ||
throwWithError(); | ||
@@ -285,2 +290,3 @@ if (options.testMode) csn = cloneCsn(csn, options); // sort, keep hidden properties | ||
// TODO: Move this to checks? | ||
// @ts-ignore | ||
function checkTemporalAnnotationsAssignment(artifact, artifactName, propertyName, path) { | ||
@@ -422,2 +428,3 @@ // Gather all element names with @cds.valid.from/to/key | ||
forEachMemberRecursively(def, (member) => { | ||
// @ts-ignore | ||
if (member.type && isAssocOrComposition(member.type) && member.on) { | ||
@@ -449,2 +456,3 @@ removeLeadingDollarSelfInOnCondition(member); | ||
// Sanity check | ||
// @ts-ignore | ||
if (!isArtifactInSomeService(artifactName, services)) { | ||
@@ -460,5 +468,7 @@ throw new Error('Expecting artifact to be part of a service: ' + JSON.stringify(artifact)); | ||
// Generate the DraftAdministrativeData projection into the service, unless there is already one | ||
// @ts-ignore | ||
let draftAdminDataProjectionName = `${getServiceOfArtifact(artifactName, services)}.DraftAdministrativeData`; | ||
let draftAdminDataProjection = csn.definitions[draftAdminDataProjectionName]; | ||
if (!draftAdminDataProjection) { | ||
// @ts-ignore | ||
draftAdminDataProjection = createAndAddDraftAdminDataProjection(getServiceOfArtifact(artifactName, services)); | ||
@@ -546,14 +556,9 @@ } | ||
// Barf if the draft node has @odata.draft.enabled itself | ||
if (hasBoolAnnotation(draftNode, '@odata.draft.enabled', true)) { | ||
if (hasAnnotationValue(draftNode, '@odata.draft.enabled', true)) { | ||
error(null, ['definitions', artifactName, 'elements', elemName], 'Composition in draft-enabled entity can\'t lead to another entity with “@odata.draft.enabled”'); | ||
} | ||
// Ignore composition if not part of a service | ||
else if (!getServiceName(elem.target)) { | ||
warning(null, ['definitions', artifactName, 'elements', elemName], { target: elem.target }, | ||
'Ignoring draft node for composition target $(TARGET) because it is not part of a service'); | ||
// Ignore composition if not part of a service or explicitly draft disabled | ||
else if (!getServiceName(elem.target) || hasAnnotationValue(draftNode, '@odata.draft.enabled', false)) { | ||
return; | ||
} | ||
else if (hasBoolAnnotation(draftNode, '@odata.draft.enabled', false)) { | ||
return; | ||
} | ||
else { | ||
@@ -560,0 +565,0 @@ // Generate draft stuff into the target |
@@ -6,3 +6,3 @@ 'use strict'; | ||
const { hasErrors } = require('../base/messages'); | ||
const { cloneCsnDictionary } = require('../json/to-csn'); | ||
const { cloneCsnDictionary } = require('../model/csnUtils'); | ||
const { cleanSymbols } = require('../base/cleanSymbols.js'); | ||
@@ -161,3 +161,3 @@ const { | ||
}, | ||
elements: cloneCsnDictionary(entity.elements, false, options), | ||
elements: cloneCsnDictionary(entity.elements, options), | ||
[_isViewForEntity]: true, | ||
@@ -192,3 +192,3 @@ }; | ||
if (!shouldUseJoin) { | ||
return createColumnRef( [ entityName ], 'L', false ); | ||
return createColumnRef( [ entityName ], 'L'); | ||
} | ||
@@ -199,3 +199,3 @@ | ||
args: [ | ||
createColumnRef( [ entityName ], 'L_0', false ), | ||
createColumnRef( [ entityName ], 'L_0'), | ||
createColumnRef( [ textsEntityName(entityName) ], 'localized_1' ), | ||
@@ -243,3 +243,3 @@ ], | ||
convenienceView.elements = cloneCsnDictionary(view.elements, false, options); | ||
convenienceView.elements = cloneCsnDictionary(view.elements, options); | ||
convenienceView[_isViewForView] = true; | ||
@@ -253,3 +253,3 @@ copyLocation(convenienceView, view); | ||
if (view.params) | ||
convenienceView.params = cloneCsnDictionary(view.params, false, options); | ||
convenienceView.params = cloneCsnDictionary(view.params, options); | ||
@@ -345,3 +345,3 @@ return convenienceView; | ||
info( null, artPath, { name: artName }, | ||
`Skipped creation of convenience view for $(NAME) because the artifact is missing localization elements` ); | ||
'Skipped creation of convenience view for $(NAME) because the artifact is missing localization elements' ); | ||
return null; | ||
@@ -355,3 +355,3 @@ } | ||
info( null, artPath, { name: artName }, | ||
`Skipped creation of convenience view for $(NAME) because its texts entity could not be found` ); | ||
'Skipped creation of convenience view for $(NAME) because its texts entity could not be found' ); | ||
return null; | ||
@@ -362,3 +362,3 @@ } | ||
info( null, [ 'definitions', textsName ], { name: artName }, | ||
`Skipped creation of convenience view for $(NAME) because its texts entity does not appear to be valid` ); | ||
'Skipped creation of convenience view for $(NAME) because its texts entity does not appear to be valid' ); | ||
return null; | ||
@@ -619,9 +619,6 @@ } | ||
* @param {string} [as] Alias for path. | ||
* @param {boolean} [withEnv=true] If true, add `$env:1` | ||
* @return {CSN.Column} | ||
*/ | ||
function createColumnRef(ref, as = null, withEnv = true) { | ||
function createColumnRef(ref, as = null) { | ||
const column = { ref }; | ||
if (withEnv) | ||
setProp(column, '$env', 1); | ||
if (as) | ||
@@ -647,5 +644,3 @@ column.as = as; | ||
.map(elementName => { | ||
const column = { ref: [ entityName, elementName ] } | ||
setProp(column, '$env', 1); | ||
return column; | ||
return { ref: [ entityName, elementName ] }; | ||
}); | ||
@@ -652,0 +647,0 @@ } |
@@ -20,3 +20,3 @@ 'use strict'; | ||
*/ | ||
module.exports = function (csn, referenceFlattener, csnUtils) { | ||
module.exports = function (csn, referenceFlattener, csnUtils, isExternalServiceMember) { | ||
@@ -27,3 +27,3 @@ forEachManagedAssociation(csn, (element) => { | ||
} | ||
}); | ||
}, isExternalServiceMember); | ||
@@ -30,0 +30,0 @@ // update paths and resolve references |
@@ -21,3 +21,3 @@ 'use strict'; | ||
*/ | ||
module.exports = function (csn, options, referenceFlattener, csnUtils, error) { | ||
module.exports = function (csn, options, referenceFlattener, csnUtils, error, isExternalServiceMember) { | ||
@@ -28,3 +28,3 @@ const structuredOData = options.toOdata.odataFormat === 'structured' && options.toOdata.version === 'v4'; | ||
// sort all associations by their dependencies | ||
const sortedAssociations = sortByAssociationDependency(csn, referenceFlattener); | ||
const sortedAssociations = sortByAssociationDependency(csn, referenceFlattener, isExternalServiceMember); | ||
@@ -31,0 +31,0 @@ // generate foreign keys |
@@ -212,3 +212,6 @@ const { forEachRef } = require('../../model/csnUtils'); | ||
// is there a better way to detect if we are dealing with on-cond? | ||
if (!['on', '$self', null].includes(node.$scope)) return; | ||
// TODO: please do not use inspectRef() results for "where am I?" info | ||
if (!['$self', 'parent'].includes(node.$scope)) return; | ||
// the above condition means: only handle references starting with | ||
// $self or element references outside queries | ||
@@ -215,0 +218,0 @@ let { links } = inspectRef(path); |
@@ -21,3 +21,3 @@ 'use strict'; | ||
function buildDependenciesFor(csn, referenceFlattener) { | ||
function buildDependenciesFor(csn, referenceFlattener, isExternalServiceMember) { | ||
@@ -51,3 +51,3 @@ let dependencies = {}; | ||
}) // forEachMemberRecursively | ||
}) // forEachDefinition | ||
}, { skipArtifact: isExternalServiceMember }) // forEachDefinition | ||
@@ -54,0 +54,0 @@ let result = []; // final list of sorted association items |
@@ -54,3 +54,2 @@ 'use strict'; | ||
* Each generated element gets hidden attributes: | ||
* - $viaTransform - states that the element was generated during transformation | ||
* - _flatElementNameWithDots - names in the element path concatenated with dot | ||
@@ -63,5 +62,5 @@ * @param {CSN.Model} csn CSN-object to flatten | ||
*/ | ||
function flattenCSN(csn, csnUtils, options, referenceFlattener, error) { | ||
function flattenCSN(csn, csnUtils, options, referenceFlattener, error, isExternalServiceMember) { | ||
forEachDefinition(csn, (def, defName, propertyName, path) => | ||
flattenDefinition(def, path, csnUtils, options, referenceFlattener, error)); | ||
flattenDefinition(def, path, csnUtils, options, referenceFlattener, error), { skipArtifact: isExternalServiceMember }); | ||
} | ||
@@ -166,3 +165,2 @@ | ||
if (!isTopLevelElement) { | ||
setProp(newElement, '$viaTransform', true); | ||
setProp(newElement, '_flatElementNameWithDots', elementNameWithDots); | ||
@@ -169,0 +167,0 @@ } |
'use strict'; | ||
const { | ||
cloneCsn, forEachDefinition, | ||
forEachMemberRecursively, isBuiltinType, | ||
forEachDefinition, forEachMemberRecursively, | ||
isBuiltinType, cloneCsnDictionary, | ||
} = require('../../model/csnUtils'); | ||
const { isArtifactInSomeService, isArtifactInService } = require('./utils'); | ||
function expandToFinalBaseType(csn, transformers, csnUtils, services, options) { | ||
function expandToFinalBaseType(csn, transformers, csnUtils, services, options, isExternalServiceMember) { | ||
const isV4 = options.toOdata.version === 'v4'; | ||
@@ -63,3 +63,3 @@ forEachDefinition(csn, (def, defName) => { | ||
} | ||
}); | ||
}, { skipArtifact: isExternalServiceMember }); | ||
@@ -74,3 +74,3 @@ // In case we have in the model something like: | ||
if (csnUtils.isStructured(finalType)) { | ||
def.items.elements = cloneCsn(finalType.elements, options); | ||
def.items.elements = cloneCsnDictionary(finalType.elements, options); | ||
delete def.items.type; | ||
@@ -77,0 +77,0 @@ } |
@@ -11,3 +11,3 @@ 'use strict'; | ||
const { defNameWithoutServiceOrContextName, isArtifactInService } = require('./utils'); | ||
const { cloneCsn, isBuiltinType, forEachDefinition, forEachMember } = require('../../model/csnUtils'); | ||
const { cloneCsn, isBuiltinType, forEachDefinition, forEachMember, cloneCsnDictionary } = require('../../model/csnUtils'); | ||
const { copyAnnotations } = require('../../model/csnUtils'); | ||
@@ -171,3 +171,3 @@ | ||
* in case that the element has a named type. | ||
* | ||
* | ||
* @param {string} typeName type of the element | ||
@@ -201,3 +201,3 @@ * @param {string} service current service name | ||
* in case that the element has an anonymous type. | ||
* | ||
* | ||
* @param {string} typeName type of the element | ||
@@ -218,3 +218,3 @@ * @param {string} parentName name of the parent def holding the element | ||
* Tf does not exists, create a context with the given name in the CSN | ||
* @param {string} name | ||
* @param {string} name | ||
*/ | ||
@@ -330,3 +330,3 @@ function createSchema(name) { | ||
if (csnUtils.isStructured(finalType)) { | ||
if (!def.items.elements) def.items.elements = cloneCsn(finalType.elements, options); | ||
if (!def.items.elements) def.items.elements = cloneCsnDictionary(finalType.elements, options); | ||
delete def.items.type; | ||
@@ -333,0 +333,0 @@ } |
@@ -25,3 +25,3 @@ const { | ||
function forEachManagedAssociation(csn, callback) { | ||
function forEachManagedAssociation(csn, callback, isExternalServiceMember) { | ||
@@ -34,3 +34,3 @@ forEachDefinition(csn, (def) => { | ||
}) | ||
}) | ||
}, { skipArtifact: isExternalServiceMember }); | ||
@@ -37,0 +37,0 @@ } |
@@ -25,3 +25,3 @@ 'use strict'; | ||
getFinalBaseType, | ||
hasBoolAnnotation, | ||
hasAnnotationValue, | ||
inspectRef, | ||
@@ -66,3 +66,3 @@ isStructured, | ||
setAnnotation, | ||
expandStructsInOnConditions, | ||
expandStructsInExpression, | ||
}; | ||
@@ -273,3 +273,2 @@ | ||
delete flatElem.notNull; | ||
setProp(flatElem, '$viaTransform', true); // FIXME: This name is not ideal but used elsewhere, too) | ||
setProp(flatElem, '_flatElementNameWithDots', elementPath.concat(childName).join('.')); | ||
@@ -309,3 +308,4 @@ addGeneratedFlattenedElement(flatElem, flatElemName); | ||
} catch (e) { | ||
if (e.message && e.message === "Scope 'ref-where' but no entity was provided.") { | ||
if (e.message && e.message === "Scope 'ref-target' but no entity was provided.") { | ||
// TODO: should not happen anymore | ||
return flatten(ref, path); | ||
@@ -385,2 +385,4 @@ } else { | ||
if (finalBaseType.elements) { | ||
// ATTENTION: This should be cloneCsnDictionary() but that would | ||
// change the foreign key order in many tests! | ||
node.elements = cloneCsn(finalBaseType.elements, options); // copy elements | ||
@@ -773,9 +775,9 @@ delete node.type; // delete the type reference as edm processing does not expect it | ||
let validFroms = [], validTos = [], validKeys = []; | ||
if (hasBoolAnnotation(element, '@cds.valid.from')) { | ||
if (hasAnnotationValue(element, '@cds.valid.from')) { | ||
validFroms.push({ element, path: [...path] }); | ||
} | ||
if (hasBoolAnnotation(element, '@cds.valid.to')) { | ||
if (hasAnnotationValue(element, '@cds.valid.to')) { | ||
validTos.push({ element, path: [...path] }); | ||
} | ||
if (hasBoolAnnotation(element, '@cds.valid.key')) { | ||
if (hasAnnotationValue(element, '@cds.valid.key')) { | ||
validKeys.push({ element, path: [...path] }); | ||
@@ -865,3 +867,3 @@ } | ||
} | ||
if (annotation == undefined) { | ||
if (annotation === undefined) { | ||
throw Error('Annotation ' + fromName + ' not found in ' + JSON.stringify(node)); | ||
@@ -1015,9 +1017,10 @@ } | ||
/* | ||
* Expand structured ON condition arguments to flat reference paths. | ||
* Expand structured expression arguments to flat reference paths. | ||
* Structured elements are real sub element lists and managed associations. | ||
* All unmanaged association definitions are rewritten if applicable (elements/mixins). | ||
* Also, HAVING and WHERE clauses are rewritten. | ||
* | ||
* TODO: Check if can be skipped for abstract entity and or cds.persistence.skip ? | ||
*/ | ||
function expandStructsInOnConditions(artifact, artifactName, prop, path) { | ||
function expandStructsInExpression(artifact, artifactName, prop, path) { | ||
forEachMemberRecursively(artifact, | ||
@@ -1033,10 +1036,15 @@ (elem, elemName, prop, path) => { | ||
if(artifact.query) { | ||
forAllQueries(artifact.query, (query) => { | ||
if(query.SELECT && query.SELECT.mixin) { | ||
forEachGeneric(query.SELECT, 'mixin', (mixin, mixinName, prop, path) => { | ||
if(mixin.target && mixin.on) { | ||
mixin.on = expand(mixin.on, path); | ||
} | ||
}, | ||
forAllQueries(artifact.query, (query, queryPath) => { | ||
if(query.SELECT) { | ||
if(query.SELECT.mixin) | ||
forEachGeneric(query.SELECT, 'mixin', (mixin, mixinName, prop, path) => { | ||
if(mixin.target && mixin.on) { | ||
mixin.on = expand(mixin.on, path); | ||
} | ||
}, | ||
path); | ||
if(query.SELECT.having) | ||
query.SELECT.having = expand(query.SELECT.having, queryPath.concat(['SELECT', 'having'])) | ||
if(query.SELECT.where) | ||
query.SELECT.where = expand(query.SELECT.where, queryPath.concat(['SELECT', 'where'])) | ||
} | ||
@@ -1043,0 +1051,0 @@ }, path.concat([ 'query' ])); |
@@ -98,3 +98,8 @@ 'use strict'; | ||
const ignoreTimeTrace = { | ||
start: () => { /* ignore */ }, | ||
stop: () => { /* ignore */ }, | ||
}; | ||
const doTimeTrace = process && process.env && process.env.CDSC_TIMETRACING !== undefined; | ||
module.exports = doTimeTrace ? new TimeTracer() : { start: () => {}, stop: () => {} }; | ||
module.exports = doTimeTrace ? new TimeTracer() : ignoreTimeTrace; |
{ | ||
"name": "@sap/cds-compiler", | ||
"version": "2.4.4", | ||
"version": "2.5.0", | ||
"description": "CDS (Core Data Services) compiler and backends", | ||
@@ -14,2 +14,3 @@ "homepage": "https://cap.cloud.sap/", | ||
"main": "lib/main.js", | ||
"types": "lib/main.d.ts", | ||
"scripts": { | ||
@@ -16,0 +17,0 @@ "postinstall": "node lib/fix_antlr4-8_warning.js" |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
3491047
152
69420