@sap/cds-compiler
Advanced tools
Comparing version 1.19.2 to 1.20.3
@@ -25,2 +25,4 @@ #!/usr/bin/env node | ||
const { translatePathLocations } = require('../lib/base/messages') | ||
const { translateAssocsToJoins } = require('../lib/transform/translateAssocsToJoins'); | ||
const term = require('../lib/utils/term'); | ||
@@ -78,3 +80,6 @@ // Note: Instead of throwing ProcessExitError, we would rather just call process.exit(exitCode), | ||
} | ||
// Default color omde is 'auto' | ||
term.useColor(cmdLine.options.color || 'auto'); | ||
// Do the work for the selected command (default 'toCsn') | ||
@@ -135,26 +140,8 @@ executeCommandLine(cmdLine.command || 'toCsn', cmdLine.options, cmdLine.args, cmdLine.unknownOptions); | ||
// special case when we want to call a transformer directly | ||
// without compiling before that (for now only available for toOdata, toHana, toSql) | ||
// works only with combination of --beta-mode in the transformers | ||
if (options.skipCompile && ['toOdata', 'toHana', 'toSql'].includes(command)) { | ||
if (args.length !== 1) | ||
throw new Error(`--skip-compile option works with only one file`); | ||
let csn = ''; | ||
try { | ||
csn = JSON.parse(fs.readFileSync(args[0])); | ||
} catch (e) { | ||
throw new Error(`${args[0]} must be a valid json file`); | ||
} | ||
options.betaMode = true; | ||
csn.options = options; | ||
commands[command](csn); | ||
} else { | ||
const fileCache = Object.create(null) | ||
compiler.compile( args, undefined, options, fileCache ) | ||
.then( commands[command] ) | ||
.then( displayMessages, displayErrors ) | ||
.catch( catchErrors ); | ||
compiler.compile( args, undefined, options ) | ||
.then( commands[command] ) | ||
.then( displayMessages, displayErrors ) | ||
.catch( catchErrors ); | ||
} | ||
if (unknownOptions.length) { | ||
@@ -187,8 +174,8 @@ let out = process.stdout; | ||
let hanaResult; | ||
if(options.newTransformers) { | ||
if(options.oldTransformers) { | ||
hanaResult = compiler.toHana(model); | ||
} else { | ||
let csn = compactModel(model, options); | ||
csn.messages = model.messages; // pass messages as compactModel misses that | ||
hanaResult = toHanaWithCsn(csn, options) | ||
} else { | ||
hanaResult = compiler.toHana(model); | ||
} | ||
@@ -206,8 +193,8 @@ for (let name in hanaResult.hdbcds) { | ||
let odataResult; | ||
if(options.newTransformers) { | ||
if(options.oldTransformers) { | ||
odataResult = compiler.toOdata(model) | ||
} else { | ||
let csn = compactModel(model, options); | ||
csn.messages = model.messages; // pass messages as compactModel misses that | ||
odataResult = toOdataWithCsn(csn, options) | ||
} else { | ||
odataResult = compiler.toOdata(model) | ||
odataResult = toOdataWithCsn(csn, options); | ||
} | ||
@@ -254,3 +241,3 @@ translatePathLocations(model.messages, model); | ||
function toSql( model ) { | ||
let sqlResult = options.newTransformers ? toSqlWithCsn(compactModel(model, options), options) : compiler.toSql(model); | ||
let sqlResult = options.oldTransformers ? compiler.toSql(model) : toSqlWithCsn(compactModel(model, options), options); | ||
@@ -285,3 +272,3 @@ ['hdbtabletype', 'hdbtable', 'hdbindex', 'hdbfulltextindex', 'hdbview', 'sql'].forEach(pluginName => { | ||
if (options.rawOutput) | ||
console.error( util.inspect( reveal( err.model ), false, null )); | ||
console.error( util.inspect( reveal( err.model, options.rawOutput ), false, null )); | ||
else | ||
@@ -309,6 +296,15 @@ displayMessages( err.model, err.errors ); | ||
for (let msg of messages) { | ||
if (options.internalMsg) | ||
if (options.internalMsg) { | ||
console.error( util.inspect( msg, { depth: null, maxArrayLength: null} ) ); | ||
else if (messageLevels[ msg.severity ] <= options.warning) | ||
console.error( compiler.messageString( msg, normalizeFilename, !options.showMessageId ) ); | ||
} else if (messageLevels[ msg.severity ] <= options.warning) { | ||
if (!options.showMessageContext) { | ||
console.error(compiler.messageString(msg, normalizeFilename, !options.showMessageId)); | ||
} else { | ||
console.error(compiler.messageStringMultiline(msg, normalizeFilename, !options.showMessageId)); | ||
const fullfilepath = (msg.location && msg.location.filename )? path.resolve('', msg.location.filename) : undefined; | ||
const sourceLines = fileCache[fullfilepath] ? fileCache[fullfilepath].split('\n') : fileCache; | ||
console.error(compiler.messageContext(sourceLines, msg)); | ||
} | ||
} | ||
} | ||
@@ -333,3 +329,9 @@ } | ||
if (options.rawOutput) { | ||
writeToFileOrDisplay(options.out, name + '_raw.txt', util.inspect(reveal(xsn), false, null), true); | ||
if(options.toCsn && options.toCsn.associations === "joins"){ | ||
options.forHana = {}; | ||
options.forHana.associations = options.toCsn.associations; | ||
writeToFileOrDisplay(options.out, name + '_raw.txt', util.inspect(reveal(translateAssocsToJoins(xsn, options), options.rawOutput), false, null), true); | ||
} else { | ||
writeToFileOrDisplay(options.out, name + '_raw.txt', util.inspect(reveal(xsn, options.rawOutput), false, null), true); | ||
} | ||
} | ||
@@ -336,0 +338,0 @@ else if (options.internalMsg) { |
@@ -16,3 +16,3 @@ #!/usr/bin/env node | ||
const commands = { | ||
complete, lint | ||
complete, find, lint | ||
} | ||
@@ -23,2 +23,3 @@ | ||
const compiler = require('../lib/main'); | ||
const { locationString } = require('../lib/base/messages'); | ||
@@ -31,2 +32,3 @@ let argv = process.argv; | ||
let frel = path.relative( '', file||'' ); | ||
// TODO: proper realname | ||
@@ -41,3 +43,7 @@ if (argv.length > 5 && cmd && line > 0 && column > 0) | ||
console.error( 'ERROR:', err.toString() ); | ||
console.error( 'Usage: cdsse complete <line> <col> <file> [-]' ); | ||
console.error( 'Usage: cdsse <cmd> <line> <col> <file> [-]' ); | ||
console.error( '----------- supported commands <cmd>:' ); | ||
console.error( ' complete: syntactic and semantic code completion' ); | ||
console.error( ' find: location of definition' ); | ||
console.error( ' lint: linter (<line> and <col> are ignored, should be numbers)' ); | ||
process.exitCode = 2; | ||
@@ -86,2 +92,65 @@ return false; | ||
function find( err, buf ) { | ||
if (err) | ||
return usage( err ); | ||
let fname = path.resolve( '', file ); | ||
compiler.compile( [file], '', { lintMode: true, betaMode: true } , { [fname]: buf } ) | ||
.then( select, () => true ); | ||
return true; | ||
function select( xsn ) { | ||
const [src] = Object.values( xsn.sources ); | ||
pathitem( src.usings ); | ||
pathitem( src.extensions ); | ||
pathitem( src.artifacts ); | ||
return true; | ||
} | ||
} | ||
const inspect = { | ||
'_': () => false, | ||
'$': () => false, | ||
id: () => false, | ||
location: () => false, | ||
queries: () => false, // XSN TODO: remove | ||
origin: () => false, // XSN TODO: remove? | ||
elements: (n, p) => (!p || !p.query && !p.columns) && pathitem( n ), | ||
} | ||
function pathitem( node ) { | ||
if (!node || typeof node !== 'object') | ||
return false; | ||
else if (Array.isArray( node )) | ||
return node.some( pathitem ); | ||
else if (!Object.getPrototypeOf( node )) | ||
return Object.values( node ).some( pathitem ); | ||
const { location } = node; | ||
if (!location || node.$inferred || location.$weak) | ||
return false; | ||
if (location.filename !== frel) | ||
return false; | ||
if (location.start.line > line || location.start.line === line && location.start.column > column) | ||
return 'stop'; // 'stop' means: stop search if in array or dictionary | ||
if (location.end.line < line || location.end.line === line && location.end.column < column) { | ||
if (!node.id) | ||
return false; | ||
} | ||
else if (node.id) { | ||
const art = node._artifact; | ||
// TODO: we could skip over some internal artifacts here | ||
if (art && art.location) | ||
console.log( locationString( art.name.location || art.location ) + ': Definition' ); | ||
return true; // true means: stop in array or dictionary or other object | ||
} | ||
let found = false; | ||
for (const prop in node) { | ||
found = (inspect[prop] || inspect[prop.charAt()] || pathitem)( node[prop], node ) || found; | ||
if (found === true) | ||
return found; | ||
} | ||
return found; | ||
} | ||
function lint( err, buf ) { | ||
@@ -88,0 +157,0 @@ if (err) |
@@ -143,1 +143,34 @@ # Error Messages Explained | ||
would provide little benefit, but would add complexity and confusion. | ||
### Redirection issues | ||
The target `OrigTarget` of an existing association can only be redirected to another target `NewTarget` | ||
if the `NewTarget` is a direct or indirect projection of `OrigTarget` | ||
(complex views are questionable and lead to a Warning), | ||
or an entity definition which directly or indirectly includes `OrigTarget`. | ||
``` | ||
entity Base { | ||
key i: Integer; | ||
} | ||
entity Proj as projection on Base; | ||
entity NewTarget as projection on Intermediate; | ||
entity Intermediate as projection on Base; | ||
entity Assocs { | ||
base: association to Base; | ||
proj: association to Proj; | ||
} | ||
entity Redirect as projection on Assocs { | ||
base: redirected to NewTarget, // works | ||
proj: redirected to NewTarget // ERROR: does not originate from Proj | ||
} | ||
``` | ||
For the above CDS code, you get the following error message: | ||
``` | ||
redirect-to-unrelated.cds:16:25-34: Error: The redirected target does not originate from "Proj" | ||
(in entity:"Redirect"/element:"proj") | ||
``` |
@@ -11,3 +11,3 @@ 'use strict'; | ||
const { compactModel, sortCsn } = require('./json/to-csn') | ||
const { toCdsSource } = require('./render/toCdl'); | ||
const { toCdsSource, toCdsSourceCsn } = require('./render/toCdl'); | ||
const { toSqlDdl } = require('./render/toSql'); | ||
@@ -23,2 +23,3 @@ const { toRenameDdl } = require('./render/toRename'); | ||
const { optionProcessor } = require('./optionProcessor') | ||
const { translateAssocsToJoinsCSN } = require('./transform/translateAssocsToJoins'); | ||
@@ -152,6 +153,12 @@ // Transform an augmented CSN 'model' into HANA-compatible CDS source. | ||
if (options.toHana.src) { | ||
result.hdbcds = toCdsSource(forHanaCsn, options); | ||
if(options.testMode){ | ||
const sorted = sortCsn(forHanaCsn, true); | ||
setProp(sorted, "options", forHanaCsn.options); | ||
//const sorted = forHanaCsn; | ||
result.hdbcds = toCdsSourceCsn(sorted, options); | ||
} else { | ||
result.hdbcds = toCdsSourceCsn(forHanaCsn, options); | ||
} | ||
} | ||
if (options.toHana.csn) { | ||
result._augmentedCsn = options.testMode ? sortCsn(forHanaCsn) : forHanaCsn; | ||
result.csn = options.testMode ? sortCsn(forHanaCsn) : forHanaCsn; | ||
@@ -187,2 +194,3 @@ } | ||
// toOdata.version | ||
// toOdata.odataFormat | ||
// toOdata.xml | ||
@@ -867,4 +875,12 @@ // toOdata.json | ||
} | ||
return options.newCsn === false ? (options.testMode ? compactSorted(model) : compact(model)) | ||
let csn = options.newCsn === false ? (options.testMode ? compactSorted(model) : compact(model)) | ||
: compactModel(model); | ||
if(options.toCsn.associations === "joins"){ | ||
options.forHana = {}; | ||
options.forHana.associations = options.toCsn.associations; | ||
csn = translateAssocsToJoinsCSN(csn, options); | ||
} | ||
return csn; | ||
} | ||
@@ -884,2 +900,3 @@ | ||
version : 'v4', | ||
odataFormat: 'flat', | ||
}, | ||
@@ -886,0 +903,0 @@ toRename: { |
@@ -5,2 +5,4 @@ // Functions and classes for syntax messages | ||
const term = require('../utils/term'); | ||
// For messageIds, where no severity has been provided via code (central def) | ||
@@ -53,4 +55,4 @@ const standardSeverities = { | ||
'expected-type': 'A type or an element of a type is expected here', | ||
'expected-entity': 'An entity, projection or view is expected here', | ||
'expected-source': 'A query source must be an entity or an association to an entity', | ||
'expected-entity': 'A non-abstract entity, projection or view is expected here', | ||
'expected-source': 'A query source must be a non-abstract entity or an association to an entity', | ||
} | ||
@@ -103,3 +105,3 @@ | ||
* Class for individual compile errors. | ||
* | ||
* | ||
* @class CompileMessage | ||
@@ -115,5 +117,5 @@ * @extends {Error} | ||
* @param {any} id The ID of the message - visible as property messageId | ||
* @param {any} home | ||
* @param {any} home | ||
* @param {any} context Some further context information, if needed | ||
* | ||
* | ||
* @memberOf CompileMessage | ||
@@ -306,3 +308,6 @@ */ | ||
// Return message string with location if present | ||
// Return message string with location if present in compact form (i.e. one line) | ||
// | ||
// Example: | ||
// <source>.cds:3:11: Error: cannot find value `nu` in this scope | ||
function messageString( err, normalizeFilename, noMessageId, noHome ) { | ||
@@ -314,4 +319,56 @@ return (err.location ? locationString( err.location, normalizeFilename ) + ': ' : '') + | ||
(!err.home || noHome && err.location && !err.location.$weak ? '' : ' (in ' + err.home + ')'); | ||
} | ||
// Return message string with location if present. | ||
// | ||
// Example: | ||
// Error: cannot find value `nu` in this scope | ||
// <source>.cds:3:11 | ||
// | ||
function messageStringMultiline( err, normalizeFilename, noMessageId, noHome ) { | ||
const msgId = (err.messageId && !noMessageId ? '[' + err.messageId + ']' : ''); | ||
const home = (!err.home || noHome && err.location && !err.location.$weak ? '' : ' (in ' + err.home + ')'); | ||
const severity = err.severity || 'Error'; | ||
const location = (err.location ? '\n=> ' + locationString( err.location, normalizeFilename ) : ''); | ||
return term.asSeverity(severity, severity + msgId) + ': ' + err.message + home + location; | ||
} | ||
// Returns a context (code) string that is human readable (similar to rust's compiler) | ||
// | ||
// Example: | ||
// | | ||
// 3 | num * nu | ||
// | ^^ | ||
// | ||
function messageContext(sourceLines, err) { | ||
if (!err.location || !(err.location instanceof Object)) { | ||
return ""; | ||
} | ||
const loc = normalizeLocation(err.location); | ||
if (!loc.start) { | ||
return ""; | ||
} | ||
const startLine = loc.start.line; | ||
if (!sourceLines[startLine - 1]) { | ||
return ""; | ||
} | ||
const digits = loc.end ? String(loc.end.line).length : String(loc.start.line).length; | ||
const severity = err.severity || 'Error'; | ||
const indent = ' '.repeat(2 + digits); | ||
const endColumn = loc.end ? loc.end.column : loc.start.column + 1; | ||
let msg = ""; | ||
msg += indent + '| ' + '\n'; | ||
msg += ' ' + startLine + ' | ' + sourceLines[startLine - 1].replace('\t', ' ') + '\n'; | ||
msg += indent + '| ' + term.asSeverity(severity, ' '.repeat(loc.start.column - 1).padEnd(endColumn - 1, '^')) + '\n'; | ||
return msg; | ||
} | ||
// Compare two messages `a` and `b`. Return 0 if they are equal, 1 if `a` is | ||
@@ -344,3 +401,3 @@ // larger than `b`, and -1 if `a` is smaller than `b`. Messages without a location | ||
let r = (name.absolute) ? [ quoted( name.absolute ) ] : []; | ||
if (name.query || name.query != null && art.kind !== 'element' || name.$mixin ) // Yes, omit $query.0 for element - TODO: rename to block | ||
if (name.query || name.query != null && art.kind !== 'element' || name.$mixin ) // Yes, omit $query.0 for element - TODO: rename to block | ||
r.push( (art.kind === 'block' ? 'block:' : 'query:') + (name.query + 1) ); | ||
@@ -354,2 +411,3 @@ if (name.action && omit !== 'action') | ||
if (name.element && omit !== 'element') | ||
// r.push( `${ art.kind }: ${ quoted( name.element )}` ); or even better element:"assoc"/key:"i" same with enum | ||
r.push( (art.kind === 'enum' ? 'enum:' : 'element:') + quoted( name.element ) ); | ||
@@ -414,2 +472,4 @@ return r.join('/'); | ||
messageString, | ||
messageStringMultiline, | ||
messageContext, | ||
searchName, | ||
@@ -416,0 +476,0 @@ getMessageFunction, |
@@ -88,4 +88,7 @@ // | ||
// Like `obj.prop = value`, but not contained in JSON / CSN | ||
// It's important to set enumerable explicitly to false (although 'false' is the default), | ||
// as else, if the property already exists, it keeps the old setting for enumerable. | ||
function setProp (obj, prop, value) { | ||
Object.defineProperty( obj, prop, { value, configurable: true, writable: true } ); | ||
let descriptor = { value, configurable: true, writable: true, enumerable: false }; | ||
Object.defineProperty( obj, prop, descriptor ); | ||
} | ||
@@ -92,0 +95,0 @@ |
@@ -113,3 +113,3 @@ // Consistency checker on model (XSN = augmented CSN) | ||
}, | ||
$weak: { test: isBoolean }, | ||
$weak: { test: isBoolean, parser: true }, | ||
$notFound: { test: isBoolean }, | ||
@@ -174,2 +174,3 @@ }, | ||
foreignKeys: { kind: true, inherits: 'definitions' }, | ||
$keysNavigation: { kind: true, test: TODO }, | ||
foreignKeys_: { kind: true, parser: true, test: TODO }, // TODO: remove | ||
@@ -390,3 +391,3 @@ _foreignKeysIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove | ||
projection: { kind: true, test: TODO }, // TODO: remove in JSON/CDL parser | ||
technicalConfig: { kind: [ 'entity' ], test: TODO }, // TODO: some spec | ||
technicalConfig: { enumerable: () => true, kind: [ 'entity' ], test: TODO }, // TODO: some spec | ||
sequenceOptions: { kind: true, test: TODO }, // hanaFlavor | ||
@@ -393,0 +394,0 @@ targetElement: { kind: true, inherits: 'type' }, // for foreign keys |
@@ -69,6 +69,6 @@ // Compiler functions and utilities shared across all phases | ||
// TODO: delete envFn again and specify some reject for extend/annotate ? | ||
function fns( model, environment = artifactsEnv ) { | ||
const options = model.options || {}; | ||
const message = getMessageFunction( model ); | ||
// TODO: combine envFn and assoc ? | ||
const specExpected = { | ||
@@ -81,3 +81,4 @@ annotation: { useDefinitions: true, noMessage: true }, | ||
type: { reject: rejectNonType }, // TODO: more detailed later (e.g. for enum base type?) | ||
typeOf: { next: '_$next', assoc: false }, // warn | ||
// if we want to disallow assoc nav for TYPE, do not do it her | ||
typeOf: { next: '_$next' }, | ||
include: { reject: rejectNonStruct, envFn: artifactsEnv }, | ||
@@ -88,3 +89,4 @@ context: { reject: rejectNonContext, envFn: artifactsEnv }, | ||
// TODO: dep for (explicit+implicit!) foreign keys | ||
element: { next: '__none_' }, // foreign key element | ||
element: { next: '__none_' }, // TODO: something for technical config - re-think | ||
targetElement: { next: '__none_', assoc: false }, | ||
filter: { next: '_$next', lexical: 'main' }, | ||
@@ -116,2 +118,5 @@ from: { reject: rejectNonSource, assoc: 'from' }, | ||
// TODO: for reject, distinguish between "completely wrong", i.e. assoc | ||
// target is no struct, and "rejected"; the former have _artifact: null, the | ||
// latter the referred one. | ||
function rejectNonConst( art ) { | ||
@@ -143,3 +148,3 @@ return [ 'builtin', 'param', 'const' ].includes( art.kind ) ? undefined : 'expected-const'; | ||
function rejectNonEntity( art ) { | ||
return ([ 'view', 'entity' ].includes( art.kind )) // TODO: also not abstract | ||
return ([ 'view', 'entity' ].includes( art.kind ) && !(art.abstract && art.abstract.val)) | ||
? undefined | ||
@@ -150,3 +155,6 @@ : 'expected-entity'; | ||
function rejectNonTarget( art ) { | ||
return (options.betaMode && art.kind === 'type') ? rejectNonStruct( art ) : rejectNonEntity( art ); | ||
return (options.betaMode && // TODO: delete 'abstract', proper kind: 'aspect' | ||
(art.kind === 'type' || art.kind === 'entity' && art.abstract && art.abstract.val)) | ||
? rejectNonStruct( art ) | ||
: rejectNonEntity( art ); | ||
} | ||
@@ -156,5 +164,6 @@ | ||
if ([ 'view', 'entity' ].includes( art.kind )) | ||
return undefined; | ||
const main = [ ...path ].reverse().find( item => !item._artifact._main ); | ||
if (![ 'view', 'entity' ].includes( main._artifact.kind )) | ||
return (art.abstract && art.abstract.val) ? 'expected-source' : undefined; | ||
const main = [ ...path ].reverse().find( item => !item._artifact._main )._artifact; | ||
// TODO: better error location if error for main | ||
if (![ 'view', 'entity' ].includes( main.kind ) || main.abstract && main.abstract.val) | ||
return 'expected-source'; // orig: 'A source must start at an entity, projection or view'; | ||
@@ -471,3 +480,4 @@ environment( art ); // sets _finalType on art | ||
let art; | ||
const assoc = spec.assoc && (spec.assoc == null || user); | ||
let nav = spec.assoc !== '$keys' && null; // false for '$keys' | ||
const last = path[path.length - 1]; | ||
for (const item of path) { | ||
@@ -482,3 +492,3 @@ if (!item || !item.id) // incomplete AST due to parse error | ||
else { | ||
const env = (spec.envFn || environment)( art, item.location, assoc ); | ||
const env = (spec.envFn || environment)( art, item.location, user, spec.assoc ); | ||
const sub = setLink( item, env && env[item.id] ); | ||
@@ -489,2 +499,26 @@ if (!sub) | ||
return false; | ||
if (nav) { // we have already "pseudo-followed" a managed association | ||
// We currently rely on the check that targetElement references do | ||
// not (pseudo-) follow associations, otherwise potential redirection | ||
// there had to be considered, too. Also, fk refs to sub elements in | ||
// combinations with redirections of the target which directly access | ||
// the potentially renamed sub elements would be really complex. | ||
// With our restriction, no renaming must be considered for item.id. | ||
nav = setTargetReferenceKey( item.id, item ); | ||
} | ||
// Now set an _navigation link for managed assocs in ON condition etc | ||
else if (art && art.target && nav != null) { | ||
// Find the original ref for sub and the original foreign key | ||
// definition. This way, we do not need the foreign keys with | ||
// rewritten target element path, which might not be available at | ||
// this point (rewriteKeys in Resolver Phase 5). If we want to | ||
// follow associations in foreign key definitions, rewriteKeys must | ||
// be moved to the on-demand Resolver Phase 2. | ||
let orig; // for the original target element | ||
for (let o = sub; o; o = o.value && o.value._artifact) | ||
orig = o; | ||
nav = (orig._finalType || orig).$keysNavigation; | ||
nav = setTargetReferenceKey( orig.name.id, item ); | ||
} | ||
art = sub; | ||
@@ -496,2 +530,22 @@ setXref( art, user, item ); | ||
function setTargetReferenceKey( id, item ) { | ||
const node = nav && nav[id]; | ||
nav = null; | ||
if (node) { | ||
if (node._artifact) { | ||
// set the original(!) foreign key for the assoc - the "right" ones | ||
// after rewriteKeys() is the one with the same name.id | ||
setLink( item, node._artifact, '_navigation' ); | ||
if (item === last) | ||
return; | ||
} | ||
else if (item !== last) { | ||
nav = node.$keysNavigation; | ||
return; | ||
} | ||
} | ||
message( null, item.location, user, {}, 'Error', | ||
'You cannot follow associations other than to elements referred to in a managed association\'s key' ); | ||
} | ||
function error( item, env ) { | ||
@@ -498,0 +552,0 @@ if (!spec.next) { // artifact ref |
@@ -72,3 +72,3 @@ 'use strict'; | ||
const edmlib = require('../edm.js')(options); | ||
const Edm = require('../edm.js')(csn, options); | ||
@@ -158,5 +158,5 @@ // annotation preprocessing | ||
// generate the edmx "frame" around the annotations | ||
let schema = new edmlib.Schema(v, serviceName, serviceName, g_annosArray, false); | ||
let service = new edmlib.DataServices(v, schema); | ||
let edm = new edmlib.Edm(v, service); | ||
let schema = new Edm.Schema(v, serviceName, serviceName, g_annosArray, false); | ||
let service = new Edm.DataServices(v, schema); | ||
let edm = new Edm.Edm(v, service); | ||
@@ -166,4 +166,4 @@ // add references for the used vocabularies | ||
if(vocabularyDefinitions[n].used) { | ||
let r = new edmlib.Reference(v, vocabularyDefinitions[n].ref); | ||
r.append(new edmlib.Include(v, vocabularyDefinitions[n].inc)) | ||
let r = new Edm.Reference(v, vocabularyDefinitions[n].ref); | ||
r.append(new Edm.Include(v, vocabularyDefinitions[n].inc)) | ||
edm._defaultRefs.push(r); | ||
@@ -370,3 +370,3 @@ } | ||
if(carrier['@cds.api.ignore']) { | ||
if(carrier['@cds.api.ignore'] || (carrier['@cds.odata.'+options.version+'.ignore'] && options.betaMode)) { | ||
return; | ||
@@ -405,4 +405,4 @@ } | ||
// newAnnosStd: the corresponding <Annotations ...> tag | ||
// for some carriers (service, entity), there can be an alternative target | ||
// altName: name of alternative target | ||
// for some carriers (service, entity), there can be an alternative target (usually the EntitySet) | ||
// alternativeEdmTargetName: name of alternative target | ||
// newAnnosAlt: the corresponding <Annotations ...> tag | ||
@@ -415,3 +415,4 @@ // which one to choose depends on the "AppliesTo" info of the single annotations, so we have | ||
let stdName = edmTargetName; | ||
let altName = null; | ||
let alternativeEdmTargetName = null; | ||
let hasAlternativeCarrier = true; // is the alternative annotation target available in the EDM? | ||
if (carrier.kind === 'entity' || carrier.kind === 'view') { | ||
@@ -421,4 +422,10 @@ // if annotated object is an entity, annotation goes to the EntityType, | ||
appliesTest = (x => x.match(/EntitySet/) && !x.match(/EntityType/)); | ||
// if carrier has an alternate 'entitySetName' use this instead of EdmTargetName | ||
// (see edmPreprocessor.initializeParameterizedEntityOrView(), where parameterized artifacts | ||
// are split into *Parameter and *Type entities and their respective EntitySets are eventually | ||
// renamed. | ||
// (which is the definition key in the CSN and usually the name of the EntityType) | ||
// find last . in name and insert "EntityContainer/" | ||
altName = edmTargetName.replace(/\.(?=[^.]*$)/, '.EntityContainer/'); | ||
alternativeEdmTargetName = (carrier.entitySetName || edmTargetName).replace(/\.(?=[^.]*$)/, '.EntityContainer/'); | ||
hasAlternativeCarrier = carrier.hasEntitySet; | ||
} | ||
@@ -430,9 +437,10 @@ else if (carrier.kind === 'service') { | ||
stdName = edmTargetName + '.EntityContainer'; | ||
altName = edmTargetName; | ||
alternativeEdmTargetName = edmTargetName; | ||
hasAlternativeCarrier = true; // EntityContainer is always available | ||
} | ||
// result objects that holds all the annotation objects to be created | ||
let newAnnosStd = new edmlib.Annotations(v, stdName); // used in closure | ||
let newAnnosStd = new Edm.Annotations(v, stdName); // used in closure | ||
g_annosArray.push(newAnnosStd); | ||
let newAnnosAlt = null; // create only on demand | ||
let newAnnosAlternative = null; // create only on demand | ||
@@ -448,8 +456,9 @@ return function(annotation, appliesTo) { | ||
} | ||
else { | ||
if (!newAnnosAlt) { // only create upon insertion of first anno | ||
newAnnosAlt = new edmlib.Annotations(v, altName); | ||
g_annosArray.push(newAnnosAlt); | ||
// Add <Annotations> to existing carrier only | ||
else if(hasAlternativeCarrier) { | ||
if (!newAnnosAlternative) { // only create upon insertion of first anno | ||
newAnnosAlternative = new Edm.Annotations(v, alternativeEdmTargetName); | ||
g_annosArray.push(newAnnosAlternative); | ||
} | ||
newAnnosAlt.append(annotation); | ||
newAnnosAlternative.append(annotation); | ||
} | ||
@@ -540,3 +549,3 @@ } | ||
// create the <Annotation ...> tag | ||
let newAnno = new edmlib.Annotation(v, termName); | ||
let newAnno = new Edm.Annotation(v, termName); | ||
@@ -648,3 +657,3 @@ // termName may contain a qualifier: @UI.FieldGroup#shippingStatus | ||
if(oTermName == "Core.OperationAvailable" && dTypeName === "Edm.Boolean" && cAnnoValue === null) { | ||
oTarget.append(new edmlib.ValueThing(v, 'Null')); | ||
oTarget.append(new Edm.ValueThing(v, 'Null')); | ||
oTarget._ignoreChildren = true; | ||
@@ -834,3 +843,5 @@ } | ||
} | ||
else { | ||
else if(val === null && dTypeName == null && typeName == 'String') { | ||
dTypeName = 'Edm.String'; | ||
} else { | ||
warningMessage(context, "expected simple value, but found value '" + val + "' with type '" + typeof val + "'"); | ||
@@ -855,3 +866,3 @@ } | ||
function generateRecord(obj, termName, dTypeName, context) { | ||
let newRecord = new edmlib.Record(v); | ||
let newRecord = new Edm.Record(v); | ||
@@ -935,3 +946,3 @@ // first determine what is the actual type to be used for the record | ||
let newPropertyValue = new edmlib.PropertyValue(v, i); | ||
let newPropertyValue = new Edm.PropertyValue(v, i); | ||
// property value can be anything, so delegate handling to handleValue | ||
@@ -952,3 +963,3 @@ handleValue(obj[i], newPropertyValue, termName, dictPropertyTypeName, context); | ||
function generateCollection(annoValue, termName, dTypeName, context) { | ||
let newCollection = new edmlib.Collection(v); | ||
let newCollection = new Edm.Collection(v); | ||
@@ -979,3 +990,3 @@ let innerTypeName = null; | ||
let res = handleExpression(value["="], innerTypeName, context); | ||
let newPropertyPath = new edmlib.ValueThing(v, res.name, res.value ); | ||
let newPropertyPath = new Edm.ValueThing(v, res.name, res.value ); | ||
newPropertyPath.setJSON( { [res.name] : res.value } ); | ||
@@ -994,3 +1005,3 @@ newCollection.append(newPropertyPath); | ||
let res = handleSimpleValue(value, innerTypeName, context); | ||
let newThing = new edmlib.ValueThing(v, res.name, value ); | ||
let newThing = (value === null) ?new Edm.ValueThing(v, 'Null') : new Edm.ValueThing(v, res.name, value ); | ||
newThing.setJSON( { [res.jsonName] : res.value }); | ||
@@ -1025,3 +1036,3 @@ newCollection.append(newThing); | ||
let k = Object.keys(obj)[0]; | ||
return new edmlib.ValueThing(v, k.slice(1), obj[k] ); | ||
return new Edm.ValueThing(v, k.slice(1), obj[k] ); | ||
} | ||
@@ -1033,3 +1044,3 @@ warningMessage(context, "edmJson code contains no special property out of: " + specialProperties); | ||
// name of special property determines element kind | ||
let newElem = new edmlib.Thing(v, subset[0].slice(1)); | ||
let newElem = new Edm.Thing(v, subset[0].slice(1)); | ||
let mainAttribute = null; | ||
@@ -1065,3 +1076,3 @@ | ||
} | ||
newElem.append(new edmlib.ValueThing(v, getTypeName(a), a)); | ||
newElem.append(new Edm.ValueThing(v, getTypeName(a), a)); | ||
} | ||
@@ -1148,3 +1159,3 @@ } | ||
let type = getDictType(dTypeName); | ||
return type && type["$kind"] == "ComplexType"; | ||
return dTypeName == 'Edm.ComplexType' || type && type["$kind"] == "ComplexType"; | ||
} | ||
@@ -1151,0 +1162,0 @@ |
@@ -15,2 +15,5 @@ 'use strict'; | ||
const { getUtils, cloneCsn } = require('../model/csnUtils'); | ||
const { isNewCSN } = require('../json/csnVersion'); | ||
const { CompilationError } = require('../base/messages'); | ||
/* | ||
@@ -25,17 +28,22 @@ OData V2 spec 06/01/2017 PDF version is available from here: | ||
function csn2edm(csn, serviceName, _options) { | ||
return csn2edmAll(csn, _options, serviceName)[serviceName]; | ||
function csn2edm(_csn, serviceName, _options) { | ||
return csn2edmAll(_csn, _options, serviceName)[serviceName]; | ||
} | ||
function csn2edmAll(csn, _options, serviceName=undefined) { | ||
function csn2edmAll(_csn, _options, serviceName=undefined) { | ||
// get us a fresh model copy that we can work with | ||
let csn = cloneCsn(_csn); | ||
const { error, warning, info, signal } = alerts(csn); | ||
if (!isNewCSN(csn, _options)) { | ||
let version = csn.version ? csn.version.csn : csn.$version ? csn.$version : 'not available'; | ||
signal(error`CSN Version not supported, version tag: "${version}", options.newCsn: ${_options.newCsn}`); | ||
throw new CompilationError(csn.messages, csn); | ||
} | ||
const { isBuiltinType } = getUtils(csn); | ||
// get us a fresh model copy that we can work with | ||
let model = cloneCsn(csn); | ||
let rc = Object.create(null); | ||
const { isBuiltinType, hasBoolAnnotation } = getUtils(csn); | ||
let [services, options] = initializeModel(model, _options, signal, error, warning, info); | ||
const Edm = require('./edm.js')(options, signal, error, warning, info); | ||
let [services, options] = initializeModel(csn, _options, signal, error, warning, info); | ||
const Edm = require('./edm.js')(csn, options); | ||
@@ -47,3 +55,2 @@ | ||
let rc = Object.create(null); | ||
if(serviceName) { | ||
@@ -95,16 +102,16 @@ let serviceCsn = services.filter(s => s.name == serviceName)[0]; | ||
*/ | ||
edmUtils.foreach(model.definitions, | ||
edmUtils.foreach(csn.definitions, | ||
a => edmUtils.isEntityOrView(a) && !a.abstract && a.name.startsWith(serviceName + '.'), | ||
a => createEntityTypeAndSet(a, !(options.isV4() && edmUtils.isContainee(a)) && !a.$proxy) | ||
createEntityTypeAndSet | ||
); | ||
// create unbound actions/functions | ||
edmUtils.foreach(model.definitions, a => edmUtils.isActionOrFunction(a) && a.name.startsWith(serviceName + '.'), | ||
edmUtils.foreach(csn.definitions, a => edmUtils.isActionOrFunction(a) && a.name.startsWith(serviceName + '.'), | ||
(options.isV4()) ? createActionV4 : createActionV2); | ||
// create the complex types | ||
edmUtils.foreach(model.definitions, a => edmUtils.isStructuredType(a) && a.name.startsWith(serviceName + '.'), createComplexType); | ||
edmUtils.foreach(csn.definitions, a => edmUtils.isStructuredType(a) && a.name.startsWith(serviceName + '.'), createComplexType); | ||
if(options.isV4()) | ||
{ | ||
edmUtils.foreach(model.definitions, | ||
edmUtils.foreach(csn.definitions, | ||
artifact => edmUtils.isDerivedType(artifact) && | ||
@@ -149,3 +156,3 @@ !edmUtils.isAssociationOrComposition(artifact) && | ||
function createEntityTypeAndSet(entityCsn, createEntitySet=true) | ||
function createEntityTypeAndSet(entityCsn) | ||
{ | ||
@@ -156,3 +163,3 @@ // EntityType attributes are: Name, BaseType, Abstract, OpenType, HasStream | ||
let EntityTypeName = entityCsn.name.replace(namespace, ''); | ||
let EntitySetName = (entityCsn.setName || entityCsn.name).replace(namespace, ''); | ||
let EntitySetName = (entityCsn.entitySetName || entityCsn.name).replace(namespace, ''); | ||
@@ -177,3 +184,3 @@ let [ properties, hasStream ] = createProperties(entityCsn); | ||
if (createEntitySet) | ||
if (entityCsn.hasEntitySet) | ||
{ | ||
@@ -237,3 +244,3 @@ let entitySet = new Edm.EntitySet(v, { Name: EntitySetName, EntityType: fullQualified(EntityTypeName) }, entityCsn); | ||
{ | ||
let definition = model.definitions[rt]; | ||
let definition = csn.definitions[rt]; | ||
if(definition && definition.kind == 'entity' && !definition.abstract) | ||
@@ -282,3 +289,3 @@ { | ||
{ | ||
let defintion = model.definitions[rt]; | ||
let defintion = csn.definitions[rt]; | ||
if(defintion && edmUtils.isEntityOrView(defintion)) | ||
@@ -380,3 +387,4 @@ { | ||
} | ||
else if(!elementCsn['@cds.api.ignore'] || elementCsn['@cds.api.ignore'] !== true) | ||
else if(!hasBoolAnnotation(elementCsn, '@cds.api.ignore', true) && | ||
!(hasBoolAnnotation(elementCsn, '@cds.odata.'+options.version+'.ignore', true) && options.betaMode)) | ||
{ | ||
@@ -403,3 +411,3 @@ // CDXCORE-CDXCORE-173 | ||
if ( options.isV2() && elementCsn['@Core.MediaType'] | ||
|| options.isV4() && isContainerAssoc && !elementCsn.key ) { | ||
|| options.isV4() && isContainerAssoc /*&& !elementCsn.key*/ ) { | ||
hasStream = true; | ||
@@ -474,4 +482,4 @@ // CDXCORE-CDXCORE-177: remove elementCsn from elements dictionary | ||
// differing set names (<T>Parameters => <T>, <T>Type => <T>Set) | ||
let fromEntitySet = ( navigationProperty._csn._parent.setName || fromEntityType).replace(namespace, ''); | ||
let toEntitySet = (navigationProperty._targetCsn.setName || toEntityType).replace(namespace, ''); | ||
let fromEntitySet = ( navigationProperty._csn._parent.entitySetName || fromEntityType).replace(namespace, ''); | ||
let toEntitySet = (navigationProperty._targetCsn.entitySetName || toEntityType).replace(namespace, ''); | ||
@@ -598,3 +606,3 @@ // from and to roles must be distinguishable (in case of self association entity E { toE: association to E; ... }) | ||
{ | ||
let annoEdm = translate.csn2annotationEdm(model, serviceName, options); | ||
let annoEdm = translate.csn2annotationEdm(csn, serviceName, options); | ||
for(let i = 0; i < annoEdm.getSchemaCount(); i++) | ||
@@ -601,0 +609,0 @@ { |
'use strict' | ||
const edmUtils = require('./edmUtils.js'); | ||
const alerts = require('../base/alerts'); | ||
const transformUtils = require('../transform/transformUtilsNew.js'); | ||
const { getUtils } = require('../model/csnUtils'); | ||
module.exports = function (options, signal, error, warning, info) { | ||
module.exports = function (csn, options) { | ||
const { error, info, signal } = alerts(csn); | ||
const { flattenStructuredElement } = transformUtils.getTransformers(csn, '/'); | ||
const { | ||
getFinalTypeDef, | ||
isStructured, | ||
} = getUtils(csn); | ||
class Node | ||
@@ -675,5 +686,18 @@ { | ||
this.append(...properties); | ||
let keys = properties.filter(c => c.isKey).map(c => c.Name); | ||
if(keys.length > 0) | ||
this.set( { _keys: new Key(v, keys) } ); | ||
let keys = properties.filter(c => c.isKey);// .map(c => c.Name); | ||
let keyPaths = []; | ||
// if a key has a complex type, transform the key into an array of paths | ||
keys.forEach(k => { | ||
// TODO: assert !v4? | ||
// why is flattenStructuredElements not working on simple types? | ||
if (isStructured(k._csn.type, csn) || (k._csn.type && getFinalTypeDef(k._csn.type, csn).elements)) { | ||
let flatElems = flattenStructuredElement(k._csn, k.Name); | ||
keyPaths.push(...Object.keys(flatElems)); | ||
} else { | ||
keyPaths.push(k.Name); | ||
} | ||
}); | ||
if(keyPaths.length > 0) | ||
this.set( { _keys: new Key(v, keyPaths) } ); | ||
} | ||
@@ -896,9 +920,2 @@ | ||
} | ||
if(csn.type === 'cds.Composition') | ||
{ | ||
// TODO: to be specified via @sap.on.delete | ||
this.append(new OnDelete(v, { Action: 'Cascade' } ) ); | ||
} | ||
let partner = (csn._partnerCsn.length > 0) ? csn._partnerCsn[0] : csn._constraints._originAssocCsn; | ||
@@ -909,2 +926,3 @@ if(partner && partner['@odata.navigable'] !== false) { | ||
/* | ||
@@ -919,4 +937,11 @@ 1) If this navigation property belongs to an EntityType for a parameterized entity | ||
*/ | ||
if(csn['@odata.contained'] == true || csn.containsTarget) | ||
if(csn['@odata.contained'] == true || csn.containsTarget) { | ||
this.ContainsTarget = true; | ||
} | ||
if(this.ContainsTarget === undefined && csn.type === 'cds.Composition') { | ||
// Delete is redundant in containment | ||
// TODO: to be specified via @sap.on.delete | ||
this.append(new OnDelete(v, { Action: 'Cascade' } ) ); | ||
} | ||
} | ||
@@ -923,0 +948,0 @@ |
@@ -5,2 +5,4 @@ 'use strict'; | ||
const { getUtils, cloneCsn } = require('../model/csnUtils'); | ||
const alerts = require('../base/alerts'); | ||
const edmUtils = require('./edmUtils.js') | ||
@@ -28,3 +30,3 @@ const { | ||
function initializeModel(model, _options, signal, error, warning, info) | ||
function initializeModel(csn, _options) | ||
{ | ||
@@ -34,14 +36,18 @@ if(_options == undefined) | ||
const { error, warning, info, signal } = alerts(csn); | ||
const { | ||
isStructured, | ||
isAssocOrComposition, | ||
} = getUtils(csn); | ||
// make sure options are complete | ||
let options = validateOptions(_options); | ||
// are we working with structured OData or not | ||
//const structuredOData = options.betaModeOdata && options.toOdata.version === 'v4'; | ||
// First attach names to all definitions in the model | ||
forAll(model.definitions, (a, n) => { | ||
// First attach names to all definitions in the model | ||
forAll(csn.definitions, (a, n) => { | ||
a.name = n; | ||
}); | ||
foreach(model.definitions, isActionOrFunction, a => { | ||
foreach(csn.definitions, isActionOrFunction, a => { | ||
forAll(a.params, (p,n) => { | ||
@@ -53,4 +59,4 @@ p.name = n; | ||
// Fetch service objects | ||
let services = Object.keys(model.definitions).reduce((services, artName) => { | ||
let art = model.definitions[artName]; | ||
let services = Object.keys(csn.definitions).reduce((services, artName) => { | ||
let art = csn.definitions[artName]; | ||
if(art.kind === 'service' && !art.abstract) { | ||
@@ -64,19 +70,97 @@ services.push(art); | ||
// Link association targets and spray @odata.contained over untagged compositions | ||
foreach(model.definitions, isStructuredArtifact, linkAssociationTarget); | ||
foreach(csn.definitions, isStructuredArtifact, linkAssociationTarget); | ||
// Create data structures for containments | ||
foreach(model.definitions, isStructuredArtifact, initializeContainments); | ||
foreach(csn.definitions, isStructuredArtifact, initializeContainments); | ||
// Initialize entities with parameters (add Parameter entity) | ||
foreach(model.definitions, isParameterizedEntityOrView, initializeParameterizedEntityOrView); | ||
foreach(csn.definitions, isParameterizedEntityOrView, initializeParameterizedEntityOrView); | ||
// Initialize structures | ||
foreach(model.definitions, isStructuredArtifact, initializeStructure); | ||
foreach(csn.definitions, isStructuredArtifact, initializeStructure); | ||
// Initialize associations | ||
foreach(model.definitions, isStructuredArtifact, initializeAssociation); | ||
foreach(csn.definitions, isStructuredArtifact, initializeAssociation); | ||
// get constraints for associations | ||
foreach(model.definitions, isStructuredArtifact, initializeConstraints); | ||
foreach(csn.definitions, isStructuredArtifact, initializeConstraints); | ||
// create association target proxies | ||
foreach(model.definitions, isStructuredArtifact, redirectDanglingAssociationsToProxyTargets); | ||
foreach(csn.definitions, isStructuredArtifact, redirectDanglingAssociationsToProxyTargets); | ||
foreach(csn.definitions, isStructuredArtifact, determineEntitySet); | ||
return [services, options]; | ||
// link association target to association and add @odata.contained to compositions in V4 | ||
function linkAssociationTarget(struct) { | ||
foreach(struct.elements, isAssociationOrComposition, element => { | ||
if (element._ignore) | ||
return; | ||
if(!element._target) { | ||
let target = csn.definitions[element.target]; | ||
if(target) { | ||
setProp(element, '_target', target); | ||
} | ||
else { | ||
signal(error`Target ${element.target} cannot be found in the model`, [ 'definitions', struct.name, 'elements', element.name ]); | ||
} | ||
} | ||
// TODO: Remove betaMode if finalized | ||
// in V4 tag all compositions to be containments | ||
if(options.betaMode && | ||
options.isV4() && | ||
isComposition(element) && | ||
element['@odata.contained'] === undefined) { | ||
element['@odata.contained'] = true; | ||
} | ||
}); | ||
} | ||
// Perform checks and add attributes for "contained" sub-entities: | ||
// - A container is recognized by having an association/composition annotated with '@odata.contained'. | ||
// - All targets of such associations ("containees") are marked with a property | ||
// '_containerEntity: []', having as value an array of container names (i.e. of entities | ||
// that have a '@odata.contained' association pointing to the containee). Note that this | ||
// may be multiple entities, possibly including the container itself. | ||
// - All associations in the containee pointing back to the container are marked with | ||
// a boolean property '_isToContainerEntity : true', except if the association itself | ||
// has the annotation '@odata.contained' (indicating the top-down link in a hierarchy). | ||
// - Rewrite annotations that would be assigned to the containees entity set for the | ||
// non-containment rendering. If containment rendering is active, the containee has no | ||
// entity set. Instead try to rewrite the annotation in such a way that it is effective | ||
// on the containment navigation property. | ||
function initializeContainments(container) { | ||
foreach(container.elements, isAssociationOrComposition, (element, elementName) => { | ||
if (element._ignore) | ||
return; | ||
if(element['@odata.contained']) { | ||
// Let the containee know its container | ||
// (array because the contanee may contained more then once) | ||
let containee = element._target; | ||
if (!element._target._containerEntity) { | ||
setProp(element._target, '_containerEntity', []); | ||
} | ||
// add container only once per containee | ||
if (!containee._containerEntity.includes(container.name)) { | ||
containee._containerEntity.push(container.name); | ||
// Mark associations in the containee pointing to the container (i.e. to this entity) | ||
for (let containeeElementName in containee.elements) { | ||
let containeeElement = containee.elements[containeeElementName]; | ||
if (containeeElement._target && containeeElement._target.name) { | ||
// If this is an association that points to a container (but is not by itself contained, | ||
// which would indicate the top role in a hierarchy) mark it with '_isToContainer' | ||
if (containeeElement._target.name == container.name && !containeeElement['@odata.contained']) { | ||
setProp(containeeElement, '_isToContainer', true); | ||
} | ||
} | ||
} | ||
} | ||
rewriteContainmentAnnotations(container, containee, elementName); | ||
} | ||
}); | ||
} | ||
// Split an entity with parameters into two entity types with their entity sets, | ||
// one named <name>Parameter and one named <name>Type. Parameter contains Type. | ||
// Containment processing must take place before because it might be that this | ||
// artifact with parameters is already contained. In such a case the existing | ||
// containment chain must be propagated and reused. This requires that the | ||
// containment data structures must be manually added here and rewriteContainmentAnnotations() | ||
// must be called. | ||
function initializeParameterizedEntityOrView(entityCsn, entityName) { | ||
@@ -107,3 +191,3 @@ | ||
name: parameterEntityName, | ||
setName: parameterEntitySetName, | ||
entitySetName: parameterEntitySetName, | ||
kind: 'entity', | ||
@@ -142,6 +226,8 @@ isParamEntity:true, | ||
model.definitions[parameterCsn.name] = parameterCsn; | ||
csn.definitions[parameterCsn.name] = parameterCsn; | ||
// modify the original parameter entity with backlink and new name | ||
csn.definitions[originalEntityName] = entityCsn; | ||
delete csn.definitions[entityCsn.name]; | ||
entityCsn.name = originalEntityName; | ||
entityCsn.setName = originalEntitySetName; | ||
setProp(entityCsn, 'entitySetName', originalEntitySetName); | ||
@@ -154,9 +240,11 @@ // add backlink association | ||
type: 'cds.Association', | ||
_partnerCsn: [], | ||
on: [ { ref: [ 'Parameters', 'Set' ] }, '=', { ref: [ '$self' ] } ] | ||
}; | ||
setProp(entityCsn.elements[backlinkAssocName], '_partnerCsn', []); | ||
setProp(entityCsn.elements[backlinkAssocName], '_target', parameterCsn); | ||
setProp(entityCsn.elements[backlinkAssocName], '_parameterCsn', []); | ||
} | ||
rewriteContainmentAnnotations(parameterCsn, entityCsn, parameterToOriginalAssocName); | ||
} | ||
// Initialize structured artifact (type or entity) 'struct' by doing the | ||
@@ -226,3 +314,2 @@ // following: | ||
initializeActions(struct.actions); | ||
} | ||
@@ -241,27 +328,2 @@ | ||
// link association target to association and add @odata.contained to compositions in V4 | ||
function linkAssociationTarget(struct) { | ||
foreach(struct.elements, isAssociationOrComposition, element => { | ||
if (element._ignore) | ||
return; | ||
if(!element._target) { | ||
let target = model.definitions[element.target]; | ||
if(target) { | ||
setProp(element, '_target', target); | ||
} | ||
else { | ||
signal(error`Target ${element.target} cannot be found in the model`, [ 'definitions', struct.name, 'elements', element.name ]); | ||
} | ||
} | ||
// TODO: Remove betaMode if finalized | ||
// in V4 tag all compositions to be containments | ||
if(options.betaMode && | ||
options.isV4() && | ||
isComposition(element) && | ||
element['@odata.contained'] === undefined) { | ||
element['@odata.contained'] = true; | ||
} | ||
}); | ||
} | ||
// Resolve the association type of 'element' in 'struct' by doing the following: | ||
@@ -292,39 +354,2 @@ // - collect the foreign key elements for the target into attribute 'elements' | ||
// Perform checks and add attributes for "contained" sub-entities: | ||
// - A container is recognized by having an association/composition annotated with '@odata.contained'. | ||
// - All targets of such associations ("containees") are marked with a property | ||
// '_containerEntity: []', having as value an array of container names (i.e. of entities | ||
// that have a '@odata.contained' association pointing to the containee). Note that this | ||
// may be multiple entities, possibly including the container itself. | ||
// - All associations in the containee pointing back to the container are marked with | ||
// a boolean property '_isToContainerEntity : true', except if the association itself | ||
// has the annotation '@odata.contained' (indicating the top-down link in a hierarchy). | ||
function initializeContainments(container) { | ||
foreach(container.elements, isAssociationOrComposition, element => { | ||
if (element._ignore) | ||
return; | ||
if(element['@odata.contained']) { | ||
// Let the containee know its container | ||
// (array because the contanee may contained more then once) | ||
if (!element._target._containerEntity) { | ||
setProp(element._target, '_containerEntity', []); | ||
} | ||
// add container only once per containee | ||
if (!element._target._containerEntity.includes(container.name)) { | ||
element._target._containerEntity.push(container.name); | ||
// Mark associations in the containee pointing to the container (i.e. to this entity) | ||
for (let containeeElementName in element._target.elements) { | ||
let containeeElement = element._target.elements[containeeElementName]; | ||
if (containeeElement._target && containeeElement._target.name) { | ||
// If this is an association that points to a container (but is not by itself contained, | ||
// which would indicate the top role in a hierarchy) mark it with '_isToContainer' | ||
if (containeeElement._target.name == container.name && !containeeElement['@odata.contained']) { | ||
setProp(containeeElement, '_isToContainer', true); | ||
} | ||
} | ||
} | ||
} | ||
} | ||
}); | ||
} | ||
@@ -360,6 +385,2 @@ function initializeConstraints(struct) { | ||
function redirectDanglingAssociationsToProxyTargets(struct) { | ||
const { | ||
isStructured, | ||
isAssocOrComposition, | ||
} = getUtils(model); | ||
@@ -395,7 +416,7 @@ function whatsMyServiceName(n) { | ||
if(myServiceName !== whatsMyServiceName(element._target.name)) { | ||
if(options.betaModeProxy) { | ||
if(options.isStructured() && options.isV4() && options.odataProxies) { | ||
// search for eventually existing proxy | ||
let proxy = element._target.$proxies.filter(p => p.name.startsWith(myServiceName + '.'))[0]; | ||
if(!proxy) { | ||
let name = myServiceName + '.' + element._target.name.split('.').join('_') + '_Proxy_0'; | ||
let name = myServiceName + '.' + element._target.name.split('.').join('_') + '_proxy'; | ||
proxy = { name, kind: 'entity', $proxy: true, elements: Object.create(null) }; | ||
@@ -429,6 +450,11 @@ let hasKeys = false; | ||
} | ||
signal(info`Created proxy entity type "${name}" for unexposed association target "${element._target.name}"`, | ||
// wire up proxy | ||
if(csn.definitions[name] !== undefined) | ||
signal(error`Duplicate proxy entity type "${name}" for unexposed association target "${element._target.name}"`, | ||
['definitions', struct.name, 'elements', element.name]); | ||
// wire up proxy | ||
model.definitions[name] = proxy; | ||
else { | ||
signal(info`Created proxy entity type "${name}" for unexposed association target "${element._target.name}"`, | ||
['definitions', struct.name, 'elements', element.name]); | ||
csn.definitions[name] = proxy; | ||
} | ||
element._target.$proxies.push(proxy); | ||
@@ -442,3 +468,3 @@ } | ||
element._ignore = true; | ||
signal(info`Association is auto-excluded, as target "${element._target.name}" is outside any service`, | ||
signal(warning`No OData navigation property generated for association "${element.name}", as target "${element._target.name}" is outside any service`, | ||
['definitions', struct.name, 'elements', element.name]); | ||
@@ -451,2 +477,51 @@ return; | ||
} | ||
function determineEntitySet(struct) { | ||
// if this is an entity or a view, determine if an entity set is required or not | ||
// 1) must not be a proxy and not a containee in V4 | ||
// No annos are rendered for non-existing EntitySet targets. | ||
if(struct.hasEntitySet === undefined) { | ||
let hasEntitySet = ['entity', 'view'].includes(struct.kind) && !(options.isV4() && edmUtils.isContainee(struct)) && !struct.$proxy; | ||
setProp(struct, 'hasEntitySet', hasEntitySet); | ||
} | ||
} | ||
// If containment in V4 is active, annotations that would be assigned to the containees | ||
// entity set are not renderable anymore. In such a case try to reassign the annotations to | ||
// the containment navigation property. | ||
// Today only Capabilities.*Restrictions are known to be remapped as there exists a CDS | ||
// short cut annotation @readonly that gets expanded and can be safely remapped. | ||
function rewriteContainmentAnnotations(container, containee, assocName) { | ||
// rectify Restrictions to NavigationRestrictions | ||
if(options.isV4() && container['@Capabilities.NavigationRestrictions'] === undefined) { | ||
let navRestr = { | ||
RestrictedProperties: [ | ||
{ | ||
NavigationProperty: assocName | ||
} | ||
] | ||
}; | ||
let hasRestrictions = false; | ||
if(containee['@Capabilities.DeleteRestrictions.Deletable'] !== undefined) { | ||
navRestr.RestrictedProperties[0].DeleteRestrictions = | ||
{ 'Deletable': containee['@Capabilities.DeleteRestrictions.Deletable'] }; | ||
delete containee['@Capabilities.DeleteRestrictions.Deletable']; | ||
hasRestrictions = true; | ||
} | ||
if(containee['@Capabilities.InsertRestrictions.Insertable'] !== undefined) { | ||
navRestr.RestrictedProperties[0].InsertRestrictions = | ||
{ 'Insertable': containee['@Capabilities.InsertRestrictions.Insertable'] }; | ||
delete containee['@Capabilities.InsertRestrictions.Insertable']; | ||
hasRestrictions = true; | ||
} | ||
if(containee['@Capabilities.UpdateRestrictions.Updatable'] !== undefined) { | ||
navRestr.RestrictedProperties[0].UpdateRestrictions = | ||
{ 'Updatable': containee['@Capabilities.UpdateRestrictions.Updatable'] }; | ||
delete containee['@Capabilities.UpdateRestrictions.Updatable']; | ||
hasRestrictions = true; | ||
} | ||
if(hasRestrictions) | ||
container['@Capabilities.NavigationRestrictions'] = navRestr; | ||
} | ||
} | ||
} | ||
@@ -453,0 +528,0 @@ |
@@ -25,2 +25,6 @@ 'use strict'; | ||
options.isV4 = function() { return this.v[1] == true; } | ||
options.isStructured = function() { return options.toOdata.odataFormat === 'structured' } | ||
options.isFlat = function() { return options.toOdata.odataFormat === 'flat' } | ||
return options; | ||
@@ -27,0 +31,0 @@ } |
@@ -82,2 +82,4 @@ let W = require("./walker"); | ||
processLocations(model) | ||
//returns the last element of an array | ||
@@ -88,2 +90,34 @@ function getLastElement(a) { | ||
//converts $location to xsn-location | ||
function toXSNLocation(l) { | ||
let r = { | ||
filename: l.file, | ||
start: {line:l.line, column: l.col}, | ||
end: {line:l.line, column: l.col}, | ||
$weak:true | ||
} | ||
return r; | ||
} | ||
// walks object in depth and replaces location with $location recursively by passing down the $location | ||
function processLocations(NODE,location,path=[]) { | ||
if(typeof NODE != "object") return; | ||
W.forEach(NODE, (name,node) => { | ||
if(!node) return; | ||
if(name=="$location") return; | ||
if(name=="location") return; | ||
let newLocation = location; | ||
if(node.$location) { | ||
newLocation = toXSNLocation(node.$location) | ||
} | ||
processLocations(node,newLocation,path.concat(name)) | ||
if(newLocation && node.location) | ||
node.location=newLocation; | ||
if(node.$location) | ||
delete node.$location; | ||
}) | ||
} | ||
return model; | ||
} // function augment | ||
@@ -93,2 +127,2 @@ | ||
augment | ||
}; | ||
}; |
@@ -529,2 +529,5 @@ // contains query relevant augmentor functions | ||
} | ||
if(X.args) { | ||
R.namedArgs = augmentViewArgs(X.args,iRefPath.concat("args")) | ||
} | ||
U.setAugmented(R) | ||
@@ -531,0 +534,0 @@ return R; |
@@ -14,3 +14,5 @@ function newUtils(model) { | ||
throw Error("Double augmentation: " + path.join("/")); | ||
node.location = this.newLocation(path, which) | ||
let location = this.newLocation(path, which); | ||
if(location !== undefined) | ||
node.location = location; | ||
}, | ||
@@ -24,2 +26,10 @@ | ||
_newFakeLocation() { | ||
return { | ||
filename: "<stdin>.csn", | ||
start: {line:1, column:1}, | ||
$weak: true | ||
} | ||
}, | ||
_newLocation: function (path, which=this.WILO_FULL) { | ||
@@ -33,3 +43,3 @@ | ||
if(locations===undefined) | ||
throw Error("no locations"); | ||
return this._newFakeLocation(); | ||
if(path===undefined) | ||
@@ -74,2 +84,4 @@ throw Error("no path"); | ||
setAugmented: function (node) { | ||
if(node===undefined) | ||
return; | ||
// hidden property "augmented" which prevents recursive augmentation | ||
@@ -76,0 +88,0 @@ if(node===null) |
@@ -169,5 +169,5 @@ // Transform augmented CSN into compact "official" CSN | ||
// Only intended to be used for tests, as no non-enumerable properties are kept. | ||
function sortCsn( csn ) { // only returns enumerable properties | ||
function sortCsn( csn, keepLocation = false ) { // only returns enumerable properties, except for $location, if requested | ||
if (csn instanceof Array) | ||
return csn.map( v => (!v || typeof v !== 'object' ? v : sortCsn(v) ) ); | ||
return csn.map( v => (!v || typeof v !== 'object' ? v : sortCsn(v, keepLocation) ) ); | ||
let r = {}; | ||
@@ -180,12 +180,14 @@ for (let n of Object.keys(csn).sort( compareProperties ) ) { | ||
else if (csnDictionaries.includes(n)) { | ||
r[n] = csnDictionary( val, n === 'definitions' ); | ||
r[n] = csnDictionary( val, n === 'definitions', keepLocation ); | ||
} | ||
else { | ||
r[n] = sortCsn(val); | ||
r[n] = sortCsn(val, keepLocation); | ||
} | ||
} | ||
if (keepLocation && typeof csn === 'object' && csn.$location) | ||
setHidden(r, '$location', csn.$location); | ||
return r; | ||
} | ||
function csnDictionary( csn, sort ) { | ||
function csnDictionary( csn, sort, keepLocation = false ) { | ||
if (!csn || csn instanceof Array) // null or strange CSN | ||
@@ -195,3 +197,3 @@ return csn; | ||
for (let n of (sort) ? Object.keys(csn).sort() : Object.keys(csn)) { | ||
r[n] = sortCsn( csn[n] ); | ||
r[n] = sortCsn( csn[n], keepLocation ); | ||
} | ||
@@ -460,4 +462,6 @@ return r; | ||
else if (typeof node.scope === 'number') { | ||
index = node.scope || length; // artifact refs in CDL have scope:0 in XSN | ||
id = (node.scope ? path.slice(0,index) : path).map( id => id.id ).join('.'); | ||
index = node.scope || // artifact refs in CDL have scope:0 in XSN | ||
terse === false && path.findIndex( i => i.where || i.namedArgs || i.cardinality) + 1 || | ||
length; | ||
id = path.slice( 0, index ).map( id => id.id ).join('.'); | ||
} | ||
@@ -703,6 +707,6 @@ else { // JSON or i18n input (without compiler links) | ||
else if (!node._artifact || node._artifact._main) { // CQL or follow assoc | ||
return extra( addExplicitAs( artifactRef( node, null ), node.name ), node ); | ||
return extra( addExplicitAs( artifactRef( node, false ), node.name ), node ); | ||
} | ||
else // if FROM ref is from USING, we might need an AS | ||
return extra( addExplicitAs( artifactRef( node, null ), node.name, function(id) { | ||
return extra( addExplicitAs( artifactRef( node, false ), node.name, function(id) { | ||
let name = node._artifact.name.absolute; | ||
@@ -774,3 +778,3 @@ let dot = name.lastIndexOf('.'); | ||
function setHidden( obj, prop, value ) { | ||
Object.defineProperty( obj, prop, { value, configurable: true, writable: true } ); | ||
Object.defineProperty( obj, prop, { value, configurable: true, writable: true, enumerable: false } ); | ||
} | ||
@@ -777,0 +781,0 @@ |
@@ -9,2 +9,3 @@ let W = require("../walker") | ||
const $classes = "$classes"; | ||
const magicElements=["$location"]; // accepted on all levels | ||
@@ -126,13 +127,15 @@ function reportError(ctx,path,node,msg) { | ||
if(element===$classes) continue; | ||
if(ctx.options && ctx.options.fuzzyCsnError) | ||
reportError(ctx,epath,node,"{"+className+"}: Unknown element '"+element+"'"); | ||
else | ||
reportInfo(ctx,epath,node,"{"+className+"}: Unknown element '"+element+"'"); | ||
if(!(ctx.options && ctx.options.skipFuzzyDelete)) { | ||
// prepare, copy and delete | ||
if(!Object.getOwnPropertyNames(node).includes($extra)) { | ||
Object.defineProperty(node,$extra,{configurable:true,enumerable:true,value:{}}) | ||
if(!magicElements.includes(element)) { | ||
if(ctx.options && ctx.options.fuzzyCsnError) | ||
reportError(ctx,epath,node,"{"+className+"}: Unknown element '"+element+"'"); | ||
else | ||
reportInfo(ctx,epath,node,"{"+className+"}: Unknown element '"+element+"'"); | ||
if(!(ctx.options && ctx.options.skipFuzzyDelete)) { | ||
// prepare, copy and delete | ||
if(!Object.getOwnPropertyNames(node).includes($extra)) { | ||
Object.defineProperty(node,$extra,{configurable:true,enumerable:true,value:{}}) | ||
} | ||
node[$extra][element]=node[element]; | ||
delete node[element]; | ||
} | ||
node[$extra][element]=node[element]; | ||
delete node[element]; | ||
} | ||
@@ -139,0 +142,0 @@ } |
@@ -44,3 +44,3 @@ // Main entry point for the Research Vanilla CDS Compiler | ||
var { CompilationError, messageString, handleMessages, hasErrors, getMessageFunction } | ||
var { CompilationError, messageString, messageStringMultiline, messageContext, handleMessages, hasErrors, getMessageFunction } | ||
= require('./base/messages'); | ||
@@ -78,2 +78,9 @@ var { promiseAllDoNotRejectImmediately } = require('./base/node-helpers'); | ||
else if (['.json', '.csn'].includes(ext)) { | ||
if(options.stdJsonParser) { | ||
let csn = JSON.parse(source); | ||
let augmentor3 = require("./json/augmentor3"); | ||
let xsn = augmentor3.augment(csn); // in-place | ||
xsn.$frontend = 'json'; | ||
return xsn; | ||
} | ||
let parseCSNfromJson = require("./json/from-json"); | ||
@@ -99,4 +106,10 @@ let model = parseCSNfromJson( source, filename, options ); | ||
function collectSources( filenames, dir ) { | ||
return compile( filenames, dir, { collectSources: true } ); | ||
// Collect all sources for the given files in the given base directory. | ||
// This means that all dependency sources (e.g. `using` statements) are | ||
// loaded and stored. | ||
// This function returns a Promise (i.e. calls compile() which returns | ||
// a promise). The fulfullment value is an object with all sources and | ||
// their dependencies. | ||
function collectSources( filenames, basedir ) { | ||
return compile( filenames, basedir, { collectSources: true } ); | ||
} | ||
@@ -193,9 +206,13 @@ | ||
else { | ||
a.fileContentDict[filename] = source; | ||
let ast = parse( source, rel, parseOptions ); | ||
a.sources[filename] = ast; | ||
ast.filename = rel; | ||
ast.dirname = path.dirname( filename ); | ||
assertConsistency( ast, parseOptions ); | ||
fulfill( ast ); | ||
try { | ||
a.fileContentDict[filename] = source; | ||
let ast = parse( source, rel, parseOptions ); | ||
a.sources[filename] = ast; | ||
ast.filename = rel; | ||
ast.dirname = path.dirname( filename ); | ||
assertConsistency( ast, parseOptions ); | ||
fulfill( ast ); | ||
} catch(e){ | ||
reject(e); | ||
} | ||
} | ||
@@ -440,4 +457,8 @@ }); | ||
} | ||
// else | ||
// sources[filename] = source; | ||
else { // source is a CSN object | ||
let augmentor3 = require("./json/augmentor3.js"); | ||
let xsn = augmentor3.augment(source); // in-place | ||
xsn.$frontend = 'json'; | ||
sources[filename] = xsn; | ||
} | ||
} | ||
@@ -569,2 +590,4 @@ | ||
messageString, | ||
messageStringMultiline, | ||
messageContext, | ||
InvocationError, | ||
@@ -571,0 +594,0 @@ hasErrors, |
// CSN functionality for resolving references | ||
// The functions in this module expect a CSN with valid references. If that is | ||
// not the case, it simply throws an error without any claim for the error | ||
// message to be user-friendly. CSN processors can provide user-friendly error | ||
// messages by calling the Core Compiler in that case. For details, see | ||
// The functions in this module expect a well-formed CSN with valid references. | ||
// If that is not the case, it simply throws an error (which might even be a | ||
// plain TypeError) without any claim for the error message to be | ||
// user-friendly. CSN processors can provide user-friendly error messages by | ||
// calling the Core Compiler in that case. For details, see | ||
// internalDoc/CoreCompiler.md#use-of-the-core-compiler-for-csn-processors. | ||
@@ -35,2 +36,4 @@ | ||
const views = Object.create(null); // cache for views - OK to add it to CSN? | ||
let refCsnPath = []; | ||
let refLinks = null; | ||
return { effectiveType, artifactRef, inspectRef, queryOrMain }; | ||
@@ -97,7 +100,19 @@ | ||
function inspectRef( csnPath, whereEntity ) { | ||
const { obj, parent, query, scope } = analyseCsnPath( csnPath, csn ); | ||
function whereEntity( csnPath, refCsnPathIndex ) { | ||
if (refCsnPath.length !== refCsnPathIndex || | ||
refCsnPath.some( ( prop, idx ) => prop !== csnPath[idx] )) { | ||
// safety: ref-where in ref-where -> first store in locals | ||
const path = csnPath.slice( 0, refCsnPathIndex ); | ||
const links = inspectRef( path ).links; | ||
refCsnPath = path; | ||
refLinks = links; | ||
} | ||
return refLinks[ csnPath[ refCsnPathIndex + 1 ] ].env; | ||
} | ||
function inspectRef( csnPath ) { | ||
const { obj, parent, query, scope, refCsnPathIndex } = analyseCsnPath( csnPath, csn ); | ||
const name = csnPath[1]; | ||
const main = csn.definitions[ name ]; | ||
const queries = views[ name ] || main.query && allQueries( name, main.query ); | ||
const queries = views[ name ] || main.query && allQueries( name, main ); | ||
@@ -109,3 +124,3 @@ const path = (typeof obj === 'string') ? [ obj ] : obj.ref; | ||
// 1,2: | ||
// 1,2: with 'param' or 'global' property, in `keys` | ||
if (obj.param) | ||
@@ -117,3 +132,3 @@ return expandRefPath( path, main.params[ head ], 'param' ); | ||
return expandRefPath( path, csn.definitions[ parent.target ].elements[ head ], 'keys' ); | ||
// 3: | ||
// 3: $magic | ||
if (head.charAt() === '$') { | ||
@@ -128,12 +143,9 @@ if (head === '$self' || head === '$projection') { | ||
} | ||
// 4 (where inside ref - art for environment must be provided as computing | ||
// 4: where inside ref - art for environment must be provided as computing | ||
// it here would be too expensive) | ||
if (scope === 'ref-where'){ | ||
if(!whereEntity){ | ||
// Caller SHOULD catch this and then try again with a correct whereEntity | ||
throw new Error("Scope '" + scope + "' but no entity was provided."); | ||
} | ||
return expandRefPath( path, whereEntity.elements[ head ], scope ); | ||
if (scope === 'ref-where') { | ||
const { elements } = whereEntity( csnPath, refCsnPathIndex ); | ||
return expandRefPath( path, elements[ head ], scope ); | ||
} | ||
// 5,6,7: | ||
// 5,6,7: outside queries, in queries where inferred elements are referred to | ||
if (!query) | ||
@@ -153,7 +165,7 @@ return expandRefPath( path, (parent || main).elements[ head ] ); | ||
else if (typeof obj.$env === 'string') { | ||
const source = queryOrMain( select._sources[ obj.$env ], main ); | ||
const source = select._sources[ obj.$env ]; | ||
return expandRefPath( path, source.elements[ head ], 'source' ); | ||
} | ||
// 8: | ||
// 8: try to search in MIXIN section (not in ON of JOINs) | ||
if (scope !== 'from-on' && select.mixin) { | ||
@@ -164,3 +176,3 @@ const art = select.mixin[ head ]; | ||
} | ||
// 9: | ||
// 9: try to search for table aliases (partially in ON of JOINs) | ||
if (path.length > 1 && (select.$alias || scope !== 'from-on')) { | ||
@@ -171,3 +183,3 @@ const art = select._sources[ head ]; | ||
} | ||
// 10: | ||
// 10: search in elements of source entity | ||
// TODO: do not do this if current query has a parent query !!! | ||
@@ -178,3 +190,3 @@ if (scope === 'orderBy') { | ||
if (typeof select.$alias === 'string') { // with unique source | ||
const source = queryOrMain( select._sources[ select.$alias ], main ); | ||
const source = select._sources[ select.$alias ]; | ||
return expandRefPath( path, source.elements[ head ], 'source' ); | ||
@@ -201,5 +213,5 @@ } | ||
function allQueries( name, topQuery ) { | ||
function allQueries( name, main ) { | ||
const all = []; | ||
traverseQuery( topQuery, null, function memorize( query, select ) { | ||
traverseQuery( main.query, null, function memorize( query, select ) { | ||
if (query.ref) { // ref in from | ||
@@ -212,3 +224,3 @@ const as = query.as || implicitAs( query.ref ); | ||
const as = query.as; | ||
select._sources[ as ] = query; | ||
select._sources[ as ] = queryOrMain( query, main ); | ||
setLink( select, '$alias', | ||
@@ -292,6 +304,6 @@ (select.$alias != null) ? typeof select.$alias === 'string' : as ); | ||
let isName = false; | ||
let refCsnPathIndex = 0; | ||
for (const prop of csnPath) { | ||
// if (isName || Array.isArray( obj )) { // array item, name/index of artifact/member, (named) argument | ||
if (isName || typeof prop !== 'string') { // array item, name/index of artifact/member, (named) argument | ||
csnPath.forEach( function loop( prop, index ) { | ||
if (isName || Array.isArray( obj )) { // array item, name/index of artifact/member, (named) argument | ||
if (typeof isName === 'string') { | ||
@@ -318,2 +330,3 @@ parent = art; | ||
scope = 'ref-where'; | ||
refCsnPathIndex = index - 2; | ||
} | ||
@@ -330,5 +343,5 @@ else if (prop === 'on' && scope === 'from') { | ||
obj = obj[ prop ]; | ||
} | ||
} ); | ||
// console.log( 'CPATH:', csnPath, scope, obj ); | ||
return { obj, parent, query, scope }; | ||
return { obj, parent, query, scope, refCsnPathIndex }; | ||
} | ||
@@ -335,0 +348,0 @@ |
@@ -383,2 +383,4 @@ 'use strict' | ||
return (getFinalBaseType(type.type, path, cycleCheck)); | ||
else if (type.items) | ||
return getFinalBaseType(type.items, path, cycleCheck); | ||
else | ||
@@ -431,2 +433,4 @@ // TODO: this happens if we don't have a type, e.g. an expression in a select list | ||
} | ||
if (node.technicalConfig && !resultNode.technicalConfig) | ||
setProp(resultNode, 'technicalConfig', node.technicalConfig); | ||
@@ -457,2 +461,6 @@ if(node.$env && !resultNode.$env){ | ||
if(construct._ignore || obj._ignore){ | ||
return; | ||
} | ||
if(construct.returns){ | ||
@@ -459,0 +467,0 @@ obj_path.push("returns"); |
@@ -55,3 +55,3 @@ // For testing: reveal non-enumerable properties in CSN, display result of csnRefs | ||
function standard( parent, prop, node, whereEntity ) { | ||
function standard( parent, prop, node ) { | ||
if (!node || typeof node !== 'object' || !{}.propertyIsEnumerable.call( parent, prop )) | ||
@@ -62,3 +62,3 @@ return; | ||
if (node instanceof Array) { | ||
node.forEach( (n, i) => standard( node, i, n, whereEntity ) ); | ||
node.forEach( (n, i) => standard( node, i, n ) ); | ||
} | ||
@@ -68,3 +68,3 @@ else { | ||
const trans = transformers[name] || standard; | ||
trans( node, name, node[name], whereEntity ); | ||
trans( node, name, node[name] ); | ||
} | ||
@@ -104,6 +104,6 @@ } | ||
function pathRef( node, prop, path, whereEntity ) { | ||
function pathRef( node, prop, path ) { | ||
const { links, art, scope } = (() => { | ||
try { | ||
return inspectRef( csnPath, whereEntity ); | ||
return inspectRef( csnPath ); | ||
} | ||
@@ -127,3 +127,3 @@ catch (e) { | ||
if (s.where) | ||
standard( s, 'where', s.where, links[i].env ); | ||
standard( s, 'where', s.where ); | ||
csnPath.pop(); | ||
@@ -130,0 +130,0 @@ } |
@@ -20,3 +20,3 @@ // Make internal properties of the XSN / augmented CSN visible | ||
function revealInternalProperties( model ) { | ||
function revealInternalProperties( model, name ) { | ||
var unique_id = 0; | ||
@@ -42,2 +42,3 @@ | ||
$navigation: dictionary, | ||
$keysNavigation: dictionary, | ||
$combined: artifactDictionary, | ||
@@ -81,3 +82,3 @@ $dictOrderBy: artifactDictionary, | ||
} | ||
return reveal( model ); | ||
return reveal( name && name !== '+' ? { definitions: model.definitions[name] } : model ); | ||
@@ -84,0 +85,0 @@ function artifactIdentifier( node, parent ) { |
@@ -14,2 +14,4 @@ const { createOptionProcessor } = require('./base/optionProcessorHelper'); | ||
.option(' --show-message-id') | ||
.option(' --show-message-context') | ||
.option(' --color <mode>', ['auto', 'always', 'never']) | ||
.option('-o, --out <dir>') | ||
@@ -22,8 +24,9 @@ .option(' --lint-mode') | ||
.option('-E, --enrich-csn') | ||
.option('-R, --raw-output') | ||
.option('-R, --raw-output <name>') | ||
.option(' --internal-msg') | ||
.option(' --beta-mode') | ||
.option(' --new-transformers') | ||
.option(' --old-transformers') | ||
.option(' --new-csn') | ||
.option(' --old-csn') | ||
.option(' --std-json-parser') | ||
.option(' --long-autoexposed') | ||
@@ -55,2 +58,8 @@ .option(' --hana-flavor') | ||
--show-message-id Show message ID in error, warning and info messages | ||
--show-message-context Print messages as human readable text similar to Rust's compiler | ||
(with code excerpt) | ||
--color <mode> Use colors for warnings. Modes are: | ||
auto: (default) Detect color support of the tty. | ||
always: | ||
never: | ||
-o, --out <dir> Place generated files in directory <dir>, default is "-" for <stdout> | ||
@@ -73,8 +82,10 @@ --lint-mode Generate nothing, just produce messages if any (for use by editors) | ||
-E, --enrich-csn Show non-enumerable CSN properties and locations of references | ||
-R, --raw-output Write raw augmented CSN and error output to <stdout>, long! | ||
-R, --raw-output <name> Write XSN for definition "name" and error output to <stdout>, | ||
with name = "+", write complete XSN, long! | ||
--internal-msg Write raw messages with call stack to <stdout>/<stderr> | ||
--beta-mode Enable unsupported, incomplete (beta) features | ||
--new-transformers Use the new transformers that work on CSN instead of XSN | ||
--old-transformers Use the old transformers that work on XSN instead of CSN | ||
--new-csn Produce CSN version 1.0 format (default) | ||
--old-csn Produce CSN version 0.1 format (deprecated, overrides --new-csn) | ||
--std-json-parser Use standard JSON parser for CSN parsing | ||
--hana-flavor Compile with backward compatibility for HANA CDS (incomplete) | ||
@@ -138,2 +149,3 @@ --parse-only Stop compilation after parsing and write result to <stdout> | ||
.option('-c, --csn') | ||
.option('-f, --odata-format <format>', ['flat', 'structured']) | ||
.option('-n, --names <style>', ['plain', 'quoted', 'hdbcds']) | ||
@@ -155,2 +167,6 @@ .help(` | ||
-c, --csn Generate "odata_csn.json" with ODATA-preprocessed model | ||
-f, --odata-format <format> Set the format of the identifier rendering | ||
(ignored by '--old-transformers') | ||
flat : (default) Flat type and property names | ||
structured : (V4 only) Render structured metadata | ||
-n, --names <style> Annotate artifacts and elements with "@cds.persistence.name", which is | ||
@@ -262,2 +278,3 @@ the corresponding database name (see "--names" for "toHana or "toSql") | ||
.option('-f, --flavor <flavor>', ['client', 'gensrc']) | ||
.option('-a, --associations <proc>', ['assocs', 'joins']) | ||
.help(` | ||
@@ -276,2 +293,5 @@ Usage: cdsc toCsn [options] <file...> | ||
backends | ||
-a, --associations <proc> Processing of associations: | ||
assocs : (default) Keep associations in HANA CDS as far as possible | ||
joins : Transform associations to joins | ||
`); | ||
@@ -278,0 +298,0 @@ |
@@ -10,3 +10,2 @@ /** | ||
const walker = require("../json/walker") | ||
const { transformLocation } = require('./renderUtil'); | ||
@@ -51,5 +50,5 @@ // database name - uppercase if not quoted | ||
*/ | ||
addArtifact( name, location ){ | ||
addArtifact( name, location, modelName ){ | ||
const dbname = asDBName(name); | ||
this.currentArtifact = { name, location, elements: {} }; | ||
this.currentArtifact = { name, location, elements: {}, modelName }; | ||
if(!this.seenArtifacts[dbname]) | ||
@@ -70,3 +69,3 @@ this.seenArtifacts[dbname] = [this.currentArtifact]; | ||
*/ | ||
addElement(name,location){ | ||
addElement(name,location, modelName){ | ||
if(!this.currentArtifact.elements) | ||
@@ -76,3 +75,3 @@ return; | ||
let currentElements = this.currentArtifact.elements; | ||
const element = {name,location}; | ||
const element = {name,location, modelName}; | ||
if(!currentElements[dbname]) | ||
@@ -93,3 +92,3 @@ currentElements[dbname] = [element]; | ||
artifacts.forEach(artifact => { // report all colliding artifacts | ||
signal(error`Duplicated artifact '${artifactName}', origin: '${artifact.name}'`, transformLocation(artifact.location)); | ||
signal(error`Duplicated artifact '${artifactName}', origin: '${artifact.name}'`, ['definitions', artifact.modelName] ); | ||
}); | ||
@@ -101,3 +100,3 @@ } | ||
elements.forEach(element => { // report all colliding elements | ||
signal(error`Duplicated element '${element.name}' in artifact '${artifact.name}'`, transformLocation(element.location)); | ||
signal(error`Duplicated element '${element.name}' in artifact '${artifact.name}'`, ['definitions', artifact.modelName,'elements', element.modelName]); | ||
}) | ||
@@ -104,0 +103,0 @@ } |
@@ -33,24 +33,5 @@ // Common render functions for toCdl.js and toSql.js | ||
/** | ||
* Get the $location from an object and make it look like the XSN location - delete - not necessary | ||
* | ||
* @param {any} location | ||
*/ | ||
function transformLocation(location){ | ||
return { | ||
start: { | ||
line: location.line, | ||
column: location.col | ||
}, | ||
end: { | ||
line: location.line, | ||
column: location.col | ||
}, | ||
filename: location.file | ||
} | ||
} | ||
module.exports = { | ||
renderFunc, | ||
transformLocation | ||
renderFunc | ||
} |
@@ -6,9 +6,22 @@ "use strict"; | ||
getLastPartOfRef, getParentNamesOf } = require('../model/modelUtils'); | ||
const { compactModel } = require('../json/to-csn'); | ||
const { compactModel, sortCsn } = require('../json/to-csn'); | ||
const keywords = require('../base/keywords'); | ||
const version = require('../../package.json').version; | ||
const alerts = require('../base/alerts'); | ||
const { transformLocation, renderFunc } = require('./renderUtil'); // TODO: transformLocation should not be necessary | ||
const { renderFunc } = require('./renderUtil'); | ||
const DuplicateChecker = require('./DuplicateChecker'); | ||
const { cloneCsn } = require('../model/csnUtils'); | ||
const { setProp } = require('../base/model'); | ||
function toCdsSource(model, options){ | ||
options = mergeOptions(model.options, options); | ||
let csn = compactModel(model); | ||
if (options.testMode) | ||
csn = sortCsn(csn, true); | ||
if(model.messages) | ||
setProp(csn, "messages", model.messages); | ||
return toCdsSourceCsn(csn, options); | ||
} | ||
// Render the CSN model 'model' to CDS source text. One source is created per | ||
@@ -23,4 +36,3 @@ // top-level artifact. Return a dictionary of top-level artifacts | ||
// FIXME: This comment no longer tells the whole truth | ||
function toCdsSource(model, options) { | ||
function toCdsSourceCsn(model, options) { | ||
// Merge options (arguments first, then model options) | ||
@@ -30,5 +42,5 @@ options = mergeOptions(model.options, options); | ||
let hdbcdsNames = options.forHana && options.forHana.names == 'hdbcds'; | ||
// Skip compactModel if already using CSN | ||
const csn = (options.newTransformers) ? model : compactModel(model); | ||
const csn = cloneCsn(model); | ||
@@ -273,3 +285,3 @@ const { signal, warning, error } = alerts(csn); | ||
let normalizedArtifactName = renderArtifactName(artifactName, env); | ||
globalDuplicateChecker && globalDuplicateChecker.addArtifact(normalizedArtifactName, art && art.$location); | ||
globalDuplicateChecker && globalDuplicateChecker.addArtifact(normalizedArtifactName, art && art.$location, artifactName); | ||
result += env.indent + (art.abstract ? 'abstract ' : '') + 'entity ' + normalizedArtifactName; | ||
@@ -284,3 +296,3 @@ let parameters = Object.keys(art.params || []).map(name => renderParameter(name, art.params[name], childEnv)).join(',\n'); | ||
let duplicateChecker = new DuplicateChecker(); // registry for all artifact names and element names | ||
duplicateChecker.addArtifact(artifactName, art && art.$location) | ||
duplicateChecker.addArtifact(artifactName, art && art.$location, artifactName) | ||
for (let name in art.elements) { | ||
@@ -384,3 +396,3 @@ result += renderElement(name, art.elements[name], childEnv, duplicateChecker); | ||
let result = renderAnnotationAssignments(elm, env); | ||
duplicateChecker && elm && duplicateChecker.addElement(quoteOrUppercaseId(elementName), elm && elm.$location); | ||
duplicateChecker && elm && duplicateChecker.addElement(quoteOrUppercaseId(elementName), elm && elm.$location, elementName); | ||
result += env.indent + (elm.virtual ? 'virtual ' : '') | ||
@@ -563,3 +575,3 @@ + (elm.key ? 'key ' : '') | ||
// Return the resulting source string (no trailing LF). | ||
function renderViewColumn(col, env) { | ||
function renderViewColumn(col, env, path) { | ||
// Ignore if toHana says so | ||
@@ -580,3 +592,3 @@ // FIXME: This probably needs to change ... | ||
if(col.key && env.skipKeys){ | ||
signal(error`KEY must only be added in the first query of a UNION`, transformLocation(col.$location)); | ||
signal(error`KEY must only be added in the first query of a UNION`, path); | ||
} | ||
@@ -633,3 +645,3 @@ const key = (!env.skipKeys && (col.key || (options.forHana && leaf && env._artifact.elements[leaf] && env._artifact.elements[leaf].key)) ? 'key ' : ''); | ||
env._artifact = art; | ||
result += renderQuery(art.query, true, syntax, env); | ||
result += renderQuery(art.query, true, syntax, env, ['definitions',artifactName,'query']); | ||
result += ';\n'; | ||
@@ -645,3 +657,3 @@ result += renderQueryElementAnnotations(artifactName, art, env); | ||
// or 'entity') | ||
function renderQuery(query, isLeadingQuery, syntax, env) { | ||
function renderQuery(query, isLeadingQuery, syntax, env, path=[]) { | ||
let result = ''; | ||
@@ -651,3 +663,3 @@ // Set operator, like UNION, INTERSECT, ... | ||
// First arg may be leading query | ||
result += `(${renderQuery(query.SET.args[0], isLeadingQuery, 'view', env)}` | ||
result += `(${renderQuery(query.SET.args[0], isLeadingQuery, 'view', env, path.concat(['SET','args',0]))}` | ||
// FIXME: Clarify if set operators can be n-ary (assuming binary here) | ||
@@ -661,3 +673,3 @@ if (query.SET.op) { | ||
for(let i = 1; i < query.SET.args.length; i++){ | ||
result += `\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} ${renderQuery(query.SET.args[i], false, 'view', env)}`; | ||
result += `\n${env.indent}${query.SET.op}${query.SET.all ? ' all' : ''} ${renderQuery(query.SET.args[i], false, 'view', env, path.concat(['SET','args',i]))}`; | ||
} | ||
@@ -707,3 +719,3 @@ // Reset afterwards | ||
result += ' {\n' + | ||
(select.columns||['*']).map(col => renderViewColumn(col, childEnv)) | ||
(select.columns||['*']).map((col, index) => renderViewColumn(col, childEnv, path.concat('SELECT','columns',index))) | ||
.filter(s => s != '') | ||
@@ -1492,2 +1504,2 @@ .join(',\n') + '\n'; | ||
module.exports = { toCdsSource }; | ||
module.exports = { toCdsSource, toCdsSourceCsn }; |
@@ -9,3 +9,3 @@ | ||
const version = require('../../package.json').version; | ||
const { renderFunc } = require('./renderUtil'); // TODO: transformLocation should not be necessary | ||
const { renderFunc } = require('./renderUtil'); | ||
const DuplicateChecker = require("./DuplicateChecker"); | ||
@@ -213,3 +213,3 @@ | ||
let tableName = quoteSqlId(absoluteCdsName(artifactName)); | ||
duplicateChecker.addArtifact(tableName, art && art.$location) | ||
duplicateChecker.addArtifact(tableName, art && art.$location, artifactName) | ||
result += 'TABLE ' + tableName; | ||
@@ -290,3 +290,3 @@ result += ' (\n'; | ||
let quotedElementName = quoteSqlId(elementName); | ||
duplicateChecker.addElement(quotedElementName, elm && elm.$location); | ||
duplicateChecker.addElement(quotedElementName, elm && elm.$location, elementName); | ||
let result = env.indent + quotedElementName + ' ' | ||
@@ -553,3 +553,3 @@ + renderTypeReference(artifactName, elementName, elm) | ||
let viewName = quoteSqlId(absoluteCdsName(artifactName)); | ||
duplicateChecker.addArtifact(viewName, art && art.$location) | ||
duplicateChecker.addArtifact(viewName, art && art.$location, artifactName) | ||
let result = 'VIEW ' + viewName; | ||
@@ -686,3 +686,3 @@ result += renderParameterDefinitions(artifactName, art.params); | ||
let typeName = quoteSqlId(absoluteCdsName(artifactName)); | ||
duplicateChecker.addArtifact(typeName, art && art.$location) | ||
duplicateChecker.addArtifact(typeName, art && art.$location, artifactName) | ||
let result = 'TYPE ' + quoteSqlId(absoluteCdsName(artifactName)) + ' AS TABLE (\n'; | ||
@@ -893,3 +893,3 @@ let childEnv = increaseIndent(env); | ||
if(options.forHana.dialect === 'sqlite') { | ||
return "current_time"; | ||
return "current_timestamp"; | ||
} | ||
@@ -896,0 +896,0 @@ else if(options.forHana.dialect === 'hana') { |
@@ -33,3 +33,3 @@ 'use strict'; | ||
function transform4odata(inputModel, options) { | ||
const { error, warning, signal } = alerts(inputModel); | ||
const { error, warning, info, signal } = alerts(inputModel); | ||
let model; | ||
@@ -165,2 +165,5 @@ | ||
// key fields in types we have already processed. | ||
// Check that each service has max one draft root | ||
// This must be done before draft processing, since all composition targets | ||
// are annotated with @odata.draft.enabled in that step | ||
forEachDefinition(model, (artifact) => { | ||
@@ -196,2 +199,12 @@ forEachMemberRecursively(artifact, (member) => { | ||
}); | ||
if (artifact._service && (artifact.kind == 'entity' || artifact.kind == 'view') && hasBoolAnnotation(artifact, '@odata.draft.enabled')) { | ||
let location = artifact['@odata.draft.enabled'].name && artifact['@odata.draft.enabled'].name.location; | ||
if(artifact._service.$draftCount) | ||
artifact._service.$draftCount++; | ||
else | ||
setProp(artifact._service, '$draftCount', 1); | ||
if(artifact._service.$draftCount > 1) | ||
signal(info`Service "${artifact._service.name.absolute}" should not have more then one draft root artifact`, location); | ||
} | ||
}); | ||
@@ -209,4 +222,4 @@ | ||
// Ignore if not part of a service | ||
let location = artifact['@odata.draft.enabled'].name && artifact['@odata.draft.enabled'].name.location; | ||
if (!artifact._service) { | ||
let location = artifact['@odata.draft.enabled'].name && artifact['@odata.draft.enabled'].name.location; | ||
signal(warning`Ignoring annotation "@odata.draft.enabled" - artifact "${artifact.name.absolute}" is not part of a service`, location); | ||
@@ -401,2 +414,7 @@ } | ||
// 'rootArtifact' is the root artifact where composition traversal started. | ||
// Constraints | ||
// Draft Root: Exactly one PK of type UUID | ||
// Draft Node: One PK of type UUID + 0..1 PK of another type | ||
// Draft Node: Must not be reachable from multiple draft roots | ||
function generateDraftForOdata(artifact, rootArtifact, visitedArtifacts) { | ||
@@ -414,19 +432,35 @@ // Sanity check | ||
// FIXME: Current restriction: Must only have exactly one key, which is of type UUID | ||
let keyNames = Object.keys(artifact.elements).filter(elemName => { | ||
return artifact.elements[elemName].key && artifact.elements[elemName].key.val; | ||
}); | ||
if (keyNames.length != 1) { | ||
signal(warning`"${artifact.name.absolute}": Ignoring annotation "@odata.draft.enabled" - currently only supported for artifacts with exactly one key of type "UUID"`, artifact.location); | ||
return; | ||
if(artifact === rootArtifact) { | ||
// draft root | ||
if(keyNames.length != 1) { | ||
signal(info`"${artifact.name.absolute}": "@odata.draft.enabled" - Draft root should have exactly one key element`, artifact.location); | ||
} else { | ||
let keyElem = artifact.elements[keyNames[0]]; | ||
if (keyElem._finalType.name.absolute != 'cds.UUID' && keyElem._finalType.name.$renamed != 'cds.UUID') { | ||
signal(info`"${artifact.name.absolute}": "@odata.draft.enabled" - Draft root key element should be of type "cds.UUID"`, keyElem.location); | ||
} | ||
} | ||
} else { | ||
// draft node | ||
if(keyNames.length < 1) { | ||
signal(info`"${artifact.name.absolute}": "@odata.draft.enabled" - Draft node should have at least one key element`, artifact.location); | ||
} else if(keyNames.length > 2) { | ||
signal(info`"${artifact.name.absolute}": "@odata.draft.enabled" - Draft node should have not more then two key elements`, artifact.location); | ||
} else { | ||
let uuidCount = 0; | ||
keyNames.forEach(kn => { | ||
let keyElem = artifact.elements[kn]; | ||
if (keyElem._finalType.name.absolute == 'cds.UUID' || keyElem._finalType.name.$renamed == 'cds.UUID') { | ||
uuidCount++; | ||
} | ||
}); | ||
if(uuidCount != 1) { | ||
signal(info`"${artifact.name.absolute}": "@odata.draft.enabled" - Draft node should have one key element of type "cds.UUID"`, artifact.location); | ||
} | ||
} | ||
} | ||
let keyElem = artifact.elements[keyNames[0]]; | ||
// Sanity check | ||
if (!keyElem._finalType) { | ||
throw new Error('Expecting artifact to have final type: ' + JSON.stringify(keyElem)); | ||
} | ||
if (keyElem._finalType.name.absolute != 'cds.UUID' && keyElem._finalType.name.$renamed != 'cds.UUID') { | ||
signal(warning`"${artifact.name.absolute}": Ignoring annotation "@odata.draft.enabled" - currently only supported for key of type "UUID"`, keyElem.location); | ||
return; | ||
} | ||
@@ -457,2 +491,3 @@ // Generate the DraftAdministrativeData projection into the service, unless there is already one | ||
let isActiveEntity = createScalarElement('IsActiveEntity', 'cds.Boolean', true, true); | ||
addBoolAnnotationTo('@UI.Hidden', true, isActiveEntity); | ||
addElement(isActiveEntity, artifact); | ||
@@ -462,2 +497,3 @@ | ||
let hasActiveEntity = createScalarElement('HasActiveEntity', 'cds.Boolean', false, false); | ||
addBoolAnnotationTo('@UI.Hidden', true, hasActiveEntity); | ||
addElement(hasActiveEntity, artifact); | ||
@@ -467,2 +503,3 @@ | ||
let hasDraftEntity = createScalarElement('HasDraftEntity', 'cds.Boolean', false, false); | ||
addBoolAnnotationTo('@UI.Hidden', true, hasDraftEntity); | ||
addElement(hasDraftEntity, artifact); | ||
@@ -480,2 +517,3 @@ | ||
addBoolAnnotationTo('@odata.contained', true, draftAdministrativeData); | ||
addBoolAnnotationTo('@UI.Hidden', true, draftAdministrativeData); | ||
addElement(draftAdministrativeData, artifact); | ||
@@ -488,2 +526,3 @@ // Note that we need to do the ODATA transformation steps for managed associations | ||
addBoolAnnotationTo('@odata.contained', true, foreignKeyElement); | ||
addBoolAnnotationTo('@UI.Hidden', true, foreignKeyElement); | ||
} | ||
@@ -490,0 +529,0 @@ // SiblingEntity : Association to one <artifact> on (... IsActiveEntity unequal, all other key fields equal ...) |
'use strict'; | ||
const alerts = require('../base/alerts'); | ||
const { CompilationError, hasErrors } = require('../base/messages'); | ||
const { CompilationError, hasErrors, sortMessages } = require('../base/messages'); | ||
const { setProp } = require('../base/model'); | ||
@@ -36,2 +36,3 @@ const transformUtils = require('./transformUtilsNew'); | ||
// (3.2) Flatten on-conditions in unmanaged associations | ||
// (3.3) Check that each service has max one draft root | ||
// Step (3.1) is in the third walk of the model | ||
@@ -60,5 +61,5 @@ // | ||
let csn = cloneCsn(inputModel); | ||
const { error, warning, signal } = alerts(csn); | ||
const { error, warning, info, signal } = alerts(csn); | ||
// the new transformer works only with new CSN | ||
@@ -90,2 +91,3 @@ if (!isNewCSN(inputModel, options)) { | ||
getCsnDef, | ||
getFinalBaseType, | ||
getFinalType, | ||
@@ -95,12 +97,11 @@ getFinalTypeDef, | ||
getServiceName, | ||
isBuiltinType, | ||
hasBoolAnnotation, | ||
isAssocOrComposition, | ||
isAssociation, | ||
isManagedAssociationElement, | ||
isStructured, | ||
hasBoolAnnotation, | ||
isManagedAssociationElement, | ||
} = getUtils(csn); | ||
// are we working with structured OData or not | ||
const structuredOData = options.betaModeOdata && options.toOdata.version === 'v4'; | ||
const structuredOData = options.toOdata.odataFormat === 'structured' && options.toOdata.version === 'v4'; | ||
@@ -137,12 +138,73 @@ // exposed struct types (see function exposeStructType) | ||
// First walk: | ||
// (1.1) Flatten structs | ||
// (1.2) Expose (named or anonymous) structured types used in structured types | ||
// (1.3) Unravel derived type chains, | ||
// (1.4) Mark fields with @odata.on.insert/update as @Core.Computed | ||
// (1.5) Resolve annotation shorthands, | ||
// (1.6) Check @cds... annotations | ||
// (1.1) Unravel derived type chains to final one for elements, actions, action parameters (propagating annotations) | ||
forEachDefinition(csn, (def, defName) => { | ||
forEachMemberRecursively(def, (member) => { | ||
if (!member.elements && !member.items) // structured does not have final base type and arrays are covered on the next line | ||
toFinalBaseType(member); | ||
toFinalBaseType(member.items); | ||
toFinalBaseType(member.returns); | ||
toFinalBaseType(member.returns && member.returns.items); | ||
}, ['definitions', defName]); | ||
}); | ||
// (1.1) Unravel derived type chains for types and annotations (propagating annotations) | ||
forEachDefinition(csn, (def) => { | ||
if (def.kind != 'entity') { | ||
if (!isStructured(getFinalTypeDef(def))) | ||
toFinalBaseType(def); | ||
toFinalBaseType(def.items); | ||
toFinalBaseType(def.returns); | ||
toFinalBaseType(def.returns && def.returns.items); | ||
} | ||
// If the definition('def' variable) is a type definition and the assigned type of this very same definition('def' variable) | ||
// is structured type, e.g.: | ||
// | ||
// type Struct1 { | ||
// a : Integer; | ||
// b : Integer; | ||
// }; | ||
// type Struct2: Struct1; | ||
// after compilation the csn looks like this: | ||
// ... | ||
// "S.Struct1": { | ||
// "kind": "type", | ||
// "elements": { | ||
// "a": { "type": "cds.Integer" }, | ||
// "b": { "type": "cds.Integer" } | ||
// } }, | ||
// "S.Struct2": { | ||
// "kind": "type", | ||
// "type": "S.Struct1", | ||
// "elements": { | ||
// "a": { "type": "cds.Integer" }, | ||
// "b": { "type": "cds.Integer" } | ||
// } } ... | ||
// | ||
// "S.Struct2" should looks just like "S.Struct1" => the "type": "S.Struct1" property has to be removed | ||
if (def.kind === 'type' && def.type && isStructured(getFinalBaseType(def)) && !def.type.ref) { | ||
// elements are already there -> do not show the type | ||
delete def.type; | ||
} | ||
}); | ||
// (1.2) Mark fields with @odata.on.insert/update as @Core.Computed | ||
// (1.3) Resolve annotation shorthands, | ||
// (1.4) Check @cds... annotations | ||
// (1.5) Flatten structs | ||
// (1.6) Expose (named or anonymous) structured types used in structured types | ||
// TODO: only for V2 or via special option??? | ||
forEachDefinition(csn, (def, defName, propertyName, path) => { | ||
// (1.1) Flatten structs - for entities and views only (might result in new elements) | ||
forEachMemberRecursively(def, (member, memberName, propertyName, path) => { | ||
// (1.2) Mark fields with @odata.on.insert/update as @Core.Computed | ||
annotateCoreComputed(member); | ||
// (1.3) Resolve annotation shorthands for elements, actions, action parameters | ||
renameShorthandAnnotations(member); | ||
// (1.4) check annotations | ||
checkAnnotations(member, path); | ||
}, ['definitions', defName]); | ||
// (1.5) Flatten structs - for entities and views only (might result in new elements) | ||
if (def.kind === 'entity' || def.kind === 'view') { | ||
@@ -172,4 +234,4 @@ for (let elemName in def.elements) { | ||
} | ||
// (1.2) Expose (named or anonymous) structured types used in structured types | ||
else if (isArtifactInSomeService(defName, services) && def.kind == 'type') { | ||
// (1.6) Expose (named or anonymous) structured types used in structured types | ||
else if (isArtifactInSomeService(defName, services) && def.kind === 'type') { | ||
for (let elemName in def.elements) { | ||
@@ -182,40 +244,2 @@ let elem = def.elements[elemName]; | ||
forEachMemberRecursively(def, (member, memberName, propertyName, path) => { | ||
// (1.3) Unravel derived type chains for elements, actions, action parameters (propagating annotations) | ||
toFinalBaseType(member); | ||
toFinalBaseType(member.items); | ||
toFinalBaseType(member.returns); | ||
toFinalBaseType(member.returns && member.returns.items); | ||
// (1.4) Mark fields with @odata.on.insert/update as @Core.Computed | ||
annotateCoreComputed(member); | ||
// (1.5) Resolve annotation shorthands for elements, actions, action parameters | ||
renameShorthandAnnotations(member); | ||
// (1.6) check annotations | ||
checkAnnotations(member, path); | ||
}, ['definitions', defName]); | ||
// (1.3) Unravel derived type chains for types and annotations (propagating annotations) | ||
if (def.kind != 'entity') { | ||
toFinalBaseType(def); | ||
toFinalBaseType(def.items); | ||
toFinalBaseType(def.returns); | ||
toFinalBaseType(def.returns && def.returns.items); | ||
} | ||
// If the artifact is a derived structured type, unravel that as well | ||
// "S.Struct2": { "kind": "type", "type": "S.Struct1", | ||
// "elements": | ||
// { "a": { "type": "cds.Integer" }, | ||
// "b": { "type": "cds.Integer" } } | ||
// } where S.Struct1 is also struct type def and the elements come from there | ||
if (def.kind == 'type' && def.type && | ||
!isBuiltinType(def.type) && getCsnDef(def.type).elements) { | ||
// elements are already there -> do not show the type | ||
delete def.type; | ||
} | ||
// (1.5) Resolve annotation shorthands for entities, types, annotations, ... | ||
@@ -248,5 +272,17 @@ renameShorthandAnnotations(def); | ||
// (3.2) Flatten on-conditions in unmanaged associations | ||
// (3.3) Check that each service has max one draft root | ||
// This must be done before 4.1, since all composition targets are annotated with @odata.draft.enabled in this step | ||
let flattenedKeyArts = Object.create(null); | ||
forEachDefinition(csn, (def, defName) => { | ||
flattenForeignKeysForArt(def, defName, flattenedKeyArts); | ||
if (isArtifactInSomeService(defName, services) && ['entity', 'view'].includes(def.kind) && def['@odata.draft.enabled']) { | ||
let serviceName = getServiceOfArtifact(defName, services); | ||
let service = getCsnDef(serviceName); | ||
if (service.$draftCount) | ||
service.$draftCount++; | ||
else | ||
setProp(service, '$draftCount', 1); | ||
if (service.$draftCount > 1) | ||
signal(info`Service "${serviceName}" should not have more then one draft root artifact`, ['definitions', defName]); | ||
} | ||
}); | ||
@@ -424,8 +460,8 @@ | ||
/** | ||
* Check if any warnings were raised in earlier steps that need to be reclassified - i.e. as errors | ||
* | ||
* @param {any} csn The csn | ||
* @returns {Array} Reclassified messages-Array | ||
*/ | ||
/** | ||
* Check if any warnings were raised in earlier steps that need to be reclassified - i.e. as errors | ||
* | ||
* @param {any} csn The csn | ||
* @returns {Array} Reclassified messages-Array | ||
*/ | ||
function reclassifyWarnings(csn) { | ||
@@ -445,3 +481,3 @@ return csn.messages.map(message => { | ||
csn.messages = reclassifyWarnings(csn); | ||
throw new CompilationError(csn.messages, csn); | ||
throw new CompilationError(sortMessages(csn.messages), csn); | ||
} | ||
@@ -623,3 +659,3 @@ return csn; | ||
if (member.type && isAssocOrComposition(member.type, csn) && member.on) { | ||
flattenOnCond(member, memberName, def.elements); | ||
flattenOnCond(member, memberName, def.elements, defName); | ||
} | ||
@@ -632,2 +668,7 @@ }); | ||
// 'rootArtifact' is the root artifact where composition traversal started. | ||
// Constraints | ||
// Draft Root: Exactly one PK of type UUID | ||
// Draft Node: One PK of type UUID + 0..1 PK of another type | ||
// Draft Node: Must not be reachable from multiple draft roots | ||
function generateDraftForOdata(artifact, artifactName, rootArtifact, visitedArtifacts) { | ||
@@ -638,3 +679,2 @@ // Sanity check | ||
} | ||
// Nothing to do if already draft-enabled (composition traversal may have circles) | ||
@@ -646,15 +686,35 @@ if ((artifact['@Common.DraftRoot.PreparationAction'] || artifact['@Common.DraftNode.PreparationAction']) | ||
// FIXME: Current restriction: Must only have exactly one key, which is of type UUID | ||
let keyNames = Object.keys(artifact.elements).filter(elemName => { | ||
return artifact.elements[elemName].key && artifact.elements[elemName].key === true; | ||
}); | ||
if (keyNames.length != 1) { | ||
signal(warning`"${artifactName}": Ignoring annotation "@odata.draft.enabled" - currently only supported for artifacts with exactly one key of type "UUID"`, ['definitions', artifactName]); | ||
return; | ||
if (artifact === rootArtifact) { | ||
// draft root | ||
if (keyNames.length != 1) { | ||
signal(info`"${artifactName}": "@odata.draft.enabled" - Draft root should have exactly one key element`, ['definitions', artifactName]); | ||
} else { | ||
let keyElem = artifact.elements[keyNames[0]]; | ||
if (keyElem.type != 'cds.UUID' /* && keyElem._finalType.name.$renamed != 'cds.UUID' */) { | ||
signal(info`"${artifactName}": "@odata.draft.enabled" - Draft root key element should be of type "cds.UUID"`, ['definitions', artifactName, 'elements', keyNames[0]]); | ||
} | ||
} | ||
} else { | ||
// draft node | ||
if (keyNames.length < 1) { | ||
signal(info`"${artifactName}": "@odata.draft.enabled" - Draft node should have at least one key element`, ['definitions', artifactName]); | ||
} else if (keyNames.length > 2) { | ||
signal(info`"${artifactName}": "@odata.draft.enabled" - Draft node mshouldust have not more then two key elements`, ['definitions', artifactName]); | ||
} else { | ||
let uuidCount = 0; | ||
keyNames.forEach(kn => { | ||
let keyElem = artifact.elements[kn]; | ||
if (keyElem.type == 'cds.UUID' /* && keyElem._finalType.name.$renamed != 'cds.UUID' */) { | ||
uuidCount++; | ||
} | ||
}); | ||
if (uuidCount != 1) { | ||
signal(info`"${artifactName}": "@odata.draft.enabled" - Draft node should have one key element of type "cds.UUID"`, ['definitions', artifactName]); | ||
} | ||
} | ||
} | ||
let keyElem = artifact.elements[keyNames[0]]; | ||
if (keyElem.type != 'cds.UUID' /* && keyElem._finalType.name.$renamed != 'cds.UUID' */) { | ||
signal(warning`"${artifactName}": Ignoring annotation "@odata.draft.enabled" - currently only supported for key of type "UUID"`); | ||
return; | ||
} | ||
@@ -685,2 +745,3 @@ // Generate the DraftAdministrativeData projection into the service, unless there is already one | ||
let isActiveEntity = createScalarElement('IsActiveEntity', 'cds.Boolean', true, true); | ||
isActiveEntity.IsActiveEntity['@UI.Hidden'] = true; | ||
addElement(isActiveEntity, artifact); | ||
@@ -690,2 +751,3 @@ | ||
let hasActiveEntity = createScalarElement('HasActiveEntity', 'cds.Boolean', false, false); | ||
hasActiveEntity.HasActiveEntity['@UI.Hidden'] = true; | ||
addElement(hasActiveEntity, artifact); | ||
@@ -695,2 +757,3 @@ | ||
let hasDraftEntity = createScalarElement('HasDraftEntity', 'cds.Boolean', false, false); | ||
hasDraftEntity.HasDraftEntity['@UI.Hidden'] = true; | ||
addElement(hasDraftEntity, artifact); | ||
@@ -703,2 +766,3 @@ | ||
draftAdministrativeData.DraftAdministrativeData['@odata.contained'] = true; | ||
draftAdministrativeData.DraftAdministrativeData['@UI.Hidden'] = true; | ||
addElement(draftAdministrativeData, artifact); | ||
@@ -712,4 +776,3 @@ | ||
uuidDraftKey = uuidDraftKey[0]; // filter returns an array, but it has only one element | ||
let foreignKeyElement = createForeignKeyElement(draftAdministrativeData.DraftAdministrativeData, 'DraftAdministrativeData', uuidDraftKey, artifact, artifactName); | ||
foreignKeyElement['@odata.contained'] = true; | ||
createForeignKeyElement(draftAdministrativeData.DraftAdministrativeData, 'DraftAdministrativeData', uuidDraftKey, artifact, artifactName); | ||
} | ||
@@ -744,2 +807,3 @@ // SiblingEntity : Association to one <artifact> on (... IsActiveEntity unequal, all other key fields equal ...) | ||
let draftNode = csn.definitions[elem.target]; | ||
// Ignore if that is our own draft root | ||
@@ -796,3 +860,3 @@ if (draftNode != rootArtifact) { | ||
if (isAssociation(member.type, csn)) { | ||
let navigable = member['@odata.navigable']!==false; // navigable disabled only if explicitly set to false | ||
let navigable = member['@odata.navigable'] !== false; // navigable disabled only if explicitly set to false | ||
let targetDef = getCsnDef(member.target); | ||
@@ -799,0 +863,0 @@ if (navigable && targetDef['@cds.odata.valuelist'] && !member[vlAnno]) { |
@@ -287,3 +287,4 @@ 'use strict'; | ||
// type properties 'key', 'notNull', 'virtual', 'masked' to the flattened elements. | ||
function flattenStructuredElement(elem) { | ||
function flattenStructuredElement(elem, ipath=[]) { | ||
let path=ipath.concat(elem.name.id) | ||
// Sanity check | ||
@@ -299,3 +300,3 @@ if (!isStructuredElement(elem)) { | ||
// Descend recursively into structured children | ||
let grandChildElems = flattenStructuredElement(childElem); | ||
let grandChildElems = flattenStructuredElement(childElem, path); | ||
for (let grandChildName in grandChildElems) { | ||
@@ -310,3 +311,2 @@ let flatElemName = elem.name.id + pathDelimiter + grandChildName; | ||
// Preserve the generated element name as it would have been with 'hdbcds' names | ||
setProp(flatElem, '_flatElementNameWithDots', elem.name.id + '.' + grandChildName); | ||
result[flatElemName] = flatElem; | ||
@@ -329,3 +329,3 @@ } | ||
// Preserve the generated element name as it would have been with 'hdbcds' names | ||
setProp(flatElem, '_flatElementNameWithDots', elem.name.id + '.' + childName); | ||
setProp(flatElem, '_flatElementNameWithDots', path.concat(childName).join(".")); | ||
result[flatElemName] = flatElem; | ||
@@ -332,0 +332,0 @@ } |
@@ -13,3 +13,3 @@ 'use strict'; | ||
const { dfilter } = require('./udict'); | ||
const { getUtils, cloneCsn } = require('../model/csnUtils'); | ||
const { cloneCsn, forEachRef, getUtils } = require('../model/csnUtils'); | ||
@@ -25,7 +25,7 @@ // Return the public functions of this module, with 'model' captured in a closure (for definitions, options etc). | ||
getCsnDef, | ||
getFinalBaseType, | ||
hasBoolAnnotation, | ||
inspectRef, | ||
isStructured, | ||
isBuiltinType, | ||
hasBoolAnnotation, | ||
getFinalBaseType, | ||
inspectRef | ||
isBuiltinType | ||
} = getUtils(model); | ||
@@ -43,2 +43,3 @@ | ||
toFinalBaseType, | ||
copyTypeProperties, | ||
isAssociationOperand, | ||
@@ -68,3 +69,3 @@ isDollarSelfOperand, | ||
function addDefaultTypeFacets(element) { | ||
if(!element || !element.type) | ||
if (!element || !element.type) | ||
return; | ||
@@ -74,7 +75,9 @@ | ||
element.length = model.options && model.options.length ? model.options.length : 5000; | ||
if (!(model.options && model.options.length)) | ||
setProp(element, "$default", true); | ||
} | ||
if(element.type === 'cds.Decimal' && element.precision === undefined && model.options.precision) { | ||
if (element.type === 'cds.Decimal' && element.precision === undefined && model.options.precision) { | ||
element.precision = model.options.precision; | ||
} | ||
if(element.type === 'cds.Decimal' && element.scale === undefined && model.options.scale) { | ||
if (element.type === 'cds.Decimal' && element.scale === undefined && model.options.scale) { | ||
element.scale = model.options.scale; | ||
@@ -109,3 +112,4 @@ } | ||
// this is flattened elem -> keys still not flattened, have to check if starts with key ref | ||
return assoc.keys.map(key => key.ref.join('.')).some(keyDotName => elemName.startsWith(keyDotName)); | ||
// FIXME: review why join('.')? what about join(fkSeparator)? | ||
return assoc.keys.map(key => key.ref.join('.')).some(keyDotName => elemName.startsWith(`${keyDotName}${fkSeparator}`)); | ||
} else { | ||
@@ -138,3 +142,3 @@ // exact match of the name | ||
for (let keyName in targetKeys) { | ||
if (targetKeys[keyName].viaTransform && keyName.startsWith(fKeyName)) | ||
if (targetKeys[keyName].viaTransform && keyName.startsWith(`${fKeyName}${fkSeparator}`)) | ||
flattenedKeys.push(keyName); | ||
@@ -174,7 +178,7 @@ } | ||
let fkArtifact; | ||
if(!path){ | ||
if (!path) { | ||
fkArtifact = assocTargetDef.elements[foreignKey.ref.join(pathDelimiter)]; // foreignKey.as ??? | ||
} else { | ||
const {art} = inspectRef(path); | ||
const { art } = inspectRef(path); | ||
fkArtifact = art; | ||
@@ -184,3 +188,3 @@ } | ||
// In case of compiler errors the foreign key might be missing | ||
if(!fkArtifact && hasErrors((model.options && model.options.messages) || model.messages)) { | ||
if (!fkArtifact && hasErrors((model.options && model.options.messages) || model.messages)) { | ||
return null; | ||
@@ -314,8 +318,10 @@ } | ||
// } | ||
function flattenStructuredElement(elem, elemName) { | ||
function flattenStructuredElement(elem, elemName, ipath=[]) { | ||
let path=ipath.concat(elemName) | ||
// in case the element is of user defined type => take the definition of the type | ||
// for type of 'x' -> elem.type is an object, not a string -> use directly | ||
let elemType = getFinalBaseType(elem.type); | ||
let elemType; | ||
if(!elem.elements) // structures does not have final base type | ||
elemType = getFinalBaseType(elem.type); | ||
// if no elements => check if the element is of user defined structured type | ||
@@ -337,3 +343,3 @@ if (!elem.elements && elem.type && !isBuiltinType(elem.type)) { | ||
// Descend recursively into structured children | ||
let grandChildElems = flattenStructuredElement(childElem, childName); | ||
let grandChildElems = flattenStructuredElement(childElem, childName, path); | ||
for (let grandChildName in grandChildElems) { | ||
@@ -349,3 +355,2 @@ let flatElemName = elemName + pathDelimiter + grandChildName; | ||
// Preserve the generated element name as it would have been with 'hdbcds' names | ||
setProp(flatElem, '_flatElementNameWithDots', elemName + '.' + grandChildName); | ||
result[flatElemName] = flatElem; | ||
@@ -358,3 +363,3 @@ } | ||
flatElem.viaTransform = true; // FIXME: This name is not ideal but used elsewhere, too) | ||
setProp(flatElem, '_flatElementNameWithDots', elemName + '.' + childName); | ||
setProp(flatElem, '_flatElementNameWithDots', path.concat(childName).join(".")); | ||
result[flatElemName] = flatElem; | ||
@@ -386,16 +391,16 @@ } | ||
// Refs of length 1 cannot contain steps - no need to check | ||
if(ref.length < 2){ | ||
return ref; | ||
if (ref.length < 2) { | ||
return ref; | ||
} | ||
try{ | ||
try { | ||
return flatten(ref, path); | ||
} catch(e){ | ||
if(e.message && e.message == "Scope 'ref-where' but no entity was provided."){ | ||
} catch (e) { | ||
if (e.message && e.message == "Scope 'ref-where' but no entity was provided.") { | ||
const main = path.slice(0, path.lastIndexOf("ref")); | ||
const { links } = inspectRef(main); | ||
const whereEntity = links[path[path.lastIndexOf("ref")+1]].art; | ||
return flatten(ref, path, whereEntity.target ? getCsnDef(whereEntity.target) : whereEntity ); | ||
const whereEntity = links[path[path.lastIndexOf("ref") + 1]].art; | ||
return flatten(ref, path, whereEntity.target ? getCsnDef(whereEntity.target) : whereEntity); | ||
} else { | ||
@@ -406,12 +411,12 @@ throw e; | ||
function flatten(ref, path, whereEntity){ | ||
function flatten(ref, path, whereEntity) { | ||
let result = []; | ||
//let stack = []; // IDs of path steps not yet processed or part of a struct traversal | ||
const { links, scope } = inspectRef( path, whereEntity ); | ||
//let stack = []; // IDs of path steps not yet processed or part of a struct traversal | ||
const { links, scope } = inspectRef(path, whereEntity); | ||
if (scope === "$magic") | ||
return ref; | ||
let flattenStep = false; | ||
links.forEach( (value, idx) => { | ||
links.forEach((value, idx) => { | ||
if (flattenStep) | ||
result[result.length-1] += pathDelimiter + ref[idx]; | ||
result[result.length - 1] += pathDelimiter + ref[idx]; | ||
else { | ||
@@ -422,4 +427,4 @@ result.push(ref[idx]); | ||
}); | ||
// If the path starts with '$self', this is now redundant (because of flattening) and can be omitted, | ||
// making life easier for consumers | ||
// If the path starts with '$self', this is now redundant (because of flattening) and can be omitted, | ||
// making life easier for consumers | ||
if (result[0] == '$self' && result.length > 1) { | ||
@@ -431,95 +436,53 @@ result = result.slice(1); | ||
} | ||
// After flattening of elements we need to flatten the on-conditions of | ||
// unmanaged associations using those newly created elements | ||
// | ||
// Examples: | ||
// Input as: assoc.on: [{ ref: ['a1', 'x'] }, '=', { ref: ['$self', 'x'] }] | ||
// ^^^ ^^^ ^^^ ^^^ | ||
// (1) (2) (3) (4) | ||
// (1) is the current association which is a sub element of struct elem 's' ( ... elements: { s: { a1: association to ... }}) | ||
// (2) is non-structured element in the target | ||
// (3) is '$self' ... clearly | ||
// (4) is non-structured element in the current scope | ||
// The flatten on-condition is: [{ ref: ['s_a1', 'x'] }, '=', { ref: ['x'] }] | ||
// | ||
// Input as: assoc.on: [{ ref: ['a2', 'x'] }, '=', { ref: ['$self', 's', 'y'] }] | ||
// ^^^ ^^^ ^^^ ^^^^^^^ | ||
// (1) (2) (3) (4) | ||
// (1) is the current association which is a sub element of struct elem 's' ( ... elements: { s: { a2: association to ... }}) | ||
// (2) is non-structured element in the target | ||
// (3) is '$self' ... clearly | ||
// (4) is structured element in the current scope | ||
// The flatten on-condition is: [{ ref: ['s_a2', 'x'] }, '=', { ref: ['s_y'] }] | ||
// | ||
// Input as: assoc.on: [{ ref: [{ ref: ['a3', 'x'] }, '=', { ref: ['y'] }] | ||
// ^^^ ^^^ ^^^ | ||
// (1) (2) (3) | ||
// (1) is the current association which is a sub element of struct elem 's' ( ... elements: { s: { a3: association to ... }}) | ||
// (2) is non-structured element in the target | ||
// (3) element from the current nameresolution scope ( .. elements: { s : { y: ... ; a3: association to ... }) | ||
// The flatten on-condition is: [{ ref: ['s_a3', 'x'] }, '=', { ref: ['s_y'] }] | ||
// | ||
// Input as: assoc.on: [{ ref: ['a4', 'struct', 'k1'] }, '=', { ref: ['$self', 's', 'struct', 'k2'] }] | ||
// ^^^ ^^^^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^^^^^^^ | ||
// (1) (2) (3) (4) | ||
// (1) is the current association which is a sub element of struct elem 's' ( ... elements: { s: { a4: association to ... }}) | ||
// (2) is structured element in the target | ||
// (3) is '$self' ... clearly | ||
// (4) is structured element in the current scope | ||
// The flatten on-condition is: [{ ref: ['s_a4', 'struct_k1'] }, '=', { ref: ['s_struct_k2'] }] | ||
function flattenOnCond(assoc, assocName, defElements) { | ||
// Flatten on conditions of unmanaged associations. This method assumes that | ||
// other flattening of the elements was already performed and there are | ||
// no left over structure elements marked with some ignore flag. Also, uses | ||
// the non-enumerable property '_flatElementNameWithDots'. | ||
function flattenOnCond(assoc, assocName, defElements, defName) { | ||
if (!assoc.on) return; // nothing to do | ||
let allRefs = assoc.on.filter(elem => typeof elem === 'object' && elem.ref); | ||
let targetDef = getCsnDef(assoc.target); | ||
for (let refObj of allRefs) { | ||
// if reference is just '$self' | ||
if (refObj.ref.length === 1 && refObj.ref[0] === '$self') | ||
continue; | ||
// remove '$self' when first elem in ref and flatten the rest if needed | ||
if (refObj.ref[0] === '$self') { | ||
refObj.ref.shift(); | ||
// the rest of the reference should point to an element from the current definition | ||
let potentialFlattenElem = refObj.ref.join(pathDelimiter); | ||
if (defElements && defElements[potentialFlattenElem] && defElements[potentialFlattenElem].viaTransform) { | ||
refObj.ref = [potentialFlattenElem]; | ||
forEachRef(assoc, (ref, node, path) => { | ||
// assoc itself is inside a struct -> all the first refs need to be flattened; | ||
if (assoc.viaTransform) { | ||
// when the first ref id is the same association | ||
if (assoc._flatElementNameWithDots.endsWith(`.${ref[0]}`)) | ||
ref.splice(0, 1, assocName); | ||
// elem from the curr name resolution scope | ||
// but not a $self, will deal with this one later | ||
else if (ref[0] !== '$self') { | ||
// .splice(-1, 1, ref[0]) does not work here, why??? | ||
let currStructFlatName = `${assoc._flatElementNameWithDots.split('.').slice(0, -1).join('_')}` | ||
node.ref.splice(0, 1, `${currStructFlatName}_${ref[0]}`); | ||
} | ||
continue; | ||
} | ||
// assoc element itself was flatten | ||
// -> every first step where current assoc is specified needs to be replaced | ||
if (assoc.viaTransform && assocName.endsWith(`${pathDelimiter}${refObj.ref[0]}`)) { | ||
refObj.ref.splice(0, 1, assocName); | ||
// the rest of the reference should point to an element from the target | ||
let potentialFlattenElem = refObj.ref.slice(1).join(pathDelimiter); | ||
if (targetDef.elements && targetDef.elements[potentialFlattenElem] && targetDef.elements[potentialFlattenElem].viaTransform) { | ||
refObj.ref.splice(1, refObj.ref.length - 1, potentialFlattenElem); | ||
} | ||
} | ||
let flatRef = []; | ||
let needToFlat = false; | ||
// $user.locale must not be flatten and they are not understand by inspectRef | ||
if (ref.join('.') === '$user.locale') | ||
return; | ||
// reference to flattened element in the target | ||
let flattenRefName = refObj.ref.join(pathDelimiter); | ||
if (targetDef.elements) { | ||
// exact match of flatten element name | ||
if (targetDef.elements[flattenRefName] && targetDef.elements[flattenRefName].viaTransform) { | ||
refObj.ref = [flattenRefName]; | ||
continue; | ||
} | ||
// when element is defined in the current name resolution scope, like | ||
// entity E { | ||
// key x: Integer; | ||
// s : { | ||
// y : Integer; | ||
// a3 : association to E on a3.x = y; | ||
// } | ||
// } | ||
let potentialFlattenElem = Object.keys(targetDef.elements) | ||
.find(elemName => elemName.endsWith(`${pathDelimiter}${flattenRefName}`)); | ||
if (potentialFlattenElem && targetDef.elements[potentialFlattenElem].viaTransform) { | ||
refObj.ref = [potentialFlattenElem]; | ||
continue; | ||
} | ||
} | ||
} | ||
ref.slice().forEach(refId => { | ||
if (needToFlat) { | ||
let flatLastElem = `${flatRef[flatRef.length - 1]}${pathDelimiter}${refId}`; | ||
flatRef.splice(-1, 1, flatLastElem); | ||
needToFlat = false; | ||
} else | ||
flatRef.push(refId); | ||
node.ref = flatRef; | ||
let { art } = inspectRef(path); | ||
// if not resolved to an art, then this element was flattened | ||
if (!art) | ||
needToFlat = true; | ||
}); | ||
// remove leading $self when at the begining of a ref | ||
if (ref.length > 1 && ref[0] === '$self') | ||
node.ref.splice(0, 1); | ||
}, ['definitions', defName, 'elements', assocName]); | ||
} | ||
@@ -537,2 +500,33 @@ | ||
/** | ||
* Copy properties of the referenced type, but don't resolve to the final base type. | ||
* | ||
* @param {any} node Node to copy to | ||
* @returns {undefined} | ||
*/ | ||
function copyTypeProperties(node) { | ||
// Nothing to do if no type (or if array/struct type) | ||
if (!node || !node.type) return; | ||
// .. or if it is a ref | ||
if (node.type && node.type.ref) return; | ||
// .. or builtin already | ||
if (node.type && typeof node.type === 'string' && node.type.startsWith('cds.') && !node.type.startsWith('cds.foundation.')) return; | ||
// The type might already be a full fledged type def (array of) | ||
const typeDef = typeof node.type === 'string' ? getCsnDef(node.type) : node.type; | ||
// Nothing to do if type is an array or a struct type | ||
if (typeDef.items || typeDef.elements) return; | ||
// if the declared element is an enum, these values are with priority | ||
if (!node.enum && typeDef.enum) | ||
Object.assign(node, { enum: typeDef.enum }); | ||
if (!node.length && typeDef.length && !typeDef.$default) | ||
Object.assign(node, { length: typeDef.length }); | ||
if (!node.precision && typeDef.precision) | ||
Object.assign(node, { precision: typeDef.precision }); | ||
if (!node.scale && typeDef.scale) | ||
Object.assign(node, { scale: typeDef.scale }); | ||
if (!node.srid && typeDef.srid) | ||
Object.assign(node, { srid: typeDef.srid }); | ||
} | ||
// Replace the type of 'node' with its final base type (in contrast to the compiler, | ||
@@ -545,3 +539,3 @@ // also unravel derived enum types, i.e. take the final base type of the enum's base type. | ||
// In case of a ref -> Follow the ref | ||
if(node.type && node.type.ref) { | ||
if (node.type && node.type.ref) { | ||
node.type = getFinalBaseType(node.type); | ||
@@ -562,4 +556,2 @@ return; | ||
Object.assign(node, { length: typeDef.length }); | ||
if (!node.length && typeDef.length) | ||
Object.assign(node, { length: typeDef.length }); | ||
if (!node.precision && typeDef.precision) | ||
@@ -726,6 +718,6 @@ Object.assign(node, { precision: typeDef.precision }); | ||
if (!arg.ref) { | ||
// Not a path, hence not an association (literal, expression, function, whatever ...) | ||
// Not a path, hence not an association (literal, expression, function, whatever ...) | ||
return false; | ||
} | ||
const {art} = inspectRef(path); | ||
const { art } = inspectRef(path); | ||
// If it has a target, it is an association or composition | ||
@@ -911,10 +903,10 @@ return art.target !== undefined; | ||
let validFroms = [], validTos = [], validKeys = []; | ||
if(hasBoolAnnotation(element, '@cds.valid.from')) { | ||
validFroms.push({element, path: [...path]}); | ||
if (hasBoolAnnotation(element, '@cds.valid.from')) { | ||
validFroms.push({ element, path: [...path] }); | ||
} | ||
if(hasBoolAnnotation(element, '@cds.valid.to')) { | ||
validTos.push({element, path: [...path]}); | ||
if (hasBoolAnnotation(element, '@cds.valid.to')) { | ||
validTos.push({ element, path: [...path] }); | ||
} | ||
if(hasBoolAnnotation(element, '@cds.valid.key')) { | ||
validKeys.push({element, path: [...path]}); | ||
if (hasBoolAnnotation(element, '@cds.valid.key')) { | ||
validKeys.push({ element, path: [...path] }); | ||
} | ||
@@ -941,5 +933,5 @@ return [validFroms, validTos, validKeys]; | ||
function checkAssignment(annoName, element, path, artifact) { | ||
if(artifact.kind !== 'type' && !artifact.query) { | ||
if (artifact.kind !== 'type' && !artifact.query) { | ||
// path.length > 4 to check for structured elements | ||
if(element.elements || element.target || path.length > 4) { | ||
if (element.elements || element.target || path.length > 4) { | ||
signal(error`Element cannot be annotated with "${annoName}"`, path); | ||
@@ -961,5 +953,5 @@ return false; | ||
*/ | ||
function checkMultipleAssignments(array, annoName, artifact, artifactName, err=true) { | ||
if(array.length > 1) { | ||
if(err == true) { | ||
function checkMultipleAssignments(array, annoName, artifact, artifactName, err = true) { | ||
if (array.length > 1) { | ||
if (err == true) { | ||
signal(error`"${annoName}" must be assigned only once`, ['definitions', artifactName]); | ||
@@ -966,0 +958,0 @@ } else { |
'use strict' | ||
const { setProp, forEachGeneric, forEachDefinition } = require('../base/model'); | ||
var { CompilationError, hasErrors, sortMessages } = require('../base/messages'); | ||
const modelUtils = require('../model/modelUtils.js'); | ||
@@ -9,3 +10,4 @@ const compactor = require('../json/compactor'); | ||
const {compactModel} = require('../json/to-csn'); | ||
const csn2xsn = require('../json/csn2xsn'); | ||
const { cloneCsn } = require('../model/csnUtils'); | ||
// Paths that start with an artifact of protected kind are special | ||
@@ -16,4 +18,3 @@ // either ignore them in QAT building or in path rewriting | ||
function translateAssocsToJoinsCSN(csn, options){ | ||
let main = require('../main'); | ||
const model = main.compileSources({ '<stdin>.json' : JSON.stringify(csn, null, 2) }, options); | ||
const model = csn2xsn(cloneCsn(csn), options); | ||
forEachDefinition(model, art => { | ||
@@ -38,3 +39,11 @@ if (art.$queries) { | ||
}); | ||
return compactModel(translateAssocsToJoins(model)); | ||
translateAssocsToJoins(model); | ||
// If A2J reports error - end! Continuing with a broken CSN makes no sense | ||
if (hasErrors(model.messages)) { | ||
throw new CompilationError(sortMessages(model.messages), csn) | ||
} | ||
const compact = compactModel(model); | ||
setProp(compact, "options", csn.options); | ||
setProp(compact, "messages", csn.messages); | ||
return compact; | ||
} | ||
@@ -41,0 +50,0 @@ |
{ | ||
"name": "@sap/cds-compiler", | ||
"version": "1.19.2", | ||
"version": "1.20.3", | ||
"lockfileVersion": 1, | ||
@@ -5,0 +5,0 @@ "requires": true, |
@@ -1,1 +0,1 @@ | ||
{"bin":{"cdsc":"bin/cdsc.js","cdshi":"bin/cdshi.js","cdsse":"bin/cdsse.js"},"bundleDependencies":false,"dependencies":{"antlr4":"4.7.1","resolve":"1.8.1","sax":"1.2.4"},"deprecated":false,"description":"CDS (Core Data Services) compiler and backends","keywords":["CDS"],"main":"lib/main.js","name":"@sap/cds-compiler","version":"1.19.2","license":"SEE LICENSE IN developer-license-3.1.txt"} | ||
{"bin":{"cdsc":"bin/cdsc.js","cdshi":"bin/cdshi.js","cdsse":"bin/cdsse.js"},"bundleDependencies":false,"dependencies":{"antlr4":"4.7.1","resolve":"1.8.1","sax":"1.2.4"},"deprecated":false,"description":"CDS (Core Data Services) compiler and backends","keywords":["CDS"],"main":"lib/main.js","name":"@sap/cds-compiler","version":"1.20.3","license":"SEE LICENSE IN developer-license-3.1.txt"} |
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 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 not supported yet
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
Shell access
Supply chain riskThis module accesses the system shell. Accessing the system shell increases the risk of executing arbitrary code.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3468429
123
68975
1