Socket
Socket
Sign inDemoInstall

@sap/cds-compiler

Package Overview
Dependencies
6
Maintainers
3
Versions
99
Alerts
File Explorer

Advanced tools

Install Socket

Detect and block malicious and high-risk dependencies

Install

Comparing version 1.1.1 to 1.5.0

lib/checks/checkExpressions.js

221

bin/cdsc.js

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

.description(`Compile a CDS model given from the input files. Input files may be CDS source files (.cds), CSN
model files (.json), property files for localized annotations (.properties) and XML files (.xml)
model files (.json) and XML files (.xml)
for pre-processed ODATA annotations.`)

@@ -39,12 +39,12 @@

.option('@@', 'Generation options (can be combined, default if none given is "--to-csn --out -")')
.option('@@', 'Generation options (can be combined, default if none given is "--to-csn client --out -")')
.option('-o, --out <dir>', 'Place generated files in directory <dir>, default is "-" for <stdout>')
.option('-H, --to-hana <flags>', `Generate HANA CDS source files, <flags> can be a comma-separated
combination of either "flat" (default), "deep" or "hdbcds" for entity
names, either "assocs" (default) or "joins" for associations and any
of "src,csn"
flat : Produce HANA entity and element names in uppercase and
combination of either "plain" (default), "quoted" or "hdbcds" for
entity names, either "assocs" (default) or "joins" for associations
and any of "src,csn"
plain : Produce HANA entity and element names in uppercase and
flattened with underscores. Do not generate structured
types.
deep : Produce HANA entity and element names in original case
quoted : Produce HANA entity and element names in original case
as in CDL. Keep nested contexts (resulting in entity names

@@ -54,4 +54,4 @@ with dots), but flatten element names with underscores.

hdbcds : Produce HANA entity end element names as HANA CDS would
generate them from the same CDS source (like "deep",
but using element names with dots).
generate them from the same CDS source (like "quoted", but
using element names with dots).
assocs : Keep associations in HANA CDS as far as possible

@@ -63,4 +63,5 @@ joins : Transform associations to HANA CDS joins

separated combination of "xml,json,separate,combined,csn" and either
"v2" (default) or "v4" version. Additionally, one of "flat,deep,hdbcds" can
be specified to annotate resulting DB names in the generated CSN.
"v2" (default) or "v4" version. Additionally, one of
"plain,quoted,hdbcds" can be specified to annotate resulting DB names
in the generated CSN.
v2 : Generate ODATA V2 output

@@ -73,5 +74,8 @@ v4 : Generate ODATA V4 output

csn : Generate "odata_csn.json" with ODATA-preprocessed model
flat : Annotate DB names in "flat" style (see --to-hana, --to-sql)
deep : Annotate DB names in "deep" style (see --to-hana, --to-sql)
hdbcds : Annotate DB names in "hdbcds" style (see --to-hana, --to-sql)`, verifyToOdataOption)
plain : Annotate DB names in "plain" style (see --to-hana,
--to-sql)
quoted : Annotate DB names in "quoted" style (see --to-hana,
--to-sql)
hdbcds : Annotate DB names in "hdbcds" style (see --to-hana,
--to-sql)`, verifyToOdataOption)
.option('-C, --to-cdl', 'Generate CDS source files "<artifact>.cds"')

@@ -82,16 +86,17 @@ .option('-S, --to-swagger <flags>', `Generate Swagger (OpenAPI) JSON, <flags> can be a comma-separated

"<svc>_swagger.json"
csn : Generate "swagger_csn.json" with Swagger-preprocessed model`, verifyToSwaggerOption)
csn : Generate "swagger_csn.json" with Swagger-preprocessed
model`, verifyToSwaggerOption)
.option('-Q, --to-sql <flags>', `Generate SQL DDL statements to create tables and views, <flags> can be
a comma-separated combination of either "flat" (default), "deep" or
a comma-separated combination of either "plain" (default), "quoted" or
"hdbcds" for entity names, either "assocs" (default) or "joins" for
associations, either "hana" or "sqlite" for the SQL dialect and any
of "src,csn"
flat : Produce SQL table and view names in uppercase and
plain : Produce SQL table and view names in uppercase and
flattened with underscores (no quotes required)
deep : Produce SQL table and view names in original case
quoted : Produce SQL table and view names in original case
as in CDL (with dots), but flatten element names with
underscores (requires quotes)
hdbcds : Produce SQL table, view and column names as HANA CDS
would generate them from the same CDS source (like "deep",
but using element names with dots).
would generate them from the same CDS source (like
"quoted", but using element names with dots).
assocs : Keep associations as far as possible (only usable for

@@ -104,12 +109,11 @@ HANA SQL)

csn : Generate "sql_csn.json" with SQL-preprocessed model`, verifyToSqlOption)
.option('-I, --to-i18n <style>', `Generate files for translation of localized annotations, <style> can
be either "prop" (default) for a property file or "ui5" for a UI5-
style combination of property file and modified model
prop : Generate property file with annotation text
ui5 : Generate model with placeholders and property file with
annotation text`,
verifyI18nOption)
.option(' --to-csn <flavor>', `Generate original model as CSN to "csn.json", <flavor> can be either
"client" (default) or "gensrc"
client : Generate standard CSN consumable by clients and backends
gensrc : Generate CSN specifically for use as a source, e.g. for
combination with additional extend/annotate statements,
but not suitable for consumption by clients or backends`,
verifyToCsnOption)
.option('-l, --lint-mode', `Generate nothing, just produce single-file error messages if any (for
use by editors)`)
.option(' --to-csn', 'Generate original model as CSN to "csn.json"')
.option('@@', 'Backward compatibility options (deprecated, do not use)')

@@ -125,2 +129,5 @@ .option(' --oldstyle-self', 'Allow "self" alternatively to "$self" (implied by --tnt-flavor)') // FIXME: We should get rid of that

.option('@@', 'Validation options')
.option(' --fuzzy-csn', 'Allow free-style CSN properties by disabling CSN input validation')
.option('@@', 'Internal options (for testing only, may be changed/removed at any time)')

@@ -133,9 +140,7 @@ .option('-R, --raw-output', 'Write raw augmented CSN and error output to stdout')

.option(' --to-extensions', 'Generate augmented CSN for extensions from properties file') // FIXME: Previously 'generate-extensions', should later become part of normal compilation, producing plain CSN
.option(' --test-mode', `Produce extra-stable output for automated tests (normalize filenames
in errors, sort properties in CSN, omit version in CSN)`)
.option(' --test-mode', `Produce extra-stable output for automated tests (normalize
filenames in errors, sort properties in CSN, omit version in CSN)`)
.option(' --extra-augment', 'Compile to plain CSN, augment and compile again, augmentation tests')
.option(' --re-augmented', 'Re-augmented CSN and error output') // FIXME: What does that mean/do? Isn't that what --extra-augment is supposed to do?
.option(' --old-propagate', 'Use old propagation logic')
.option(' --disable-propagate',`Do not propagate properties with "--to-csn" (makes result re-usable
with extensions)`) // FIXME: Should probably become a flag for '--to-csn'
.option(' --old-sql-impl', `Use old "toSql" implementation (deprecated, only as fallback)`)

@@ -145,9 +150,9 @@ .option('@@', 'Table renaming (tentative, subject to change, requires "--beta-mode")')

columns so that they match the result of "--to-hana" or "--to-sql"
with the "flat" option. Possible values for <flags> are either
"deep" or "hdbcds" (default) for the names of the existing tables.
with the "plain" option. Possible values for <flags> are either
"quoted" or "hdbcds" (default) for the names of the existing tables.
Results are generated as "rename_<artifact>.sql"
deep : Assume existing SQL tables and views were named in
quoted : Assume existing SQL tables and views were named in
original case as in CDL (with dots), but element names
were flattened with underscores (as a result e.g. from
"cdsc --to-hana src,deep")
"cdsc --to-hana src,quoted")
hdbcds : Assume existing SQL tables, views and columns were

@@ -208,7 +213,6 @@ generated by HANA CDS from the same CDS source (or from

toSql : program.toSql,
toI18n : program.toI18n,
lintMode : program.lintMode,
// By default, toCsn is on if no other to-option is set
toCsn : program.toCsn || (!program.toHana && !program.toOdata && !program.toCdl && !program.toSwagger && !program.toSql
&& !program.toI18n && !program.lintMode && !program.toRename),
&& !program.lintMode && !program.toRename),
oldstyleSelf : program.oldstyleSelf,

@@ -220,2 +224,3 @@ tntFlavor : program.tntFlavor && getDefaultTntFlavorOptions(),

traceFs : program.traceFs,
fuzzyCsn: program.fuzzyCsn,
rawOutput : program.rawOutput,

@@ -230,4 +235,3 @@ betaMode : program.betaMode,

reAugmented : program.reAugmented,
oldPropagate : program.oldPropagate,
disablePropagate : program.disablePropagate, // TODO: how to handle this?
oldSqlImpl : program.oldSqlImpl,
toRename : program.toRename,

@@ -293,3 +297,3 @@ }

'csn',
'flat', 'deep', 'hdbcds'], value, '-O, --to-odata');
'plain', 'quoted', 'hdbcds'], value, '-O, --to-odata');

@@ -309,8 +313,8 @@ if (result.v2) {

let nrOfNameFlags = 0;
if (result.flat) {
result.names = 'flat'
if (result.plain) {
result.names = 'plain'
nrOfNameFlags++;
}
if (result.deep) {
result.names = 'deep'
if (result.quoted) {
result.names = 'quoted'
nrOfNameFlags++;

@@ -323,6 +327,6 @@ }

if (nrOfNameFlags > 1) {
displayUsage(`Only one of "flat", "deep" or "hdbcds" can be specified for names with option "-O, --to-odata", but not combinations`, 2);
displayUsage(`Only one of "plain", "quoted" or "hdbcds" can be specified for names with option "-O, --to-odata", but not combinations`, 2);
}
delete result.flat;
delete result.deep;
delete result.plain;
delete result.quoted;
delete result.hdbcds;

@@ -341,24 +345,24 @@

let result = verifyFlags([
'flat', 'deep', 'hdbcds',
'plain', 'quoted', 'hdbcds',
'assocs', 'joins',
'src', 'csn'], value, '-H, --to-hana');
let nrOfNameFlags = 0;
if (result.flat) {
result.names = 'flat'
nrOfNameFlags++;
let flagCount = 0;
if (result.plain) {
result.names = 'plain'
flagCount++;
}
if (result.deep) {
result.names = 'deep'
nrOfNameFlags++;
if (result.quoted) {
result.names = 'quoted'
flagCount++;
}
if (result.hdbcds) {
result.names = 'hdbcds'
nrOfNameFlags++;
flagCount++;
}
if (nrOfNameFlags > 1) {
displayUsage(`Only one of "flat" (default), "deep" or "hdbcds" can be specified for names with option "-H, --to-hana", but not combinations`, 2);
if (flagCount > 1) {
displayUsage(`Only one of "plain" (default), "quoted" or "hdbcds" can be specified for names with option "-H, --to-hana", but not combinations`, 2);
}
delete result.flat;
delete result.deep;
delete result.plain;
delete result.quoted;
delete result.hdbcds;

@@ -383,3 +387,3 @@

let result = verifyFlags([
'flat', 'deep', 'hdbcds',
'plain', 'quoted', 'hdbcds',
'assocs', 'joins',

@@ -389,30 +393,33 @@ 'src', 'csn',

let nrOfNameFlags = 0;
if (result.flat) {
result.names = 'flat'
nrOfNameFlags++;
let flagCount = 0;
if (result.plain) {
result.names = 'plain'
flagCount++;
}
if (result.deep) {
result.names = 'deep'
nrOfNameFlags++;
if (result.quoted) {
result.names = 'quoted'
flagCount++;
}
if (result.hdbcds) {
result.names = 'hdbcds'
nrOfNameFlags++;
flagCount++;
}
if (nrOfNameFlags > 1) {
displayUsage(`Only one of "flat" (default), "deep" or "hdbcds" can be specified for names with option "-Q, --to-sql", but not combinations`, 2);
if (flagCount > 1) {
displayUsage(`Only one of "quoted" (default), "quoted" or "hdbcds" can be specified for names with option "-Q, --to-sql", but not combinations`, 2);
}
delete result.flat;
delete result.deep;
delete result.quoted;
delete result.quoted;
delete result.hdbcds;
flagCount = 0;
if (result.assocs) {
result.associations = 'assocs'
flagCount++;
}
if (result.joins) {
result.associations = 'joins'
flagCount++;
}
if (result.assocs && result.joins) {
displayUsage(`Either "assocs" (default) or "joins" can be specified for associations with option "-Q, --to-sql", but not both`, 2);
if (flagCount > 1) {
displayUsage(`Only one of "assocs" (default) or "joins" can be specified for associations with option "-Q, --to-sql", but not both`, 2);
}

@@ -438,6 +445,6 @@ delete result.assocs;

function verifyToRenameOption(value) {
let result = verifyFlags(['deep','hdbcds'], value, '--to-rename');
let result = verifyFlags(['quoted','hdbcds'], value, '--to-rename');
if (result.deep) {
result.names = 'deep'
if (result.quoted) {
result.names = 'quoted'
}

@@ -447,6 +454,6 @@ if (result.hdbcds) {

}
if (result.deep && result.hdbcds) {
displayUsage(`Either "deep" or "hdbcds" (default) can be specified for names with option "--to-rename", but not both`, 2);
if (result.quoted && result.hdbcds) {
displayUsage(`Either "quoted" or "hdbcds" (default) can be specified for names with option "--to-rename", but not both`, 2);
}
delete result.deep;
delete result.quoted;
delete result.hdbcds;

@@ -456,6 +463,6 @@ return result;

// Check the value of --to-i18n for legal values. Return the value as an object with sub-options.
function verifyI18nOption(value) {
verifyFlags(['prop','ui5'], value, '-I, --to-i18n', 'style');
return { style : value } ;
// Check the value of --to-csn for legal values. Return the value as an object with sub-options.
function verifyToCsnOption(value) {
verifyFlags(['client', 'gensrc'], value, '--to-csn', 'flavor');
return (value == 'gensrc') ? { gensrc: true } : undefined;
}

@@ -508,5 +515,2 @@

}
if (options.toI18n) {
run = run.then( toI18n )
}
if (options.toOdata) {

@@ -564,22 +568,2 @@ run = run.then( toOdata );

// Execute the command line option '--to-i18n' and display the results.
// Return the original model (for chaining)
function toI18n(model) {
let i18nResult = compiler.toI18n(model);
// Different results depending on 'style'
if (options.toI18n.style == 'prop') {
writeToFileOrDisplay(options.out, 'annotations.properties', Object.keys(i18nResult).map(prop => `${prop}=${i18nResult[prop]}`).join('\n'), true);
} else if (options.toI18n.style == 'ui5') {
let props = '';
for (let key in i18nResult.properties) {
props += `${key}=${i18nResult.properties[key]}\n`;
}
writeToFileOrDisplay(options.out, 'i18n.properties', props);
displayNamedCsn(i18nResult, 'definitions', options);
} else {
throw new Error(`Invalid style ${options.toI18n.style} for toI18n`);
}
return model;
}
// Execute the command line option '--to-odata' and display the results.

@@ -677,11 +661,9 @@ // Return the original model (for chaining)

function displayErrors (err) {
if (options.rawOutput) {
err.model = reveal( err.model );
console.error( util.inspect( err, false, null ));
if (err instanceof compiler.CompilationError) {
if (options.rawOutput)
console.error( util.inspect( reveal( err.model ), false, null ));
else
displayMessages( err.model, err.errors );
process.exitCode = 1;
}
else if (err instanceof compiler.CompilationError) {
displayMessages( err.model, err.errors );
process.exitCode = 1;
}
else if (err instanceof compiler.InvocationError) {

@@ -720,7 +702,4 @@ console.error( '' );

}
else if (options.newCsn) {
writeToFileOrDisplay(options.out, name + '.json', compiler.compactModel(model), true);
}
else {
writeToFileOrDisplay(options.out, name + '.json', compiler.compactSortedJson(model), true);
writeToFileOrDisplay(options.out, name + '.json', compiler.toCsn(model), true);
}

@@ -727,0 +706,0 @@ }

// Make internal properties of the augmented CSN visible
const { locationString } = require('../lib/base/messages');
const msg = require('../lib/base/messages');
function locationString( loc ) {
return (typeof loc === 'object' && loc && loc.start)
? msg.locationString(loc)
: (loc === null ? 'null' : typeof loc) + ':' + msg.locationString(loc);
}
function revealInternalProperties( model ) {

@@ -13,2 +19,3 @@ var unique_id = 0;

elements: (e,n) => (n.self ? artifactDictionary(e) : dictionary(e)),
columns,
actions: dictionary,

@@ -25,2 +32,3 @@ params: dictionary,

$dictOrderBy: artifactDictionary,
$layerNumber: n => n,
_layerRepresentative: s => s.realname,

@@ -93,3 +101,4 @@ _layerExtends: layerExtends,

let kind = (node.kind === 'element' && !node.name.element) ? 'mixin' : node.kind;
return (kind || '<kind>') + outer + '(' + names.join(',') + ')';
return (kind || '<kind>') + outer + '(' + names.join(',') +
( node.name && node.name.$renamed ? ', orig:"' + node.name.$renamed + '")' : ')' );
}

@@ -146,2 +155,6 @@ }

function columns( nodes ) {
return nodes.map( c => (c._parent) ? artifactIdentifier( c ) : reveal( c ) );
}
function dictionary( node ) {

@@ -148,0 +161,0 @@ return reveal( node, '__proto__' );

# ChangeLog for cdx compiler and backends
## Version 1.5.0
Features
* The DDL statements in the output of `toSql` are now sorted according to kind
(views after tables), so that they can be deployed sequentially to HANA (view
dependencies not yet considered).
* (Still work in progress): The output of `toSql` now also contains kind-specific
dictionaries for `hdbtable`, `hdbview` etc., which should be directly deployable
to HDI.
Changes
* Element definitions in multiple entity/structure extensions are now sorted
according to the layer hierarchy – elements from highest layers come last.
Report such multiple extensions only if they are potentially problematic.
* The values for the `names` option of `toSql`, `toHana` and `toOdata` have
been renamed: `flat` (default) is now `plain`, `deep` is now `quoted`. The old
values are still accepted (with a warning) but **will be removed in a subsequent
release**.
Fixes
* OData, annotation processing for v2: In a view where translation of analytical annotations
is switched on, the annotations `@Common.Text`, `@Common.Label`, and
`@Measures.ISOCurrency/@Measures.Unit` are now translated into the corresponding v2-style annotations
`sap:label`, `sap:text`, and `sap:unit`, respectively, even if the value is a path or
has a nested annotation.
* OData V2, generation of EDMX: The Parameters of a FunctionImport now always have
an attribute `Nullable="true"` if not specified as `not null` in CDS.
* Produce better parentheses for nested set operations (`union`, `intersect`, ...) in views
for SQL output.
* Correctly strip off the `enum` property of types for HANA CDS, even when derived types are
involved.
## Version 1.4.0
Features
* OData, annotation processing: Provide a shortcut for the nesting of the `TextArrangement`
annotation: In order to annotate a `@Common.Text` annotation, just put an annotation
`@Common.TextArrangement` next to it.
* Parameters can now be referred to with `:param`, `:1` or `?` in the parse API functions.
Changes
* More checks for the correct usage of `$self` and associations as values in expressions.
* Backlink-Assocations: When transforming an ON-condition `on $self = foo.bar`, check that
the association `bar` really points to the entity enclosing association `foo`.
* Allow and transform multiple `$self`-comparisons in one association ON-condition
(but a true backlink association still requires exactly one such comparison).
* Warn if a "to many" association or composition does not have an ON-condition
(likely not intended because the resulting managed association will at most match a single
item)
Fixes
* Add missing `as` for flattened structured elements.
* Allow `using cds;` to make the namespace `cds` explicitly known, which is
useful if that had been shadowed by a namespace declaration ending with `cds`.
* OData: don't generate empty `<Annotations ...>` elements any more.
* Draft for OData v2: in the `DraftRoot` and `DraftNode` annotations, the path
to the draft annotations now contains `EntityContainer`.
* Improved checks for parameters of actions and functions. Inappropriate warnings like
"The type of input parameter ... must be from the current service" and
"The action ... can only return an array of entities" don't appear any more.
* Correctly generate foreign key fields for associations in structured types.
* For `toHana()` and `toSql()`, enclose the artificial condition resulting from
`$self`-comparisons in parentheses.
* Warn properly when draft-enabled artifacts are not exposed in a service.
* Do not render a full entity name for paths like `$self.foo` to SQL (just skip `$self`).
## Version 1.3.0
Features
* The `using` declaration can now appear top-level also after artifact definitions.
* Support for `$user.locale` and `$user.id` with HANA generation `SESSION_CONTEXT(…)`.
* For entities annotated with `@odata.draft.enabled`, the generated `DraftAdministrativeData`
association for ODATA is now annotated with `@odata.contained: true` (avoiding the
generation of an `<Attribute>` for its foreign key in ODATA V4).
Changes
* Having just `$user` in CDL is now rendered as `{ref:['$user','id'], as:'$user'}`
in new-style CSN.
* Using SQL's parameter-less functions not having parentheses (like `current_date`)
is now rendered as `{func:'current_date'}` in new-style CSN.
* `betaMode` is currently required for entities with parameters.
* In old-style CSN, the `on` condition as source text has been removed.
* Explicit redirection of an association to a target that is completely unrelated to
the original target is now an error, not just a warning.
* The API function `toI18n()` and the corresponding command line option `--to-i18n` have
been removed.
* Annotation assignments after sub structure definitions, enum definitions, and
parameters are now considered an error instead of just a warning.
* For bound actions and functions, the name of the corresponding function import in
OData v2 edmx is now prefixed with the name of the entity.
Fixes
* For ODATA V2, create correct `<Principal>` and `<Dependent>` for backlink associations
having `@odata.navigable:false`.
* Avoid the `Expecting artifact to be part of a service` error that occurred when generating
multiple entities with `@odata.draft.enabled` to SQL.
* Generate correct (fully qualified) action names into the `@Common.DraftRoot` and
`@Common.DraftNode` annotations.
* When generating the `DRAFT.DraftAdministrativeData` entity for SQL, provide proper
lengths for all `NVARCHAR` fields.
## Version 1.2.0
Features
* Provide semantic code completion for the `excluding` clause.
* Add support for "deep drafts", i.e. follow compositions from entities annotated
with `@odata.draft.enabled` ("draft roots") and draft-enable them as "draft nodes".
Changes
* Finalize the propagation of the `key` property.
Provide Info messages if it is not obvious why it has not been propagated.
* Finalize the propagation of the `keys` property and `items` property.
* Check for illegal use of `$self` and associations in expressions (may only occur
as values in an expression as part of the ON-condition in a backlink association).
Fixes
* Produce warnings instead of errors in the translation of OData annotations.
* For ODATA, in case of managed associations to draft-enabled entities, do not add
an extra foreign key for the ODATA-generated key field `IsActiveEntity`.
* For HANA, in the generated draft shadow entities, redirect all associations (not
just compositions) so that they point to the draft shadow entities.
* For ODATA V2, produce an `<EntitySet>` for `DraftAdministrativeData`, too. Ignore
the `@cds.odata.NoEntitySet` annotation.
* For ODATA V4, do not generate `<Nullable>` for `<NavigationProperty>`s that are
collections.
## Version 1.1.3
Features
* A `;` is now always optional before `}` and more often optional after a `}`.
Changes
* In `toOdata()` for v2, in the edmx the
**names of bound actions and functions now are prefixed with the corresponding entity's name**
in order to disambiguate actions and functions with the same name at two or more entities.
The corresponding implementation code in the CDS runtime needs to be adapted.
* Check `redirected to` target.
Fixes
* Make the compiler more robust wrt/ parse errors and redefinitions.
* Correctly propagate properties inside `returns` and `items`.
* Some corrections to EDM `ActionImport` and `FunctionImport` in ODATA V2 and V4.
* Generate correct joins for mixin associations that are traversed with different filters.
* Generate joins not only for views, but also for projections.
* For entities annotated with `@odata.draft.enabled`, make all non-key fields nullable in
`toOdata()`.
## Version 1.1.2
Features
* Allow reserved names for annotations/properties in assignments.
* Allow final `,` for much more "lists" (e.g. arguments).
* It is now possible to omit the select list in a view definition,
which is the same as writing `select from <name> {*}`.
* Allow `array of` as type spec for a parameter definition.
* SQL generation for sqlite now supports a mode where associations are resolved
to joins.
Changes
* Improved messages for syntax errors.
* `where` now is a reserved keyword and so cannot be used anymore as name at many places.
Fixes
* In `toOdata()` with the `hdbcds` naming convention, the value of the `@cds.persistence.name`
annotation now uses `.` rather than `_` as separator for the names of flattened structured
entity elements.
* Numeric values in OData annotations are now correctly mapped to edmx.
## Version 1.1.1

@@ -4,0 +172,0 @@

@@ -35,4 +35,2 @@ # API Migration

| `toOdataOutput(model, {oDataVersion: 'v2'}` | `toOdata(model, {version: 'v2', xml: true, json: true, separate: true, combined: true, csn: true})`|
| `exportAnnotations(model)` | `toI18n(model)`|
| `exportAnnosUi5Style` | `toI18n(model, {style: 'ui5'})`|
| `toSqlDdl(model)` | `toSql(model)`|

@@ -60,4 +58,4 @@ | `compactJson(model)` | `toCsn(model)`|

// options : {
// toHana.names : either 'flat' (generate uppercased flattened entity names with
// underscores) or 'deep' (default, generate entity names with nested
// toHana.names : either 'plain' (generate uppercased flattened entity names with
// underscores) or 'quoted' (default, generate entity names with nested
// contexts as in CDL)

@@ -70,3 +68,3 @@ // toHana.associations : either 'assocs' (default, keep associations as they are if possible)

// Options provided here are merged with (and take precedence over) options from 'model'.
// If 'toHana.names' is not provided, 'deep' is used.
// If 'toHana.names' is not provided, 'quoted' is used.
// If 'toHana.associations' is not provided, 'assocs' is used.

@@ -196,4 +194,4 @@ // If neither 'toHana.src' nor 'toHana.csn' are provided, the default is to generate only HANA CDS

// options : {
// toSql.names : either 'flat' (generate uppercased flattened table/view names with
// underscores) or 'deep' (default, generate quoted table/view names
// toSql.names : either 'plain' (generate uppercased flattened table/view names with
// underscores) or 'quoted' (default, generate quoted table/view names
// with dots as in CDL)

@@ -206,3 +204,3 @@ // toSql.associations : either 'assocs' (default, keep associations as they are if possible)

// Options provided here are merged with (and take precedence over) options from 'model'.
// If 'toSql.names' is not provided, 'deep' is used.
// If 'toSql.names' is not provided, 'quoted' is used.
// If 'toSql.associations' is not provided, 'assocs' is used.

@@ -225,19 +223,2 @@ // If neither 'toSql.src' nor 'toSql.csn' are provided, the default is to generate only SQL DDL

### `toI18n(model, options)`
```
// Generate files for translation of localized annotations for augmented CSN 'model'.
// The following options control what is actually generated:
// options : {
// FIXME: Should not be a choice between the two styles but rather a collection of flags
// toI18n.style : can be 'prop' for property files (default) or 'ui5' for a CSN model
// }
// Options provided here are merged with (and take precedence over) options from 'model'.
// If all provided options are part of 'toI18n', the 'toI18n' wrapper can be omitted.
// FIXME: Document what is returned
function toI18n(model, options) {
...
}
```
### `toCsn(model, options)`

@@ -244,0 +225,0 @@

@@ -14,3 +14,3 @@ # Command Line Migration

Compile a CDS model given from the input files. Input files may be CDS source files (.cds), CSN
model files (.json), property files for localized annotations (.properties) and XML files (.xml)
model files (.json) and XML files (.xml)
for pre-processed ODATA annotations.

@@ -26,7 +26,7 @@

-H, --to-hana <flags> Generate HANA CDS source, <flags> can be a comma-separated combination
of either "flat" or "deep" (default) for entity names, either "assocs"
of either "plain" or "quoted" (default) for entity names, either "assocs"
(default) or "joins" for associations and any of "src,csn"
flat : produce uppercased flattened HANA entity names with
plain : produce uppercased flattened HANA entity names with
underscores
deep : produce HANA entity names with dots and nested contexts
quoted : produce HANA entity names with dots and nested contexts
as in CDL

@@ -50,8 +50,8 @@ assocs : keep associations in HANA CDS as far as possible

-Q, --to-sql <flags> Generate SQL DDL statements, <flags> can be a comma-separated
combination of either "flat" or "deep" (default) for entity names,
combination of either "plain" or "quoted" (default) for entity names,
either "assocs" (default) or "joins" for associations and any of
"src,csn"
flat : produce uppercased flattened table/view names with
plain : produce uppercased flattened table/view names with
underscores
deep : produce quoted table/view names with dots
quoted : produce quoted table/view names with dots
assocs : keep associations as far as possible (only usable for

@@ -62,8 +62,2 @@ HANA SQL)

csn : generate "sql_csn.json" with SQL-preprocessed model
-I, --to-i18n <style> Generate files for translation of localized annotations, <style> can
be either "prop" (default) for a property file or "ui5" for a UI5-
style combination of property file and modified model
prop : generate property file with annotation text
ui5 : generate model with placeholders and property file with
annotation text
-l, --lint-mode Generate nothing, just produce single-file error messages if any (for

@@ -120,4 +114,2 @@ use by editors)

| `cdsv --hana-preprocess ...` | `cdsc --to-hana csn ...` |
| `cdsv --export-annotations ...` | `cdsc --to-i18n prop ...` |
| `cdsv --export-annos-ui5-style ...` | `cdsc --to-i18n ui5 ...` |
| `cdsv --cdl-output ...` | `cdsc --to-cdl ...` |

@@ -124,0 +116,0 @@ | `cdsv --to-sql ...` | `cdsc --to-sql src ...` |

@@ -69,6 +69,6 @@ # Error Messages Explained

```
e.cds:3:20-26: Error: No 'EXTEND artifact' within 'EXTEND CONTEXT'
e.cds:4:20-28: Error: No 'ANNOTATE artifact' within 'EXTEND SERVICE'
e.cds:5:14-22: Error: No 'ANNOTATE artifact' within 'ANNOTATE context'
e.cds:6:12-36: Error: No 'EXTEND artifact' or element definition within 'EXTEND service'
e.cds:3:20-26: Error: No `EXTEND artifact` within CONTEXT extensions
e.cds:4:20-28: Error: No `ANNOTATE artifact` within SERVICE extensions
e.cds:5:14-22: Error: Elements only exist in entities, types or typed constructs
e.cds:6:12-36: Error: Elements only exist in entities, types or typed constructs
```

@@ -90,9 +90,12 @@

The reason for these messages is – if we would allow it:
* The [name resolution](NameResolution.md) would become more complex due to extra rules;
e.g. most people would expect to write just `E` instead `C.E`/`S.E` in line 3 and 4.
* The CSN would list the extensions for `E` for line 5 and 6 inside a property named `elements`
which would look strange.
* The CDL parser expects `E` in line 6 to be an element,
and therefore accepts CDL clauses which are useful for elements, not entities.
* If we follow the [normal name resolution rules](NameResolution.md),
people would have to refer to the entity the same way
as outside `extend context`/`extend service`.
Most people would probably expect being able
to write just `E` instead `C.E`/`S.E` in line 3 and 4,
but this not only require special rules, but leads to other surprises – see below.
* Using `{ … }` inside a plain `annotate` or `extend` statement
is supposed to annotate/extend elements (or enums), not containing artifacts.
The CDL code can be corrected as follows:

@@ -109,1 +112,27 @@

Now consider that you could use the following to extend the entity `C.E`:
```
context C { entity E { key d: Integer; } }
entity E { key x: Integer; }
extend context C {
extend E { e: Integer; } // i.e. extend C.E
}
extend context C {
entity F { a: association to E; } // target: E, not C.E (normal name resolution)
}
```
What about combining the two `extend context`:
```
context C { entity E { key d: Integer; } }
entity E { key x: Integer; }
extend context C {
extend E { e: Integer; } // i.e. extend C.E
entity F { a: association to E; } // target: E or C.E ?
}
```
In summary, allowing artifact extensions inside `extend context`/`extend service`
would provide little benefit, but would add complexity and confusion.

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

const exportAnnos = require('./i18n/export-annotations'); //export localized annotations values from a given CSN model
const exportUi5Style = require('./i18n/export-annotations-ui5-style');
const csnToSwagger = require('./render/toSwagger');

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

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

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

const alerts = require('./base/alerts');
const propagator = require('./compiler/propagator');
var { CompilationError, sortMessages } = require('./base/messages');

@@ -30,13 +29,13 @@ // Transform an augmented CSN 'model' into HANA-compatible CDS source.

// options : {
// toHana.names : either 'flat', 'deep' (default) or 'hdbcds'
// 'flat': Produce HANA entity and element names in uppercase and
// flattened with underscores. Do not generate structured
// types.
// 'deep': Produce HANA entity and element names in original case
// as in CDL. Keep nested contexts (resulting in entity names
// with dots), but flatten element names with underscores.
// Generate structured types, too.
// 'hdbcds: Produce HANA entity and element as HANA CDS would generate
// them from the same CDS source (like "deep", but using
// element names with dots).
// toHana.names : either 'plain', 'quoted' (default) or 'hdbcds'
// 'plain': Produce HANA entity and element names in uppercase and
// flattened with underscores. Do not generate structured
// types.
// 'quoted': Produce HANA entity and element names in original case
// as in CDL. Keep nested contexts (resulting in entity names
// with dots), but flatten element names with underscores.
// Generate structured types, too.
// 'hdbcds: Produce HANA entity and element as HANA CDS would generate
// them from the same CDS source (like "quoted", but using
// element names with dots).
// toHana.associations : either 'assocs' (default, keep associations as they are if possible)

@@ -48,3 +47,3 @@ // or 'joins' (replace associations by joins)

// Options provided here are merged with (and take precedence over) options from 'model'.
// If 'toHana.names' is not provided, 'deep' is used.
// If 'toHana.names' is not provided, 'quoted' is used.
// If 'toHana.associations' is not provided, 'assocs' is used.

@@ -67,6 +66,2 @@ // If neither 'toHana.src' nor 'toHana.csn' are provided, the default is to generate only HANA CDS

function toHana(model, options) {
// Must perform propagation now if it was disabled for the original CSN output
if (model.options.disablePropagate) {
model = propagator.propagateAssignments(model);
}
// Optional wrapper?

@@ -84,2 +79,14 @@ if (options && !options.toHana) {

// Backward compatibility for old naming modes
// FIXME: Remove after a few releases
const { warning, signal } = alerts(model);
if (options.toHana.names == 'flat') {
signal(warning`Option "{ toHana.names: 'flat' }" is deprecated, use "{ toHana.names: 'plain' }" instead`);
options.toHana.names = 'plain';
}
else if (options.toHana.names == 'deep') {
signal(warning`Option "{ toHana.names: 'deep' }" is deprecated, use "{ toHana.names: 'quoted' }" instead`);
options.toHana.names = 'quoted';
}
// Special case: For naming variant 'hdbcds' in combination with 'toHana' (and only there!), 'forHana'

@@ -134,3 +141,3 @@ // must leave namespaces, structs and associations alone.

// toOdata.csn : if true, generate the transformed CSN model
// toOdata.names : either 'flat', 'deep' or 'hdbcds'. If set, artifacts and elements
// toOdata.names : either 'plain', 'quoted' or 'hdbcds'. If set, artifacts and elements
// are annotated with "@cds.persistence.name" containing the

@@ -164,7 +171,3 @@ // corresponding database name (see "toHana.names" or "toSql.names")

function toOdata(model, options) {
// Must perform propagation now if it was disabled for the original CSN output
if (model.options.disablePropagate) {
model = propagator.propagateAssignments(model);
}
const { error, signal } = alerts(model);
const { error, warning, signal } = alerts(model);
// Optional wrapper?

@@ -185,2 +188,13 @@ if (options && !options.toOdata) {

// Backward compatibility for old naming modes
// FIXME: Remove after a few releases
if (options.toOdata.names == 'flat') {
signal(warning`Option "{ toOdata.names: 'flat' }" is deprecated, use "{ toOdata.names: 'plain' }" instead`);
options.toOdata.names = 'plain';
}
else if (options.toOdata.names == 'deep') {
signal(warning`Option "{ toOdata.names: 'deep' }" is deprecated, use "{ toOdata.names: 'quoted' }" instead`);
options.toOdata.names = 'deep';
}
// Perform extra-magic for TNT if requested

@@ -210,3 +224,3 @@ if (model.options.tntFlavor) {

{
let forOdata = postProcessForBackwardCompatibility(forOdataAugmented, serviceName);
let forOdata = compactForService(forOdataAugmented, serviceName);
// FIXME: Unify handling of version and tntFlavor (use original options)

@@ -259,7 +273,2 @@ let l_edm = csn2edm(forOdata, { version: options.toOdata.version, tntFlavor : options.tntFlavor });

function toCdl(model, options) {
// Must perform propagation now if it was disabled for the original CSN output
// FIXME: Hmm, or maybe we don't - this might even benefit from not propagating anything ...
if (model.options.disablePropagate) {
model = propagator.propagateAssignments(model);
}
// Merge options with those from model

@@ -299,6 +308,2 @@ options = mergeOptions({ toCdl : true }, model.options, options);

function toSwagger(model, options) {
// Must perform propagation now if it was disabled for the original CSN output
if (model.options.disablePropagate) {
model = propagator.propagateAssignments(model);
}
// Optional wrapper?

@@ -321,13 +326,15 @@ if (options && !options.toSwagger) {

// options : {
// toSql.names : either 'flat' (default), 'deep' or 'hdbcds'
// 'flat': Produce SQL table and view names in uppercase and
// flattened with underscores (no quotes required)
// 'deep': Produce SQL table and view names in original case
// as in CDL (with dots), but flatten element names with
// underscores (requires quotes).
// 'hdbcds: Produce SQL table, view and column names as HANA CDS
// would generate them from the same CDS source (like "deep",
// but using element names with dots).
// toSql.names : either 'plain' (default), 'quoted' or 'hdbcds'
// 'plain': Produce SQL table and view names in uppercase and
// flattened with underscores (no quotes required)
// 'quoted': Produce SQL table and view names in original case
// as in CDL (with dots), but flatten element names with
// underscores (requires quotes).
// 'hdbcds: Produce SQL table, view and column names as HANA CDS
// would generate them from the same CDS source (like "quoted",
// but using element names with dots).
// toSql.associations : either 'assocs' (default, keep associations as they are if possible)
// or 'joins' (replace associations by joins)
// or 'joins' (replace associations by joins). Note that some associations
// (e.g. those defined in a mixin and used in the same view) must always be
// replaced by joins because of SQL limitations.
// toSql.dialect : either 'hana' or 'sqlite' (default)

@@ -353,6 +360,2 @@ // toSql.src : if true, generate SQL DDL source files (default)

function toSql(model, options) {
// Must perform propagation now if it was disabled for the original CSN output
if (model.options.disablePropagate) {
model = propagator.propagateAssignments(model);
}
// Optional wrapper?

@@ -370,15 +373,38 @@ if (options && !options.toSql) {

// Backward compatibility for old naming modes
// FIXME: Remove after a few releases
const { warning, signal } = alerts(model);
if (options.toSql.names == 'flat') {
signal(warning`Option "{ toSql.names: 'flat' }" is deprecated, use "{ toSql.names: 'plain' }" instead`);
options.toSql.names = 'plain';
}
else if (options.toSql.names == 'deep') {
signal(warning`Option "{ toSql.names: 'deep' }" is deprecated, use "{ toSql.names: 'quoted' }" instead`);
options.toSql.names = 'quoted';
}
// FIXME: Currently, '--to-sql' implies transformation for HANA (transferring the options to forHana)
let forHanaOptions = options.toSql;
// Special case: For naming variant 'hdbcds' in combination with 'toSql', 'forHana' must leave
// namespaces alone (but must still flatten structs because we need the leaf element names).
if (options.toSql.names == 'hdbcds') {
options = mergeOptions(options, { forHana : { keepNamespaces: true } });
forHanaOptions.keepNamespaces = true;
}
// FIXME: Currently, '--to-sql' implies transformation for HANA (transferring the options to forHana)
let forSqlAugmented = transformForHana(model, mergeOptions(options, { forHana : options.toSql } ));
// Because (even HANA) SQL cannot deal with associations in mixins that are published in the same view,
// the association processing must at least be 'mixin', even if callers specified 'assocs' or nothing
if (forHanaOptions.associations == 'assocs') {
forHanaOptions.associations = 'mixin';
}
// FIXME: Should not be necessary
forHanaOptions.alwaysResolveDerivedTypes = true;
let forSqlAugmented = transformForHana(model, mergeOptions(options, { forHana : forHanaOptions } ));
// Assemble result
let result = {};
if (options.toSql.src) {
result.sql = toSqlDdl(forSqlAugmented);
result = options.oldSqlImpl ? toSqlDdl(forSqlAugmented) : toSqlDdlNew(forSqlAugmented);
}

@@ -399,8 +425,8 @@ if (options.toSql.csn) {

// Generate SQL DDL rename statements for a migration, renaming existing tables and their
// columns so that they match the result of "toHana" or "toSql" with the "{ names: 'flat' }
// columns so that they match the result of "toHana" or "toSql" with the "{ names: 'plain' }
// option.
// Expects the naming convention of the existing tables to be either 'deep' or 'hdbcds' (default).
// Expects the naming convention of the existing tables to be either 'quoted' or 'hdbcds' (default).
// The following options control what is actually generated:
// options : {
// toRename.names : existing names, either 'deep' or 'hdbcds' (default)
// toRename.names : existing names, either 'quoted' or 'hdbcds' (default)
// }

@@ -423,8 +449,4 @@ // Return a dictionary of top-level artifacts by their names, like this:

function toRename(model, options) {
const { error, signal } = alerts(model);
const { error, warning, signal } = alerts(model);
// Must perform propagation now if it was disabled for the original CSN output
if (model.options.disablePropagate) {
model = propagator.propagateAssignments(model);
}
// Optional wrapper?

@@ -437,2 +459,13 @@ if (options && !options.toRename) {

// Backward compatibility for old naming modes
// FIXME: Remove after a few releases
if (options.toRename.names == 'flat') {
signal(warning`Option "{ toRename.names: 'flat' }" is deprecated, use "{ toRename.names: 'plain' }" instead`);
options.toRename.names = 'plain';
}
else if (options.toRename.names == 'deep') {
signal(warning`Option "{ toRename.names: 'deep' }" is deprecated, use "{ toRename.names: 'quoted' }" instead`);
options.toRename.names = 'quoted';
}
// Requires beta mode

@@ -464,43 +497,26 @@ if (!options.betaMode) {

// Generate files for translation of localized annotations for augmented CSN 'model'.
// The following options control what is actually generated:
// options : {
// FIXME: Should not be a choice between the two styles but rather a collection of flags
// toI18n.style : can be 'prop' for property files (default) or 'ui5' for a CSN model
// }
// Options provided here are merged with (and take precedence over) options from 'model'.
// If all provided options are part of 'toI18n', the 'toI18n' wrapper can be omitted.
// Returns Object with having as property names the representation of the location of the annotation
// and respective values - the annotation string value
function toI18n(model, options) {
// No propagation required (actually, we are better off better without it)
// Optional wrapper?
if (options && !options.toI18n) {
options = { toI18n : options };
}
// Provide defaults and merge options with those from model
options = mergeOptions({ toI18n : getDefaultBackendOptions().toI18n }, model.options, options);
// Generate what was requested
if (options.toI18n.style == 'prop') {
return exportAnnos(model, options);
} else if (options.toI18n.style == 'ui5') {
return exportUi5Style(model, options);
} else {
throw new Error(`Invalid style ${options.toI18n.style} for toI18n`);
}
}
// Generate compact CSN for augmented CSN 'model'
// The following options control what is actually generated:
// options : {
// testMode : if true, the result is extra-stable for automated tests (sorted, no 'version')
// testMode : if true, the result is extra-stable for automated tests (sorted, no 'version')
// newCsn : if true, CSN is returned in the (well-defined) new format '0.1.99' planned for future versions
// (default is the old format '0.1.0')
// toCsn.gensrc : if true, the result CSN is only suitable for use as a source, e.g. for combination with
// additional extend/annotate statements, but not for consumption by clients or backends
// (default is to produce 'client' CSN with all properties propagated and inferred as required
// by consumers and backends)
// }
// Options provided here are merged with (and take precedence over) options from 'model'.
// Throws a CompilationError on errors.
function toCsn(model, options) {
// Propagation has been performed (or not) by the compiler - leave that as it was.
const { error, signal } = alerts(model);
// Can't have an optional wrapper here because 'testMode' and 'newCsn' are global options
// Merge options with those from model
options = mergeOptions({ toCsn : true }, model.options, options);
if (options.toCsn.gensrc && !options.newCsn) {
signal(error`CSN in "gensrc" flavor can only be generated as new-style CSN (option "newCsn" required)`);
throw new CompilationError(sortMessages(model.messages), model);
}
return options.newCsn ? compactModel(model)

@@ -517,3 +533,3 @@ : (options.testMode ? compactSorted(model) : compact(model));

toHana: {
names : 'flat',
names : 'plain',
associations: 'assocs',

@@ -528,9 +544,6 @@ },

toSql: {
names : 'flat',
names : 'plain',
associations: 'assocs',
dialect: 'sqlite',
},
toI18n: {
style: 'prop',
},
};

@@ -545,3 +558,2 @@ }

toSql,
toI18n,
toCsn,

@@ -548,0 +560,0 @@ getDefaultBackendOptions,

@@ -25,2 +25,4 @@ // Functions for dictionaries (Objects without prototype)

messageCallback( name, found.name.location, found );
if (messageCallback !== null)
found.$duplicate = true;
}

@@ -38,5 +40,9 @@ }

messageCallback( name, found.name.location, found );
if (messageCallback !== null)
found.$duplicate = true;
}
if (messageCallback)
messageCallback( name, entry.name.location, entry );
if (messageCallback !== null)
entry.$duplicate = true;
}

@@ -66,3 +72,3 @@ return entry;

function clearDict( parent, env, inPlace ) {
if (!inPlace)
if (!inPlace || !parent[env])
parent[env] = Object.create(null);

@@ -121,2 +127,9 @@ else {

function forEachInDict( dict, callback ) {
let r = Object.create(null);
for (let name of Object.keys(dict))
r[name] = callback( dict[name], name, dict );
return r;
}
module.exports = {

@@ -128,4 +141,5 @@ addToDict,

combinedLocation,
pushToDict
pushToDict,
forEachInDict,
}

@@ -5,2 +5,53 @@ // Functions and classes for syntax messages

// For messageIds, where no severity has been provided via code (central def)
const standardSeverities = {
'syntax-anno-after-struct': ['Error'],
'syntax-anno-after-enum': ['Error'],
'syntax-anno-after-params': ['Error'],
'ref-undefined-def': 'Error',
'ref-undefined-art': 'Error',
'anno-undefined-def': 'Info', // for annotate statement (for CSN or CDL path cont)
'anno-undefined-art': 'Info', // for annotate statement (for CDL path root)
'anno-undefined-element': 'Info',
'anno-undefined-action': 'Info',
'anno-undefined-param': 'Info',
}
// For messageIds, where no text has been provided via code (central def)
const standardTexts = {
'syntax-anno-after-struct': 'Avoid annotation assignments after structure definitions',
'syntax-anno-after-enum': 'Avoid annotation assignments after enum definitions',
'syntax-anno-after-params': 'Avoid annotation assignments after parameters',
'ref-undefined-def': {
std: 'Artifact $(ART) has not been found',
element: 'Artifact $(ART) has no element $(MEMBER)'
},
'ref-undefined-art': 'No artifact has been found with name $(NAME)',
'ref-undefined-element': {
std: 'Element $(ART) has not been found',
element: 'Artifact $(ART) has no element $(MEMBER)'
},
'anno-undefined-def': 'Artifact $(ART) has not been found',
'anno-undefined-art': 'No artifact has been found with name $(NAME)',
'anno-undefined-element': {
std: 'Element $(ART) has not been found',
element: 'Artifact $(ART) has no element $(MEMBER)',
enum: 'Artifact $(ART) has no enum $(MEMBER)'
},
'anno-undefined-action': {
std: 'Action $(ART) has not been found',
action: 'Artifact $(ART) has no action $(MEMBER)'
},
'anno-undefined-param': {
std: 'Parameter $(ART) has not been found',
param: 'Artifact $(ART) has no parameter $(MEMBER)'
},
'expected-const': 'A constant value is expected here',
'expected-struct': 'A structured type or a non-query entity is expected here',
'expected-context': 'A context or service is expected here',
'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',
}
function hasErrors( messages ) {

@@ -32,12 +83,19 @@ return messages && messages.some( m => m.severity === 'Error' );

// `model`: the CSN model
// TODO: standard param order
class CompilationError extends Error {
constructor(errs, model, ...args) {
super(...args);
this.errors = errs;
this.model = model;
constructor(errs, model, text, ...args) {
super( text || 'CDS compilation failed\n' + errs.map( m => m.toString() ),
...args );
this.errors = errs; // TODO: rename to messages
Object.defineProperty( this, 'model', { value: model, configurable: true } );
}
toString() { // does not really help -> set message
return this.message.includes('\n')
? this.message
: this.message + '\n' + this.errors.map( m => m.toString() );
}
}
// Class for individual compile errors.
// TODO: an error code would be good in future (configuration, translation, …)
// TODO: make it really extend Error?, change param order
class CompileMessage extends Error {

@@ -49,6 +107,7 @@ constructor(location, msg, severity = 'Error', id) {

if (id)
this.messageId = id;
Object.defineProperty( this, 'messageId', { value: id } );
// this.messageId = id; // ids not yet finalized
}
toString() { // should have no argument...
return messageString(this);
return messageString( this, undefined, true ); // no message-id before finalization!
}

@@ -85,6 +144,10 @@ }

return function message( id, location, params = {}, severity = undefined, texts = undefined ) {
let s = normalizedSeverity( severity ); // as specified
if (!severity) // TODO: check that they are always eq per messageId
severity = standardSeverities[id];
let s = normalizedSeverity( severity );
if ((s !== 'Error' || severity instanceof Array) && id && id in config )
s = normalizedSeverity( config[id] );
let text = (typeof params === 'string') ? params : messageText( texts, params );
let text = (typeof params === 'string')
? params
: messageText( texts || standardTexts[id], params );
let msg = new CompileMessage( location, text, s, id );

@@ -97,6 +160,10 @@ model.messages.push( msg );

const paramsTransform = {
alias: n => msgName(n),
alias: msgName,
anno: transformAnno,
art: transformArg,
id: n => msgName(n)
code: n => '`' + n + '`',
name: msgName,
names: transformManyWith( msgName ),
id: msgName,
file: s => "'" + s.replace( /'/g, "''" ) + "'", // sync ;
};

@@ -112,3 +179,3 @@

function transformArg( arg, r, args, texts ) {
if (!arg || typeof arg !== 'object' || !texts.elem || args.elem != null)
if (!arg || typeof arg !== 'object' || args['#'] || args.member )
return msgName( arg );

@@ -119,9 +186,40 @@ if (arg._artifact)

arg = arg.name;
if (!arg.element)
let prop = ['element','param','action','alias'].find( p => arg[p] );
if (!prop || !texts[prop] )
return msgName( arg );
r[''] = 'elem'; // text variant 'elem'
r.elem = msgName( arg.element );
return msgName( arg, true );
r['#'] = texts[ arg.$variant ] && arg.$variant || prop; // text variant (set by searchName)
r.member = msgName( arg[prop] );
return msgName( arg, prop );
}
function transformManyWith( t ) {
return function transformMany( many, r, args, texts ) {
let prop = ['none','one'][ many.length ];
if (!prop || !texts[prop] || args['#'] )
return many.map(t).join(', ');
r['#'] = prop; // text variant
return many.length && t( many[0] );
};
}
const nameProp = {
enum: 'element',
key: 'element',
function: 'action',
};
function searchName( art, id, variant ) {
if (!variant) {
// TODO: probably use null instead undefinedArtifact in resolver.js
let type = art._finalType && art._finalType.kind !== 'undefined' ? art._finalType : art;
art = type.target && type.target._artifact || type;
variant = ['context','service','namespace'].includes(art.kind) ? 'absolute' : 'element';
}
let prop = nameProp[variant] || variant;
let name = Object.assign( {}, (art._artifact||art).name );
name[prop] = (name[prop]) ? name[prop] + '.' + id : id;
name.$variant = variant;
return name;
}
function messageText( texts, params ) {

@@ -135,3 +233,3 @@ if (typeof texts === 'string')

}
let variant = args[''];
let variant = args['#'];
return replaceInString( variant && texts[ variant ] || texts.std, args );

@@ -152,3 +250,3 @@ }

parts.push( text.substring( start ) );
let remain = Object.keys( params ).filter( n => n && params[n] );
let remain = ('#' in params) ? [] : Object.keys( params ).filter( n => params[n] );
return (remain.length)

@@ -194,3 +292,3 @@ ? parts.join('') + '; ' +

// Return string for complete reference
function refString( name, noElement ) {
function refString( name, omit ) {
// prepare that resolvePath does not set ref.absolute etc:

@@ -204,10 +302,11 @@ if (name._artifact)

compact = '.$alias.' + name.alias;
if (name.action)
if (name.action && omit !== 'action')
compact = '.$action.' + name.action;
if (name.param)
if (name.param && omit !== 'param')
compact += '.$param.' + name.param;
if (name.element && !noElement)
if (name.element && omit !== 'element')
compact += (compact ? '.' : '..') + name.element;
// Yes, omit $query.0 -> test is (name.query), not (name.query != null)
return name.absolute + (name.query ? '.$query.' + name.query : '') + compact;
return name.absolute +
(name.query ? '.$query.' + name.query : '') + compact;
}

@@ -218,4 +317,4 @@

// function
function msgName( ref, noElement ) {
let name = (typeof ref === 'string') ? ref : refString( ref, noElement );
function msgName( ref, omit ) {
let name = (typeof ref === 'string') ? ref : refString( ref, omit );
return '"' + name.replace( /"/g, '""' ) + '"'; // sync ";

@@ -229,2 +328,3 @@ }

refString,
searchName,
getMessageFunction,

@@ -231,0 +331,0 @@ msgName,

@@ -5,47 +5,11 @@ //

const kindProperties = {
// TODO: also foreignKeys ?
namespace: { artifacts: true }, // on-the-fly context
context: { artifacts: true, normalized: 'namespace' },
service: { artifacts: true, normalized: 'namespace' }, // actions: true with "service-bound" actions
entity: { elements: true, actions: true, params: () => false },
view: { elements: true, actions: true, params: () => false }, // elements disabled via special check
query: { elements: true },
$tableAlias: { normalized: 'alias', $navigation: true }, // table alias in select
$navElement: { normalized: 'element', $navigation: true },
type: { elements: propExists, enum: propExists },
annotation: { elements: propExists, enum: propExists },
const: {},
enum: { normalized: 'element' },
package: {},
accesspolicy: { artifacts: true },
role: {},
aspect: {},
element: { elements: propExists, enum: propExists, dict: 'elements' },
action: { params: () => false, elements: () => false, enum: () => false, dict: 'actions' }, // no extend params, only annotate
function: { params: () => false, elements: () => false, enum: () => false, normalized: 'action' }, // no extend params, only annotate
key: { normalized: 'element' },
param: { elements: () => false, enum: () => false, dict: 'params' },
source: { artifacts: true },
block: { artifacts: true },
using: {},
extend: { isExtension: true },
annotate: { isExtension: true, elements: true, enum: true, actions: true, params: true },
builtin: {}, // = CURRENT_DATE, TODO: improve
}
const queryOps = {
query: true, // TODO: rename to SELECT
union: true,
unionAll: true,
intersect: true,
except: true,
subquery: true, // for (subquery) with ORDER BY or LIMIT/OFFSET
query: 'select', // TODO: rename to SELECT
union: 'union',
unionAll: 'union',
intersect: 'union',
except: 'union',
subquery: 'union', // for (subquery) with ORDER BY or LIMIT/OFFSET
}
function propExists( prop, parent ) {
let obj = parent.returns || parent;
return (obj.items || obj)[prop];
}
// Apply function `callback` to all artifacts in dictionary

@@ -137,2 +101,4 @@ // `model.definitions`. See function `forEachGeneric` for details.

// FIXME: Do we really need this ?
// If `withArtifactLink` is set, the `_artifact` links of `node` are copied too (not cloned,
// of course).
// Regardless of their names, transformers are never applied to dictionary elements.

@@ -142,3 +108,3 @@ //

// transformer(value, node, resultNode, key)
function cloneWithTransformations(node, transformers) {
function cloneWithTransformations(node, transformers, withArtifactLink) {

@@ -171,2 +137,8 @@ return transformNode(node);

}
// For non-dictionaries, take over `_artifact` if requested
if (withArtifactLink && proto) {
let pd = Object.getOwnPropertyDescriptor(node, '_artifact')
if (pd)
Object.defineProperty(resultNode, '_artifact', pd);
}
return resultNode;

@@ -178,3 +150,2 @@ }

module.exports = {
kindProperties,
queryOps,

@@ -181,0 +152,0 @@ forEachDefinition,

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

if (!anno.literal && !anno.path && getFinalTypeNameOf(elementDecl) != 'cds.Boolean') {
if (elementDecl.type && elementDecl.type.absolute) {
signal(warning`Expecting a value of type "${elementDecl.type.absolute}" for the annotation`, anno.location || anno.name.location);
if (elementDecl.type && elementDecl.type._artifact.name.absolute) {
signal(warning`Expecting a value of type "${elementDecl.type._artifact.name.absolute}" for the annotation`, anno.location || anno.name.location);
} else {

@@ -238,6 +238,9 @@ signal(warning`Expecting a value for the annotation`, anno.location || anno.name.location);

function getFinalTypeNameOf(node) {
return node._finalType.type && node._finalType.type.absolute;
let type = node._finalType;
if (type.type)
type = type.type._artifact;
return type && type.name && type.name.absolute;
}
}
module.exports = checkAnnotationAssignments;
module.exports = checkAnnotationAssignments;

@@ -7,24 +7,2 @@ 'use strict';

// For an artifact 'artifact' that has an 'implemented in' clause, check its legality
function checkArtifactImplementedIn(artifact, model) {
const { error, signal } = alerts(model);
if (!isAllowedId(artifact.impl.id)) {
signal(error`'${artifact.impl.id}' is not an allowed id for the syntax 'entity <name> implemented in <id>'`, artifact.impl.location);
}
}
// we reserve all ids that do not contain a ":" or that start with
// CDS:, CDX:, HANA:, SQL:, CQL: (case insensitive)
// reserve means: we currently do not allow them, because we may relate
// them with some special semantic at a later point in time
// temporarily we also allow "calcview" (although it doesn't contain a ":") for
// compatibility reasons
function isAllowedId(id) {
// temporarily allow "calcview" for compatibility reasons
// id must contain a ":"
// id must not start with CDS, CDX, ...
return ( (id === 'calcview') ||
(id.match(/^.+:.+$/g) !== null && id.match(/^(CDS|CDX|HANA|SQL|CQL):.+/ig) === null) );
}
// Check that artifact 'art' is not empty and does not contain only virtual elements

@@ -73,5 +51,4 @@ // (not applicable to abstract artifacts)

module.exports = {
checkArtifactImplementedIn,
checkNotEmptyOrOnlyVirtualElems,
checkNoUnmanagedAssocsInGroupByOrderBy,
};

@@ -6,17 +6,26 @@ 'use strict';

// Check when there is a managed association 'm' without explicit declared foreign keys,
// that the target entity does specify at least one key
function checkManagedAssoc(m, model) {
const { error, signal } = alerts(model);
if (isManagedAssocWithoutKeys(m) && !m.redirected) {
if(m.target.absolute !== undefined) { // non-resolved targets have no 'absolute'
signal(error`The target ${m.target.absolute} of the association ${m.name.id} does not have key(s).`, m.target.location);
}
// Perform checks for element (or type) 'elem' concerning managed associations,
// only for managed assocs directly declared on 'elem', not those from derived
// types etc (checked there): If no explicit foreign keys are provided, there
// must be at least one implicit one (i.e. the target must have keys).
// Generally, the cardinality of a managed association should not be to-many.
function checkManagedAssoc(elem, model) {
const { error, warning, signal } = alerts(model);
let target = elem.target;
// Not a managed assoc at all or not inferred => nothing to check
if (!target || elem.on || elem.onCond || elem.$inferred) {
return;
}
// No foreign keys at all => error
let foreignKeys = elem.foreignKeys;
if (!foreignKeys) {
signal(error`The target "${target._artifact.name.absolute}" of the managed association "${elem.name.id}" does not have keys.`, elem.location);
}
// Cardinality to-many => warning
let targetMax = (elem.cardinality && elem.cardinality.targetMax && elem.cardinality.targetMax.val);
if (targetMax == '*' || Number(targetMax) > 1) {
signal(warning`The association "${elem.name.id}" has cardinality "to many" but no ON-condition`, elem.location);
}
}
function isManagedAssocWithoutKeys(elem) {
return elem.target && !elem.on && !elem.onCond && !elem.foreignKeys;
}
// Check element 'elem' for semantical errors involving virtual elements

@@ -134,4 +143,7 @@ function checkVirtualElement(elem, model) {

function isAssoc(obj) {
return obj && obj.type && obj.type.absolute && obj.type.absolute === 'cds.Association';
function isAssoc(obj) { // also checks for Compositions
if (!obj)
return obj;
let type = obj._finalType || obj;
return !!type.target;
}

@@ -149,13 +161,15 @@

if (node.typeArguments) {
signal(error`Too many parameters in type reference to '${node.type.absolute}'`,
signal(error`Too many parameters in type reference to '${node.type._artifact.name.absolute}'`,
node.typeArguments[0].location);
}
let parameters = node.type._artifact? node.type._artifact.parameters : [];
let type = node.type._artifact;
let absolute = type && type.name && type.name.absolute;
for (let name in parameters) {
let param = parameters[name];
if (!node[param] && node.type.absolute != "hana.ST_POINT" && node.type.absolute != "hana.ST_GEOMETRY")
signal(error`Actual value for type parameter '${param}' missing in reference to type '${node.type.absolute}'`,
if (!node[param] && absolute != "hana.ST_POINT" && absolute != "hana.ST_GEOMETRY")
signal(error`Actual value for type parameter '${param}' missing in reference to type '${absolute}'`,
node.type.location );
}
switch (node.type.absolute) {
switch (absolute) {
case "cds.String":

@@ -200,3 +214,3 @@ case "cds.Binary":

if (!(Number.isInteger(paramValue) && paramValue >= 0)) {
signal(error`Actual parameter '${paramName}' for '${node.type.absolute}' must be positive integer`,
signal(error`Actual parameter '${paramName}' for '${node.type._artifact.name.absolute}' must be positive integer`,
node[paramName].location);

@@ -211,3 +225,3 @@ return false;

if (range.max && paramValue > range.max) {
signal(error`Actual parameter '${paramName}' for '${node.type.absolute}' is larger than allowed (max: ${range.max})`,
signal(error`Actual parameter '${paramName}' for '${node.type._artifact.name.absolute}' is larger than allowed (max: ${range.max})`,
node[paramName].location);

@@ -217,3 +231,3 @@ return false;

if (range.min && paramValue < range.min) {
signal(error`Actual parameter '${paramName}' for '${node.type.absolute}' is smaller than allowed (min: ${range.min})`,
signal(error`Actual parameter '${paramName}' for '${node.type._artifact.name.absolute}' is smaller than allowed (min: ${range.min})`,
node[paramName].location);

@@ -227,2 +241,33 @@ return false;

// Check that min and max cardinalities of 'elem' in 'art' have legal values
function checkCardinality(elem, model) {
const { error, signal } = alerts(model);
if (!elem.cardinality) {
return;
}
// Max cardinalities must be a positive number or '*'
for (let prop of ['sourceMax', 'targetMax']) {
if (elem.cardinality[prop]) {
if (!(elem.cardinality[prop].literal == 'number' && elem.cardinality[prop].val > 0
|| elem.cardinality[prop].literal == 'string' && elem.cardinality[prop].val == '*')) {
signal(error`Element "${elem.name.absolute}.${elem.name.id}": Illegal value "${elem.cardinality[prop].val}" for max cardinality (must a positive number or "*")`, elem.cardinality[prop].location);
}
}
}
// Min cardinality must be a non-negative number (already checked by parser)
if (elem.cardinality.targetMin) {
if (!(elem.cardinality.targetMin.literal == 'number' && elem.cardinality.targetMin.val >= 0)) {
signal(error`Element "${elem.name.absolute}.${elem.name.id}": Illegal value "${elem.cardinality.targetMin.val}" for min cardinality (must a non-negative number)`, elem.cardinality.targetMin.location);
}
}
// If provided, min cardinality must not exceed max cardinality (note that '*' is considered to be >= any number)
if (elem.cardinality.targetMin && elem.cardinality.targetMax && elem.cardinality.targetMax.literal == 'number'
&& elem.cardinality.targetMin.val > elem.cardinality.targetMax.val) {
signal(error`Element "${elem.name.absolute}.${elem.name.id}": Target minimum cardinality must not be greater than target maximum cardinality`, elem.cardinality.location);
}
}
module.exports = {

@@ -232,2 +277,3 @@ checkManagedAssoc,

checkTypeParameters,
checkCardinality,
};
'use strict';
const has = Object.prototype.hasOwnProperty;
const alerts = require('../base/alerts');

@@ -11,140 +10,132 @@

return {
checkBoundActionOrFunction,
checkUnboundActionOrFunction,
checkActionOrFunctionParameter,
};
checkActionOrFunction,
checkActionOrFunctionParameter
}
// Check a bound action or function (i.e. one that lives within an entity or view)
function checkBoundActionOrFunction(act) {
function checkActionOrFunction(act, serviceName) {
if (Array.isArray(act)) // duplicated declaration -> will be handle in some of the other steps of the compiler
return undefined;
// in ODATA v2 scalar types cannot be returned
if (hasReturnType(act) && options.toOdata && options.toOdata.version == 'v2') {
let typeAbs = getPropFromChain(act, ['returns', 'type', 'absolute']);
if (typeAbs && isScalar(act, typeAbs))
signal(warning`Scalar return types are not allowed in OData V2`, act.location);
return;
let location = act.location;
if (!act.name.action) { // unbound action or function
let actServiceName = getAbsNameWithoutId(act.name.absolute);
if (actServiceName !== serviceName)
signal(warning`Functions and actions must be declared in a service or bound to an entity`, location);
}
// check if return type is entity from the current service
if (hasReturnType(act) && isArrayed(act.returns))
checkReturnTypeArray(act);
else if (getPropFromChain(act, ['returns', 'type', '_artifact', 'kind']) === 'entity')
checkReturnEntity(act);
else if (getPropFromChain(act, ['returns', 'type', '_artifact', 'kind']) === 'type' &&
!getPropFromChain(act, ['returns', 'type', '_artifact', 'builtin']))
checkReturnUserDefinedType(act);
// FIXME: What is the semantics of the return value (here and elsewhere in this file?)
return undefined;
}
if (act.origin) {
act = act.origin._artifact || /* old csn does not have the _artifact */ act;
if (act.params) // params are still not propagated at this point, so have to be checked separately
Object.keys(act.params).forEach(p => checkActionOrFunctionParameter(act.params[p], serviceName, location));
}
function checkReturnEntity(act) {
let returnBlockName = getPropFromChain(act, ['returns', 'type', '_artifact', '_block', 'name', 'absolute']);
let actBlockName = getPropFromChain(act, ['_block', 'name', 'absolute']);
if (returnBlockName !== actBlockName)
return signal(warning`The return entity must be from the current service '${actBlockName}'`, act.location);
return undefined;
}
// check all the cases of the return statement
if (act.returns) {
if (act.returns._finalType.builtin) {
// in ODATA v2 scalar types cannot be returned
if (options.toOdata && options.toOdata.version === 'v2')
signal(warning`Scalar return types are not allowed in OData V2`, location);
return;
}
// anonymous types not supported yet by OData processor
if (act.returns._finalType.elements && !act.returns._finalType.kind)
signal(warning`Anonymous types not supported`, location);
// check array return type
if (act.returns.items)
checkReturnTypeArray(act);
// check if return type is entity from the current service
else if (act.returns._finalType.kind === 'entity')
checkReturnEntity(act);
else if (act.returns._finalType.kind === 'type')
checkReturnUserDefinedType(act);
}
function checkReturnTypeArray(act) {
if (!(hasReturnType(act) && isArrayed(act.returns)))
return undefined;
let returnKind = getPropFromChain(act, ['returns', 'items', 'type', '_artifact', 'kind']);
if (!returnKind || returnKind !== 'entity')
return signal(warning`The action '${act.name.id}' can only return an array of entities`, act.location);
let returnTypeEntity = getPropFromChain(act, ['returns', 'items', 'type', '_artifact', '_block', 'name', 'absolute']);
let actEntity = getPropFromChain(act, ['_parent', '_block', 'name', 'absolute']) /* when bound*/ || getPropFromChain(act, ['_block', 'name', 'absolute']) /* when unbound */;
if (returnTypeEntity !== actEntity)
return signal(warning`The action '${act.name.id}' can only return an array of entities from the current service '${actEntity}'`, act.location);
return undefined;
}
function checkReturnTypeArray(act) {
if (!act.returns.items)
return;
// anonymous types not supported yet by OData processor
if (act.returns._finalType.items.elements && !act.returns._finalType.kind)
signal(warning`Anonymous types not supported`, location);
function checkReturnUserDefinedType(act) {
let actParentName = getPropFromChain(act, ['_block', 'name', 'absolute']);
let returnsBlockName = getPropFromChain(act, ['returns', 'type', '_artifact', '_block', 'name', 'absolute']);
if (actParentName !== returnsBlockName)
return signal(warning`The user defined return type of action '${act.name.id}' must be from the current service '${actParentName}'`, act.location);
return undefined;
}
// array of array is not allowed
if (act.returns._finalType.items._finalType.items)
signal(warning`Array element cannot be an array`, location);
// Check parameter 'param' of action or function 'act'
function checkActionOrFunctionParameter(param, act) {
let paramArtifact = getPropFromChain(param, ['type', '_artifact']);
if (options.toOdata && options.toOdata.version == 'v4' && paramArtifact.kind && paramArtifact.kind === 'entity')
checkEntityParam(act, paramArtifact);
if (options.toOdata && options.toOdata.version == 'v2')
if (!isScalar(act, getPropFromChain(param, ['type', 'absolute'])))
signal(warning`Only scalar type input parameters are allowed in OData V2`, act.location);
// check when the parameter is of user defined type
if (paramArtifact.kind && paramArtifact.kind === 'type') {
checkUserDefinedTypeParam(act, paramArtifact, param.name.id);
// array of builtin not allowed in OData V2
if (act.returns.items._finalType.builtin && options.toOdata && options.toOdata.version === 'v2')
signal(warning`Scalar return types are not allowed in OData V2`, location);
// array of <entity> - check if entity is from the current service
if (act.returns.items._finalType.kind === 'entity')
checkReturnEntity(act);
}
function checkEntityParam(act, paramArtifact) {
let actionBlock = getPropFromChain(act, ['_block', 'name', '_artifact']);
let paramArtifactBlock = getPropFromChain(paramArtifact, ['_block']);
if (paramArtifactBlock.kind && paramArtifactBlock.kind === 'source') /* param of type top-level entity */
return signal(warning`The parameter entity type must be from the current service`, act.location);
if (getPropFromChain(paramArtifactBlock, ['name', 'absolute']) !== getPropFromChain(actionBlock, ['name', 'absolute']))
return signal(warning`The parameter entity type must be from the current service '${actionBlock.name.absolute}'`, act.location);
return undefined;
function checkReturnEntity(act) {
// take the full name with the entity name
let returnServiceName = act.returns.items
? act.returns.items._finalType.name.absolute
: act.returns._finalType.name.absolute;
// remove the entity name
returnServiceName = getAbsNameWithoutId(returnServiceName);
if (serviceName && returnServiceName !== serviceName)
signal(warning`Entity type must be from the current service '${serviceName}'`, location);
}
function checkUserDefinedTypeParam(act, paramArtifact, paramName) {
if (getPropFromChain(paramArtifact, ['_block', 'kind']) === 'source')
return signal(warning`The type of input parameter '${paramName}' must be from the current service`, act.location);
else if (getPropFromChain(paramArtifact, ['_block', 'kind']) === 'block') {
let paramArtParentName = getPropFromChain(paramArtifact, ['_block', 'name', 'absolute']);
let actParentBlockName = getPropFromChain(act, ['_block', 'name', 'absolute']);
if (paramArtParentName !== actParentBlockName)
return signal(warning`The type of input parameter '${paramName}' must be from the current service`, act.location);
function checkReturnUserDefinedType(act) {
if (act.returns._finalType.builtin && options.toOdata && options.toOdata.version === 'v2') {
signal(warning`Scalar return types are not allowed in OData V2`, location);
return;
}
return undefined;
// the case when user defined is resolved to builtin
if (act.returns._finalType.type && act.returns._finalType.type._artifact.builtin) {
if (options.toOdata && options.toOdata.version == 'v2')
signal(warning`Scalar return types are not allowed in OData V2`, location);
return;
}
// check if user defined structured type is from the current service
let actReturnTypeServiceName = getAbsNameWithoutId(act.returns._finalType.name.absolute);
if (actReturnTypeServiceName !== serviceName)
signal(warning`The user defined return type of action '${act.name.id}' must be from the current service '${serviceName}'`, act.location);
}
}
function isScalar(act, typeAbsolute) {
if (['cds.Integer', 'cds.String', 'cds.Boolean',
'cds.Date', 'cds.Time', 'cds.Binary',
'cds.Integer64', 'cds.Decimal', 'cds.DecimalFloat',
'cds.Double', 'cds.DateTime', 'cds.Timestamp'].includes(typeAbsolute))
return true;
return false;
}
function checkActionOrFunctionParameter(param, serviceName, location) {
location = location || param.location;
// Check an unbound action or function (i.e. one that is not part of an entity but a definition of its own)
function checkUnboundActionOrFunction(def) {
if (Array.isArray(def)) // duplicated declaration -> will be handle in some of the other steps of the compiler
return undefined;
if (getPropFromChain(def, ['_block', 'kind']) === 'source')
return signal(warning`Functions and actions must be inside of a service or bound to an entity`, def.location);
if (getPropFromChain(def, ['_block', 'name', '_artifact', 'kind']) !== 'service')
return signal(warning`Functions and actions must be declared in a service`, def.location);
if (hasReturnType(def) && isArrayed(def.returns))
checkReturnTypeArray(def);
return undefined;
}
}
let paramTypeArtifact = param.type._artifact;
// check if the entity type is from the current service
if (paramTypeArtifact.kind === 'entity')
checkEntityParam(paramTypeArtifact);
// check when the parameter is of user defined type
if (paramTypeArtifact.kind === 'type') {
checkUserDefinedTypeParam(param, paramTypeArtifact);
}
function hasReturnType(obj) {
return has.call(obj, 'returns');
}
function checkEntityParam(paramTypeArtifact) {
if (serviceName && getAbsNameWithoutId(paramTypeArtifact.name.absolute) !== serviceName)
signal(warning`The parameter entity type must be from the current service '${serviceName}'`, location);
}
function isArrayed(obj) {
return has.call(obj, 'items');
}
function checkUserDefinedTypeParam(param, paramTypeArtifact) {
// if the type is resolved to a builtin
if (paramTypeArtifact._finalType.builtin)
return;
function getPropFromChain(obj, chain) {
if (!Array.isArray(chain))
return undefined;
let temp = obj;
for (let i = 0; i < chain.length; i++) {
if (!has.call(temp, chain[i])) {
temp = undefined;
break;
// user defined type resolved to builtin
if (paramTypeArtifact._finalType.type && paramTypeArtifact._finalType.type._artifact.builtin)
return;
if (getAbsNameWithoutId(paramTypeArtifact.name.absolute) !== serviceName)
signal(warning`The type of input parameter '${param.name.id}' must be from the current service`, location);
}
temp = temp[chain[i]];
}
return temp;
}
function getAbsNameWithoutId(name) {
return name.split('.').slice(0, -1).join('.');
}
module.exports = getFunctionAndActionChecks;

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

const getFunctionAndActionChecks = require('./checkFunctionsActions');
const { checkArtifactImplementedIn, checkNotEmptyOrOnlyVirtualElems, checkNoUnmanagedAssocsInGroupByOrderBy } = require('./checkArtifacts');
const { checkVirtualElement, checkManagedAssoc } = require('./checkElements');
const { checkNotEmptyOrOnlyVirtualElems, checkNoUnmanagedAssocsInGroupByOrderBy } = require('./checkArtifacts');
const { checkVirtualElement, checkManagedAssoc, checkCardinality } = require('./checkElements');
const { checkExpression } = require('./checkExpressions');
const { foreachPath } = require('../model/modelUtils');

@@ -35,5 +37,6 @@ // Note: For the organization of these checks, we distinguish the following terms:

const { error, signal } = alerts(model);
const { checkBoundActionOrFunction, checkUnboundActionOrFunction,
checkActionOrFunctionParameter} = getFunctionAndActionChecks(model);
const { checkActionOrFunction, checkActionOrFunctionParameter} = getFunctionAndActionChecks(model);
let currentService = undefined;
// Iterate the model and call generic/specific checkers on each construct

@@ -47,7 +50,11 @@ // (please do not put any actual checks here)

}
callKindSpecificCheck(artifact);
if (artifact.kind === 'context' || artifact.kind === 'service')
currentService = artifact.name.absolute;
callKindSpecificCheck(artifact, currentService);
baseModel.forEachMemberRecursively(artifact, member => {
checkGenericConstruct(member);
checkGenericMember(member);
callKindSpecificCheck(member);
callKindSpecificCheck(member, currentService);
});

@@ -57,3 +64,3 @@ });

// Call the appropriate kind-specific check function for 'construct'
function callKindSpecificCheck(construct) {
function callKindSpecificCheck(construct, serviceName) {
// For each kind, there must be a check function (so that we don't forget one)

@@ -73,4 +80,4 @@ const checkFunctions = {

key: nothingToCheckYet,
function: checkActionOrFunction,
action: checkActionOrFunction,
function: checkActionFunction,
action: checkActionFunction,
query: nothingToCheckYet,

@@ -86,3 +93,3 @@ view: checkView,

}
func(construct);
func(construct, serviceName);
}

@@ -96,3 +103,3 @@

function checkGenericConstruct(construct) {
if (construct.name.id.indexOf('.') != -1) {
if (construct.name.id && construct.name.id.indexOf('.') != -1) {
signal(error`The character '.' is not allowed in an identifier: "${construct.name.id}"`, construct.name.location);

@@ -134,5 +141,2 @@ }

function checkEntity(entity) {
if (entity.impl) {
checkArtifactImplementedIn(entity, model);
}
if (entity.source) { // projection

@@ -147,4 +151,5 @@ checkProjection(entity, model);

function checkProjection(entity, model) {
let sourceEntity = model.definitions[entity.source.absolute];
function checkProjection(entity) {
// TODO: check FROM clauses of queries instead
let sourceEntity = entity.source._artifact;
if(sourceEntity && isAbstractEntity(sourceEntity)) {

@@ -156,7 +161,5 @@ signal(error`Projection ${entity.name.absolute} on abstract entity ${sourceEntity.name.absolute}`, entity.source.location);

function checkView(view) {
if (view.impl) {
checkArtifactImplementedIn(view, model);
}
// TODO: check FROM clauses of queries instead
if (view.source) {
let sourceEntity = model.definitions[view.source.absolute];
let sourceEntity = view.source._artifact;
if(sourceEntity && isAbstractEntity(sourceEntity)) {

@@ -167,4 +170,52 @@ signal(error`View ${view.name.absolute} on abstract entity ${sourceEntity.name.absolute}`, view.source.location);

checkNoUnmanagedAssocsInGroupByOrderBy(view, model);
// Check expressions in the various places where they may occur
for (let query of view.queries || []) {
if (query.from) {
checkExpressionsInPaths(query.from);
}
if (query.where) {
checkExpression(query.where, model);
checkExpressionsInPaths(query.where);
}
if (query.groupBy) {
for (let groupByEntry of query.groupBy) {
checkExpression(groupByEntry, model);
checkExpressionsInPaths(groupByEntry);
}
}
if (query.having) {
checkExpression(query.having, model);
checkExpressionsInPaths(query.having);
}
if (query.orderBy) {
for (let orderByEntry of query.orderBy) {
checkExpression(orderByEntry.value, model);
checkExpressionsInPaths(orderByEntry.value);
}
}
if (query.elements) {
for (let elemName in query.elements) {
checkExpressionsInPaths(query.elements[elemName].value);
}
}
}
}
// Traverses 'node' recursively and applies 'checkExpression' to all expressions
// found within paths (e.g. filters, parameters, ...)
function checkExpressionsInPaths(node) {
foreachPath(node, path => {
for (let pathStep of path) {
if (pathStep.where) {
checkExpression(pathStep.where, model);
}
// FIXME: I can't actually think of a way to make this check fail, because
// params are limited to actual values and params
if (pathStep.namedArgs) {
checkExpression(pathStep.namedArgs, model);
}
}
});
}
function checkType(type) {

@@ -177,16 +228,19 @@ checkManagedAssoc(type, model);

checkManagedAssoc(elem, model);
checkCardinality(elem, model);
if (elem.onCond && !elem.onCond.$inferred) {
checkExpression(elem.onCond, model);
}
if (elem.value) {
checkExpression(elem.value, model);
}
}
// Actions and functions are almost identical, so we use only one check function
function checkActionOrFunction(act) {
if (act._parent && (act._parent.kind == 'entity' || act._parent.kind == 'view')) {
checkBoundActionOrFunction(act);
} else {
checkUnboundActionOrFunction(act);
}
function checkActionFunction(act, serviceName) {
checkActionOrFunction(act, serviceName);
}
function checkParam(param) {
function checkParam(param, serviceName) {
if (param._parent && (param._parent.kind == 'action' || param._parent.kind == 'function')) {
checkActionOrFunctionParameter(param, param._parent);
checkActionOrFunctionParameter(param, serviceName);
}

@@ -193,0 +247,0 @@ }

// Consistency checker on model (XSN = augmented CSN)
// Docs about XSN: internalDoc/IdeasModelChanges.md, internalDoc/Model.md (the
// latter is quite outdated). The use of the XSN is PACKAGE-INTERNAL! If you
// want to use it, you MUST contact us.
//
// The consistency check gives the consumer of XSN same safety of what values
// they can expect to see at certain places in the model. It gives produces
// some safety that they have produced a consistent model. That being said,
// the consistency check is work-in-progress.
//
// The consisteny check is NOT A SYNTAX CHECK: it accepts invalid CDS models,
// it is usually not run in productive use, and its error message contains
// property names the user is not aware of. It is considered an _internal
// error_ if the consistency check throws an error.
// A value in the model is one of:
//
// - Simple value: String, Boolean, Integer, null, undefined.
// - Dictionary: object without prototype - its property names are _user
// defined_ and all property values have same "type" (example: elements).
// - Array: all items have same "type" (which is often a "union type").
// - Standard object: object with `Object.prototype` as prototype - its
// property names are predefined (or at least their first: `@` in case of
// annotation assignments) and the value type depends on the property name.
// - Special object: currently just for the messages.
//
// The CENTRAL CHARACTERISTIC in XSN (as well as plain CSN) is: in ALL standard
// objects, the SAME PROPERTY NAME contains values of the SAME TYPE - we might
// restrict the value space in certain contexts, though. Example: the value of
// a `type` property looks the same in objects for definitions or in the object
// which is the value of the `items` property. TODO: check `technicalConfig`.
// The model is described by a schema which specifies the type for all property
// names in standard objects. Such a type can be a "union type", e.g. can
// allow a simple value or array. This is done by a assert function in
// property `test` of a properties' specification in the schema. The test
// function can use other properties in the specification. If no such function
// is specified, it uses function `standard` which checks for a standard
// objects with:
//
// - Certain required sub properties whose names are listed in the array value
// of `requires` in the specification. This can be loosened: by default, no
// property is required with syntax errors (overwritten by `isRequired`).
// - Optional sub properties are listed in `optional`, which can also be a
// - function returning true if property is allowed.
//
// The above mentioned restriction of the value space in certain contexts can
// be specified by a property `schema` of the properties' specification in the
// schema. With it, direct sub properties are checked against that
// specification. Specifications can also inherit properties from other
// specifications by using the name as value of `inherits` in the
// specification.
// The consistency check also checks the following conventions for names in
// standard objects:
//
// - A property is non-enumerable if and only if its name starts with `_`.
// This convention can be overwritten by `enumerable` of the properties'
// specification in the schema. Such properties should be used for "links"
// to other nodes in the model.
// - A property must not be produced by a parser if its name starts with `_` or
// `$`. This convention can be overwritten by `parser` of the properties'
// specification in the schema. Such properties must not be used for links,
// and should be used for information which does not make it into plain CSN.
'use strict';

@@ -11,2 +75,3 @@

let options = stageParser && stage || model.options || { testMode: true };
// The new-style CSN parser is always checked, independently from --test-mode:
if (stageParser

@@ -19,10 +84,11 @@ ? !options.testMode && (model.$frontend !== 'json' || !options.newCsn)

const schema = {
'': { // top-level
required: ['messages','options','definitions','sources'],
reqParser: ['$frontend'],
optional: ['extensions','version','$magicVariables','$builtins'], // version not with --test-mode
optParser: [
':model': { // top-level from compiler
requires: ['messages','options','definitions','sources'],
optional: ['extensions','version','$magicVariables','$builtins','$internal'] // version without --test-mode
},
':parser': { // top-level from parser
requires: ['$frontend'],
optional: [
'messages','options','definitions','extensions',
'artifacts','namespace','usings', // CDL parser
'package', // HANA CDS
'filename', 'dirname', // TODO: move filename into a normal location?

@@ -32,31 +98,74 @@ 'dependencies', // for USING..FROM

'version', // TODO: do not set in parser
],
'@sql_mapping', // TODO: it is time that a 'header' attribute replaces 'version'
]
},
$frontend: { parser: true, test: () => true }, // TODO
messages: { test: isVector( () => true ) }, // TODO: check message object
options: { test: () => true }, // TODO
location: { // location req if at least one property:
isRequired: parent => noSyntaxErrors() || Object.keys( parent ).length,
requires: ['filename','start'],
optional: ['end','$notFound'],
schema: {
start: {
requires: ['line','column'], optional: ['offset'],
schema: { line: {test:isNumber}, column: {test:isNumber}, offset: {test:TODO} }
},
end: {
requires: ['line','column'], optional: ['offset'],
schema: { line: {test:isNumber}, column: {test:isNumber}, offset: {test:TODO} }
},
$notFound: { test: isBoolean }
}
},
sources: { test: isDictionary( isObject ) },
filename: { test: isString },
dirname: { test: isString },
realname: { test: isString },
dependencies: { test: TODO }, // TODO: describe
$frontend: { parser: true, test: isString, enum: ['cdl','json','xml'] },
messages: { test: isArray( TODO ) }, // TODO: check message object
options: { test: TODO }, // TODO: check option object
definitions: {
test: isDictionary( definition ),
required: ['kind','location','name'],
reqParser: ['kind','location'], // add 'name' to optional as still allowed
requires: ['kind','location','name'],
optional: thoseWithKind
},
extensions: {
kind: ['context'], // syntax error (as opposed to HANA CDS), but still there
inherits: 'definitions',
test: isVector( standard ),
reqParser: null // like normal "require"
test: isArray(),
schema: { name: { inherits: 'name', isRequired: noSyntaxErrors } } // name is required in parser, too
},
$magicVariables: { test: TODO },
$builtins: { test: TODO },
$internal: { test: TODO },
version: { test: TODO }, // TODO: describe - better: 'header'
namespace: {
test: (model.$frontend !== 'json') ? standard : TODO,
// TODO: the JSON parser should augment 'namespace' correctly or better: hide it
requires: ['location'],
optional: ['path','dcPath'] // dcPath with --hana-flavor
},
usings: {
test: isArray(),
requires: ['kind','location'],
optional: ['name','extern','usings','annotationAssignments'], // TODO: get rid of annos: []
},
extern: {
requires: ['location','path'],
optional: ['dcPath'],
schema: { path: { inherits: 'path', optional: ['quoted'] } },
},
dcPath: { inherits: 'path', optional: ['quoted'] },
elements: { kind: true, inherits: 'definitions' },
elements_: { kind: true, parser: true, test: () => true }, // TODO: remove
_elementsIndexNo: { kind: true, parser: true, test: () => true }, // TODO: remove
elements_: { kind: true, parser: true, test: TODO }, // TODO: remove
_elementsIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove
actions: { kind: true, inherits: 'definitions' },
actions_: { kind: true, parser: true, test: () => true }, // TODO: remove
actions_: { kind: true, parser: true, test: TODO }, // TODO: remove
enum: { kind: true, inherits: 'definitions' },
enum_: { kind: true, parser: true, test: () => true }, // TODO: remove
enum_: { kind: true, parser: true, test: TODO }, // TODO: remove
foreignKeys: { kind: true, inherits: 'definitions' },
foreignKeys_: { kind: true, parser: true, test: () => true }, // TODO: remove
_foreignKeysIndexNo: { kind: true, parser: true, test: () => true }, // TODO: remove
foreignKeys_: { kind: true, parser: true, test: TODO }, // TODO: remove
_foreignKeysIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove
params: { kind: true, inherits: 'definitions' },
params_: { kind: true, parser: true, test: () => true }, // TODO: remove
_paramsIndexNo: { kind: true, parser: true, test: () => true }, // TODO: remove
params_: { kind: true, parser: true, test: TODO }, // TODO: remove
_paramsIndexNo: { kind: true, parser: true, test: TODO }, // TODO: remove
mixin: { inherits: 'definitions' },

@@ -66,13 +175,14 @@ query: {

test: query, // properties below are "sub specifications"
op: {
schema: { args: { inherits: 'query', test: isVector( query ) } },
required: ['op','location','args'],
optional: ['quantifier','orderBy','limit','offset','_leadingQuery']
union: {
schema: { args: { inherits: 'query', test: isArray( query ) } },
requires: ['op','location','args'],
optional: ['name','quantifier','orderBy','limit','offset','_leadingQuery']
},
select: {
required: ['op','location','from','elements'],
select: { // sub query
requires: ['op','location','from'],
optional: [
'name','quantifier','mixin','all','exclude',
'_tableAlias', // for sub query in FROM
'name','quantifier','mixin','exclude', 'columns', 'elements', '_deps',
'where','groupBy','having','orderBy','$orderBy','limit','offset',
'_elementsIndexNo','_projections','_block','_parent','_main',
'_elementsIndexNo','_projections','_block','_parent','_main', '_finalType',
'$tableAliases','kind','_firstAliasInFrom','queries','_$next','$combined',

@@ -83,7 +193,45 @@ '$dictOrderBy'

},
_typeIsExplicit: { kind: true, enumerable: true, test: () => true }, // TODO: remove
from: {
inherits: 'query', test: isArray( query ), // TODO: not array
op: { // join
schema: { args: { inherits: 'from', test: isArray( query ) } },
requires: ['op','location','args','join'],
optional: [
'on','kind','name',
'$tableAliases','queries','$combined',
'_block','_parent','_main','_leadingQuery','_$next', '_deps',
]
},
path: {
requires: ['location','path'],
optional: [
'name', '_tableAlias', '_status', // TODO: only in from
'scope','_artifact','$inferred', // TODO: remove the rest
]
}
},
columns: {
inherits: 'definitions', test: isArray( column ), enum: ['*'],
requires: ['location']
// schema: { kind: { isRequired: () => {} } } // kind not required
},
exclude: { // TODO: -> excluding, re-think structure
test: isDictionary( definition ), // definition since redef
requires: ['location','name'],
optional: ['annotationAssignments'] // TODO: get rid of annos: []
},
orderBy: { test: isArray(), requires: ['value'], optional: ['sort','nulls','_$queryNode'] },
sort: { test: locationVal( isString ), enum: ['asc','desc'] },
nulls: { test: locationVal( isString ), enum: ['first','last'] },
$orderBy: { inherits: 'orderBy' },
_$queryNode: { test: TODO }, // TODO: remove
$dictOrderBy: { test: TODO },
groupBy: { inherits: 'value', test: isArray( expression ) },
limit: { inherits: 'value' },
offset: { inherits: 'value' },
$combined: { test: TODO },
_typeIsExplicit: { kind: true, enumerable: true, parser: true, test: TODO }, // TODO: remove
type: {
kind: true,
required: ['location','path'],
schema: { query: { test: () => true } }, // TODO: rename query prop in name
requires: ['location','path'],
optional: [

@@ -93,3 +241,2 @@ 'scope','_artifact','$inferred', // TODO: remove the rest

'resolveSemantics', // replace by scope: 'typeOf'
'absolute','element','alias','query','action','param' // do not set in compiler
]

@@ -99,9 +246,23 @@ },

path: {
test: isVector( pathItem ),
required: ['location','id'], // TODO: it can be `func` instead of `id` later
test: isArray( pathItem ),
requires: ['location','id'], // TODO: it can be `func` instead of `id` later
// TODO: rename namedArgs to args
optional: ['quoted','namedArgs','where','cardinality','_artifact','_navigation']
optional: ['quoted','args','namedArgs','where','cardinality','_artifact','_navigation','$inferred']
},
id: { test: string },
kind: { test: string },
id: { test: isString },
quoted: { test: isBoolean },
scope: { test: TODO }, // TODO: clarify
func: { test: TODO }, // TODO: change structure
resolveSemantics: { test: TODO }, // replace by scope: 'typeOf'
kind: {
isRequired: () => true, // required even with parse errors
test: isString,
enum: [
'context','service','view','entity','type','const','annotation',
'element','enum','action','function','param','key',
'annotate','extend',
'query','mixin',
'source','namespace','using',
]
},
value: {

@@ -111,27 +272,51 @@ kind: true,

ref: { inherits: 'type' },
none: { optional: ['location'] }, // TODO: why optional / enough in name?
val: {
required: ['literal','location'],
requires: ['literal','location'],
// TODO: remove augmented, rename symbol to sym
optional: ['val','symbol','augmented'] // struct only for annotation assignments
// TODO: struct only for annotation assignments
optional: ['val','symbol','name','$inferred','augmented']
},
op: {
required: ['op','location','args'],
optional: ['func','quantifier','$inferred','augmented']
schema: { args: { inherits: 'args', args: 'positional' } },
requires: ['op','location'],
optional: ['args','func','quantifier','$inferred','augmented']
},
query: { inherits: 'query' }
},
literal: { // TODO: check value against literal
test: isString,
enum: ['string','number','boolean','hex','time','date','timestamp','struct','array','enum','null']
},
symbol: { requires: ['location','id'], optional: ['quoted','augmented'] },
val: {
test: isVal, // the following for array/struct value
requires: ['location'],
optional: ['literal','val','symbol','struct','path','name','augmented','$duplicate']
// TODO: restrict path to #simplePath
},
struct: { inherits: 'val', test: isDictionary( definition ) }, // def because double @
args: { inherits: 'value', test: args },
namedArgs: { inherits: 'value', optional: ['name','$duplicate'], test: args },
onCond: { kind: true, inherits: 'value' },
on: { kind: true, test: () => true }, // TODO: rename 'onCond' to 'on', remove 'on'
on: { kind: true, inherits: 'value', test: expressionOrString }, // TODO: rename 'onCond' to 'on', remove 'on'
where: { inherits: 'value' },
having: { inherits: 'value' },
op: { required: ['location', 'val'] }, // TODO: make it 'id' instead of 'val'
augmented: { enumerable: false, test: () => true }, // FIXME: remove
op: { test: locationVal( isString ) },
join: { test: isString },
quantifier: { test: locationVal( isString ) },
augmented: { enumerable: false, test: TODO }, // FIXME: remove
// preliminary -----------------------------------------------------------
'@': { kind: true, test: () => true },
annotationAssignments: { kind: true, test: () => true },
'@': {
kind: true,
inherits: 'value',
optional: ['name','_block','priority','$duplicate'] // TODO: name requires if not in parser?
},
'priority': { test: TODO }, // TODO: rename to $priority
annotationAssignments: { kind: true, test: TODO }, // TODO: rename to $assignments
name: {
isRequired: stageParser && (() => false), // not required in parser
kind: true,
schema: { query: { test: () => true } }, // TODO: rename query prop in name
required: ['location'],
schema: { query: { test: TODO }, $mixin: { test: TODO } }, // TODO: rename query prop in name, delete $mixin
requires: ['location'],
optional: [

@@ -145,59 +330,85 @@ 'path','id','quoted', // TODO: req path, opt id for main, req id for member

},
expectedKind: { kind: ['extend'], test: () => true },
abstract: { kind: true, test: () => true },
virtual: { kind: true, test: () => true },
key: { kind: true, test: () => true },
masked: { kind: true, test: () => true },
notNull: { kind: true, test: () => true },
includes: { kind: true, test: () => true },
returns: { kind: true, test: () => true },
dbType: { kind: true, test: () => true },
temporary: { kind: true, test: () => true }, // TODO: HANA-CDS - keep?
source: { kind: true, test: () => true }, // TODO: remove in JSON/CDL parser
projection: { kind: true, test: () => true }, // TODO: remove in JSON/CDL parser
technicalConfig: { kind: ['entity'], test: () => true },
sequenceOptions: { kind: true, test: () => true }, // hanaFlavor
targetElement: { kind: true, test: () => true }, // for foreign keys
indexNo: { kind: true, test: () => true },
artifacts: { kind: true, test: () => true }, // just test whether in definitions
artifacts_: { kind: true, parser: true, test: () => true }, // TODO: remove
blocks: { kind: true, test: () => true }, // make it $blocks ?
length: { kind: true, test: () => true },
viaTransform: { kind: true, test: () => true },
precision: { kind: true, test: () => true },
scale: { kind: true, test: () => true },
localized: { kind: true, test: () => true },
items: { kind: true, test: () => true },
cardinality: { kind: true, test: () => true },
default: { kind: true, test: () => true },
typeArguments: { kind: true, test: () => true }, // TODO: only in CDL parser or compiler w errors
$tableAliases: { kind: true, test: () => true }, // containing $self outside queries
_block: { kind: true, test: () => true },
_parent: { kind: true, test: () => true },
_main: { kind: true, test: () => true },
_finalType: { kind: true, test: () => true },
queries: { kind: true, test: () => true }, // TODO: $queries with other structure
_leadingQuery: { kind: true, test: () => true },
$replacement: { kind: true, test: () => true }, // for smart * in queries
origin: { kind: true, test: () => true }, // TODO: define some _origin
$from: { kind: true, test: () => true }, // all table refs necesary to compute elements
_origTarget: { kind: true, test: () => true }, // for REDIRECTED TO
_$next: { kind: true, test: () => true }, // next lexical search environment for values
_extend: { kind: true, test: () => true }, // for collecting extend/annotate on artifact
_annotate: { kind: true, test: () => true }, // for collecting extend/annotate on artifact
_extension: { kind: true, test: () => true }, // on artifact to its "super extend/annotate" statement
_deps: { kind: true, test: () => true }, // for cyclic calculation
_scc: { kind: true, test: () => true }, // for cyclic calculation
_sccCaller: { kind: true, test: () => true }, // for cyclic calculation
_status: { kind: true, test: () => true }, // TODO: $status
_projections: { kind: true, test: () => true }, // for mixin definitions
$extension: { kind: true, test: () => true }, // TODO: introduce $applied instead or $status
$inferred: { parser:true, kind: true, test: string },
calculated: { kind: true, test: () => true }, // TODO remove
viaAll: { kind: true, test: () => true }, // TODO remove
implicitForeignKeys: { kind: true, test: () => true }, // TODO: do it with $inferred with object value
redirected: { kind: true, test: () => true }, // TODO: do it with not-$inferred
absolute: { test: isString },
element: { test: TODO }, // TODO: { test: isString },
action: { test: isString },
param: { test: isString },
alias: { test: isString },
expectedKind: { kind: ['extend'], inherits: 'kind' },
abstract: { kind: true, test: locationVal() },
virtual: { kind: true, test: locationVal() },
key: { kind: true, test: locationVal(), also: [undefined] },
masked: { kind: true, test: locationVal() },
notNull: { kind: true, test: locationVal() },
includes: { kind: true, inherits: 'type', test: isArray() },
returns: {
kind: ['action','function'],
requires: ['location'],
optional: [
'type','typeArguments','length','precision','scale','enum',
'elements','cardinality','target','on','onCond','foreignKeys','items',
'_outer','_finalType',
'elements_','_elementsIndexNo', // TODO: remove
]
},
items: { kind: true, inherits: 'returns' }, // yes, also optional 'items'
dbType: { kind: true, test: locationVal() },
source: { kind: true, test: TODO }, // TODO: remove in JSON/CDL parser
projection: { kind: true, test: TODO }, // TODO: remove in JSON/CDL parser
technicalConfig: { kind: ['entity'], test: TODO }, // TODO: some spec
sequenceOptions: { kind: true, test: TODO }, // hanaFlavor
targetElement: { kind: true, inherits: 'type' }, // for foreign keys
indexNo: { kind: true, test: TODO }, // TODO: remove
artifacts: { kind: true, inherits: 'definitions', test: isDictionary( inDefinitions ) },
artifacts_: { kind: true, parser: true, test: TODO }, // TODO: remove
blocks: { kind: true, test: TODO }, // TODO: make it $blocks ?
viaTransform: { kind: true, test: TODO }, // TODO remove
length: { kind: true, inherits: 'value' }, // for number is to be checked in resolver
precision: { kind: true, inherits: 'value' },
scale: { kind: true, inherits: 'value' },
localized: { kind: true, test: locationVal() },
cardinality: {
kind: true,
requires: ['location'],
optional: ['sourceMax','targetMin','targetMax']
},
sourceMax: { test: locationVal( isNumber ), also: ['*'] },
targetMin: { test: locationVal( isNumber ) },
targetMax: { test: locationVal( isNumber ), also: ['*'] },
default: { kind: true, inherits: 'value' },
typeArguments: { kind: true, test: TODO }, // TODO $typeArgs: only in CDL parser or compiler w errors
$tableAliases: { kind: true, test: TODO }, // containing $self outside queries
_block: { kind: true, test: TODO },
_parent: { kind: true, test: TODO },
_main: { kind: true, test: TODO },
_artifact: { test: TODO },
_navigation: { test: TODO },
_finalType: { kind: true, test: TODO },
_tableAlias: { test: TODO },
_firstAliasInFrom: { test: TODO },
_outer: { test: TODO }, // for returns/items
queries: { kind: true, test: TODO }, // TODO: $queries with other structure
_leadingQuery: { kind: true, test: TODO },
$replacement: { kind: true, test: TODO }, // for smart * in queries
origin: { kind: true, test: TODO }, // TODO: define some _origin
$from: { kind: true, test: TODO }, // all table refs necesary to compute elements
_origTarget: { kind: true, test: TODO }, // for REDIRECTED TO
_$next: { kind: true, test: TODO }, // next lexical search environment for values
_extend: { kind: true, test: TODO }, // for collecting extend/annotate on artifact
_annotate: { kind: true, test: TODO }, // for collecting extend/annotate on artifact
_extension: { kind: true, test: TODO }, // on artifact to its "super extend/annotate" statement
_deps: { kind: true, test: TODO }, // for cyclic calculation
_scc: { kind: true, test: TODO }, // for cyclic calculation
_sccCaller: { kind: true, test: TODO }, // for cyclic calculation
_status: { kind: true, test: TODO }, // TODO: $status
_projections: { kind: true, test: TODO }, // for mixin definitions
$duplicate: { parser: true, kind: true, test: isBoolean },
$extension: { kind: true, test: TODO }, // TODO: introduce $applied instead or $status
$inferred: { parser:true, kind: true, test: isString },
calculated: { kind: true, test: TODO }, // TODO remove
viaAll: { kind: true, test: TODO }, // TODO remove
implicitForeignKeys: { kind: true, test: TODO }, // TODO: do it with $inferred with object value
redirected: { kind: true, test: TODO }, // TODO: do it with not-$inferred
}
var _noSyntaxErrors = null;
assertProp( model, null, '' );
assertProp( model, null, stageParser ? ':parser' : ':model', null, true );
return

@@ -224,10 +435,9 @@

function assertProp( node, parent, prop, extraSpec ) {
function assertProp( node, parent, prop, extraSpec, noPropertyTest ) {
let spec = extraSpec || schema[ prop ] || schema[ prop.charAt() ];
if (!spec) // TODO temp
spec = { test: () => true };
else if (spec.inherits)
spec = Object.assign( {}, schema[ spec.inherits ], spec );
if (!spec)
throw new Error( `Property '${prop}' has not been specified`);
spec = inheritSpec( spec );
if (prop) {
if (!noPropertyTest) {
let char = prop.charAt();

@@ -245,2 +455,4 @@ let parser = ('parser' in spec) ? spec.parser : char !== '_' && char !== '$';

function definition( node, parent, prop, spec, name ) {
if (name === 'cds' && node.kind === 'namespace') // do not check namespace 'cds'
return;
if (!(node instanceof Array))

@@ -254,2 +466,11 @@ node = [node];

function column( node, ...args ) {
if (node.val)
locationVal( isString )( node, ...args )
else if (stageParser)
standard( node, ...args );
else
isObject( node, ...args ); // TODO: and inside elements
}
function pathItem( node, ...args ) {

@@ -261,14 +482,15 @@ if (node !== null || noSyntaxErrors())

function standard( node, parent, prop, spec, name ) {
if (!node || typeof node !== 'object' || Object.getPrototypeOf( node ) !== Object.prototype)
throw new Error( `Expected standard object${at( [null, parent], prop, name )}` );
isObject( node, parent, prop, spec, name );
let names = Object.getOwnPropertyNames( node );
let required = stageParser && spec.reqParser || spec.required;
// Do not test 'required' with parse errors:
for (let p of required || []) {
// if (!names.includes(p) && (noSyntaxErrors() || spec.isReqWithParseErrors))
if (!names.includes(p) && (noSyntaxErrors() || p === 'location' && names.length))
throw new Error( `Required property '${p}' missing in object${at( [node, parent], prop, name )}` );
let requires = spec.requires || [];
// Do not test 'requires' with parse errors:
for (let p of requires) {
if (!names.includes(p)) {
let req = spec.schema && spec.schema[p] && spec.schema[p].isRequired;
if ((req || schema[p] && schema[p].isRequired || noSyntaxErrors)( node ))
throw new Error( `Required property '${p}' missing in object${at( [node, parent], prop, name )}` );
}
}
let optional = stageParser && spec.optParser || spec.optional || [];
let optional = spec.optional || [];
for (let n of names) {

@@ -278,4 +500,4 @@ let opt = (optional instanceof Array)

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

@@ -291,18 +513,38 @@ assertProp( node[n], node, n, spec.schema && spec.schema[n] );

function query( node, parent, prop, spec ) {
function query( node, parent, prop, spec, idx ) {
while (node instanceof Array) {
if (node.length !== 1)
throw new Error( `Unexpected length ${node.length} for query expression${at( [node, parent], prop )}` );
throw new Error( `Unexpected length ${node.length} for query expression${at( [node, parent], prop, idx )}` );
// TODO: also check getOwnPropertyNames(node)
node = node[0];
}
if (!node || typeof node !== 'object' || Object.getPrototypeOf( node ) !== Object.prototype)
throw new Error( `Expected standard object${at( [null, parent], prop )}` );
isObject( node, parent, prop, spec, idx );
if (node.op && node.op.val !== 'query')
standard( node, parent, prop, spec.op );
else
standard( node, parent, prop, spec.select );
let op = node.op && node.op.val
? (queryOps[ node.op.val ] || 'op')
: (node.path) ? 'path' : 'none';
if (spec[op])
assertProp( node, parent, prop, spec[op], true );
else {
throw new Error( `No specification for computed variant '${op}'${at( [node, parent], prop, idx )}` );
}
}
function inheritSpec( spec ) {
if (!spec.inherits)
return spec;
let chain = [spec];
while (spec.inherits) {
spec = schema[ spec.inherits ];
chain.push( spec );
}
chain.reverse();
return Object.assign( {}, ...chain );
}
function expressionOrString( node, ...args ) { // TODO: remove with onCond
if (typeof node !== 'string')
expression( node, ...args );
}
function expression( node, parent, prop, spec, idx ) {

@@ -312,3 +554,3 @@ while (node instanceof Array) {

if (node.length !== 1) {
node.map( n => ( n, parent, prop, spec ) )
node.forEach( n => expression( n, parent, prop, spec ) );
return;

@@ -318,8 +560,13 @@ }

}
if (!node || typeof node !== 'object' || Object.getPrototypeOf( node ) !== Object.prototype)
throw new Error( `Expected standard object${at( [null, parent], prop, idx )}` );
if (node == null && !noSyntaxErrors())
return;
isObject( node, parent, prop, spec, idx );
let sub = spec[ expressionSpec(node) ];
if (sub.inherits)
sub = Object.assign( {}, schema[ sub.inherits ], sub );
let s = spec[ expressionSpec(node) ] || {};
let sub = Object.assign( {}, s.inherits && schema[ s.inherits ], s );
if (spec.requires && sub.requires)
sub.requires = [...sub.requires, ...spec.requires];
if (spec.optional && sub.optional)
sub.optional = [...sub.optional, ...spec.optional];
// console.log(expressionSpec(node) );
(sub.test||standard)( node, parent, prop, sub, idx );

@@ -333,3 +580,5 @@ }

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

@@ -341,20 +590,23 @@ else

function args( node, parent, prop, spec ) {
if (!(node instanceof Array))
throw new Error( `Expected array${at( [null, parent], prop )}` );
if (parent.op && parent.op.val === 'xpr') // remove keywords for `xpr` expressions
node = node.filter( a => typeof a !== 'string');
node.forEach( (item, idx) => expression( item, parent, prop, spec, idx ) );
if (node instanceof Array) {
if (parent.op && parent.op.val === 'xpr') // remove keywords for `xpr` expressions
node = node.filter( a => typeof a !== 'string');
node.forEach( (item, idx) => expression( item, parent, prop, spec, idx ) );
}
else if (spec.args === 'positional')
throw new Error( `Expected array ${at( [null, parent], prop )}` );
else if (node && typeof node === 'object' && !Object.getPrototypeOf( node ))
{
for (let n in node)
expression( node[n], parent, prop, spec, n );
}
else
throw new Error( `Expected array or dictionary${at( [null, parent], prop )}` );
}
function string( node, parent, prop ) {
if (typeof node !== 'string')
throw new Error( `Expected string${at( [node, parent], prop )}` );
// TODO: also check getOwnPropertyNames(node)
}
function at( nodes, prop, name ) {
let n = (typeof name === 'number') ? ` for index ${name}` : name ? ` for "${name}"` : '';
let loc = nodes.find( o => o && typeof o === 'object' && o.location );
let f = (prop && loc !== nodes[0]) ? ` in property '${prop}'` : '';
let l = loc && locationString( loc.location ) || model.filename;
let loc = nodes.find( o => o && typeof o === 'object' && (o.location||o.start) );
let f = (prop) ? ` in property '${prop}'` : '';
let l = loc && locationString( loc.location||loc ) || model.filename;
return (!l) ? n+f : n+f+' at '+l;

@@ -374,3 +626,3 @@ }

function isVector( func ) {
function isArray( func = standard ) {
return function vector( node, parent, prop, spec ) {

@@ -382,11 +634,61 @@ if (!(node instanceof Array))

}
}
function assertConsistency2(model, stage) {
let sql_mapping = model["@sql_mapping"];
delete model["@sql_mapping"]
assertConsistency(model, stage)
model["@sql_mapping"]=sql_mapping
function locationVal( func = isBoolean ) {
return function valWithLocation( node, parent, prop, spec, name ) {
var schema = { val: Object.assign( {}, spec, { test: func } ) };
var requires = ['val','location'];
var optional = ['literal','$inferred','augmented']; // TODO: remove augmented
standard( node, parent, prop, { schema, requires, optional }, name );
};
}
function isBoolean( node, parent, prop, spec ) {
if (spec.also && spec.also.includes( node ))
return;
if (typeof node !== 'boolean')
throw new Error( `Expected boolean${at( [node, parent], prop )}` );
}
function isNumber( node, parent, prop, spec ) {
if (spec.also && spec.also.includes( node ))
return;
if (typeof node !== 'number')
throw new Error( `Expected number${at( [node, parent], prop )}` );
}
function isString( node, parent, prop, spec ) {
if (typeof node !== 'string')
throw new Error( `Expected string${at( [node, parent], prop )}` );
// TODO: also check getOwnPropertyNames(node)
if (spec.enum && !spec.enum.includes( node ))
throw new Error( `Unexpected value '${ node }'${at( [node, parent], prop )}` );
}
function isVal( node, parent, prop, spec ) {
if (node instanceof Array)
node.forEach( (item, n) => standard( item, parent, prop, spec, n ) );
else if (node !== null && !['string','number','boolean'].includes( typeof node ))
throw new Error( `Expected array or simple value${at( [null, parent], prop )}` );
}
function isObject( node, parent, prop, spec, name ) {
if (!node || typeof node !== 'object' || Object.getPrototypeOf( node ) !== Object.prototype)
throw new Error( `Expected standard object${at( [null, parent], prop, name )}` );
}
function inDefinitions( art, parent, prop, spec, name ) {
if (art instanceof Array) // do not check with redefinitions
return;
isObject( art, parent, prop, spec, name )
if (stageParser) {
if (prop === 'artifacts')
standard( art, parent, prop, spec, name );
}
else if (!art.name.absolute || !model.definitions[ art.name.absolute ])
throw new Error( `Expected definition${at( [art, parent], prop, name )}` );
}
function TODO() {}
}
module.exports = assertConsistency2;
module.exports = assertConsistency;

@@ -5,2 +5,4 @@ // The builtin artifacts of CDS

const { forEachInDict } = require('../base/dictionaries');
var core = {

@@ -58,3 +60,6 @@ String: { parameters: ['length'] },

// SQL-92: also SYSTEM_USER, just USER (is with parens in HANA SQL), VALUE
'$user': {}, // CDS-specific, not part of SQL
'$user': {
elements: { id: {}, locale: {} },
$autoElement: 'id'
}, // CDS-specific, not part of SQL
'$now': {}, // Dito

@@ -76,3 +81,11 @@ }

setMagicVariables( magicVariables );
model.$builtins = env( core, 'cds.' );
let cds = {
kind: 'namespace',
name: { absolute: 'cds' },
blocks: [],
artifacts: Object.create(null) };
// setProp( model.definitions, 'cds', cds );
model.definitions.cds = cds; // not setProp - oData
model.$builtins = env( core, 'cds.', undefined, cds );
model.$builtins.cds = cds;
}

@@ -85,9 +98,13 @@ else {

}
model.$internal = { $frontend: '$internal' };
return;
function env( builtins, prefix, extraPrefix ) {
var artifacts = Object.create(null);
function env( builtins, prefix, extraPrefix, parent ) {
let artifacts = Object.create(null);
for (let name in builtins) {
let absolute = prefix + name;
let art = { kind: 'type', builtin: true, name: {absolute}, type: { absolute } };
let art = { kind: 'type', builtin: true, name: {absolute}, type: { path:[{id:absolute}] } };
setProp( art.type, '_artifact', art );
if (parent)
parent.artifacts[ name ] = art;
setProp( art, '_finalType', art );

@@ -108,7 +125,24 @@ setProp( art, '_deps', [] );

for (let name in builtins) {
let magic = builtins[name];
// TODO: rename to $builtinFunction
artifacts[name] = { kind: 'builtin', name: { action: name } };
let art = { kind: 'builtin', name: { id: name, element: name } };
artifacts[name] = art;
if (magic.elements)
art.elements = forEachInDict( magic.elements, (e,n) => magicElement( e, n, art ));
if (magic.$autoElement)
art.$autoElement = magic.$autoElement;
// setProp( art, '_finalType', art );
}
model.$magicVariables = { kind: '$magicVariables', artifacts };
}
function magicElement( spec, name, parent ) {
let magic = {
kind: 'builtin',
name: { id: name, element: parent.name.element + '.' + name }
};
setProp( magic, '_parent', parent );
// setProp( magic, '_finalType', magic );
return magic;
}
}

@@ -118,3 +152,3 @@

function setProp ( obj, prop, value ) {
Object.defineProperty( obj, prop, { value, configurable: true } ); // not enumerable!
Object.defineProperty( obj, prop, { value, configurable: true, writable: true } ); // not enumerable!
return value;

@@ -121,0 +155,0 @@ }

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

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

@@ -101,2 +102,3 @@

// Get simplified "resolve" functionality and the message function:
const message = getMessageFunction( model );
const {

@@ -106,3 +108,2 @@ resolveUncheckedPath,

defineAnnotations,
message
} = fns( model );

@@ -113,2 +114,3 @@ model.definitions = Object.create(null);

var extensionsDict = Object.create(null);
var lateExtensionsDict = Object.create(null);
initBuiltins( model );

@@ -120,2 +122,4 @@ for (let name in model.sources) {

forEachGeneric( model, 'definitions', checkRedefinitions );
processLocalizedData();
lateExtensions();
return model;

@@ -127,17 +131,14 @@

return;
let loc = obj.name.location;
if (prop === 'elements')
message( `Duplicate definition of element ${msgName( name )}`, loc );
else if (prop === 'enum')
message( `Duplicate definition of enum ${msgName( name )}`, loc );
else if (prop === 'foreignKeys')
message( `Duplicate definition of foreign key ${msgName( name )}`, loc );
else if (prop === 'actions')
message( `Duplicate definition of action or function ${msgName( name )}`, loc );
else if (prop === 'params')
message( `Duplicate definition of parameter ${msgName( name )}`, loc );
else if (obj.kind === 'namespace')
message( `Other definition blocks ${msgName( name )} for namespace name`, loc );
else
message( `Duplicate definition of artifact ${msgName( name )}`, loc );
message( 'duplicate-definition', obj.name.location,
{ name, '#': (obj.kind === 'namespace') ? 'namespace' : dictKinds[prop] },
'Error', {
std: 'Duplicate definition of $(NAME)',
absolute: 'Duplicate definition of artifact $(NAME)',
namespace: 'Other definition blocks $(NAME) for namespace name',
element: 'Duplicate definition of element $(NAME)',
enum: 'Duplicate definition of enum $(NAME)',
key: 'Duplicate definition of key $(NAME)',
action: 'Duplicate definition of action or function $(NAME)',
param: 'Duplicate definition of parameter $(NAME)',
});
}

@@ -178,4 +179,5 @@

if (builtin && !builtin.internal) {
message( `You might also need 'using ${builtin.name.absolute};'`,
src.namespace.location, 'Info' );
message( 'ref-shadowed-builtin', src.namespace.location,
{ id: last.id, art: src.namespace, code: `using ${builtin.name.absolute};` },
'Warning', '$(ID) now refers to $(ART) - consider $(CODE)' );
}

@@ -202,2 +204,4 @@ let block = {

let absolute;
if (path.broken)
return parent;
for (let item of path) {

@@ -233,2 +237,3 @@ let id = item.id;

// another artifact has already been defined with the same absolute name.
// TODO: do our own check for reserved names in compiler !!!
function addToDefinitions( art, absolute = art.name.absolute, prefix, parent ) {

@@ -249,3 +254,9 @@ let context = reuse();

}
addToDict( model.definitions, absolute, art );
if (absolute === 'cds') {
// TODO: move all 'cds' prefix checks into compiler
message( null, art.name.location,
`The namespace "cds" is reserved for CDS builtins` );
}
else
addToDict( model.definitions, absolute, art );
return art;

@@ -311,4 +322,6 @@

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

@@ -325,8 +338,9 @@ }

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

@@ -407,5 +421,8 @@ if (def instanceof Array) {

}
initDollarSelf( art ); // to allow extend projection with auto-mixin assoc, see #924
initParams( art );
art.$from = []; // for sequence of resolve steps
art.queries = [];
setProp( art, '_leadingQuery', initQueryExpression( art, art.query ) );
setProp( art._leadingQuery, '_$next', art );
art.queries.forEach( initSubQuery ); // TODO: per art.query

@@ -419,6 +436,2 @@ // TODO: simplified for simple views = just one query with one table source

// check checks whether union can be used)?
if (art._leadingQuery)
art.elements = art._leadingQuery.elements;
if (!options.hanaFlavor)
initDollarSelf( art ); // to allow extend projection with auto-mixin assoc, see #924
}

@@ -428,2 +441,4 @@ else {

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

@@ -434,4 +449,3 @@ initMembers( art, art, block );

extensionsDict[absolute] = []; // structure with includes must be "extended"
if (art.elements)
initDollarSelf( art );
initDollarSelf( art );
}

@@ -442,4 +456,4 @@ }

let selfname = (options.hanaFlavor) ? art.name.id : '$self';
art.$tableAliases = Object.create(null);
art.$tableAliases[selfname] = {
// TODO: users setMemberParent() ?
let self = {
name: { id: selfname, alias: selfname, absolute: art.name.absolute },

@@ -449,6 +463,30 @@ kind: '$tableAlias', self: true,

location: art.location };
setProp( art.$tableAliases[selfname].type, '_artifact', art );
setProp( self, '_parent', art );
setProp( self, '_main', art ); // used on main artifact
setProp( self.type, '_artifact', art );
art.$tableAliases = Object.create(null);
art.$tableAliases[selfname] = self;
if (options.oldstyleSelf)
art.$tableAliases.self = art.$tableAliases[selfname];
setProp( art, '_$next', model.$magicVariables );
}
function initParams( art ) {
// TODO: users setMemberParent() ?
let parameters = {
name: { id: '$parameters', param: '$parameters', absolute: art.name.absolute },
kind: '$parameters',
location: art.location };
setProp( parameters, '_parent', art );
setProp( parameters, '_main', art );
// Search for :const after :param. If there will be a posibility in the
// future that we can extend <query>.columns, we must be sure to use
// _block of that new column after :param (or just allow $parameters there).
setProp( parameters, '_block', art._block );
if (art.params) {
parameters.elements = art.params;
parameters.$tableAliases = art.params; // TODO: find better name - $lexical?
}
art.$tableAliases.$parameters = parameters;
}
}

@@ -461,6 +499,8 @@

// TODO: MIXIN with name = ...subquery
for (let name in query.elements) {
let elem = query.elements[name];
if (elem && elem.value)
for (let elem of query.columns || []) {
if (elem && elem.value) {
setProp( elem, '_block', query._block );
defineAnnotations( elem, elem, query._block );
initExprForQuery( elem.value, query ); // adds to query.queries
}
}

@@ -523,4 +563,3 @@ if (query.where)

if (parents.length)
// It is ensured that query.elements is defined (see `queryTerm` in grammar)
addAlias( { elements: query.elements }, query );
addAlias( {}, query );
for (let tab of query.from)

@@ -531,8 +570,22 @@ initQueryExpression( art, tab, [query] );

if (!query.$tableAliases.$projection) {
// TODO: use setMemberParent() ?
query.$tableAliases.$projection = {
name: { alias: '$projection', query: query.name.query, absolute: art.name.absolute },
kind: '$tableAlias', self: true,
elements: query.elements, // TODO: shouldn't we set type._artifact: query instead?
location: query.location };
location: query.location
};
setProp( query.$tableAliases.$projection, '_parent', query );
setProp( query.$tableAliases.$projection, '_main', query._main );
setProp( query.$tableAliases.$projection, '_finalType', query );
}
if (!query.$tableAliases.$self) { // same as $projection
query.$tableAliases.$self = {
name: { alias: '$self', query: query.name.query, absolute: art.name.absolute },
kind: '$tableAlias', self: true,
location: query.location
};
setProp( query.$tableAliases.$self, '_main', query._main );
setProp( query.$tableAliases.$self, '_finalType', query );
setProp( query.$tableAliases.$self, '_finalType', query );
}
}

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

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

@@ -560,8 +613,13 @@ // else: with parse error (`select from <EOF>`)

function signalDuplicate( name, loc ) {
message( `Duplicate definition of table alias or mixin ${msgName( name )}`, loc );
message( 'duplicate-definition', loc, { name, '#': '$tableAlias' },
'Error',
{ '$tableAlias': 'Duplicate definition of table alias or mixin $(NAME)' } );
}
function addAlias( alias, subquery ) {
if (!alias.type)
setProp( alias, '_finalType', subquery );
if (!query.name || !query.name.id) {
message( `Table alias is required for this subquery`, query.location );
message( 'query-req-alias', query.location, {},
'Error', 'Table alias is required for this subquery' );
return;

@@ -653,5 +711,5 @@ }

else {
if (obj.elements && checkDefinitions( construct, parent, 'elements', obj.elements ))
if (checkDefinitions( construct, parent, 'elements', obj.elements || false ))
forEachInOrder( obj, 'elements', init );
if (obj.enum && checkDefinitions( construct, parent, 'enum', obj.enum ))
if (checkDefinitions( construct, parent, 'enum', obj.enum || false ))
forEachGeneric( obj, 'enum', init );

@@ -661,5 +719,5 @@ }

forEachInOrder( obj, 'foreignKeys', init );
if (construct.actions && checkDefinitions( construct, parent, 'actions' ))
if (checkDefinitions( construct, parent, 'actions' ))
forEachGeneric( construct, 'actions', init );
if (construct.params && checkDefinitions( construct, parent, 'params' ))
if (checkDefinitions( construct, parent, 'params' ))
forEachInOrder( construct, 'params', init );

@@ -683,3 +741,4 @@ return;

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

@@ -696,2 +755,6 @@ else {

function checkDefinitions( construct, parent, prop, dict = construct[prop] ) {
// To have been checked by parsers:
// - artifacts (CDL-only anyway) only inside [extend] context|service
if (!dict)
return false;
let feature = kindProperties[ parent.kind ][prop];

@@ -701,15 +764,36 @@ if (feature &&

return true;
let propname = (prop === 'enum') ? 'enum symbols' : prop;
let where = {
element: 'an element',
action: 'an action',
param: 'a parameter',
}[parent.kind] || `artifact "${parent.name.absolute}"`;
// TODO: no constructed messages
message( (construct.kind !== 'extend')
? 'You cannot define ' + propname + ' inside ' + where
: 'You cannot add ' + propname + ' to ' + where + ' via EXTEND',
dictLocation( dict ) );
// If the members are directly defined, define them anyway - ignore them
// for extensions
let location = dictLocation( dict );
if (prop === 'actions') {
message( 'unexpected-actions', location, {}, 'Error',
'Actions and functions only exist top-level and for entities' );
}
else if (parent.kind === 'action' || parent.kind === 'function') {
message( 'extend-action', construct.location, {}, 'Error',
'Actions and functions cannot be extended, only annotated' );
}
else if (prop === 'params') {
if (!feature) {
if (!['entity','view'].includes(parent.kind) ||
!options.betaMode && !options.hanaFlavor && construct.kind !== 'annotate')
message( 'unexpected-params', location, {}, 'Error',
'Parameters only exist for actions or functions' );
else if (construct.kind === 'annotate')
return true;
}
else
message( 'extend-with-params', location, {}, 'Error', // remark: we could allow this
'Extending artifacts with parameters is not supported' );
}
else if (feature) { // allowed in principle, but not with extend
message( 'extend-type', location, {}, 'Error',
'Only structures or enum types can be extended with elements/enums' );
}
else if (prop === 'elements') {
message( 'unexpected-elements', location, {}, 'Error',
'Elements only exist in entities, types or typed constructs' );
}
else { // if (prop === 'enum') {
message( 'unexpected-enum', location, {}, 'Error',
'Enum symbols can only be defined for types or typed constructs' );
}
return construct === parent;

@@ -724,3 +808,3 @@ }

// normal ref in outer extend, relative ref inside EXTEND CONTEXT
let name = resolveUncheckedPath( ext.name, ext.kind, block );
let name = resolveUncheckedPath( ext.name, ext.kind, ext );
// TODO: somehow provide expectedKind as filter?

@@ -735,2 +819,34 @@ if (name) {

function lateExtensions() {
for (let name in lateExtensionsDict) {
let art = model.definitions[name];
let exts = lateExtensionsDict[name];
if (art) { // created texts entity
extendArtifact( exts, art, 'noExtend' );
}
else if (!options.lintMode) {
// If not lint-mode, complain about unused extensions, i.e. those
// which do not point to a valid artifact
for (let ext of exts) {
resolvePath( ext.name, ext.kind, ext ); // should issue error/info
if (ext.kind === 'annotate')
delete ext.name._artifact; // make it be considered by extendArtifact()
}
// create "super" ANNOTATE containing all non-applied ones
let first = exts[0];
let location = first.name.location;
art = {
kind: 'annotate',
name: { path: [ { id: name, location } ], absolute: name, location },
location: first.location
};
if (model.extensions)
model.extensions.push(art);
else
model.extensions = [art];
extendArtifact( exts, art ); // also sets _artifact link in extensions
}
}
}
// phase 1: context extends, 2: extends with structure includes, 3: extends

@@ -754,21 +870,3 @@ // without structure includes (in the case of cyclic includes)

if (!art) {
if (!options.lintMode) {
// If not lint-mode, complain about unused extensions, i.e. those
// which do not point to a valid artifact: TODO: not so for i18n
for (let ext of extensionsDict[name]) {
resolvePath( ext.name, ext.kind, undefined, ext._block ); // should issue error/info
if (ext.kind === 'annotate')
delete ext.name._artifact; // make it be considered by extendArtifact()
}
// create "super" ANNOTATE containing all non-applied ones
let first = extensionsDict[name][0];
let location = first.name.location;
art = {
kind: 'annotate',
name: { path: [ { id: name, location } ], absolute: name, location },
location: first.location
};
model.extensions ? model.extensions.push(art) : model.extensions = [art];
extendArtifact( name, art ); // also sets _artifact link in extensions
}
lateExtensionsDict[name] = extensionsDict[name];
delete extensionsDict[name];

@@ -781,3 +879,3 @@ }

? extendContext( name, art )
: extendArtifact( name, art, phase > 2 )) {
: extendArtifact( extensionsDict[name], art, phase > 2 )) {
delete extensionsDict[name];

@@ -805,8 +903,6 @@ }

}
else if (ext.elements) {
message( (ext.kind === 'extend')
? `No 'EXTEND artifact' or element definition within 'EXTEND ${art.kind}'`
: `No 'ANNOTATE artifact' within 'ANNOTATE ${art.kind}'`,
dictLocation( ext.elements ) );
}
checkDefinitions( ext, art, 'elements'); // error for elements etc
checkDefinitions( ext, art, 'enum');
checkDefinitions( ext, art, 'actions');
checkDefinitions( ext, art, 'params');
defineAnnotations( ext, art, ext._block, ext.kind );

@@ -823,5 +919,6 @@ initArtifacts( ext, art, ext );

if (!options.tntFlavor)
message( 'Service includes are not supported yet', dictLocation( art.includes ));
message( null, dictLocation( art.includes ),
'Service includes are not supported yet' );
for (let ref of art.includes) {
resolvePath( ref, 'context', art, art._block );
resolvePath( ref, 'context', art );
}

@@ -834,6 +931,6 @@ }

function extendArtifact( name, art, noIncludes ) {
function extendArtifact( extensions, art, noIncludes ) {
if (!noIncludes && art.includes) {
for (let ref of art.includes) {
let template = resolvePath( ref, 'include', art, art._block );
let template = resolvePath( ref, 'include', art );
if (template && template.name.absolute in extensionsDict)

@@ -845,3 +942,3 @@ return false;

}
extendMembers( extensionsDict[name], art );
extendMembers( extensions, art, noIncludes === 'noExtend' );
// TODO: complain about element extensions inside projection

@@ -851,7 +948,13 @@ return true;

function extendMembers( extensions, art ) {
function extendMembers( extensions, art, noExtend ) {
let elemExtensions = [];
extensions.sort( compareLayer );
for (let ext of extensions) {
if (!('_artifact' in ext.name)) { // not already applied
setProp( ext.name, '_artifact', art );
if (noExtend && ext.kind === 'extend') {
message( 'extend-for-generated', ext.name.location, { art },
'Error', 'You cannot use EXTEND on the generated $(ART)' );
continue;
}
defineAnnotations( ext, art, ext._block, ext.kind );

@@ -861,11 +964,14 @@ // TODO: do we allow to add elements with array of {...}? If yes, adapt

}
if (ext.elements && // at least one elem definition (not extension)
Object.keys( ext.elements ).some( n => ext.elements[n].kind === 'element' ))
elemExtensions.push( ext.elements );
for (let name in ext.elements) {
let elem = ext.elements[ name ];
if (elem.kind === 'element') {
elemExtensions.push( elem );
break;
}
}
}
if (elemExtensions.length > 1) {
for (let dict of elemExtensions)
message( `Unstable element order due to repeated extensions`,
dictLocation( dict ), 'Warning' );
}
if (elemExtensions.length > 1)
reportUnstableExtensions( elemExtensions );
['elements', 'actions'].forEach( function ext(prop) {

@@ -876,3 +982,3 @@ let dict = art._extend && art._extend[prop];

if (!member)
extendNothing( dict[name], prop, name, art.name.absolute );
extendNothing( dict[name], prop, name, art );
else if (!(member instanceof Array))

@@ -884,17 +990,48 @@ extendMembers( dict[name], member );

function extendNothing( extensions, prop, name, absolute ) {
// TODO: re-check messages
function reportUnstableExtensions( extensions ) {
// Report 'Warning: Unstable element order due to repeated extensions'.
// Similar to chooseAssignment(), TODO there: also extra intralater message
// as this is a modeling error
let lastExt = null;
let open = []; // the "highest" layers
for (let ext of extensions) {
if (prop === 'elements')
message( `Element "${name}" has not been found in "${absolute}"`,
ext.name.location );
else if (prop === 'enum')
message( `Enum symbol "${name}" has not been found in "${absolute}"`,
ext.name.location );
else
message( `Action "${name}" has not been found in "${absolute}"`,
ext.name.location );
let extLayer = layer( ext );
if (!open.length) {
lastExt = ext;
open = [ extLayer.realname ];
}
else if (extLayer.realname === open[ open.length-1 ]) { // in same layer
if (lastExt) {
message( 'extend-repeated-intralayer', lastExt.location, {}, 'Warning',
'Unstable element order due to repeated extensions in same layer' );
lastExt = null;
}
message( 'extend-repeated-intralayer', ext.location, {}, 'Warning',
'Unstable element order due to repeated extensions in same layer' );
}
else {
if (lastExt && (open.length > 1 || !extLayer._layerExtends[ open[0] ])) {
// report for lastExt if that is unrelated to other open exts or current ext
message( 'extend-unrelated-layer', lastExt.location, {}, 'Warning',
'Unstable element order due to other extension in unrelated layer' );
}
lastExt = ext;
open = open.filter( name => !extLayer._layerExtends[ name ] );
open.push( extLayer.realname );
}
}
}
function extendNothing( extensions, prop, name, art ) {
for (let ext of extensions) {
message( 'extend-undefined', ext.name.location,
{ art: searchName( art, name, dictKinds[prop] ) },
'Error', {
std: 'Unknown $(ART) - nothing to extend',
element: 'Artifact $(ART) has no element or enum $(MEMBER) - nothing to extend',
action: 'Artifact $(ART) has no action $(MEMBER) - nothing to extend',
} );
}
}
function includeMembers( art, prop, forEach ) {

@@ -908,3 +1045,3 @@ // TODO: a projection cannot be used as include, right?

for (let ref of art.includes) {
let template = resolvePath( ref, 'include', art, art._block );
let template = resolvePath( ref, 'include', art );
if (template) { // be robust

@@ -931,2 +1068,125 @@ forEach( template, prop, function( origin, name ) {

}
function processLocalizedData() {
if (!options.betaMode)
return;
for (let absolute in model.definitions) {
let art = model.definitions[absolute];
if (art instanceof Array || // redefininition
!art.elements || // potential entity parse error
art.kind !== 'entity' || art.query) // not non-query entity
continue;
let textsName = art.name.absolute + '_txts';
let localized = localizedData( art, textsName );
if (localized) {
createTextsEntity( art, textsName, localized );
addTextsAssociations( art, textsName, absolute );
}
}
}
function localizedData( art, textsName ) {
let keys = 0;
let textElems = [];
let protectedElems = [];
for (let name in art.elements) {
let elem = art.elements[name];
if (elem instanceof Array)
return false; // no localized-data unfold with redefined elems
if (['locale','texts','localized'].includes( name ))
protectedElems.push( elem );
if (elem.key && elem.key.val) { // key with localized is wrong - ignore localized
keys += 1;
textElems.push( elem );
}
else if (elem.localized && elem.localized.val)
textElems.push( elem );
}
if (textElems.length <= keys)
return false;
if (!keys) {
message( null, art.name.location, {}, 'Warning',
'No texts entity can be created when no key element exists' );
}
for (let elem of protectedElems) {
message( null, elem.name.location, { name: elem.name.id }, 'Warning',
'No texts entity can be created when element $(NAME) exists' );
}
let textsEntity = model.definitions[ textsName ];
if (!textsEntity)
return !protectedElems.length && keys && textElems;
message( null, art.name.location, { art: textsName }, 'Warning',
'Name $(ART) for localized texts entity used by other definition' );
if (!(textsEntity instanceof Array)) {
message( null, textsEntity.name.location, { art }, 'Info',
'No texts entity for $(ART) can be created with this definition' );
}
return false;
}
function createTextsEntity( base, absolute, textElems ) {
let elements = Object.create(null);
let location = base.name.location;
let art = {
kind: 'entity',
name: { absolute, location },
location: base.location,
elements,
'@cds.autoexpose': { name: augmentGlobal( location, '@cds.autoexpose' ), location },
$inferred: 'localized'
}
setProp( art, '_block', model.$internal );
model.definitions[absolute] = art;
let locale = {
name: { location, id: 'locale' },
kind: 'element',
type: augmentGlobal( location, 'cds.String' ),
length: { literal: 'number', val: 5, location },
location
};
setMemberParent( locale, 'locale', art, 'elements' );
setProp( locale, '_block', model.$internal );
for (let orig of textElems) {
let elem = linkToOrigin( orig, orig.name.id, art, 'elements' );
if (!orig.key || !orig.key.val) // use location of LOCALIZED keyword
elem.localized = { val: false, $inferred: 'localized', location: orig.localized.location };
}
}
function addTextsAssociations( art, textsName ) {
// texts : Composition of many Books_txts on texts.ID=ID;
let location = art.name.location;
let texts = {
name: { location, id: 'texts' },
kind: 'element',
location,
$inferred: 'localized',
type: augmentGlobal( location, 'cds.Composition' ),
cardinality: { targetMax: { literal: 'string', val: '*', location }, location },
target: augmentGlobal( location, textsName ),
onCond: augmentEqual( location, ['texts.ID', 'ID'] )
}
setMemberParent( texts, 'texts', art, 'elements' );
setProp( texts, '_block', model.$internal );
// localized : Association to Books_txts on
// localized.ID=ID and localized.locale = $user.locale;
let localized = {
name: { location, id: 'localized' },
kind: 'element',
location,
$inferred: 'localized',
type: augmentGlobal( location, 'cds.Association' ),
target: augmentGlobal( location, textsName ),
onCond: augmentEqual( location,
['localized.ID', 'ID'], ['localized.locale', '$user.locale'] )
}
setMemberParent( localized, 'localized', art, 'elements' );
setProp( localized, '_block', model.$internal );
}
}

@@ -940,2 +1200,20 @@

function augmentGlobal( location, id ) {
return { path: [{ id, location }], location };
}
function augmentEqual( location, ...relations ) {
let args = relations.map( eq );
return (args.length === 1)
? args[0]
: { op: { val: 'and', location }, args, location };
function eq( args ) {
return { op: { val: '=', location }, args: args.map( ref ), location };
}
function ref( path ) {
return { path: path.split('.').map( id => ({ id, location }) ), location };
}
}
module.exports = define;

@@ -7,16 +7,7 @@ // Module handling, layers and packages

function setLayers( sources, filenames ) {
let fname = filenames.find( name => !name.endsWith( '.properties' ) );
let representative = fname && sources[fname];
function setLayers( sources ) {
// set dependencies
let srcs = Object.create(null);
let annofiles = [];
for (let name in sources) {
let ast = sources[name];
ast.realname = name;
if (representative && name.endsWith( '.properties' )) {
annofiles.push( ast );
continue;
}
srcs[name] = ast;
setProp( ast, '_deps', [] );

@@ -29,24 +20,30 @@ for (let d of ast.dependencies || []) {

}
detectCycles( srcs, null, setExtends );
for (let ast of annofiles)
setProp( ast, '_layerRepresentative', representative._layerRepresentative );
}
let layerNumber = 0;
let layerRepresentative;
detectCycles( sources, null, setExtends );
// it is ensured that the representative is called last in SCC and that
// dependent SCCs are called first
function setExtends( node, representative, sccDeps = Object.create(null) ) {
setProp( node, '_layerRepresentative', representative );
for (let dep of node._deps) {
if (dep.art._scc.lowlink !== node._scc.lowlink) { // not in same SCC
let depRepr = dep.art._layerRepresentative;
sccDeps[ depRepr.realname ] = depRepr;
// It is ensured that the representative is called last in SCC and that
// dependent SCCs are called first
function setExtends( node, representative, sccDeps = Object.create(null) ) {
setProp( node, '_layerRepresentative', representative );
if (layerRepresentative !== representative) {
layerRepresentative = representative;
++layerNumber;
}
node.$layerNumber = layerNumber; // for sorting
for (let dep of node._deps) {
if (dep.art._scc.lowlink !== node._scc.lowlink) { // not in same SCC
let depRepr = dep.art._layerRepresentative;
sccDeps[ depRepr.realname ] = depRepr;
}
}
if (node === representative) {
let exts = Object.keys( sccDeps ).map( name => sccDeps[name]._layerExtends );
Object.assign( sccDeps, ...exts );
setProp( representative, '_layerExtends', sccDeps );
// console.log ('SCC:', node.realname)
}
return sccDeps;
}
if (node === representative) {
let exts = Object.keys( sccDeps ).map( name => sccDeps[name]._layerExtends );
Object.assign( sccDeps, ...exts );
setProp( representative, '_layerExtends', sccDeps );
// console.log ('SCC:', node.realname)
}
return sccDeps;
}

@@ -60,2 +57,10 @@

function compareLayer( a, b ) {
while (a && a.kind !== 'source')
a = a._block;
while (b && b.kind !== 'source')
b = b._block;
return a.$layerNumber - b.$layerNumber;
}
// Like `obj.prop = value`, but not contained in JSON / CSN

@@ -70,2 +75,3 @@ function setProp ( obj, prop, value ) {

layer,
compareLayer,
}

@@ -5,180 +5,8 @@ //

const { forEachDefinition, forEachMember, applyLinearly, setProp }
const { forEachDefinition, forEachMember, setProp }
= require( '../base/model');
var { setMemberParent, linkToOrigin, withAssociation } = require('./shared');
var { linkToOrigin, withAssociation } = require('./shared');
// const { refString } = require( '../base/messages')
function propagateLinearly( ...args ) {
return applyLinearly( 'propagated', ...args );
}
// the export function
function propagateAssignments( model, options = model.options || {} ) {
const typeProperties = {
type: null, // propagateType,
length: null,
precision: null,
scale: null,
items: propagateTypeProperties,
target: 0,
implicitForeignKeys: null,
source: null,
// default: null, // see propagateSingle()
// notNull: null, // see propagateSingle()
cardinality: null,
foreignKeys: propagateDictionary,
on: null, onCond: 0, // combine, copy when resolve in ON
elements: propagateDictionary,
enum: propagateDictionary,
localized: null,
virtual: null,
};
forEachDefinition( model, function( def ) {
// TODO: make exportAnnotations inspect the _finalType instead:
if (options.toI18n && options.toI18n.style == 'prop') {
propagateLinearly( def, o => o.type, propagateTypeProperties );
return;
}
if (options.tntFlavor) {
if (!options.tntFlavor.skipPropagatingFromInclude)
propagateLinearly( def, includesSource, propagateSingle );
if (!options.tntFlavor.skipPropagatingFromProjectionSrc)
propagateLinearly( def, p => p.source, propagateSingle );
}
propagateInMembers( def );
});
return model;
function propagateInMembers( def ) {
forEachMember( def, function( elem ) {
propagateLinearly( elem, o => o.origin, propagateSingle );
propagateInMembers( elem );
});
}
// Propagation function. Used to inherit along properties `origin` (used for
// members only), and with --tnt-flavor: `source` and `includes[0]`. The
// properties are the type properties and annotation assignments.
function propagateSingle( target, source ) {
if (target.kind === 'key')
return;
propagateTypeProperties( target, source );
if (target.origin && source === target.origin._artifact) { // along `origin`
// inherit `default` only along structure includes
if ('default' in source && !('default' in target)) { //&&
// !target.value) { // from structure includes
target.default = source.default;
}
// inherit `notNull` along select items without association or structure includes
if ('notNull' in source && !('notNull' in target) &&
(!target.value || withoutAssociation( target.value.path )) ) {
target.notNull = source.notNull;
}
}
if (source.params) {
target.params = propagateDictionary( target.params, source.params, target );
}
if (source.returns)
target.returns = propagateTypeProperties( null, source.returns, target );
if (options.tntFlavor) {
if (!options.tntFlavor.skipPropagatingActions)
propagateActions( target, source );
// FIXME: very questionable if we really want that
if (source.includes && !options.tntFlavor.skipPropagatingIncludes) {
target.includes = source.includes;
}
}
for (let prop in source) {
if (prop.charAt(0) !== '@' ||
prop in target ||
options.tntFlavor && !options.tntFlavor.skipNotPropagatingIndexableAnno && prop === '@com.sap.gtt.core.CoreModel.Indexable')
continue;
// TODO: if we decide that annotations have a link to the attached
// construct, we need to shallow-copy and set this link here, too
target[prop] = source[prop];
// FIXME: the following makes toI18n fail! Why?
// target[prop] = Object.assign( { $inferred: 'prop' }, source[prop] );
}
}
function withoutAssociation( path ) {
for (let item of path || []) {
if (item._artifact && item._artifact._finalType && item._artifact._finalType.target)
return false;
}
return true;
}
//function propagateType( target, source )
function propagateTypeProperties( target, source, parent ) {
if (!target) {
target = Object.assign( {}, source );
// Object.defineProperty( target, '_finalType', { value: source._finalType } );
}
if (target.kind === 'key' && // TODO: this should move below !!!!!!!
( source.target || source.type && source.type._artifact && !source.type._artifact.builtin ))
return target; // do not propagate for foreign keys which are assocs - TODO
let value = (target.redirected) ? target : (source._finalType || source);
// Propagated `elements`, `foreignKeys` etc should have a _finalType:
if (!target._finalType)
setProp( target, '_finalType', value );
// TODO: adapt foreign keys with redirect and on condition in general
if (!parent && !target.redirected) {
for (let prop in typeProperties) {
if (prop in target && typeProperties[prop] !== 0) {
setProp( target, '_finalType', target );
return target;
}
}
}
for (let prop in typeProperties) {
// if (prop === 'foreignKeys') console.log ('FK', source.kind, source.name)
if (prop in source && !(prop in target)) {
let transfer = typeProperties[prop];
target[prop] = (transfer)
? transfer( null, source[prop], parent || target )
: source[prop];
}
}
return target;
}
function propagateDictionary( target, source, parent ) {
if (!target)
target = Object.create(null);
for (let name in source) {
if (name in target)
continue;
let src = source[name];
let elem = target[name] = Object.assign( {}, src );
elem.name = Object.assign( {}, src.name );
// console.log(elem.kind,elem.name)
setMemberParent( elem, name, parent );
if (src.returns) // TODO: what about items?
elem.returns = propagateTypeProperties( null, src.returns, elem );
else
propagateTypeProperties( elem, src, elem );
if (src.params) // for actions
elem.params = propagateDictionary( null, src.params, elem );
}
return target;
}
function propagateActions( target, source ) {
if (source.actions) {
target.actions = propagateDictionary( target.actions, source.actions, target );
}
}
// If we have more than one include later, we need something like applyInOrder
function includesSource( def ) {
return def.includes && def.includes.length >= 1 && def.includes[0];
}
}
function propagate( model ) {
function propagate( model, options = model.options || {} ) {
const props = {

@@ -188,4 +16,4 @@ '@com.sap.gtt.core.CoreModel.Indexable': never,

'@cds.persistence.table': never,
'@': always,
default: always,
'@': withKind, // always except in 'returns' and 'items'
default: withKind, // always except in 'returns' and 'items'
virtual: notViaType,

@@ -197,4 +25,3 @@ notNull, // a variant of notViaType()

// key: special = done in resolver
// actions: struct includes & primary source - TODO: do in definer/resolver
// params/returns
// actions: struct includes & primary source = in definer/resolver
type: always,

@@ -209,7 +36,7 @@ length: always,

on: ( prop, target, source ) => { target[prop] = source[prop]; }, // TODO: get rid of this soon!
foreignKeys: expensive,
foreignKeys: expensive, // actually always, but dictionary copy
items,
elements: expensive,
enum: expensive,
params: expensive,
params: expensive, // actually only with parent action
returns,

@@ -225,4 +52,4 @@ };

if (!checkAndSetStatus( art )) {
// console.log('DONE:',refString(art), art.elements ? Object.keys(art.elements) : 0)
forEachMember( art, run );
if ( art.status !== 'propagated')
runMembers( art );
return;

@@ -239,21 +66,41 @@ }

}
if (source) { // the source has fully propagated properties
if (source) { // the source has fully propagated properties
step({ target, source });
}
else if (target._main) { // source is element, which has not inherited props yet
run( target._main ) // run on main artifact first
}
else if (target.includes) {
let news = [ target ];
while (news.length) {
let structs = [].concat( ...news );
news = [];
for (target of structs) {
let incl = target.includes;
if (incl) {
chain.push( ...incl.map( i => ({ target, source: i._artifact }) ) );
news.push( incl.map( i => i._artifact ).filter( checkAndSetStatus ) );
let targets = [ target ];
while (targets.length) {
let news = [];
for (let t of targets) {
for (let ref of t.includes || []) {
let s = ref._artifact;
if (!s) // ref error
continue;
chain.push( { target: t, source: s } );
if (checkAndSetStatus( s ))
news.push( s );
}
}
targets = news;
}
}
chain.reverse().forEach( step );
runMembers( art );
// console.log('DONE:',refString(art), art.elements ? Object.keys(art.elements) : 0)
}
function runMembers( art ) {
// console.log('MEMBERS:',refString(art), art.elements ? Object.keys(art.elements) : 0)
forEachMember( art, run ); // after propagation in parent!
let obj = art;
if (art.returns) {
obj = art.returns;
run( obj );
}
if (obj.items)
run( obj.items );
setProp( art, '_status', 'propagated' );
}

@@ -272,2 +119,3 @@

}
// propagate NOT NULL and VIRTUAL from sub elements:
if (target.$inferred !== 'proxy' &&

@@ -285,2 +133,3 @@ target.kind === 'element' && source.kind === 'element') {

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

@@ -294,13 +143,21 @@

setProp( target[prop], '_artifact', source[prop]._artifact );
if ('_artifact' in source[prop])
setProp( target[prop], '_artifact', source[prop]._artifact );
}
function availableAtType( prop, target, source ) {
if (target.kind === 'type' && !options.hanaFlavor)
return false;
let ref = target.type || source.type;
let type = ref && ref._artifact;
return type && !type._main && type[prop];
}
// Expensive properties are not really propagated if they can be directly
// accessed at their _finalType being a main artifact
// (TODO: improve for type T: OtherStruct.elem; )
// accessed at their type being a main artifact
// This should be adapted if elements of referred types can be annotated
// (i.e. check whether there are annotations on it)
function expensive( prop, target, source ) {
if (target._finalType && !target._finalType._main)
// console.log(prop,refString(source),'->',target.kind,refString(target),refString(type));
if (prop !== 'foreignKeys' && availableAtType( prop, target, source ))
// foreignKeys must always be copied with target to avoid any confusion
// whether we have to generated implicit keys
return;

@@ -328,4 +185,8 @@ let location = target.type && !target.type.$inferred && target.type.location

function withKind( prop, target, source ) {
if (target.kind) // not in 'returns' and 'items'
always( prop, target, source );
}
function notNull( prop, target, source, viaType ) {
// TODO: also if we access sub element - set in definer/resolver
if (!(viaType || target.value && withAssociation( target.value, targetMinZero )))

@@ -337,8 +198,7 @@ always( prop, target, source );

// TODO: remove returns in XSN
if (ok || target.$inferred === 'proxy') {
if (ok || target.$inferred === 'proxy' || target.$inferred === 'include' ) {
let origin = {};
target[prop] = { $inferred: 'proxy', origin };
setProp( origin, '_artifact', source[prop] );
setProp( target[prop], '_outer', target._outer || target );
run( target[prop] );
setProp( target[prop], '_outer', target._outer || target ); // for setMemberParent
}

@@ -348,4 +208,9 @@ }

function items( prop, target, source ) {
if (!target._finalType || target._finalType._main)
returns( prop, target, source, true );
// usually considered expensive, except:
// - array of Entity, array of String(3), array of DerivedScalar
let line = availableAtType( prop, target, source );
if (!line ||
line.type && line.type._artifact && line.type._artifact.kind === 'entity' ||
!line.elements && !line.enum && !line.items )
returns( prop, target, source, !options.hanaFlavor );
}

@@ -374,5 +239,5 @@ }

function checkAndSetStatus( art ) {
if (art._status === 'propagated')
if (art._status === 'propagated' || art._status === 'propagating')
return false;
setProp( art, '_status', 'propagated' );
setProp( art, '_status', 'propagating' );
return true;

@@ -383,3 +248,2 @@ }

propagate,
propagateAssignments
};

@@ -14,4 +14,4 @@ // Compiler phase "resolve": resolve all references

// For all type references, we set the properties `type.absolute` and
// `type._artifact`, the latter is the actual type definition.
// For all type references, we set the property `type._artifact`, the latter is
// the actual type definition.

@@ -34,3 +34,3 @@ // If the referred type definition has a `parameters` property, we use it to

// Future sequence (currently, 1 and 3 are mixed):
// Future sequence (currently, sub phases 1 and 3 are mixed):
// 1. Resolve "element building" constructs: calculate main query elements

@@ -43,7 +43,7 @@ // 2. Add implicit foreign keys

const { kindProperties, queryOps, setProp, forEachDefinition, forEachMember, forEachGeneric, forEachInOrder }
const { queryOps, setProp, forEachDefinition, forEachMember, forEachGeneric, forEachInOrder }
= require('../base/model');
var { addToDict, addToDictWithIndexNo, clearDict, dictLocation }
= require('../base/dictionaries');
const { msgName, getMessageFunction } = require('../base/messages');
const { msgName, getMessageFunction, searchName } = require('../base/messages');

@@ -53,10 +53,8 @@ var detectCycles = require('./cycle-detector');

var { fns, linkToOrigin, setMemberParent, withAssociation, storeExtension }
var { kindProperties, fns, linkToOrigin, setMemberParent, withAssociation, storeExtension }
= require('./shared');
const undefinedArtifact = { kind: 'undefined', name: { absolute: ".<unknown>." } };
const annotationPriorities = { define: 1, extend: 2, annotate: 2, edmx: 3 };
const annotationPriorities = { define: 1, extend: 2, annotate: 2, edmx: 3, i18n: 4 };
// Export function of this file. Resolve type references in augmented CSN

@@ -90,4 +88,4 @@ // `model`. If the model has a property argument `messages`, do not throw

message( 'ref-cyclic', location, { art }, 'Error', {
std: 'Illegal circular reference to $(ART)',
elem: 'Illegal circular reference to element $(ELEM) of $(ART)'
std: 'Illegal circular reference to $(ART)',
element: 'Illegal circular reference to element $(MEMBER) of $(ART)'
});

@@ -111,7 +109,5 @@ }

function resolveRefs( art ) {
// console.log( 'RESOLVE:', art.kind, refString(art.name) );
// console.log( 'RESOLVE:', art.kind, msgName(art.name) );
if (!art._deps)
setProp( art, '_deps', [] );
if (art.$tableAliases && !art._$next) // TODO: in definer - yes, with param support
setProp( art, '_$next', model.$magicVariables );
if (art.key && !art.key.$inferred && art._parent &&

@@ -126,5 +122,5 @@ !['entity', 'view', 'query'].includes( art._parent.kind ))

if (obj.type)
resolveTypeWithArguments( obj, art, art._block );
resolveTypeWithArguments( obj, art );
if (obj.target) {
resolvePath( obj.target, 'target', art, art._block );
resolvePath( obj.target, 'target', art );
if (obj.onCond) {

@@ -139,7 +135,9 @@ if (!art._parent || !art._parent.elements) {

// TODO: provide "expected" special for on condition of association
resolveExpr( obj.onCond, 'expr', art, art._main, environment( art._parent ), art._block );
resolveExpr( obj.onCond, 'expr', art, environment( art._parent ) );
}
}
else if (!obj.foreignKeys && !obj.redirected && obj.target._artifact)
else if (!obj.foreignKeys && !obj.redirected && obj.target._artifact && obj.type && obj.type._artifact && obj.type._artifact.internal) {
// no ON/explicit keys, no redirection, with existing target, type is cds.Association|Composition
implicitForeignKeys.push( art );
}
}

@@ -152,3 +150,3 @@ if (art.targetElement) { // in foreign keys

resolvePath( art.targetElement, 'element', art,
{ $msg: target._artifact }, environment( target._artifact ) );
environment( target._artifact ), target._artifact );
}

@@ -170,3 +168,3 @@ }

if (art.default)
resolveExpr( art.default, 'const', art, art._main, environment( art._parent ), art._block )
resolveExpr( art.default, 'const', art, environment( art._parent ) )

@@ -200,2 +198,6 @@ annotateMembers( art );

return;
if (!options.hanaFlavor && !options.betaMode && tc.location) {
message( null, tc.location, {}, 'Error',
'TECHNICAL CONFIGURATION is not supported yet' );
}

@@ -205,7 +207,7 @@ // secondary and fulltext indexes

index.columns.forEach( col => {
resolvePath( col, 'element', undefined, {}, art.elements );
resolvePath( col, 'element', art, art.elements );
});
if (index.language)
resolvePath( index.language.column, 'element', undefined, {}, art.elements );
resolvePath( index.mimeTypeColumn, 'element', undefined, {}, art.elements );
resolvePath( index.language.column, 'element', art, art.elements );
resolvePath( index.mimeTypeColumn, 'element', art, art.elements );
});

@@ -216,3 +218,3 @@ // fuzzy indexes

i.columns.forEach(c => {
resolvePath( c, 'element', undefined, {}, art.elements )
resolvePath( c, 'element', art, art.elements )
});

@@ -224,3 +226,3 @@ });

s.columns && s.columns.forEach( p => {
resolvePath( p, 'element', undefined, {}, art.elements)
resolvePath( p, 'element', art, art.elements)
})

@@ -239,5 +241,5 @@ })

return art.artifacts;
let type = finalType(art);
let type = finalType(art) || art;
if (type.target)
type = resolvePath( type.target, 'target', type, type._block ) || {};
type = resolvePath( type.target, 'target', type ) || {};
if (type.$from)

@@ -249,17 +251,15 @@ resolveView( type, true );

function directType( art ) {
// only used with !art.target && !art.enum ! (and !art.elements)
if (art.origin)
// only to be used with !art.target && !art.enum ! (and !art.elements)
if (art.origin) // TODO: _origin
return art.origin._artifact;
if (art.type)
return resolveType( art.type, art, art._block );
return resolveType( art.type, art );
// console.log( 'EXPR-IN', art.kind, refString(art.name) )
let query = art._parent && art._parent._leadingQuery || art._parent;
// console.log( 'EXPR-QUERY', query.kind, refString(query.name) )
if (query && query.kind === 'query') {
resolveExpr( art.value, 'expr', art, query, query.$combined, art._block );
// console.log( 'EXPR-OUT', art.value._artifact.kind, refString(art.value._artifact.name) );
return art.value._artifact;
}
// TODO: calculated fields in general, inline select item def (expand)
return undefined;
if (!query || query.kind !== 'query')
return undefined;
resolveExpr( art.value, 'expr', art, query.$combined );
// console.log( 'EXPR-OUT', art.value._artifact.kind, refString(art.value._artifact.name) );
return art.value._artifact;
}

@@ -272,32 +272,33 @@

// TODO: resolvePath( origin ) should be part of this
if (art._finalType)
if ('_finalType' in art)
return art._finalType;
var chain = [];
while (art && !art._finalType && (art.type || art.origin || art.value && art.value.path) && !art.target && !art.enum) {
while (art && !('_finalType' in art) &&
(art.type || art.origin || art.value && art.value.path) &&
!art.target && !art.enum) {
chain.push( art );
setProp( art, '_finalType', undefinedArtifact ); // initial setting in case of cycles
setProp( art, '_finalType', 0 ); // initial setting in case of cycles
art = directType( art );
}
if (!art || art._finalType === undefinedArtifact)
return undefinedArtifact; // undefined or in cycle
if (art.builtin && chain.length) {
let builtin = art;
art = chain.pop();
if (art.length || art.precision || art.scale || art.typeArguments) // TODO: move to above like art.target
if (art) {
if (art.builtin && chain.length) {
let builtin = art;
art = chain.pop();
if (art.length || art.precision || art.scale || art.typeArguments)
setProp( art, '_finalType', art );
else {
setProp( art, '_finalType', builtin );
art = builtin;
}
}
else if ('_finalType' in art) {
art = art._finalType;
}
else {
setProp( art, '_finalType', art );
else {
setProp( art, '_finalType', builtin );
art = builtin;
}
}
else if (art._finalType) {
art = art._finalType;
}
else {
setProp( art, '_finalType', art );
}
for (let a of chain) {
// TODO: we had (git tag 'nr-hana') an auto expansion of referred
// TODO: we had (git tag 'nr-hana') an auto expansion of referred
// structure types if there existed an annotation assignment for elements

@@ -350,5 +351,4 @@ // of that referred type. This is not such a good idea since later

function annotateMembers( art, extensions = [], prop, name, parent, kind ) {
let severity = !art && parent && parent.kind !== 'annotate' &&
(art === null ? 'Error' : 'Info');
if (!art && extensions.length && severity !== 'Error') {
let showMsg = !art && parent && parent.kind !== 'annotate';
if (!art && extensions.length) {
let parentExt = extensionFor(parent);

@@ -378,26 +378,29 @@ art = parentExt[prop] && parentExt[prop][name];

}
if (severity) {
// TODO: use some more generic resolveMember() function - in shared.js,
// TODO: proper use of message()
// TODO: message more like `"${parent.name.absolute}" cannot have elements`
if (prop === 'elements' || prop === 'enum')
message( null, ext.name.location,
art === null
? `No elements or enums have been found in "${parent.name.absolute}"`
: (parent.elements)
? `Element "${name}" has not been found in "${parent.name.absolute}"`
: `Enum symbol "${name}" has not been found in "${parent.name.absolute}"`,
severity )
else if (prop === 'actions')
message( null, ext.name.location,
art === null
? `No action has been found in "${parent.name.absolute}"`
: `Action "${name}" has not been found in "${parent.name.absolute}"`,
severity );
else
message( null, ext.name.location,
art === null
? `No parameter has been found in "${parent.name.absolute}"`
: `Parameter "${name}" has not been found in "${parent.name.absolute}"`,
severity );
if (showMsg) {
// somehow similar to checkDefinitions():
let feature = kindProperties[ parent.kind ][prop];
if (prop === 'elements' || prop === 'enum') {
if (!feature)
message( 'anno-unexpected-elements', ext.name.location, {}, 'Warning',
'Elements only exist in entities, types or typed constructs' );
else
message( 'anno-undefined-element', ext.name.location,
{ art: searchName( parent, name, parent.enum && 'enum' ) } );
}
else if (prop === 'actions') {
if (!feature)
message( 'anno-unexpected-actions', ext.name.location, {}, 'Warning',
'Actions and functions only exist top-level and for entities' );
else
message( 'anno-undefined-action', ext.name.location,
{ art: searchName( parent, name, 'action' ) } );
}
else {
if (!feature)
message( 'anno-unexpected-params', ext.name.location, {}, 'Warning',
'Parameters only exist for actions or functions' ); // TODO: entities betaMod
else
message( 'anno-undefined-param', ext.name.location,
{ art: searchName( parent, name, 'param' ) } );
}
}

@@ -453,3 +456,3 @@ }

message( 'anno-duplicate', a.name.location,
{ anno: annoName, '': justOnePerLayer && 'unrelated' },
{ anno: annoName, '#': justOnePerLayer && 'unrelated' },
['Error'], {

@@ -485,12 +488,17 @@ std: 'Duplicate assignment with $(ANNO)',

function resolveType( ref, user, block ) {
function resolveType( ref, user ) {
if ('_artifact' in ref)
return ref._artifact;
if (!ref.resolveSemantics)
return resolvePath( ref, 'type', user, block );
return resolvePath( ref, 'type', user );
// currently just `typeOf`
if (options.hanaFlavor && ref.path.length > 1)
return resolvePath( ref, 'type', user, block );
else if (user._parent && user._parent.elements)
return resolvePath( ref, 'typeOf', user, user._main, user._parent.elements );
message( 'ref-no-elements', ref.location, { art: user._parent || user },
'Error', 'Artifact $(ART) has no elements' );
return resolvePath( ref, 'type', user );
let parent = user._parent || user;
if (parent.elements)
return resolvePath( ref, 'typeOf', user, parent.elements );
else if (ref.path && ref.path[0])
message( 'ref-undefined-element', ref.location,
{ art: searchName( parent, ref.path[0].id ) } );
setProp( ref, '_artifact', null );
return undefined;

@@ -507,4 +515,5 @@ }

// TODO: error if no parameters applicable
function resolveTypeWithArguments( art, user, block ) {
let typeArt = resolveType( art.type, user, block );
// TODO: also check for number
function resolveTypeWithArguments( art, user ) {
let typeArt = resolveType( art.type, user );
if (!typeArt)

@@ -520,4 +529,7 @@ return;

par = { name: par };
if (!art[ par.name ] && (i < args.length || par.literal))
art[ par.name ] = args[i] || { literal: par.literal, val: par.val };
if (!art[ par.name ] && (i < args.length || par.literal)) {
let location = art.type.location;
art[ par.name ] = args[i] ||
{ literal: par.literal, val: par.val, location, $inferred: 'type-param' };
}
}

@@ -574,4 +586,5 @@ if (args.length > parLength) {

continue;
setProp( from, '_status', '_query' );
let source = resolvePath( from, 'from', view, view._block );
setProp( from, '_status', '_query' ); // before resolvePath (Cycles!)
let source = resolvePath( from, 'from', view ); // TODO: filter and args at later stage!
// console.log('ST:',msgName(source),from._status)
if (source && source.$from && source._status !== '_query')

@@ -581,2 +594,3 @@ fromChain.push( source );

}
// console.log( resolveChain.map( v => msgName(v)+v._status ) );
for (let view of resolveChain.reverse()) {

@@ -586,8 +600,7 @@ if (view._status === '_query' ) // already resolved

setProp( view, '_status', '_query' );
// TODO $parameters: params (before $magicVariables)
resolveQueryExpr( view.query, onlyInit || view !== art );
resolveQueryExpr( view.query, onlyInit || view !== art, true );
inheritActions( view );
}
if (!resolveChain.length && !onlyInit)
resolveQueryExpr( art.query );
resolveQueryExpr( art.query, false, true );
}

@@ -619,26 +632,8 @@

function resolveQueryExpr( query, onlyInit ) {
while (query instanceof Array) // query in parentheses
query = query[0];
if (!query)
return;
if (query.join) {
for (let tab of query.args)
resolveQueryExpr( tab, onlyInit );
if (!onlyInit && query.on)
resolveQuery( query, onlyInit );
}
else if (query.op && query.op.val === 'query') { // select
for (let tab of query.from)
resolveQueryExpr( tab, onlyInit );
resolveQuery( query, onlyInit );
}
else if (query.args) { // UNION, INTERSECT, ..., sub query
for (let tab of query.args)
resolveQueryExpr( tab, onlyInit );
}
// else: with parse error (`select from <EOF>`)
function resolveQueryExpr( query, onlyInit, keyPropagation ) {
traverseQueryPost( (q, s) => resolveQuery( q, onlyInit, s ),
query, keyPropagation );
}
function resolveQuery( query, onlyInit ) {
function resolveQuery( query, onlyInit, keyPropagation ) {
if (!query._$next)

@@ -649,7 +644,10 @@ setProp( query, '_$next', model.$magicVariables );

forEachGeneric( query, '$tableAliases', resolveTabRef);
if (query.all)
expandWildcard();
if (query.from)
initFromColumns( query._main._leadingQuery === query && query._main.elements );
if (query.exclude)
resolveExcluding();
forEachGeneric( { elements: query.elements }, 'elements', initElem );
}
if (!withKeyPropagation())
// must be somehow outside "onlyInit" as we check complete expressions (of select items)
if (!keyPropagation || !withKeyPropagation())
forEachGeneric( { elements: query.elements }, 'elements', resetKey );

@@ -663,4 +661,3 @@ if (onlyInit)

// TODO: name resolution for types etc in MIXIN definitions of subqueries?
if (options.betaMode || options.newCsn)
forEachGeneric( { elements: query.elements }, 'elements', resolveRedirected );
forEachGeneric( { elements: query.elements }, 'elements', resolveRedirected );
forEachGeneric( { elements: query.elements }, 'elements', resolveElem );

@@ -670,10 +667,10 @@ if (dictOB)

if (query.on)
resolveExpr( query.on, 'expr', null, query, query.$combined, query._block );
resolveExpr( query.on, 'expr', query, query.$combined );
if (query.where)
resolveExpr( query.where, 'expr', null, query, query.$combined, query._block );
resolveExpr( query.where, 'expr', query, query.$combined );
if (query.groupBy) {
for (let gb of query.groupBy)
resolveExpr( gb, 'expr', null, query, query.$combined, query._block );
resolveExpr( gb, 'expr', query, query.$combined );
}
resolveExpr( query.having, 'expr', null, query, query.$combined, query._block );
resolveExpr( query.having, 'expr', query, query.$combined );
resolveOrderBy();

@@ -683,2 +680,15 @@ return;

function withKeyPropagation() {
let propagateKeys = false;
for (let name in query.elements) {
let es = query.elements[name];
for (let elem of (es instanceof Array) ? es : [es]) {
if (elem.key) {
if (!elem.key.$inferred || elem.key.val === undefined)
return false; // explicit key or already resetted
propagateKeys = true;
}
}
}
if (!propagateKeys)
return false; // no keys to propagate -> no Info messages
let leading = query._leadingQuery || query;

@@ -689,8 +699,17 @@ // Check that only one source, not navigating along to-many association:

let from = leading.from[0]; // TODO: what about `union` - allow currently
if (!from) // parse error SELECT FROM <EOF>
return false;
if (from.join)
return false;
if (from && from.path && // TODO: re-check sub query in FROM
withAssociation( from, targetMaxNotOne, true ))
return false;
let toMany = from && withAssociation( from, targetMaxNotOne, true );
if (toMany) {
propagateKeys = false;
message( 'query-from-many', toMany.location, { art: toMany },
'Info', {
std: 'Selecting from to-many association $(ART) - key properties are not propagated',
element: 'Selecting from to-many association $(MEMBER) of $(ART) - key properties are not propagated'
} );
}
// Check that all keys from the source are projected:
let notProjected = [];
let navElems = leading._firstAliasInFrom && leading._firstAliasInFrom.$navigation;

@@ -700,4 +719,12 @@ for (let name in navElems) {

if (nav.key && nav.key.val && !(nav._projections && nav._projections.length))
return false;
notProjected.push( nav.name.id );
}
if (notProjected.length) {
propagateKeys = false;
message( 'query-missing-keys', from.location, { names: notProjected },
'Info', {
std: 'Keys $(NAMES) have not been projected - key properties are not propagated',
one: 'Key $(NAMES) has not been projected - key properties are not propagated'
} );
}
// Check that there is no to-many assoc used in select item:

@@ -707,8 +734,21 @@ for (let name in leading.elements) {

if (!elem.$inferred && elem.value &&
testExpr( elem.value, r => withAssociation( r, targetMaxNotOne ), ()=>false ))
return false;
testExpr( elem.value, selectTest, ()=>false ))
propagateKeys = false;
}
return true;
return propagateKeys;
}
function selectTest( expr ) {
let art = withAssociation( expr, targetMaxNotOne );
if (art) {
message( 'query-navigate-many', art.location, { art },
'Info', {
std: 'Navigating along to-many association $(ART) - key properties are not propagated',
element: 'Navigating along to-many association $(MEMBER) of $(ART) - key properties are not propagated',
alias: 'Navigating along to-many mixin association $(MEMBER) - key properties are not propagated'
} );
}
return art;
}
function resetKey( elem ) {

@@ -719,2 +759,83 @@ if (elem.key && elem.key.$inferred)

function initFromColumns( specifiedElements ) {
let elements = specifiedElements || Object.create(null);
if (specifiedElements)
query.elements = specifiedElements;
else {
clearDict( query, 'elements' );
if (query._main._leadingQuery === query)
query._main.elements = query.elements;
}
let wildcard = false;
for (let col of query.columns || [{ val: '*' }]) {
if (col.val === '*') {
wildcard = col.location || query.from[0] && query.from[0].location || query.location;
continue;
}
col.kind = 'element';
if (!col.value)
continue; // error should have been reported by parser
if (!col.name) {
let path = col.value.path;
if (!path) {
message( 'query-req-name', col.value.location || col.location, {},
'Error', 'Alias name is required for this select item' );
}
else if (path.length && !path.broken) {
let last = path[ path.length-1 ];
if (last)
col.name = { id: last.id, location: last.location, $inferred: 'as' };
}
}
let id = col.name && col.name.id;
if (!id)
continue;
let elem = specifiedElements && specifiedElements[id];
if (elem) {
elem.value = col.value;
elem.$replacement = true;
// `on` and `keys` rewrite not finished yet:
delete elem.onCond;
delete elem.foreignKeys;
if (elem.target)
delete elem.type;
else if (elem.type && !col.type)
elem.type.$inferred = 'elements';
delete elem.target;
if (col.target) { // TODO: get rid of redirected property
elem.redirected = { val: true, location: col.target.location };
elem.target = col.target;
}
if (elem.key && !col.key)
elem.key.$inferred = 'elements';
setProp( elem, '_parent', query );
elem.name.query = query.name.query;
continue;
}
if (specifiedElements) {
message( 'query-missing-element', col.name.location, { id },
'Warning', 'Element $(ID) is missing in specified elements' );
}
addToDict( elements, id, col, function( name, location ) {
message( 'duplicate-definition', location, { name, '#': 'element' },
'Error', { element: 'Duplicate definition of element $(NAME)' } );
});
setMemberParent( col, id, query );
if (!wildcard) {
addToDictWithIndexNo( query, 'elements', id, col );
col.$replacement = true;
}
}
if (wildcard)
expandWildcard( elements, wildcard, specifiedElements );
if (specifiedElements) {
for (let name in specifiedElements) {
let elem = specifiedElements[name];
if (!elem.$replacement && elem.name) // no name with redefs
message( 'query-unspecified-element', elem.name.location, { id: name },
'Error', 'Element $(ID) is not properly specified' );
}
}
}
function initElem( elem ) {

@@ -724,3 +845,3 @@ //if (elem.type) console.log(msgName(elem),elem)

if (elem.type && !elem.type.$inferred && !elem.viaAll) {
resolveTypeWithArguments( elem, elem, elem._block );
resolveTypeWithArguments( elem, elem );
return;

@@ -730,4 +851,4 @@ }

return;
let origin = resolvePath( elem.value, 'expr', undefined,
query, query.$combined, elem._block );
//console.log(msgName(query),query.name.query,query.$tableAliases,msgName(elem))
let origin = resolvePath( elem.value, 'expr', elem, query.$combined );
if (!origin)

@@ -739,3 +860,5 @@ return;

let pathnav = navElem.element; // undefined for CURRENT_DATE
if (!elem.key && pathnav && pathnav.key && !withAssociation( elem.value ))
if (!elem.key && pathnav && pathnav.key &&
navElem.item === elem.value.path[ elem.value.path.length-1 ] && // I can now probably remove some other sub conditions...
!withAssociation( elem.value ))
elem.key = Object.assign( { $inferred: 'query' }, pathnav.key );

@@ -752,3 +875,3 @@ if (pathnav && navElem.item === elem.value.path[ elem.value.path.length-1 ])

// console.log('REDIRECTED:',elem.target)
let target = resolvePath( elem.target, 'target', elem, elem._block );
let target = resolvePath( elem.target, 'target', elem );
setProp( elem, '_origTarget', undefined ); // null = do not touch path steps after assoc

@@ -779,8 +902,12 @@ let assoc = elem.origin && elem.origin._artifact;

while (target.query) {
let from = target.query.from;
let from = (target.query.args) ? [1,2] : target.query.from;
if (!from || !from.length) // parse error
return;
if (from.length > 1 || !from[0].path) {
message( 'redirected-to-complex', elem.target.location, { art: target },
'Warning', 'The redirected target $(ART) is a complex view' );
message( 'redirected-to-complex', elem.target.location,
{ art: target, '#': target === elem.target._artifact ? 'target' : 'std' },
'Warning', {
std: 'Redirection involves the complex view $(ART)',
target: 'The redirected target $(ART) is a complex view'
});
break;

@@ -805,3 +932,3 @@ }

message( 'redirected-to-unrelated', elem.target.location, { art: origTarget },
'Warning', 'The redirected target does not originate from $(ART)' );
['Error'], 'The redirected target does not originate from $(ART)' );
return;

@@ -823,10 +950,11 @@

if (!art.query) // yes, no else if
let query = art._leadingQuery;
if (!query)
return false;
else if (!art.query.$tableAliases) // something wrong with query
return true; // -> no message about redirected target
else if (!query.$tableAliases) // something wrong with query
return true; // -> no message about redirected target
// With Node 7, we could do: Object.values(…).filter(…).map(…)
let sources = [];
for (let n in art.query.$tableAliases) {
let a = art.query.$tableAliases[n];
for (let n in query.$tableAliases) {
let a = query.$tableAliases[n];
if (a.type && !a.self && !a.name.$mixin)

@@ -848,3 +976,3 @@ sources.push( a.type._artifact );

// ony direct paths have been resolved so far (w/o explicit type), no expression:
resolveExpr( elem.value, 'expr', elem, query, query.$combined, elem._block );
resolveExpr( elem.value, 'expr', elem, query.$combined );
finalType( elem );

@@ -865,6 +993,6 @@ }

let pathnav = pathNavigation( elem.value );
// console.log( 'ASSOC:', elem.value, pathnav )
// console.log( 'ASSOC:', msgName(elem), elem.value,(pathnav) )
let nav = pathnav.element;
if (!nav) // $projection, $magic, or future ref to const
return;
if (!nav)
return; // $projection, $magic, or future ref to const
// Currently, having an unmanaged association inside a struct is not

@@ -879,3 +1007,3 @@ // supported by this function:

let target = elem.target && !elem.target.$inferred &&
resolvePath( elem.target, 'target', elem, elem._block );
resolvePath( elem.target, 'target', elem );
if (target) {

@@ -933,8 +1061,18 @@ let from = target.query && target.query.from; // query entity (no assoc)

function expandWildcard() {
let elements = Object.assign( Object.create(null), query.elements );
clearDict( query, 'elements', true );
function resolveExcluding() {
for (let name in query.exclude) {
if (!query.$combined[name]) {
message( 'ref-undefined-excluding', query.exclude[name].location, { name },
'Info', 'Element $(NAME) has not been found' )
.validNames = query.$combined;
if (options.testMode)
message( null, query.exclude[name].location, {}, 'Info',
'Valid: ' + Object.keys( query.$combined ).sort().join(', ') );
}
}
}
function expandWildcard( elements, location, specifiedElements ) {
let exclude = query.exclude || Object.create(null);
// TODO: paths in exclude? - Did I mention it? EXCLUDING is ill-defined...
let location = query.all.location;
for (let name in query.$combined) {

@@ -945,5 +1083,7 @@ let navElem = query.$combined[name];

let selElem = elements[name];
if (selElem && selElem.name) { // no name with parse error or repeated def
if (selElem && (!specifiedElements || selElem.$replacement)) {
if (!selElem.name) // no name with parse error or repeated def
continue;
let path = selElem.value && selElem.value.path;
// TODO: to bring a message for ParentElem.Assoc, we should move this
// TODO: to bring the message below for ParentElem.Assoc, we should move this
// check to resolveElem for elems with $replacement, by comparing the

@@ -961,10 +1101,41 @@ // name with the name.element of the _origin.

}
selElem.$replacement = true;
addToDictWithIndexNo( query, 'elements', name, selElem );
if (!selElem.$replacement) {
selElem.$replacement = true;
addToDictWithIndexNo( query, 'elements', name, selElem );
}
}
else if (navElem instanceof Array) {
message( 'wildcard-ambiguous', location, { id: name },
'Error', 'Ambiguous, select $(ID) explicitly with table alias' );
let names = navElem.filter( e => !e.$duplicate)
.map( e => e.name.alias + '.' + e.name.element );
if (names.length)
message( 'wildcard-ambiguous', location, { id: name, names },
'Error', 'Ambiguous wildcard, select $(ID) explicitly with $(NAMES)' );
}
else if (selElem) { // from specified elements of leading query
let origin = navElem.origin._artifact;
selElem.origin = { location };
delete selElem.origin.id;
delete selElem.origin.quoted;
let elem = selElem;
setProp( selElem.origin, '_artifact', origin );
setElementOrigin( elem, navElem, name, location );
selElem.$replacement = true;
// `on` and `keys` rewrite not finished yet:
delete selElem.onCond;
delete selElem.foreignKeys;
if (selElem.target)
delete selElem.type;
else if (selElem.type)
selElem.type.$inferred = '*';
delete selElem.target;
// if (selElem.key)
// selElem.key.$inferred = '*'; // part of setElementOrigin
// TODO: we could check whether all properties on the column has been
// properly propageted: annotation assignments, type properties
}
else {
if (specifiedElements) {
message( 'query-missing-element', location, { id: name },
'Warning', 'Element $(ID) is missing in specified elements' );
}
let origin = navElem.origin._artifact;

@@ -975,18 +1146,4 @@ let elem = linkToOrigin( origin, name, query, 'elements', location );

elem.name.calculated = true;
let aliasObj = navElem._parent;
let aliasStep = { id: aliasObj.name.id, location };
let step = { id: name, location };
elem.value = elem.origin;
// always expand * to path with table alias (reason: columns current_date etc)
elem.value.path = [ aliasStep, step ];
let real = aliasObj.origin || aliasObj.type; // is undefined for sub query
setProp( aliasStep, '_artifact', real && real._artifact || aliasObj );
setProp( aliasStep, '_navigation', aliasObj );
setProp( step, '_artifact', origin );
setProp( step, '_navigation', navElem ); // TODO: really set this?
addProjection( elem, navElem );
elem.viaAll = true;
if (origin.key && aliasObj === aliasObj._parent._firstAliasInFrom)
elem.key = Object.assign( { $inferred: '*' }, origin.key );
// TODO: _finalType?
setElementOrigin( elem, navElem, name, location );
}

@@ -1000,2 +1157,20 @@ }

function setElementOrigin( queryElem, navElem, name, location ) {
let sourceElem = navElem.origin._artifact;
let alias = navElem._parent;
let path = [ { id: alias.name.id, location }, { id: name, location } ];
queryElem.value = queryElem.origin;
// always expand * to path with table alias (reason: columns current_date etc)
queryElem.value.path = path;
let real = alias.origin || alias.type; // is undefined for sub query
setProp( path[0], '_artifact', real && real._artifact || alias );
setProp( path[0], '_navigation', alias );
setProp( path[1], '_artifact', sourceElem );
addProjection( queryElem, navElem );
if (sourceElem.key && sourceElem.key.val !== undefined &&
alias === alias._parent._firstAliasInFrom)
queryElem.key = Object.assign( { $inferred: '*' }, sourceElem.key );
// TODO: _finalType?
}
// Note the strange name resolution (dynamic part) for ORDER BY: the same

@@ -1005,3 +1180,3 @@ // as for select items if it is an expression, but first look at select

// ORDER BY of an UNION, do not allow any dynamic path in an expression,
// and only allow the eements of the leading query if it is a path.
// and only allow the elements of the leading query if it is a path.
//

@@ -1018,7 +1193,6 @@ // This seem to be similar, but different in SQLite 3.22.0: ORDER BY seems

for (let ob of [...query.$orderBy||[], ...query.orderBy||[] ]) {
resolveExpr( ob.value, 'expr', null, leading,
resolveExpr( ob.value, 'expr', leading,
(ob.value && ob.value.path)
? (ob._$queryNode ? leading.elements : leading.$dictOrderBy)
: (ob._$queryNode ? emptyDict : leading.$combined ),
leading._block );
: (ob._$queryNode ? emptyDict : leading.$combined ) );
}

@@ -1032,8 +1206,8 @@ }

if (alias.type)
resolveTypeWithArguments( alias, main, main._block );
resolveTypeWithArguments( alias, main );
if (alias.target) {
resolvePath( alias.target, 'target', main, main._block );
resolvePath( alias.target, 'target', alias );
// TODO: disallow foreign keys, resolve ON, but but beware $self
if (alias.onCond)
resolveExpr( alias.onCond, 'expr', alias, query, query.$combined, main._block );
resolveExpr( alias.onCond, 'expr', alias, query.$combined );
else

@@ -1051,14 +1225,16 @@ message( 'assoc-in-mixin', alias.target.location, {},

if (!alias.$navigation) {
let tab = resolvePath( alias.type, 'from', query._main._block, query._main );
// if (main._block.$frontend!=='json') console.log('TABREF:',alias.name,main,main._block)
let tab = resolvePath( alias.type, 'from', query );
for (let step of tab && alias.type.path || []) {
if (!step._artifact)
break;
if (step.namedArgs)
resolveParams( step.namedArgs, step._artifact, query._main._block ); // block for future :const
if (step.namedArgs) {
let extDict = query._parent.$combined; // for arguments
resolveParams( step.namedArgs, step._artifact, query, extDict );
}
if (step.where) // TODO: support for $projection, $parameters, ...?
resolveExpr( step.where, 'element', null, {},
environment( step._artifact ), query._main._block );
resolveExpr( step.where, 'filter', query, environment( step._artifact ) );
}
let isPrimaryAlias = alias === alias._parent._firstAliasInFrom;
let elements = alias.elements || environment(tab); // sub query or table ref
let elements = environment( alias ); // sub query or table ref
forEachGeneric( { elements }, 'elements', function( origin, name ) {

@@ -1078,3 +1254,3 @@ let elem = linkToOrigin( origin, name, alias, '$navigation',

forEachGeneric( { elements: alias.$navigation }, 'elements', function( elem, name ) {
addToDict( query.$combined, name, elem );
addToDict( query.$combined, name, elem, null );
});

@@ -1105,4 +1281,3 @@ }

// even with lint-mode, we complain about undefined "cds.*"
message( 'ref-undefined-art', location, { art: def.name },
'Error', 'Artifact $(ART) has not been found' );
message( 'ref-undefined-def', location, { art: def.name } );
}

@@ -1121,2 +1296,3 @@ }

if (expr.path.length > 1 && expr.path[1].id.charAt(0) !== '$') {
// console.log('!nav:',expr.path.map(x=>x.id), tableAlias); throw new Error()
// some backends have problems with $self -> delete it if unnecessary

@@ -1130,2 +1306,3 @@ expr.path.shift();

if (tableAlias) { // comparison with $self -> semantics to define
// TODO: strange with test/toHana/BacklinkAssocs.cds
message( null, user && user.value && user.value.location || pathnav.item.location,

@@ -1162,4 +1339,9 @@ `Association ${msgName( user.name.element )} has inferred ON condition with unclear $self comparison`,

// - tableAlias.elem -> { element: navElem, item: path[1], tableAlias }
// - sourceElem (in query) -> { element: navElem, item: path[0], tableAlias }
// - mixinElem -> { element: mixinElement, item: path[0] }
// - $self/$projection -> { item: path[0] }
// - $now, current_date -> {}
function pathNavigation( ref ) {
// currently, indirectly projectable elements are not included - we might
// currently, indirectly projectable elements are not included - we might
// keep it this way! If we want them to be included - be aware: cycles

@@ -1177,3 +1359,3 @@ let item = ref.path && ref.path[0];

if (root.kind !== '$tableAlias' || ref.path.length < 2)
return {};
return {}; // should not happen
item = ref.path[1];

@@ -1199,14 +1381,14 @@ return { element: root.$navigation[ item.id ], item, tableAlias: root };

function resolveExpr( expr, expected, user, env, extDict, block ) {
function resolveExpr( expr, expected, user, extDict ) {
if (!expr || typeof expr === 'string') // parse error or keywords in {xpr:...}
return;
if (expr instanceof Array)
expr.forEach( e => resolveExpr( e, expected, user, env, extDict, block ) );
expr.forEach( e => resolveExpr( e, expected, user, extDict ) );
else if (expr.path) {
if (expected instanceof Function) {
expected( expr, user, env, extDict, block );
expected( expr, user, extDict );
return;
}
// TODO: user!!!, TODO: block for constant and parameter access!
resolvePath( expr, expected, undefined, env, extDict ); // TODO: user!!!
resolvePath( expr, expected, user, extDict );
// TODO: put the following in resolvePath (extra arg if allowed)?

@@ -1218,3 +1400,3 @@ for (let step of expr.path) {

// TODO: also check for just assoc
resolveParams( step.namedArgs, step._artifact, block ); // block for future :const
resolveParams( step.namedArgs, step._artifact, user, extDict );
if (step.where) { // TODO: support for $projection, $parameters, ...?

@@ -1227,4 +1409,3 @@ let assoc = finalType( step._artifact );

else
resolveExpr( step.where, 'element', null, { $msg: assoc.target },
environment( step._artifact ), block );
resolveExpr( step.where, 'filter', user, environment( step._artifact ) );
}

@@ -1244,10 +1425,9 @@ }

if (func && func.toUpperCase() === 'ROUND') // TODO: temp?
resolveExpr( expr.args[0], expected, user, env, extDict, block )
resolveExpr( expr.args[0], expected, user, extDict )
else
expr.args.forEach( e => resolveExpr( e, expected, user, env, extDict, block ) );
expr.args.forEach( e => resolveExpr( e, expected, user, extDict ) );
}
}
// eslint-disable-next-line no-unused-vars
function resolveParams( dict, art, block ) { // TODO: block for :param or :const
function resolveParams( dict, art, user, extDict ) {
let type = finalType( art );

@@ -1263,3 +1443,3 @@ if (type.target) {

first = first[0];
message( 'ref-no-params',
message( 'args-no-params',
dictLocation( dict, first && first.name && first.name.location ), { art },

@@ -1275,5 +1455,5 @@ ['Error'], 'Entity $(ART) has no parameters' );

if (!param)
message( 'ref-undefined-param', a.name.location, { art, id: name },
message( 'args-undefined-param', a.name.location, { art, id: name },
['Error'], 'Entity $(ART) has no parameter $(ID)' );
resolveExpr( a, 'expr', null, {} ); // only param and const, TODO: block
resolveExpr( a, 'expr', user, extDict );
}

@@ -1326,2 +1506,4 @@ }

// Return true if the path `item` with a final type `assoc` has a max target
// cardinality greater than one - either specified on the path item or assoc type.
function targetMaxNotOne( assoc, item ) {

@@ -1333,2 +1515,37 @@ // Semantics of associations without provided cardinality: [*,0..1]

// Query tree post-order traversal - called for everything which makes a query
// except "real ones": operands of UNION etc, JOIN with ON, and sub queries in FROM
function traverseQueryPost( callback, query, simple ) {
// console.log( msgName(query), query.name.query, onlyInit );
while (query instanceof Array) // query in parentheses
query = query[0];
if (!query || !query.op) // parser error or just path
return;
if (query.join) { // JOIN -> not simple
for (let tab of query.args)
traverseQueryPost( callback, tab );
if (query.on)
callback( query );
}
else if (query.op.val === 'query') { // SELECT -> simple with simple FROM
if (query.from.length != 1)
simple = false;
else if (simple) {
let from = query.from[0];
while (from instanceof Array)
from = from[0];
if (from.join)
simple = false;
}
for (let tab of query.from)
traverseQueryPost( callback, tab, simple );
callback( query, simple );
}
else if (query.args) { // UNION, INTERSECT, ... -> not simple
for (let tab of query.args)
traverseQueryPost( callback, tab );
}
// else: with parse error (`select from <EOF>`)
}
module.exports = resolve;

@@ -6,8 +6,48 @@ // Compiler functions and utilities shared across all phases

const { msgName } = require('../base/messages');
const alerts = require('../base/alerts');
const { kindProperties } = require('../base/model');
const { searchName, getMessageFunction } = require('../base/messages');
const { addToDict, addToDictWithIndexNo, pushToDict } = require('../base/dictionaries');
const dictKinds = {
definitions: 'absolute',
elements: 'element',
enum: 'enum',
foreignKeys: 'key',
actions: 'action',
params: 'param',
}
const kindProperties = {
// TODO: also foreignKeys ?
namespace: { artifacts: true }, // on-the-fly context
context: { artifacts: true, normalized: 'namespace' },
service: { artifacts: true, normalized: 'namespace' }, // actions: true with "service-bound" actions
entity: { elements: true, actions: true }, // beta-mode - params: () => false
view: { elements: true, actions: true }, // beta-mode - params: () => false
query: { elements: true },
$tableAlias: { normalized: 'alias', $navigation: true }, // table alias in select
$navElement: { normalized: 'element', $navigation: true },
type: { elements: propExists, enum: propExists },
annotation: { elements: propExists, enum: propExists },
const: {},
enum: { normalized: 'element' },
element: { elements: propExists, enum: propExists, dict: 'elements' },
action: { params: () => false, elements: () => false, enum: () => false, dict: 'actions' }, // no extend params, only annotate
function: { params: () => false, elements: () => false, enum: () => false, normalized: 'action' }, // no extend params, only annotate
key: { normalized: 'element' },
param: { elements: () => false, enum: () => false, dict: 'params' },
source: { artifacts: true },
block: { artifacts: true },
using: {},
extend: { isExtension: true },
annotate: { isExtension: true, elements: true, enum: true, actions: true, params: true },
builtin: {}, // = CURRENT_DATE, TODO: improve
$parameters: {}, // $parameters in query entitis
}
function propExists( prop, parent ) {
let obj = parent.returns || parent;
return (obj.items || obj)[prop];
}
// Main export function of this file. Return "resolve" functions shared for phase

@@ -20,7 +60,7 @@ // "define" and "resolve". Argument `model` is the augmented CSN. Optional

var options = model.options || {};
const { error, warning, signal } = alerts(model);
const message = getMessageFunction( model );
const specExpected = {
annotation: { useDefinitions: true, severity: 'None' },
extend: { useDefinitions: 'NullIfNotFound' }, // ref in top-level EXTEND
annotate: { useDefinitions: true, severity: 'Info' }, // ref in top-level ANNOTATE
annotation: { useDefinitions: true, noMessage: true },
extend: { useDefinitions: true }, // ref in top-level EXTEND
annotate: { useDefinitions: true, undefinedDef: 'anno-undefined-def', undefinedArt: 'anno-undefined-art' },
type: { reject: rejectNonType }, // TODO: more detailed later (e.g. for enum base type?)

@@ -30,7 +70,11 @@ typeOf: { next: '_$next' },

context: { reject: rejectNonContext },
target: { reject: rejectNonEntity, noDep: true },
target: { reject: rejectNonEntity, noDep: true }, // TODO: dep for (explicit+implicit!) foreign keys
element: { next: '__none_' },
filter: { next: '_$next', lexical: 'main' },
from: { reject: rejectNonSource },
const: { next: '_$next', reject: rejectNonConst },
expr: { next: '_$next' } // TODO: better - on condition for assoc, other on
expr: { next: '_$next', escape: 'param', noDep: true },
// expr TODO: better - on condition for assoc, other on
// expr TODO: write dependency, but care for $self
param: { reject: rejectNonConst },
}

@@ -42,8 +86,6 @@

defineAnnotations,
message: signal,
error, warning,
};
function rejectNonConst( art ) {
return (art.kind === 'builtin') ? undefined : 'A constant value is expected here';
return ['builtin','param','const'].includes( art.kind ) ? undefined : 'expected-const';
}

@@ -54,3 +96,3 @@

? undefined
: 'A structured type or a non-query entity is expected here';
: 'expected-struct';
}

@@ -61,3 +103,3 @@

? undefined
: 'A context or service is expected here';
: 'expected-context'
}

@@ -71,3 +113,3 @@

? undefined
: 'A type or an element of a type is expected here';
: 'expected-type';
}

@@ -78,3 +120,3 @@

? undefined
: 'An entity, projection or view is expected here';
: 'expected-entity';
}

@@ -87,7 +129,7 @@

if (!['view', 'entity'].includes( main._artifact.kind ))
return 'A source must start at an entity, projection or view';
return 'expected-source'; // orig: 'A source must start at an entity, projection or view';
environment( art ); // sets _finalType on art
return (!art._finalType || art._finalType.target)
? undefined
: 'The path must end with an association';
: 'expected-source'; // orig: 'The path must end with an association'
}

@@ -101,12 +143,12 @@

// Used for collecting artifact extension, and annotation assignments.
function resolveUncheckedPath( ref, expected, env ) {
if( ref.absolute && !ref.path )
return ref.absolute;
function resolveUncheckedPath( ref, expected, user ) {
if (!ref.path || ref.path.broken) // incomplete type AST
return undefined;
let spec = specExpected[expected];
let art = getPathRoot( ref.path, spec, env );
if (!art)
return (spec.useDefinitions === true) ? pathName( ref.path ) : undefined;
setProp( ref.path[0], '_artifact', art );
let art = getPathRoot( ref.path, spec, user._block, null, true );
if (art === false) // redefinitions
art = ref.path[0]._artifact[0]; // array stored in head's _artifact
else if (!art)
return (spec.useDefinitions) ? pathName( ref.path ) : null;
// art can be using proxy...
if (ref.path.length > 1)

@@ -122,96 +164,112 @@ return art.name.absolute + '.' + pathName( ref.path.slice(1) );

// `user` to the found artifact if `user` is provided.
function resolvePath( ref, expected, user, env, extDict ) {
if (ref == undefined) // no references -> nothing to do
function resolvePath( ref, expected, user, extDict, msgArt ) {
if (ref == null) // no references -> nothing to do
return undefined;
if ('_artifact' in ref) // also true for _artifact: undefined
return ref._artifact;
if (ref.param) // :path not yet supported
if (!ref.path || ref.path.broken || !ref.path.length) // incomplete type AST or empty env (already reported)
return undefined;
let spec = specExpected[expected];
if( ref.absolute && !ref.path ) { // TODO: this should disappear
let art = !ref.query && !ref.alias && model.definitions[ref.absolute];
art = resolveMember( art, ref.action, 'actions' );
art = resolveMember( art, ref.param, 'params' );
art = resolveMember( art, ref.element );
setProp( ref, '_artifact', art );
if (!art && spec.severity !== 'None')
signal( error`Artifact ${msgName( ref )} has not been found`,
ref.location, spec.severity );
return art;
let path = ref.path;
let head = path[0];
//message(null,head.location,{art:user,expected, id: head.id},'Info','User $(ART), $(EXPECTED) $(ID)')
let env = user._block; // artifact references: block
if (ref.scope === 'param') {
if (!spec.escape) {
let variant = (env.$frontend && env.$frontend !== 'cdl') ? 'std' : 'cdl';
message( 'ref-unexpected-scope', head.location, { name: head.id, '#': variant },
'Error', {
std: 'Unexpected parameter scope for name $(NAME)',
cdl: 'Unexpected `:` before name $(NAME)'
} );
return setLink( ref, null );
}
spec = specExpected[ spec.escape ];
// In queries and query entities, the first lexical search environment
// are the parameters, otherwise the block. It is currently ensured that
// _block in queries is the same as _block of the query entity:
let lexical = (user._main||user).$tableAliases; // queries (but also query entities)
env = lexical && lexical.$parameters || user._block;
extDict = null; // let getPathRoot() choose it
}
if (!ref.path || ref.path.broken || !ref.path.length || !env) // incomplete type AST or empty env (already reported)
return undefined;
var [head, ...tail] = ref.path;
else if (spec.next === '__none_')
env = {};
else if (spec.next) { // TODO: combine spec.next / spec.lexical to spec.lexical
let query = (spec.lexical === 'main')
? user._main // in path filter, just $magic (and $parameters)
: (user.kind === 'query')
? user
: user._parent && user._parent.kind === 'query' && user._parent;
env = (spec.lexical === 'from') ? query._parent : query || user._main || user;
// queries: first tabaliases, then $magic - value refs: first $self, then $magic
// TODO: set extDict to query.$combined if extDict is not set
}
// if (!head) console.error(ref)
// 'global' for CSN later in value paths, CDL for Association/Composition:
var art = (ref.scope === 'global')
? getPathRoot( ref.path, spec, {}, model.definitions )
: getPathRoot( ref.path, spec, env, extDict );
if (art && art.kind === 'using') {
let art = (ref.scope === 'global')
? getPathRoot( path, spec, {}, model.definitions )
: getPathRoot( path, spec, env, extDict, msgArt || 0 );
if (!art)
return setLink( ref, art );
else if (art.kind === 'using') {
art = model.definitions[ art.name.absolute ];
if (!art)
return setLink( ref, art );
else if (art instanceof Array) // redefined art referenced by using proxy
return setLink( ref, false );
setLink( head, art ); // we do not want to see the using
}
else if (art && art.name.$mixin) {
setProp( head, '_navigation', art );
else if (art.name.$mixin) { // TODO: art.kind === 'mixin'
setLink( head, art, '_navigation' );
}
else if (art && kindProperties[ art.kind ].$navigation ) { // '$tableAlias, $navElement
setProp( head, '_navigation', art );
let real = art.origin || art.type; // is undefined for sub query
art = real && real._artifact || art;
else if (art.kind === '$navElement') {
setLink( head, art, '_navigation' );
setLink( head, art.origin._artifact );
}
setProp( head, '_artifact', art );
else if (art.kind === '$tableAlias') {
setLink( head, art, '_navigation' );
if (art.type) { // FROM reference
let assoc = art._finalType && art._finalType.target;
art = setLink( head, assoc ? assoc._artifact : art.type._artifact );
if (!art)
return setLink( ref, art );
}
else { // FROM subquery, $projection
setLink( head, art._finalType ); // the query (sub or self)
}
}
if (art) {
art = getPathItem( tail, spec, art, head );
art = getPathItem( path, spec );
if (!art)
return setLink( ref, art );
if (art.$autoElement) {
let location = path[ path.length-1 ].location;
let step = { id: art.$autoElement, $inferred: '$autoElement', location };
art = art.elements[ step.id ];
setLink( step, art );
path.push( step );
}
if (art && spec.reject) {
let msg = spec.reject( art, ref.path );
if (spec.reject) {
let msg = spec.reject( art, path );
if (msg) {
signalNotFound( msg, ref.location );
art = undefined;
return setLink( ref, false );
}
}
if (art) {
if (art.name.absolute) // TODO: theses settings will disappear
ref.absolute = art.name.absolute;
if (art.name.element)
ref.element = art.name.element;
if (art.name.alias)
ref.alias = art.name.alias;
if (art.name.query != null)
ref.query = art.name.query;
if (art.name.action)
ref.action = art.name.action;
if (art.name.param)
ref.param = art.name.param;
if (user && !spec.noDep) {
let location = ref.location || combinedLocation( head, ref.path[tail.length] );
if (!user._deps)
setProp( user, '_deps', [] );
user._deps.push( { art, location } );
// Without on-demand resolve, we can simply signal 'undefined "x"'
// instead of 'illegal cycle' in the following case:
// element elem: type of elem.x;
}
if (user && !spec.noDep) {
let location = ref.location; // || combinedLocation( head, path[tail.length] );
if (!user._deps)
setProp( user, '_deps', [] );
user._deps.push( { art, location } );
// Without on-demand resolve, we can simply signal 'undefined "x"'
// instead of 'illegal cycle' in the following case:
// element elem: type of elem.x;
}
setProp( ref, '_artifact', art );
return art;
return setLink( ref, art );
}
function resolveMember( art, ref, memberProp ) {
if (!ref || !art)
return art;
let path = ref.split(':');
for (let id of path) {
let dict = (memberProp)
? art[memberProp]
: (art.elements || art.enum || art.foreignKeys);
art = dict && dict[id];
if (!art)
return undefined;
}
return art;
}
function transformMagicName( name ) {

@@ -226,10 +284,14 @@ // TODO: store magic variable in lower case (nicer for code completion)

// non-existing external using references if `unchecked` is truthy.
function getPathRoot( path, spec, env, extDict ) {
if (!spec.next && !extDict)
extDict = (spec.useDefinitions || ['json','i18n','xml'].includes(env.$frontend))
function getPathRoot( path, spec, env, extDict, msgArt ) {
let head = path[0];
if ('_artifact' in head)
return (head._artifact instanceof Array) ? false : head._artifact;
// console.log(pathName(path), !spec.next && !extDict && (spec.useDefinitions || env.$frontend === 'json' || env))
if (!spec.next && !extDict) {
extDict = (spec.useDefinitions || env.$frontend && env.$frontend !== 'cdl')
? model.definitions
: model.$builtins;
}
// TODO: remove resolveSemanics, just have typeOf for type refs
let nextProp = spec.next || '_block';
let head = path[0];
for (let art = env; art; art = art[nextProp]) {

@@ -242,10 +304,17 @@ let e = art.artifacts || art.$tableAliases || Object.create(null);

if (r) {
if (r instanceof Array) // redefinitions
return undefined;
if (r instanceof Array) { // redefinitions
setLink( head, r );
return false;
}
else if (r.kind === 'block')
return r.name._artifact;
return setLink( head, r.name._artifact );
else if (r.kind === '$parameters') {
if (!head.quoted && path.length > 1)
return setLink( head, r );
}
else if (r.kind !== '$tableAlias' ||
(r.self ? !head.quoted : path.length > 1))
// except $self if quoted, or "real" table aliases (not $self) with path len 1
return r;
// TODO: $projection only if not quoted _and_ length > 1
return setLink( head, r );
}

@@ -255,15 +324,18 @@ }

let r = extDict[head.id];
if (r instanceof Array)
{
// TODO: collect table refs - ok if only one, otherwise ambiguity (better
// msg) - let functionality be provided by function as argument
if (r[0].kind === '$navElement')
signal( `Ambiguous, use table alias"`, head.location );
return undefined;
if (r instanceof Array) {
if (r[0].kind === '$navElement') {
let names = r.filter( e => !e.$duplicate)
.map( e => e.name.alias + '.' + e.name.element );
if (names.length)
message( 'ref-ambiguous', head.location, { id: head.id, names },
'Error', 'Ambiguous $(ID), replace by $(NAMES)' );
}
setLink( head, r );
return false;
}
else if (r)
return r;
return setLink( head, r );
}
if (spec.severity === 'None')
return undefined;
if (spec.noMessage || msgArt === true && extDict === model.definitions)
return setLink( head, null );

@@ -286,18 +358,20 @@ let valid = [];

if (spec.next) {
if (env.$msg)
signalNotFound( error`Element ${msgName( head.id )} has not been found in ${msgName( env.$msg )}`,
head.location, valid, spec.severity );
else
signalNotFound( `No element has been found with name ${msgName( head.id )}`,
// TODO: we could sometimes use "Element ... not been found in ..."
head.location, valid, spec.severity );
if (spec.next) { // value ref
// TODO: if not in query, specify where we search for elements and delete env.$msg
// TODO: also something special if it starts with '$'
if (msgArt)
signalNotFound( 'ref-undefined-element', head.location, valid,
{ art: searchName( msgArt, head.id, 'element' ) } );
else
signalNotFound( 'ref-undefined-var', head.location, valid, { id: head.id },
'Error', 'Element or variable $(ID) has not been found' );
}
else if (['json','i18n','xml'].includes(env.$frontend)) // only searches in definitions
signalNotFound( error`Artifact ${msgName( head.id )} has not been found`,
head.location, [env], spec.severity );
else if (env.$frontend && env.$frontend !== 'cdl')
// IDE can inspect <model>.definitions - provide null for valid
signalNotFound( spec.undefinedDef || 'ref-undefined-def', head.location, null,
{ art: head.id } );
else
signalNotFound( `No artifact has been found with name ${msgName( head.id )}`,
head.location, valid, spec.severity );
return undefined
signalNotFound( spec.undefinedArt || 'ref-undefined-art', head.location, valid,
{ name: head.id } );
return setLink( head, null );
}

@@ -309,45 +383,56 @@

// element item in the path)
function getPathItem( tail, spec, art, head ) {
for (let item of tail) {
if (!item) // incomplete AST
function getPathItem( path, spec ) {
var art;
for (let item of path) {
if (!item) // incomplete AST due to parse error
return undefined;
let env = (head) ? environment(art) : art.elements;
let sub = env && env[item.id];
if (sub) {
if (sub instanceof Array) // redefinitions
return undefined;
art = setProp( item, '_artifact', sub );
continue;
if (item._artifact) { // should be there on first path element
art = item._artifact;
if (art instanceof Array)
return false;
}
if (art.kind === 'context' || art.kind == 'service' || art.kind == 'namespace' ) {
signalNotFound( error`Artifact ${msgName( art.name.absolute + '.' + item.id )} has not been found`,
item.location, [env], spec.severity );
else {
let env = environment(art);
let sub = setLink( item, env && env[item.id] );
if (!sub)
return error( item, env );
else if (sub instanceof Array) // redefinitions
return false;
art = sub;
}
else if (art.self && art.name.query != null) {
signalNotFound( error`Element ${msgName( item.id )} has not been found in the elements of the query`,
item.location, [env], spec.severity );
}
return art;
function error( item, env ) {
if (!spec.next) { // artifact ref
// TODO: better for TYPE OF, FROM e.Assoc (even disallow for other refs)
signalNotFound( spec.undefinedDef || 'ref-undefined-def', item.location, [env],
{ art: searchName( art, item.id ) } );
}
else if (art.name.alias && art.type && art.type._artifact)
{
signalNotFound( error`Element ${msgName( item.id )} has not been found in ${msgName( art.type )}`,
item.location, [env], spec.severity );
else if (art.name.query != null) {
// TODO: probably not extra messageId, but text variant
signalNotFound( 'query-undefined-element', item.location, [env],
{ id: item.id }, 'Error',
'Element $(ID) has not been found in the elements of the query' );
// TODO: 'The current query has no element $(MEMBER)' with name.self
// and 'The sub query $(NAME) has no element $(MEMBER)'
}
else if (art.kind === '$parameters') {
signalNotFound( 'ref-undefined-param', item.location, [env],
{ art: searchName( art._main, item.id, 'param' ) },
'Error', { param: 'Entity $(ART) has no parameter $(MEMBER)' } );
}
else {
let elem = (art.kind === 'element') ? art.name.element + '.' + item.id : item.id;
signalNotFound( error`Element ${msgName( elem )} has not been found in ${msgName( art.name, true )}`,
item.location, [env], spec.severity );
signalNotFound( 'ref-undefined-element', item.location, [env],
{ art: searchName( art, item.id ) } );
}
return undefined;
return null;
}
// TODO: $navElement
// TODO: extra with $parameters if that is supported
return art;
}
function signalNotFound( text, location, valid, severity = 'Error' ) {
if (location._notFound)
function signalNotFound( msgId, location, valid, ...args ) {
if (location.$notFound)
return;
setProp( location, '_notFound', true );
signal( text, location, severity ); // arg, it returns true/false
let err = model.messages[ model.messages.length-1 ];
location.$notFound = true;
let err = message( msgId, location, ...args );
// console.log( Object.keys( Object.assign( Object.create(null), ...valid.reverse() ) ) )

@@ -358,4 +443,5 @@ if (valid && (options.attachValidNames || options.testMode))

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

@@ -372,2 +458,3 @@ }

function defineAnnotations( construct, art, block, priority ) {
// TODO: block should be construct._block
if (!construct.annotationAssignments || !construct.annotationAssignments.length) {

@@ -391,8 +478,6 @@ if (construct === art)

let ref = anno.name;
let name = resolveUncheckedPath( ref, 'annotation', block );
let name = resolveUncheckedPath( ref, 'annotation', { _block: block } );
let annoProp = (anno.name.variant)
? '@' + name + '#' + anno.name.variant.id
: '@' + name;
if(anno.priority === 'i18n')
priority = 'i18n';
flatten( ref.path, annoProp, anno.value || {}, anno.name.variant, anno.name.location );

@@ -412,3 +497,4 @@ }

if (iHaveVariant)
signal('Annotation variant has been already provided', item.name.variant.location);
message( 'anno-duplicate-variant', item.name.variant.location, {}, // TODO: params
'Error', 'Annotation variant has been already provided' );
prop = prop + '#' + item.name.variant.id; // TODO: check for double variants

@@ -454,3 +540,3 @@ }

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

@@ -464,13 +550,22 @@

// The link (_artifact,_finalType,...) usually has the artifact as value.
// Falsy values are:
// - undefined: not computed yet, parse error, no ref
// - null: no valid reference, param:true if that is not allowed
// - false (only complete ref): multiple definitions, rejected
// - 0 (for _finalType only): circular reference
function setLink ( obj, value = null, prop = '_artifact' ) {
Object.defineProperty( obj, prop, { value, configurable: true, writable: true } );
return value;
}
function linkToOrigin( origin, name, parent, prop, location ) {
let elem = {
name: { location, id: origin.name.id }, // more by setMemberParent()
name: { location: location || origin.name.location, id: origin.name.id },
kind: origin.kind,
origin: Object.assign( {}, origin.name ), // TODO: {} should be enough - but tests fail
location
origin: { location },
location: location || origin.location
};
if (origin.name.$inferred)
elem.name.$inferred = origin.name.$inferred;
delete elem.origin.id;
delete elem.origin.quoted;
if (parent)

@@ -521,3 +616,3 @@ setMemberParent( elem, name, parent, prop ); // TODO: redef in template

// Return true if the path navigates along an association whose final type
// Return path step if the path navigates along an association whose final type
// satisfies function `test`; "navigates along" = last path item not considered

@@ -529,3 +624,3 @@ // without truthy optional argument `alsoTestLast`.

if (art && art._finalType && art._finalType.target && test( art._finalType, item ))
return alsoTestLast || item !== ref.path[ ref.path.length-1 ]; // false on last
return (alsoTestLast || item !== ref.path[ ref.path.length-1 ]) && item;
}

@@ -536,2 +631,4 @@ return false;

module.exports = {
dictKinds,
kindProperties,
fns,

@@ -541,2 +638,3 @@ linkToOrigin, setMemberParent,

withAssociation,
combinedLocation };
combinedLocation
};

@@ -61,2 +61,5 @@ const parseXml = require('./xmlParserWithLocations');

annotations.forEach(obj => {
if (obj._selfClosing)
return;
// the value has the form of 'ServiceName.EntityName[/elem1/elem2]'

@@ -146,2 +149,8 @@ let elements = obj._attributes.Target.split('/');

},
Float: val => {
return {
literal: 'decimal',
val: Number(val)
}
},
Int: val => {

@@ -256,6 +265,11 @@ return {

return obj.map(e => handleString(e, false));
return Object.assign(edmxAttrs.String(obj._text), obj._location);
return Object.assign(edmxAttrs.String(obj._text), { location: obj._location });
}
function handleValue(obj, isCollection) {
// this prevents the map execution of not an array, valid only for the collection tag
// the validation throws a specific error for the empty collection
if (isCollection && obj._selfClosing)
return [];
if (isCollection)

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

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

@@ -273,0 +287,0 @@ }

@@ -1,2 +0,2 @@

const edmxDict = require('./vocabularies/Dictionary.json');
const edmxDict = require('./Dictionary.json');
const { CompileMessage } = require('../../base/messages');

@@ -13,2 +13,5 @@

return;
// handle the case when a colletion is empty -> result in empty array
if (obj._name === 'Collection' && obj._selfClosing)
return;
env.expectedType = expectedType;

@@ -38,2 +41,4 @@ if (expectedType.startsWith('Edm.'))

case 'Edm.Double':
checkIfProp('Float');
break;
case 'Edm.Decimal':

@@ -57,6 +62,6 @@ checkIfProp('Decimal');

if (!obj._attributes[prop] && !obj._attributes.Path)
env.messages.push(new CompileMessage(obj._location, `Error in annotation translation: expected: ${env.expectedType}, target: ${env.target}, annotation: ${env.term}`));
env.messages.push(new CompileMessage(obj._location, `Unexpected kind in annotation translation: expected: ${env.expectedType}, target: ${env.target}, annotation: ${env.term}`, 'Warning'));
} else {
if (!obj[prop])
env.messages.push(new CompileMessage(obj._location, `Error in annotation translation: expected: ${env.expectedType}, target: ${env.target}, annotation: ${env.term}`));
env.messages.push(new CompileMessage(obj._location, `Unexpected kind in annotation translation: ${env.expectedType}, target: ${env.target}, annotation: ${env.term}`, 'Warning'));
}

@@ -68,3 +73,3 @@ }

if (!obj.Collection)
env.messages.push(new CompileMessage(obj._location, `Error in annotation translation: expected: ${env.expectedType}, but 'Collection' was not found, target: ${env.target}, annotation: ${env.term}`));
env.messages.push(new CompileMessage(obj._location, `Unexpected kind in annotation translation: expected: ${env.expectedType}, but 'Collection' was not found, target: ${env.target}, annotation: ${env.term}`, 'Warning'));
else {

@@ -91,3 +96,3 @@ env.expectedType = env.expectedType.slice(11, -1);

if (!baseTypeName.includes(env.expectedType)) {
env.messages.push(new CompileMessage(record._location, `Error in annotation translation: found type: ${record._attributes.Type}, but expected: ${env.expectedType}, target: ${env.target}`));
env.messages.push(new CompileMessage(record._location, `Unexpected kind in annotation translation: found type: ${record._attributes.Type}, but expected: ${env.expectedType}, target: ${env.target}`, 'Warning'));
return;

@@ -103,3 +108,3 @@ }

if (prop._attributes && !expectedProperties[prop._attributes.Property])
env.messages.push(new CompileMessage(prop._location, `Error in annotation translation: not existing property: ${prop._attributes.Property} of type ${env.expectedType}, target: ${env.target}`));
env.messages.push(new CompileMessage(prop._location, `Unexpected kind in annotation translation: not existing property: ${prop._attributes.Property} of type ${env.expectedType}, target: ${env.target}`, 'Warning'));
else

@@ -118,3 +123,3 @@ // 3. check the type of the property

} else
env.messages.push(new CompileMessage(record._location, `Error in annotation translation: empty record of type ${env.expectedType}, target: ${env.target}`));
env.messages.push(new CompileMessage(record._location, `Unexpected kind in annotation translation: empty record of type ${env.expectedType}, target: ${env.target}`, 'Warning'));
}

@@ -127,3 +132,3 @@

} else if (obj._attributes && obj._attributes.EnumMember) // when enum is declared, but the type is not an enum
env.messages.push(new CompileMessage(obj._location, `Error in annotation translation: the type ${env.expectedType} is not an enum type, target: ${env.target}`));
env.messages.push(new CompileMessage(obj._location, `Unexpected kind in annotation translation: the type ${env.expectedType} is not an enum type, target: ${env.target}`, 'Warning'));
}

@@ -141,6 +146,6 @@

if (!edmxDict.types[env.expectedType].Members.includes(enumSymbol))
env.messages.push(new CompileMessage(obj._location, `Error in annotation translation: unexisting value ${enumSymbol} in enum type ${env.expectedType}, target: ${env.target}`));
env.messages.push(new CompileMessage(obj._location, `Unexpected kind in annotation translation: unexisting value ${enumSymbol} in enum type ${env.expectedType}, target: ${env.target}`, 'Warning'));
}
if (obj._attributes && !obj._attributes.EnumMember && !obj._attributes.Path)
env.messages.push(new CompileMessage(obj._location, `Error in annotation translation: enum value expected for enum type ${env.expectedType}, target: ${env.target}`));
env.messages.push(new CompileMessage(obj._location, `Unexpected kind in annotation translation: enum value expected for enum type ${env.expectedType}, target: ${env.target}`, 'Warning'));
}

@@ -147,0 +152,0 @@

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

const preprocessAnnotations = require('./preprocessAnnotations.js');
const g_dict = require('./vocabularies/Dictionary.json');
const oDataDictionary = require('./Dictionary.json');
const alerts = require('../../base/alerts');

@@ -14,6 +14,5 @@

* options:
* v
* v - array with two boolean entries, first is for v2, second is for v4
* tntFlavor
* embeddedV2
* suppressMessages --- currently not used
* dictReplacement: for test purposes, replaces the standard oDataDictionary
*/

@@ -29,11 +28,18 @@ function csn2annotationEdm(csn, options=undefined) {

// FIXME: Omitting the record type is theoretically valid but UIs don't like it, so we always enforce writing it
let g_enforceRecordType = true;
let g_annosArray = [];
let g_annosArray = [];
let g_dict = oDataDictionary;
// tests can use a different dictionary
if (options.dictReplacement) {
g_dict = options.dictReplacement;
}
let v = options.v;
const { error, signal } = alerts(csn);
const { warning, signal } = alerts(csn);
// TODO only works for single service
// Note: only works for single service
// Note: we assume that all objects ly flat in the service, i.e. objName always
// looks like <service name, can contain dots>.<id>
// Note: it is NOT safe to assume that the service itself is the first definition in the csn
// -> in general "serviceName" is NOT correctly set during processing of other objects
let serviceName = null;

@@ -46,10 +52,18 @@ for (let objName in csn.definitions) {

// handle the annotations directly tied to the object
handleAnnotations(objName, object);
// handle the annotations of the object's elements
handleElements(objName, object);
// handle the annotations of the object's actions
handleActions(objName, object);
if (object.kind == "action" || object.kind == "function") {
handleAction(objName, object, null);
}
else { // service, entity, anything else?
// handle the annotations directly tied to the object
handleAnnotations(objName, object);
// handle the annotations of the object's elements
handleElements(objName, object);
// handle the annotations of the object's actions
handleBoundActions(objName, object);
}
}
// filter out empty <Annotations...> elements
g_annosArray = g_annosArray.filter(x => x._children.length > 0 || x.kind !== 'Annotations');
let schema = Edm.Schema.create(v, serviceName, serviceName, g_annosArray, false);

@@ -60,2 +74,9 @@ let service = Edm.DataServices.create(v, schema);

// helper to determine the OData version
// tnt is always v2
// TODO: improve option handling and revoke this hack for tnt
function isV2() {
return options.tntFlavor || (v && v[0]);
}
//-------------------------------------------------------------------------------------------------

@@ -68,6 +89,6 @@ //-------------------------------------------------------------------------------------------------

//
// this function is called in the translation code to issue an error message
// this function is called in the translation code to issue a warning message
// messages are reported via the alerts attribute of csn
//
function errorMessage(context, message) {
function warningMessage(context, message) {
let fullMessage = "in annotation translation: " + message;

@@ -81,6 +102,7 @@ if (context) {

}
signal(error`${fullMessage}`);
signal(warning`${fullMessage}`);
}
// there are 4 possible kinds of targets for annotations

@@ -119,3 +141,3 @@ // csn: annotation at entity

// handling annotations at nested elements is not yet supported
// => issue an error, but only if there actually are annotations
// => issue a warning, but only if there actually are annotations
function handleNestedElements(objname, baseElemName, elementsObj) {

@@ -126,3 +148,3 @@ for (let elemName in elementsObj) {

if (Object.keys(element).filter( x => x.substr(0,1) == "@" ).filter(filterKnownVocabularies).length > 0) {
errorMessage(null, "annotations at nested elements are not yet supported, object " + objname + ", element " + baseElemName + "." + elemName);
warningMessage(null, "annotations at nested elements are not yet supported, object " + objname + ", element " + baseElemName + "." + elemName);
}

@@ -137,28 +159,60 @@

// handle the annotations of actions and action parameters
// in: cObjectname : name of the object that holds the actions
// cObject : the object itself
function handleActions(cObjectname, cObject) {
// edm target name is "<serviceName>.EntityContainer/<actionName>" or
// "<serviceName>.EntityContainer/<actionName>/<parameterName>"
// TODO the respective entity names do not appear in the target name
// => in edm all actions are in the same namespace
// => what if two or more entities have actions with same names?
// annotations for actions and functions (and their parameters)
// v2, unbound: Target = <service>.EntityContainer/<action/function>
// v2, bound: Target = <service>.EntityContainer/<entity>_<action/function>
// v4, unbound action: Target = <service>.<action>()
// v4, bound action: Target = <service>.<action>(<service>.<entity>)
// v4, unbound function: Target = <service>.<function>(<1st param type>, <2nd param type>, ...)
// v4, bound function: Target = <service>.<function>(<service>.<entity>, <1st param type>, <2nd param type>, ...)
// service name -> remove last part of the name
// TODO this only works if all objects ly flat in the service
// handle the annotations of cObject's (an entity) bound actions/functions and their parameters
// in: cObjectname : qualified name of the object that holds the actions
// cObject : the object itself
function handleBoundActions(cObjectname, cObject) {
// service name -> remove last part of the object name
// only works if all objects ly flat in the service
let nameParts = cObjectname.split(".")
nameParts.pop();
let entityName = nameParts.pop();
let serviceName = nameParts.join(".");
for (let actionName in cObject.actions) {
let action = cObject.actions[actionName];
let edmTargetName = serviceName + ".EntityContainer/" + actionName;
handleAnnotations(edmTargetName, action);
for (let paramName in cObject.actions[actionName].params) {
let param = cObject.actions[actionName].params[paramName];
edmTargetName = serviceName + ".EntityContainer/" + actionName + "/" + paramName;
handleAnnotations(edmTargetName, param);
for (let n in cObject.actions) {
let action = cObject.actions[n];
let actionName = serviceName + "." + (isV2() ? entityName + '_' : '') + n;
handleAction(actionName, action, cObjectname);
}
}
// handle the annotations of an action and its parameters
// called by handleBoundActions and directly for unbound actions/functions
// in: cActionName : qualified name of the action
// cAction : the action object
// entityNameIfBound : qualified name of entity if bound action/function
function handleAction(cActionName, cAction, entityNameIfBound) {
let actionName = cActionName;
if (isV2()) { // insert before last "."
actionName = actionName.replace(/\.(?=[^.]*$)/, '.EntityContainer/')
}
else { // add parameter type list
actionName += relParList(cAction, entityNameIfBound);
}
handleAnnotations(actionName, cAction);
for (let n in cAction.params) { // handle parameters
let edmTargetName = actionName + "/" + n;
handleAnnotations(edmTargetName, cAction.params[n]);
}
}
function relParList(action, bindingParam) {
// we rely on the order of params in the csn being the correct one
let params = [];
if (bindingParam) params.push(bindingParam);
if (action.kind === 'function') {
for (let n in action.params) {
let p = action.params[n];
let otype = p.type.startsWith('cds.') ? glue.mapCdsToEdmType(p.type, false /*is only called for v4*/) : p.type;
params.push(otype);
}
}
return '(' + params.join(',') + ')';
}

@@ -201,7 +255,8 @@

// figure out where to put the annotations
// addAnnotation() is used to actually add an <Annotation ...> to the
// respective <Annotations ...> element
let addAnnotation = function() {
let appliesTest = null;
let appliesTest = null; // used in closure
let stdName = edmCarrierName;
let altName = null;
let altName = null; // used in closure
if (carrier.kind === 'entity' || carrier.kind === 'view') {

@@ -223,5 +278,5 @@ // if annotated object is an entity, annotation goes to the EntityType,

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

@@ -249,3 +304,3 @@ return function(annotation, appliesTo) {

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

@@ -316,3 +371,3 @@

// they are regrouped here
// context : for error messages
// context : for messages
// return : object that represents the annotation in the result edmx

@@ -331,3 +386,3 @@ function handleTerm(termName, annoValue, context) {

if (p.length>2) {
errorMessage(context, "multiple qualifiers (" + p[1] + "," + p[2] + (p.length>3?',...':'') + ")")
warningMessage(context, "multiple qualifiers (" + p[1] + "," + p[2] + (p.length>3?',...':'') + ")")
}

@@ -342,6 +397,6 @@

else {
errorMessage(context, "unknown term " + termNameWithoutQualifiers);
warningMessage(context, "unknown term " + termNameWithoutQualifiers);
}
handleValue(annoValue, newAnno, termNameWithoutQualifiers, termTypeName, 0, context);
handleValue(annoValue, newAnno, termNameWithoutQualifiers, termTypeName, context);
return newAnno;

@@ -355,13 +410,12 @@ }

if (!expectedType && !isPrimitiveType(expectedTypeName)) {
// TODO: test for this error?
errorMessage(context, "internal error: dictionary inconsistency: type '" + expectedTypeName + "' not found");
warningMessage(context, "internal error: dictionary inconsistency: type '" + expectedTypeName + "' not found");
}
else if (isComplexType(expectedTypeName)) {
errorMessage(context, "found enum value, but expected complex type " + expectedTypeName);
warningMessage(context, "found enum value, but expected complex type " + expectedTypeName);
}
else if (isPrimitiveType(expectedTypeName) || expectedType["$kind"] != "EnumType") {
errorMessage(context, "found enum value, but expected non-enum type " + expectedTypeName);
warningMessage(context, "found enum value, but expected non-enum type " + expectedTypeName);
}
else if (!expectedType["Members"].includes(enumValue)) {
errorMessage(context, "enumeration type " + expectedTypeName + " has no value " + enumValue);
warningMessage(context, "enumeration type " + expectedTypeName + " has no value " + enumValue);
}

@@ -384,3 +438,3 @@ return;

if (!expr) {
errorMessage(context, "empty expression value");
warningMessage(context, "empty expression value");
}

@@ -401,2 +455,7 @@ else {

// expected type is dTypeName
// mappping rule for values:
// if expected type is ... the expression to be generated is ...
// floating point type except Edm.Decimal -> Float
// Edm.Decimal -> Decimal
// integer tpye -> Int
function handleSimpleValue(val, dTypeName, context) {

@@ -409,12 +468,3 @@ // caller already made sure that val is neither object nor array

if (typeof val === 'string') {
if (dTypeName == "Edm.AnnotationPath") {
typeName = "AnnotationPath";
}
else if (dTypeName == "Edm.PropertyPath") {
typeName = "PropertyPath";
}
else if (dTypeName == "Edm.String") {
typeName = "String";
}
else if (dTypeName == "Edm.Boolean") {
if (dTypeName == "Edm.Boolean") {
if (val == "true" || val == "false") {

@@ -424,8 +474,8 @@ typeName = "Bool";

else {
errorMessage(context, "found String, but expected type " + dTypeName);
warningMessage(context, "found String, but expected type " + dTypeName);
}
}
else if (dTypeName == "Edm.Double") {
else if (dTypeName == "Edm.Decimal") {
if (isNaN(val) || isNaN(parseFloat(val))) {
errorMessage(context, "found non-numeric string, but expected type " + dTypeName);
warningMessage(context, "found non-numeric string, but expected type " + dTypeName);
}

@@ -436,11 +486,23 @@ else {

}
else if (dTypeName == "Edm.Double" || dTypeName == "Edm.Single") {
if (isNaN(val) || isNaN(parseFloat(val))) {
warningMessage(context, "found non-numeric string, but expected type " + dTypeName);
}
else {
typeName = "Float";
}
}
else if (isComplexType(dTypeName)) {
errorMessage(context, "found String, but expected complex type " + dTypeName);
warningMessage(context, "found String, but expected complex type " + dTypeName);
}
else if (isEnumType(dTypeName)) {
errorMessage(context, "found String, but expected enum type " + dTypeName);
warningMessage(context, "found String, but expected enum type " + dTypeName);
typeName = "EnumMember";
}
else if (dTypeName && dTypeName.startsWith("Edm.") && dTypeName !== "Edm.PrimitiveType") {
typeName = dTypeName.substring(4);
}
else {
// TODO
//warningMessage(context, "type is not yet handled: found String, expected type: " + dTypeName);
}

@@ -457,3 +519,3 @@ }

else {
errorMessage(context, "found Boolean, but expected type " + dTypeName);
warningMessage(context, "found Boolean, but expected type " + dTypeName);
}

@@ -463,3 +525,3 @@ }

if (isComplexType(dTypeName)) {
errorMessage(context, "found number, but expected complex type " + dTypeName);
warningMessage(context, "found number, but expected complex type " + dTypeName);
}

@@ -470,16 +532,19 @@ else if (dTypeName === 'Edm.String') {

else if (dTypeName == "Edm.PropertyPath") {
errorMessage(context, "found number, but expected " + dTypeName);
warningMessage(context, "found number, but expected " + dTypeName);
}
else if (dTypeName == "Edm.Double") {
else if (dTypeName == "Edm.Boolean") {
warningMessage(context, "found number, but expected type " + dTypeName);
}
else if (dTypeName == "Edm.Decimal") {
typeName = "Decimal";
}
else if (dTypeName == "Edm.Boolean") {
errorMessage(context, "found number, but expected type " + dTypeName);
else if (dTypeName == "Edm.Double") {
typeName = "Float";
}
else {
typeName = Number.isInteger(val) ? 'Int' : 'Decimal';
typeName = Number.isInteger(val) ? 'Int' : 'Float';
}
}
else {
errorMessage(context, "expected simple value, but found value '" + val + "' with type '" + typeof val + "'");
warningMessage(context, "expected simple value, but found value '" + val + "' with type '" + typeof val + "'");
}

@@ -499,3 +564,3 @@

// dTypeName: expected type of cAnnoValue according to dictionary, may be null
function handleValue(cAnnoValue, oTarget, oTermName, dTypeName, depth, context) {
function handleValue(cAnnoValue, oTarget, oTermName, dTypeName, context) {
// value can be: array, expression, enum, pseudo-record, record, simple value

@@ -508,11 +573,9 @@

// if we find an array although we expect an enum, this may be a "flag enum"
// oTarget.EnumMember = generateMultiEnumValue(cAnnoValue, dTypeName, context);
checkMultiEnumValue(cAnnoValue, dTypeName, context);
oTarget.setJSON({ "EnumMember@odata.type" : '#'+dTypeName, EnumMember: generateMultiEnumValue(cAnnoValue, dTypeName, false) });
oTarget.setXml({ "EnumMember": generateMultiEnumValue(cAnnoValue, dTypeName, true) });
}
else
{
oTarget.append(generateCollection(cAnnoValue, oTermName, dTypeName, depth+1, context));
oTarget.append(generateCollection(cAnnoValue, oTermName, dTypeName, context));
}

@@ -522,3 +585,3 @@ }

if (Object.keys(cAnnoValue).length == 0) {
errorMessage(context, "empty record");
warningMessage(context, "empty record");
}

@@ -545,7 +608,7 @@ else if ("=" in cAnnoValue) {

// "pseudo-structure" used for annotating scalar annotations
handleValue(cAnnoValue["$value"], oTarget, oTermName, dTypeName, depth, context);
handleValue(cAnnoValue["$value"], oTarget, oTermName, dTypeName, context);
let k = Object.keys(cAnnoValue).filter( x => x.charAt(0) == "@");
if (!k || k.length == 0) {
errorMessage(context, "pseudo-struct without nested annotation");
warningMessage(context, "pseudo-struct without nested annotation");
}

@@ -563,3 +626,3 @@ for (let nestedAnnoName of k) {

// regular record
oTarget.append(generateRecord(cAnnoValue, oTermName, false, dTypeName, depth+1, context));
oTarget.append(generateRecord(cAnnoValue, oTermName, dTypeName, context));
}

@@ -581,3 +644,3 @@ }

if (!type || type["IsFlags"] != "true") {
errorMessage(context, "enum type '" + dTypeName + "' doesn't allow multiple values");
warningMessage(context, "enum type '" + dTypeName + "' doesn't allow multiple values");
}

@@ -593,3 +656,3 @@

// TODO improve message: but found ...
errorMessage(context, "expected an enum value");
warningMessage(context, "expected an enum value");
}

@@ -600,5 +663,5 @@ context.stack.pop();

function generateMultiEnumValue(cAnnoValue, dTypeName, forXml=true)
function generateMultiEnumValue(cAnnoValue, dTypeName, forXml)
{
// remove all invalid entries (error message has already been issued)
// remove all invalid entries (warnining message has already been issued)
// replace short enum name by the full name

@@ -617,3 +680,3 @@ // concatenate all the enums to a string, separated by spaces

// into the record as attribute "Type"
function generateRecord(obj, termName, inCollection, dictRecordTypeName, depth, context) {
function generateRecord(obj, termName, dictRecordTypeName, context) {
let newRecord = Edm.Record.create(v);

@@ -623,35 +686,32 @@ let actualTypeName = null;

if (dictRecordTypeName && !isComplexType(dictRecordTypeName)) {
errorMessage(context, "found complex type, but expected type '" + dictRecordTypeName + "'");
if (!g_dict.types[dictRecordTypeName] && !isPrimitiveType(dictRecordTypeName) && !isCollection(dictRecordTypeName))
warningMessage(context, "internal error: dictionary inconsistency: type '" + dictRecordTypeName + "' not found");
else
warningMessage(context, "found complex type, but expected type '" + dictRecordTypeName + "'");
return newRecord;
}
if (obj["$Type"]) {
// type is explicitly specified
if (obj["$Type"]) { // type is explicitly specified
actualTypeName = obj["$Type"];
if (!g_dict.types[actualTypeName]) {
// this type doesn't exist
errorMessage(context, "explicitly specified type '" + actualTypeName + "' not found in vocabulary");
newRecord.Type = actualTypeName;
warningMessage(context, "explicitly specified type '" + actualTypeName + "' not found in vocabulary");
}
else if (dictRecordTypeName && !isDerivedFrom(actualTypeName, dictRecordTypeName)) {
// this type doesn't fit the expected one
errorMessage(context, "explicitly specified type '" + actualTypeName
warningMessage(context, "explicitly specified type '" + actualTypeName
+ "' is not derived from expected type '" + dictRecordTypeName + "'");
actualTypeName = dictRecordTypeName;
newRecord.Type = actualTypeName;
}
else if (isAbstractType(actualTypeName)) {
// this type is abstract
errorMessage(context, "explicitly specified type '" + actualTypeName + "' is abstract, specify a concrete type");
warningMessage(context, "explicitly specified type '" + actualTypeName + "' is abstract, specify a concrete type");
actualTypeName = dictRecordTypeName;
newRecord.Type = actualTypeName;
}
else {
// ok
if (actualTypeName != dictRecordTypeName || g_enforceRecordType) {
newRecord.Type = actualTypeName;
}
}
newRecord.Type = actualTypeName;
}
else if (dictRecordTypeName) {
else if (dictRecordTypeName) { // there is an expected type name according to dictionary
// convenience for common situation:

@@ -667,18 +727,9 @@ // if DataFieldAbstract is expected and no explicit type is provided, automatically choose DataField

if (isAbstractType(actualTypeName)) {
errorMessage(context, "type '" + dictRecordTypeName + "' is abstract, use '$Type' to specify a concrete type");
newRecord.Type = actualTypeName;
warningMessage(context, "type '" + dictRecordTypeName + "' is abstract, use '$Type' to specify a concrete type");
}
if (!g_dict.types[actualTypeName]) {
// TODO: test?
errorMessage(context, "internal error: dictionary inconsistency, type '" + actualTypeName + "' not found");
}
// do not set Type attribute unless explicitly requested
if (g_enforceRecordType || (options.tntFlavor && !options.tntFlavor.skipAnnosEnforceRecordType)) {
newRecord.Type = actualTypeName;
}
newRecord.Type = actualTypeName;
}
else if (depth == 1) {
newRecord.Type = termName + "Type";
else {
// no expected type set -> do not set newRecord.Type
}

@@ -704,3 +755,3 @@

if (!dictPropertyTypeName){
errorMessage(context, "record type '" + actualTypeName + "' doesn't have a property '" + i + "'");
warningMessage(context, "record type '" + actualTypeName + "' doesn't have a property '" + i + "'");
}

@@ -710,3 +761,3 @@ }

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

@@ -725,3 +776,3 @@ }

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

@@ -736,3 +787,3 @@

else {
errorMessage(context, "found collection value, but expected non-collection type " + dTypeName);
warningMessage(context, "found collection value, but expected non-collection type " + dTypeName);
}

@@ -746,3 +797,3 @@ }

if (Array.isArray(value)) {
errorMessage(context, "nested collections are not supported");
warningMessage(context, "nested collections are not supported");
}

@@ -756,7 +807,6 @@ else if (value && typeof value === 'object') {

else if (value["#"]) {
// TODO test
errorMessage(context, "collections of enums are not yet supported");
warningMessage(context, "enum inside collection is not yet supported");
}
else {
let rec = generateRecord(value, termName, true, innerTypeName, depth+1, context);
let rec = generateRecord(value, termName, innerTypeName, context);
newCollection.append(rec);

@@ -777,3 +827,3 @@ }

function handleEdmJson(obj, context)
function handleEdmJson(obj, context)
{

@@ -784,3 +834,3 @@ let specialProperties = [ '$Apply', '$LabeledElement' ];

if(subset.length > 1) { // doesn't work for three or more...
errorMessage(context, "edmJson code contains more than one special property: " + subset);
warningMessage(context, "edmJson code contains more than one special property: " + subset);
return null;

@@ -794,3 +844,3 @@ }

}
errorMessage(context, "edmJson code contains no special property out of: " + specialProperties);
warningMessage(context, "edmJson code contains no special property out of: " + specialProperties);
return null;

@@ -812,2 +862,5 @@ }

}
else {
warningMessage(context, "unexpected element without $: " + p);
}
}

@@ -823,3 +876,3 @@ else { // we are either $Apply or $LabeledElement

else if (Array.isArray(a)) {
errorMessage(context, "verbatim code contains nested array");
warningMessage(context, "verbatim code contains nested array");
}

@@ -904,2 +957,6 @@ else {

function isCollection(typeName) {
return typeName.match(/^Collection\((.+)\)/) !== null;
}
function isEnumType(dTypeName) {

@@ -906,0 +963,0 @@ let type = g_dict.types[dTypeName];

@@ -13,8 +13,10 @@ 'use strict';

*
* This module never produces errors. In case of "unexpected" situations we issue a warning and
* try to proceed with the processing as good as possible.
*
*/
function preprocessAnnotations(csn, options) {
const { error, signal } = alerts(csn);
const { warning, signal } = alerts(csn);
let fkSeparator = (options && options.tntFlavor && !options.tntFlavor.skipGeneratedFKsWithout_) ? '' : '_';
if (options && options.tntFlavor) {

@@ -30,5 +32,18 @@ addCommonTextAndCommonValueListToAssociations();

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

@@ -40,16 +55,17 @@ // assoc.target can be the name of the target or the object itself

let keyNames = Object.keys(target.elements).filter(x => target.elements[x].key);
if (keyNames.length == 0)
throw "target " + targetName + " has no key";
if (keyNames.length > 1)
signal(error`in annotation preprocessing: target ${targetName} has multiple key elements`);
if (keyNames.length == 0) {
keyNames.push['MISSING'];
signal(warning`in annotation preprocessing: target ${targetName} has no key`);
}
else if (keyNames.length > 1)
signal(warning`in annotation preprocessing: target ${targetName} has multiple key elements`);
// let fkElement = target.elements[keyNames[0]];
// if(glue.isAssociationOrComposition(fkElement))
// // FIXME: throw Error, not string - or do you meant to add an Error to messages?
// throw "target " + targetName + " has assoc as key";
// TODO: what happens if key of target is itself a managed association?
return keyNames[0];
}
// ----------------------------------------------------------------------------------------------
// main annotation processors
// ----------------------------------------------------------------------------------------------
function addCommonTextAndCommonValueListToAssociations() {

@@ -68,3 +84,3 @@

if(!((object.kind == 'entity' || object.kind == 'view') &&
(elem.type == "cds.Association" || elem.type == "Association") && elem.on == undefined))
(elem.type == "cds.Association" || elem.type == "Association") && elem.on == undefined && elem.onCond == undefined))
return;

@@ -142,2 +158,50 @@

/*
Forward all annotations from the association element to its generated foreign key elements
and remove the annotations from the association afterwards (move)
Input: OData preprocessed CSN with entity definitions
*/
function moveAnnotationsFromAssocToForeignKeys() {
// managed association
// (not at compositions, because we think they never are managed ... ???)
glue.foreach(csn.definitions, obj => (obj.kind == 'entity' || obj.kind == 'view'), entity => {
glue.foreach(entity.elements, e => glue.isManagedAssociation(e), (element, elementName) =>
{
// copy annotations from assoc to all generated foreign key fields
// FIXME: This should actually be done always, regardless of TNT (the ODATA preprocesor has already
// done it for all existing annotations, but all creation/modification magic happening during this
// post-processing needs to go to the foreign key fields as well). Alternatively, one could make
// sure that all magic happening here is directly applied to foreign key fields, too.
if (element.foreignKeys) {
glue.forAll(element.foreignKeys, fk => {
addAnnos(fk.generatedFieldName)
});
}
else if (element.keys) {
for (let x of element.keys) {
addAnnos(elementName + fkSeparator + x.ref[0])
}
}
function addAnnos(fk_generatedFieldName) {
glue.forAll(element, (attr, attrName) => {
if(attrName[0] == '@')
entity.elements[fk_generatedFieldName][attrName] = attr;
});
}
// remove annotations from assoc (separated from copy because there might be multiple foreign keys)
if (!(options && options.tntFlavor && options.tntFlavor.skipAnnosRemoveManagedAssociationAnnos)) {
for (let a in element) {
if (a[0] == "@") {
delete element[a];
}
}
}
});
});
}
// resolve shortcuts and do some TNT specific modifications

@@ -164,8 +228,16 @@ function resolveShortcutsAndTNTspecificModifications() {

let path = ae["="];
if (carrier.kind == 'entity') {
let element = carrier.elements[path];
// FIXME: Deal with multi-step paths properly
if (element && glue.isAssociation(element) && !element.onCond) {
ae["="] = path + fkSeparator + getKeyOfTargetOfManagedAssoc(element);
let steps = path.split('.');
let ent = carrier;
for (let i in steps) {
if (!ent || ent.kind != 'entity') return;
let el = ent.elements[steps[i]];
if (el && glue.isAssociation(el)) {
if (i < steps.length-1) {
ent = getTargetOfAssoc(el);
}
else { //last step
if (!el.onCond && !el.on) { // only for managed
ae["="] += fkSeparator + getKeyOfTargetOfManagedAssoc(el);
}
}
}

@@ -182,2 +254,28 @@ }

//for warning messages
let ctx = "target: " + art + "/" + carrierName;
// Always - draft annotations, value is action name
// - v2: prefix with entity name
// - prefix with service name
if ((carrier.kind === 'entity' || carrier.kind === 'view') &&
(aNameWithoutQualifier == "@Common.DraftRoot.PreparationAction" ||
aNameWithoutQualifier == "@Common.DraftRoot.ActivationAction" ||
aNameWithoutQualifier == "@Common.DraftRoot.EditAction" ||
aNameWithoutQualifier == "@Common.DraftNode.PreparationAction")
) {
let value = carrier[aName];
// prefix with service name, if not already done
if (value == "draftPrepare" || value == "draftActivate" || value == "draftEdit") {
let serviceName = carrierName.replace(/.[^.]+$/, '');
value = carrier[aName] = serviceName + '.' + value;
}
// for v2: function imports live inside EntityContainer -> path needs to contain "EntityContainer/"
// we decided to prefix names of bound action/functions with entity name -> needs to be reflected in path, too
if (isV2()) {
let entityNameShort = carrierName.split('.').pop();
carrier[aName] = value.replace(/(draft(Prepare|Activate|Edit))$/, (match, p1) => 'EntityContainer/' + entityNameShort + '_' + p1)
}
}
// Always - FixedValueListShortcut

@@ -192,5 +290,2 @@ // expand shortcut form of ValueList annotation

//for error messages
let ctx = "target: " + art + "/" + carrierName;
// check on "type"? e.g. if present, it must be #fixed ... ?

@@ -201,3 +296,3 @@

if (entityName["="]) {
signal(error`in annotation preprocessing/value help shortcut: 'entity' must be a string, ${ctx}`);
signal(warning`in annotation preprocessing/value help shortcut: 'entity' must be a string, ${ctx}`);
}

@@ -208,3 +303,3 @@ let ename = entityName["="] || entityName;

if (!vlEntity) {
signal(error`in annotation preprocessing/value help shortcut: entity ${ename} does not exist, ${ctx}`);
signal(warning`in annotation preprocessing/value help shortcut: entity ${ename} does not exist, ${ctx}`);
throw "leave";

@@ -228,11 +323,11 @@ }

// valueListProp: the (single) key field of the value list entity
// if no key or multiple keys -> error
// if no key or multiple keys -> warning
let valueListProp = null;
let keys = Object.keys(vlEntity.elements).filter( x => vlEntity.elements[x].key );
if (keys.length == 0) {
signal(error`in annotation preprocessing/value help shortcut: entity ${ename} has no key, ${ctx}`);
signal(warning`in annotation preprocessing/value help shortcut: entity ${ename} has no key, ${ctx}`);
throw "leave";
}
else if (keys.length > 1)
signal(error`in annotation preprocessing/value help shortcut: entity ${ename} has more than one key, ${ctx}`);
signal(warning`in annotation preprocessing/value help shortcut: entity ${ename} has more than one key, ${ctx}`);
valueListProp = keys[0];

@@ -303,3 +398,3 @@

catch (e) {
// avoid subsequent errors
// avoid subsequent warnings
delete carrier["@Common.ValueList.entity"];

@@ -310,21 +405,24 @@ delete carrier["@Common.ValueList.type"];

// TNT - TextArrangementReordering
// convert TextArrangement annotation that is on same level as
// Text annotation into a nested annotation
if ((aNameWithoutQualifier == "@UI.TextArrangement" ||
aNameWithoutQualifier == "@Common.TextArrangement")
&& (options && options.tntFlavor && !options.tntFlavor.skipAnnosTextArrangementReordering)) {
// can only occur if there is a @Common.Text annotation at the
// same target
// Always - TextArrangementReordering
// convert @Common.TextArrangement annotation that is on same level as Text annotation into a nested annotation
// TnT only: also accept @UI.TextArrangement
let tntTextArrangement = options && options.tntFlavor && !options.tntFlavor.skipAnnosTextArrangementReordering;
if (aNameWithoutQualifier == "@Common.TextArrangement" ||
(tntTextArrangement && aNameWithoutQualifier == "@UI.TextArrangement")) {
let value = carrier[aName];
let textAnno = carrier["@Common.Text"];
if (textAnno == undefined) throw "TextArrangement without Text"
//change the scalar anno into a "pseudo-structured" one
// can only occur if there is a @Common.Text annotation at the same target
if (!textAnno) {
signal(warning`in annotation preprocessing: TextArrangement shortcut without Text annotation, ${ctx}`);
}
// TNT: this fixes a bug in the TNT model where a string is
// given instead of an enum
let value = carrier[aName];
if (value === "TextFirst") {
value = { "#": value };
// TNT: this fixes a bug in the TNT model where a string is given instead of an enum
if (tntTextArrangement) {
if (value === "TextFirst") {
value = { "#": value };
}
}
//change the scalar anno into a "pseudo-structured" one
// TODO should be flattened, but then alphabetical order is destroyed
let newTextAnno = { "$value": textAnno, "@UI.TextArrangement": value };

@@ -337,9 +435,8 @@ carrier["@Common.Text"] = newTextAnno;

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

@@ -352,4 +449,3 @@ }

if (aNameWithoutQualifier == "@UI.SelectionFields") {
for (let i in a) {
let ae = a[i];
for (let ae of a) {
if ("=" in ae) {

@@ -365,37 +461,3 @@ replaceManagedAssocByFK(ae);

/*
Forward all annotations from the association element to its generated foreign key elements
and remove the annotations from the association afterwards (move)
Input: OData preprocessed CSN with entity definitions
*/
function moveAnnotationsFromAssocToForeignKeys() {
// managed association
// (not at compositions, because we think they never are managed ... ???)
glue.foreach(csn.definitions, obj => (obj.kind == 'entity' || obj.kind == 'view'), entity => {
glue.foreach(entity.elements, e => glue.isManagedAssociation(e), element =>
{
// copy annotations from assoc to all generated foreign key fields
// FIXME: This should actually be done always, regardless of TNT (the ODATA preprocesor has already
// done it for all existing annotations, but all creation/modification magic happening during this
// post-processing needs to go to the foreign key fields as well). Alternatively, one could make
// sure that all magic happening here is directly applied to foreign key fields, too.
glue.forAll(element.foreignKeys, fk => {
glue.forAll(element, (attr, attrName) => {
if(attrName[0] == '@')
entity.elements[fk.generatedFieldName][attrName] = attr;
});
});
// remove annotations from assoc (separated from copy because there might be multiple foreign keys)
if (!(options && options.tntFlavor && options.tntFlavor.skipAnnosRemoveManagedAssociationAnnos)) {
for (let a in element) {
if (a[0] == "@") {
delete element[a];
}
}
}
});
});
}
}

@@ -402,0 +464,0 @@

@@ -37,2 +37,3 @@ 'use strict';

_parent: { <root> },
_selfClosing: false,
foo: [

@@ -44,2 +45,3 @@ {

_parent: { <root> },
_selfClosing: false,
bar: [

@@ -51,2 +53,3 @@ {

_parent: { <foo> },
_selfClosing: true,
},

@@ -58,2 +61,3 @@ {

_parent: { <foo> },
_selfClosing: true,
}

@@ -66,2 +70,3 @@ ]

_parent: { <root> },
_selfClosing: false,
bar: [

@@ -73,2 +78,3 @@ {

_parent: { <foo> },
_selfClosing: false,
baz: [

@@ -80,2 +86,3 @@ {

_parent: { <bar> },
_selfClosing: false,
_text: 'Some text'

@@ -89,2 +96,3 @@ }

_parent: { <bar> },
_selfClosing: false,
_text: 'Some other text'

@@ -144,2 +152,4 @@ }

Object.defineProperty(currentNode, '_selfClosing', { value: node.isSelfClosing, enumerable: false });
if (!parser.tag.isSelfClosing) {

@@ -146,0 +156,0 @@ currentParent = currentNode;

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

{
artifact[mapping] = value || artifact[prop];
artifact[mapping] = value || artifact[prop]['='] || artifact[prop];
}

@@ -76,2 +76,5 @@

dict['@PDM.xxx8'] = [ '@sap.updatable' ];
// respect flattened anntotation $value
Object.keys(dict).forEach(k => dict[k+'.$value'] = dict[k]);
*/

@@ -118,2 +121,4 @@ return dict;

// respect flattened anntotation $value
Object.keys(dict).forEach(k => dict[k+'.$value'] = dict[k]);
return dict;

@@ -147,2 +152,5 @@ }

dict['@PDM.xxx8'] = [ '@sap.updatable' ];
// respect flattened anntotation $value
Object.keys(dict).forEach(k => dict[k+'.$value'] = dict[k]);
*/

@@ -215,2 +223,5 @@ return dict;

// respect flattened anntotation $value
Object.keys(dict).forEach(k => dict[k+'.$value'] = dict[k]);
return dict;

@@ -217,0 +228,0 @@ }

@@ -69,10 +69,7 @@ 'use strict';

V4 containment: _containerEntity is set and not equal with the artifact name
OR
artifact has '@cds.odata.NoEntitySet' annotation set to true
Required for Draft mode to disable EntitySet for DraftAdminData
*/
glue.foreach(model.definitions,
a => glue.isEntityOrView(a) && !a.abstract,
(a,n) => createEntityTypeAndSet(a, n, undefined,
!((glue.isContainee(a,n) && options.isV4()) || a['@cds.odata.NoEntitySet'])
(a,n) => createEntityTypeAndSet(a, n,
!( options.isV4() && (glue.isContainee(a,n)) )
)

@@ -117,3 +114,3 @@ );

function createEntityTypeAndSet(entityCsn, entityName, setName=undefined, createEntitySet=true)
function createEntityTypeAndSet(entityCsn, entityName, createEntitySet=true)
{

@@ -124,3 +121,3 @@ // EntityType attributes are: Name, BaseType, Abstract, OpenType, HasStream

let EntityTypeName = entityCsn.name.replace(namespace, '');
let EntitySetName = setName || EntityTypeName;
let EntitySetName = (entityCsn.setName || entityCsn.name).replace(namespace, '');
let fqEntityTypeName = fullQualified(entityName);

@@ -179,6 +176,7 @@

}
else // unbound
else // unbound => produce Action/FunctionImport
{
let actionImport = iAmAnAction ? Edm.ActionImport.create(v, { Name: actionName, Action : fullQualified(actionName) })
: Edm.FunctionImport.create(v, { Name: actionName, Function : fullQualified(actionName) });
let actionImport = iAmAnAction
? Edm.ActionImport.create(v, { Name: actionName, Action : fullQualified(actionName) })
: Edm.FunctionImport.create(v, { Name: actionName, Function : fullQualified(actionName) });

@@ -203,7 +201,10 @@ let rt = actionCsn.returns && (actionCsn.returns.type || actionCsn.returns.items.type);

// return type if any
if(actionCsn.returns)
{
actionNode._rtNode = Edm.ReturnType.create(v, actionCsn.returns, fullQualified);
if(actionCsn.returns) {
actionNode._returnType = Edm.ReturnType.create(v, actionCsn.returns, fullQualified);
// if binding type matches return type add attribute EntitySetPath
if(entityCsn && fullQualified(entityCsn.name) === actionNode._returnType._type) {
actionNode.EntitySetPath = 'in';
}
}
Schema.append(actionNode);
Schema.addAction(actionNode);
}

@@ -250,6 +251,7 @@

// Make bound function names always unique as per Ralf's recommendation
functionImport.setXml( {'sap:action-for': fullQualified(entityCsn.name) } );
functionImport.Name = entityCsn.name.replace(namespace, '') + '_' + functionImport.Name;
functionImport.setXml( {'sap:action-for': fullQualified(entityCsn.name) } );
// Binding Parameter: Primary Keys at first position in sequence, this is decisive!
// V2 XML: Nullable=false is set because we reuse the primary key property for the parameter
glue.foreach(entityCsn.elements,

@@ -269,2 +271,5 @@ elementCsn => elementCsn.key && !glue.isAssociationOrComposition(elementCsn),

// then append all other parameters
// V2 XML: Parameters that are not explicitly marked as Nullable or NotNullable in the CSN must become Nullable=true
// V2 XML spec does only mention default Nullable=true for Properties not for Parameters so omitting Nullable=true let
// the client assume that Nullable is false.... Correct Nullable Handling is done inside Parameter.create()
glue.forAll(actionCsn.params, (parameterCsn, parameterName) => {

@@ -442,4 +447,6 @@ functionImport.append(Edm.Parameter.create(v, { Name: parameterName }, parameterCsn, 'In' ));

let navPropFromRole = fromRole;
let navPropToRole = toRole;
// add V2 attributes to navigationProperty
navigationProperty.Relationship = fullQualified(assocName);
navigationProperty.FromRole = fromRole;
navigationProperty.ToRole = toRole;

@@ -480,8 +487,2 @@ // remove V4 attributes

}
// add V2 attributes to navigationProperty
navigationProperty.Relationship = fullQualified(assocName);
navigationProperty.FromRole = navPropFromRole;
navigationProperty.ToRole = navPropToRole;
if(reuseAssoc)

@@ -499,3 +500,3 @@ return;

association.append(Edm.ReferentialConstraint.createV2(v,
navigationProperty.FromRole, navigationProperty.ToRole, constraints.constraints));
fromRole, toRole, constraints.constraints));

@@ -517,10 +518,6 @@ Schema.append(association);

}
function createAnnotations(edm)
{
let annoEdm;
if(options && options.tntFlavor)
{
options.suppressMessages = true;
}
annoEdm = translate.csn2annotationEdm(model, options);

@@ -527,0 +524,0 @@

@@ -209,3 +209,3 @@ 'use strict'

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

@@ -227,2 +227,11 @@

// hold actions and functions in V4
addAction(action)
{
if(this._actions[action.Name])
this._actions[action.Name].push(action);
else
this._actions[action.Name] = [action];
}
setAnnotations(annotations)

@@ -240,2 +249,6 @@ {

xml += super.innerXML(indent);
glue.forAll(this._actions, actionArray => {
actionArray.forEach(action => {
xml += action.toXML(indent, what) + '\n'; });
});
}

@@ -258,3 +271,9 @@ if(what=='annotations' || what=='all')

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

@@ -527,9 +546,8 @@

overloaded XML and JSON rendering of parameters and
return type
The non-enumberable properties _paramNodes and _rtNode
store the parmater list and the eventually existing
ReturnType node in V4. In V2 the return type is a
direct attribute called ReturnType to the FunctionImport.
See comment on class FunctionImport
return type. Parameters are _children.
_returnType holds the eventually existing ReturnType in V4.
In V2 the return type is a direct attribute called ReturnType
to the FunctionImport. See comment in class FunctionImport.
*/
class ActionFunctionBase extends Node

@@ -541,3 +559,3 @@ {

let node = super.create(v, details);
node.set( { _paramNodes: [], _rtNode: undefined });
node.set( { _returnType: undefined });
return node;

@@ -549,15 +567,7 @@ }

let xml = super.innerXML(indent);
if(this._rtNode != undefined)
xml += this._rtNode.toXML(indent) + '\n';
if(this._returnType != undefined)
xml += this._returnType.toXML(indent) + '\n';
return xml
}
// virtual
toJSON()
{
let json = [ super.toJSON() ];
return json;
}
toJSONchildren(json)

@@ -569,5 +579,5 @@ {

json['$Parameter'] = json_parameters;
if(this._rtNode)
if(this._returnType)
{
json['$ReturnType'] = this._rtNode.toJSON();
json['$ReturnType'] = this._returnType.toJSON();
}

@@ -590,3 +600,3 @@ return json;

*/
class FunctionImport extends ActionFunctionBase {}
class FunctionImport extends Node {} //ActionFunctionBase {}
class ActionImport extends Node {}

@@ -771,3 +781,3 @@

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

@@ -902,4 +912,12 @@ return node

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

@@ -927,3 +945,7 @@ }

let navProp = super.create(v, attributes, csn);
navProp.set( { _type: attributes.Type, _isCollection: glue.isToMany(csn), _referentialConstraints: navProp.getReferentialConstraints(), } );
navProp.set( {
_type: attributes.Type,
_isCollection: glue.isToMany(csn),
_referentialConstraints: navProp.getReferentialConstraints(),
_targetCsn: csn.target } );
if (navProp.v4) {

@@ -933,2 +955,4 @@ // either csn has multiplicity or we have to use the multiplicity of the backlink

navProp.Type = `Collection(${attributes.Type})`
// attribute Nullable is not allowed in combination with Collection (see Spec)
delete navProp.Nullable;
}

@@ -987,2 +1011,6 @@

json['$Type'] = this._type;
// attribute Nullable is not allowed in combination with Collection (see Spec)
if(json['$Collection'])
delete json['$Nullable'];
return json;

@@ -1018,44 +1046,45 @@ }

{
let result = { multiplicity: ['*', '0..1' ], constraints: Object.create(null) };
let result = { multiplicity: [ ], constraints: Object.create(null), selfs: [] };
assocCsn = assocCsn || this._csn;
let prefix = assocCsn.name + '.';
// If this association points to a redirected Parameter EntityType, do not calculate any constraints,
// continue with multiplicity
if(!assocCsn.target.isParamEntity)
if(assocCsn.on)
{
if(assocCsn.onCond)
{
// fill constraint array with [prop, depProp]
getExpressionArguments(assocCsn.onCond);
// fill constraint array with [prop, depProp]
getExpressionArguments(assocCsn.on);
// if self assoc, refill constraints from target assoc
if(result.constraints[NavigationProperty.DOLLAR_SELF])
{
let originAssocCsn = assocCsn.target.elements[result.constraints[NavigationProperty.DOLLAR_SELF]];
// for all $self conditions, fill constraints of partner (if any)
let isBacklink = result.selfs.length == 1;
if(glue.isAssociationOrComposition(originAssocCsn))
{
// mark this association as backlink to surpress edm:Association generation in V2 mode
result._originAssocCsn = originAssocCsn;
// remove $SELF
delete result.constraints[NavigationProperty.DOLLAR_SELF];
// remove all other non key target elements
glue.foreach(result.constraints, c => !assocCsn.target.elements[c[1]].key,
(c, cn) => { delete result.constraints[cn]; } );
// if the assoc is marked as primary key, add all its foreign keys as constraint
// as they are primary keys of the other entity as well
if(originAssocCsn.key)
{
glue.forAll(originAssocCsn.foreignKeys, fk => {
let c = [fk.path, fk.generatedFieldName];
/* example for originalTarget:
entity E (with parameters) {
... keys and all the stuff ...
toE: association to E;
back: association to E on back.toE = $self
}
toE target 'E' is redirected to 'EParameters' (must be as the new parameter list is required)
back target 'E' is also redirected to 'EParameters' (otherwise backlink would fail)
ON Condition back.toE => parter=toE cannot be resolved in EParameters, originalTarget 'E' is
required for that
*/
result.selfs.forEach(partner => {
let originAssocCsn = assocCsn.target.elements[partner] || assocCsn.originalTarget.elements[partner];
if(glue.isAssociationOrComposition(originAssocCsn)) {
// if the assoc is marked as primary key, add all its foreign keys as constraint
// as they are primary keys of the other entity as well
if(!assocCsn.target.isParamEntity && originAssocCsn.key) {
if(originAssocCsn.keys) {
for(let fk of originAssocCsn.keys) {
let c = [ fk.ref[0], fk.$generatedFieldName ];
result.constraints[c] = c;
});
}
}
}
// Side-Effect: Set the Partner attribute with the name of the opposite
// Mark this association as backlink if $self appears exactly once
// to surpress edm:Association generation in V2 mode
if(isBacklink) {
result._originAssocCsn = originAssocCsn;
// V4 Set the Partner attribute with the name of the opposite
// association to the two corresponding csn's and to this NavProp

@@ -1066,3 +1095,2 @@ // (but only if originAssoc is navigable (undefined !== false) still evaluates to true)

this.Partner = assocCsn.$Partner = originAssocCsn.name;
// if the other NavProp has been created already, set NavProp.Partner

@@ -1075,36 +1103,46 @@ if(originAssocCsn._NavigationProperty)

}
else
{
/*
entity E {
key id : Integer;
toMe: associaton to E on toMe.id = $self; };
*/
throw "Backlink association element is not an association or composition: " + originAssocCsn.name;
}
}
else // ordinary ON condition
{
// remove all non key target elements
glue.foreach(result.constraints, c => !assocCsn.target.elements[c[1]] || !assocCsn.target.elements[c[1]].key,
(c, cn) => { delete result.constraints[cn]; } );
else {
/*
entity E {
key id : Integer;
toMe: associaton to E on toMe.id = $self; };
*/
throw "Backlink association element is not an association or composition: " + originAssocCsn.name;
}
});
if(!assocCsn.target.isParamEntity) {
// remove all arget elements that are not key
glue.foreach(result.constraints, c => !(assocCsn.target.elements[c[1]] && assocCsn.target.elements[c[1]].key),
(c, cn) => { delete result.constraints[cn]; } );
}
// this is a managed association
else
{
// If FK is key in target => constraint
// Don't consider primary key associations (fks become keys on the source entity) as
// this would impose a constraint against the target.
// FIXME: If path is something structured, perform a path resolution (or use augmented CSN)
glue.foreach(assocCsn.foreignKeys,
fk => assocCsn.target.elements[fk.path].key,
fk => {
let c = [fk.generatedFieldName, fk.path];
}
// this is a managed association
else
{
// If FK is key in target => constraint
// Don't consider primary key associations (fks become keys on the source entity) as
// this would impose a constraint against the target.
// FIXME: If path is something structured, perform a path resolution (or use augmented CSN)
if(!assocCsn.target.isParamEntity && assocCsn.keys) {
for(let fk of assocCsn.keys) {
if(assocCsn.target.elements[fk.ref[0]].key)
{
let c = [ fk.$generatedFieldName, fk.ref[0] ];
result.constraints[c] = c;
}
);
}
}
}
// If this association points to a redirected Parameter EntityType, do not calculate any constraints,
// continue with multiplicity
if(assocCsn.target.isParamEntity)
{
result.constraints = Object.create(null);
}
determineMultiplicity(result._originAssocCsn);

@@ -1116,45 +1154,53 @@ return result;

{
if(Array.isArray(expr)) // expression in parentheses
expr = expr[0];
if(expr.op == '=')
let allowedTokens = [ '=', 'and', '(', ')' ];
if(expr && Array.isArray(expr))
// if some returns true, this term is not usable as a constraint term
if(!expr.some(isNotAConstraintTerm))
expr.map(fillConstraints)
// return true if token is not one of '=', 'and', '(', ')' or object
function isNotAConstraintTerm(tok)
{
let lhs = expr.args[0]['='];
let rhs = expr.args[1]['='];
if(lhs && rhs)
if(tok.xpr)
return tok.xpr.some(isNotAConstraintTerm);
if(Array.isArray(tok))
return tok.some(isNotAConstraintTerm);
return !(typeof tok === 'object' && tok !== null || allowedTokens.includes(tok));
}
function fillConstraints(arg, pos)
{
if(arg.xpr)
arg.xpr.map(fillConstraints);
else if(pos > 0 && pos < expr.length)
{
// if exactly one operand starts with the prefix then this is potentially a constraint
if((lhs.startsWith(prefix) &&
!rhs.startsWith(prefix)) ||
(!lhs.startsWith(prefix) &&
rhs.startsWith(prefix)))
let lhs = expr[pos-1];
let rhs = expr[pos+1];
if(arg === '=' && lhs.ref && rhs.ref)
{
// order is always [ property, referencedProperty ]
//backlink [ self, assocName ]
let c;
if(lhs.startsWith(prefix))
c = [ rhs, lhs.replace(prefix, '') ];
else
c = [ lhs, rhs.replace(prefix, '') ];
lhs = lhs.ref;
rhs = rhs.ref;
// if exactly one operand starts with the prefix then this is potentially a constraint
if((lhs[0] === assocCsn.name && rhs[0] !== assocCsn.name) ||
(lhs[0] !== assocCsn.name && rhs[0] === assocCsn.name))
{
// order is always [ property, referencedProperty ]
//backlink [ self, assocName ]
let c;
if(lhs[0] === assocCsn.name)
c = [ rhs[0], lhs[1] ];
else
c = [ lhs[0], rhs[1] ];
// do we have a $self or optionally a 'self' id?
// if so, store partner with key $self, multiple $self's should be illegal
if(c[0] === NavigationProperty.DOLLAR_SELF || (NavigationProperty.OLDSTYLE_SELF && c[0] === NavigationProperty.OLDSTYLE_SELF))
{
result.constraints[NavigationProperty.DOLLAR_SELF] = c[1];
// do we have a $self or optionally a 'self' id?
// if so, store partner in selfs array
if(c[0] === NavigationProperty.DOLLAR_SELF ||
(NavigationProperty.OLDSTYLE_SELF && c[0] === NavigationProperty.OLDSTYLE_SELF))
result.selfs.push(c[1]);
else
result.constraints[c] = c;
}
else
result.constraints[c] = c;
}
}
}
else if(expr.op == 'and')
{
expr.args.forEach(sub =>
{ if(sub.op) getExpressionArguments(sub) });
}
else // any other operator will lead to reset of constraints (OR, !=, <>, ...) and return
{
result.constraints = Object.create(null);
return;
}
}

@@ -1191,19 +1237,21 @@

// reverse the default cardinality for a backlink
//let srcCardinality = '*';
//let tgtCardinality = '0..1';
/* new csn:
src, min, max
*/
if(!csn.cardinality)
csn.cardinality = Object.create(null);
if(!csn.cardinality.sourceMax)
csn.cardinality.sourceMax = '*';
if(!csn.cardinality.targetMin)
csn.cardinality.targetMin = 0;
if(!csn.cardinality.targetMax)
csn.cardinality.targetMax = 1;
let srcCardinality = (csn.cardinality.sourceMax == 1) ? '0..1' : '*';
let tgtCardinality = (csn.cardinality.targetMax > 1 || csn.cardinality.targetMax == '*') ? '*' :
(csn.cardinality.targetMin == 1) ? '1' : '0..1';
// set missing defaults
if(!csn.cardinality.src)
csn.cardinality.src = '*';
if(!csn.cardinality.min)
csn.cardinality.min = 0;
if(!csn.cardinality.max)
csn.cardinality.max = 1;
let srcCardinality = (csn.cardinality.src == 1) ? '0..1' : '*';
let tgtCardinality = (csn.cardinality.max > 1 || csn.cardinality.max == '*') ? '*' :
(csn.cardinality.min == 1) ? '1' : '0..1';
// if originCsn was provided, reverse multiplicity for backlink

@@ -1210,0 +1258,0 @@ result.multiplicity = originCsn ? [tgtCardinality, srcCardinality] : [srcCardinality, tgtCardinality];

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

{
return isAssociation(artifact) && artifact.onCond == undefined;
return isAssociation(artifact) && artifact.onCond == undefined && artifact.on == undefined;
}

@@ -83,3 +83,3 @@

// Different representations possible: array or targetMax property
let targetMax = assoc.cardinality[1] || assoc.cardinality.targetMax;
let targetMax = assoc.cardinality[1] ||assoc.cardinality.max;
if (!targetMax) {

@@ -159,9 +159,25 @@ return false;

// Naming rules
// Naming rules for aggregated views with parameters
// Parameters: EntityType <ViewName>Parameters, EntitySet <ViewName>
// with NavigationProperty "Results" pointing to the entity set of type <ViewName>Type
// with NavigationProperty "Results" pointing to the entity set of type <ViewName>Result
// Result: EntityType <ViewName>Result, EntitySet <ViewName>Results
let parameterCsn = { name: entityName+'Parameters', kind: 'entity', isParamEntity:true, elements: Object.create(null) };
// Naming rules for non aggregated views with parameters
// Parameters: EntityType <ViewName>Parameters, EntitySet <ViewName>
// with NavigationProperty "Set" pointing to the entity set of type <ViewName>Type
// Result: EntityType <ViewName>Type, EntitySet <ViewName>Set
// Backlink Navigation Property "Parameters" to <ViewName>Parameters
// this code can be extended for aggregated views
let parameterEntityName = entityName + 'Parameters';
let parameterEntitySetName = entityName;
let originalEntityName = entityName + 'Type';
let originalEntitySetName = entityName + 'Set';
let parameterToOriginalAssocName = 'Set';
let backlinkAssocName = 'Parameters';
let hasBacklink = true;
// Construct the parameter entity
let parameterCsn = { name: parameterEntityName, setName: parameterEntitySetName, kind: 'entity', isParamEntity:true, elements: Object.create(null) };
// propagate containment information, if containment is recursive, use parameterCsn.name as _containerEntity

@@ -176,3 +192,2 @@ if(entityCsn._containerEntity) {

let idx = 0;
forAll(entityCsn.params, (p,n) => {

@@ -184,15 +199,28 @@ let elt = deepCopy(p);

parameterCsn.elements[n] = elt;
idx = (p.indexNo > idx) ? p.indexNo : idx;
});
entityCsn.name = entityName+'Result';
// add assoc to result set, FIXME: is the cardinality correct?
parameterCsn.elements['Results'] = {
'@odata.contained':true,
name: 'Results',
parameterCsn.elements[parameterToOriginalAssocName] = {
'@odata.contained': true,
name: parameterToOriginalAssocName,
target: entityCsn,
type: 'cds.Association',
indexNo: ++idx,
cardinality: { sourceMax: 1, targetMin: 0, targetMax: '*' }
cardinality: { src: 1, min: 0, max: '*' }
};
model.definitions[parameterCsn.name] = parameterCsn;
// modify the original parameter entity with backlink and new name
entityCsn.name = originalEntityName;
entityCsn.setName = originalEntitySetName;
// add backlink association
if(hasBacklink) {
entityCsn.elements[backlinkAssocName] = {
name: backlinkAssocName,
target: parameterCsn,
type: 'cds.Association',
on: [ { ref: [ 'Parameters', 'Set' ] }, '=', { ref: [ '$self' ] } ]
};
}
}

@@ -269,24 +297,22 @@ // Initialize structured artifact (type or entity) 'struct' by doing the

if(typeof element.target === "string") {
let targetEntity = model.definitions[element.target];
element.target = model.definitions[element.target];
if(targetEntity._containerEntity && targetEntity.params)
targetEntity = targetEntity._containerEntity[0];
if (targetEntity == undefined) {
throw 'Target ' + element.target + ' of association ' + struct.name + '.' + element.name + ' cannot be resolved';
// if the target is a containee AND target has parameters, redirect
// target to <Type>Parameters entity (see initializeParameterizedEntityOrView() above)
// Preserve the original target for backlinks (see edm.js NavigationProperty.getReferentialConstraints()
// for details
if(element.target._containerEntity && element.target.params) {
element.originalTarget = element.target;
element.target = element.target._containerEntity[0];
}
// Replace target name by its artifact
element.target = targetEntity;
}
//forward annotations from managed association element to its foreign keys
if(!element.onCond)
{
forAll(element.foreignKeys, fk => {
if(element.keys) {
for(let fk of element.keys) {
forAll(element, (attr, attrName) => {
if(attrName[0] == '@')
struct.elements[fk.generatedFieldName][attrName] = attr;
struct.elements[fk.$generatedFieldName][attrName] = attr;
});
});
}
}

@@ -321,3 +347,2 @@ });

(key, keyName) => { pkn = keyName; });
pkn = assocName + '.' + pkn;

@@ -329,5 +354,3 @@ // add the value list association as new element into struct.elements

type: 'cds.Association',
indexNo: Object.keys(struct.elements).length + 1,
on: element.name + " = " + pkn,
onCond: { args: [ { '=' : element.name }, { '=' : pkn } ], op: "=" }
on: [ { ref: [ element.name ] },"=", { ref: [ assocName, pkn ] } ]
}

@@ -334,0 +357,0 @@ }

@@ -1,2 +0,2 @@

// Generated from JSON.g4 by ANTLR 4.6
// Generated from JSON.g4 by ANTLR 4.7.1
// jshint ignore: start

@@ -34,3 +34,3 @@ var antlr4 = require('antlr4/index');

var serializedATN = ["\u0003\u0430\ud6d1\u8206\uad2d\u4417\uaef1\u8d80\uaadd",
var serializedATN = ["\u0003\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964",
"\u0002\u0010\u00a5\b\u0001\u0004\u0002\t\u0002\u0004\u0003\t\u0003\u0004",

@@ -66,38 +66,38 @@ "\u0004\t\u0004\u0004\u0005\t\u0005\u0004\u0006\t\u0006\u0004\u0007\t",

"3;\u0004\u0002GGgg\u0004\u0002--//\u0005\u0002\u000b\f\u000f\u000f\"",
"\"\u0004\u0002\f\f\u000f\u000f\u00af\u0002\u0003\u0003\u0002\u0002\u0002",
"\u0002\u0005\u0003\u0002\u0002\u0002\u0002\u0007\u0003\u0002\u0002\u0002",
"\u0002\t\u0003\u0002\u0002\u0002\u0002\u000b\u0003\u0002\u0002\u0002",
"\u0002\r\u0003\u0002\u0002\u0002\u0002\u000f\u0003\u0002\u0002\u0002",
"\u0002\u0011\u0003\u0002\u0002\u0002\u0002\u0013\u0003\u0002\u0002\u0002",
"\u0002\u0015\u0003\u0002\u0002\u0002\u0002\u001d\u0003\u0002\u0002\u0002",
"\u0002#\u0003\u0002\u0002\u0002\u0002%\u0003\u0002\u0002\u0002\u0002",
"\'\u0003\u0002\u0002\u0002\u0003)\u0003\u0002\u0002\u0002\u0005+\u0003",
"\u0002\u0002\u0002\u0007-\u0003\u0002\u0002\u0002\t/\u0003\u0002\u0002",
"\u0002\u000b1\u0003\u0002\u0002\u0002\r3\u0003\u0002\u0002\u0002\u000f",
"5\u0003\u0002\u0002\u0002\u0011:\u0003\u0002\u0002\u0002\u0013@\u0003",
"\u0002\u0002\u0002\u0015E\u0003\u0002\u0002\u0002\u0017O\u0003\u0002",
"\u0002\u0002\u0019T\u0003\u0002\u0002\u0002\u001bZ\u0003\u0002\u0002",
"\u0002\u001ds\u0003\u0002\u0002\u0002\u001f}\u0003\u0002\u0002\u0002",
"!\u007f\u0003\u0002\u0002\u0002#\u0086\u0003\u0002\u0002\u0002%\u008c",
"\u0003\u0002\u0002\u0002\'\u009a\u0003\u0002\u0002\u0002)*\u0007}\u0002",
"\u0002*\u0004\u0003\u0002\u0002\u0002+,\u0007.\u0002\u0002,\u0006\u0003",
"\u0002\u0002\u0002-.\u0007\u007f\u0002\u0002.\b\u0003\u0002\u0002\u0002",
"/0\u0007<\u0002\u00020\n\u0003\u0002\u0002\u000212\u0007]\u0002\u0002",
"2\f\u0003\u0002\u0002\u000234\u0007_\u0002\u00024\u000e\u0003\u0002",
"\u0002\u000256\u0007v\u0002\u000267\u0007t\u0002\u000278\u0007w\u0002",
"\u000289\u0007g\u0002\u00029\u0010\u0003\u0002\u0002\u0002:;\u0007h",
"\u0002\u0002;<\u0007c\u0002\u0002<=\u0007n\u0002\u0002=>\u0007u\u0002",
"\u0002>?\u0007g\u0002\u0002?\u0012\u0003\u0002\u0002\u0002@A\u0007p",
"\u0002\u0002AB\u0007w\u0002\u0002BC\u0007n\u0002\u0002CD\u0007n\u0002",
"\u0002D\u0014\u0003\u0002\u0002\u0002EJ\u0007$\u0002\u0002FI\u0005\u0017",
"\f\u0002GI\n\u0002\u0002\u0002HF\u0003\u0002\u0002\u0002HG\u0003\u0002",
"\u0002\u0002IL\u0003\u0002\u0002\u0002JH\u0003\u0002\u0002\u0002JK\u0003",
"\u0002\u0002\u0002KM\u0003\u0002\u0002\u0002LJ\u0003\u0002\u0002\u0002",
"MN\u0007$\u0002\u0002N\u0016\u0003\u0002\u0002\u0002OR\u0007^\u0002",
"\u0002PS\t\u0003\u0002\u0002QS\u0005\u0019\r\u0002RP\u0003\u0002\u0002",
"\u0002RQ\u0003\u0002\u0002\u0002S\u0018\u0003\u0002\u0002\u0002TU\u0007",
"w\u0002\u0002UV\u0005\u001b\u000e\u0002VW\u0005\u001b\u000e\u0002WX",
"\u0005\u001b\u000e\u0002XY\u0005\u001b\u000e\u0002Y\u001a\u0003\u0002",
"\u0002\u0002Z[\t\u0004\u0002\u0002[\u001c\u0003\u0002\u0002\u0002\\",
"^\u0007/\u0002\u0002]\\\u0003\u0002\u0002\u0002]^\u0003\u0002\u0002",
"\"\u0004\u0002\f\f\u000f\u000f\u0002\u00af\u0002\u0003\u0003\u0002\u0002",
"\u0002\u0002\u0005\u0003\u0002\u0002\u0002\u0002\u0007\u0003\u0002\u0002",
"\u0002\u0002\t\u0003\u0002\u0002\u0002\u0002\u000b\u0003\u0002\u0002",
"\u0002\u0002\r\u0003\u0002\u0002\u0002\u0002\u000f\u0003\u0002\u0002",
"\u0002\u0002\u0011\u0003\u0002\u0002\u0002\u0002\u0013\u0003\u0002\u0002",
"\u0002\u0002\u0015\u0003\u0002\u0002\u0002\u0002\u001d\u0003\u0002\u0002",
"\u0002\u0002#\u0003\u0002\u0002\u0002\u0002%\u0003\u0002\u0002\u0002",
"\u0002\'\u0003\u0002\u0002\u0002\u0003)\u0003\u0002\u0002\u0002\u0005",
"+\u0003\u0002\u0002\u0002\u0007-\u0003\u0002\u0002\u0002\t/\u0003\u0002",
"\u0002\u0002\u000b1\u0003\u0002\u0002\u0002\r3\u0003\u0002\u0002\u0002",
"\u000f5\u0003\u0002\u0002\u0002\u0011:\u0003\u0002\u0002\u0002\u0013",
"@\u0003\u0002\u0002\u0002\u0015E\u0003\u0002\u0002\u0002\u0017O\u0003",
"\u0002\u0002\u0002\u0019T\u0003\u0002\u0002\u0002\u001bZ\u0003\u0002",
"\u0002\u0002\u001ds\u0003\u0002\u0002\u0002\u001f}\u0003\u0002\u0002",
"\u0002!\u007f\u0003\u0002\u0002\u0002#\u0086\u0003\u0002\u0002\u0002",
"%\u008c\u0003\u0002\u0002\u0002\'\u009a\u0003\u0002\u0002\u0002)*\u0007",
"}\u0002\u0002*\u0004\u0003\u0002\u0002\u0002+,\u0007.\u0002\u0002,\u0006",
"\u0003\u0002\u0002\u0002-.\u0007\u007f\u0002\u0002.\b\u0003\u0002\u0002",
"\u0002/0\u0007<\u0002\u00020\n\u0003\u0002\u0002\u000212\u0007]\u0002",
"\u00022\f\u0003\u0002\u0002\u000234\u0007_\u0002\u00024\u000e\u0003",
"\u0002\u0002\u000256\u0007v\u0002\u000267\u0007t\u0002\u000278\u0007",
"w\u0002\u000289\u0007g\u0002\u00029\u0010\u0003\u0002\u0002\u0002:;",
"\u0007h\u0002\u0002;<\u0007c\u0002\u0002<=\u0007n\u0002\u0002=>\u0007",
"u\u0002\u0002>?\u0007g\u0002\u0002?\u0012\u0003\u0002\u0002\u0002@A",
"\u0007p\u0002\u0002AB\u0007w\u0002\u0002BC\u0007n\u0002\u0002CD\u0007",
"n\u0002\u0002D\u0014\u0003\u0002\u0002\u0002EJ\u0007$\u0002\u0002FI",
"\u0005\u0017\f\u0002GI\n\u0002\u0002\u0002HF\u0003\u0002\u0002\u0002",
"HG\u0003\u0002\u0002\u0002IL\u0003\u0002\u0002\u0002JH\u0003\u0002\u0002",
"\u0002JK\u0003\u0002\u0002\u0002KM\u0003\u0002\u0002\u0002LJ\u0003\u0002",
"\u0002\u0002MN\u0007$\u0002\u0002N\u0016\u0003\u0002\u0002\u0002OR\u0007",
"^\u0002\u0002PS\t\u0003\u0002\u0002QS\u0005\u0019\r\u0002RP\u0003\u0002",
"\u0002\u0002RQ\u0003\u0002\u0002\u0002S\u0018\u0003\u0002\u0002\u0002",
"TU\u0007w\u0002\u0002UV\u0005\u001b\u000e\u0002VW\u0005\u001b\u000e",
"\u0002WX\u0005\u001b\u000e\u0002XY\u0005\u001b\u000e\u0002Y\u001a\u0003",
"\u0002\u0002\u0002Z[\t\u0004\u0002\u0002[\u001c\u0003\u0002\u0002\u0002",
"\\^\u0007/\u0002\u0002]\\\u0003\u0002\u0002\u0002]^\u0003\u0002\u0002",
"\u0002^_\u0003\u0002\u0002\u0002_`\u0005\u001f\u0010\u0002`b\u00070",

@@ -155,2 +155,8 @@ "\u0002\u0002ac\t\u0005\u0002\u0002ba\u0003\u0002\u0002\u0002cd\u0003",

Object.defineProperty(JSONLexer.prototype, "atn", {
get : function() {
return atn;
}
});
JSONLexer.EOF = antlr4.Token.EOF;

@@ -172,2 +178,3 @@ JSONLexer.T__0 = 1;

JSONLexer.prototype.channelNames = [ "DEFAULT_TOKEN_CHANNEL", "HIDDEN" ];

@@ -174,0 +181,0 @@ JSONLexer.prototype.modeNames = [ "DEFAULT_MODE" ];

@@ -1,2 +0,2 @@

// Generated from JSON.g4 by ANTLR 4.6
// Generated from JSON.g4 by ANTLR 4.7.1
// jshint ignore: start

@@ -34,3 +34,3 @@ var antlr4 = require('antlr4/index');

var serializedATN = ["\u0003\u0430\ud6d1\u8206\uad2d\u4417\uaef1\u8d80\uaadd",
var serializedATN = ["\u0003\u608b\ua72a\u8133\ub9ed\u417c\u3be7\u7786\u5964",
"\u0003\u0010H\u0004\u0002\t\u0002\u0004\u0003\t\u0003\u0004\u0004\t",

@@ -48,34 +48,34 @@ "\u0004\u0004\u0005\t\u0005\u0004\u0006\t\u0006\u0003\u0002\u0003\u0002",

"\u0006\u0003\u0006\u0003\u0006\u0005\u0006F\n\u0006\u0003\u0006\u0002",
"\u0002\u0007\u0002\u0004\u0006\b\n\u0002\u0002M\u0002\u000e\u0003\u0002",
"\u0002\u0002\u0004\u001d\u0003\u0002\u0002\u0002\u0006\u001f\u0003\u0002",
"\u0002\u0002\b7\u0003\u0002\u0002\u0002\nE\u0003\u0002\u0002\u0002\f",
"\u000f\u0005\u0004\u0003\u0002\r\u000f\u0005\b\u0005\u0002\u000e\f\u0003",
"\u0002\u0002\u0002\u000e\r\u0003\u0002\u0002\u0002\u000f\u0003\u0003",
"\u0002\u0002\u0002\u0010\u0011\u0007\u0003\u0002\u0002\u0011\u0016\u0005",
"\u0006\u0004\u0002\u0012\u0013\u0007\u0004\u0002\u0002\u0013\u0015\u0005",
"\u0006\u0004\u0002\u0014\u0012\u0003\u0002\u0002\u0002\u0015\u0018\u0003",
"\u0002\u0002\u0002\u0016\u0014\u0003\u0002\u0002\u0002\u0016\u0017\u0003",
"\u0002\u0002\u0002\u0017\u0019\u0003\u0002\u0002\u0002\u0018\u0016\u0003",
"\u0002\u0002\u0002\u0019\u001a\u0007\u0005\u0002\u0002\u001a\u001e\u0003",
"\u0002\u0002\u0002\u001b\u001c\u0007\u0003\u0002\u0002\u001c\u001e\u0007",
"\u0005\u0002\u0002\u001d\u0010\u0003\u0002\u0002\u0002\u001d\u001b\u0003",
"\u0002\u0002\u0002\u001e\u0005\u0003\u0002\u0002\u0002\u001f \u0007",
"\f\u0002\u0002 !\b\u0004\u0001\u0002!\"\u0007\u0006\u0002\u0002\"#\u0005",
"\n\u0006\u0002#$\b\u0004\u0001\u0002$\u0007\u0003\u0002\u0002\u0002",
"%&\u0007\u0007\u0002\u0002&\'\b\u0005\u0001\u0002\'(\u0005\n\u0006\u0002",
"(0\b\u0005\u0001\u0002)*\u0007\u0004\u0002\u0002*+\b\u0005\u0001\u0002",
"+,\u0005\n\u0006\u0002,-\b\u0005\u0001\u0002-/\u0003\u0002\u0002\u0002",
".)\u0003\u0002\u0002\u0002/2\u0003\u0002\u0002\u00020.\u0003\u0002\u0002",
"\u000201\u0003\u0002\u0002\u000213\u0003\u0002\u0002\u000220\u0003\u0002",
"\u0002\u000234\u0007\b\u0002\u000248\u0003\u0002\u0002\u000256\u0007",
"\u0007\u0002\u000268\u0007\b\u0002\u00027%\u0003\u0002\u0002\u00027",
"5\u0003\u0002\u0002\u00028\t\u0003\u0002\u0002\u00029:\u0007\f\u0002",
"\u0002:F\b\u0006\u0001\u0002;<\u0007\r\u0002\u0002<F\b\u0006\u0001\u0002",
"=F\u0005\u0004\u0003\u0002>F\u0005\b\u0005\u0002?@\u0007\t\u0002\u0002",
"@F\b\u0006\u0001\u0002AB\u0007\n\u0002\u0002BF\b\u0006\u0001\u0002C",
"D\u0007\u000b\u0002\u0002DF\b\u0006\u0001\u0002E9\u0003\u0002\u0002",
"\u0002E;\u0003\u0002\u0002\u0002E=\u0003\u0002\u0002\u0002E>\u0003\u0002",
"\u0002\u0002E?\u0003\u0002\u0002\u0002EA\u0003\u0002\u0002\u0002EC\u0003",
"\u0002\u0002\u0002F\u000b\u0003\u0002\u0002\u0002\b\u000e\u0016\u001d",
"07E"].join("");
"\u0002\u0007\u0002\u0004\u0006\b\n\u0002\u0002\u0002M\u0002\u000e\u0003",
"\u0002\u0002\u0002\u0004\u001d\u0003\u0002\u0002\u0002\u0006\u001f\u0003",
"\u0002\u0002\u0002\b7\u0003\u0002\u0002\u0002\nE\u0003\u0002\u0002\u0002",
"\f\u000f\u0005\u0004\u0003\u0002\r\u000f\u0005\b\u0005\u0002\u000e\f",
"\u0003\u0002\u0002\u0002\u000e\r\u0003\u0002\u0002\u0002\u000f\u0003",
"\u0003\u0002\u0002\u0002\u0010\u0011\u0007\u0003\u0002\u0002\u0011\u0016",
"\u0005\u0006\u0004\u0002\u0012\u0013\u0007\u0004\u0002\u0002\u0013\u0015",
"\u0005\u0006\u0004\u0002\u0014\u0012\u0003\u0002\u0002\u0002\u0015\u0018",
"\u0003\u0002\u0002\u0002\u0016\u0014\u0003\u0002\u0002\u0002\u0016\u0017",
"\u0003\u0002\u0002\u0002\u0017\u0019\u0003\u0002\u0002\u0002\u0018\u0016",
"\u0003\u0002\u0002\u0002\u0019\u001a\u0007\u0005\u0002\u0002\u001a\u001e",
"\u0003\u0002\u0002\u0002\u001b\u001c\u0007\u0003\u0002\u0002\u001c\u001e",
"\u0007\u0005\u0002\u0002\u001d\u0010\u0003\u0002\u0002\u0002\u001d\u001b",
"\u0003\u0002\u0002\u0002\u001e\u0005\u0003\u0002\u0002\u0002\u001f ",
"\u0007\f\u0002\u0002 !\b\u0004\u0001\u0002!\"\u0007\u0006\u0002\u0002",
"\"#\u0005\n\u0006\u0002#$\b\u0004\u0001\u0002$\u0007\u0003\u0002\u0002",
"\u0002%&\u0007\u0007\u0002\u0002&\'\b\u0005\u0001\u0002\'(\u0005\n\u0006",
"\u0002(0\b\u0005\u0001\u0002)*\u0007\u0004\u0002\u0002*+\b\u0005\u0001",
"\u0002+,\u0005\n\u0006\u0002,-\b\u0005\u0001\u0002-/\u0003\u0002\u0002",
"\u0002.)\u0003\u0002\u0002\u0002/2\u0003\u0002\u0002\u00020.\u0003\u0002",
"\u0002\u000201\u0003\u0002\u0002\u000213\u0003\u0002\u0002\u000220\u0003",
"\u0002\u0002\u000234\u0007\b\u0002\u000248\u0003\u0002\u0002\u00025",
"6\u0007\u0007\u0002\u000268\u0007\b\u0002\u00027%\u0003\u0002\u0002",
"\u000275\u0003\u0002\u0002\u00028\t\u0003\u0002\u0002\u00029:\u0007",
"\f\u0002\u0002:F\b\u0006\u0001\u0002;<\u0007\r\u0002\u0002<F\b\u0006",
"\u0001\u0002=F\u0005\u0004\u0003\u0002>F\u0005\b\u0005\u0002?@\u0007",
"\t\u0002\u0002@F\b\u0006\u0001\u0002AB\u0007\n\u0002\u0002BF\b\u0006",
"\u0001\u0002CD\u0007\u000b\u0002\u0002DF\b\u0006\u0001\u0002E9\u0003",
"\u0002\u0002\u0002E;\u0003\u0002\u0002\u0002E=\u0003\u0002\u0002\u0002",
"E>\u0003\u0002\u0002\u0002E?\u0003\u0002\u0002\u0002EA\u0003\u0002\u0002",
"\u0002EC\u0003\u0002\u0002\u0002F\u000b\u0003\u0002\u0002\u0002\b\u000e",
"\u0016\u001d07E"].join("");

@@ -82,0 +82,0 @@

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

function nullServiceArtifacts(node) {
if(node.kind === "service")
if(node.artifacts === undefined)
node.artifacts = null;
}
function modifyAnnotation(node, key, path) {

@@ -562,3 +556,2 @@ node[key] = newAnnotation(key, node[key], path);

augmentDefinitionIncludes(def, path);
nullServiceArtifacts(def);

@@ -565,0 +558,0 @@ if(def.items)

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

function augmentItems(path, items) {
transformers.modifyNumber(items, "length", path);
U.setLocation(items, path);
}
function augmentParam(name, node, Path) {
let path = Path.concat(name)
node.location=U.newLocation(path);
let kind = "param";
// special handling for annotate extensions - the element.kind is annotate
if( isExtensionAnnotate(path) ) {
kind="annotate";
}
node.kind = kind;
node.name = {
id:name, // TODO check this
location: U.newLocation(path, U.WILO_FIRST),
param:name // remove? just for testAugmentor3.js?
}
}
function augmentAction(name, node, Path) {
let path = Path.concat(name)
U.setLocation(node, path);
if(node.returns && node.returns.items) {
U.setLocation(node.returns.items, path.concat(["returns","items"]));
}
// special handling for annotate extensions - the element.kind is annotate
if( isExtensionAnnotate(path) ) {
node.kind = "annotate";
}
}
function isExtensionAnnotate(path) {
if( path.length>2
&& path[0] === "extensions"
&& model[path[0]][path[1]].kind==="annotate")
return true;
return false;
}
function augmentElement(name, node, Path) {
let path = Path.concat(name)
U.setLocation(node, path);
elementName(name, node, path);
elementKind(node, path);
if(node.items)
augmentItems(path.concat("items"),node.items)
// returns an array of nodes where their parents have no protos, skipping the definition name
function getElementPrefix(path) {
let R = []
path=path.slice(1) // remove definitions
let parent = model.definitions[path[0]]; // start with the definition
W.forEach(path, (index,item) => {
if(!parent) return;
if(index==="0") return; // ignore the definition
if(!Object.getPrototypeOf(parent))
R.push(item)
parent = parent[item]
})
return R;
}
function elementName(name, node, path) {
let nameParts = getElementPrefix(path);
node.name = {
id: name,
element:nameParts[-1]
};
U.setLocation(node.name, path.concat(name), U.WILO_FIRST)
}
function elementKind(node, path) {
let kind = "element";
// special handling for annotate extensions - the element.kind is annotate
if( isExtensionAnnotate(path) ) {
kind="annotate";
}
node.kind = kind;
}
}
function augmentExpression(value, path) {
return transformers.newValue(value,path)
}
function augmentEnumItem(name, node, path) {
U.setLocation(node, path.concat(name));
elementName(name, node, path); // reuse function !
enumKind(node);
if(node.val !== undefined) {
node.value = augmentExpression(node.val, path)
delete node["val"]
}
function elementName(name, node, path) {
let nameParts = getElementPrefix(path);
nameParts.push(name)
node.name = {
id: name,
element:nameParts[-1]
};
U.setLocation(node.name, path.concat(name), U.WILO_FIRST)
}
function getElementPrefix(path) {
let R = []
let parent = model.definitions[path[0]]; // start with the definition
W.forEach(path, (index,item) => {
if(!parent) return;
if(index==="0") return; // ignore the definition
if(!Object.getPrototypeOf(parent))
R.push(item)
parent = parent[item]
})
return R;
}
function enumKind(node) {
node.kind = "enum";
}
}
function augmentDefinition(name, def, path) {
U.setLocation(def, path);
definitionName(name, def);
if(def.items)
augmentItems(path.concat("items"),def.items)
function definitionName(name, def) {
let sp = name.split(".");
let last = sp[sp.length-1];
let sp1 = last.split("::"); //TODO check this split
last = sp1[sp1.length-1];
let location = U.newLocation(path, U.WILO_FIRST)
def.name = {
id: last, // TODO only for semanticChecks.js/checkGenericConstruct ?
absolute: name, // TODO only for semanticChecks.js/checkGenericArtifact ?
location
}
} // definitionName
} // augmentDefinition
function augmentExtension(key,E,path) {
let locationObj = U.newLocation(path, U.WILO_FULL)
if(E.annotate) {
let location = U.newLocation(path.concat("annotate"), U.WILO_LAST)
E.kind="annotate",
E.name={path:[{id:E.annotate,location}],location};
E.location = locationObj;
delete E["annotate"]
}
}
// here starts the augmentation

@@ -177,12 +17,13 @@

W.walkWithPath(model, (isNode,PATH,NODE) => {
if (NODE === null)
return;
if(!isNode)
return;
let section = PATH[0];
if(!["definitions","extensions"].includes(section))
return;
if (NODE == null || typeof NODE !== 'object') {
return;
}
if(section === "definitions" && PATH.length === 2) // definition
augmentDefinition(PATH[1], NODE, PATH);
transformers.augmentDefinition(PATH[1], NODE, PATH);
if(section === "extensions" && PATH.length === 2) // extension
augmentExtension(PATH[1], NODE, PATH);
transformers.augmentExtension(PATH[1], NODE, PATH);

@@ -195,26 +36,37 @@ let PROTO = Object.getPrototypeOf(NODE);

if(T) {
T(NODE, key, PATH);
if(!U.isAugmented(NODE[key])) // prevent double augmentation
T(NODE, key, PATH);
}
} else { // dict
if(getLastElement(PATH) === "elements") {
augmentElement(key, node, PATH);
transformers.augmentElement(key, node, PATH);
}
if(getLastElement(PATH) === "enum") {
augmentEnumItem(key, node, PATH);
transformers.augmentEnumItem(key, node, PATH);
}
if(getLastElement(PATH) === "params") {
augmentParam(key, node, PATH);
transformers.augmentParam(key, node, PATH);
}
if(getLastElement(PATH) === "actions") {
augmentAction(key, node, PATH);
transformers.augmentAction(key, node, PATH);
}
}
}) // forEach
}, (path/*,obj*/) => { // check function
}, (path,obj) => { // check function
let le = U.getLastElement(path)
if(le[0]==="@")
return false; // do not walk annotations
if(U.isAugmented(obj)) {
return false;
}
return true;
}) // walkWithPath
// remove "augmented" flag
W.walk(model, (isNode,NODE) => {
if(isNode && U.isAugmented(NODE)) {
U.unsetAugmented(NODE)
}
})
if(model.namespace) {

@@ -233,2 +85,3 @@ model.namespace = {

//console.log(JSON.stringify(model,null,2))
} // function augment

@@ -235,0 +88,0 @@

@@ -10,60 +10,9 @@ let W = require("./walker");

function augmentNumber(val,path, which=U.WILO_FULL) {
return {
val,
literal: "number",
location: U.newLocation(path, which)
}
}
// augment query instance
let AQ;
let newValue;
function newValue(val, path, name, which=U.WILO_FULL) {
if(val===undefined)
return undefined;
if(U.isAugmented(val))
return undefined;
let ret;
let literal = typeof val; // TODO
if(val===null) {
ret = { literal: 'null', val };
}
else if(Array.isArray(val)) { // process array
ret = { literal: 'array',
val: val.map((V,I) => newValue( V, path.concat(''+I) ) )
};
}
else if(literal !== "object") { // number, string
ret = { literal, val };
}
else if ('#' in val) { // TODO: length / other property test?
ret = { literal: 'enum', symbol: { id: val['#']} };
}
else if ('=' in val) { // TODO: length / other property test?
let location = U.newLocation(path);
ret = { path: val['='].split('.').map( id => ({ id, location }) ) };
}
else {
let struct = {};// TODO why not -> Object.create(null);
ret = { literal: 'struct', struct };
W.forEach(val, (K,V) => {
struct[K] = newValue(V, path.concat(K), K);
})
}
if(name) {
ret.name = {id:name};
U.setLocation(ret.name, path);
}
U.setLocation(ret, path, which);
if(ret.symbol)
U.setLocation(ret.symbol, path);
U.setAugmented(ret)
return ret;
}
function modifyGroupBy(node, name, path) {
let X = node[name];
X.forEach( (Y, iY) => augmentExpressionItem(Y, path.concat(name,iY)))
}
function modifyCondition(node, name, path) {
node.onCond = augmentXPR(node.on, path.concat(name));
let xpr = AQ.augmentXPR(node.on, path.concat(name));
node.onCond = xpr;
delete node["on"]

@@ -98,14 +47,5 @@ }

function annotateExtensions(/*node, name, path*/) {
}
function ignore(/*node, name, path*/) {
}
function nonEmptyDict(/*node, name, path*/) {
}
function insertOrderDict(/*node, name, path*/) {
}
function arrayAsDict(node, name, path, xsnName=name) {

@@ -154,2 +94,3 @@ let A = node[name]

let value=node[name];
let lpath = path.concat(name);
if(newName!==undefined) {

@@ -160,3 +101,3 @@ delete node[name]

if(value !== undefined) {
node[name]=augmentNumber(value, path.concat(name), U.WILO_LAST);
node[name]=AQ.augmentNumber(value, lpath, U.WILO_LAST);
U.setAugmented(node[name]);

@@ -210,14 +151,10 @@ }

} else if(val.ref) {
newPath = refAsPath(val.ref, path.concat("ref"))
} else
newPath = AQ.refAsPath(val.ref, path.concat("ref"))
} else {
throw Error ("Unexpected type reference '"+(typeof val)+"' under "+path.join("/"));
let r = {
}
return {
path:newPath,
location
}
if(val.ref) {
let element = val.ref.slice(1);
r.element = element.join(".");
}
return r;
};
}

@@ -232,281 +169,218 @@

function condition(/*node, name, path*/) {
function modifyExpression(node, name, path) {
let X = node[name];
if(!Array.isArray(X))
node[name] = AQ.augmentExpressionItem(X,name,path)
else
throw Error("Array expressions are not supported")
}
function augmentLiteralValue(val, path) {
let location = U.newLocation(path)
let literal = typeof val;
return {val, literal, location};
function modifyAnnotation(node, key, path) {
node[key] = newAnnotation(key, node[key], path);
function newAnnotation(anAnno, aValue, path) {
if (aValue instanceof Object && 'literal' in aValue)
throw new Error('strange re-run')
let ret = {};
if(aValue!==true && aValue!==undefined)
ret = newValue(aValue, path.concat(anAnno));
ret.name = { location: U.newLocation(path.concat(anAnno), U.WILO_FIRST) };
return ret;
}
}
function refAsPath(ref, path) {
let R = ref.map((id,I) => {
let location = U.newLocation(path.concat(""+I))
return {id,location}
})
return R;
function modifyQuery(node, key, path) {
let R = AQ.augmentQuery(node[key], path.concat(key));
node[key] = R
}
function augmentExpressionItem(val, path) {
let location = U.newLocation(path)
if(val === null)
return {val:null,location}
let t = typeof val;
if(t === "string") {
return val;
} else if(t === "boolean")
return {val, literal: 'boolean', location};
if(val.hasOwnProperty("ref")) {
return {
path: refAsPath(val.ref, path.concat("ref")), // TODO remove this later - here 'ref' is renamed to 'path'
location};
} else if(val.hasOwnProperty("literal")) {
if(!val.hasOwnProperty("val"))
throw Error("Literal without val property")
val.location = U.newLocation(path);
return val;
} else if(val.hasOwnProperty("val")) {
return augmentLiteralValue(val.val, path)
} else if(val.hasOwnProperty("func")) {
return {
op:{val: "call", location},
func: {path:[{id:val.func}]}, // to-csn.js has TODO XSN: remove op: 'call', func is no path
args: augmentExpression(val.args,path.concat("args")),
location
////////////////////////////////////
function augmentElement(name, node, Path) {
let path = Path.concat(name)
U.setLocation(node, path);
elementName(name, node, path);
elementKind(node, path);
if(node.items)
augmentItems(path.concat("items"),node.items)
function elementName(name, node, path) {
node.name = {
id: name
};
U.setLocation(node.name, path.concat(name), U.WILO_FIRST)
}
function elementKind(node, path) {
let kind = "element";
// special handling for annotate extensions - the element.kind is annotate
if( isExtensionAnnotate(path) ) {
kind="annotate";
}
} else if(val.hasOwnProperty("#")) {
return {
literal:"enum", symbol:{id:val["#"],location},location
}
node.kind = kind;
}
throw Error("augmentExpressionItem failed: "+JSON.stringify(val)+path.join("/"))
}
function modifyValueOrExpression(node, name, path) {
let X = node[name]
if(X.val)
node[name] = augmentExpressionItem(X,path.concat(name,"val"))
else
modifyExpression(node,name,path);
function augmentAction(name, node, Path) {
let path = Path.concat(name)
U.setLocation(node, path);
if(node.returns!==undefined) {
U.setLocation(node.returns, path.concat("returns"));
if(node.returns.items!==undefined) {
U.setLocation(node.returns.items, path.concat(["returns","items"]));
}
}
// special handling for annotate extensions - the element.kind is annotate
if( isExtensionAnnotate(path) ) {
node.kind = "annotate";
}
}
function modifyExpression(node, name, path) {
let X = node[name];
if(!Array.isArray(X)) {
node[name] = augmentExpressionItem(X,path.concat(name))
} else {
node[name] = augmentExpression(X, path.concat(name));
function augmentEnumItem(name, node, path) {
U.setLocation(node, path.concat(name));
elementName(name, node, path); // reuse function !
enumKind(node);
if(node.val !== undefined) {
node.value = AQ.augmentExpression(node.val, path);
delete node["val"];
}
function elementName(name, node, path) {
node.name = {
id: name,
location: U.newLocation(path.concat(name), U.WILO_FIRST)
}
}
function enumKind(node) {
node.kind = "enum";
}
}
function augmentXPR(xpr, path) {
if(xpr===undefined)
return undefined;
let location = U.newLocation(path, U.WILO_FULL)
return {
op: { val:"xpr", location },
args: augmentExpression(xpr, path),
location
function augmentParam(name, node, Path) {
let path = Path.concat(name);
node.location=U.newLocation(path);
let kind = "param";
// special handling for annotate extensions - the element.kind is annotate
if( isExtensionAnnotate(path) ) {
kind="annotate";
}
node.kind = kind;
node.name = {
id:name, // TODO check this
location: U.newLocation(path, U.WILO_FIRST),
param:name // remove? just for testAugmentor3.js?
}
}
function augmentExpression(X,path) {
if(!Array.isArray(X))
throw Error("Expression should be an array");
return X.map((Y,I) => augmentExpressionItem(Y, path.concat(""+I)))
function isExtensionAnnotate(path) {
if( path.length>2
&& path[0] === "extensions"
&& model[path[0]][path[1]].kind==="annotate")
return true;
return false;
}
function query(node, name, path) {
let location = U.newLocation(path.concat(name), U.WILO_FULL)
let selectPath = path.concat([name,"SELECT"]);
let fromPath = selectPath.concat("from");
let columnsPath = selectPath.concat("columns");
function augmentItems(path, items) {
transformers.modifyNumber(items, "length", path);
U.setLocation(items, path);
}
function getQuerySource(q) {
let qo = q.SELECT;
if(!qo)
throw Error("Missing SELECT in query") //TODO move to validator
if(!qo.from)
throw Error("Missing FROM in SELECT") //TODO move to validator
if(!qo.from.ref)
throw Error("Missing reference in SELECT.from") //TODO move to validator
if(qo.from.ref) {
let ref = qo.from.ref;
let refPath = fromPath.concat("ref")
return ref.map( (X,I) => {
return {
id:X,
location:U.newLocation(refPath.concat(I), U.WILO_FULL)
}
})
function augmentDefinition(name, def, path) {
U.setLocation(def, path);
definitionName(name, def);
if(def.items !== undefined)
augmentItems(path.concat("items"),def.items)
if(def.returns !== undefined) {
U.setLocation(def.returns, path.concat("returns"));
if(def.returns.items !== undefined) {
U.setLocation(def.returns.items, path.concat("returns","items"));
}
return undefined;
}
let src = getQuerySource(node.query);
function definitionName(name, def) {
let sp = name.split(".");
let last = sp[sp.length-1];
let sp1 = last.split("::"); //TODO check this split
last = sp1[sp1.length-1];
let all;
let columns = node.query.SELECT.columns;
let elements = Object.create(null);
if(columns!==undefined) {
columns.forEach((C,iC) => {
if(C.ref) {
let refPath = columnsPath.concat([iC,"ref"])
let elementNameId = U.getLastElement(C.ref)
let elementNameLocation = U.newLocation(refPath, U.WILO_FULL);
let path = C.ref.map((E,iE) => {
return {
id:E,
location:U.newLocation(refPath.concat(iE), U.WILO_FULL)
};
})
if(C.as) {
elementNameId=C.as;
elementNameLocation = U.newLocation(columnsPath.concat([iC,"as"]), U.WILO_FULL);
}
let elementName = {id: elementNameId, location: elementNameLocation};
if(!C.as)
elementName.$inferred="as";
elements[elementNameId] = {
value: {path, location: elementNameLocation},
name: elementName,
kind: "element",
location: elementNameLocation
};
}
})
let location = U.newLocation(path, U.WILO_FIRST)
def.name = {
id: last, // TODO only for semanticChecks.js/checkGenericConstruct ?
absolute: name, // TODO only for semanticChecks.js/checkGenericArtifact ?
location
}
} // definitionName
} // augmentDefinition
if(Array.isArray(columns)) {
let allIndex = columns.indexOf("*");
if(allIndex!=-1)
all = {
val: true,
location: U.newLocation(columnsPath.concat([allIndex]), U.WILO_FULL)
};
}
function augmentExtension(key,E,path) {
let locationObj = U.newLocation(path, U.WILO_FULL)
if(E.annotate) {
let location = U.newLocation(path.concat("annotate"), U.WILO_LAST)
E.kind="annotate",
E.name={path:[{id:E.annotate,location}],location};
E.location = locationObj;
delete E["annotate"]
}
let from = node.query.SELECT.from;
let fromName;
let locationFrom = U.newLocation(fromPath, U.WILO_FULL)
if(from.as) {
fromName = {
id:from.as,
location: U.newLocation(fromPath.concat("as"), U.WILO_FULL)
}
}
}
let where = augmentXPR(node.query.SELECT.where, selectPath.concat("where"))
let orderBy = node.query.SELECT.orderBy;
if(orderBy !== undefined) {
let orderByPath = selectPath.concat("orderBy");
orderBy = orderBy.map((X,iX) => {
let sort;
let nulls;
if(X.sort) {
sort = {
val: X.sort,
location: U.newLocation(orderByPath.concat(iX,"sort"), U.WILO_FULL)
function walkAndAugment(root, rootPath) {
W.walkWithPath(root, (isNode,subPATH,NODE) => {
if(!isNode)
return;
if (NODE === null)
return;
let PATH = rootPath.concat(subPATH)
let PROTO = Object.getPrototypeOf(NODE);
W.forEach(NODE, (key,node) => {
if(PROTO) {
let isAnnotation = key.charAt(0)==='@';
let T = transformers[isAnnotation ? '@' : key];
if(T) {
T(NODE, key, PATH);
}
}
if(X.nulls) {
nulls = {
val: X.nulls,
location: U.newLocation(orderByPath.concat(iX,"nulls"), U.WILO_FULL)
} else { // dict
if(getLastElement(PATH) === "elements") {
augmentElement(key, node, PATH);
}
}
let value;
if(X.xpr) {
value = augmentXPR(X.xpr, orderByPath.concat(iX,"xpr"));
} else {
value = {
path:X.ref.map((Y,iY) => {
return {
id:Y,
location: U.newLocation(orderByPath.concat(iX,"ref",iY), U.WILO_FULL)
}
}),
location: U.newLocation(orderByPath.concat(iX), U.WILO_FULL)
if(getLastElement(PATH) === "mixin") {
augmentElement(key, node, PATH);
}
if(getLastElement(PATH) === "enum") {
augmentEnumItem(key, node, PATH);
}
if(getLastElement(PATH) === "params") {
augmentParam(key, node, PATH);
}
if(getLastElement(PATH) === "actions") {
augmentAction(key, node, PATH);
}
}
let r = {value};
if(sort)
r.sort = sort;
if(nulls)
r.nulls = nulls;
return r;
});
}
let limitOffset = node.query.SELECT.limit;
let limit,offset;
if(limitOffset !== undefined) {
let limitPath = selectPath.concat("limit");
if(limitOffset.rows)
limit = augmentNumber(limitOffset.rows.val, limitPath.concat("rows","val"), U.WILO_LAST);
if(limitOffset.offset)
offset = augmentNumber(limitOffset.offset.val, limitPath.concat("offset","val"), U.WILO_LAST);
}
let excluding = node.query.SELECT.excluding
if(excluding != undefined) {
let r = Object.create(null);
excluding.forEach((X,iX) => {
let location = U.newLocation(selectPath.concat("excluding",iX));
r[X] = {
name: {id:X,location},
location
}
})
excluding = r;
}
let R = {
op: {val:"query", location},
location,
elements,
from:[ {
path:src,
location: locationFrom
}]
};
if (fromName)
R.from[0].name = fromName;
// set optional properties
if(all)
R.all=all;
if(where)
R.where=where;
if(orderBy)
R.orderBy=orderBy;
if(limit)
R.limit=limit;
if(offset)
R.offset=offset;
if(excluding)
R.exclude = excluding;
node[name] = R;
}) // forEach
}, (path,obj) => { // check function
let le = U.getLastElement(path)
if(le[0]==="@")
return false; // do not walk annotations
if(U.isAugmented(obj)) {
return false;
}
return true;
}) // walkWithPath
}
function modifyAnnotation(node, key, path) {
node[key] = newAnnotation(key, node[key], path);
function newAnnotation(anAnno, aValue, path) {
if (aValue instanceof Object && 'literal' in aValue)
throw new Error('strange re-run')
let ret = {};
if(aValue!==true && aValue!==undefined)
ret = newValue(aValue, path.concat(anAnno));
ret.name = { location: U.newLocation(path.concat(anAnno), U.WILO_FIRST) };
return ret;
}
//returns the last element of an array
function getLastElement(a) {
return a[a.length-1];
}
return {
let transformers = {
newValue,
///////
'@': modifyAnnotation,
'$': ignore,
$inferred: a => a && true,
annotationAssignments: ignore, // TODO: make it $annotations

@@ -519,11 +393,11 @@ artifacts: ignore, // almost just $artifacts

// future ------------------------------------------------------------------
extensions: annotateExtensions, // TODO: list non-applied
extensions: ignore, // TODO: list non-applied
quoted: ignore, // really? What about CURRENT_DATE vs "CURRENT_DATE"?
// members -----------------------------------------------------------------
actions: nonEmptyDict,
elements: insertOrderDict,
enum: insertOrderDict,
actions: ignore,
elements: ignore,
enum: ignore,
keys: (node, name, path) => arrayAsDict(node, name, path, 'foreignKeys'),
mixin: insertOrderDict, // only in queries with special handling
params: insertOrderDict,
mixin: ignore, // only in queries with special handling
params: ignore,
// different XSN later -----------------------------------------------------

@@ -540,3 +414,3 @@ calculated: ignore, // later in name: $inferred: 'as'

dbType: modifyValue, // TODO: currently with --hana-flavor only
default: modifyValueOrExpression,
default: modifyExpression,
impl: ignore, // obsolete - remove

@@ -549,3 +423,3 @@ key: modifyBoolean,

masked: modifyValue,
returns: standard, // storing the return type of actions
returns: ignore, // storing the return type of actions
// type properties ---------------------------------------------------------

@@ -572,11 +446,11 @@ cardinality,

all: ignore, // should not occur
exclude: (excl, csn) => { csn.excluding = Object.keys(excl); }, // XSN TODO: exclude->excluding
groupBy: modifyGroupBy,
having: condition,
exclude: ignore,
groupBy: ignore,
having: ignore,
limit: ignore, // TODO XSN: include offset
offset: ignore, // TODO XSN: move into `limit`
orderBy: ignore, // TODO XSN: make `sort` and `nulls` sibling properties
query,
query: modifyQuery,
value: ignore, // do not list for select items as elements
where: condition,
where: ignore,
// special HANA CDS featues ------------------------------------------------

@@ -586,5 +460,21 @@ sequenceOptions: ignore, // TODO: currently not in the JSON by HANA

modifyNumber,
//
augmentElement,
augmentAction,
augmentEnumItem,
augmentParam,
augmentDefinition,
augmentExtension,
augmentTypeRef,
walkAndAugment
}
// augment query instance
AQ = require("./augmentor3query")(U,{walkAndAugment,transformers});
newValue=AQ.newValue; // newValue shortcut
return transformers;
}
module.exports = createInstance;

@@ -18,3 +18,9 @@ function newUtils(model) {

newLocation: function (path, which=this.WILO_FULL) {
let L = this._newLocation(path,which)
this.setAugmented(L)
return L;
},
_newLocation: function (path, which=this.WILO_FULL) {
function point(offset, line, column) {

@@ -60,2 +66,7 @@ return {offset, line, column};

unsetAugmented: function (node) {
if(!(delete node.augmented))
throw Error("unsetAugmented failed");
},
setAugmented: function (node) {

@@ -70,2 +81,3 @@ // hidden property "augmented" which prevents recursive augmentation

writable: false,
configurable: true,
value: true

@@ -72,0 +84,0 @@ });

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

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

@@ -51,2 +52,4 @@ kind: filterKind,

query: compactQuery,
from: n => n.map( compactWithAbsolute ),
args: n => n.map( compactWithAbsolute ),
path: compactPath,

@@ -80,3 +83,3 @@ quoted: ignore, // really? What about CURRENT_DATE vs "CURRENT_DATE"?

onCond : compactCondOrExpr,
on: (x => (typeof x === 'string') ? x : compactCondOrExpr(x)),
on: (x => (typeof x === 'string') ? undefined : compactCondOrExpr(x)),
where: compactCondOrExpr,

@@ -88,3 +91,2 @@ groupBy: compactCondOrExpr,

redirected: asBool,
impl: i => i.id,
opLocation: ignore, // TODO: clarify expression parser depth and the locations to the nodes

@@ -220,2 +222,9 @@ op: compactOp,

function compactWithAbsolute( node ) {
let r = compactNode( node );
if (node._artifact)
r.absolute = node._artifact.name.absolute;
return r;
}
// Compact a condition or expression tree

@@ -307,7 +316,9 @@ // TODO: there should be nothing special about it, i.e. performed by compactNode()

let model = compact( ...args );
let result = Object.create(null);
let definitions = model.definitions;
for (let k of Object.keys( definitions ).sort())
result[k] = normalizeNode( definitions[k] );
model.definitions = result;
if (model.definitions) {
let result = Object.create(null);
let definitions = model.definitions;
for (let k of Object.keys( definitions ).sort())
result[k] = normalizeNode( definitions[k] );
model.definitions = result;
}
return model;

@@ -314,0 +325,0 @@ }

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

function cbForeignKey(/*O*/) {
return [
]
}
function directItems(items) {

@@ -46,10 +41,2 @@ if(items!==undefined) {

function cbArtifact(O) {
return [
nullProto(O.artifacts, cbArtifact),
nullProto(O.elements, cbElement),
nullProto(O.actions, cbAction),
]
}
function cbParam(/*O*/) {

@@ -71,5 +58,3 @@ return [

nullProto(O.elements, cbElement),
nullProto(O.foreignKeys, cbForeignKey),
nullProto(O.enum, cbEnum),
nullProto(O.artifacts, cbArtifact),
nullProto(O.actions, cbAction),

@@ -86,3 +71,2 @@ nullProto(O.params, cbParam),

nullProto(O.enum, cbEnum),
nullProto(O.artifacts, cbArtifact),
nullProto(O.actions, cbAction),

@@ -105,3 +89,3 @@ nullProto(O.params, cbParam),

nullProto(model.definitions),
nullProto(model.definitions);
W.walkNodesExFn(model.definitions, cbDefinition);

@@ -108,0 +92,0 @@ W.walkNodesExFn(model.extensions, cbExtension);

@@ -1041,2 +1041,5 @@ //Notes:

},
"origin": {
"type": "string"
},
"kind": {

@@ -1043,0 +1046,0 @@ "const": "enum",

@@ -150,2 +150,8 @@ //Notes:

},
"abstract": {
"type": "boolean"
},
"includes": {
"type": "array"
},
"actions": {

@@ -496,3 +502,6 @@ "$ref": "#/schemas/actions"

"type": {
"type": "string"
"type": [
"string",
"object"
]
},

@@ -554,2 +563,5 @@ "length": {

},
"cardinality": {
"$ref": "#/schemas/cardinality"
},
"localized": {

@@ -721,27 +733,3 @@ "type": "boolean"

"cardinality": {
"type": [
"object",
"array"
],
"properties": {
"min": {
"type": [
"integer",
"string"
]
},
"max": {
"type": [
"integer",
"string"
]
},
"src": {
"type": [
"integer",
"string"
]
}
},
"additionalProperties": false
"$ref": "#/schemas/cardinality"
},

@@ -768,3 +756,3 @@ "viaTransform": {"type":"object"}, // TODO remove

"items": {
"oneOf": [
"anyOf": [
{

@@ -775,5 +763,44 @@ "$ref": "#/schemas/simpleItem"

"$ref": "#/schemas/directElements"
},
{
"$ref": "#/schemas/reference"
}
]
},
"reference": {
"type":"object",
"properties": {
"type": {
"$ref": "#/schemas/typeReference"
}
},
"additionalProperties": false
},
"cardinality": {
"type": [
"object",
"array"
],
"properties": {
"min": {
"type": [
"integer",
"string"
]
},
"max": {
"type": [
"integer",
"string"
]
},
"src": {
"type": [
"integer",
"string"
]
}
},
"additionalProperties": false
},
"basicTypes": {

@@ -780,0 +807,0 @@ "string": {

@@ -45,12 +45,17 @@ /**

function validateCSN(csn, options) {
let avjOptions = defaultOptions;
let ajvOptions;
if(options && options.ajv) {
avjOptions = Object.assign({}, defaultOptions, options.ajv)
ajvOptions = Object.assign({}, defaultOptions, options.ajv)
} else {
ajvOptions = Object.assign({}, defaultOptions);
}
if(options && options.fuzzyCsn) {
ajvOptions.removeAdditional = true
}
var ajv = new Ajv(ajvOptions);
var ajv = new Ajv(avjOptions);
let schemaFile = "./CSN.json";
if(csn.version && csn.version.csn === "0.1.99" || options && options.newCsn)
if(csn.version && csn.version.csn === "0.1.99" || options && options.newCsn) {
schemaFile = "./CSN2.json";
}

@@ -57,0 +62,0 @@ let txt = readModuleTextFile(schemaFile);

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

const { queryOps } = require('../base/model');
const { locationString } = require('../base/messages');

@@ -19,17 +20,11 @@ // in main:

var csn_gensrc = true; // good enough here...
var mode_strict = false; // whether to dump with unknown properties (in standard)
const transformers = {
'@': value,
'$': ignore,
annotationAssignments: ignore, // TODO: make it $annotations
artifacts: ignore, // almost just $artifacts
blocks: ignore, // TODO: make it $blocks
indexNo: ignore, // TODO XSN: remove
queries: ignore, // TODO: make it $queries
location: ignore,
// future ------------------------------------------------------------------
quoted: ignore, // really? What about CURRENT_DATE vs "CURRENT_DATE"?
// members -----------------------------------------------------------------
// definitions, extensions, members ----------------------------------------
definitions: sortedDict,
extensions: standard, // is array - TODO: sort
kind,
name: ignore,
actions: nonEmptyDict,

@@ -41,27 +36,7 @@ elements,

params: insertOrderDict,
// different XSN later -----------------------------------------------------
_typeIsExplicit: ignore,
calculated: ignore, // later in name: $inferred: 'as'
implicitForeignKeys: ignore, // later in assoc: $inferred: { foreignKeys: 'fk' } or $inferred on each fk
origin: ignore, // remove (introduce non-enum _origin link)
projection: ignore, // later in entity: $syntax: 'projection'
source: ignore, // remove
viaAll: ignore, // TODO remove, later in elem: $inferred: '*'
// general properties of constructs ----------------------------------------
abstract: value,
dbType: value, // TODO: currently with --hana-flavor only
default: expression,
impl: a => a, // not yet obsolete - still required by toHana (FIXME: maybe rename to $impl, or try to keep the annotation?)
_ignore: a => a, // not yet obsolete - still required by toHana (FIXME: maybe rename to $ignore, or use an annotation instead?)
key: value,
localized: value,
kind: filterKind,
name: annotationName,
virtual: value,
notNull: value,
masked: value,
returns: standard, // storing the return type of actions
// type properties ---------------------------------------------------------
cardinality: standard,
cardinality: standard, // sub: src, min, max
includes: arrayOf( artifactRef ), // also entities
items: standard,
length: value,

@@ -71,29 +46,62 @@ on: (cond) => (typeof cond === 'string' ? undefined : condition( cond )),

precision: value,
redirected: ignore, // TODO: no need for this
scale: value,
target: artifactRef,
type: artifactRef,
items: standard,
typeArguments: ignore,
// inner properties --------------------------------------------------------
path: ignore, // should not occur
sourceMax: renameTo( 'src', value ), // TODO XSN: rename?
targetMin: renameTo( 'min', value ),
targetMax: renameTo( 'max', value ),
targetElement: ignore, // special display of foreign key
// queries -----------------------------------------------------------------
// general properties of constructs ----------------------------------------
abstract: value,
dbType: value, // TODO: currently with --hana-flavor only
default: expression,
key: value,
localized: value,
masked: value,
notNull: value,
// targetElement: ignore, // special display of foreign key, renameTo: select
value: enumValue, // do not list for select items as elements
virtual: value,
// queries, expressions ----------------------------------------------------
query,
from: fromOld, // XSN TODO just one (cross if necessary)
quantifier: ( q, csn ) => { csn[ q.val ] = true; },
columns,
exclude: renameTo( 'excluding', Object.keys ), // XSN TODO: exclude->excluding
groupBy: arrayOf( expression ),
having: condition,
limit, // TODO XSN: include offset
offset: ignore, // TODO XSN: move into `limit`
limit, // TODO XSN: include offset
offset: ignore, // TODO XSN: move into `limit`
orderBy: arrayOf( orderBy ), // TODO XSN: make `sort` and `nulls` sibling properties
query,
value: enumValue, // do not list for select items as elements
where: condition,
// special HANA CDS featues ------------------------------------------------
sequenceOptions: ignore, // TODO: currently not in the JSON by HANA
technicalConfig
sequenceOptions: ignore, // TODO: currently not in the JSON by HANA
technicalConfig, // TODO: spec, re-check
// Old-style XSN/CSN -------------------------------------------------------
indexNo: ignore, // TODO XSN: remove
origin: ignore, // remove (introduce non-enum _origin link)
projection: ignore, // later in entity: $syntax: 'projection'
source: ignore, // remove
// protected (non-public) --------------------------------------------------
//'_' not here, as non-enumerable properties are not transformed anyway
'$': ignore,
artifacts: ignore, // well-introduced, hence not $artifacts
location: ignore, // TODO: think about $location with flat struct (w/o offset)
annotationAssignments: ignore, // FIXME: make it $annotations
blocks: ignore, // FIXME: make it $blocks
queries: ignore, // FIXME: make it $queries (flat)
typeArguments: ignore, // FIXME: make it $typeArgs
// protected - to be subsumed by $inferred ---------------------------------
_typeIsExplicit: ignore,
calculated: ignore, // later in name: $inferred: 'as'
implicitForeignKeys: ignore, // later in assoc: $inferred: { foreignKeys: 'fk' } or $inferred on each fk
redirected: ignore, // TODO: no need for this
viaAll: ignore, // TODO remove, later in elem: $inferred: '*'
// protected created by transformers ---------------------------------------
_containerEntity: n => n, // FIXME: prop starting with _ is link and non-enumerable
_ignore: a => a, // not yet obsolete - still required by toHana (FIXME: maybe rename to $ignore, or use an annotation instead?)
_ignoreMasked: standard, // FIXME: prop starting with _ is link and non-enumerable
_isToContainer: standard, // FIXME: prop starting with _ is link and non-enumerable
generatedFieldName: renameTo( '$generatedFieldName', n => n ), // TODO: XSN name
viaTransform: standard, // FIXME: not a standard prop, start with $
}

@@ -121,4 +129,4 @@

function compactModel( model, options = model.options || {} ) {
csn_gensrc = options.disablePropagate;
//strict = options.testMode;
csn_gensrc = options.toCsn && options.toCsn.gensrc;
mode_strict = options.testMode;
let csn = {};

@@ -152,6 +160,11 @@ set( 'definitions', csn, model );

// in definitions (without redef) with potential inferred elements:
if (!(art instanceof Array) && art.elements && (art.query || art.includes)) {
let elements = inferred( art.elements );
if (!(art instanceof Array) && art.elements &&
(art.query || art.includes || art.$inferred)) {
let annos = art.$inferred && annotations( art, true );
let elements = inferred( art.elements, art.$inferred );
let annotate = Object.assign( { annotate: name }, annos );
if (Object.keys( elements ).length)
extensions.push( { annotate: name, elements } );
annotate.elements = elements;
if (Object.keys( annotate ).length > 1)
extensions.push( annotate );
}

@@ -163,11 +176,11 @@ }

function inferred( elements ) {
function inferred( elements, inferredParent ) {
let ext = Object.create(null);
for (let name in elements) {
let elem = elements[name];
if (elem instanceof Array || !elem.$inferred)
if (elem instanceof Array || !inferredParent && !elem.$inferred)
continue;
let csn = annotations( elem, true );
if (Object.keys(csn).length)
ext[name] = Object.assign( { kind: 'annotate' }, csn );
ext[name] = csn;
}

@@ -186,4 +199,6 @@ return ext;

let transformer = transformers[prop] || transformers[prop.charAt(0)];
// TODO: complain if falsy with strict
// Apply transformer, or use standard() if there is none
if (mode_strict && !transformer) {
let loc = node[prop] && node[prop].location || node.location;
throw new Error( `Unexpected property ${prop} in ${ locationString(loc) }`);
}
let sub = (transformer || standard)( node[prop], csn, node, prop );

@@ -276,4 +291,7 @@ if (sub !== undefined)

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

@@ -283,18 +301,16 @@ return standard( art );

function filterKind( kind, csn, node ) {
if (kind === 'view') // XSN TODO: kind: 'entity', $syntax: 'view'
function kind( k, csn, node ) {
if (!node._main && ['annotate', 'extend'].includes( k )) {
// We just use `name.absolute` because it is very likely a "constructed"
// extensions. The CSN parser must produce name.path like for other refs.
csn[k] = node.name.absolute || artifactRef( node.name, true );
return undefined;
}
if (k === 'view') // XSN TODO: kind: 'entity', $syntax: 'view'
return 'entity';
if (['element', 'key', 'enum', 'annotate'].includes(kind) ||
'extend' === kind && !node._main)
if (['element', 'key', 'enum', 'annotate'].includes(k))
return undefined;
return kind;
return k;
}
function annotationName( name, csn, node ) {
if (!node._main && ['annotate', 'extend'].includes( node.kind ))
// We just use `name.absolute` because it is very likely a "constructed"
// extensions. The CSN parser must produce name.path like for other refs.
csn[node.kind] = name.absolute || artifactRef( name, true );
}
function artifactRef( node, terse ) {

@@ -388,2 +404,6 @@ if (node.$inferred && csn_gensrc)

const magicFunctions = // TODO: calculate from compiler/builtins.js (more with HANA?):
['CURRENT_DATE','CURRENT_TIME','CURRENT_TIMESTAMP','CURRENT_USER','SESSION_USER'];
// TODO: quoted magic names like $now should be complained about in the compiler
function expression( node ) {

@@ -399,5 +419,18 @@ if (typeof node === 'string')

}
if (node.scope === 'param') {
if (node.path)
return { param: true, ref: node.path.map( pathItem ) };
else
return { param: true, ref: [ node.param.val ] };
}
if (node.path) {
// TODO: param/global
return { ref: node.path.map( pathItem ) };
// TODO: global
if (node.path.length !== 1)
return { ref: node.path.map( pathItem ) };
let item = pathItem( node.path[0] );
if (typeof item === 'string' && !node.path[0].quoted &&
magicFunctions.includes( item.toUpperCase() )) {
return { func: item };
}
return { ref: [item] };
}

@@ -413,3 +446,6 @@ if (node.literal) {

if (node.func) { // TODO XSN: remove op: 'call', func is no path
return { func: node.func.path[0].id, args: args( node.args || node.namedArgs ) };
let call = { func: node.func.path[0].id };
if (node.args || node.namedArgs) // no args from CSN input for CURRENT_DATE etc
call.args = args( node.args || node.namedArgs );
return call;
}

@@ -467,3 +503,3 @@ if (queryOps[ node.op.val ])

!args[0].all === !node.all && args[0].args)
args = [ ...args[0], ...args.slice(1) ]
args = [ ...args[0].args, ...args.slice(1) ]
if (node.op.val === 'unionAll') // TODO grammar: set DISTINCT - quantifier: 'all'|'distinct'

@@ -476,10 +512,4 @@ csn.all = true;

set( 'mixin', csn, node );
set( 'columns', csn, node );
set( 'quantifier', csn, node );
if (node.elements && (!node.all || node.all.val !== 'implicit')) {
let columns = (node.all) ? ['*'] : [];
for (let name in node.elements)
addElementAsColumn( node.elements[name], columns );
if(columns.length>0)
csn.columns = columns
}
set( 'exclude', csn, node ); // XSN TODO: exclude->excluding

@@ -495,2 +525,19 @@ set( 'where', csn, node );

function columns( xsnColumns, csn, xsn ) {
let csnColumns = [];
if (xsnColumns) {
for (let col of xsnColumns) {
if (col.val === '*')
csnColumns.push( '*' );
else
addElementAsColumn( col, csnColumns );
}
}
else { // null = use elements
for (let name in xsn.elements)
addElementAsColumn( xsn.elements[name], csnColumns );
}
return csnColumns;
}
function fromOld( node ) {

@@ -517,3 +564,3 @@ // TODO: currently an array in XSN:

while (node.join === 'cross' && args[0] && args[0].join === node.join && args[0].args)
args = [ ...args[0], ...args.slice(1) ]
args = [ ...args[0].args, ...args.slice(1) ]
let join = { join: joinTrans[node.join] || node.join, args: node.args.map( from ) };

@@ -529,7 +576,8 @@ set( 'on', join, node );

}
else {
let name = node._artifact.name.absolute;
let dot = name.lastIndexOf('.');
return addExplicitAs( artifactRef( node, null ), node.name, name.substring( dot+1 ) );
}
else
return addExplicitAs( artifactRef( node, null ), node.name, function(id) {
let name = node._artifact.name.absolute;
let dot = name.lastIndexOf('.');
return name.substring( dot+1 ) !== id;
});
}

@@ -547,3 +595,8 @@

csn_gensrc = true;
Object.assign( col, addExplicitAs( expression( elem.value ), elem.name ) );
addExplicitAs( Object.assign( col, expression(elem.value) ), elem.name, function(id) {
// $user should be rendered as { ref: ['$user','id'], as: '$user' }
let path = elem.value && elem.value.path;
let last = path[ path.length-1 ];
return (last && last.id) !== id;
});
set( 'key', col, elem );

@@ -583,3 +636,3 @@ if (elem._typeIsExplicit || elem.redirected) { // TODO XSN: introduce $inferred

function addExplicitAs( node, name, implicit ) {
if (name && (!name.calculated && !name.$inferred || implicit && implicit !== name.id))
if (name && (!name.calculated && !name.$inferred || implicit && implicit(name.id) ))
node.as = name.id;

@@ -604,2 +657,3 @@ return node;

csn_gensrc = true;
mode_strict = false;
return q && query( q );

@@ -610,2 +664,3 @@ }

csn_gensrc = true;
mode_strict = false;
return e && expression( e );

@@ -625,5 +680,3 @@ }

if(tc.storeType) {
if(!be.tableSuffix)
be.tableSuffix = [];
be.tableSuffix.push({ xpr: [value(tc.storeType), 'store'] });
be.storeType = value(tc.storeType);
}

@@ -640,3 +693,3 @@ if(tc.extendedStorage) {

if(tc.group.name) {
group.xpr.push('group', 'name ', { ref: [tc.group.name.id] });
group.xpr.push('group', 'name', { ref: [tc.group.name.id] });
}

@@ -710,3 +763,3 @@ if(tc.group.type) {

}
stream.push('index', idx.name.id, 'on', '(');
stream.push('index', { ref: [idx.name.id] }, 'on', '(');
columns(idx.columns, stream);

@@ -717,3 +770,3 @@ stream.push(')');

} else if(idx.kind === 'fulltextindex') {
stream.push('fulltext', 'index', idx.name.id, 'on', '(');
stream.push('fulltext', 'index', { ref: [idx.name.id] }, 'on', '(');
columns(idx.columns, stream);

@@ -786,3 +839,3 @@ stream.push(')');

if(asp.minutes) {
stream.push('every ', expression(asp.minutes), 'minutes');
stream.push('every', expression(asp.minutes), 'minutes');
if(asp.documents) {

@@ -878,6 +931,5 @@ stream.push('or');

if(c.unit)
stream.push(value(c.unit), '(');
stream.push(expression(c));
if(c.unit)
stream.push(')');
stream.push({func: value(c.unit), args: [expression(c)]});
else
stream.push(expression(c));
if(c.sort)

@@ -884,0 +936,0 @@ stream.push(value(c.sort));

@@ -336,3 +336,3 @@ /**

/**
* Loops over all elements in an object and calls the specified callback
* Loops over all object-elements in an object and calls the specified callback
* @param {object} obj

@@ -360,3 +360,3 @@ * @param {forEachObjectCallback} callback

/**
* Loops over all elements in an object and calls the specified callback
* Loops over all leafs in an object and calls the specified callback
* @param {object} obj

@@ -376,2 +376,27 @@ * @param {forEachPropCallback} callback

/**
* Callback of the dmap function called for each leaf it walks
* @callback dmapCallback
* @param {string} name of the node
* @param {object} leaf
* @return resulting object
*/
/**
* Transforms all elements of a dictionary into another.
* Loops over all elements in an object and calls the specified callback.
* The callback function returns the new representation of the passed element.
* @param {object} obj
* @param {dmapCallback} callback
* @return resulting dictionary
*/
function dmap(obj, callback) {
let R = Object.create(null);
for(var key in obj) {
let iobj = obj[key];
R[key] = callback(key,iobj);
}
return R;
}
/**
* Callback of the walkNodesEx function to obtain the next node to walk

@@ -457,2 +482,3 @@ * @callback getNextElements

module.exports = {
dmap,
forEach,

@@ -459,0 +485,0 @@ forEachObject,

@@ -9,3 +9,3 @@ // Wrapper around generated ANTLR parser

var { CompileMessage } = require('../base/messages');
var { getMessageFunction, CompileMessage } = require('../base/messages');
var errorStrategy = require('./errorStrategy');

@@ -20,17 +20,8 @@

super(...args);
this.messages = [];
}
// Push message `msg` with location `loc` to array of errors:
message( msg, loc, severity ) {
this.messages.push( new CompileMessage( loc, msg, severity ) );
}
// method which is called by generated parser:
// method which is called by generated parser with --trace-parser[-amg]:
syntaxError( recognizer, offendingSymbol, line, column, msg, e ) {
var loc = recognizer.tokenLocation( offendingSymbol );
//console.log(e); throw new CompileMessage( loc, msg );
var err = new CompileMessage( loc, msg[0].toUpperCase() + msg.slice(1) );
if (e && e.expectedTokens)
err.expectedTokens = e.expectedTokens;
this.messages.push( err );
if (!(e instanceof CompileMessage)) // not already reported
recognizer.message( null, offendingSymbol, msg );
}

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

parser.filename = filename;
parser.options = options;
parser.$message = getMessageFunction( parser ); // sets parser.messages
initTokenRewrite( parser, tokenStream );
parser.options = options;
parser.filename = filename;
// comment the following 2 lines if you want to output the parser errors directly:

@@ -111,5 +104,2 @@ parser.messageErrorListener = errorListener;

parser.match = errorStrategy.match;
// parser.consume = errorStrategy.consume;
// parser.exitRule = errorStrategy.exitRule;
// parser.epsilon = errorStrategy.epsilon; // for empty alts
parser._interp.predictionMode = antlr4.atn.PredictionMode.SLL;

@@ -134,4 +124,5 @@ // parser._interp.predictionMode = antlr4.atn.PredictionMode.LL_EXACT_AMBIG_DETECTION;

parser.removeErrorListeners();
parser.addErrorListener( errorListener );
parser.avoidErrorListeners = true;
}
parser.addErrorListener( errorListener );

@@ -148,3 +139,3 @@ if (options.parseListener) {

ast.messages = errorListener.messages;
ast.messages = parser.messages;
if (options.attachTokens === true || options.attachTokens === filename)

@@ -151,0 +142,0 @@ ast.tokenStream = tokenStream;

@@ -39,26 +39,5 @@ // Error strategy with special handling for (non-reserved) keywords

// Remember context for potential error message
function epsilon() {
let lt1 = this._input.LT(1);
if (this.state >= 0 && lt1 !== this.$exitToken) {
this.$exitToken = lt1;
this.$exitState = this.state;
this.$exitCtx = this._ctx;
}
}
var SEMI = null;
var RBRACE = null;
function exitRule() {
epsilon.call( this );
antlr4.Parser.prototype.exitRule.call( this );
}
function consume() {
// Unfortunately, ANTLR does not generate "this.state = <endState>" after
// this.consume / this.match if nothing can follow the token. But exitRule
// needs to know whether the token has been consumed or not.
let t = antlr4.Parser.prototype.consume.call( this );
this.state = -1; // illegal state stands for <endState>
return t;
}
// Match current token against token type `ttype` and consume it if successful.

@@ -101,2 +80,3 @@ // Also allow to match keywords as identifiers. This function should be set as

reportMissingToken,
reportIgnoredWith,
// getErrorRecoverySet,

@@ -107,2 +87,3 @@ consumeUntil,

getExpectedTokensForMessage,
getTokenDisplay,
constructor: KeywordErrorStrategy

@@ -112,3 +93,3 @@ });

// Attemp to recover from problems in subrules, except if rule has defined a
// local variable `LeaveLoop` with truthy value
// local variable `_sync` with value 'nop'
function sync( recognizer ) {

@@ -126,11 +107,16 @@ // If already recovering, don't try to sync

var nextTokens = recognizer.atn.nextTokens(s);
// console.log('SYNC:', recognizer._ctx._sync, s.stateType, token.text, intervalSetToArray( recognizer, nextTokens ))
// console.log(antlr4.Token.EPSILON, nextTokens.contains(antlr4.Token.EPSILON),nextTokens)
if (nextTokens.contains(token.type)) { // we are sure the token matches
recognizer.$nextTokensToken = null;
recognizer.$nextTokensState = ATNState.INVALID_STATE_NUMBER;
recognizer.$nextTokensContext = null;
// console.log('REMOVE:',token.type,recognizer.state)
if (token.text === '}' && recognizer.$nextTokensToken !== token &&
nextTokens.contains(SEMI)) {
// if the '}' could be matched alternative to ';', we had an opt ';' (rule requiredSemi)
recognizer.$nextTokensToken = token;
recognizer.$nextTokensState = recognizer.state;
recognizer.$nextTokensContext = recognizer._ctx;
}
return;
}
// TODO: expected token is identifier, current is KEYWORD
if (nextTokens.contains(antlr4.Token.EPSILON)) {

@@ -146,5 +132,4 @@ if (recognizer.$nextTokensToken !== token) {

if (recognizer._ctx.LeaveLoop)
if (recognizer._ctx._sync === 'nop')
return;
switch (s.stateType) {

@@ -156,5 +141,12 @@ case ATNState.BLOCK_START:

// report error and recover if possible
if( this.singleTokenDeletion(recognizer) !== null) {
if( token.text !== '}' && // do not just delete a '}'
this.singleTokenDeletion(recognizer) !== null) { // also calls reportUnwantedToken
return;
} else {
}
else if (recognizer._ctx._sync === 'recover') {
this.reportInputMismatch( recognizer, new InputMismatchException(recognizer) );
this.consumeUntil( recognizer, nextTokens );
return;
}
else {
throw new InputMismatchException(recognizer);

@@ -174,2 +166,6 @@ }

}
// singleTokenInsertion called by recoverInline (called by match / in else),
// calls reportMissingToken
// Report `NoViableAltException e` signalled by parser `recognizer`

@@ -195,8 +191,16 @@ function reportNoViableAlternative( recognizer, e ) {

this.getExpectedTokensForMessage( recognizer, e.offendingToken, deadEnds );
var msg = "Mismatched input " + this.getTokenErrorDisplay(e.offendingToken);
if (expecting) {
msg += " expecting " + expecting.toString(recognizer.literalNames, recognizer.symbolicNames);
e.expectedTokens = intervalSetToArray( recognizer, expecting );
let offending = this.getTokenDisplay( e.offendingToken, recognizer );
let err;
if (expecting && expecting.length) {
err = recognizer.message( 'syntax-mismatched-token', e.offendingToken,
{ offending, expecting: expecting.join(', ') },
'Error', 'Mismatched $(OFFENDING), expecting $(EXPECTING)' );
err.expectedTokens = expecting;
}
recognizer.notifyErrorListeners(msg, e.offendingToken, e);
else { // should not really happen anymore... -> no messageId !
err = recognizer.message( null, e.offendingToken, { offending },
'Error', 'Mismatched $(OFFENDING)' );
}
if (!recognizer.avoidErrorListeners) // with --trace-parser or --trace-parser-ambig
recognizer.notifyErrorListeners( err.message, e.offendingToken, err );
}

@@ -206,14 +210,15 @@

function reportUnwantedToken( recognizer ) {
if (this.inErrorRecoveryMode(recognizer)) {
if (this.inErrorRecoveryMode(recognizer))
return;
}
this.beginErrorCondition(recognizer);
var t = recognizer.getCurrentToken();
var tokenName = this.getTokenErrorDisplay(t);
var expecting = this.getExpectedTokensForMessage( recognizer, t );
var e = new antlr4.error.InputMismatchException( recognizer );
e.expectedTokens = intervalSetToArray( recognizer, expecting );
var msg = "Extraneous input " + tokenName + " expecting " +
expecting.toString(recognizer.literalNames, recognizer.symbolicNames);
recognizer.notifyErrorListeners(msg, t, e);
var token = recognizer.getCurrentToken();
var expecting = this.getExpectedTokensForMessage( recognizer, token );
var offending = this.getTokenDisplay( token, recognizer );
let err = recognizer.message( 'syntax-extraneous-token', token,
{ offending, expecting: expecting.join(', ') },
'Error', 'Extraneous $(OFFENDING), expecting $(EXPECTING)' );
err.expectedTokens = expecting;
if (!recognizer.avoidErrorListeners) // with --trace-parser or --trace-parser-ambig
recognizer.notifyErrorListeners( err.message, token, err );
}

@@ -223,16 +228,27 @@

function reportMissingToken( recognizer ) {
if ( this.inErrorRecoveryMode(recognizer)) {
if ( this.inErrorRecoveryMode(recognizer))
return;
}
this.beginErrorCondition(recognizer);
var t = recognizer.getCurrentToken();
var expecting = this.getExpectedTokensForMessage( recognizer, t );
var e = new antlr4.error.InputMismatchException( recognizer );
e.expectedTokens = intervalSetToArray( recognizer, expecting );
var msg = "Missing " + expecting.toString(recognizer.literalNames, recognizer.symbolicNames) +
" at " + this.getTokenErrorDisplay(t);
recognizer.notifyErrorListeners(msg, t, e);
var token = recognizer.getCurrentToken();
var expecting = this.getExpectedTokensForMessage( recognizer, token );
var offending = this.getTokenDisplay( token, recognizer );
// TODO: if non-reserved keyword will not been parsed as keyword, use Identifier for offending
let err = recognizer.message( 'syntax-missing-token', token,
{ offending, expecting: expecting.join(', ') },
'Error', 'Missing $(EXPECTING) before $(OFFENDING)' );
err.expectedTokens = expecting;
if (!recognizer.avoidErrorListeners) // with --trace-parser or --trace-parser-ambig
recognizer.notifyErrorListeners( err.message, token, err );
}
var SEMI = null;
function reportIgnoredWith( recognizer, t ) {
let next = recognizer._interp.atn.states[ recognizer.state ].transitions[0].target;
recognizer.state = next.stateNumber; // previous match() does not set the state
let expecting = this.getExpectedTokensForMessage( recognizer, t );
let m = recognizer.message( 'syntax-ignored-with', t,
{ offending: "';'", expecting: expecting.join(', ') },
'Warning', `Unexpected $(OFFENDING), expecting $(EXPECTING) - ignored previous WITH` );
m.expectedTokens = expecting;
}

@@ -242,9 +258,28 @@ function consumeUntil( recognizer, set ) {

SEMI = recognizer.literalNames.indexOf( "';'" );
if (SEMI < 1 || set.contains(SEMI)) {
if (RBRACE == null)
RBRACE = recognizer.literalNames.indexOf( "'}'" );
// let s=this.getTokenDisplay( recognizer.getCurrentToken(), recognizer );
if (SEMI < 1 || RBRACE < 1) {
super1.consumeUntil.call( this, recognizer, set );
}
else if (set.contains(SEMI)) { // do not check for RBRACE here!
super1.consumeUntil.call( this, recognizer, set );
// console.log('CONSUMED-ORIG:',s,this.getTokenDisplay( recognizer.getCurrentToken(), recognizer ),recognizer.getCurrentToken().line,intervalSetToArray( recognizer, set ));
}
else {
set.addOne( SEMI );
super1.consumeUntil.call( this, recognizer, set );
// if (recognizer.getTokenStream().LA(1) === SEMI) console.log( 'CONSUME: Semi' )
// DO NOT modify input param `set`, as the set might be cached in the ATN
let stop = new IntervalSet.IntervalSet();
stop.addSet( set );
stop.removeOne( recognizer.constructor.Identifier );
stop.addOne( SEMI );
// I am not that sure whether to add RBRACE...
stop.addOne( RBRACE );
super1.consumeUntil.call( this, recognizer, stop );
if (recognizer.getTokenStream().LA(1) === SEMI ||
recognizer.getTokenStream().LA(1) === RBRACE && !set.contains(RBRACE)) {
recognizer.consume();
this.reportMatch(recognizer); // we know current token is correct
}
// if matched '}', also try to match next ';' (also matches double ';')
if (recognizer.getTokenStream().LA(1) === SEMI) {

@@ -254,2 +289,4 @@ recognizer.consume();

}
// console.log('CONSUMED:',s,this.getTokenDisplay( recognizer.getCurrentToken(), recognizer ),recognizer.getCurrentToken().line);
// throw new Error('Sync')
}

@@ -263,2 +300,4 @@ }

// We now also allow keywords if the Identifier is expected.
// Called by match() and in generated parser in "else part" before consume()
// for ( TOKEN1 | TOKEN2 )
function recoverInline( recognizer ) {

@@ -284,10 +323,7 @@ var identType = recognizer.constructor.Identifier;

function getMissingSymbol( recognizer ) {
var identType = recognizer.constructor.Identifier;
if (!recognizer.isExpectedToken( identType ))
return super1.getMissingSymbol.call( this, recognizer );
var expectedTokenType = this.getExpectedTokens(recognizer).first(); // get any element
var current = recognizer.getCurrentToken();
return recognizer.getTokenFactory().create(
current.source,
identType, '_some_ident_', antlr4.Token.DEFAULT_CHANNEL,
current.source, // do s/th special if EOF like in DefaultErrorStrategy ?
expectedTokenType, '', antlr4.Token.DEFAULT_CHANNEL, // empty string as token text
-1, -1, current.line, current.column);

@@ -304,6 +340,46 @@ }

}
if (recognizer.$nextTokensToken === recognizer.$removeSemiFor)
names = names.filter( n => n !== "';'" && n !== "'}'" );
else if (names.includes("';'"))
names = names.filter( n => n !== "'}'" );
names.sort( (a, b) => tokenPrecedence(a) < tokenPrecedence(b) ? -1 : 1 );
return names;
}
const token1sort = {
// 0: Identifier, Number, ...
// 1: separators:
',': 1, '.': 1, ':': 1, ';': 1,
// 2: parentheses:
'(': 2, ')': 2, '[': 2, ']':2, '{': 2, '}': 2,
// 3: special:
'!': 3, '#': 3, '$': 3, '?': 3, '@': 3,
// 4: operators:
'*': 4, '+': 4, '-': 4, '/': 4, '<': 4, '=': 4, '>': 4, '|': 4,
// 8: KEYWORD
// 9: <EOF>
}
function tokenPrecedence( name ) {
if (name.length < 2 || name === '<EOF>')
return '9' + name;
let prec = token1sort[ name.charAt(1) ];
if (prec)
return '' + prec + name;
else
return (name.charAt(1) < 'a' ? '8' : '0') + name;
}
function getTokenDisplay( token, recognizer ) {
if (!token)
return '<EOF>';
let t = token.type;
if (t === antlr4.Token.EOF || t === antlr4.Token.EPSILON )
return '<EOF>';
else if (token.text === '.') // also for DOTbeforeBRACE
return "'.'";
else
return recognizer.literalNames[t] || recognizer.symbolicNames[t];
}
// Return an IntervalSet of token types which the parser had expected. Do not

@@ -320,3 +396,3 @@ // include non-reserved keywords if not mentioned explicitly (i.e. other than

if (recognizer.state < 0)
return null;
return [];
if (recognizer.state >= atn.states.length)

@@ -329,3 +405,3 @@ throw( 'Invalid state number ' + recognizer.state + ' for ' +

if (!identType || !beforeUnreserved || beforeUnreserved + 2 > identType)
return super1.getExpectedTokens.call( this, recognizer );
return intervalSetToArray( recognizer, super1.getExpectedTokens.call( this, recognizer ) );

@@ -359,3 +435,3 @@ var ll1 = new antlr4_LL1Analyzer(atn);

// console.log(state, recognizer.$nextTokensState, expected.toString(recognizer.literalNames, recognizer.symbolicNames));
return expected;
return intervalSetToArray( recognizer, expected );

@@ -388,10 +464,5 @@ // Add an interval `v` to the IntervalSet `this`. If `v` contains the token

// probably overwrite getTokenErrorDisplay() - use token text
module.exports = {
epsilon,
exitRule,
consume,
match,
KeywordErrorStrategy
};

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

var antlr4 = require('antlr4');
var ATNState = require('antlr4/atn/ATNState').ATNState;
var { addToDictWithIndexNo } = require('../base/dictionaries');

@@ -47,4 +48,7 @@

setOnce,
notYet,
hanaFlavorOnly,
csnParseOnly,
noAssignmentInSameLine,
noSemicolonHere,
isStraightBefore,
constructor: GenericAntlrParser // keep this last

@@ -89,33 +93,81 @@ });

// Push message `msg` with location `loc` to array of errors:
function message( msg, loc, severity ) {
if (!this.options.parseOnly) // TODO: remove this test
this.messageErrorListener.message( msg, loc, severity );
function message( id, loc, ...args ) {
return this.$message( id, // function $message is set in antlrParser.js
(loc instanceof antlr4.CommonToken) ? this.tokenLocation(loc) : loc,
...args );
}
// Push a message to the array of errors complaining that language construct
// 'feature' is not yet supported (using the location `loc`, which can also be a token for its locatio'), unless
// option 'parseOnly' or any of the options from 'optionsArray' are set.
function notYet( feature, loc, optionsArray=[] ) {
if (this.options.parseOnly) {
// Generally ignore if only parsing
// Use the following function for language constructs which we (currently)
// just being able to parse, in able to run tests from HANA CDS. As soon as we
// create ASTs for the language construct and put it into a CSN, a
// corresponding check should actually be inside the compiler, because the same
// language construct can come from a CSN as source.
// TODO: this is not completely done this way
function hanaFlavorOnly( text, ...tokens ) {
if (!text || this.options.hanaFlavor)
return;
if (typeof text !== 'string') {
tokens = [ text, ...tokens ];
text = tokens.map( t => t.text.toUpperCase() ).join(' ') + ' is not supported';
}
for (let option of optionsArray) {
// Grammar says to ignore for this option
if (this.options[option]) {
return;
}
this.message( null, this.tokenLocation( tokens[0], tokens[ tokens.length-1 ] ), text );
}
// Use the following function for language constructs which we (currently) do
// not really compile, just use to produce a CSN for functions parseToCqn() and
// parseToExpr().
function csnParseOnly( text, ...tokens ) {
if (!text || this.options.parseOnly)
return;
if (typeof text !== 'string') {
tokens = [ text, ...tokens ];
text = tokens.map( t => t.text.toUpperCase() ).join(' ') + ' is not supported';
}
this.messageErrorListener.message( `${feature} not supported yet`,
(loc instanceof antlr4.CommonToken) ? this.tokenLocation(loc) : loc );
this.message( null, this.tokenLocation( tokens[0], tokens[ tokens.length-1 ] ), text );
}
function noSemicolonHere() {
let handler = this._errHandler;
var t = this.getCurrentToken();
if (t.text === ';')
this.messageErrorListener.message( `Unexpected ';' - previous keyword 'with' is ignored`,
this.tokenLocation(t), 'Warning' );
// TODO remove ';' from set of expected tokens
this.$removeSemiFor = t;
this.$nextTokensToken = t;
this.$nextTokensContext = null; // match() of WITH does not reset
this.$nextTokensState = ATNState.INVALID_STATE_NUMBER;
if (t.text === ';' && handler && handler.reportIgnoredWith ) {
handler.reportIgnoredWith( this, t );
}
}
// // Special function for rule `requiredSemi` before return $ctx
// function braceForSemi() {
// if (RBRACE == null)
// RBRACE = this.literalNames.indexOf( "'}'" );
// console.log(RBRACE)
// // we are called before match('}') and this.state = ...
// let atn = this._interp.atn;
// console.log( atn.nextTokens( atn.states[ this.state ], this._ctx ) )
// let next = atn.states[ this.state ].transitions[0].target;
// // if a '}' is not possible in the grammar after the fake-'}', throw error
// if (!atn.nextTokens( next, this._ctx ).contains(RBRACE))
// console.log( atn.nextTokens( next, this._ctx ) )
// // throw new antlr4.error.InputMismatchException(this);
// }
function noAssignmentInSameLine() {
var t = this.getCurrentToken();
if (t.text === '@' && t.line <= this._input.LT(-1).line)
this.message( 'syntax-anno-same-line', t, {},
'Warning', `Annotation assignment belongs to next statement` );
}
// Use after matching ',' to allow ',' in front of the closing paren. Be sure
// that you know what to do if successful - break/return/... = check the
// generated grammar; inside loops, you can use `break`. This function is
// still the preferred way to express an optional ',' at the end, because it
// does not influence the error reporting. It might also allow to match
// reserved keywords, because there is no ANTLR generated decision in front of it.
function isStraightBefore( closing ) {
return this.getCurrentToken().text === closing;
}
// Attach location matched by current rule to node `art`. If a location is

@@ -170,2 +222,4 @@ // already provided, only set the end location. Use this function only

function combinedLocation( start, end ) {
if (!start || !start.location)
start = { location: this.startLocation() };
return {

@@ -182,2 +236,4 @@ filename: start.location.filename,

var id = token.text;
if (token.type !== this.constructor.Identifier && !/^[a-zA-Z]+$/.test( id ))
id = '';
if (token.text[0] !== '"')

@@ -188,4 +244,4 @@ return { id, location: this.tokenLocation( token ) };

if (!id) {
this.message( "Quoted identifier must contain at least one character",
this.tokenLocation( token ) );
this.message( 'syntax-empty-ident', token, {},
'Error', 'Quoted identifier must contain at least one character' );
}

@@ -207,5 +263,10 @@ return { id, quoted: true, location: this.tokenLocation( token ) };

}
var num = Number.parseFloat( text ); // not Number.parseInt() !
var val = (Number.isSafeInteger(num)) ? num : text;
return { literal: 'number', val, location };
var num = Number.parseFloat( text||'0' ); // not Number.parseInt() !
if (!Number.isSafeInteger(num)) {
if (sign != null)
return { literal: 'number', val: text, location };
this.message( 'syntax-no-integer', token, {},
'Error', 'An integer number is expected here' );
}
return { literal: 'number', val: num, location };
}

@@ -225,4 +286,6 @@

if (p.test_fn && !p.test_fn(val) || p.test_re && !p.test_re.test(val))
this.message( p.test_msg, location );
// TODO: make tests available for CSN parser
if ((p.test_fn && !p.test_fn(val) || p.test_re && !p.test_re.test(val)) &&
!this.options.parseOnly)
this.message( null, location, p.test_msg ); // TODO: message id

@@ -233,7 +296,7 @@ if (p.unexpected_char)

if (~idx) {
this.message( p.unexpected_msg, {
this.message( null, { // TODO: message id
filename: location.filename,
start: atChar( idx ),
end: atChar( idx + (val[idx] == '\'' ? 2 : 1) )
} );
}, p.unexpected_msg );
}

@@ -294,3 +357,3 @@ }

}
else if (kind) {
else if (kind || this.options.parseOnly) {
addToDictWithIndexNo( parent, env, art.name.id, art );

@@ -302,7 +365,10 @@ }

if (kind === 0)
this.message( `Duplicate value for view parameter "${name}"`, loc );
this.message( 'duplicate-argument', loc, { name },
'Error', 'Duplicate value for parameter $(NAME)' );
else if (kind === '')
this.message( `Duplicate EXCLUDING for source element "${name}"`, loc );
this.message( 'duplicate-excluding', loc, { name },
'Error', 'Duplicate EXCLUDING for source element $(NAME)' );
else
this.message( `Duplicate assignment for structure property "${name}"`, loc );
this.message( 'duplicate-prop', loc, { name },
'Error', 'Duplicate value for structure property $(NAME)' );
} );

@@ -352,3 +418,3 @@ }

if (val != null &&
(typeof val !== "object" ||
(typeof val !== 'object' ||
(val instanceof Array ? val.length : Object.getOwnPropertyNames(val).length) ) ) {

@@ -376,3 +442,4 @@ target[key] = val;

if (prev) {
this.message( `Option ${prev.option} has already been specified`, loc );
this.message( 'syntax-repeated-option', loc, { option: prev.option },
'Error', 'Option $(OPTION) has already been specified' );
}

@@ -379,0 +446,0 @@ if (typeof value === 'boolean') {

@@ -30,6 +30,6 @@ // Main entry point for the Research Vanilla CDS Compiler

// 0.0.1 : Used by HANA CDS for its CSN output (incomplete, not well defined, quite different from CDX ...)
// 0.0.2 : CDX in the initial versions with old-style CSN, default for SQL name mapping is 'deep'
// 0.0.2 : CDX in the initial versions with old-style CSN, default for SQL name mapping is 'quoted'
// 0.0.99 : Like 0.0.2, but with new-style CSN
// Versions that are currently produced by compiler:
// 0.1.0 : Like 0.0.2, default for SQL name mapping is 'flat'
// 0.1.0 : Like 0.0.2, default for SQL name mapping is 'plain'
// 0.1.99 : Like 0.1.0, but with new-style CSN

@@ -43,3 +43,4 @@ function csnVersion( options ) {

var { CompilationError, messageString, handleMessages, hasErrors } = require('./base/messages');
var { CompilationError, messageString, handleMessages, hasErrors, getMessageFunction }
= require('./base/messages');
var { promiseAllDoNotRejectImmediately } = require('./base/node-helpers');

@@ -57,9 +58,7 @@

const { compactModel, compactQuery, compactExpr } = require('./json/to-csn')
var generateExts = require('./i18n/generate-extensions'); // extensions from an properties file
var fs = require('fs');
var { postProcessForBackwardCompatibility } = require('./transform/forOdata');
var { getDefaultTntFlavorOptions } = require('./transform/tntSpecific');
var { compactForService } = require('./transform/forOdata');
var { getDefaultTntFlavorOptions, propagateIncludesForTnt } = require('./transform/tntSpecific');
var csn2edm = require('./edm/csn2edm');
const emdx2csn = require('./edm/annotations/edmx2csnNew'); // translate edmx annotations into csn
var alerts = require('./base/alerts');
const { mergeOptions } = require('../lib/model/modelUtils');

@@ -79,5 +78,3 @@ const { setProp } = require('./base/model');

let ext = path.extname( filename ).toLowerCase();
if (ext === '.properties')
return generateExts( source, filename, options );
else if (ext === '.xml')
if (ext === '.xml')
return emdx2csn( source, filename, options );

@@ -88,4 +85,5 @@ else if (['.json', '.csn'].includes(ext)) {

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

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

else {
var model = { messages: [] };
const { signal, error } = alerts( model );
//console.log(`Unknown file extension '${ext}'`, filename);
signal( error`Unknown file extension '${ext}'`,
{ filename, start: { offset: 0, line: 1, column: 1 } } );
let model = {};
const message = getMessageFunction( model );
message( 'file-unknown-ext',
{ filename, start: { offset: 0, line: 1, column: 1 } },
{ file: ext && ext.slice(1), '#': !ext && 'none' },
'Error', {
std: 'Unknown file extension $(FILE)',
none: 'No file extension'
} );
return model;

@@ -148,3 +150,3 @@ }

var messagesArray = [[]];
const { signal, error } = alerts( { messages: messagesArray[0] } );
const message = getMessageFunction({ messages: messagesArray[0] });

@@ -164,3 +166,3 @@ var all = promiseAllDoNotRejectImmediately( a.files.map(readAndParse) );

return all.then( function() {
moduleLayers.setLayers( a.sources, a.files );
moduleLayers.setLayers( a.sources );
for (let name in a.sources)

@@ -375,11 +377,19 @@ messagesArray.push( a.sources[name].messages || [] );

for (let from of dep.usingFroms)
signal( error`Cannot read file '${resolved}'`, from.location );
message( 'file-not-readable', from.location, { file: resolved },
'Error', 'Cannot read file $(FILE)' );
}
else if (/^\.\.?\//.test( dep.module ) ) {
for (let from of dep.usingFroms)
signal( error`Cannot find module '${dep.module}'`, from.location );
message( 'file-unknown-local', from.location, { file: dep.module },
'Error', 'Cannot find local module $(FILE)' );
}
else {
let internal = /[\\/]/.test( dep.module ) && 'internal';
for (let from of dep.usingFroms)
signal( error`Cannot find module - do you mean './${dep.module}'?`, from.location );
message( 'file-unknown-package', from.location,
{ file: dep.module, '#': internal },
'Error', {
std: 'Cannot find package $(FILE)',
internal: 'Cannot find package module $(FILE)'
} );
}

@@ -437,5 +447,2 @@ return false;

let ast = parse( source, filename, options );
if (sourcesDict.dependencies) {
ast.dependencies = [];
}
sources[filename] = ast;

@@ -453,10 +460,13 @@ ast.filename = filename;

for (let filename in sourcesDict.dependencies) {
let dependency = sourcesDict.dependencies[filename];
for(let val in dependency) {
sources[filename].dependencies.push({ literal: 'string', val, realname: dependency[val] /*, location: ???*/});
let dependency = sourcesDict.dependencies[ filename ];
for (let val in dependency) {
let dep = { literal: 'string', val, realname: dependency[val] /*, location: ???*/};
let arr = sources[filename].dependencies;
if (arr)
arr.push( dep );
else
sources[filename].dependencies = [ dep ];
}
}
if (sourcesDict.files) {
moduleLayers.setLayers( sources, sourcesDict.files );
}
moduleLayers.setLayers( sources );

@@ -487,10 +497,8 @@ return compileDo( sources, messagesArray, options, sourcesDict );

handleMessages( model ); // stop compilation with errors
if (options.lintMode)
return model;
semanticChecks(model);
handleMessages( model );
if (options.newCsn)
model = propagator.propagate( model );
else if (!options.disablePropagate)
// TODO use new-style propagate also for TnT once actions/functions are propagated correctly
model = ((options.oldPropagate || options.tntFlavor) ? propagator.propagateAssignments : propagator.propagate)( model );
model = propagator.propagate( model );
if (!options.modelExtender || '$draft.cds' in sources)

@@ -540,13 +548,4 @@ return model;

function generateExtensions(source, options = {}) {
let content = options.toExtensions ?
fs.readFileSync(source).toString()
: source;
let srcLoc = options.toExtensions ? source : '<code>';
return generateExts(content, srcLoc);
}
// TNT-specific, temporary: Transforms augmented CSN 'model' into an object '{ annotations, metadata, csn, alerts, services }'
// TNT-specific, temporary: Transforms augmented CSN 'model' into an object '{ annotations, metadata, csn, services }'
// containing
// FIXME: Please document what callers should expect in 'alerts'
// - 'annotations': (for backward compatibility only): the 'annotations' property of the first entry in 'services'

@@ -592,6 +591,10 @@ // - 'metadata': (for backward compatibility only): the 'metadata' property of the first entry in 'services'

if (!options.tntFlavor.skipPropagatingIncludes) {
propagateIncludesForTnt(result.csn);
}
// FIXME: For backward compatibility, replace the 'annotations.xml' in all services with the V4 version
// (unfortunately we used to deliver this really as a V4 version, which was probably unnecessary ...)
for (let serviceName in result.services) {
let forOdata = postProcessForBackwardCompatibility(odataResult._augmentedCsn, serviceName);
let forOdata = compactForService(odataResult._augmentedCsn, serviceName);
let l_annotations_edm = csn2edm(forOdata, mergeOptions(options, { toOdata : { version : 'v4' }}));

@@ -640,3 +643,3 @@ result.services[serviceName].annotations = l_annotations_edm.toXML('annotations');

function parseToCqn( cdl, filename = '<query>.cds', options = {} ) {
let xsn = parseLanguage( cdl, filename, options, 'query' );
let xsn = parseLanguage( cdl, filename, Object.assign( {parseOnly:true}, options ), 'query' );
handleMessages( xsn );

@@ -647,3 +650,3 @@ return compactQuery( xsn );

function parseToExpr( cdl, filename = '<expr>.cds', options = {} ) {
let xsn = parseLanguage( cdl, filename, options, 'expr' );
let xsn = parseLanguage( cdl, filename, Object.assign( {parseOnly:true}, options ), 'expr' );
handleMessages( xsn );

@@ -676,3 +679,2 @@ return compactExpr( xsn );

toSql : backends.toSql,
toI18n : backends.toI18n,
toCsn : backends.toCsn,

@@ -686,4 +688,3 @@ toRename : backends.toRename, // Tentative, subject to change

// Everything below is for backward compatibility only and should no longer be used
generateExtensions, // Should be removed
toTntSpecificOutput, // FIXME: Temporary, subject to change
}

@@ -24,3 +24,7 @@ 'use strict'

function isAssociation(type) {
return type && (type.absolute == 'cds.Association' || type.absolute == 'cds.Composition');
if (!type)
return type;
if (type._artifact)
type = type._artifact.name;
return type.absolute == 'cds.Association' || type.absolute == 'cds.Composition';
}

@@ -293,2 +297,3 @@

// if there is no artifact surrounding 'name' in the model
// TODO: to be checked by author: still intended behaviour with 'cds' prefix?
function getTopLevelArtifactNameOf(name, model) {

@@ -305,3 +310,3 @@ let dotIdx = name.indexOf('.');

// Skip forward through '.'s until finding a non-namespace
while (dotIdx != -1 && model.definitions[name.substring(0, dotIdx)].kind == 'namespace') {
while (dotIdx != -1 && (!model.definitions[name.substring(0, dotIdx)] || model.definitions[name.substring(0, dotIdx)].kind == 'namespace')) {
dotIdx = name.indexOf('.', dotIdx + 1);

@@ -363,4 +368,4 @@ }

// the border between namespace and top-level artifact.
// - For the 'flat' naming convention, it means converting all '.' to '_' and uppercasing.
// - For the 'deep' naming convention, this is just 'artifactName'.
// - For the 'plain' naming convention, it means converting all '.' to '_' and uppercasing.
// - For the 'quoted' naming convention, this is just 'artifactName'.
// No other naming conventions are accepted

@@ -376,6 +381,6 @@ function getArtifactDatabaseNameOf(artifactName, namingConvention, model) {

}
else if (namingConvention == 'flat') {
else if (namingConvention == 'plain') {
return artifactName.replace(/\./g, '_').toUpperCase();
}
else if (namingConvention == 'deep') {
else if (namingConvention == 'quoted') {
return artifactName;

@@ -391,4 +396,4 @@ }

// - For the 'hdbcds' naming convention, this is just 'elemName'.
// - For the 'flat' naming convention, it means converting all '.' to '_' and uppercasing.
// - For the 'deep' naming convention, it means converting all '.' to '_'.
// - For the 'plain' naming convention, it means converting all '.' to '_' and uppercasing.
// - For the 'quoted' naming convention, it means converting all '.' to '_'.
// No other naming conventions are accepted

@@ -399,6 +404,6 @@ function getElementDatabaseNameOf(elemName, namingConvention) {

}
else if (namingConvention == 'flat') {
else if (namingConvention == 'plain') {
return elemName.replace(/\./g, '_').toUpperCase();
}
else if (namingConvention == 'deep') {
else if (namingConvention == 'quoted') {
return elemName.replace(/\./g, '_');

@@ -405,0 +410,0 @@ }

@@ -37,53 +37,53 @@ "use strict";

} else if (x.op.val == 'case') {
let result = `${inline ? '' : env.indent}case `;
let result = `${inline ? '' : env.indent}CASE `;
x.args.forEach(a => {
if (a.op && a.op.val === 'when')
// assuming that when statement will have 2 args representing the 2 sides of the THEN in the expression
result = result.concat(`when ${a.args.map(wArg => renderExpressionOrCondition(wArg, env)).join(' then ')} `);
result = result.concat(`WHEN ${a.args.map(wArg => renderExpressionOrCondition(wArg, env)).join(' THEN ')} `);
else if (a.op && a.op.val === 'else')
result = result.concat(`else ${a.args.map(arg => renderExpressionOrCondition(arg, env)).join('')} `);
result = result.concat(`ELSE ${a.args.map(arg => renderExpressionOrCondition(arg, env)).join('')} `);
else
result = result.concat(`${callbacks.renderPathOrValue(a, env)} `);
});
result = result.concat('end');
result = result.concat('END');
return result;
}
else if (isUnaryOperator(x.op.val) && x.args.length == 1) {
return x.op.val + (x.op.val == 'not' ? ' ' : '')
return x.op.val.toUpperCase() + ' '
+ renderExpressionOrCondition(x.args[0], env);
} else if (isBinaryOperator(x.op.val) && x.args.length == 2) {
return renderExpressionOrCondition(x.args[0], env)
+ ' ' + x.op.val + ' '
+ ' ' + x.op.val.toUpperCase() + ' '
+ (x.quantifier ? x.quantifier.val + ' ' : '')
+ renderExpressionOrCondition(x.args[1], env);
} else if (isInfixOperator(x.op.val)) {
return x.args.map(arg => renderExpressionOrCondition(arg, env)).join(' ' + x.op.val + ' ');
return x.args.map(arg => renderExpressionOrCondition(arg, env)).join(' ' + x.op.val.toUpperCase() + ' ');
} else if ((x.op.val == 'isNull' || x.op.val == 'isNotNull') && x.args.length == 1) {
return renderExpressionOrCondition(x.args[0], env)
+ ' is' + (x.op.val == 'isNotNull' ? ' not' : '')
+ ' null';
+ ' IS' + (x.op.val == 'isNotNull' ? ' NOT' : '')
+ ' NULL';
} else if ((x.op.val == 'between' || x.op.val == 'notBetween') && x.args.length == 3) {
return renderExpressionOrCondition(x.args[0], env)
+ (x.op.val == 'notBetween' ? ' not' : '')
+ ' between '
+ (x.op.val == 'notBetween' ? ' NOT' : '')
+ ' BETWEEN '
+ renderExpressionOrCondition(x.args[1], env)
+ ' and '
+ ' AND '
+ renderExpressionOrCondition(x.args[2], env);
} else if ((x.op.val == 'in' || x.op.val == 'notIn') && x.args.length == 2) {
return renderExpressionOrCondition(x.args[0], env)
+ (x.op.val == 'notIn' ? ' not' : '')
+ ' in '
+ (x.op.val == 'notIn' ? ' NOT' : '')
+ ' IN '
+ renderExpressionOrCondition(x.args[1], env);
} else if ((x.op.val == 'like' || x.op.val == 'notLike') && (x.args.length == 2 || x.args.length == 3)) {
return renderExpressionOrCondition(x.args[0], env)
+ (x.op.val == 'notLike' ? ' not' : '')
+ ' like '
+ (x.op.val == 'notLike' ? ' NOT' : '')
+ ' LIKE '
+ renderExpressionOrCondition(x.args[1], env)
+ (x.args.length == 3 ? ' escape ' + renderExpressionOrCondition(x.args[2], env) : '');
+ (x.args.length == 3 ? ' ESCAPE ' + renderExpressionOrCondition(x.args[2], env) : '');
} else if (x.op.val == 'exists' && (x.args.length == 1)) {
return 'exists ' + renderExpressionOrCondition(x.args[0], callbacks.increaseIndent(env));
return 'EXISTS ' + renderExpressionOrCondition(x.args[0], callbacks.increaseIndent(env));
} else if (x.op.val == 'query' || x.op.val == 'subquery') {
// FIXME: Actually, we should pass the surrounding artifact and not null, but we don't have that here
// (and it is not yet used except being passed to renderAbsoluteNameWithQuotes, which currently ignores it)
return '(' + callbacks.renderQuery(x, null, false, env) + '\n' + callbacks.decreaseIndent(env).indent + ')';
return '(' + callbacks.renderQuery(x, null, false, callbacks.increaseIndent(env)) + ')';
}

@@ -144,7 +144,7 @@ throw new Error('Unknown operator "' + x.op.val + '" in expression or condition: ' + JSON.stringify(compactCondOrExpr(x)));

case 'leftOuter':
return 'left outer join';
return 'left join';
case 'rightOuter':
return 'right outer join';
return 'right join';
case 'fullOuter':
return 'full outer join';
return 'full join';
case 'cross':

@@ -151,0 +151,0 @@ return 'cross join';

@@ -21,3 +21,3 @@ "use strict";

options = mergeOptions(model.options, options);
let flatNames = options.forHana && options.forHana.names == 'flat';
let plainNames = options.forHana && options.forHana.names == 'plain';
let hdbcdsNames = options.forHana && options.forHana.names == 'hdbcds';

@@ -49,3 +49,3 @@ let result = Object.create(null);

if (sourceStr != '') {
result[flatNames ? uppercaseAndUnderscore(artifactName) : artifactName]
result[plainNames ? uppercaseAndUnderscore(artifactName) : artifactName]
= renderNamespaceDeclaration(artifactName, env) + renderUsings(artifactName, env) + sourceStr;

@@ -297,3 +297,7 @@ }

// Fixed parts belonging to the table (includes migration, store type, unload prio, extended storage,
// Store type (must be separate because SQL wants it between 'CREATE' and 'TABLE')
if (tc.storeType) {
result += `${tc.storeType} store;\n`;
}
// Fixed parts belonging to the table (includes migration, unload prio, extended storage,
// auto merge, partitioning, ...)

@@ -304,3 +308,3 @@ if (tc.tableSuffix) {

// the simplicity of "the whole bandwurm is just one expression that can be
// rendered to SQL without further knowledge") and at the same time telling
// rendered to SQL without further knowledge" and at the same time telling
// CDS about the boundaries, the compactor has put each part into its own `xpr`

@@ -407,3 +411,3 @@ // object. Semantically equivalent because a "trivial" SQL renderer would just

function renderQueryElementAnnotations(artifactName, art, env) {
// If we are renderinmg for HANA CDS, never render any annotation (the only one we haven't already
// If we are rendering for HANA CDS, never render any annotation (the only one we haven't already
// stripped when we come here is '@cds.persistence.name', which is meant to stay, but only in the CSN).

@@ -483,3 +487,3 @@ if (options.toHana) {

// Render a path that starts with an absolute name (as used e.g. for the source of a query),
// with flat or deep names, depending on options. Expects an object 'path' that has a 'ref'.
// with plain or quoted names, depending on options. Expects an object 'path' that has a 'ref'.
// Returns the name as a string.

@@ -498,13 +502,16 @@ function renderAbsolutePath(path, env) {

let result = '';
// Render the first path step (absolute name, with different quoting/flatness ..)
if (flatNames) {
result += renderAbsoluteNameFlat(firstArtifactName, env);
// Render the first path step (absolute name, with different quoting/naming ..)
if (plainNames) {
result += renderAbsoluteNamePlain(firstArtifactName, env);
} else {
result += renderAbsoluteNameWithQuotes(firstArtifactName, env);
}
// Even the first step might have a filter
// Even the first step might have parameters and/or a filter
if (path.ref[0].args) {
result += `(${renderArgs(path.ref[0].args, ':', env)})`;
}
if (path.ref[0].where) {
result += `[${path.ref[0].cardinality ? (path.ref[0].cardinality.max + ': ') : ''}${renderExpr(path.ref[0].where, env)}]`;
}
// Add any path steps (possibly with filters) that may follow after that
// Add any path steps (possibly with parameters and filters) that may follow after that
if (path.ref.length > 1) {

@@ -517,3 +524,3 @@ result += `.${renderExpr({ref: path.ref.slice(1)}, env)}`;

// Render a path that starts with an absolute name (as used for the source of a query),
// possibly with an alias, with flat or deep names, depending on options. Expects an object 'path' that has a
// possibly with an alias, with plain or quoted names, depending on options. Expects an object 'path' that has a
// 'ref' and (in case of an alias) an 'as'. If necessary, an artificial alias

@@ -551,3 +558,3 @@ // is created to the original implicit name.

// HANA requires an alias for 'key' columns just for syntactical reasons
// FIXME: This will not complain for non-refs (but that shpuld be checked in forHana)
// FIXME: This will not complain for non-refs (but that should be checked in forHana)
if (options.forHana && col.key && !alias) {

@@ -576,4 +583,12 @@ alias = col.ref && col.ref[col.ref.length - 1];

let result = renderAnnotationAssignments(art, env);
result += env.indent + 'view ' + renderArtifactName(artifactName, env)
+ ' as ' + renderQuery(art.query, true, env);
result += env.indent + 'view ' + renderArtifactName(artifactName, env);
if (art.params) {
let childEnv = increaseIndent(env);
result += ' with parameters\n' + Object.keys(art.params).map(name => renderParameter(name, art.params[name], childEnv)).join(',\n') + '\n';
result += env.indent + 'as ';
}
else {
result += ' as ';
}
result += renderQuery(art.query, true, env);
result += ';\n';

@@ -743,3 +758,3 @@ result += renderQueryElementAnnotations(artifactName, art, env);

let childEnv = increaseIndent(env);
if (art.elements) {
if (art.elements && !art.type) {
// Structured type or annotation with anonymous struct type

@@ -789,4 +804,4 @@ result += " {\n";

result += renderCardinality(elm.cardinality) + (elm.type == 'cds.Association' ? ' to ' : ' of ');
result += flatNames ? renderAbsoluteNameFlat(elm.target, env)
: renderAbsoluteNameWithQuotes(elm.target, env);
result += plainNames ? renderAbsoluteNamePlain(elm.target, env)
: renderAbsoluteNameWithQuotes(elm.target, env);

@@ -806,5 +821,5 @@ // ON-condition (if any)

// Reference to another element
// FIXME: Shouldn't that be 'type of' ??
if (elm.type.ref) {
return renderAbsolutePath(elm.type, env);
// For HANA CDS, we need a 'type of'
return (options.forHana ? 'type of ' : '') + renderAbsolutePath(elm.type, env);
}

@@ -909,2 +924,10 @@

else if (x.ref) {
if (options.forHana && !x.param && !x.global && x.ref[0] === '$user') {
// FIXME: this is all not enough: we might need an explicit select item alias
if (x.ref[1] === 'id')
return "SESSION_CONTEXT('XS_APPLICATIONUSER')";
else if (x.ref[1] === 'locale')
return "SESSION_CONTEXT('LOCALE')";
}
// FIXME: no extra magic with x.param or x.global
return `${(x.param || x.global) ? ':' : ''}${x.ref.map(renderPathStep).join('.')}`;

@@ -914,3 +937,6 @@ }

else if (x.func) {
return `${x.func}(${renderArgs(x.args, '=>')})`;
if (x.args)
return `${x.func}(${renderArgs(x.args, '=>', env)})`;
else
return x.func;
}

@@ -922,5 +948,10 @@ // Nested expression

// Sub-select
else if (x.SELECT || x.SET) {
else if (x.SELECT) {
// renderQuery for SELECT does not bring its own parentheses (because it is also used in renderView)
return `(${renderQuery(x, false, increaseIndent(env))})`;
}
else if (x.SET) {
// renderQuery for SET always brings its own parentheses (because it is also used in renderViewSource)
return `${renderQuery(x, false, increaseIndent(env))}`;
}
else {

@@ -944,4 +975,4 @@ throw new Error('Unknown expression: ' + JSON.stringify(x));

if (idx == 0 && s == '$self') {
return flatNames ? renderAbsoluteNameFlat(env.currentArtifactName, env)
: renderAbsoluteNameWithQuotes(env.currentArtifactName, env);
return plainNames ? renderAbsoluteNamePlain(env.currentArtifactName, env)
: renderAbsoluteNameWithQuotes(env.currentArtifactName, env);
}

@@ -951,4 +982,2 @@ // HANA-specific translation of '$now' and '$user'

return 'CURRENT_TIMESTAMP';
} else if (s == '$user') {
return "SESSION_CONTEXT('XS_APPLICATIONUSER')";
}

@@ -958,4 +987,11 @@ }

// FIXME: We should rather explicitly recognize quoting somehow
// TODO: quote $parameters if it doesn't reference a parameter, this requires knowledge about the kind
// Example: both views are correct in HANA CDS
// entity E { key id: Integer; }
// view EV with parameters P1: Integer as select from E { id, $parameters.P1 };
// view EVp as select from E as "$parameters" { "$parameters".id };
if (idx == 0
&& (['$projection', '$self'].includes(s)
&& (['$projection', '$self', '$parameters'].includes(s)
|| getMagicVariables().map(id => id.toLowerCase()).includes(s.toLowerCase())

@@ -969,17 +1005,21 @@ || s == 'self' && options.oldstyleSelf)) {

else if (typeof s == 'object') {
// Filter, possibly with cardinality
if (s.where) {
return `${quoteOrUppercaseId(s.id)}[${s.cardinality ? (s.cardinality.max + ': ') : ''}${renderExpr(s.where, env)}]`;
// Sanity check
if (!s.func && !s.id) {
throw new Error('Unknown path step object: ' + JSON.stringify(s));
}
// Not really a path but an object-like function call
else if (s.func) {
return `${s.func}(${renderArgs(s.args, '=>')})`;
// Not really a path step but an object-like function call
if (s.func) {
return `${s.func}(${renderArgs(s.args, '=>', env)})`;
}
// View with arguments (use ':' for named args)
else if (s.args) {
return `${quoteOrUppercaseId(s.id)}(${renderArgs(s.args, ':')})`;
// Path step, possibly with view parameters and/or filters
let result = `${quoteOrUppercaseId(s.id)}`;
if (s.args) {
// View parameters
result += `(${renderArgs(s.args, ':', env)})`;
}
else {
throw new Error('Unknown path step object: ' + JSON.stringify(s));
if (s.where) {
// Filter, possibly with cardinality
result += `[${s.cardinality ? (s.cardinality.max + ': ') : ''}${renderExpr(s.where, env)}]`;
}
return result;
}

@@ -990,18 +1030,19 @@ else {

}
}
// Render function arguments or view parameters (positional if array, named if object/dict),
// using 'sep' as separator for positional parameters
function renderArgs(args, sep) {
// Positional arguments
if (args instanceof Array) {
return args.map(arg => renderExpr(arg, env)).join(', ');
}
// Named arguments (object/dict)
else if (typeof args == 'object') {
return Object.keys(args).map(key => `${quoteOrUppercaseId(key)} ${sep} ${renderExpr(args[key], env)}`).join(', ');
}
else {
throw new Error('Unknown args: ' + JSON.stringify(args));
}
// Render function arguments or view parameters (positional if array, named if object/dict),
// using 'sep' as separator for positional parameters
function renderArgs(args, sep, env) {
// Positional arguments
if (args instanceof Array) {
return args.map(arg => renderExpr(arg, env)).join(', ');
}
// Named arguments (object/dict)
else if (typeof args == 'object') {
return Object.keys(args).map(key => `${quoteOrUppercaseId(key)} ${sep} ${renderExpr(args[key], env)}`).join(', ');
}
else {
throw new Error('Unknown args: ' + JSON.stringify(args));
}
}

@@ -1078,4 +1119,4 @@

function renderAnnotationAssignment(ann, name, env) {
// If we are renderinmg for HANA CDS, never render any annotation (the only one we haven't already
// stripped when we come here is '@cds.persistence.name', which is meant to stay, but only in the CSN).
// If we are rendering for HANA CDS, never render any annotation (the only ones we haven't already
// stripped when we come here are '@cds.persistence.*', which are meant to stay, but only in the CSN).
if (options.toHana) {

@@ -1090,3 +1131,3 @@ return '';

let nameAfterVariant = parts[5];
let topLevelName = getTopLevelArtifactNameOf(nameBeforeVariant, model);
let topLevelName = getTopLevelArtifactNameOf(nameBeforeVariant, csn);
let result = env.indent + '@';

@@ -1139,6 +1180,6 @@ if (topLevelName) {

// Render an absolute name in 'flat' mode, i.e. uppercased and underscored. Also record the
// Render an absolute name in 'plain' mode, i.e. uppercased and underscored. Also record the
// fact that 'absName' is used in 'env', so that an appropriate USING can be constructed
// if necessary.
function renderAbsoluteNameFlat(absName, env) {
function renderAbsoluteNamePlain(absName, env) {
// Add using declaration

@@ -1165,7 +1206,3 @@ env.topLevelAliases[absName] = {

// 'implemented in' something, we need to treat the whole name like a top-level id.
// Note: The 'impl' might also have been generated by toHana based on '@cds.persistence.exists';
// we use 'impl' to transport this info because the annotations have already been removed when
// arriving here
// FIXME: Make that '$impl' or similar
if (options.toHana && csn.definitions[absName] && csn.definitions[absName].impl) {
if (options.toHana && csn.definitions[absName] && csn.definitions[absName]['@cds.persistence.exists']) {
env.topLevelAliases[absName] = {

@@ -1206,3 +1243,3 @@ quotedName: quoteAbsoluteNameAsId(absName),

return Object.keys(env.topLevelAliases)
.filter(name => !(flatNames && env.topLevelAliases[name].quotedName == uppercaseAndUnderscore(artifactName))) // avoid "using FOO as FOO" in FOO.cds
.filter(name => !(plainNames && env.topLevelAliases[name].quotedName == uppercaseAndUnderscore(artifactName))) // avoid "using FOO as FOO" in FOO.cds
.map(name => 'using ' + env.topLevelAliases[name].quotedName + ' as ' + env.topLevelAliases[name].quotedAlias + ';\n')

@@ -1214,8 +1251,8 @@ .join('');

// if it has a namespace parent. Assume that this is only called for top-level artifacts.
// - For 'deep' and 'hdbcds' names, render the namespace declaration (resulting in '.' or '::' style names)
// - For 'flat' names, do not render anything (namespace already part of flattened names).
// - For 'quoted' and 'hdbcds' names, render the namespace declaration (resulting in '.' or '::' style names)
// - For 'plain' names, do not render anything (namespace already part of flattened names).
// Return the namespace declaration (with trailing LF) or an empty string.
function renderNamespaceDeclaration(topLevelName, env) {
if (flatNames) {
// No namespaces in flat mode
if (plainNames) {
// No namespaces in plain mode
return '';

@@ -1235,5 +1272,5 @@ }

for (let name in csn.definitions) {
if (flatNames) {
if (plainNames) {
let art = csn.definitions[name];
// For 'flat' naming, take all entities and views, nothing else
// For 'plain' naming, take all entities and views, nothing else
if (art.kind == 'entity' || art.kind == 'view') {

@@ -1346,3 +1383,3 @@ result[name] = art;

function quoteOrUppercaseId(id) {
if (flatNames) {
if (plainNames) {
return id.replace(/\./g, '_').toUpperCase();

@@ -1355,7 +1392,7 @@ } else {

// Render the name of an artifact, using the current name prefix from 'env'
// and just the last part of the artifact's name. In case of flat names, this
// and just the last part of the artifact's name. In case of plain names, this
// is equivalent to simply flattening and uppercasing the whole name.
function renderArtifactName(artifactName, env) {
return (flatNames) ? quoteOrUppercaseId(artifactName)
: env.namePrefix + quoteId(getLastPartOf(artifactName));
return (plainNames) ? quoteOrUppercaseId(artifactName)
: env.namePrefix + quoteId(getLastPartOf(artifactName));
}

@@ -1362,0 +1399,0 @@

@@ -8,7 +8,7 @@

// Render the augmented CSN 'model' to SQL DDL statements renaming existing tables and their
// columns so that they match the result of "toHana" or "toSql" with the 'flat' option for names.
// Expects the naming convention of the existing tables to be either 'deep' or 'hdbcds' (default).
// columns so that they match the result of "toHana" or "toSql" with the 'plain' option for names.
// Expects the naming convention of the existing tables to be either 'quoted' or 'hdbcds' (default).
// The following options control what is actually generated:
// options : {
// toRename.names : existing names, either 'deep' or 'hdbcds' (default)
// toRename.names : existing names, either 'quoted' or 'hdbcds' (default)
// }

@@ -45,3 +45,3 @@ // Return a dictionary of top-level artifacts by their names, like this:

// table and its columns from the naming conventions given in 'options.toRename.name'
// (either 'deep' or 'hdbcds') to 'flat'. In addition, drop any existing associations
// (either 'quoted' or 'hdbcds') to 'plain'. In addition, drop any existing associations
// from the columns (they would likely become invalid anyway).

@@ -53,3 +53,3 @@ // Do not rename anything if the names are identical.

let beforeTableName = quoteSqlId(absoluteCdsName(art.name.absolute));
let afterTableName = flatSqlId(art.name.absolute);
let afterTableName = plainSqlId(art.name.absolute);

@@ -65,3 +65,3 @@ if (beforeTableName != afterTableName) {

let beforeColumnName = quoteSqlId(e.name.id);
let afterColumnName = flatSqlId(e.name.id);
let afterColumnName = plainSqlId(e.name.id);

@@ -98,5 +98,5 @@ if (!e._ignore) {

// Return 'name' with appropriate "-quotes, also replacing '::' by '.' if 'options.toRename.names'
// is 'deep'
// is 'quoted'
function quoteSqlId(name) {
if (options.toRename.names == 'deep') {
if (options.toRename.names == 'quoted') {
name = name.replace(/::/g, '.');

@@ -107,4 +107,5 @@ }

// Return 'name' with appropriate "-quotes, also replacing '::' and '.' by '_'
function flatSqlId(name) {
// Return 'name' with uppercasing and appropriate "-quotes, also replacing '::' and '.' by '_'
// (to be used by 'plain' naming convention).
function plainSqlId(name) {
return '"' + name.toUpperCase().replace(/(::|\.)/g, '_').replace(/"/g, '""') + '"';

@@ -111,0 +112,0 @@ }

@@ -11,6 +11,16 @@

// Render the CSN model 'model' to SQL DDL statements. One statement is created
// per top-level artifact, without trailing semicolon. Return a dictionary of top-level
// artifacts by their names, like this:
// { "foo" : "CREATE TABLE foo ...",
// "bar::wiz" : "CREATE VIEW \"bar::wiz\" AS SELECT FROM ..."
// per top-level artifact into dictionaries 'hdbtable', 'hdbview', ..., without
// leading CREATE, without trailing semicolon. All statements (in proper order)
// are copied into dictionary 'sql', with trailing semicolon.
// Return an object like this:
// { "hdbtable": {
// "foo" : "COLUMN TABLE foo ...",
// },
// "hdbview": {
// "bar::wiz" : "VIEW \"bar::wiz\" AS SELECT \"x\" FROM ..."
// },
// "sql: {
// "foo" : "CREATE TABLE foo ...;\n",
// "bar::wiz" : "CREATE VIEW \"bar::wiz\" AS SELECT \"x\" FROM ...;\n"
// }
// }

@@ -22,3 +32,2 @@ function toSqlDdl(model) {

let options = model.options;
let result = Object.create(null);

@@ -37,4 +46,17 @@ const { renderExpressionOrCondition, renderJoinOp } = renderUtils.getRenderUtils(model, options, {

// The final result in hdb-kind-specific form, without leading CREATE, without trailing newlines
// (note that the order here is relevant for transmission into 'resultObj.sql' below)
let resultObj = {
hdbtabletype: Object.create(null),
hdbtable: Object.create(null),
hdbindex: Object.create(null),
hdbfulltextindex: Object.create(null),
hdbview: Object.create(null),
}
// Render each artifact on its own
for (let artifactName in model.definitions) {
let artifactNames = Object.keys(model.definitions);
// Note: Sorting here just to minimize the diff with the new version based on new-style compact CSN, which is always sorted
artifactNames.sort();
for (let artifactName of artifactNames) {
// This environment is passed down the call hierarchy, for dealing with

@@ -46,7 +68,3 @@ // indentation issues

}
let sourceStr = renderArtifact(model.definitions[artifactName], env);
if (sourceStr != '') {
result[artifactName] = sourceStr;
}
renderArtifactInto(model.definitions[artifactName], resultObj, env);
}

@@ -57,9 +75,26 @@ // Throw up if we have errors

}
return result;
// Render an artifact. Return the resulting source string.
function renderArtifact(art, env) {
// Transfer results from hdb-specific dictionaries into 'sql' dictionary in proper order
// (relying on the order of dictionaries above)
// FIXME: Should consider inter-view dependencies, too
let sql = Object.create(null);
for (let hdbKind of Object.keys(resultObj)) {
for (let name in resultObj[hdbKind]) {
let sourceString = resultObj[hdbKind][name];
// Hack: Other than in 'hdbtable' files, in HANA SQL COLUMN is not mandatory but default.
if (options.toSql.dialect == 'hana' && hdbKind == 'hdbtable' && sourceString.startsWith('COLUMN ')) {
sourceString = sourceString.slice('COLUMN '.length);
}
sql[name] = `CREATE ${sourceString};`;
}
}
resultObj.sql = sql;
return resultObj;
// Render an artifact into the appropriate dictionary of 'resultObj'.
function renderArtifactInto(art, resultObj, env) {
// Ignore whole artifacts if forHana says so
if (art._ignore) {
return '';
return;
}

@@ -69,10 +104,27 @@ switch (art.kind) {

if (art.source) {
return renderProjection(art, env);
let result = renderProjection(art, env);
if (result) {
resultObj.hdbview[art.name.absolute] = result;
}
} else {
return renderEntity(art, env);
let result = renderEntityInto(art, resultObj, env);
if (result) {
resultObj.hdbentity[art.name.absolute] = result;
}
}
case 'view':
return renderView(art, env);
case 'type':
return renderType(art, env);
break;
case 'view': {
let result = renderView(art, env);
if (result) {
resultObj.hdbview[art.name.absolute] = result;
}
break;
}
case 'type': {
let result = renderType(art, env);
if (result) {
resultObj.hdbtabletype[art.name.absolute] = result;
}
break;
}
case 'context':

@@ -85,3 +137,3 @@ case 'service':

// Ignore: not SQL-relevant
return '';
return;
default:

@@ -92,4 +144,5 @@ throw new Error('Unknown artifact kind: ' + art.kind);

// Render a (non-projection) entity. Return the resulting source string.
function renderEntity(art, env) {
// Render a (non-projection, non-view) entity (and possibly its indices) into the appropriate
// dictionaries of 'resultObj'.
function renderEntityInto(art, resultObj, env) {
// FIXME: Took this from toCdl, but do entities have parameters yet at all? Views apparently don't ... I am confused

@@ -105,3 +158,7 @@ if (art.params && Object.keys(art.params)[0]) {

let result = env.indent + 'CREATE ' + storeType + 'TABLE ' + quoteSqlId(absoluteCdsName(art.name.absolute));
// in 'hdbtable' files, COLUMN or ROW is mandatory, and COLUMN is the default
if (options.toSql.dialect == 'hana' && storeType == '') {
storeType += 'COLUMN ';
}
let result = storeType + 'TABLE ' + quoteSqlId(absoluteCdsName(art.name.absolute));
result += ' (\n';

@@ -129,7 +186,6 @@ result += Object.keys(art.elements).map(name => renderElement(art.elements[name], childEnv))

}
result += ';';
result += renderIndexes(tc, childEnv);
result += renderIndexesInto(tc, art.name.absolute, resultObj, childEnv);
return result;
resultObj.hdbtable[art.name.absolute] = result;
}

@@ -180,3 +236,3 @@

result += ' JOIN ';
result += quoteSqlId(absoluteCdsName(elm.target.absolute)) + ' AS ' + quoteSqlId(elm.name.id) + ' ON (';
result += quoteSqlId(absoluteCdsName(elm.target._artifact.name.absolute)) + ' AS ' + quoteSqlId(elm.name.id) + ' ON (';
result += renderExpressionOrCondition(elm.onCond, env, true) + ')';

@@ -190,3 +246,2 @@ }

function renderTechnicalConfiguration(tc, env) {
let childEnv = increaseIndent(env);
let result = '';

@@ -229,13 +284,13 @@ if (!tc) {

if (tc.partition) {
result += '\n' + env.indent + 'PARTITION BY \n';
result += '\n' + env.indent + 'PARTITION BY ';
let i = 0;
tc.partition.specs.forEach(p => {
if (i > 0) {
result += ',\n';
result += ', ';
}
result += childEnv.indent + renderPartition(p, env);
result += renderPartition(p, env);
i++;
});
if (tc.partition.wpoac) {
result += '\n' + childEnv.indent + 'WITH PARTITIONING ON ANY COLUMNS ' + renderPathOrValue(tc.partition.wpoac, env);
result += ' ' + 'WITH PARTITIONING ON ANY COLUMNS ' + renderPathOrValue(tc.partition.wpoac, env);
}

@@ -247,3 +302,2 @@ }

function renderPartition(partition, env) {
let env2 = increaseIndent(increaseIndent(env));
let result = renderPathOrValue(partition.scheme, env);

@@ -255,3 +309,3 @@ if (partition.columns) {

if (i > 0) {
result += ',\n' + env2.indent;
result += ', ';
}

@@ -273,3 +327,3 @@ if (column.unit) {

if (partition.ranges) {
result += '\n' + env2.indent + '(';
result += ' (';
let oppStore = (partition.ranges[0].store == 'default' ? 'extended' : 'default');

@@ -282,3 +336,3 @@ let delimiter = false;

if (delimiter) {
result += ')\n' + env2.indent;
result += ') ';
}

@@ -323,3 +377,5 @@ result += 'USING ' + range.store.toUpperCase() + ' STORAGE (';

function renderIndexes(tc, env) {
// Render the indices belonging to 'artifactName' into the appropriate
// dictionary of 'resultObj'.
function renderIndexesInto(tc, artifactName, resultObj, env) {
let result = '';

@@ -330,6 +386,6 @@ if (tc && tc.indexes) {

if (Array.isArray(idx)) {
idx.forEach(i => result += renderIndex(i, env));
idx.forEach(i => renderIndexInto(i, artifactName, resultObj, env));
}
else {
result += renderIndex(idx, env);
renderIndexInto(idx, artifactName, resultObj, env);
}

@@ -340,5 +396,4 @@ }

function renderIndex(idx, env) {
let childEnv = increaseIndent(env);
let result = '\nCREATE ';
function renderIndexInto(idx, artifactName, resultObj, env) {
let result = '';
if (idx.kind === 'index') {

@@ -352,2 +407,3 @@ if (idx.unique) {

}
resultObj.hdbindex[`${artifactName}.${idx.name.id}`] = result;
}

@@ -358,44 +414,44 @@ else if (idx.kind === 'fulltextindex') {

if (idx.language.column) {
result += '\n' + childEnv.indent + 'LANGUAGE COLUMN ' + renderPathOrValue(idx.language.column, env);
result += ' ' + 'LANGUAGE COLUMN ' + renderPathOrValue(idx.language.column, env);
}
if (idx.language.detection) {
result += '\n' + childEnv.indent + 'LANGUAGE DETECTION (' + renderArray(idx.language.detection) + ')'
result += ' ' + 'LANGUAGE DETECTION (' + renderArray(idx.language.detection) + ')'
}
}
if (idx.mimeTypeColumn) {
result += '\n' + childEnv.indent + 'MIME TYPE COLUMN ' + renderPathOrValue(idx.mimeTypeColumn, env);
result += ' ' + 'MIME TYPE COLUMN ' + renderPathOrValue(idx.mimeTypeColumn, env);
}
if (idx.fuzzySearchIndex) {
result += '\n' + childEnv.indent + 'FUZZY SEARCH INDEX ' + renderPathOrValue(idx.fuzzySearchIndex, env);
result += ' ' + 'FUZZY SEARCH INDEX ' + renderPathOrValue(idx.fuzzySearchIndex, env);
}
if (idx.phraseIndexRatio) {
result += '\n' + childEnv.indent + 'PHRASE INDEX RATIO ' + renderPathOrValue(idx.phraseIndexRatio, env);
result += ' ' + 'PHRASE INDEX RATIO ' + renderPathOrValue(idx.phraseIndexRatio, env);
}
if (idx.configuration) {
result += '\n' + childEnv.indent + 'CONFIGURATION ' + renderPathOrValue(idx.configuration, env);
result += ' ' + 'CONFIGURATION ' + renderPathOrValue(idx.configuration, env);
}
if (idx.textAnalysis) {
result += '\n' + childEnv.indent + 'TEXT ANALYSIS ' + renderPathOrValue(idx.textAnalysis, env);
result += ' ' + 'TEXT ANALYSIS ' + renderPathOrValue(idx.textAnalysis, env);
}
if (idx.searchOnly) {
result += '\n' + childEnv.indent + 'SEARCH ONLY ' + renderPathOrValue(idx.searchOnly, env);
result += ' ' + 'SEARCH ONLY ' + renderPathOrValue(idx.searchOnly, env);
}
if (idx.fastPreprocess) {
result += '\n' + childEnv.indent + 'FAST PREPROCESS ' + renderPathOrValue(idx.fastPreprocess, env);
result += ' ' + 'FAST PREPROCESS ' + renderPathOrValue(idx.fastPreprocess, env);
}
if (idx.mimeType) {
result += '\n' + childEnv.indent + 'MIME TYPE ' + renderPathOrValue(idx.mimeType, env);
result += ' ' + 'MIME TYPE ' + renderPathOrValue(idx.mimeType, env);
}
if (idx.tokenSeparators) {
result += '\n' + childEnv.indent + 'TOKEN SEPARATORS ' + renderPathOrValue(idx.tokenSeparators, env);
result += ' ' + 'TOKEN SEPARATORS ' + renderPathOrValue(idx.tokenSeparators, env);
}
if (idx.textMining) {
if (idx.textMining.state) {
result += '\n' + childEnv.indent + 'TEXT MINING ' + renderPathOrValue(idx.textMining.state, env);
result += ' ' + 'TEXT MINING ' + renderPathOrValue(idx.textMining.state, env);
}
if (idx.textMining.config) {
result += '\n' + childEnv.indent + 'TEXT MINING CONFIGURATION ' + renderPathOrValue(idx.textMining.config, env);
result += ' ' + 'TEXT MINING CONFIGURATION ' + renderPathOrValue(idx.textMining.config, env);
}
if (idx.textMining.overlay) {
result += '\n' + childEnv.indent + 'TEXT MINING CONFIGURATION OVERLAY ' + renderPathOrValue(idx.textMining.overlay, env);
result += ' ' + 'TEXT MINING CONFIGURATION OVERLAY ' + renderPathOrValue(idx.textMining.overlay, env);
}

@@ -405,3 +461,3 @@ }

let ct = idx.changeTracking;
result += '\n' + childEnv.indent + renderPathOrValue(ct.mode);
result += ' ' + renderPathOrValue(ct.mode);
if (ct.asyncSpec) {

@@ -424,5 +480,5 @@ let asp = ct.asyncSpec;

}
resultObj.hdbfulltextindex[`${artifactName}.${idx.name.id}`] = result;
}
result += ';'
return result;
return;

@@ -433,3 +489,3 @@ function renderArray(arr) {

if (i > 0) {
r += ',\n' + increaseIndent(childEnv).indent;
r += ', ';
}

@@ -452,7 +508,9 @@ r += renderPathOrValue(v, env);

let childEnv = increaseIndent(env);
let result = env.indent + 'CREATE VIEW ' + quoteSqlId(absoluteCdsName(art.name.absolute)) + ' AS SELECT\n';
let result = 'VIEW ' + quoteSqlId(absoluteCdsName(art.name.absolute));
result += renderParameterDefinitions(art.params);
result += ' AS SELECT\n';
result += Object.keys(art.elements).map(name => renderViewOrProjectionElement(art.elements[name], childEnv))
.filter(s => s != '')
.join(',\n') + '\n';
result += env.indent + 'FROM ' + renderSourcePathWithAlias(art.source, art) + ';';
result += env.indent + 'FROM ' + renderSourcePathWithAlias(art.source, art);
return result;

@@ -484,3 +542,3 @@ }

// Source had an alias - render it
result += ' as ' + quoteSqlId(source.name.id);
result += ' AS ' + quoteSqlId(source.name.id);
}

@@ -505,33 +563,55 @@ return result;

}
if (source.path[0].where) {
// FIXME: This is all very ugly (will likely improve once we consume new-style CSN here)
// Determine the index of the first entity on the path (skipping over contexts etc) - after that we may have assoc elements
let firstEntityIdx = 0;
for (let i = 0; i < source.path.length; i++) {
if (source.path[i]._artifact && source.path[i]._artifact.kind != 'element') {
// Still within the entity name
firstEntityIdx = i;
}
}
if (source.path[firstEntityIdx].where) {
signal(error`"${art.name.absolute}": Filters in FROM are not supported for conversion to SQL`, source.location);
return '';
}
let firstEntity = source.path[firstEntityIdx]._artifact;
// Start with the absolute name of the first path step
let result = absoluteCdsName(source.path[0]._artifact.name.absolute);
let result = quoteSqlId(absoluteCdsName(firstEntity.name.absolute));
// Even the first step might have parameters
if (source.path[firstEntityIdx].namedArgs) {
result += renderNamedArgs(source.path[firstEntityIdx].namedArgs);
}
// Add any paths that may follow after that (separating the artifact name from the rest by ':' !)
for (let i = 1; i < source.path.length; i++) {
for (let i = firstEntityIdx + 1; i < source.path.length; i++) {
// Sanity check
if (source.path[i]._artifact && source.path[i]._artifact.kind != 'element') {
// Still part of the artifact name, just append with dots
result += '.' + source.path[i].id;
throw new Error('Expecting an element here: ' + JSON.stringify(source.path[i], null, 2));
}
// Path continues with elements - complain if it has a filter
if (source.path[i].where) {
signal(error`"${art.name.absolute}": Filters in FROM are not supported for conversion to SQL`, source.location);
return '';
}
if (i > firstEntityIdx + 1) {
// We are already in the elements part of a path - append '.' and quoted id
result += '.' + quoteSqlId(source.path[i].id);
// Append view params if any
if (source.path[i].namedArgs) {
result += renderNamedArgs(source.path[i].namedArgs);
}
} else {
// Path continues with elements - complain if it has a filter
if (source.path[i].where) {
signal(error`"${art.name.absolute}": Filters in FROM are not supported for conversion to SQL`, source.location);
return '';
// Sanity check
if (i < 1) {
throw new Error('Cannot be the first path step: ' + JSON.stringify(source.path[i], null, 2));
}
if (result.includes(':')) {
// We are already in the elements part of a path - append '.' and quoted id
result += '.' + quoteSqlId(source.path[i].id);
} else {
// We have just left the artifact and entered elements - quote artifact name, append ':' and quoted id
result = quoteSqlId(result) + ':' + quoteSqlId(source.path[i].id);
// We have just left the artifact and entered elements - append ':' and the quoted id of the current path step
result += ':' + quoteSqlId(source.path[i].id);
if (source.path[i].namedArgs) {
result += renderNamedArgs(source.path[i].namedArgs);
}
}
}
// Quote if not yet done so
if ((options.toSql.names !== 'flat' && !result.startsWith('"'))
|| (result.includes('.') && !result.includes(':'))) {
result = quoteSqlId(result);
}

@@ -549,2 +629,16 @@ // Sanity check: must have a name

function renderNamedArgs(args) {
let result = '(';
let i = 0;
for (let argName in args) {
let arg = args[argName];
if (i > 0) {
result += ', ';
}
result += quoteSqlId(arg.name.id) + ' => ' + renderPathOrValue(arg, { indent: '' });
i++;
}
return result + ')';
}
// Render a single view or projection element 'elm', as it occurs in a select list or projection list,

@@ -560,8 +654,10 @@ // possibly with annotations. Return the resulting source string (no trailing LF).

}
let elementPath = renderExpressionOrCondition(elm.value, env, true);
let elementValue = renderExpressionOrCondition(elm.value, env, true);
if (elm._typeIsExplicit) {
// FIXME: We may want to wrap a cast around 'elementPath' in this case?
// FIXME: We may want to wrap a cast around 'elementValue' in this case?
}
let result = env.indent + elementPath
if (!elm.viaAll && !elm.name.calculated) {
let result = env.indent + elementValue;
// Render an alias unless it is redundant
let path = elm.value.path;
if (!path || elm.name.id != path[path.length-1].id) {
result += ' AS ' + quoteSqlId(elm.name.id);

@@ -574,4 +670,5 @@ }

function renderView(art, env) {
let result = env.indent + 'CREATE VIEW ' + quoteSqlId(absoluteCdsName(art.name.absolute))
+ ' AS ' + renderQuery(art.query, art, true, env);
let result = 'VIEW ' + quoteSqlId(absoluteCdsName(art.name.absolute));
result += renderParameterDefinitions(art.params);
result += ' AS ' + renderQuery(art.query, art, true, env);
let childEnv = increaseIndent(env);

@@ -586,6 +683,10 @@ let associations = Object.keys(art.elements).filter(name => isAssociation(art.elements[name].type))

}
result += ';';
return result;
}
function renderParameterDefinitions(params) {
let paramDefs = Object.keys(params || []).map(name =>
{ return 'IN ' + quoteSqlId(name) + ' ' + renderTypeReference(params[name])}).join(', ');
return (paramDefs == '') ? paramDefs : '(' + paramDefs + ')';
}
// Render a query 'query', i.e. a select statement with where-condition etc, possibly as part of artifact 'art'.

@@ -611,4 +712,4 @@ // If 'isLeadingQuery' is true, mixins of 'art' are also rendered into the query.

// Ordinary query operators (first may be leading query)
result += `${renderQuery(query.args[0], art, isLeadingQuery, env)}`
result += `\n${env.indent}${query.op.val.replace('All', ' all').toUpperCase()} ${renderQuery(query.args[1], art, false, env)}`;
result += `(${renderQuery(query.args[0], art, isLeadingQuery, env)}`
result += `\n${env.indent}${query.op.val.replace('All', ' all').toUpperCase()} ${renderQuery(query.args[1], art, false, env)})`;
} else {

@@ -626,2 +727,6 @@ throw new Error('Unexpected query operation ' + query.op.val);

}
// Need extra parentheses if ORDER BY or LIMIT is involved, because they have strange precedence in relation to UNION, INTERSECT, ...
if (query.orderBy || query.limit) {
result = `(${result})`;
}
if (query.orderBy) {

@@ -659,3 +764,3 @@ result += `\n${env.indent}ORDER BY ${query.orderBy.map(entry => renderOrderByEntry(entry, env)).join(', ')}`;

}
let result = env.indent + 'CREATE TYPE ' + quoteSqlId(absoluteCdsName(art.name.absolute)) + ' AS TABLE (\n';
let result ='TYPE ' + quoteSqlId(absoluteCdsName(art.name.absolute)) + ' AS TABLE (\n';
let childEnv = increaseIndent(env);

@@ -667,3 +772,3 @@ if (art._finalType.elements) {

.join(',\n') + '\n';
result += env.indent + ');';
result += env.indent + ')';
} else {

@@ -697,3 +802,3 @@ // Non-structured HANA table type

// Association type
if (elm._finalType.type.absolute == 'cds.Association' || elm._finalType.type.absolute == 'cds.Composition') {
if (elm._finalType.target) {
// We can't do associations yet

@@ -706,9 +811,2 @@ signal(error`"${elm._main.name.absolute}.${elm.name.element}": Association and composition types are not yet supported for conversion to SQL`, elm.location);

if (elm._finalType.type._artifact.builtin) {
// Special magic: For members of derived type chains ending in 'cds.UUID', '_finalType'
// points directly to 'UUID' and not to the derived type _having_ type 'UUID'. Although
// 'forHana' has adapted the last type in the chain to now have type 'String(36)',
// the other members of the chain don't benefit from that, so we have to check here again.
if (elm._finalType.type._artifact.name.absolute == 'cds.UUID') {
return 'NVARCHAR(36)';
}
// cds.Integer => render as INTEGER (no quotes)

@@ -720,2 +818,6 @@ result += renderBuiltinType(elm._finalType.type._artifact);

result += renderTypeParameters(elm._finalType);
// FIXME: Quickhack: Apparently we sometimes omit the default length for strings
if (result == 'NVARCHAR') {
result += '(5000)';
}
return result;

@@ -820,3 +922,5 @@ }

// date'2017-11-02'
return result + v.literal + "'" + v.val + "'";
// date('2017-11-02') if sqlite
return result + v.literal + `${options.toSql.dialect === 'sqlite' ?
"('" : "'"}` + v.val + `${options.toSql.dialect === 'sqlite' ? "')" : "'"}`;
} else if (v.literal == 'struct') {

@@ -841,33 +945,50 @@ // { foo: 1 }

// Render a path or query path (provided as an array of path steps, possibly with filters)
function renderPath(path, env, art) {
function renderPath(path, env) {
// Magic special case: SQL functions that have no parentheses (CURRENT_*) are not recognized as
// function expressions by the parser - instead they appear here as paths of length 1 with a
// 'builtin' artifact.
if (path.length == 1 && path[0]._artifact && path[0]._artifact.kind == 'builtin') {
if (options.forHana) {
// HANA-specific translation of '$now' and '$user'
// FIXME: This should rather happen in forHana, but it is non-trivial to catch all the different
// flavors in which a path can be used there (e.g. for 'foo.origin.path', we would have to modify
// 'foo' to have a 'foo.value'). So much easier to do it here...
if (path[0].id == '$now') {
return 'CURRENT_TIMESTAMP';
} else if (path[0].id == '$user') {
return "SESSION_CONTEXT('XS_APPLICATIONUSER')";
const magicForHana = {
'$now': 'CURRENT_TIMESTAMP',
'$user.id': "SESSION_CONTEXT('XS_APPLICATIONUSER')",
'$user.locale': "SESSION_CONTEXT('LOCALE')",
}
let ref = path.length && path[ path.length-1 ]._artifact;
if (ref) {
if(ref.kind == 'builtin') {
if (options.forHana) {
// HANA-specific translation of '$now' and '$user'
// FIXME: This should rather happen in forHana, but it is non-trivial to catch all the different
// flavors in which a path can be used there (e.g. for 'foo.origin.path', we would have to modify
// 'foo' to have a 'foo.value'). So much easier to do it here...
let impl = magicForHana[ ref.name.element ];
// FIXME: this is all not enough: we might need an explicit select item alias
if (impl)
return impl;
}
// TODO: really toUpperCase() if not forHana - even the $now etc?
let start = String(path[0].id).toUpperCase();
return (path.length > 1) ? start + renderPath(path.slice(1), env) : start;
}
return String(path[0].id).toUpperCase();
if(ref.kind == 'param') {
let [ head, ...tail] = path;
if(head.id == '$parameters') {
path = tail;
}
path[0].id = ':' + path[0].id;
// Note: parameters should be of path length 1
return path.map(p => p.id.toUpperCase()).join('.')
}
}
// FIXME: Not the most elegant solution to do that here: filter out initial '$projection' (because SQL
// FIXME: Not the most elegant solution to do that here: filter out initial '$projection' and '$self' (because SQL
// neither understands nor needs it).
if (options.forHana && path[0].id === '$projection') {
if (options.forHana && (path[0].id === '$projection' || path[0].id === '$self')) {
return renderPath(path.slice(1), env);
}
// transform $self in the beginning of a path to the current artifact absolute name in case of forHana
if (options.forHana && path[0].id === '$self' && path.length > 1) {
return `${quoteSqlId(absoluteCdsName(art.absolute))}.${renderPath(path.slice(1), env)}`
}
return path.map(step => {
let result = quoteSqlId(step.id);
if(step.namedArgs) {
result += renderNamedArgs(step.namedArgs);
}
if (step.where) {

@@ -909,8 +1030,8 @@ result += '[' + (step.cardinality ? step.cardinality.targetMax.val + ': ' : '') + renderExpressionOrCondition(step.where, env, true) + ']';

// Additionally perform the following conversions on 'name'
// If 'options.toSql.names' is 'flat'
// If 'options.toSql.names' is 'plain'
// - replace '.' or '::' by '_' and convert to uppercase
// else if 'options.toSql.names' is 'deep'
// else if 'options.toSql.names' is 'quoted'
// - replace '::' by '.'
function quoteSqlId(name) {
if (options.toSql.names == 'flat') {
if (options.toSql.names == 'plain') {
name = name.replace(/(\.|::)/g, '_');

@@ -925,3 +1046,3 @@ if (name.match(/\W/g)

}
else if (options.toSql.names == 'deep') {
else if (options.toSql.names == 'quoted') {
name = name.replace(/::/g, '.');

@@ -928,0 +1049,0 @@ }

@@ -213,4 +213,6 @@ const schemaObjects = require('./swaggerSchemaObjects');

if (art.type) {
// is a primitive
if (art.type.absolute.startsWith('cds.')) {
let type = art;
if (type.type && type.type._artifact)
type = type.type._artifact;
if (type.name.absolute.startsWith('cds.')) {
// representing the length specified for strings or binary

@@ -227,3 +229,3 @@ if (art.length)

// associations
if (art.type.absolute === "cds.Association" || art.type.absolute === "cds.Composition" && art.target) {
if (type.name.absolute === "cds.Association" || art.type._artifact.name.absolute === "cds.Composition" && art.target) {
let resultSchema = art._swaggerTntString ?

@@ -237,3 +239,3 @@ { type: 'string', maxLength: 255 }

}
return Object.assign(result, convertBuiltInType(art.type));
return Object.assign(result, convertBuiltInType(type));
}

@@ -245,4 +247,6 @@ else { // reference

return Object.assign(result, schemaObjects.referenceObject('#/components/schemas', `${art._swaggerType}`));
else
return Object.assign(result, schemaObjects.referenceObject('#/components/schemas', `${art.type.absolute.slice(art.type.absolute.lastIndexOf('.') + 1)}${art.type.element ? '/properties/' + art.type.element.replace(/\./g, '/properties/') : ''}`));
else {
let name = art.type._artifact.name;
return Object.assign(result, schemaObjects.referenceObject('#/components/schemas', `${name.absolute.slice(name.absolute.lastIndexOf('.') + 1)}${name.element ? '/properties/' + name.element.replace(/\./g, '/properties/') : ''}`));
}
}

@@ -294,7 +298,7 @@ }

// if it is not a built-in
if (!(type.absolute && type.absolute.startsWith('cds.')))
if (!(type.name.absolute && type.name.absolute.startsWith('cds.')))
return undefined;
let result = Object.create(null);
result.type = type.absolute.slice(4).toLowerCase(); // crop the starting 'cds.'
result.type = type.name.absolute.slice(4).toLowerCase(); // crop the starting 'cds.'
switch (result.type) {

@@ -301,0 +305,0 @@ case 'binary':

'use strict';
const { forEachDefinition, forEachMemberRecursively, setProp } = require('../base/model');
const { compactSorted, compact } = require('../json/compactor');
const { compactModel } = require('../json/to-csn');
const deepCopy = require('../base/deepCopy');

@@ -43,4 +43,4 @@ const { CompilationError, hasErrors, sortMessages } = require('../base/messages');

addImplicitRedirections, createAndAddDraftAdminDataProjection,
createScalarElement, createAssociationElement, createAssociationPathComparison, addElement,
createAction, addAction } = transformUtils.getTransformers(model, '_');
createScalarElement, createAssociationElement, createAssociationPathComparison,
addElement, createAction, addAction } = transformUtils.getTransformers(model, '_');
// First walk through the model: perform preparations only

@@ -50,3 +50,3 @@ // Set '_service' property for all artifacts and sub-artifacts, for each service in the model

// Second walk through the model: Do the main part of the work
// Second walk: Flatten structs, unravel derived types, deal with annotations
forEachDefinition(model, (artifact) => {

@@ -74,6 +74,2 @@ // For entities and views only

}
// Generate artificial draft fields if requested
if (hasBoolAnnotation(artifact, '@odata.draft.enabled')) {
generateDraftForOdata(artifact);
}
}

@@ -98,36 +94,3 @@ // Types must not have anonymous structured elements

checkAnnotations(member);
// Generate foreign key elements for managed associations
if (isManagedAssociationElement(member)) {
// Flatten foreign keys (replacing foreign keys that are managed associations by their respective foreign keys)
member.foreignKeys = flattenForeignKeys(member.foreignKeys);
// Generate foreign key elements
for (let name in member.foreignKeys) {
let foreignKeyElement = createForeignKeyElement(member, member.foreignKeys[name]);
toFinalBaseType(foreignKeyElement);
// Propagate the association's annotations to the foreign key element
// (Overwriting because they should win over the derived type unraveling)
copyAnnotations(member, foreignKeyElement, true);
}
// If the managed association is NOT NULL, we give it a target min cardinality of 1
// if it didn't already have an explicitly specified min cardinality
if (member.notNull) {
if (!member.cardinality) {
member.cardinality = {};
}
if (!member.cardinality.targetMin) {
member.cardinality.targetMin = {
literal: 'number',
val: 1,
}
}
}
}
// min <= max cardinality
if(member.cardinality && member.cardinality.targetMin && member.cardinality.targetMax) {
if(member.cardinality.targetMin.val > member.cardinality.targetMax.val) {
signal(error`Element "${artifact.name.absolute}.${member.name.id}" target minimum cardinality must not be greater than target maximum cardinality`, member.cardinality.location);
}
}
// Entities only: Flatten structs used in paths

@@ -160,7 +123,61 @@ if (artifact.kind == 'entity') {

// Perform implicit redirection of non-exposed association targets
// Note that this can only happen after struct flattening has been performed, because
// the current implementation relies on modifying associations in place, which would
// not be possible when an entity has an element typed with a named struct containing
// an association (we would modify the type instead).
addImplicitRedirections(model);
// Third walk through the model: Now all artificially generated things are in place
// Third walk: Generate foreign key fields for managed associations (must be done
// after struct flattening, otherwise we might encounter already generated foreign
// key fields in types we have already processed.
forEachDefinition(model, (artifact) => {
forEachMemberRecursively(artifact, (member) => {
// Generate foreign key elements for managed associations
if (isManagedAssociationElement(member)) {
// Flatten foreign keys (replacing foreign keys that are managed associations by their respective foreign keys)
member.foreignKeys = flattenForeignKeys(member.foreignKeys);
// Generate foreign key elements
for (let name in member.foreignKeys) {
let foreignKeyElement = createForeignKeyElement(member, member.foreignKeys[name], artifact);
toFinalBaseType(foreignKeyElement);
// Propagate the association's annotations to the foreign key element
// (Overwriting because they should win over the derived type unraveling)
copyAnnotations(member, foreignKeyElement, true);
}
// If the managed association is NOT NULL, we give it a target min cardinality of 1
// if it didn't already have an explicitly specified min cardinality.
// (No need to check again for min <= max cardinality, because max has already been checked to be > 0)
if (member.notNull) {
if (!member.cardinality) {
member.cardinality = {};
}
if (!member.cardinality.targetMin) {
member.cardinality.targetMin = {
literal: 'number',
val: 1,
}
}
}
}
});
});
// Fourth walk through the model: Now all artificially generated things are in place
forEachDefinition(model, artifact => {
if (artifact.kind == 'entity' || artifact.kind == 'view') {
// Generate artificial draft fields if requested
// Note that this needs to happen after implicit redirection has been performed, because it checks
// for all draft nodes (additional artifacts reachable via compositions) to be part of a service.
// This is typically achieved only by means of implicit redirection.
if (hasBoolAnnotation(artifact, '@odata.draft.enabled')) {
// Ignore if not part of a service
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);
}
else {
generateDraftForOdata(artifact, artifact);
}
}
for (let elemName in artifact.elements) {

@@ -282,2 +299,6 @@ let elem = artifact.elements[elemName];

if (['element', 'key', 'param'].includes(member.kind)) {
// If we have a 'preserved dotted name' (i.e. we are a result of flattening), use that for the @cds.persistence.name annotation
if (member._flatElementNameWithDots) {
memberName = member._flatElementNameWithDots;
}
addStringAnnotationTo('@cds.persistence.name', getElementDatabaseNameOf(memberName, options.toOdata.names), member);

@@ -361,5 +382,6 @@ }

// Generate all that is required in ODATA for draft enablement of 'artifact' into the artifact
// and into the model
function generateDraftForOdata(artifact) {
// Generate all that is required in ODATA for draft enablement of 'artifact' into the artifact,
// into its transitively reachable composition targets, and into the model.
// 'rootArtifact' is the root artifact where composition traversal started.
function generateDraftForOdata(artifact, rootArtifact) {
// Sanity check

@@ -370,2 +392,8 @@ if (!artifact._service) {

// Nothing to do if already draft-enabled (composition traversal may have circles)
if ((artifact['@Common.DraftRoot.PreparationAction'] || artifact['@Common.DraftNode.PreparationAction'])
&& artifact.actions && artifact.actions.draftPrepare) {
return;
}
// FIXME: Current restriction: Must only have exactly one key, which is of type UUID

@@ -384,3 +412,3 @@ let keyNames = Object.keys(artifact.elements).filter(elemName => {

}
if (keyElem._finalType.name.absolute != 'cds.UUID') {
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);

@@ -395,3 +423,2 @@ return;

draftAdminDataProjection = createAndAddDraftAdminDataProjection(artifact._service);
addBoolAnnotationTo('@cds.odata.NoEntitySet', true, draftAdminDataProjection);
}

@@ -403,6 +430,10 @@ // Barf if it is not an entity or not what we expect

// Generate the annotations describing the draft actions
addStringAnnotationTo('@Common.DraftRoot.PreparationAction', 'draftPrepare', artifact);
addStringAnnotationTo('@Common.DraftRoot.ActivationAction', 'draftActivate', artifact);
addStringAnnotationTo('@Common.DraftRoot.EditAction', 'draftEdit', artifact);
// Generate the annotations describing the draft actions (only draft roots can be activated/edited)
if (artifact == rootArtifact) {
addStringAnnotationTo('@Common.DraftRoot.PreparationAction', 'draftPrepare', artifact);
addStringAnnotationTo('@Common.DraftRoot.ActivationAction', 'draftActivate', artifact);
addStringAnnotationTo('@Common.DraftRoot.EditAction', 'draftEdit', artifact);
} else {
addStringAnnotationTo('@Common.DraftNode.PreparationAction', 'draftPrepare', artifact);
}

@@ -423,2 +454,3 @@ // Generate the additional elements into the draft-enabled artifact

// @odata.contained: true
// DraftAdministrativeData : Association to one DraftAdministrativeData;

@@ -432,5 +464,12 @@ let draftAdministrativeData = createAssociationElement('DraftAdministrativeData', draftAdminDataProjection, true);

};
addBoolAnnotationTo('@odata.contained', true, draftAdministrativeData);
addElement(draftAdministrativeData, artifact);
// SiblingEntity : Association to one Books on (... IsActiveEntity unequal, all other key fields equal ...)
// Note that we need to do the ODATA transformation steps for managed associations
// (foreign key field generation, generatedFieldName) by hand, because the corresponding
// transformation steps have already been done on all artifacts when we come here)
if (draftAdministrativeData.foreignKeys['DraftUUID']) {
let foreignKeyElement = createForeignKeyElement(draftAdministrativeData, draftAdministrativeData.foreignKeys['DraftUUID'], artifact);
addBoolAnnotationTo('@odata.contained', true, foreignKeyElement);
}
// SiblingEntity : Association to one <artifact> on (... IsActiveEntity unequal, all other key fields equal ...)
let siblingEntity = createAssociationElement('SiblingEntity', artifact, false);

@@ -446,5 +485,8 @@ siblingEntity.cardinality = {

siblingEntity.onCond = createAssociationPathComparison(siblingEntity, isActiveEntity, '!=', isActiveEntity);
// Iterate elements
for (let elemName in artifact.elements) {
let elem = artifact.elements[elemName];
if (elemName != 'IsActiveEntity' && elem.key) {
// Amend the ON-condition above:
// ... and SiblingEntity.<keyfield> = <keyfield> ... (for all key fields except 'IsActiveEntity')

@@ -461,5 +503,31 @@ siblingEntity.onCond = {

}
// Make all non-key elements nullable
if (elem.notNull && !(elem.key && elem.key.val)) {
elem.notNull.val = false;
}
// Draft-enable the targets of composition elements (draft nodes), too
if (elem.target && elem._finalType.type && elem._finalType.type._artifact.name.absolute === 'cds.Composition') {
let draftNode = elem.target._artifact;
// Ignore if that is our own draft root
if (draftNode != rootArtifact) {
// Barf if the draft node has @odata.draft.enabled itself
if (hasBoolAnnotation(draftNode, '@odata.draft.enabled')) {
signal(error`"${elem.name.absolute}.${elem.name.element}": Composition in draft-enabled entity cannot lead to another entity with "@odata.draft.enabled"`, elem.location);
}
// Ignore composition if not part of a service
else if (!draftNode._service) {
signal(warning`Target "${draftNode.name.absolute}" of composition "${elem.name.absolute}.${elem.name.element}" cannot be a draft node because it is not part of a service`, elem.location);
continue;
}
else {
// Generate draft stuff into the target
generateDraftForOdata(draftNode, rootArtifact);
}
}
}
}
// Generate the actions into the draft-enabled artifact
// Generate the actions into the draft-enabled artifact (only draft roots can be activated/edited)

@@ -470,11 +538,12 @@ // action draftPrepare (SideEffectsQualifier: String) return <artifact>;

// action draftActivate() return <artifact>;
let draftActivate = createAction('draftActivate', artifact);
addAction(draftActivate, artifact);
if (artifact == rootArtifact) {
// action draftActivate() return <artifact>;
let draftActivate = createAction('draftActivate', artifact);
addAction(draftActivate, artifact);
// action draftEdit (PreserveChanges: Boolean) return <artifact>;
let draftEdit = createAction('draftEdit', artifact, 'PreserveChanges', 'cds.Boolean');
addAction(draftEdit, artifact);
// action draftEdit (PreserveChanges: Boolean) return <artifact>;
let draftEdit = createAction('draftEdit', artifact, 'PreserveChanges', 'cds.Boolean');
addAction(draftEdit, artifact);
}
}
}

@@ -541,32 +610,17 @@

// Post-process an augmented CSN model 'model' that has been produced by 'transform4odata' so that it
// looks like the original (compacted) output of the '4odata' transformation'.
// If 'serviceName' is provided, only artifacts from this service are considered for the output.
// Most of this should not be necessary once we adapt the EDMX generation to use augmented CSN.
// Currently this involves the following steps:
// - Convert to compact CSN
// => EDMX processors should actually use augmented CSN
// - Keep only exposed artifacts
// => EDMX processors should instead look only at those artifacts in augmented CSN that have '_service'
// - Filter out everything but the one service selected
// => EDMX processors should handle this based on '_service', too
// Return a (compacted) copy of 'model' with the transformation applied. Do not change the original model.
function postProcessForBackwardCompatibility(model, serviceName=undefined) {
// Compact model and remove everything that that does not belong to the specified service
// model: augmented csn, prepared for OData
function compactForService(model, serviceName) {
// Compact the model
let compactedModel = (model.options.testMode) ? compactSorted(model) : compact(model);
let compactedModel = compactModel(model);
setProp(compactedModel, 'messages', model.messages);
// Iterate artifact definitions on the augmented model, but modify the compacted one in parallel
forEachDefinition(model, (artifact, artifactName) => {
// Remove artifacts that are not exposed
if (!artifact._service) {
delete compactedModel.definitions[artifactName];
return;
// Remove definitions that don't belong to given service
if (serviceName) {
let dict = compactedModel.definitions;
let namesNotInService = Object.keys(dict).filter(n => !n.startsWith(serviceName + '.') && n != serviceName);
for (let name of namesNotInService) {
delete compactedModel.definitions[name];
}
// Ignore if not part of specified service (if any)
if (serviceName != undefined && serviceName != artifact._service.name.absolute) {
delete compactedModel.definitions[artifactName];
return;
}
});
}
return compactedModel;

@@ -578,3 +632,3 @@ }

getServiceNames,
postProcessForBackwardCompatibility,
compactForService
}

@@ -49,12 +49,12 @@ const { setProp, forEachDefinition, forEachGeneric, forEachMemberRecursively } = require('../base/model');

let memberType = member.items ? member.items.type : member.type;
if (!memberType)
if (!memberType || !memberType._artifact)
return;
if (!memberType.absolute.startsWith('cds.')) {
if (!memberType._artifact.name.absolute.startsWith('cds.')) {
let memberTypeService = memberType._artifact._service || memberType._artifact._main && memberType._artifact._main._service;
if (memberTypeService !== currentService) {
// if the type is builtin and not from the current service -> expand it
if (!art.projection && memberType._artifact.type && memberType._artifact.type.absolute.startsWith('cds.'))
if (!art.projection && memberType._artifact.type && memberType._artifact.type._artifact.name.absolute.startsWith('cds.'))
member.items ? setProp(member.items, '_swaggerExpandType', true) : setProp(member, '_swaggerExpandType', true);
else if (/* the projection case is handle in processProjection */!art.projection)// structured type not from the current service
signal(error`The type ${memberType.absolute} of ${member.name.absolute} is not from the service ${currentService.name.absolute}`, member.location);
signal(error`The type ${memberType._artifact.name.absolute} of ${member.name.absolute} is not from the service ${currentService.name.absolute}`, member.location);
}

@@ -104,5 +104,5 @@ }

let elemType = elem.items ? elem.items.type : elem.type;
if (!elemType)
if (!elemType || !elemType._artifact)
return;
if (elemType.absolute.startsWith('cds.'))
if (elemType._artifact.name.absolute.startsWith('cds.'))
return;

@@ -119,7 +119,7 @@ let elemTypeService = elemType._artifact._service || elemType._artifact._main && elemType._artifact._main._service;

elem.items ? setProp(elem.items, '_swaggerType', typeFromCurrectService) : setProp(elem, '_swaggerType', typeFromCurrectService);
else if (elemType._artifact.type && elemType._artifact.type.absolute.startsWith('cds.'))
else if (elemType._artifact.type && elemType._artifact.type._artifact.name.absolute.startsWith('cds.'))
// if the type is builtin and not from the current service -> expand it
elem.items ? setProp(elem.items, '_swaggerExpandType', true) : setProp(elem, '_swaggerExpandType', true);
else
signal(error`The type ${elemType.absolute} of element ${elem.name.absolute}.${elem.name.id} is not exposed in service ${parent.name.absolute} via a type definition`, elem.location);
signal(error`The type ${elemType._artifact.name.absolute} of element ${elem.name.absolute}.${elem.name.id} is not exposed in service ${parent.name.absolute} via a type definition`, elem.location);
}

@@ -140,5 +140,5 @@ }

let memberType = member.items ? member.items.type : member.type;
if (!memberType)
if (!memberType || !memberType._artifact)
return;
if (memberType.absolute.startsWith('cds.'))
if (memberType._artifact.name.absolute.startsWith('cds.'))
return;

@@ -148,2 +148,3 @@ // try to find the exposition of the type in the current service

.find(typeName =>
parent.artifacts[typeName].type &&
parent.artifacts[typeName].type._artifact === memberType._artifact

@@ -154,3 +155,3 @@ );

else
signal(error`The type ${memberType.absolute} of artifact ${member.name.absolute}.${member.name.id} is not exposed in service ${parent.name.absolute} via a type definition`, member.location);
signal(error`The type ${memberType._artifact.name.absolute} of artifact ${member.name.absolute}.${member.name.id} is not exposed in service ${parent.name.absolute} via a type definition`, member.location);
});

@@ -157,0 +158,0 @@ }

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

skipServiceIncludes: false, // if true: Do not include contexts into services via "@extends" annotation
skipPropagatingFromProjectionSrc: false, // if true: Do not propagate properties from source to projection
skipPropagatingFromInclude: false, // if true: Do not propagate properties from (first) included artifact
skipPropagatingActions: false, // if true: Do not propagate bound actions (and functions)
skipPropagatingIncludes: false, // if true: Do not propagate the list of included artifacts

@@ -28,3 +25,2 @@ skipNotPropagatingIndexableAnno: false, // if true: Do not make an exception for the propagation of the '@Indexable' annotation

skipAnnosTextArrangementReordering: false, // if true: Do not automatically produce correct form for TextArrangementAnnotation
// skipAnnosEnforceRecordType: false, // if true: Let `--odata-omit-record-type` decide about unnecessary record type attributes
skipAnnosTextAndValueListForAssocs: false, // if true: Do not artificially generate '@Common.Text' and '@Common.ValueList' for associations

@@ -87,3 +83,3 @@ skipAnnosRemoveManagedAssociationAnnos: false, // if true: Do not remove annotations from managed associations

}
includedContextName = artifact.includes[0].absolute;
includedContextName = artifact.includes[0]._artifact.name.absolute;
// For now, only allow inclusion of (typically abstract) services

@@ -111,3 +107,3 @@ if (model.definitions[includedContextName].kind != 'service') {

let sourceArtifact = model.definitions[sourceName];
let targetArtifact = cloneWithTransformations(sourceArtifact, {});
let targetArtifact = cloneWithTransformations(sourceArtifact, {}, true);
// FIXME: In order not to have to manually adapt all internal links for actions and their parameters,

@@ -122,15 +118,2 @@ // we refrain from deep-copying them here and simply take them as they are - niemand hat die Absicht, sie zu veraendern anyway ...

let targetElement = targetArtifact.elements[elementName];
// Restore _artifact in element type
if (targetElement.type) {
setProp(targetElement.type, '_artifact', sourceElement.type._artifact);
}
// Restore _artifact and _status in element origins
// FIXME: Remove once the compactor no longer renders 'origin'
if (targetElement.origin) {
setProp(targetElement.origin, '_artifact', sourceElement.origin._artifact);
setProp(targetElement.origin, '_status', sourceElement.origin._status);
}
if (targetElement.value) {
setProp(targetElement.value, '_artifact', sourceElement.value._artifact);
}
setProp(targetElement, '_main', targetArtifact);

@@ -144,10 +127,2 @@ // FIXME: Not entirely complete for nested stuff, but good enough for now...

}
// Restore _artifact in artifact type
if (targetArtifact.type) {
setProp(targetArtifact.type, '_artifact', sourceArtifact.type._artifact);
}
// Restore _artifact in includes (if any)
for (let i = 0; i < (targetArtifact.includes || []).length; i++) {
setProp(targetArtifact.includes[i], '_artifact', sourceArtifact.includes[i]._artifact);
}
// Adapt the target artifact's name (artificial, i.e. no path)

@@ -174,5 +149,8 @@ targetArtifact.name = {

let element = artifact.elements[elementName];
if (element.target != undefined) {
element.target.absolute = element.target.absolute.replace(includedContextName, serviceName);
setProp(element.target, '_artifact', model.definitions[element.target.absolute]);
if (element.target != undefined && element.target._artifact) {
let elementTargetAbsolute = element.target._artifact.name.absolute.replace(includedContextName, serviceName);
setProp(element.target, '_artifact', model.definitions[elementTargetAbsolute]);
if (element.target.path) {
setProp(element.target.path[element.target.path.length - 1], '_artifact', model.definitions[elementTargetAbsolute]);
}
if (element.foreignKeys != undefined) {

@@ -198,5 +176,38 @@ for (let foreignKeyName in element.foreignKeys) {

// Within compact CSN 'csn', propagate the 'include' property along included entities and query sources,
// as the old implementation of propagation in the compiler did (ending up with the value of the
// 'includes' property from the lowest artifact in the chain that has one being copied to all above,
// which doesn't actually make much sense - but that's what TNT apparently got used to).
// Modify the model in place.
function propagateIncludesForTnt(csn) {
for (let name in csn.definitions) {
let artifact = csn.definitions[name];
propagateIncludesFrom(artifact);
}
function propagateIncludesFrom(artifact) {
if (artifact.includes) {
// Perform propagation for all of our own includes (if any)
for (let includeName of artifact.includes) {
propagateIncludesFrom(csn.definitions[includeName]);
}
// Take over the first include's includes (if any)
if (csn.definitions[artifact.includes[0]].includes) {
artifact.includes = csn.definitions[artifact.includes[0]].includes;
}
} else if (artifact.source) {
// Perform propagation for our projection source (if any)
propagateIncludesFrom(csn.definitions[artifact.source]);
// Take over our projectiuon source's includes (if any)
if (csn.definitions[artifact.source].includes) {
artifact.includes = csn.definitions[artifact.source].includes;
}
}
}
}
module.exports = {
getDefaultTntFlavorOptions,
transformTntExtensions,
propagateIncludesForTnt,
}

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

addImplicitRedirections,
isBacklinkAssociation,
isBacklinkComparison,
collectAllSimpleExpressions,
isAssociationOperand,
isDollarSelfOperand,
createExposingProjection,

@@ -38,2 +37,4 @@ createAndAddDraftAdminDataProjection,

createAssociationPathComparison,
createForeignKey,
addForeignKey,
addElement,

@@ -78,2 +79,3 @@ copyAndAddElement,

element: name + '.' + targetGeneratedForeignKeyFieldName,
$inferred: 'flatten',
viaTransform: true

@@ -127,7 +129,9 @@ },

// Create an artificial foreign key element for association 'assoc' (possibly part
// of nested struct, i.e. containing dots), using foreign key info from 'foreignKey',
// inserting it into 'elements' of the assoc's main artifact. Add a property
// of nested struct, i.e. containing dots) in 'artifact', using foreign key info
// from 'foreignKey', inserting it into 'elements' of 'artifact'. Add a property
// 'generatedFieldName' to the corresponding 'foreignKey' of the assoc.
// Note that this must happen after struct flattening (because it assumes that the
// element names it encounters are relative to 'artifact').
// Return the newly generated foreign key element.
function createForeignKeyElement(assoc, foreignKey) {
function createForeignKeyElement(assoc, foreignKey, artifact) {
let fkArtifact = foreignKey.targetElement._artifact;

@@ -141,5 +145,2 @@ // Sanity checks

}
if (assoc._main == undefined) {
throw Error('Expecting association ' + printableName(assoc) + ' to have a main artifact');
}
if (fkArtifact.target) {

@@ -158,2 +159,3 @@ throw Error('Not expecting foreign key of association ' + printableName(assoc) + ' to be an association after flattening')

// Assemble artificial foreign key element
let fkType = fkArtifact.type._artifact;
let foreignKeyElement = {

@@ -164,2 +166,3 @@ name: {

element: foreignKeyElementName,
$inferred: 'flatten',
viaTransform: true

@@ -169,4 +172,3 @@ },

type: {
path: [ { id: fkArtifact.type.absolute } ],
absolute: fkArtifact.type.absolute, // TODO: enough?
path: [ { id: fkType.name.$renamed || fkType.name.absolute } ],
},

@@ -181,4 +183,4 @@ };

}
setProp(foreignKeyElement.type, '_artifact', fkArtifact.type._artifact);
setProp(foreignKeyElement.type.path[0], '_artifact', fkArtifact.type._artifact);
setProp(foreignKeyElement.type, '_artifact', fkType);
setProp(foreignKeyElement.type.path[0], '_artifact', fkType);
// If the association is non-fkArtifact resp. key, so should be the foreign key field

@@ -196,6 +198,15 @@ for (let prop of ['notNull', 'key']) {

if (assoc.value) {
let valueForeignKeyElementName = assoc.value.element.replace(/\./g, pathDelimiter) + fkSeparator + foreignKey.name.id;
// TODO: previously, the code directly accessed assoc.value.element, probably set by forHana
let elemName = assoc.value.element || (assoc.value._artifact ? assoc.value._artifact.name.element : '');
let valueForeignKeyElementName = elemName.replace(/\./g, pathDelimiter) + fkSeparator + foreignKey.name.id;
// For the foreign key element, take the same path as for the assoc, just without the last step
let valueForeignKeyElementPath = [];
if (assoc.value.path) {
valueForeignKeyElementPath = cloneWithTransformations(assoc.value.path, {}, true).slice(0, -1);
}
valueForeignKeyElementPath.push({ id: valueForeignKeyElementName });
foreignKeyElement.value = {
path: [{ id: valueForeignKeyElementName }],
absolute: assoc.value.absolute,
path: valueForeignKeyElementPath,
// TODO: keep the following, needed by toSql ?
absolute: assoc.value.absolute || assoc.value._artifact && assoc.value._artifact.name.absolute,
element: valueForeignKeyElementName,

@@ -214,4 +225,3 @@ viaTransform: true, // FIXME: Do we still need this?

// Insert artificial element into assoc's main artifact, with all cross-links (must not exist already)
let artifact = assoc._main;
// Insert artificial element into artifact, with all cross-links (must not exist already)
if (artifact.elements[foreignKeyElementName]) {

@@ -293,2 +303,4 @@ signal(error`Generated foreign key element "${foreignKeyElementName}" for association "${assoc.name.absolute}.${assoc.name.element}" conflicts with existing element`, assoc.location);

}
// Preserve the generated element name as it would have been with 'hdbcds' names
setProp(flatElem, '_flatElementNameWithDots', elem.name.id + '.' + grandChildName);
result[flatElemName] = flatElem;

@@ -299,3 +311,5 @@ }

let flatElemName = elem.name.id + pathDelimiter + childName;
let flatElem = cloneWithTransformations(childElem, {});
let flatElem = cloneWithTransformations(childElem, {}, true);
flatElem.name.element = flatElemName;
flatElem.name.$inferred = 'flatten';
flatElem.viaTransform = true; // FIXME: This name is not ideal but used elsewhere, too)

@@ -305,20 +319,2 @@ setProp(flatElem, '_finalType', childElem._finalType);

setProp(flatElem, '_parent', elem._parent);
if (childElem.foreignKeys) {
for (let foreignKeyName in childElem.foreignKeys) {
if (childElem.foreignKeys[foreignKeyName].targetElement._artifact) {
setProp(flatElem.foreignKeys[foreignKeyName].targetElement, '_artifact', childElem.foreignKeys[foreignKeyName].targetElement._artifact);
}
}
}
if (childElem.type) {
flatElem.type = childElem.type;
}
if (childElem.target) {
flatElem.target = childElem.target;
}
// FIXME: We just lost all _artifact links in the ON-condition by the cloning above,
// so we rather take the original ON-condition here
if (childElem.onCond) {
flatElem.onCond = childElem.onCond;
}
// If the original element had a value, construct one for the flattened element

@@ -328,2 +324,4 @@ if (elem.value) {

}
// Preserve the generated element name as it would have been with 'hdbcds' names
setProp(flatElem, '_flatElementNameWithDots', elem.name.id + '.' + childName);
result[flatElemName] = flatElem;

@@ -356,2 +354,3 @@ }

flatElem.value = {
// TODO: keep absolute/element, needed by toSql ?
absolute : elem.value.absolute,

@@ -363,7 +362,3 @@ element : flatElemName,

if (elem.value.path) {
flatElem.value.path = cloneWithTransformations(elem.value.path, {});
// Take over _artifact links from original path, because flattenStructStepsInPath requires them
for (let i = 0; i < elem.value.path.length; i++) {
flatElem.value.path[i]._artifact = elem.value.path[i]._artifact;
}
flatElem.value.path = cloneWithTransformations(elem.value.path, {}, true);
}

@@ -436,3 +431,3 @@ flatElem.value.path.push({ id : lastPathStep });

returnedTypes.forEach(returnType => {
if (!returnType.absolute.startsWith('cds.')) {
if (!returnType._artifact.name.absolute.startsWith('cds.')) {
let returnTypeBlock = obtainTypesService(returnType);

@@ -449,3 +444,3 @@ if (returnTypeBlock !== actionBlock)

let param = action.params[p];
if (param.type.absolute.startsWith('cds.'))
if (param.type._artifact.name.absolute.startsWith('cds.'))
continue;

@@ -456,3 +451,3 @@ let paramTypeBlock = obtainTypesService(param.type);

if (!actionBlock && paramTypeBlock === 'not in a service') {
if (/* actionBlock */ action.name.absolute.split('.').slice(0, -1).join('.') !== /* paramTypeBlock */ param.type.absolute.split('.').slice(0, -1).join('.'))
if (/* actionBlock */ action.name.absolute.split('.').slice(0, -1).join('.') !== /* paramTypeBlock */ param.type._artifact.name.absolute.split('.').slice(0, -1).join('.'))
signal(error`The type ${param.type._artifact.name.absolute} of parameter ${param.name.absolute}.${param.name.id} in action ${action.name.absolute}${action.name.action ? '.' + action.name.action : ''} is not from the current service`, param.location);

@@ -504,7 +499,7 @@ } else if (paramTypeBlock !== actionBlock)

if (association.target && association.target._artifact && association.target._artifact._service != artifact._service)
signal(error`Association "${artifact.name.absolute}.${association.name.id}" must be redirected: Target "${association.target.absolute}" is not exposed by service "${artifact._service.name.absolute}"`, association.location);
signal(error`Association "${artifact.name.absolute}.${association.name.id}" must be redirected: Target "${association.target._artifact.name.absolute}" is not exposed by service "${artifact._service.name.absolute}"`, association.location);
}
// Replace the type of 'node' with its final base type, collecting all annotations on the way
// (from least to most derived, i.e. giving precedence to those higher in the type chain).
// Replace the type of 'node' with its final base type (in contrast to the compiler,
// also unravel derived enum types, i.e. take the final base type of the enum's base type.
function toFinalBaseType(node) {

@@ -516,38 +511,20 @@ // Nothing to do if no type (or if array/struct type)

// Sanity check
if (node.type._artifact === undefined) {
throw Error('Expecting type of ' + printableName(node) + ' to be resolved');
if (!node._finalType) {
throw Error(`Expecting _finalType for ${node.name.absolute}`);
}
// Start the chain with the node's type (add in front of type chain, so that most derived one is last)
let typeChain = [node.type._artifact];
// Unravel until finding primitive type or type constructor
while (typeChain[0].type && !typeChain[0].builtin) {
// Sanity check
if (typeChain[0].type._artifact === undefined) {
throw Error('Expecting type of ' + printableName(typeChain[0]) + ' to be resolved');
}
typeChain.unshift(typeChain[0].type._artifact);
// Take the final type as provided by the compiler
let baseType = node._finalType.type || node.type;
// If that is an enum, unravel the base type of that, too (unlike what the compiler does)
if (node._finalType.enum) {
node.enum = node._finalType.enum;
baseType = node.type._artifact._finalType.type || node.type._artifact.type;
}
// Take the front of the chain as final base type (but keep the location as it was)
node.type = {
path: [ { id: typeChain[0].name.absolute } ],
absolute: typeChain[0].name.absolute,
path: [ { id: baseType._artifact.name.absolute } ],
absolute: baseType._artifact.name.absolute,
location: node.type.location,
viaTransform: true,
};
setProp(node.type, '_artifact', typeChain[0]);
// Sanity checks
if (node._finalType && node._finalType.type && node._finalType.type.absolute != node.type.absolute) {
throw Error('Expecting _finalType ' + node._finalType.type.absolute + ' to equal result of toFinalBaseType: ' + node.type.absolute);
}
// Propagate type parameters and all annotations upwards the chain (from chain[1] onwards, i.e. from least to most derived)
for (let typeDef of typeChain.slice(1)) {
// Propagate type parameters
for (let prop of ['length', 'precision', 'scale', 'enum', 'target', 'foreignKeys']) {
if (typeDef[prop] != undefined) {
node[prop] = typeDef[prop];
}
}
// Propagate annotations (not overwriting because most derived comes last and should win)
copyAnnotations(typeDef, node, false);
}
setProp(node.type, '_artifact', baseType._artifact);
setProp(node.type.path[0], '_artifact', baseType._artifact);
}

@@ -576,3 +553,3 @@

// If no candidate for implicit redirection can be found so far, and if requested by annotation: auto-expose the target
let implicitlyRedirected = (exposedByProjection[member.target.absolute] || []).filter(p => p._service == artifact._service);
let implicitlyRedirected = (exposedByProjection[member.target._artifact.name.absolute] || []).filter(p => p._service == artifact._service);
if (implicitlyRedirected.length == 0 && hasBoolAnnotation(member.target._artifact, '@cds.autoexpose')) {

@@ -583,5 +560,5 @@ let projectionId = member.target._artifact.name.absolute.replace(/\./g, '_').replace(/::/g, '__');

// Take the just-created projection as the (only!) projection exposing the target in this service
exposedByProjection[member.target.absolute] = (exposedByProjection[member.target.absolute] || []).filter(p => p._service != artifact._service);
exposedByProjection[member.target.absolute].push(projection);
// console.log(`Auto-exposing target ${member.target.absolute} of association ${artifactName}.${memberName} as ${projection.name.absolute}`); }
exposedByProjection[member.target._artifact.name.absolute] = (exposedByProjection[member.target._artifact.name.absolute] || []).filter(p => p._service != artifact._service);
exposedByProjection[member.target._artifact.name.absolute].push(projection);
// console.log(`Auto-exposing target ${member.target._artifact.name.absolute} of association ${artifactName}.${memberName} as ${projection.name.absolute}`);
}

@@ -602,12 +579,13 @@ }

// Only consider exposures in the same service for implicit redirection
let implicitlyRedirected = (exposedByProjection[assoc.target.absolute] || []).filter(p => p._service == artifact._service);
let implicitlyRedirected = (exposedByProjection[assoc.target._artifact.name.absolute] || []).filter(p => p._service == artifact._service);
// Complain if no implicit redirection or if not unique
if (implicitlyRedirected.length == 0) {
signal(error`Association "${artifactName}.${assocName}" cannot be implicitly redirected: Target "${assoc.target.absolute}" is not exposed in service "${artifact._service.name.absolute}" by any projection`, assoc.location);
signal(error`Association "${artifactName}.${assocName}" cannot be implicitly redirected: Target "${assoc.target._artifact.name.absolute}" is not exposed in service "${artifact._service.name.absolute}" by any projection`, assoc.location);
return;
} else if (implicitlyRedirected.length > 1) {
signal(error`Association "${artifactName}.${assocName}" cannot be implicitly redirected: Target "${assoc.target.absolute}" is exposed in service "${artifact._service.name.absolute}" by multiple projections: ${implicitlyRedirected.map(p => '"' + p.name.absolute + '"').join(', ')}`, assoc.location);
signal(error`Association "${artifactName}.${assocName}" cannot be implicitly redirected: Target "${assoc.target._artifact.name.absolute}" is exposed in service "${artifact._service.name.absolute}" by multiple projections: ${implicitlyRedirected.map(p => '"' + p.name.absolute + '"').join(', ')}`, assoc.location);
return;
}
// Perform implicit redirection
// console.log(`Redirecting target ${assoc.target._artifact.name.absolute} of association ${artifactName}.${assoc.name.element} to ${implicitlyRedirected[0].name.absolute}`);
assoc.redirected = { val: true };

@@ -650,12 +628,15 @@ assoc.target = {

// A projection or view with a single query and a single, simple source also exposes the source
if (exposedArtifact.queries && exposedArtifact.queries.length == 1) {
if (exposedArtifact.queries && exposedArtifact.queries.length == 1
&& exposedArtifact.queries[0].from.length == 1 && exposedArtifact.queries[0].from[0].path) {
let from = exposedArtifact.queries[0].from;
// Sanity check
if (!from[0].absolute) {
if (!from[0]._artifact) {
throw new Error(`Unresolved query source in ${exposedArtifact.name.absolute}`);
}
let sourceArtifact = model.definitions[from[0].absolute];
// FIXME: We would rather use `from[0]._artifact` here, but the stupid TNT-specific @extends-magic
// does not preserve all _artifact links in queries ...
let sourceArtifact = from[0]._artifact;
// Sanity check
if (!sourceArtifact) {
throw new Error(`Non-existing query source ${from[0].absolute} in ${exposedArtifact.name.absolute}`);
throw new Error(`Non-existing query source ${from[0]._artifact.name.absolute} in ${exposedArtifact.name.absolute}`);
}

@@ -683,3 +664,3 @@ addToResult(sourceArtifact, artifactInService);

}
// console.error('Use ' + artifactInService.name.absolute + ' as replacement for ' + exposedArtifact.name.absolute);
// console.log(`Artifact "${artifactInService.name.absolute}" exposes "${exposedArtifact.name.absolute}`);
}

@@ -773,94 +754,23 @@ return result;

// Return true if 'assoc' is a backlink association, false if it is not
function isBacklinkAssociation(assoc, currentArt) {
if (!assoc.onCond) // managed assoc
// Return true if 'arg' is an expression argument of type association or composition
function isAssociationOperand(arg) {
if (!arg.path) {
// Not a path, hence not an association (literal, expression, function, whatever ...)
return false;
let expressions = collectAllSimpleExpressions(assoc.onCond);
if (expressions.some(expr => isBacklinkComparison(expr, currentArt, assoc)))
return true;
return false;
}
// For an expression 'expr' checks if it fulfills the conditions the association to be a backlink one
// and returns true or false respectively
// The result is false when one of the following is violated:
// 1. The comparison must be '='
// 2. Not self part of the expression must be a path
// 3. Only one operand must be $self
// 4. Self part must have only '$self', but not '$self.a.b...'
// 5. Not self path must have at least two steps
// 6. Not self path must end with an association
// 7. Not self path must contain two associations
// 8. The first one from point 7 must be the current association element
// 9. The second association from point 7 must point back to the current entity
// 10. The second association from point 7 must not be a backlink assocation
function isBacklinkComparison(expr, currentArt, currentElem) {
// 1. comparison must be '='
if (expr.op.val !== '=')
return false;
// 2. operand must be a path
// in augmented csn '$self' is also a path
if (expr.args.some(a => !a.path))
return false;
if (expr.args.some(a => a.path && a.path[0].id === '$self')) {
let selfSide = expr.args.find(a => a.path && a.path[0].id === '$self');
let notSelf = expr.args.find(a => a.path && a.path[0].id !== '$self');
// 3. only one operand can be '$self'
if (expr.args.every(a => a.path && a.path[0].id === '$self'))
}
// Sanity check
if (!arg._artifact) {
// FIXME: With 'hanaFlavor, we see unresolved paths for magic function args like ROUND_HALF_UP - ignore them)
if (model.options.hanaFlavor) {
return false;
// 4. operand must be '$self', but not '$self.a.b...'
if (selfSide.path.length > 1)
return false;
// 5. path must have at least two steps
if (notSelf.path.length < 2)
return false;
// 6. <notself> path must end with an association
if (notSelf.path.slice(-1)[0]._artifact.type.absolute !== 'cds.Association')
return false;
let notSelfAssocs = notSelf.path.filter(p => p._artifact.type && p._artifact.type.absolute === 'cds.Association')
// 7. <notself> path must contain two associations
if (notSelfAssocs.length !== 2)
return false;
// 8. the first one must be the current association element
if (notSelfAssocs[0]._artifact.name.absolute !== currentElem.name.absolute
&& notSelfAssocs[0]._artifact.name.element !== currentElem.name.element
&& notSelfAssocs[0]._artifact.target !== currentElem.target)
return false;
// 9. the second association must point back
if (currentArt.source ?
(notSelfAssocs[1]._artifact.target._artifact.name.absolute !== currentArt.source._artifact.name.absolute
&& notSelfAssocs[1]._artifact.target._artifact.name.id !== currentArt.source._artifact.name.id)
:
(notSelfAssocs[1]._artifact.target._artifact.name.absolute !== currentArt.name.absolute
&& notSelfAssocs[1]._artifact.target._artifact.name.id !== currentArt.name.id)
)
return false;
// 10. the second association must not be a backlink assocaition
if (isBacklinkAssociation(notSelfAssocs[1], currentArt))
return false;
return true;
}
throw new Error(`Expected argument to be resolved: ${JSON.stringify(arg, null, 2)}`);
}
return false;
// If it has a target, it is an association or composition
return arg._artifact.target || (arg._artifact._finalType && arg._artifact._finalType.target);
}
// For a condition 'onCond', collect all sub-conditions (recursively) into the array 'simpleConditions'
// that have at least one path argument
// FIXME: Rename this to better describe what it does (And couldn't that simply be done by looking for
// '=' comparisons where left and right are a path, recursively?)
function collectAllSimpleExpressions(cond) {
let simpleExpressions = [];
if (cond instanceof Array) // on condition in parentheses
return collectAllSimpleExpressions(cond[0]);
else if (!cond.args) // on condition with function
return simpleExpressions;
if (cond.args.some(a => {
return a.path ? true : false;
}))
simpleExpressions.push(cond); // if some of the arguments has path-> put it into the array for check afterwards if it is a backling assoc
cond.args.filter(a => {
return !a.path; // if some of the sides of 'cond' may contain a condition itself -> go into it recursively
}).forEach(a => {
simpleExpressions.push(...collectAllSimpleExpressions(a));
})
return simpleExpressions;
// Return true if 'arg' is an expression argument denoting "$self"
function isDollarSelfOperand(arg) {
return arg.path && arg.path.length == 1 && (arg.path[0].id == '$self' || model.options.oldstyleSelf && arg.path[0].id == 'self');
}

@@ -991,7 +901,7 @@

],
absolute: typeName,
},
};
setProp(elem.type, '_artifact', type);
setProp(elem, '_finalType', type);
setProp(elem.type.path[0], '_artifact', type);
setProp(elem, '_finalType', elem);
if (isKey) {

@@ -1020,3 +930,2 @@ elem.key = {

],
absolute: target.name.absolute,
};

@@ -1032,21 +941,4 @@ setProp(elem.target, '_artifact', target);

}
let foreignKey = {
name: {
id: keyElemName,
$inferred: 'keys',
element: elemName + '.' + keyElemName,
},
kind: 'key',
targetElement: {
path : [
{ id: keyElemName }
],
},
calculated: true,
$inferred: 'keys',
}
setProp(foreignKey.targetElement, '_artifact', keyElem);
setProp(foreignKey, '_parent', elem);
setProp(foreignKey, '_finalType', keyElem._finalType);
elem.foreignKeys[keyElemName] = foreignKey;
let foreignKey = createForeignKey(keyElemName, keyElem);
addForeignKey(foreignKey, elem);
elem.implicitForeignKeys = true;

@@ -1090,2 +982,45 @@ }

// Create an artificial foreign key 'keyElemName' for key element 'keyElem'. Note that this
// only creates a foreign key, not the generated foreign key element.
function createForeignKey(keyElemName, keyElem) {
let foreignKey = {
name: {
id: keyElemName,
$inferred: 'keys',
},
kind: 'key',
targetElement: {
path : [
{ id: keyElemName }
],
},
calculated: true,
$inferred: 'keys',
}
setProp(foreignKey.targetElement, '_artifact', keyElem);
setProp(foreignKey, '_finalType', keyElem._finalType);
return foreignKey;
}
// Add foreign key 'foreignKey' to managed association element 'elem'.
function addForeignKey(foreignKey, elem) {
// Sanity checks
if (!elem.target || !elem.foreignKeys) {
throw new Error('Expecting managed association element with foreign keys');
}
// Foreign key must not exist
if (elem.foreignKeys[foreignKey.name.id]) {
signal(error`"${elem.name.absolute}.${elem.name.id}": Generated foreign key conflicts with existing foreign key`, elem.foreignKeys[foreignKey.name.id].location);
return;
}
// Add the foreign key
elem.foreignKeys[foreignKey.name.id] = foreignKey;
foreignKey.name.absolute = elem.name.absolute;
foreignKey.name.element = elem.name.element + '.' + foreignKey.name.id;
setProp(foreignKey, '_main', elem._main);
setProp(foreignKey, '_parent', elem._parent);
}
// Add element 'elem' to 'artifact'

@@ -1116,7 +1051,2 @@ function addElement(elem, artifact) {

function copyAndAddElement(elem, artifact, elemName) {
// Sanity check
if (elem.name.element.includes('.')) {
// FIXME: Have to figure that out later
throw new Error('Expected non-structured element');
}
if (!artifact.elements) {

@@ -1137,8 +1067,13 @@ throw new Error('Expected structured artifact');

};
// FIXME: For now, simply shallow-copy all these properties
for (let prop of ['type', 'key', 'notNull', 'length', 'precision', 'scale', 'localized', 'target', 'onCond', 'foreignKeys', 'location']) {
if (elem[prop]) {
// FIXME: For now, simply shallow-copy all these properties (and annotations)
for (let prop in elem) {
if (['type', 'key', 'notNull', 'length', 'precision', 'scale', 'localized', 'onCond', 'foreignKeys', 'location', 'cardinality'].includes(prop)
|| prop.startsWith('@')) {
result[prop] = elem[prop];
}
}
// Clone this one because it may need modification later (redirection of conditions to draft shadow entity)
if (elem.target) {
result.target = cloneWithTransformations(elem.target, {}, true);
}
setProp(result, '_parent', artifact);

@@ -1193,3 +1128,2 @@ setProp(result, '_main', artifact);

],
absolute: paramTypeName,
},

@@ -1196,0 +1130,0 @@ }

@@ -8,14 +8,12 @@ 'use strict'

var { linkToOrigin } = require('../compiler/shared');
const deepCopy = require('../base/deepCopy');
const alerts = require('../base/alerts');
function translateAssocsToJoins(inputModel)
function translateAssocsToJoins(model)
{
const { error, signal } = alerts(inputModel);
const { error, signal } = alerts(model);
var options = inputModel.options || {};
var options = model.options || {};
if(!options.betaMode)
signal(error`Conversion of associations into JOINs is not supported yet`);
const fullJoinOption = options.forHana && options.forHana.associations == 'joins';

@@ -25,9 +23,6 @@ // Note: This is called from the 'forHana' transformations, so it is controlled by its options)

const { compactCondOrExpr } = compactor.getCompactors(options);
const { compactNode } = compactor.getCompactors(options);
let model = deepCopy(inputModel);
model.alerts = inputModel.alerts;
forEachDefinition(model, prepareAssociations);
forEachDefinition(model, transformView);
forEachDefinition(model, transformQueries);

@@ -38,3 +33,2 @@ // Throw up if we have errors

}
return model;

@@ -47,15 +41,17 @@

{
/* create the prefix string up to the main artifact which is
/* Create the prefix string up to the main artifact which is
prepended to all source side paths of the resulting ON condition
(cut off name.id from name.element)
*/
art.elementPrefix = art.name.element.slice(0, art.name.element.length - art.name.id.length).replace(/\./g, pathDelimiter);
art.$elementPrefix = art.name.element.slice(0, art.name.element.length - art.name.id.length).replace(/\./g, pathDelimiter);
// create path prefix tree for Foreign Keys, required to substitute aliases in ON cond calculation
// also very useful to detect fk overlaps
if(type.foreignKeys && !type.fkPathPrefixTree)
/*
Create path prefix tree for Foreign Keys, required to substitute
aliases in ON cond calculation, also very useful to detect fk overlaps.
*/
if(type.foreignKeys && !type.$fkPathPrefixTree)
{
type.fkPathPrefixTree = { children: Object.create(null) };
type.$fkPathPrefixTree = { children: Object.create(null) };
forEachGeneric(type, 'foreignKeys', fk => {
let ppt = type.fkPathPrefixTree;
let ppt = type.$fkPathPrefixTree;
fk.targetElement.path.forEach(ps => {

@@ -67,3 +63,3 @@ if(!ppt.children[ps.id])

});
setProp(ppt, '_fk', fk);
ppt._fk = fk;
});

@@ -86,22 +82,47 @@ }

function transformView(art)
function transformQueries(art)
{
if(modelUtils.isView(art))
transformQueries(art.queries);
}
let queries = art.queries;
let fullJoins = fullJoinOption;
function transformQueries(queries)
{
if(queries.length>0)
if(queries && queries.length > 0)
{
/*
Setup QATs and leaf QAs (mixins, query, subqueries in from clause)
1a) First, mark all mixin assoc definitions with a pseudo QA that points to the assoc target.
1b) Next, for all paths in a query do flytrap checks and create the path prefix trees aka QATs.
Paths that start with a mixin assoc are Qat'ed into the mixin definition.
If a mixin assoc is published, its leaf Qat receives the pseudo QA(view) from the rootQat,
which is the mixin definition itself. See 1a)
1c) Finally create QAs for from clause subqueries, as they are not yet swept by the path walk
HANA cannot process associations with parameters, filters on first FROM path step and mixin-assoc usages
Filters and mixin usages will lead to a minimum join translation (only FROM clause and MIXINs)
An association path step with parameters requires a full join conversion or if parameter path step is
not at leaf position (entity/view with parameters followed by another association in FROM clause)
*/
if(!fullJoinOption) {
let env = {
minimum : false,
full: false,
walkover : { from:true, onCondFrom: false, select: true, filter: false },
callback :
(pathDict, env) => {
env.minimum = env.minimum ||
(env.location == 'from' ? !!pathDict.path.filter(p => isEntityOrView(p._artifact))[0].where
: (pathDict.path[0]._navigation && pathDict.path[0]._navigation.kind == 'element' && pathDict.path.length > 1));
env.full = env.full || pathDict.path.some((e, i, a) => {
return !!e.namedArgs && (e._artifact.target || i < a.length-1) })
}
}
queries.map(q => walkQuery(q, env));
if(!env.minimum && !env.full)
return;
fullJoins = env.full;
}
/*
Setup QATs and leaf QAs (mixins, query, subqueries in from clause)
1a) Mark all mixin assoc definitions with a pseudo QA that points to the assoc target.
1b) For all paths in a query do flytrap checks and create the path prefix trees aka QATs.
Paths that start with a mixin assoc are Qat'ed into the mixin definition.
If a mixin assoc is published, its leaf Qat receives the pseudo QA(view) from the rootQat,
which is the mixin definition itself. See 1a)
1c) Create QAs for FROM clause subqueries, as they are not yet swept by the path walk
*/
let env = {
fullJoins,
aliasCount: 0,

@@ -115,21 +136,23 @@ walkover: { from: true, onCondFrom:true, select:true, filter: true },

// 2) walk over each from table path, transform it into a join tree
// 2) Walk over each from table path, transform it into a join tree
env.walkover = { from:true, onCondFrom:false, select: false, filter: false };
env.callback = [ createInnerJoins ];
env.callback = createInnerJoins;
queries.map(q => walkQuery(q, env));
// 3) transform toplevel from block into cross join
// 3) Transform toplevel FROM block into cross join
queries.map(q => createCrossJoins(q));
// 4) transform all remaining join relevant paths into left outer joins and connect with
// FROM block join tree. Instead of walking paths it is sufficient to process the $qat of each tableAlias
// 4) Transform all remaining join relevant paths into left outer joins and connect with
// FROM block join tree. Instead of walking paths it is sufficient to process the $qat
// of each $tableAlias.
queries.map(q => createLeftOuterJoins(q, env));
// 5) rewrite original/native ON cond paths (same rewrite as with assoc ON cond path but with different table alias)
// 6) prepend table alias to all remaining paths
// 5) Rewrite ON condition paths that are part of the original FROM block
// (same rewrite as (injected) assoc ON cond paths but with different table alias).
// 6) Prepend table alias to all remaining paths
env.walkover = { from:false, onCondFrom:true, select: true, filter: false };
env.callback = [ rewriteGenericPaths ];
env.callback = rewriteGenericPaths;
queries.map(q => walkQuery(q, env));
// 7) attach firstFilterConds to Where Condition.
// 7) Attach firstFilterConds to Where Condition.
queries.map(q => attachFirstFilterConditions(q));

@@ -141,7 +164,7 @@

function createCrossJoins(query)
{
// if the toplevel FROM block is a comma separated list and has more then one entry,
// If the toplevel FROM block is a comma separated list and has more then one entry,
// cross join list items and replace from array
if(query.op.val === 'query')

@@ -152,3 +175,3 @@ {

// recurse into sub queries
// Recurse into sub queries
query.queries.map(q => createCrossJoins(q));

@@ -158,3 +181,3 @@ }

// transform each from table path into a join tree and attach the tree to the path object
// Transform each FROM table path into a join tree and attach the tree to the path object
function createInnerJoins(fromPathNode, env)

@@ -168,3 +191,3 @@ {

// translate all join relevant query paths into left outer join tree and attach it to the lead query
// Translate all other join relevant query paths into left outer join tree and attach it to the lead query
function createLeftOuterJoins(query, env)

@@ -181,3 +204,3 @@ {

let ta = query.$tableAliases[tan];
joinTree = createJoinTree(env, joinTree, ta.$qat, 'leftOuter', '$qat', ta.QA);
joinTree = createJoinTree(env, joinTree, ta.$qat, 'leftOuter', '$qat', ta.$QA);
}

@@ -187,3 +210,3 @@ }

// recurse into sub queries
// Recurse into sub queries
query.queries.map(q => createLeftOuterJoins(q, env));

@@ -194,17 +217,16 @@ }

/*
Each leaf node of a table path must end in either a direct or target artifact. During mergePathIntoQat() this 'leaf' artifact
is marked as QA at the corresponding 'leaf' QAT and to the respective $tableAlias which is used to link hte remaining paths to
the correct table alias.
However, subqueries are not considered in the mergePathIntoQat(), so a subquery QA must be created and added separately to
the lead query $tableAlias'es.
Also the name of the subquery (the alias) needs to be set to the final QA alias name.
Each leaf node of a table path must end in either a direct or a target artifact.
During mergePathIntoQat() this 'leaf' artifact is marked as a QA at the corresponding
'leaf' QAT and to the respective $tableAlias which is used to link paths to the correct
table alias. Subqueries are not considered in the mergePathIntoQat(), so a subquery QA
must be created and added separately to the lead query $tableAlias'es.
Also the name of the subquery (the alias) needs to be set to the final QA alias name.
*/
function createQAForFromClauseSubQuery(query, env)
{
// only subqueries of the FROM clause have a name (which is the alias)
// Only subqueries of the FROM clause have a name (which is the alias)
if(query.op.val === 'query' && query.name.id)
{
// set the QA for the outer ON cond paths and rename the query name itself
let QA = query._tableAlias._parent.$tableAliases[query.name.id].QA = createQA(env, query);
// Set the QA for the outer ON cond paths and rename the query name itself
let QA = query._tableAlias._parent.$tableAliases[query.name.id].$QA = createQA(env, query);
incAliasCount(env, QA);

@@ -218,9 +240,9 @@ query.name.id = QA.name.id;

/*
Add an artificial QA to each mixin definition. This QA completes the QAT
Add an artificial QA for each mixin definition. This QA completes the QAT
datastructure that requires a QA at the rootQat before starting the join generation.
This QA is marked as 'mixin' which indicates that the paths of the ON condition must
not receive the usual source and target table alias (which is used for generic associations)
but instead just use the rootQA of the individual ON condition path. These paths are
but instead just use the rootQA of the individual ON condition paths. These paths are
resolved against the FROM clause and must of course be connected to the respective table
aliases
aliases.
*/

@@ -232,8 +254,8 @@ function createQAForMixinAssoc(query, env)

env.lead = query;
// use view as QA origin, don't increment aliasCount
// use view as QA origin
forEachGeneric(query, 'mixin', art => {
if(!art.QA)
if(!art.$QA)
{
art.QA = createQA(env, art.target._artifact);
art.QA.mixin = true; // <=== this is the marker
art.$QA = createQA(env, art.target._artifact);
art.$QA.mixin = true;
}

@@ -250,5 +272,5 @@ });

Rewrite a given path of the native ON condition to TableAlias.ColumnName
and substitute all eventually ocurring foreign key path segments against the respective FK aliases
No flattening of structured leaf types necessary, this is done later in toSQL renderer
and substitute all eventually occurring foreign key path segments against
the respective FK aliases.
No flattening of structured leaf types necessary, this is done in renderer
*/

@@ -265,3 +287,3 @@ function rewriteGenericPaths(pathNode, env)

let pathStr = translateONCondPath(tail).map(ps => ps.id).join(pathDelimiter);
path = [ tableAlias, [ pathStr, pathNode._artifact ] ];
path = [ tableAlias, { id: pathStr, _artifact: pathNode._artifact } ];
}

@@ -278,12 +300,11 @@ else

let ps = pathNode.path[pl--];
let QA = ps._navigation._parent.QA;
let QA = ps._navigation._parent.$QA;
while(!QA && pl >= 0)
{
ps = pathNode.path[pl--];
QA = ps._navigation._parent.QA;
QA = ps._navigation._parent.$QA;
}
if(QA)
path = [ [ QA.name.id, QA._artifact ],
...pathNode.path.slice(pathNode.path.indexOf(ps)).map(ps => [ps.id, ps._artifact ]) ];
path = [ { id: QA.name.id, _artifact: QA._artifact }, ...pathNode.path.slice(pathNode.path.indexOf(ps)) ];
else

@@ -296,5 +317,5 @@ throw Error('No QA found for path ' + pathAsStr(pathNode.path, '"'));

/*
Logically AND the filter conditions of the first path steps of the FROM clause to
the WHERE condition. If no WHERE is specified, create a new one. This step must be done after running
rewriteGenericPaths because otherwise the filter expressions would be traversed twice.
AND filter conditions of the first path steps of the FROM clause to the WHERE condition.
If WHERE does not exist, create a new one. This step must be done after rewriteGenericPaths()
as the filter expressions would be traversed twice.
*/

@@ -305,3 +326,3 @@ function attachFirstFilterConditions(query)

{
if(query._startFilters)
if(query.$startFilters)
{

@@ -311,12 +332,12 @@ if(query.where)

if(query.where.op.val == 'and')
query.where.args.push(...query._startFilters);
query.where.args.push(...query.$startFilters.map(f => [ f ]));
else
query.where = { op: {val: 'and' }, args: [ [query.where], ...query._startFilters ] };
query.where = { op: {val: 'and' }, args: [ [ query.where ], ...query.$startFilters.map(f => [ f ]) ] };
}
else
query.where = query._startFilters.length > 1
? { op: {val: 'and' }, args: query._startFilters }
: query._startFilters;
query.where = query.$startFilters.length > 1
? { op: {val: 'and' }, args: query.$startFilters.map(f => [ f ]) }
: query.$startFilters;
}
// recurse into sub queries
// Recurse into sub queries
query.queries.map(q => attachFirstFilterConditions(q));

@@ -327,5 +348,6 @@ }

/*
transform a QAT into a JOIN tree
Starting from a root (parentQat) follow all QAT children and in case QAT.origin is an association,
create a new JOIN node using the existing joinTree as LHS and the QAT.QA as RHS.
Transform a QATree into a JOIN tree
Starting from a root (parentQat) follow all QAT children and in
case QAT.origin is an association, create a new JOIN node using
the existing joinTree as LHS and the QAT.QA as RHS.
*/

@@ -340,24 +362,25 @@ function createJoinTree(env, joinTree, parentQat, joinType, qatAttribName, lastAssocQA)

if(isEntityOrView(art)) // check if this pathstep is an entity or view
if(isEntityOrView(art))
{
if(!childQat.QA)
childQat.QA = createQA(env, art, childQat._parameters);
incAliasCount(env, childQat.QA);
newAssocLHS = childQat.QA;
if(!childQat.$QA)
childQat.$QA = createQA(env, art, childQat._namedArgs);
incAliasCount(env, childQat.$QA);
newAssocLHS = childQat.$QA;
if(joinTree === undefined) // this is the first artifact in the JOIN tree
if(joinTree === undefined) // This is the first artifact in the JOIN tree
{
joinTree = childQat.QA;
// collect the toplevel filters and add them to the where condition
joinTree = childQat.$QA;
// Collect the toplevel filters and add them to the where condition
if(childQat._filter)
{
// filter conditions are unique for each JOIN, they don't need to be copied
// Filter conditions are unique for each JOIN, they don't need to be copied
let filter = childQat._filter;
rewritePathsInExpression(filter, function(pathNode) {
return [ /* tableAlias=> */[childQat.QA.name.id, childQat.QA._artifact ], /*filterPath=>*/ pathNode.path ];
return [ /* tableAlias=> */ { id: childQat.$QA.name.id, _artifact: childQat.$QA._artifact },
/* filterPath=> */ pathNode.path ]; // eslint-disable-line indent-legacy
});
if(!env.lead._startFilters)
setProp(env.lead, '_startFilters', []);
env.lead._startFilters.push( [ filter ] ); // [ filter ] parenthesizes each filter
if(!env.lead.$startFilters)
env.lead.$startFilters = [];
env.lead.$startFilters.push( filter ); // [ filter ] parenthesizes each filter
}

@@ -373,16 +396,17 @@ }

if(!childQat.QA)
childQat.QA = createQA(env, art.target._artifact, childQat._parameters);
//if(!(mixinJoins && env.location !== 'from') && !childQat.QA)
if((env.fullJoins || env.location === 'from') && !childQat.$QA)
childQat.$QA = createQA(env, art.target._artifact, childQat._namedArgs);
// do not create a JOIN for that assoc if it has no subsequent path steps
// (except for the last path step in the from table path)
if(env.location == 'from' || getChildrenCount(childQat) > 0)
// Do not create a JOIN for that assoc if it has no subsequent path steps
// and only if (mixin) QA is available (except for the last path step in the from table path)
if(childQat.$QA && (env.location == 'from' || getChildrenCount(childQat) > 0))
{
incAliasCount(env, childQat.QA);
joinTree = createJoinQA(joinType, joinTree, childQat.QA, childQat, lastAssocQA);
newAssocLHS = childQat.QA;
incAliasCount(env, childQat.$QA);
joinTree = createJoinQA(joinType, joinTree, childQat.$QA, childQat, lastAssocQA);
newAssocLHS = childQat.$QA;
}
}
}
// follow the children of this qat to append more JOIN nodes
// Follow the children of this QAT to append more JOIN nodes
joinTree = createJoinTree(env, joinTree, childQat[qatAttribName], joinType, qatAttribName, newAssocLHS);

@@ -392,33 +416,3 @@ }

function isEntityOrView(node)
{
if (!node) {
return false;
}
switch (node.kind) {
case 'entity':
case 'view':
return true;
case 'context':
case 'service':
case 'namespace':
case 'type':
case 'annotation':
case 'action':
case 'function':
case 'const':
case 'role':
case 'aspect':
case 'accesspolicy':
case 'element':
case 'query':
case 'param':
case 'enum':
return false;
default:
throw new Error('Unknown artifact kind: ' + node.kind);
}
}
// return the number of direct children to the given qat accross all filters
// Return the number of direct children to the given qat accross all filters
function getChildrenCount(qat)

@@ -435,3 +429,2 @@ {

// creator methods for QAs
function createJoinQA(joinType, lhs, rhs, assocQAT, assocSourceQA)

@@ -442,14 +435,15 @@ {

// 'path steps' for the src/tgt table alias
let srcTableAlias = [assocSourceQA.name.id, assocSourceQA._artifact];
let tgtTableAlias = [assocQAT.QA.name.id, assocQAT.QA._artifact ];
let srcTableAlias = { id: assocSourceQA.name.id, _artifact: assocSourceQA._artifact };
let tgtTableAlias = { id: assocQAT.$QA.name.id, _artifact: assocQAT.$QA._artifact };
let assocElt = assocQAT.origin._artifact;
// Inject the ON condition of the managed association
if(assocElt._finalType.foreignKeys)
{
/*
get both the source and the target column names for the eq term
for the src side provide a path prefix for all paths that is the assocElement name itself preceded by
Get both the source and the target column names for the EQ term.
For the src side provide a path prefix for all paths that is the assocElement name itself preceded by
the path up to the first lead artifact (usually the entity or view) (or in QAT speak: follow the parent
QATs until a QA has been found)
QATs until a QA has been found).
*/

@@ -463,3 +457,6 @@ let srcPaths = flattenElement(assocElt, true, assocElt.name.element.replace(/\./g, pathDelimiter));

// put all src/tgt path siblings into the eq term and create the proper path objects with the src/tgt table alias path steps in front
/*
Put all src/tgt path siblings into the EQ term and create the proper path objects
with the src/tgt table alias path steps in front.
*/
let args = [];

@@ -469,9 +466,10 @@ for(let i = 0; i < srcPaths.length; i++)

args.push({op: {val: '=' },
args: [ constructPathNode( [srcTableAlias, srcPaths[i]] ),
constructPathNode( [tgtTableAlias, tgtPaths[i]] ) ] }); // eslint-disable-line indent-legacy
args: [ constructPathNode( [ srcTableAlias, srcPaths[i] ] ),
constructPathNode( [ tgtTableAlias, tgtPaths[i] ] ) ] }); // eslint-disable-line indent-legacy
}
// parenthesize each AND term
// Parenthesize each AND term
node.on = [ (args.length > 1 ? { op: { val: 'and' }, args: [ ...args.map(a=>[a]) ] } : args[0] ) ];
}
// Inject the ON condition of the unmanaged association
else if (assocElt.onCond || assocElt.on)

@@ -489,6 +487,40 @@ {

if(head.id === '$projection')
throw Error('Following mix-in association "' + assocElt.name.id + '" in defining view is not allowed with ON condition from projection: ' + pathAsStr(pathNode.path, '"'));
throw Error('Following mix-in association "' + assocElt.name.id +
'" in defining view is not allowed with ON condition from projection: ' +
pathAsStr(pathNode.path, '"'));
/*
If all mixin assoc paths would result in the same join node (that is exactly
one shared QAT for all mixin path steps) it would be sufficient to reuse the
definition QA (see createQAForMixinAssoc()) for sharing the table alias.
As mixin assoc paths may have different filter conditions, separate QATs are
created for each distinct filter, resulting in separate JOIN trees requiring
individual table aliases. This also requires separate QAs at the assoc QAT
to hold the individual table aliases (that's why the definition QA is cloned
in mergePathIntoQAT()).
Paths in the ON condition referring to the target side are linked to the
original mixin QA via head._navigation (done by the compiler), which in turn
is childQat._parent (a mixin assoc path step MUST be path root, so _parent
IS the mixin definition. Mixin QATs are created at the mixin definition).
In order to create the correct table alias path, the definition QA must
be replaced with the current childQat.QA (the clone with the correct alias).
The original QA is used as template for its clones and can safely be replaced.
Example:
select from ... mixin { toTgt: association to Tgt on toTgt.elt = elt; }
into { toTgt[f1].field1, toTgt[f2].field2 };
toTgt definition has definition QA, ON cond path 'toTgt' refers to definition QA.
assoc path 'toTgt[f1].' and 'toTgt[f2]' have separate QATs with QA clones.
'toTgt.elt' must now be rendered for each JOIN using the correct QA clone.
*/
if(assocQAT.$QA.mixin)
assocQAT._parent.$QA = assocQAT.$QA;
return constructTableAliasAndTailPath(path);
}
else
else // ON condition of non-mixin association
{

@@ -503,3 +535,3 @@ if(head.id === assocQAT.name.id) // target side

{
// eventually remove $self from path
// eventually remove $self and $projection from path
let isAbsolutePath = head.id === '$self';

@@ -511,6 +543,6 @@ if(isAbsolutePath || head.id === '$projection')

// if path is not an absolute path, prepend element prefix
path = translateONCondPath(path, !isAbsolutePath ? assocElt.elementPrefix : undefined);
path = translateONCondPath(path, !isAbsolutePath ? assocElt.$elementPrefix : undefined);
}
}
return [tableAlias, path];
return [ tableAlias, path ];
});

@@ -520,3 +552,4 @@ }

{
throw Error('assocQAT has neither foreign keys nor an on condition:' + assocElt.name.absolute + pathDelimiter + assocElt.name.element);
throw Error('assocQAT has neither foreign keys nor an on condition:' +
assocElt.name.absolute + pathDelimiter + assocElt.name.element);
}

@@ -526,3 +559,3 @@

{
// filter conditions are unique for each JOIN, they don't need to be copied
// Filter conditions are unique for each JOIN, they don't need to be copied
let filter = assocQAT._filter;

@@ -533,3 +566,3 @@ rewritePathsInExpression(filter, function(pathNode) {

// if toplevel ON cond op is AND add filter condition to the args array,
// If toplevel ON cond op is AND add filter condition to the args array,
// create a new toplevel AND op otherwise

@@ -539,5 +572,7 @@ let onCond = (Array.isArray(node.on) ? node.on[0] : node.on);

if(onCond.op.val == 'and')
onCond.args.push( [ filter ] ); // parenthesize filter
// parenthesize filter
onCond.args.push( [ filter ] );
else
node.on = [ { op: { val: 'and' }, args: [ [ onCond ], [ filter ] ] } ]; // parenthesize onCond and filter
// parenthesize onCond and filter
node.on = [ { op: { val: 'and' }, args: [ [ onCond ], [ filter ] ] } ];
}

@@ -549,6 +584,6 @@

/*
A QA (QueryArtifact) is a representative forn a table/view that must appear in the FROM clause
either named directly or indirectly through an association
A QA (QueryArtifact) is a representative for a table/view that must appear
in the FROM clause either named directly or indirectly through an association.
*/
function createQA(env, artifact, parameters, alias)
function createQA(env, artifact, namedArgs, alias)
{

@@ -558,7 +593,3 @@ if(alias === undefined)

let node = constructPathNode([ [ artifact.name.absolute, artifact ] ], alias);
//TODO: add parameter list to path
if(parameters)
setProp(node, '_parameters', parameters);
let node = constructPathNode([ { id: artifact.name.absolute, _artifact: artifact, namedArgs } ], alias);
return node;

@@ -576,10 +607,9 @@ }

/*
recursively walk over expression and replace any found path against a new
path consisting of two path steps.
The first path step is the table alias and the second path step is
the concatenated string of the original path steps. The leaf _artifact
of pathNode is used as the leaf artifact of the new path string.
Recursively walk over expression and replace any found path with a new
path consisting of two path steps. The first path step is the table alias
and the second path step is the concatenated string of the original path steps.
Leaf _artifact of pathNode is used as the leaf artifact of the new path string.
Both the table alias and the original (remaining) path steps are
returned from getTableAliasAndPathSteps()
Both the table alias and the original (remaining) path steps are to be produced
by getTableAliasAndPathSteps().

@@ -597,3 +627,3 @@ tableAlias = [ aliasName, _artifact ]

let pathStr = path.map(ps => ps.id).join(pathDelimiter);
replaceNodeContent(pathNode, constructPathNode([tableAlias, [pathStr, pathNode._artifact]]));
replaceNodeContent(pathNode, constructPathNode([ tableAlias, { id: pathStr, _artifact: pathNode._artifact } ]));
} ]

@@ -606,3 +636,3 @@ };

Return a new CSN path object constructed from an array of pathSteps
path steps is an array of [ 'pathStep id', _artifact reference ]
path steps is an array of [ 'pathStep id', _artifact reference, namedArgs (optional) ]
Alias is optional

@@ -616,4 +646,10 @@ The final _artifact ref is set as _artifact ref to the path

path : pathSteps.map(p => {
let o = Object.assign({}, { id: p[0] });
setProp(o, '_artifact', p[1]);
let o = Object.assign({}, { id: p.id });
setProp(o, '_artifact', p._artifact );
if(p.where)
o.where = p.where;
if(p.namedArgs)
o.namedArgs = p.namedArgs;
if(p.args)
o.args = p.args;
return o; })

@@ -626,3 +662,3 @@ };

// set the leaf artifact
setProp(node, '_artifact', pathSteps[pathSteps.length-1][1]);
setProp(node, '_artifact', pathSteps[pathSteps.length-1]._artifact);
return node;

@@ -632,17 +668,22 @@ }

/*
replace the content of the old node with the new one
Replace the content of the old node with the new one.
If newNode is a join tree (rewritten FROM path), oldPath must be cleared first.
If newNode is a path => oldNode._artifact === newNode._artifact, no need to
exchange _artifact (as non-iterable property it is not assigned).
*/
function replaceNodeContent(oldNode, newNode)
{
Object.keys(oldNode).forEach(k => {
delete oldNode[k] });
delete oldNode._artifact;
delete oldNode._status;
// If newNode is a join tree, throw away old path content
if(newNode.op) {
Object.keys(oldNode).forEach(k => {
delete oldNode[k] });
delete oldNode._artifact;
}
Object.assign(oldNode, newNode);
}
/* collect all of paths to all leafs for a given element
respecting the src or the target side of the ON condition
return an array of column names and it's leaf element
/*
Collect all of paths to all leafs for a given element
respecting the src or the target side of the ON condition.
Return an array of column names and it's leaf element.
*/

@@ -653,3 +694,3 @@ function flattenElement(element, srcSide, prefix)

if(!element._finalType.foreignKeys && !element.elements)
return [ [ prefix, element ] ];
return [ { id: prefix, _artifact: element } ];

@@ -688,3 +729,5 @@ let paths = [];

}
return paths.map(p => [(prefix ? prefix + pathDelimiter + p[0] : p[0]), p[1] ] );
return paths.map(p => {
return { id: (prefix ? prefix + pathDelimiter : '' ) + p.id, _artifact: p._artifact }
} );
}

@@ -694,3 +737,3 @@

/*
construct both the TA path step and the path tail for a given path array from the AST
Construct both the TA path step and the path tail for a given AST path array
*/

@@ -700,15 +743,13 @@ function constructTableAliasAndTailPath(path)

let [head, ...tail] = path;
let QA = head._navigation.QA || head._navigation._parent.QA;
let QA = head._navigation.$QA || head._navigation._parent.$QA;
// first path step is table alias, use it and pop it off
if(head._navigation.QA)
// First path step is table alias, use and pop it off
if(head._navigation.$QA)
path = tail;
let tableAlias = [ QA.name.id, QA._artifact ];
return [ tableAlias, path ];
return [ /* tableAlias => */ { id: QA.name.id, _artifac: QA._artifact }, path ];
}
/*
Translate ON cond paths and substitute FK aliases
return array of [ [pathName, _artifact] ]
Translate ON cond paths and substitute FK aliases
*/

@@ -723,10 +764,10 @@ function translateONCondPath(path, prefix)

}
// artifact is not needed, return value must fit in rewritePath expectations (array of path objects with id }
return [ { id: (prefix ? prefix + fkPrefix : fkPrefix) } ];
return [ { id: (prefix ? prefix + fkPrefix : fkPrefix), _artifact: path[path.length-1]._artifact } ];
}
/*
munch path steps and append them to a path string until an assoc step is found. The assoc path step is also
appended to the path string. If no assoc path step has occured, all path steps are added to the path string
and tail is empty.
Munch path steps and append them to a path string until an
assoc step is found. The assoc path step is also appended
to the path string. If no assoc path step has occured, all
path steps are added to the path string and tail is empty.

@@ -750,6 +791,4 @@ Return assocPathStep, the remaining tail path and the path string

Substitute the n first path steps of a given path against a FK alias name.
Resolve a foreign key of an (managaged) association by following the n first path steps of a given path.
The longest path matches:
Resolve a foreign key of a managaged association by following the n first
path steps. Longest path matches:
Example: fk tuple { a.b, a.b.c, a.b.e },

@@ -760,3 +799,3 @@ path: a.b.c.d.e.f: FK a.b.c is found, even if FK a.b is one level higher in the prefix tree.

Return remaining tail path and the path string
Return remaining tail path and the path string.
*/

@@ -766,3 +805,3 @@

{
let ppt = assocStep._artifact.fkPathPrefixTree.children;
let ppt = assocStep._artifact.$fkPathPrefixTree.children;
let fk = undefined; // last found FK

@@ -793,3 +832,3 @@ let fkPs = undefined; // last path step that found FK

let tail = path.slice(path.indexOf(fkPs)+1);
// if foreign key is an association itself, apply substituteFKAliasForPath on tail
// If foreign key is an association itself, apply substituteFKAliasForPath on tail
if(fk && fk.targetElement._artifact.target && tail.length)

@@ -801,3 +840,2 @@ return substituteFKAliasForPath(fk.targetElement, tail, pathStr);

/*

@@ -813,7 +851,8 @@ Catch the most basic path constraint violations that are not yet covered elsewhere

{
if(pathDict._artifact._finalType.elements)
signal(error`Only scalar types allowed in this location of a query: ' + pathAsStr(pathDict.path, "'")`);
if(path[path.length-1].id != '$self' && pathDict._artifact._finalType.elements)
signal(error`${'Only scalar types allowed in this location of a query: ' + pathAsStr(pathDict.path, "'")}`);
let [head, ...tail] = path;
// pop head if it is a table alias or $projection
if(env.tableAliases && env.tableAliases.includes(head.id) || head.id === '$projection')

@@ -833,4 +872,5 @@ path = tail;

let la1 = pathDict.path[pathDict.path.indexOf(ps)+1];
if(la1 && !ps._artifact._finalType.fkPathPrefixTree.children[la1.id])
signal(error`Pathstep ' + la1.id + ' is not foreign key of association ' + ps.id + ' in ON condition path: ' + pathAsStr(pathDict.path)`);
if(la1 && !ps._artifact._finalType.$fkPathPrefixTree.children[la1.id])
signal(error`Pathstep ' + la1.id + ' is not foreign key of association ' +
ps.id + ' in ON condition path: ' + pathAsStr(pathDict.path)`);
}

@@ -843,3 +883,4 @@ }

if(pathDict.path[0].id === '$projection')
signal(error`'$projection' outside of ON conditions not supported: " + pathAsStr(pathDict.path)`);
signal(error`'$projection' outside of ON conditions not supported: " +
pathAsStr(pathDict.path)`);
}

@@ -849,18 +890,19 @@ }

/*
Create path prefix trees and merge paths into the trees depending on the path location.
There are three prefix trees for FROM table paths, ON conditions (of either JOINs in FROM clause or
of ad-hoc associations) and all other paths. It is not the job of this transformer to semantically
check for illegal association path steps in the various clauses of the query.
Create path prefix trees and merge paths into the trees depending on the path location.
There are prefix trees for FROM table paths and all other paths. Paths of ON conditions
(of either JOINs in FROM clause or of mixin associations) are not added to the QATree,
as no associations can be followed in these paths. It is not the job of this transformer
to semantically check for illegal association path steps in the various clauses of the query.
All prefix trees are located underneath the $tableAlias structure and are distinguished
by their attribute $qat, $fqat and $nqat. Each path step appears exactly once for a given filter condition
in the prefix tree and has a link to it's definition (origin). The default filter is an empty string ''.
All prefix trees are put underneath the $tableAlias structure with attribute $qat or $fqat.
Each path step appears exactly once for a given filter condition in the prefix tree and
has a link to it's definition (origin). The default filter is an empty string ''.
A special note on paths in filter conditions. Filter paths are treated like postfix
paths to an association path step, meaning, they are inserted into the assoc's $qat or $fqat depending on where
the association was traversed. As HANA CDS doesn't allow to traverse assocs in filter path, this is checked
in flyTrap above.
A node in the path prefix tree is abbreviated as QAT (which stands for query association tree, a term
originating from way back in time).
A special note on paths in filter conditions. Filter paths are treated like postfix
paths to an association path step, meaning, they are inserted into the assoc's $qat or $fqat
depending on where the association was traversed.
As HANA CDS doesn't allow to traverse assocs in filter paths, this is checked in flyTrap above.
A node in the path prefix tree is abbreviated as QAT (which stands for Query Association Tree,
a term originating from way back in time).
*/

@@ -878,3 +920,3 @@ function mergePathIntoQAT(pathDict, env)

if(env.location == 'onCondFrom')
return; //qatChildrenName = '$nqat'
return;

@@ -890,3 +932,4 @@ let [head, ...tail] = path;

{
// speciality for OrderBy: If path has no _navigation don't merge it. Path is alias to select item expression
// speciality for OrderBy: If path has no _navigation don't merge it.
// Path is alias to select item expression
if(env.location === 'OrderBy')

@@ -905,16 +948,17 @@ return;

}
// all other paths have a _navigation attribute
// All other paths have a _navigation attribute
else if(head._navigation)
{
// Always start with QAT merge at $tableAlias, even if path doesn't start there:
// First identify $tableAlias (must be either head or head's parent)
// The resolver sets a _navigation at the very first path step that either points to
// $tableAlias or to a top level element from $combined which itself parent's to $tableAlias).
/*
Always start with QAT merge at $tableAlias, even if path doesn't start there.
First identify $tableAlias (must be either head or head's parent) The resolver
sets a _navigation at the very first path step that either points to $tableAlias
or to a top level element from $combined which itself parent's to $tableAlias).
*/
if(head._navigation.kind == '$navElement')
{
qatParent = head._navigation._parent;
tail = path; // start with the full path (no table alias prefix)
tail = path; // Start with the full path (no table alias prefix)
}
else if(head._navigation.kind == 'element') // this is a mixin assoc
else if(head._navigation.kind == 'element') // This is a mixin assoc
{

@@ -924,3 +968,3 @@ qatParent = head._navigation;

}
else // head is a table alias already
else // Head is a table alias already
{

@@ -936,42 +980,69 @@ qatParent = head._navigation;

// create the very first QAT if it doesn't exist yet (no filter condition for table alias prefix)
// Create the very first QAT if it doesn't exist
// (filter condition for table alias prefix not allowed)
let qatChildren = createQATChildren(qatParent);
let qat = undefined;
for(let pathStep of tail)
{
// if the current path step has not yet been inserted into the child list of
// the parent QAT, create a new QAT (linkToOrigin) and a dictionary for the
// subsequent path steps (a separate one for each filter condition).
let filterStr = '';
/*
If the current path step has not yet been inserted into the list of children at
the parent QAT, create a new QAT (linkToOrigin) and a dictionary for subsequent
path steps (a separate one for each filter condition).
*/
let qatName = pathStep.id;
if(pathStep.where)
filterStr = JSON.stringify(compactCondOrExpr(pathStep.where));
qatName += JSON.stringify(compactNode(pathStep.where));
qat = qatChildren[pathStep.id + filterStr];
if (!qat)
if(pathStep.namedArgs) {
// sort named arguments
let sortedNamedArgs = Object.create(null);
Object.keys(pathStep.namedArgs).sort().forEach(p => {
sortedNamedArgs[p] = pathStep.namedArgs[p];
})
qatName += JSON.stringify(compactNode(sortedNamedArgs));
}
qat = qatChildren[qatName];
if (!qat)
{
qat = linkToOrigin(pathStep._artifact, pathStep.id, qatParent, undefined, pathStep.location);
// this is for mixin associations: if a mixin assoc is traversed, it's definition already has a QA, attach it to the qat
// to make sure that the same alias is used.
if(qat.origin._artifact.QA)
qat.QA = qat.origin._artifact.QA;
if(pathStep.where)
setProp(qat, '_filter', pathStep.where);
qat._filter = pathStep.where;
if(pathStep.namedArgs)
qat._namedArgs= pathStep.namedArgs;
/*
If qat.origin._artifact has a QA, it must be a mixin association
(No other QA's have been created so far). Clone new QA from this
template to have space for table aliases (if mixin assoc is
followed with different filter conditions).
*/
if(qat.origin._artifact.$QA) {
qat.$QA = clone(qat.origin._artifact.$QA);
if(qat._namedArgs)
qat.$QA.path[0].namedArgs = qat._namedArgs;
}
qat.kind = '$navElement';
qatChildren[pathStep.id+filterStr] = qat;
qatChildren[qatName] = qat;
}
qatChildren = createQATChildren(qat);
qatParent = qat; // now the current qat becomes parent to the next level of children
qatParent = qat; // Current qat becomes parent to the next level of children
setProp( pathStep, '_navigation', qat );
}
/* If this path terminates on either an entity or an association
(from clause, published Ad-Hoc Assocs), create a QA and attach it
to the (leaf) QAT and to the rootQAT (which is the tableAlias).
This QA will later serve as the initial 'lastAssocQA'
to all other join relevant paths of the query that originate from this alias.
Also this is the only place where the from table path alias is accessible.
*/
if(!qat)
throw Error('No leaf qat for head: ' + head + ' tail: ' + pathAsStr(tail, '"') + ' produced');
/*
If path terminates on an entity or an association (from clause,
published Ad-Hoc Assocs), attach QA to the (leaf) QAT and to the
rootQAT (which is the tableAlias).
This QA will later serve as the initial 'lastAssocQA' for all other
join relevant paths that originate from this alias. Also this is the
only place where the original FROM alias is available.
*/
let art = qat.origin._artifact;

@@ -986,11 +1057,12 @@ if(!modelUtils.isArtifact(art))

{
// if rootQat ($tableAlias) already has a QA, reuse it, otherwise create a new one
if(!rootQat.QA)
// If rootQat ($tableAlias) already has a QA, reuse it, create a new one otherwise.
if(!rootQat.$QA)
{
// Use the original FROM alias if available!
let alias = pathDict.name ? pathDict.name.id : undefined;
rootQat.QA = qat.QA = createQA(env, art, pathDict._parameters, alias);
rootQat.$QA = qat.$QA = createQA(env, art, qat._namedArgs, alias);
}
}
// return or create a new children dictionary for a given QAT
// Return or create a new children dictionary for a given QAT
// Children are grouped under the filter condition that precedes them.

@@ -1005,3 +1077,3 @@ function createQATChildren(parentQat)

// crawl all relevant sections of the AST for paths
// Crawl all relevant sections of the query AST for paths
function walkQuery(query, env)

@@ -1023,4 +1095,2 @@ {

// maybe not very sharp: walkover=select will unlock
// all generic paths
env.location = 'select';

@@ -1046,8 +1116,6 @@ if(env.walkover[env.location])

walk(query.offset, env);
// TODO: which clauses are missing
// union, intersect, except?
}
}
// finally apply walkQuery on all sub queries
// walk all subqueries of this query
query.queries.map(q => walkQuery(q, env));

@@ -1095,3 +1163,3 @@

{
// in some expressions queries can occur, do not follow them as they
// In some expressions queries can occur, do not follow them as they
// are walked as member of the queries array

@@ -1105,3 +1173,3 @@ if(!env || !node || (node && node.op && node.op.val == 'query'))

// ask for Array before typeof object (which would also be true for Array)
// Ask for Array before typeof object (which would also be true for Array)
if(Array.isArray(node))

@@ -1118,4 +1186,7 @@ node.map(n => walk(n, env));

let path = node['path'];
// don't bite into paths that that have no artifact (function calls etc)
if(path && path[0]._artifact)
// Ignore paths that that have no artifact (function calls etc) or that are builtins ($now, $user)
// or that are parameters ($parameters or escaped paths (':')
//path.length && path[ path.length-1 ]._artifact
let art = path && path.length && path[path.length-1]._artifact;
if(art && !['$builtin', '$parameters', 'param'].includes(art.kind))
{

@@ -1135,4 +1206,5 @@ if(env.callback)

{
// walk over all filter expressions (not JOIN relevant,
// cannot be detected in generic walk
// Walk over all filter expressions (not JOIN relevant,
// cannot be detected in generic walk. Store path step
// to which this filter was attached to in filterEnt.pathStep
path.filter(pathStep=>pathStep.where).forEach(pathStep => {

@@ -1164,2 +1236,33 @@ filterEnv.pathStep = pathStep;

function isEntityOrView(node)
{
if (!node) {
return false;
}
switch (node.kind) {
case 'entity':
case 'view':
return true;
case 'context':
case 'service':
case 'namespace':
case 'type':
case 'annotation':
case 'action':
case 'function':
case 'const':
case 'role':
case 'aspect':
case 'accesspolicy':
case 'element':
case 'query':
case 'param':
case 'enum':
return false;
default:
throw new Error('Unknown artifact kind: ' + node.kind);
}
}
function clone(obj) {

@@ -1178,3 +1281,3 @@ let newObj;

let props = Object.getOwnPropertyNames(obj); // we clone only own properties, not inherited one's
let props = Object.getOwnPropertyNames(obj); // clone own properties only, not inherited ones
for (let p of props) {

@@ -1192,5 +1295,5 @@ let pd = Object.getOwnPropertyDescriptor(obj, p);

}
} // end of translateAssocsToJoins
}
module.exports = { translateAssocsToJoins };
{
"name": "@sap/cds-compiler",
"version": "1.1.1",
"version": "1.5.0",
"dependencies": {

@@ -23,12 +23,12 @@ "ajv": {

"commander": {
"version": "2.14.0"
"version": "2.17.1"
},
"fs-extra": {
"version": "5.0.0",
"version": "7.0.0",
"dependencies": {
"universalify": {
"version": "0.1.1"
"version": "0.1.2"
},
"graceful-fs": {
"version": "4.1.11"
"version": "4.1.15"
},

@@ -39,3 +39,3 @@ "jsonfile": {

"graceful-fs": {
"version": "4.1.11"
"version": "4.1.15"
}

@@ -42,0 +42,0 @@ }

@@ -1,1 +0,1 @@

{"bin":{"cdsc":"bin/cdsc.js"},"bundleDependencies":false,"dependencies":{"ajv":"6.1.1","antlr4":"4.7.1","commander":"2.14.0","fs-extra":"5.0.0","resolve":"1.5.0","sax":"1.2.4"},"deprecated":false,"description":"Standard-Feature-Set Vanilla-CDS in Product Quality","keywords":["CDS"],"main":"lib/main.js","name":"@sap/cds-compiler","repository":{"type":"git","url":"git@github.wdf.sap.corp/CDS/cds-compiler.git"},"version":"1.1.1","license":"SEE LICENSE IN developer-license-3.1.txt"}
{"bin":{"cdsc":"bin/cdsc.js"},"bundleDependencies":false,"dependencies":{"ajv":"6.1.1","antlr4":"4.7.1","commander":"2.17.1","fs-extra":"7.0.0","resolve":"1.5.0","sax":"1.2.4"},"deprecated":false,"description":"Standard-Feature-Set Vanilla-CDS in Product Quality","keywords":["CDS"],"main":"lib/main.js","name":"@sap/cds-compiler","repository":{"type":"git","url":"git@github.wdf.sap.corp/CDS/cds-compiler.git"},"version":"1.5.0","license":"SEE LICENSE IN developer-license-3.1.txt"}

@@ -17,11 +17,14 @@ # Getting started

### Snapshots
### Snapshots/Milestones/Releases
Unfortunately npm does not support snapshots via nexus.
The only possibility is to download manually a snapshot and install it.
Configure Nexus registry:
### Milestones/Releases
* snapshots
Configure Nexus **milestones** registry:
```
npm config set registry "http://nexus.wdf.sap.corp:8081/nexus/content/groups/build.snapshots.npm"
```
* milestones
```

@@ -31,3 +34,3 @@ npm config set registry "http://nexus.wdf.sap.corp:8081/nexus/content/groups/build.milestones.npm"

or **releases** registry:
* releases

@@ -46,6 +49,5 @@ ```

package.json
```
"dependencies": {
"@sap/cds-compiler": "*"
"@sap/cds-compiler": "latest"
}

@@ -52,0 +54,0 @@ ```

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is not supported yet

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap

Packages

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc