Socket
Socket
Sign inDemoInstall

@sap/cds-compiler

Package Overview
Dependencies
Maintainers
1
Versions
105
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@sap/cds-compiler - npm Package Compare versions

Comparing version 4.1.2 to 4.2.2

lib/checks/manyNavigations.js

9

bin/cdsc.js

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

// If set through CLI (and not options file), `beta` is a string and needs processing.
if (cmdLine.options.beta && typeof cmdLine.options.beta === 'string') {
if (typeof cmdLine.options.beta === 'string') {
const features = cmdLine.options.beta.split(',');

@@ -195,4 +195,5 @@ cmdLine.options.beta = {};

// If set through CLI (and not options file), `deprecated` is a string and needs processing.
if (cmdLine.options.deprecated && typeof cmdLine.options.deprecated === 'string') {
// If set through CLI (and not options file), `deprecated` and `moduleLookupDirectories`
// are strings and needs processing.
if (typeof cmdLine.options.deprecated === 'string') {
const features = cmdLine.options.deprecated.split(',');

@@ -204,2 +205,4 @@ cmdLine.options.deprecated = {};

}
if (typeof cmdLine.options.moduleLookupDirectories === 'string')
cmdLine.options.moduleLookupDirectories = cmdLine.options.moduleLookupDirectories.split(',');

@@ -206,0 +209,0 @@ parseSeverityOptions(cmdLine);

@@ -10,2 +10,82 @@ # ChangeLog for cds compiler and backends

## Version 4.2.2 - 2023-08-31
### Fixed
- to.sql|hdi.migration: Fix bug that caused a migration to be rendered for the HANA CDS types that were removed from the CSN
## Version 4.2.0 - 2023-08-29
### Added
- Compiler:
+ Option `moduleLookupDirectories: [ 'strings' ]` can be used to specify additional module lookup
directories, similar to `node_modules/`.
+ LIMIT and OFFSET clauses can now contain expressions, including parameter references.
- to.edm(x):
+ Detect spec violation `scale` > `precision`.
- to.sql/for.odata:
+ With the new option `fewerLocalizedViews: true|false`, an entity/view will not get a localized convenience
view, if it only has associations to localized entities/views. Only if there is actually a localized
element (e.g. new localized element or reference to one), will it get a convenience view.
- to.sql
+ In a localized scenario, create foreign key constraints for the generated `.texts` entities.
+ Casting `null` to a structure such as `null as field : StructT` is now supported. For each leaf element,
an additional `null as field_name` column is added.
### Changed
- Compiler:
+ Selecting fields of structures or associations (without filters) are now candidates for ON-condition
rewrites: It is no longer necessary to select the struct/association directly.
+ Consistently handle the case when type elements are defined to be a `key`:
the `key` property is not only preserved with `includes`, but also in other cases.
Use option `deprecated.noKeyPropagationWithExpansions` to switch to the v3 behavior.
+ When including aspects or entities into structured type definitions,
do not add actions to the type.
+ An `annotate` statement in the `extensions` section of a CSN now consistently uses the
`elements` property even if the `annotate` is intended to be used for an enum symbol.
Before v4.2, the compiler has used the `enum` property in a CSN of flavor `xtended`
(`gensrc`) if it was certain that it was to be applied to an enum symbol.
- to.cdl: If a definition has an `actions` property, an `actions {…}` block is now always rendered,
and is not ignored if empty.
- to.sql:
+ For SQL dialect "sqlite", `cds.DateTime` is now rendered as `DATETIME_TEXT` instead of `TIMESTAMP_TEXT`.
+ Casting a literal (except `null`) to a structure now yields a proper error.
+ `.texts` entities annotated with `@cds.persistence.skip` (without their original entity having that annotation)
lead to deployment issues later. It is now an error.
### Fixed
- Compiler:
+ Reject invalid reference in the `on` of `join`s already while compiling,
not just when calling the (SQL) backend.
+ Correct the calculation of annotation assignments on the return structure of actions
when both `annotate … with {…}` and `annotate … with returns {…}` had been used
on the same structure element. Ensure that it works when non-applied, too.
+ Do not remove or invent `actions` properties with zero actions or functions in it.
+ Correct auto-redirection of direct cycle associations: if the source and target of a
model association are the same entity, and the main artifact of the service association
based on the model association is a suitable auto-redirection target, then use it
as new target, independently from the value of `@cds.redirection.target`.
- to.cdl: Indirectly structured types (`type T: Struct;`) with `includes` (`extend T with T2;`), are now properly rendered.
- to.sql/hdi/hdbcds:
+ Views on views with parameters did not have localized convenience views based on
other localized views (missing `localized.` prefix in FROM clause)
+ Run less checks on entities marked with `@cds.persistence.exists`
+ Correctly render SELECT items where the column name conflicts with a table alias
- to.sql: Casting expressions to a structured type yields a proper error instead of strange compiler error.
- to.edm(x):
+ Don't expand `@mandatory` if element has an annotation with prefix `@Common.FieldControl.`.
+ Fix a bug when referencing nested many structures, especially referring to a managed association via
`$self` comparison.
+ Improve handling of non-collection annotation values for collection-like vocabulary types.
+ Don't render `Scale: variable` for `cds.Decimal(scale:0)`.
- to.sql|hdi.migration:
+ Fixed a bug that caused rendering of `ALTER` statements to abort early and not render some statements.
+ CSN output now only contains real `cds` builtins, no early remapping to HANA CDS types or similar.
- to.sql.migration: Don't drop-create views marked with `@cds.persistence.exists`
### Removed
## Version 4.1.2 - 2023-07-31

@@ -15,3 +95,4 @@

- to.hdi.migration: Changes in constraints are not rendered as part of the .hdbmigrationtable file, as they belong in other HDI artifacts
- to.hdi.migration: Changes in constraints are not rendered as part of the .hdbmigrationtable file,
as they belong in other HDI artifacts

@@ -201,3 +282,22 @@ ## Version 4.1.0 - 2023-07-28

## Version 3.9.10 - 2023-08-25
### Fixed
- to.edm(x): Error reporting for incorrect handling of "Collection()" has been improved.
- to.sql/hdi/hdbcds: Views on views with parameters did not have localized convenience views based on
other localized views (missing `localized.` prefix in FROM clause)
- to.sql: Casting expressions to a structured type yields a proper error instead of strange compiler error.
- to.sql.migration: Don't drop-create views marked with `@cds.persistence.exists` or `@cds.persistence.skip`
## Version 3.9.8 - 2023-08-03
### Fixed
- to.edm(x):
+ Don't expand `@mandatory` if element has an annotation with prefix `@Common.FieldControl.`.
+ Fix a bug when referencing nested many structures, especially referring to a managed association via
`$self` comparison.
- to.sql/hdi/hdbcds: Detect navigation into arrayed structures and raise helpful errors instead of running into internal errors.
## Version 3.9.6 - 2023-07-27

@@ -204,0 +304,0 @@

@@ -76,2 +76,7 @@ # ChangeLog of Beta Features for cdx compiler and backends

### Added `annotationExpressions`
This option allows to use expressions as annotation values, e.g.
`@anno: (1+2)`.
### Added `calculatedElements`

@@ -78,0 +83,0 @@

@@ -14,2 +14,17 @@ # ChangeLog of deprecated Features for cdx compiler and backends

## Version 4.2.0 - 2023-08-29
### Added `noKeyPropagationWithExpansions`
When this option is set, element `id` in types `Orig` and `I` are keys,
but `id` in `D` is not.
```cds
type Orig { key id: Integer };
type I: Orig {};
type D: Orig;
```
When this option is not set, element `id` in all three types are keys.
## Version 4.0.0 - 2023-06-06

@@ -16,0 +31,0 @@

@@ -403,4 +403,3 @@ /** @module API */

// TODO: exists, abstract? isPersistedOnDb?
if (diffArtifact && diffArtifact['@cds.persistence.name'] && !diffArtifact['@cds.persistence.skip'] &&
(diffArtifact.query || diffArtifact.projection) &&
if (diffArtifact && diffArtifact['@cds.persistence.name'] && csnUtils.isPersistedAsView(diffArtifact) &&
(diffArtifact[modelCompare.isChanged] === true || // we know it changed because we compared two views

@@ -413,2 +412,3 @@ diffArtifact[modelCompare.isChanged] === undefined)) { // if it was removed in the after, then we don't have the flag

diffArtifact.kind === beforeArtifact.kind && // detect action -> entity
csnUtils.isPersistedAsTable(diffArtifact) === csnUtils.isPersistedAsTable(beforeArtifact) && // detect removal of @cds.persistence.exists
csnUtils.isPersistedAsView(diffArtifact) === csnUtils.isPersistedAsView(beforeArtifact) // detect view -> entity

@@ -415,0 +415,0 @@ ) { // don't render again, but need info for primary key extension

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

'betterSqliteSessionVariables',
'fewerLocalizedViews',
// ODATA

@@ -56,3 +57,2 @@ 'odataOpenapiHints',

'lintMode',
'fuzzyCsnError',
'traceFs',

@@ -71,3 +71,3 @@ 'traceParser',

'disableHanaComments', // in case of issues with hana comment rendering
'localizedWithoutCoalesce', // deprecated version of 'localizedLanguageFallback', TODO(v4): Remove option
'localizedWithoutCoalesce', // deprecated version of 'localizedLanguageFallback', TODO(v5): Remove option
];

@@ -74,0 +74,0 @@

@@ -133,22 +133,24 @@ 'use strict';

// Note: if `validate()` returns true, it means the option is _invalid_!
const allCombinationValidators = {
'valid-structured': {
validate: options => options.odataVersion === 'v2' && options.odataFormat === 'structured',
severity: 'error',
getParameters: () => {},
getMessage: () => 'Structured OData is only supported with OData version v4',
'valid-structured': (options, message) => {
if (options.odataVersion === 'v2' && options.odataFormat === 'structured')
message.error('api-invalid-combination', null, { '#': 'valid-structured' });
},
'sql-dialect-and-naming': {
validate: options => options.sqlDialect && options.sqlMapping && ![ 'hana' ].includes(options.sqlDialect) && [ 'quoted', 'hdbcds' ].includes(options.sqlMapping),
severity: 'error',
getParameters: options => ({ name: options.sqlDialect, prop: options.sqlMapping }),
getMessage: () => 'sqlDialect $(NAME) can\'t be combined with sqlMapping $(PROP)',
'sql-dialect-and-naming': (options, message) => {
if (options.sqlDialect && options.sqlMapping && options.sqlDialect !== 'hana' && [ 'quoted', 'hdbcds' ].includes(options.sqlMapping))
message.error('api-invalid-combination', null, { '#': 'sql-dialect-and-naming', name: options.sqlDialect, prop: options.sqlMapping });
},
'beta-no-test': {
validate: options => options.beta && !options.testMode,
severity: 'warning',
getParameters: () => {},
getMessage: () => 'Option "beta" was used. This option should not be used in productive scenarios!',
'sql-dialect-and-localized': (options, message) => {
if (options.fewerLocalizedViews && options.sqlDialect === 'hana')
message.error('api-invalid-combination', null, { '#': 'sql-dialect-and-localized', option: 'fewerLocalizedViews', value: 'hana' });
},
'beta-no-test': (options, message) => {
if (options.beta && !options.testMode)
message.warning('api-unexpected-combination', null, { '#': 'beta-no-test', option: 'beta' });
},
};
const alwaysRunValidators = [ 'beta-no-test', 'sql-dialect-and-localized' ];
/* eslint-disable jsdoc/no-undefined-types */

@@ -176,7 +178,8 @@ /**

if (!validator.validate(optionValue)) {
error('invalid-option', null, {
error('api-invalid-option', null, {
'#': 'value',
prop: optionName,
value: validator.expected(optionValue),
othervalue: validator.found(optionValue),
}, 'Expected option $(PROP) to have $(VALUE). Found: $(OTHERVALUE)');
});
}

@@ -189,7 +192,4 @@ });

for (const combinationValidatorName of combinationValidators.concat([ 'beta-no-test' ])) {
const combinationValidator = allCombinationValidators[combinationValidatorName];
if (combinationValidator.validate(options))
message[combinationValidator.severity]('invalid-option-combination', null, combinationValidator.getParameters(options), combinationValidator.getMessage(options));
}
for (const combinationValidatorName of combinationValidators.concat(alwaysRunValidators))
allCombinationValidators[combinationValidatorName](options, message);

@@ -196,0 +196,0 @@ message.throwWithAnyError();

@@ -66,2 +66,3 @@ // Central registry for messages.

'anno-unexpected-ellipsis': { severity: 'Error', configurableFor: 'deprecated' },
'anno-unexpected-localized-skip': { severity: 'Error', configurableFor: true },

@@ -118,2 +119,3 @@ 'name-invalid-dollar-alias': { severity: 'Error', configurableFor: true },

'ref-unexpected-autoexposed': { severity: 'Error' },
'ref-unexpected-many-navigation': { severity: 'Error' },
// Published! Used in @sap/cds-lsp; if renamed, add to oldMessageIds and contact colleagues

@@ -235,2 +237,4 @@ 'ref-undefined-art': { severity: 'Error' },

magicVars: 'Option $(PROP) is no longer supported! Use $(OTHERPROP) instead. See <https://cap.cloud.sap/docs/guides/databases#configuring-variables> for details',
value: 'Expected option $(PROP) to have $(VALUE). Found: $(OTHERVALUE)',
type: 'Expected option $(OPTION) to be of type $(VALUE). Found: $(OTHERVALUE)',
},

@@ -245,2 +249,18 @@

'api-invalid-combination': {
std: 'Invalid option combination found: $(OPTION) and $(PROP)', // unused
'valid-structured': 'Structured OData is only supported with OData version v4',
'sql-dialect-and-naming': 'sqlDialect $(NAME) can\'t be combined with sqlMapping $(PROP)',
'sql-dialect-and-localized': 'Option $(OPTION) can\'t be combined with SQL dialect $(VALUE) or the to.hdi()/to.hdbcds() backend',
},
'api-unexpected-combination': {
std: 'Unexpected option combination: $(OPTION) and $(PROP)', // unused
'beta-no-test':'Option $(OPTION) was used. This option should not be used in productive scenarios!',
},
'api-invalid-lookup-dir': {
std: '',
slash: 'Expected directory $(VALUE) in option $(OPTION) to end with $(OTHERVALUE)',
relative: 'Expected directory $(VALUE) in option $(OPTION) to not start with $(OTHERVALUE)',
},
'anno-duplicate': {

@@ -553,2 +573,5 @@ std: 'Duplicate assignment with $(ANNO)',

},
'ref-unexpected-many-navigation': {
std: 'Unexpected navigation into arrayed structure',
},
'ref-unexpected-scope': {

@@ -745,2 +768,8 @@ std: 'Unexpected parameter reference',

},
'ref-invalid-element': {
std: 'Invalid element reference',
$tableAlias: 'Can\'t refer to source elements of table alias $(ID)',
mixin: 'Can\'t refer to the query\'s own mixin $(ID)',
$self: 'Can\'t refer to the query\'s own elements',
},
'ref-invalid-override': {

@@ -857,10 +886,13 @@ std: 'Overridden element of include must not change element structure', // unused

'type-invalid-cast': {
std: 'Invalid cast to $(TYPE)', // unused
'to-structure': 'Can\'t cast to a structured type',
'from-structure': 'Structured elements can\'t be cast to a different type',
'expr-to-structure': 'Can\'t cast an expression to a structured type',
'val-to-structure': 'Can\'t cast $(VALUE) to a structured type'
},
// -----------------------------------------------------------------------------------
// Expressions
// -----------------------------------------------------------------------------------
'expr-invalid-operator': 'Comparing $(ID) is only allowed with $(OP)',
'expr-missing-comparison': {
std: 'Expected a comparison with $(OP) when using $(ID) in an ON-condition',
},
'type-invalid-cardinality': {

@@ -878,2 +910,4 @@ std: 'Invalid value $(VALUE) for cardinality', // unused variant

'expr-missing-foreign-key': 'Path step $(ID) of $(ELEMREF) has no valid foreign keys',
// OData version dependent messages

@@ -911,2 +945,3 @@ 'odata-spec-violation-array': 'Unexpected array type for OData $(VERSION)',

external: 'Referenced type $(TYPE) marked as $(ANNO) can\'t be rendered as $(CODE) in service $(NAME) for OData $(VERSION)',
scale: 'Expected scale $(NUMBER) to be less than or equal to precision $(RAWVALUE)'
},

@@ -977,3 +1012,3 @@ 'odata-spec-violation-property-name': 'Expected element name to be different from declaring $(META)',

'std': 'Unexpected value $(VALUE) for $(ANNO) of type $(TYPE)',
'struct': 'Unexpected $(STR) value for $(ANNO) of type $(TYPE)',
'incompval': 'Unexpected $(STR) value for $(ANNO) of type $(TYPE)',
'nestedcollection': 'Nested collections are not supported for $(ANNO)',

@@ -980,0 +1015,0 @@ 'enumincollection': 'Enum inside collection is not supported for $(ANNO)',

@@ -1303,2 +1303,9 @@ // Functions and classes for syntax messages

const name = getArtifactName( art );
if (!name) {
const loc = art.location ? ` at ${ locationString( art.location ) }` : '';
throw new CompilerAssertion(
art.path
? `No artifact for ${ art.path.map( i => i.id ).join( '.' )}${ loc }`
: `No name found in ${ Object.keys( art ).join( '+' )}${ loc }` );
}
if ([ 'select', 'action', 'alias', 'param' ].every( n => name[n] == null || name[n] === 1 ) &&

@@ -1305,0 +1312,0 @@ !name.absolute.includes(':'))

@@ -37,2 +37,3 @@ // module- and csn/XSN-independent definitions

associationDefault: true,
annotateForeignKeys: true,
// disabled by --beta-mode

@@ -48,2 +49,3 @@ nestedServices: false,

eagerPersistenceForGeneratedEntities: true,
noKeyPropagationWithExpansions: true,
}

@@ -135,5 +137,7 @@

// Apply function `callback` to all artifacts in dictionary
// `model.definitions`. See function `forEachGeneric` for details.
// TODO: should we skip "namespaces" already here?
/**
* Apply function `callback` to all artifacts in dictionary
* `model.definitions`. See function `forEachGeneric` for details.
* TODO: should we skip "namespaces" already here?
*/
function forEachDefinition( model, callback ) {

@@ -143,7 +147,13 @@ forEachGeneric( model, 'definitions', callback );

// Apply function `callback` to all members of object `obj` (main artifact or
// parent member). Members are considered those in dictionaries `elements`,
// `enum`, `actions` and `params` of `obj`, `elements` and `enums` are also
// searched inside property `items` (array of). See function `forEachGeneric`
// for details.
/**
* Apply function `callback` to all members of object `obj` (main artifact or
* parent member). Members are considered those in dictionaries `elements`,
* `enum`, `actions` and `params` of `obj`, `elements` and `enums` are also
* searched inside property `items` (array of). See function `forEachGeneric`
* for details.
*
* @param {XSN.Artifact} construct
* @param {(member: XSN.Artifact, memberName: string, prop: string) => void} callback
* @param {XSN.Artifact} [target]
*/
function forEachMember( construct, callback, target ) {

@@ -162,2 +172,20 @@ let obj = construct;

/**
* Same as forEachMember, but inside each member, calls itself recursively, i.e.
* sub members are traversed as well.
*
* @param {XSN.Artifact} construct
* @param {(member: XSN.Artifact, memberName: string, prop: string) => void} callback
* @param {XSN.Artifact} [target]
*/
function forEachMemberRecursively( construct, callback, target ) {
forEachMember( construct, ( member, memberName, prop ) => {
callback( member, memberName, prop );
if (Array.isArray(member.$duplicates)) // redefinitions
member.$duplicates.forEach( o => callback( o, memberName, prop ) );
// Descend into nested members, too
forEachMemberRecursively( member, callback );
}, target);
}
// Apply function `callback` to all objects in dictionary `dict`, including all

@@ -205,2 +233,3 @@ // duplicates (found under the same name). Function `callback` is called with

forEachMember,
forEachMemberRecursively,
forEachGeneric,

@@ -207,0 +236,0 @@ forEachInOrder,

@@ -122,14 +122,15 @@ 'use strict';

!isManagedComposition.bind(this)(member)) {
// Implementation note: Imported services (i.e. external ones) may contain to-many associations
// with an empty foreign key list. If the user (in this case importer) explicitly sets an empty
// foreign key array, we won't emit a warning to avoid spamming the user.
const max = member.cardinality?.max ? member.cardinality.max : 1;
if (max !== 1 && !member.on) {
if (max !== 1 && !member.on && (!member.keys || member.keys.length > 0)) {
const isNoDb = art['@cds.persistence.skip'] || art['@cds.persistence.exists'];
this.warning(isNoDb ? 'to-many-no-on-noDB' : 'to-many-no-on', member.cardinality ? member.cardinality.$path : member.$path,
{
value: cardinality2str(member, false),
'#': this.csnUtils.isComposition(member) ? 'comp' : 'std',
},
{
std: 'Expected association with target cardinality $(VALUE) to have an ON-condition',
comp: 'Expected composition with target cardinality $(VALUE) to have an ON-condition',
});
this.warning(isNoDb ? 'to-many-no-on-noDB' : 'to-many-no-on', member.cardinality ? member.cardinality.$path : member.$path, {
value: cardinality2str(member, false),
'#': this.csnUtils.isComposition(member) ? 'comp' : 'std',
}, {
std: 'Expected association with target cardinality $(VALUE) to have an ON-condition',
comp: 'Expected composition with target cardinality $(VALUE) to have an ON-condition',
});
}

@@ -136,0 +137,0 @@ }

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

// For cases where `Association to T { struct.one, struct.two };` is used instead of `{ struct }`.
// Note: We know that `{ struct, struct.one }` is not possible, so no prefix check required.
// We know that `{ struct, struct.one }` is not possible, so no prefix check required.
// If `ref.length` does not cover any full foreign key, then we call noForeignKeyCallback()
// as well. This could happen for `struct.one` as foreign key, and `assoc.struct = …`
// in ON-condition.
let fkIndex = 0;
let success = false;
while (!success && possibleKeys.length > 0) {
while (!success && possibleKeys.length > 0 && refIndex + fkIndex + 1 < ref.length) {
const pathStep = ref[refIndex + fkIndex + 1].id || ref[refIndex + fkIndex + 1];

@@ -191,0 +194,0 @@

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

function checkQueryRef( obj, inColumns ) {
if (!obj || obj.$scope === 'alias')
if (!obj)
return;

@@ -183,4 +183,3 @@

if (art.keys && !hasForeignKeys.call(this, art)) {
this.error(null, $path, { id: pathStep, elemref: { ref } },
'Path step $(ID) of $(ELEMREF) has no foreign keys');
this.error('expr-missing-foreign-key', $path, { id: pathStep, elemref: { ref } } );
break; // only one error per path

@@ -187,0 +186,0 @@ }

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

/**
* Validate select items of a query. If a column reference starts with $self or $projection, it must not contain association steps.
* Validate select items of a query. If a column reference starts with $self or
* $projection, it must not contain association steps.
* Furthermore, for to.hdbcds, window functions are not allowed.
*
* For to.hdbcds-hdbcds, structures and managed associations are not allowed as they are not flattened - @see rejectManagedAssociationsAndStructuresForHdbcdsNames
* For to.hdbcds-hdbcds, structures and managed associations are not allowed
* as they are not flattened - @see rejectManagedAssociationsAndStructuresForHdbcdsNames
*

@@ -21,55 +23,3 @@ * @param {CSN.Query} query query object

return;
/**
* Check for a $self.<assoc> in columns etc. - since the $self.<assoc> references the "outside" view
* of the association, this is not allowed.
*
* @param {string} queryPart Part of the query that is being checked
* @returns {Function} Function as callback for applyTransformations
*/
function checkRefForInvalid$Self( queryPart ) {
const signalError = (error, parent, type) => {
if (queryPart === 'columns') {
error(null, parent.$path,
{ name: parent.ref[0], type },
'Select items starting with $(NAME) must not contain path steps of type $(TYPE)');
}
else if (queryPart === 'orderBy') {
error(null, parent.$path,
{ id: queryPart, type },
'Items of the $(ID)-clause must not contain path steps of type $(TYPE)');
}
else {
error(null, parent.$path,
{ id: queryPart, name: parent.ref[0], type },
'Items of the $(ID)-clause starting with $(NAME) must not contain path steps of type $(TYPE)');
}
};
return function checkForInvalid$SelfInRef(parent) {
if (parent.ref && (parent.$scope === '$self' || parent.$scope === '$query')) {
const { _links } = parent;
for (let j = parent.$scope === '$self' ? 1 : 0; j < _links.length - 1; j++) {
if (_links[j].art.target) {
if (_links[j].art.on) {
// It's an unmanaged association - traversal is always forbidden
signalError(this.error, parent, _links[j].art.type);
}
else {
// It's a managed association - access of the foreign keys is allowed
const nextRef = parent.ref[j + 1].id || parent.ref[j + 1];
if (!_links[j].art.keys.some(r => r.ref[0] === nextRef))
signalError(this.error, parent, _links[j].art.type);
}
}
}
const last = _links[_links.length - 1];
if (last.art.target && last.art.on) {
// It's an unmanaged association - traversal is always forbidden
signalError(this.error, parent, last.art.type);
} // managed is okay, can be handled via tuple expansion
}
};
}
/**

@@ -95,3 +45,2 @@ * Check the given assoc filter for usage of $self - in an assoc-filter, you must only

applyTransformationsOnNonDictionary(parent, prop, {
ref: checkRefForInvalid$Self(prop).bind(this),
where: checkFilterForInvalid$Self.bind(this),

@@ -98,0 +47,0 @@ }, { skipStandard: { on: true }, drillRef: true });

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

function resolveArtifactType( art ) {
if (art && art.type && !isBuiltinType(art.type))
return this.csnUtils.getFinalTypeInfo(art.type);
const type = art?._type?.type || art?.type;
if (type && !isBuiltinType(type))
return this.csnUtils.getFinalTypeInfo(type);

@@ -65,0 +66,0 @@ return art;

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

const validateCdsPersistenceAnnotation = require('./cdsPersistence');
const navigationIntoMany = require('./manyNavigations');
const checkUsedTypesForAnonymousAspectComposition = require('./managedInType');

@@ -75,3 +76,3 @@ const validateHasPersistedElements = require('./hasPersistedElements');

const forRelationalDBCsnValidators = [ nonexpandableStructuredInExpression ];
const forRelationalDBCsnValidators = [ nonexpandableStructuredInExpression, navigationIntoMany ];
/**

@@ -238,2 +239,3 @@ * @type {Array<(query: CSN.Query, path: CSN.Path) => void>}

hasAnnotationValue(artifact, '@cds.persistence.skip') ||
hasAnnotationValue(artifact, '@cds.persistence.exists') ||
[ 'action', 'function', 'event' ].includes(artifact.kind),

@@ -240,0 +242,0 @@ });

@@ -5,4 +5,5 @@ {

"rules": {
"cds-compiler/message-no-quotes": "warn"
"cds-compiler/message-no-quotes": "warn",
"cds-compiler/space-in-func-call": "warn"
}
}

@@ -83,3 +83,3 @@ // Consistency checker on model (XSN = augmented CSN)

constructor(msg) {
super(`cds-compiler XSN consistency: ${ msg }`);
super( `cds-compiler XSN consistency: ${ msg }` );
}

@@ -274,3 +274,4 @@ }

'name', '$parens', 'quantifier', 'mixin', 'excludingDict', 'columns', 'elements', '_deps',
'where', 'groupBy', 'having', 'orderBy', '$orderBy', 'limit', '_origin', '_block',
'where', 'groupBy', 'having', 'orderBy', '$orderBy', 'limit', '$limit',
'_origin', '_block',
'_projections', '_parent', '_main', '_effectiveType', '$effectiveSeqNo', '$expand',

@@ -289,3 +290,3 @@ '$tableAliases', 'kind', '_$next', '_combined', '$inlines', '_status',

'on', '$parens', 'cardinality',
'kind', 'name', '_block', '_parent', '_main',
'kind', 'name', '_block', '_parent', '_main', '_user',
'$tableAliases', '_combined', '_joinParent', '$joinArgsIndex',

@@ -341,2 +342,3 @@ '_leadingQuery', '_$next', '_deps',

groupBy: { inherits: 'value', test: isArray( expression ) },
$limit: { test: TODO },
limit: { requires: [ 'rows' ], optional: [ 'offset', 'location' ] },

@@ -514,3 +516,3 @@ rows: { inherits: 'value' },

},
$priority: { test: isOneOf([ undefined, false, 'extend', 'annotate' ]) },
$priority: { test: isOneOf( [ undefined, false, 'extend', 'annotate' ] ) },
$annotations: { parser: true, kind: true, test: TODO }, // deprecated, still there for cds-lsp

@@ -613,3 +615,4 @@ name: {

'orderBy', '$orderBy', 'groupBy', 'excludingDict', 'having',
'limit', '_status', '_origin', '_effectiveType', '$effectiveSeqNo',
'$limit', 'limit', '_status', '_origin',
'_effectiveType', '$effectiveSeqNo',
],

@@ -656,3 +659,3 @@ },

kind: true,
test: isOneOf([
test: isOneOf( [
'', // constructed “super annotate” statement, redirected user-provided target

@@ -689,3 +692,3 @@ // Uppercase values are used in logic, lowercase value are "just for us", i.e.

// $origin and is not a direct annotation
]),
] ),
},

@@ -699,3 +702,3 @@

// See description of `setExpandStatus()` of in `lib/compiler/utils.js`.
test: isOneOf([ 'origin', 'annotate', 'target' ]),
test: isOneOf( [ 'origin', 'annotate', 'target' ] ),
},

@@ -708,3 +711,3 @@ $inCycle: { kind: true, test: isBoolean },

$sources: { parser: true, test: isArray( isString ) },
$expected: { parser: true, test: isOneOf([ 'approved-exists', 'exists' ]) },
$expected: { parser: true, test: isOneOf( [ 'approved-exists', 'exists' ] ) },
$messageFunctions: { test: TODO },

@@ -797,3 +800,3 @@ $functions: { test: TODO },

for (const p of requires) {
if (!names.includes(p)) {
if (!names.includes( p )) {
const req = spec.schema && spec.schema[p] && spec.schema[p].isRequired;

@@ -806,3 +809,3 @@ if ((req || schema[p] && schema[p].isRequired || noSyntaxErrors)( node ))

for (const n of names) {
const opt = Array.isArray(optional)
const opt = Array.isArray( optional )
? optional.includes( n ) || optional.includes( n.charAt(0) )

@@ -870,3 +873,3 @@ : optional( n, spec );

return;
while (Array.isArray(node)) {
while (Array.isArray( node )) {
// TODO: also check getOwnPropertyNames(node)

@@ -883,3 +886,3 @@ if (node.length !== 1) {

const s = spec[expressionSpec(node)] || {};
const s = spec[expressionSpec( node )] || {};
const sub = Object.assign( {}, s.inherits && schema[s.inherits], s );

@@ -907,5 +910,5 @@ if (spec.requires && sub.requires)

function args( node, parent, prop, spec ) {
if (Array.isArray(node)) {
if (Array.isArray( node )) {
if (parent.op && parent.op.val === 'xpr') // remove keywords for `xpr` expressions
node = node.filter( a => typeof a !== 'string');
node = node.filter( a => typeof a !== 'string' );
node.forEach( (item, idx) => expression( item, parent, prop, spec, idx ) );

@@ -945,3 +948,3 @@ }

return function vector( node, parent, prop, spec ) {
if (!Array.isArray(node))
if (!Array.isArray( node ))
throw new InternalConsistencyError( `Expected array${ at( [ null, parent ], prop ) }` );

@@ -979,4 +982,4 @@ node.forEach( (item, n) => func( item, parent, prop, spec, n ) );

return function isOneOfInner( node, parent, prop ) {
if (!values.includes(node))
throw new InternalConsistencyError( `Unexpected value '${ node }', expected ${ JSON.stringify(values) }${ at( [ node, parent ], prop ) }` );
if (!values.includes( node ))
throw new InternalConsistencyError( `Unexpected value '${ node }', expected ${ JSON.stringify( values ) }${ at( [ node, parent ], prop ) }` );
};

@@ -994,3 +997,3 @@ }

function isVal( node, parent, prop, spec ) {
if (Array.isArray(node))
if (Array.isArray( node ))
node.forEach( (item, n) => standard( item, parent, prop, spec, n ) );

@@ -1016,3 +1019,3 @@ else if (node !== null && ![ 'string', 'number', 'boolean' ].includes( typeof node ))

function inDefinitions( art, parent, prop, spec, name ) {
if (Array.isArray(art)) // do not check with redefinitions
if (Array.isArray( art )) // do not check with redefinitions
return;

@@ -1029,3 +1032,3 @@ isObject( art, parent, prop, spec, name );

if (parent.kind === 'source' ||
art.name.absolute && art.name.absolute.startsWith('localized.'))
art.name.absolute && art.name.absolute.startsWith( 'localized.' ))
standard( art, parent, prop, spec, name );

@@ -1039,7 +1042,7 @@ else

// artifact refs in CDL have scope:0 in XSN
if (Number.isInteger(node))
if (Number.isInteger( node ))
return;
const validValues = [ 'typeOf', 'global', 'param' ];
if (!validValues.includes(node))
throw new InternalConsistencyError( `Property '${ prop }' must be either "${ validValues.join('", "') }" or a number but was "${ node }"` );
if (!validValues.includes( node ))
throw new InternalConsistencyError( `Property '${ prop }' must be either "${ validValues.join( '", "' ) }" or a number but was "${ node }"` );
}

@@ -1046,0 +1049,0 @@

@@ -32,3 +32,7 @@ // Base Definitions for the Core Compiler

type: { elements: propExists, enum: propExists, include: true },
aspect: { elements: propExists, actions: true, include: true },
aspect: {
elements: propExists,
actions: ( _p, parent ) => propExists( 'elements', parent ),
include: true,
},
annotation: { elements: propExists, enum: propExists },

@@ -114,3 +118,3 @@ enum: { normalized: 'element', dict: 'enum' },

return 'element';
throw new CompilerAssertion( `Member not found in parent properties ${ Object.keys( obj ).join('+') }` );
throw new CompilerAssertion( `Member not found in parent properties ${ Object.keys( obj ).join( '+' ) }` );
}

@@ -117,0 +121,0 @@

@@ -238,3 +238,3 @@ // The builtin artifacts of CDS

test_variant: 'uneven-hex',
test_fn: (str => Number.isInteger(str.length / 2)),
test_fn: (str => Number.isInteger( str.length / 2 )),
unexpected_variant: 'invalid-hex',

@@ -297,8 +297,8 @@ unexpected_char: /[^0-9a-f]/i,

// Negative years are allowed
year = Math.abs(Number.parseInt(year, 10));
month = Number.parseInt(month, 10);
day = Number.parseInt(day, 10);
year = Math.abs( Number.parseInt( year, 10 ) );
month = Number.parseInt( month, 10 );
day = Number.parseInt( day, 10 );
// If any is NaN, the condition will be false.
// Year 0 does not exist, but ISO 8601 allows it and defines it as 1 BC.
return !Number.isNaN(year) && month > 0 && month < 13 && day > 0 && day < 32;
return !Number.isNaN( year ) && month > 0 && month < 13 && day > 0 && day < 32;
}

@@ -313,5 +313,5 @@

function checkTime( hour, minutes, seconds ) {
hour = Number.parseInt(hour, 10);
minutes = Number.parseInt(minutes, 10);
seconds = seconds ? Number.parseInt(seconds, 10) : 0;
hour = Number.parseInt( hour, 10 );
minutes = Number.parseInt( minutes, 10 );
seconds = seconds ? Number.parseInt( seconds, 10 ) : 0;
if (hour === 24) // allow 24:00:00 (ISO 8601 version earlier than 2019)

@@ -338,43 +338,43 @@ return minutes === 0 && seconds === 0;

// Fill type categories with `cds.*` types
Object.keys(core).forEach((type) => {
Object.keys( core ).forEach( (type) => {
if (core[type].category)
typeCategories[core[type].category].push(`cds.${ type }`);
});
typeCategories[core[type].category].push( `cds.${ type }` );
} );
// Fill type categories with `cds.hana.*` types
Object.keys(coreHana).forEach((type) => {
Object.keys( coreHana ).forEach( (type) => {
if (coreHana[type].category)
typeCategories[coreHana[type].category].push(`cds.hana.${ type }`);
});
typeCategories[coreHana[type].category].push( `cds.hana.${ type }` );
} );
/** @param {string} typeName */
function isIntegerTypeName( typeName ) {
return typeCategories.integer.includes(typeName);
return typeCategories.integer.includes( typeName );
}
/** @param {string} typeName */
function isDecimalTypeName( typeName ) {
return typeCategories.decimal.includes(typeName);
return typeCategories.decimal.includes( typeName );
}
/** @param {string} typeName */
function isNumericTypeName( typeName ) {
return isIntegerTypeName(typeName) || isDecimalTypeName(typeName);
return isIntegerTypeName( typeName ) || isDecimalTypeName( typeName );
}
/** @param {string} typeName */
function isStringTypeName( typeName ) {
return typeCategories.string.includes(typeName);
return typeCategories.string.includes( typeName );
}
/** @param {string} typeName */
function isDateOrTimeTypeName( typeName ) {
return typeCategories.dateTime.includes(typeName);
return typeCategories.dateTime.includes( typeName );
}
/** @param {string} typeName */
function isBooleanTypeName( typeName ) {
return typeCategories.boolean.includes(typeName);
return typeCategories.boolean.includes( typeName );
}
/** @param {string} typeName */
function isBinaryTypeName( typeName ) {
return typeCategories.binary.includes(typeName);
return typeCategories.binary.includes( typeName );
}
/** @param {string} typeName */
function isGeoTypeName( typeName ) {
return typeCategories.geo.includes(typeName);
return typeCategories.geo.includes( typeName );
}

@@ -387,3 +387,3 @@ /**

function isRelationTypeName( typeName ) {
return typeCategories.relation.includes(typeName);
return typeCategories.relation.includes( typeName );
}

@@ -398,6 +398,6 @@

function isInReservedNamespace( absolute ) {
return absolute === 'cds' || absolute.startsWith( 'cds.') &&
!absolute.match(/^cds\.foundation(\.|$)/) &&
!absolute.match(/^cds\.outbox(\.|$)/) && // Requested by Node runtime
!absolute.match(/^cds\.xt(\.|$)/); // Requested by Mtx
return absolute === 'cds' || absolute.startsWith( 'cds.' ) &&
!absolute.match( /^cds\.foundation(\.|$)/ ) &&
!absolute.match( /^cds\.outbox(\.|$)/ ) && // Requested by Node runtime
!absolute.match( /^cds\.xt(\.|$)/ ); // Requested by Mtx
}

@@ -417,3 +417,3 @@

function isBuiltinType( type ) {
return typeof type === 'string' && isInReservedNamespace(type);
return typeof type === 'string' && isInReservedNamespace( type );
}

@@ -456,3 +456,3 @@

};
setProp( art, '_subArtifacts', Object.create(null) );
setProp( art, '_subArtifacts', Object.create( null ) );
return art;

@@ -471,3 +471,3 @@ }

function env( builtins, prefix, parent ) {
const artifacts = Object.create(null);
const artifacts = Object.create( null );
for (const name of Object.keys( builtins )) {

@@ -493,3 +493,3 @@ const absolute = prefix + name;

function setMagicVariables( builtins ) {
const artifacts = Object.create(null);
const artifacts = Object.create( null );
for (const name in builtins) {

@@ -523,5 +523,5 @@ const magic = builtins[name];

const names = Object.keys(elements);
const names = Object.keys( elements );
if (names.length > 0 && !art.elements)
art.elements = Object.create(null);
art.elements = Object.create( null );

@@ -539,3 +539,3 @@ for (const n of names) {

if (elements[n] && typeof elements[n] === 'object')
createMagicElements(magic, elements[n]);
createMagicElements( magic, elements[n] );

@@ -542,0 +542,0 @@ art.elements[n] = magic;

@@ -18,2 +18,3 @@ // Checks on XSN performed during compile() that are useful for the user

forEachMember,
forEachMemberRecursively,
isBetaEnabled,

@@ -23,6 +24,6 @@ } = require('../base/model');

const { pathName } = require('./utils');
const { forEachMemberRecursively } = require('../model/csnUtils');
const { typeParameters } = require('./builtins');
const $location = Symbol.for('cds.$location');
const $location = Symbol.for( 'cds.$location' );
/**

@@ -43,2 +44,3 @@ * Run compiler checks on the given XSN model.

forEachGeneric( model, 'vocabularies', checkAnnotationDefinition );
return;

@@ -50,3 +52,3 @@

checkElementIncludeOverride( def );
forEachMember( def, member => checkMember(member) );
forEachMember( def, member => checkMember( member ) );
if (def.$queries)

@@ -62,3 +64,3 @@ def.$queries.forEach( checkQuery );

warning( 'def-unexpected-localized-anno', [ member.localized.location, member ] );
});
} );
}

@@ -100,4 +102,4 @@

if (isKey && isVirtual) {
error('def-unexpected-key', [ isKey.location, elem ],
{ '#': 'virtual', art: elem.name.element, prop: 'key' });
error( 'def-unexpected-key', [ isKey.location, elem ],
{ '#': 'virtual', art: elem.name.element, prop: 'key' } );
}

@@ -128,5 +130,7 @@ }

// Maybe remove the check? But consider runtimes that rely on '.' as element separator.
if (construct.name.id?.includes( '.' )) {
error(null, [ construct.name.location, construct ], {},
'The character \'.\' is not allowed in identifiers');
if (construct.kind === 'element' || construct.kind === 'action' || construct.kind === 'param') {
if (construct.name.id?.includes( '.' )) {
error( null, [ construct.name.location, construct ], {},
'The character \'.\' is not allowed in identifiers' );
}
}

@@ -150,3 +154,3 @@ }

const actualParams = typeParameters.list.filter(param => art[param] !== undefined);
const actualParams = typeParameters.list.filter( param => art[param] !== undefined );
if (actualParams.length === 0)

@@ -167,5 +171,5 @@ return;

else if (!art.type && !effectiveType.type && !effectiveType?.builtin) {
error('type-missing-type', [ art.location, user ],
{ otherprop: 'type', prop: actualParams[0] },
'Missing $(OTHERPROP) property next to $(PROP)');
error( 'type-missing-type', [ art.location, user ],
{ otherprop: 'type', prop: actualParams[0] },
'Missing $(OTHERPROP) property next to $(PROP)' );
return;

@@ -175,6 +179,6 @@ }

const expectedParams = effectiveType.parameters &&
effectiveType.parameters.map(p => p.name || p) || [];
effectiveType.parameters.map( p => p.name || p ) || [];
for (const param of actualParams) {
if (!expectedParams.includes(param)) {
if (!expectedParams.includes( param )) {
// Whether the type ref itself is a builtin or a custom type with a builtin as base.

@@ -189,9 +193,9 @@ let variant;

error('type-unexpected-argument', [ art[param].location, user ], {
error( 'type-unexpected-argument', [ art[param].location, user ], {
'#': variant, prop: param, art: art.type || art._effectiveType, type: effectiveType,
});
} );
break; // Avoid spam: Only emit the first error.
}
else if (!typeParameters.expectedLiteralsFor[param].includes(art[param].literal)) {
error('type-unexpected-argument', [ art[param].location, user ], {
else if (!typeParameters.expectedLiteralsFor[param].includes( art[param].literal )) {
error( 'type-unexpected-argument', [ art[param].location, user ], {
'#': 'incorrect-type',

@@ -201,3 +205,3 @@ prop: param,

names: typeParameters.expectedLiteralsFor[param],
});
} );
break; // Avoid spam: Only emit the first error.

@@ -210,4 +214,4 @@ }

if (!art.type) {
error('expr-missing-type', [ art.location, user ], { },
'Missing type in SQL cast function');
error( 'expr-missing-type', [ art.location, user ], { },
'Missing type in SQL cast function' );
}

@@ -221,5 +225,5 @@ }

if (!type || !type.builtin || type.category !== 'string') {
info('ref-expecting-localized-string', [ elem.type?.location, elem ],
{ keyword: 'localized' },
'Expecting a string type in combination with keyword $(KEYWORD)');
info( 'ref-expecting-localized-string', [ elem.type?.location, elem ],
{ keyword: 'localized' },
'Expecting a string type in combination with keyword $(KEYWORD)' );
}

@@ -234,4 +238,4 @@ }

if (elem._origin?.localized?.val && !elem._origin.key?.val) {
warning('def-ignoring-localized', [ elem.key.location, elem ], { keyword: 'localized' },
'Keyword $(KEYWORD) is ignored for primary keys');
warning( 'def-ignoring-localized', [ elem.key.location, elem ], { keyword: 'localized' },
'Keyword $(KEYWORD) is ignored for primary keys' );
}

@@ -242,25 +246,24 @@ }

function checkQuery( query ) {
checkNoUnmanagedAssocsInGroupByOrderBy( query );
// TODO: check too simple (just one source), as most of those in this file
// Check expressions in the various places where they may occur
if (query.from)
visitSubExpression(query.from, query, checkGenericExpression);
visitSubExpression( query.from, query, checkGenericExpression );
if (query.where)
visitExpression(query.where, query, checkGenericExpression);
visitExpression( query.where, query, checkGenericExpression );
if (query.groupBy) {
for (const groupByEntry of query.groupBy)
visitExpression(groupByEntry, query, checkGenericExpression);
visitExpression( groupByEntry, query, checkGenericExpression );
}
if (query.having)
visitExpression(query.having, query, checkGenericExpression);
visitExpression( query.having, query, checkGenericExpression );
if (query.orderBy) {
for (const orderByEntry of query.orderBy)
visitExpression(orderByEntry, query, checkGenericExpression);
visitExpression( orderByEntry, query, checkGenericExpression );
}
if (query.mixin) {
for (const mixinName in query.mixin)
checkAssociation(query.mixin[mixinName]);
checkAssociation( query.mixin[mixinName] );
}

@@ -284,4 +287,4 @@ }

if (type === 'enum') {
warning('ref-unexpected-enum', [ loc, enumNode ], {},
'References to other values are not allowed as enum values');
warning( 'ref-unexpected-enum', [ loc, enumNode ], {},
'References to other values are not allowed as enum values' );
}

@@ -317,3 +320,3 @@ }

error('type-invalid-enum', [ enumNode.type.location, enumNode ], { '#': typeClass }, {
error( 'type-invalid-enum', [ enumNode.type.location, enumNode ], { '#': typeClass }, {
std: 'Only builtin types are allowed as enums',

@@ -324,7 +327,7 @@ binary: 'Binary types are not allowed as enums',

items: 'Arrayed types are not allowed as enums',
});
} );
return;
}
checkEnumValue(enumNode);
checkEnumValue( enumNode );
}

@@ -349,7 +352,7 @@

// Non-string enums MUST have a value as the value is only deducted for string types.
const emptyValue = Object.keys(enumNode.enum)
.find(name => !enumNode.enum[name].value);
const emptyValue = Object.keys( enumNode.enum )
.find( name => !enumNode.enum[name].value );
if (emptyValue) {
const failedEnum = enumNode.enum[emptyValue];
warning('type-missing-value', [ failedEnum.location, failedEnum ], {
warning( 'type-missing-value', [ failedEnum.location, failedEnum ], {
'#': isNumeric ? 'numeric' : 'std', name: emptyValue,

@@ -359,3 +362,3 @@ }, {

numeric: 'Missing value for numeric enum element $(NAME)',
});
} );
}

@@ -377,8 +380,8 @@ }

for (const key of Object.keys(enumNode.enum)) {
for (const key of Object.keys( enumNode.enum )) {
const element = enumNode.enum[key];
if (hasWrongType(element)) {
if (hasWrongType( element )) {
const actualType = element.value.literal;
warning('type-unexpected-value', [ element.value.location, element ], {
'#': expectedType, name: key, prop: actualType,
warning( 'type-unexpected-value', [ element.value.location, element ], {
'#': expectedType, name: key, prop: actualType || 'unknown',
}, {

@@ -388,3 +391,3 @@ std: 'Incorrect value type $(PROP) for enum element $(NAME)', // Not used

string: 'Expected string value for enum element $(NAME) but was $(PROP)',
});
} );
}

@@ -418,3 +421,3 @@ }

function checkLocalizedSubElement( element ) {
if (element._parent.kind !== 'element')
if (element._parent?.kind !== 'element')
return;

@@ -425,8 +428,8 @@

const loc = isLocalizedSubElement ? element.localized.location : element.type.location;
warning('localized-sub-element', [ loc, element ],
{ type: element.type, '#': isLocalizedSubElement ? 'std' : 'type' },
{
std: 'Keyword "localized" is ignored for sub elements',
type: 'Keyword "localized" in type $(TYPE) is ignored for sub elements',
} );
warning( 'localized-sub-element', [ loc, element ],
{ type: element.type, '#': isLocalizedSubElement ? 'std' : 'type' },
{
std: 'Keyword "localized" is ignored for sub elements',
type: 'Keyword "localized" in type $(TYPE) is ignored for sub elements',
} );
}

@@ -457,13 +460,2 @@ }

// Min cardinality must be a non-negative number
// Note: Already checked by parser (syntax error if -1 is used) and
// from-csn.json (expected non-negative number)
for (const prop of [ 'sourceMin', 'targetMin' ]) {
if (art.cardinality[prop]) {
const { literal, val, location } = art.cardinality[prop];
if (!(literal === 'number' && val >= 0))
error( 'type-invalid-cardinality', [ location, art ], { '#': prop, prop: val } );
}
}
// If provided, min cardinality must not exceed max cardinality (note that

@@ -475,3 +467,3 @@ // '*' is considered to be >= any number)

];
pair.forEach(([ lhs, rhs, variant ]) => {
pair.forEach( ([ lhs, rhs, variant ]) => {
if (art.cardinality[lhs] && art.cardinality[rhs] &&

@@ -481,27 +473,5 @@ art.cardinality[rhs].literal === 'number' &&

error( 'type-invalid-cardinality', [ art.cardinality.location, art ], { '#': variant } );
});
} );
}
// TODO: make this part of the name resolution in the compiler
// Check that queries in 'art' do not contain unmanaged associations in GROUP BY or ORDER BY
function checkNoUnmanagedAssocsInGroupByOrderBy( query ) {
const art = query._main; // TODO - remove, use query for semantic location
for (const groupByEntry of query.groupBy || []) {
if (groupByEntry._artifact && groupByEntry._artifact._effectiveType &&
groupByEntry._artifact._effectiveType.on) {
// Unmanaged association - complain
error(null, [ groupByEntry.location, art ], {},
'Unmanaged associations are not allowed in GROUP BY');
}
}
for (const orderByEntry of query.orderBy || []) {
if (orderByEntry._artifact && orderByEntry._artifact._effectiveType &&
orderByEntry._artifact._effectiveType.on) {
// Unmanaged association - complain
error(null, [ orderByEntry.location, art ], {},
'Unmanaged associations are not allowed in ORDER BY');
}
}
}
function checkAssociation( elem ) {

@@ -517,4 +487,4 @@ if (!elem.target && !elem.targetAspect)

const key = elem.foreignKeys[k].targetElement;
if (key && isVirtualElement(key._artifact))
error('ref-unexpected-virtual', [ key.location, elem ], { '#': 'fkey' });
if (key && isVirtualElement( key._artifact ))
error( 'ref-unexpected-virtual', [ key.location, elem ], { '#': 'fkey' } );
else if (key._artifact?.$syntax === 'calc' && !key._artifact.value.stored?.val)

@@ -540,3 +510,3 @@ error( 'ref-unexpected-calculated', [ key.location, elem ], { '#': 'fkey' } );

else {
const fkName = Object.keys(elem.foreignKeys)[0];
const fkName = Object.keys( elem.foreignKeys )[0];
if (elem.foreignKeys[fkName].targetElement._artifact?._effectiveType?.elements) {

@@ -550,3 +520,3 @@ error( 'type-unexpected-default', [ elem.default.location, elem ], {

checkOnCondition(elem);
checkOnCondition( elem );
}

@@ -577,14 +547,14 @@

if (artifact.items && !finalType.items) {
warning('type-items-mismatch', [ artifact.type.location, artifact ],
{ type: artifact.type, prop: 'items' },
'Used type $(TYPE) is not arrayed and conflicts with $(PROP) property');
warning( 'type-items-mismatch', [ artifact.type.location, artifact ],
{ type: artifact.type, prop: 'items' },
'Used type $(TYPE) is not arrayed and conflicts with $(PROP) property' );
}
else if (artifact.elements && !finalType.elements) {
warning('type-elements-mismatch', [ artifact.type.location, artifact ],
{ type: artifact.type, prop: 'elements' },
'Used type $(TYPE) is not structured and conflicts with $(PROP) property');
warning( 'type-elements-mismatch', [ artifact.type.location, artifact ],
{ type: artifact.type, prop: 'elements' },
'Used type $(TYPE) is not structured and conflicts with $(PROP) property' );
}
}
if (artifact.items)
checkTypeStructure(artifact.items);
checkTypeStructure( artifact.items );
}

@@ -606,3 +576,3 @@

if (include._artifact?.elements?.[name] !== undefined)
checkElementOverride( element, include._artifact.elements[name]);
checkElementOverride( element, include._artifact.elements[name] );
}

@@ -622,8 +592,8 @@ }

const loc = elem.type?.location || elem.elements?.[$location] || elem.location;
error('ref-invalid-override', [ loc, elem ],
{ '#': prop, art: original._main, name });
error( 'ref-invalid-override', [ loc, elem ],
{ '#': prop, art: original._main, name } );
return false;
}
else if (original.elements &&
!checkSubStructureOverride(elem, elem.elements, original.elements)) {
!checkSubStructureOverride( elem, elem.elements, original.elements )) {
return false;

@@ -643,6 +613,6 @@ }

const loc = [ elements[$location], user ];
error('ref-invalid-override', loc, { '#': 'missing', id: user.name.id, name: element });
error( 'ref-invalid-override', loc, { '#': 'missing', id: user.name.id, name: element } );
return false; // only report once
}
else if (!checkElementOverride(elem, orig)) {
else if (!checkElementOverride( elem, orig )) {
return false;

@@ -664,4 +634,4 @@ }

function checkGenericExpression( xpr, user ) {
checkExpressionNotVirtual(xpr, user);
checkExpressionAssociationUsage(xpr, user, false);
checkExpressionNotVirtual( xpr, user );
checkExpressionAssociationUsage( xpr, user, false );
if (xpr.op?.val === 'cast') {

@@ -674,12 +644,15 @@ requireExplicitTypeInSqlCast( xpr, user );

function checkExpressionNotVirtual( xpr, user ) {
if (xpr._artifact && isVirtualElement(xpr._artifact))
error('ref-unexpected-virtual', [ xpr.location, user ], { '#': 'expr' });
if (xpr._artifact && isVirtualElement( xpr._artifact ))
error( 'ref-unexpected-virtual', [ xpr.location, user ], { '#': 'expr' } );
}
function checkOnCondition( elem ) {
if (elem.$inferred === 'localized')
return; // ignore
// TODO: Move to checkAssociation
if (elem.on && !elem.on.$inferred) {
visitExpression(elem.on, elem, (xpr, user) => {
checkExpressionNotVirtual(xpr, user);
checkExpressionAssociationUsage(xpr, user, true);
visitExpression( elem.on, elem, (xpr, user) => {
checkExpressionNotVirtual( xpr, user );
checkExpressionAssociationUsage( xpr, user, true );
// Essential check. Dependency handling for `on` conditions must change if

@@ -689,7 +662,3 @@ // this is allowed. See test3/Associations/Dependencies/.

error( 'ref-unexpected-calculated', [ xpr.location, user ], { '#': 'on' } );
});
if (isDollarSelfOrProjectionOperand(elem.on)) {
// Bare $self usages are not allowed and don't work in A2J.
error('expr-missing-comparison', [ elem.on.location, elem ], { id: '$self', op: '=' } );
}
} );
}

@@ -707,33 +676,37 @@ }

}
visitSubExpression(elem.value, elem, (xpr) => {
visitSubExpression( elem.value, elem, (xpr) => {
checkGenericExpression( xpr, elem );
});
} );
}
function checkCalculatedElementValue( elem ) {
visitExpression(elem.value, elem, (xpr, user) => {
if (xpr._artifact) { // we only need to check artifact references
visitExpression( elem.value, elem, (xpr, user) => {
// We only need to check artifact references. To avoid false positives and conflicts
// with $self comparison-checks, ignore bare $self.
const isArtRef = xpr._artifact && !(xpr.path?.length === 1 &&
xpr.path[0]._navigation?.kind === '$self');
if (isArtRef) {
const sourceLoc = xpr.path?.[xpr.path.length - 1].location || xpr.location;
checkExpressionNotVirtual(xpr, user);
checkExpressionNotVirtual( xpr, user );
// For inferred (e.g. included) calc elements, this error is already emitted at the origin.
// And users can't change structured to non-structured elements.
if (!elem.$inferred && xpr._artifact._effectiveType?.elements) {
error('ref-unexpected-structured', [ sourceLoc, elem ], { '#': 'expr' } );
error( 'ref-unexpected-structured', [ sourceLoc, elem ], { '#': 'expr' } );
}
else if (xpr._artifact.target !== undefined) {
const variant = isComposition(model, xpr._artifact) ? 'expr-comp' : 'expr';
error('ref-unexpected-assoc', [ sourceLoc, elem ], { '#': variant });
const variant = isComposition( model, xpr._artifact ) ? 'expr-comp' : 'expr';
error( 'ref-unexpected-assoc', [ sourceLoc, elem ], { '#': variant } );
}
else if (xpr._artifact.localized?.val && elem.value.stored?.val) {
error('ref-unexpected-localized', [ sourceLoc, elem ], { '#': 'calc' });
error( 'ref-unexpected-localized', [ sourceLoc, elem ], { '#': 'calc' } );
}
}
});
} );
// Calculated elements must not refer to keys, because that may lead to another
// key in an SQL view, which is missing in OData (for on-read).
// Following associations does not lead to this issue.
if (elem.value.path && isKeyElement(elem.value._artifact) &&
!followsAnAssociation(elem.value.path)) {
error('ref-unexpected-key', [ elem.value.location, elem ], {},
'Calculated elements can\'t refer directly to key elements');
if (elem.value.path && isKeyElement( elem.value._artifact ) &&
!followsAnAssociation( elem.value.path )) {
error( 'ref-unexpected-key', [ elem.value.location, elem ], {},
'Calculated elements can\'t refer directly to key elements' );
}

@@ -800,17 +773,11 @@ }

// We don't check token-stream-like 'xpr's.
const isNotSelfComparison = xpr.op?.val !== 'xpr' &&
!isBinaryDollarSelfComparisonWithAssoc(xpr);
const args = Array.isArray(xpr.args) ? xpr.args : Object.values(xpr.args);
const op = getBinaryOp(xpr);
const args = Array.isArray( xpr.args ) ? xpr.args : Object.values( xpr.args );
const isNotSelfComparison = args.length > 0 && xpr.op?.val !== 'xpr' &&
!isBinaryDollarSelfComparisonWithAssoc( xpr );
if (isNotSelfComparison) {
const op = getBinaryOp( xpr );
for (const arg of args) {
if (op?.val !== '=' && isDollarSelfOrProjectionOperand(arg)) {
// `nary` operators don't have a "good" location; use $self in that case.
const loc = (op?.location.endLine ? op : arg).location;
error('expr-invalid-operator', [ loc, user ], { op: '=', id: '$self' });
}
else {
checkExpressionIsNotAssocOrSelf(arg, user, allowAssocTail);
}
if (arg && !(op?.val !== '=' && isDollarSelfOrProjectionOperand( arg )))
checkExpressionIsNotAssocOrSelf( arg, user, allowAssocTail );
}

@@ -825,15 +792,10 @@ }

if (arg.$expected === 'exists') {
const variant = isComposition(model, arg._artifact) ? 'expr-comp' : 'expr';
error('ref-unexpected-assoc', [ arg.location, user ], { '#': variant });
const variant = isComposition( model, arg._artifact ) ? 'expr-comp' : 'expr';
error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': variant } );
}
}
else if (!allowAssocTail && isAssociationOperand(arg)) {
const variant = isComposition(model, arg._artifact) ? 'expr-comp' : 'expr';
error('ref-unexpected-assoc', [ arg.location, user ], { '#': variant } );
else if (!allowAssocTail && isAssociationOperand( arg )) {
const variant = isComposition( model, arg._artifact ) ? 'expr-comp' : 'expr';
error( 'ref-unexpected-assoc', [ arg.location, user ], { '#': variant } );
}
if (isDollarSelfOrProjectionOperand(arg)) {
error(null, [ arg.location, user ], { id: arg.path[0].id },
'$(ID) can only be used as a value in a comparison to an association');
}
}

@@ -871,9 +833,13 @@

// Tree-ish expression from the compiler (not augmented)
return (isAssociationOperand(xpr.args[0]) && isDollarSelfOrProjectionOperand(xpr.args[1]) ||
isAssociationOperand(xpr.args[1]) && isDollarSelfOrProjectionOperand(xpr.args[0]));
// eslint-disable-next-line max-len
return (isAssociationOperand( xpr.args[0] ) && isDollarSelfOrProjectionOperand( xpr.args[1] ) ||
// eslint-disable-next-line max-len
isAssociationOperand( xpr.args[1] ) && isDollarSelfOrProjectionOperand( xpr.args[0] ));
}
else if (xpr.args.length === 3 && xpr.args[1].val === '=') {
// Tree-ish expression from the compiler (not augmented)
return (isAssociationOperand(xpr.args[0]) && isDollarSelfOrProjectionOperand(xpr.args[2]) ||
isAssociationOperand(xpr.args[2]) && isDollarSelfOrProjectionOperand(xpr.args[0]));
// eslint-disable-next-line max-len
return (isAssociationOperand( xpr.args[0] ) && isDollarSelfOrProjectionOperand( xpr.args[2] ) ||
// eslint-disable-next-line max-len
isAssociationOperand( xpr.args[2] ) && isDollarSelfOrProjectionOperand( xpr.args[0] ));
}

@@ -903,3 +869,3 @@

for (let i = anno.name.path.length; i > 0; i--) {
const absoluteName = anno.name.path.slice(0, i).map(path => path.id).join('.');
const absoluteName = anno.name.path.slice( 0, i ).map( path => path.id ).join( '.' );
if (model.vocabularies[absoluteName]) {

@@ -917,4 +883,4 @@ fromArtifact = model.vocabularies[absoluteName];

const { artifact, endOfPath } = resolvePathFrom(anno.name.path.slice(pathStepsFound),
fromArtifact);
const { artifact, endOfPath } = resolvePathFrom( anno.name.path.slice( pathStepsFound ),
fromArtifact );

@@ -939,5 +905,5 @@ // Check what we actually want to check

if (!elementDecl) {
warning(null, [ anno.location || anno.name.location, art ],
{ name: pathName(anno.name.path), anno: annoDecl.name.absolute },
'Element $(NAME) not found for annotation $(ANNO)');
warning( null, [ anno.location || anno.name.location, art ],
{ name: pathName( anno.name.path ), anno: annoDecl.name.absolute },
'Element $(NAME) not found for annotation $(ANNO)' );
return;

@@ -948,14 +914,14 @@ }

if (!elementDecl._effectiveType)
throw new CompilerAssertion(`Expecting annotation declaration to have _finalType: ${ JSON.stringify(annoDecl) }`);
throw new CompilerAssertion(`Expecting annotation declaration to have _finalType: ${ JSON.stringify( annoDecl ) }`);
// Must have literal or path unless it is a boolean
if (!anno.literal && !anno.path && getFinalTypeNameOf(elementDecl) !== 'cds.Boolean') {
if (!anno.literal && !anno.path && getFinalTypeNameOf( elementDecl ) !== 'cds.Boolean') {
if (elementDecl.type?._artifact.name.absolute) {
warning('anno-expecting-value', [ anno.location || anno.name.location, art ],
{ '#': 'type', type: elementDecl.type._artifact });
warning( 'anno-expecting-value', [ anno.location || anno.name.location, art ],
{ '#': 'type', type: elementDecl.type._artifact } );
}
else {
warning('anno-expecting-value', [ anno.location || anno.name.location, art ],
{ '#': 'std', anno: anno.name.absolute });
warning( 'anno-expecting-value', [ anno.location || anno.name.location, art ],
{ '#': 'std', anno: anno.name.absolute } );
}

@@ -967,3 +933,3 @@

// Value must be assignable to type
checkValueAssignableTo(anno, anno, elementDecl, art);
checkValueAssignableTo( anno, anno, elementDecl, art );
}

@@ -987,3 +953,3 @@

if (value.literal !== 'array') {
warning(null, loc, { anno }, 'An array value is required for annotation $(ANNO)');
warning( null, loc, { anno }, 'An array value is required for annotation $(ANNO)' );
return;

@@ -993,3 +959,3 @@ }

for (const valueItem of value.val)
checkValueAssignableTo(value, valueItem, elementDecl._effectiveType.items, art);
checkValueAssignableTo( value, valueItem, elementDecl._effectiveType.items, art );

@@ -1002,3 +968,3 @@ return;

if (value.literal !== 'struct') {
warning(null, loc, { anno }, 'A struct value is required here for annotation $(ANNO)');
warning( null, loc, { anno }, 'A struct value is required here for annotation $(ANNO)' );
return;

@@ -1012,40 +978,40 @@ }

// TODO: Don't rely on name; use actual type
const type = getFinalTypeNameOf(elementDecl);
if (builtins.isStringTypeName(type)) {
const type = getFinalTypeNameOf( elementDecl );
if (builtins.isStringTypeName( type )) {
if (value.literal !== 'string' && value.literal !== 'enum' &&
!elementDecl._effectiveType.enum) {
warning(null, loc, { type, anno },
'A string value is required for type $(TYPE) for annotation $(ANNO)');
warning( null, loc, { type, anno },
'A string value is required for type $(TYPE) for annotation $(ANNO)' );
}
}
else if (builtins.isBinaryTypeName(type)) {
else if (builtins.isBinaryTypeName( type )) {
if (value.literal !== 'string' && value.literal !== 'x') {
warning(null, loc, { type, anno },
'A hexadecimal string value is required for type $(TYPE) for annotation $(ANNO)');
warning( null, loc, { type, anno },
'A hexadecimal string value is required for type $(TYPE) for annotation $(ANNO)' );
}
}
else if (builtins.isNumericTypeName(type)) {
else if (builtins.isNumericTypeName( type )) {
if (value.literal !== 'number' && value.literal !== 'enum' &&
!elementDecl._effectiveType.enum) {
warning(null, loc, { type, anno },
'A numerical value is required for type $(TYPE) for annotation $(ANNO)');
warning( null, loc, { type, anno },
'A numerical value is required for type $(TYPE) for annotation $(ANNO)' );
}
}
else if (builtins.isDateOrTimeTypeName(type)) {
else if (builtins.isDateOrTimeTypeName( type )) {
if (value.literal !== 'date' && value.literal !== 'time' &&
value.literal !== 'timestamp' && value.literal !== 'string') {
warning(null, loc, { type, anno },
// eslint-disable-next-line max-len
'A date/time value or a string is required for type $(TYPE) for annotation $(ANNO)');
warning( null, loc, { type, anno },
// eslint-disable-next-line max-len
'A date/time value or a string is required for type $(TYPE) for annotation $(ANNO)' );
}
}
else if (builtins.isBooleanTypeName(type)) {
else if (builtins.isBooleanTypeName( type )) {
if (value.literal && value.literal !== 'boolean') {
warning(null, loc, { type, anno },
'A boolean value is required for type $(TYPE) for annotation $(ANNO)');
warning( null, loc, { type, anno },
'A boolean value is required for type $(TYPE) for annotation $(ANNO)' );
}
}
else if (builtins.isRelationTypeName(type) || builtins.isGeoTypeName(type)) {
warning(null, loc, { type, anno },
'Type $(TYPE) can\'t be assigned a value for annotation $(ANNO)');
else if (builtins.isRelationTypeName( type ) || builtins.isGeoTypeName( type )) {
warning( null, loc, { type, anno },
'Type $(TYPE) can\'t be assigned a value for annotation $(ANNO)' );
}

@@ -1063,3 +1029,3 @@ else if (!elementDecl._effectiveType.enum) {

// ... but no such constant
warning(null, loc, { id: `#${ value.sym.id }`, anno }, 'Enum symbol $(ID) not found in enum for annotation $(ANNO)');
warning( null, loc, { id: `#${ value.sym.id }`, anno }, 'Enum symbol $(ID) not found in enum for annotation $(ANNO)' );
}

@@ -1069,4 +1035,4 @@ }

// Enum symbol provided but not expected
warning(null, loc, { id: `#${ value.sym.id }`, type, anno },
'Can\'t use enum symbol $(ID) for non-enum type $(TYPE) for annotation $(ANNO)');
warning( null, loc, { id: `#${ value.sym.id }`, type, anno },
'Can\'t use enum symbol $(ID) for non-enum type $(TYPE) for annotation $(ANNO)' );
}

@@ -1076,7 +1042,7 @@ }

// Enum symbol not provided but expected
const hasValidValue = Object.keys(expectedEnum)
.some(symbol => getEnumValue(expectedEnum[symbol]) === value.val);
const hasValidValue = Object.keys( expectedEnum )
.some( symbol => getEnumValue( expectedEnum[symbol] ) === value.val );
if (!hasValidValue) {
// ... and none of the valid enum symbols matches the value
warning(null, loc, { anno }, 'An enum value is required for annotation $(ANNO)');
warning( null, loc, { anno }, 'An enum value is required for annotation $(ANNO)' );
}

@@ -1116,3 +1082,3 @@ }

from._effectiveType?.elements || [];
return resolvePathFrom(path.slice(1), nextStepEnv[path[0].id], result);
return resolvePathFrom( path.slice(1), nextStepEnv[path[0].id], result );
}

@@ -1152,3 +1118,3 @@

othertype: 'sap.common.Locale',
});
} );
}

@@ -1188,2 +1154,5 @@ }

function visitExpression( xpr, user, callback ) {
if (!xpr)
return; // e.g. parse error
callback( xpr, user, null );

@@ -1202,8 +1171,10 @@ visitSubExpression( xpr, user, callback );

if (xpr.args) {
const args = Array.isArray(xpr.args) ? xpr.args : Object.values(xpr.args);
const args = Array.isArray( xpr.args ) ? xpr.args : Object.values( xpr.args );
// Check for illegal argument usage within the expression
for (const arg of args) {
callback( arg, user, xpr.args );
// Recursively traverse the argument expression
visitSubExpression(arg, user, callback);
if (arg) { // null for parse errors
callback( arg, user, xpr.args );
// Recursively traverse the argument expression
visitSubExpression( arg, user, callback );
}
}

@@ -1216,3 +1187,3 @@ }

callback( arg.where, user, arg );
visitSubExpression(arg.where, user, callback);
visitSubExpression( arg.where, user, callback );
}

@@ -1219,0 +1190,0 @@ }

@@ -27,4 +27,4 @@ // Base classes used as prototypes for XSN definitions, elements, etc.

dependencies = [];
artifacts = Object.create(null);
vocabularies = Object.create(null);
artifacts = Object.create( null );
vocabularies = Object.create( null );
extensions = [];

@@ -31,0 +31,0 @@ }

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

const a = definitions[name];
if (Array.isArray(a))
if (Array.isArray( a ))
a.forEach( strongConnectRec );

@@ -40,5 +40,5 @@ else

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

@@ -51,3 +51,3 @@ return;

while (a)
a = strongConnect(a);
a = strongConnect( a );
}

@@ -66,3 +66,3 @@

} );
stack.push(v);
stack.push( v );
// console.log('PUSH: ', v.kind,v.name)

@@ -134,3 +134,3 @@ }

if (w.art._scc)
todos.push(w.art);
todos.push( w.art );
}

@@ -137,0 +137,0 @@ }

@@ -144,4 +144,4 @@ // Compiler phase 1 = "define": transform dictionary of AST-like XSNs into XSN

const $location = Symbol.for('cds.$location');
const $inferred = Symbol.for('cds.$inferred');
const $location = Symbol.for( 'cds.$location' );
const $inferred = Symbol.for( 'cds.$inferred' );

@@ -198,7 +198,7 @@ /**

}
model.definitions = Object.create(null);
model.definitions = Object.create( null );
setLink( model, '_entities', [] ); // for entities with includes
model.$entity = 0;
model.$compositionTargets = Object.create(null);
model.$collectedExtensions = Object.create(null);
model.$compositionTargets = Object.create( null );
model.$collectedExtensions = Object.create( null );

@@ -233,3 +233,3 @@ initBuiltins( model );

let prefix = namespace ? `${ pathName( namespace ) }.` : '';
if (isInReservedNamespace(prefix)) {
if (isInReservedNamespace( prefix )) {
error( 'reserved-namespace-cds', [ src.namespace.location, src.namespace ], { name: 'cds' },

@@ -247,3 +247,3 @@ 'The namespace $(NAME) is reserved for CDS builtins' );

else if (src.usings || src.namespace) {
src.artifacts = Object.create(null);
src.artifacts = Object.create( null );
}

@@ -261,7 +261,7 @@ if (src.usings)

prefix = '';
dictForEach( shuffleDict( src.definitions ), def => addDefinition( def, src, prefix ));
dictForEach( shuffleDict( src.definitions ), def => addDefinition( def, src, prefix ) );
}
if (src.vocabularies) {
if (!model.vocabularies)
model.vocabularies = Object.create(null);
model.vocabularies = Object.create( null );
dictForEach( shuffleDict( src.vocabularies ), v => addVocabulary( v, src, prefix ) );

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

// TODO: art.name.absolute = art.name.id || …
art.name.absolute = (!art.name.path) ? art.name.id : prefix + pathName(art.name.path);
art.name.absolute = (!art.name.path) ? art.name.id : prefix + pathName( art.name.path );
}
const { absolute } = art.name;
// TODO: check reserved, see checkName()/checkLocalizedObjects() of checks.js
if (absolute === 'cds' || isInReservedNamespace(absolute)) {
if (absolute === 'cds' || isInReservedNamespace( absolute )) {
error( 'reserved-namespace-cds', [ art.name.location, art ], { name: 'cds' },

@@ -307,3 +307,3 @@ 'The namespace $(NAME) is reserved for CDS builtins' );

const d = artifacts[name];
const a = Array.isArray(d) ? d[0] : d;
const a = Array.isArray( d ) ? d[0] : d;
if (!a.name.absolute)

@@ -373,3 +373,3 @@ a.name.absolute = prefix + name;

const { id } = last;
if (src.artifacts[id] || last.id.includes('.'))
if (src.artifacts[id] || last.id.includes( '.' ))
// not used as we have a definition/using with that name, or dotted last path id

@@ -462,3 +462,5 @@ return;

// TODO: art.name.absolute = vocab.name.id || …
vocab.name.absolute = (!vocab.name.path) ? vocab.name.id : prefix + pathName(vocab.name.path);
vocab.name.absolute = (!vocab.name.path)
? vocab.name.id
: prefix + pathName( vocab.name.path );
}

@@ -473,4 +475,4 @@ dictAdd( model.vocabularies, name.absolute, vocab );

// TODO: the sequence should be in sync with extend / annotate / future $sources
const sortedSources = Object.keys(model.sources)
.filter(name => !!model.sources[name].i18n)
const sortedSources = Object.keys( model.sources )
.filter( name => !!model.sources[name].i18n )
.sort( (a, b) => compareLayer( model.sources[a], model.sources[b] ) );

@@ -570,3 +572,3 @@

const entry = src.artifacts[name];
if (!Array.isArray(entry)) // no local name duplicate
if (!Array.isArray( entry )) // no local name duplicate
continue;

@@ -617,3 +619,3 @@ for (const decl of entry) {

const { absolute } = art.name;
const dot = absolute.lastIndexOf('.');
const dot = absolute.lastIndexOf( '.' );
if (dot < 0)

@@ -632,3 +634,3 @@ return;

if (!parent._subArtifacts)
setLink( parent, '_subArtifacts', Object.create(null) );
setLink( parent, '_subArtifacts', Object.create( null ) );
if (art.$duplicates !== true) // no redef or "first def"

@@ -654,3 +656,3 @@ parent._subArtifacts[absolute.substring( dot + 1 )] = art; // not dictAdd()

setLink( self, '_origin', art );
art.$tableAliases = Object.create(null);
art.$tableAliases = Object.create( null );
art.$tableAliases[selfname] = self;

@@ -719,9 +721,13 @@ }

setLink( query, '_leadingQuery', leading );
if (leading && query.orderBy) {
if (leading.$orderBy)
if (leading) {
if (query.orderBy) {
leading.$orderBy ??= [ ];
leading.$orderBy.push( ...query.orderBy );
else
leading.$orderBy = [ ...query.orderBy ];
}
if (query.limit) {
leading.$limit ??= [ ];
leading.$limit.push( query.limit );
}
}
// ORDER BY to be evaluated in leading query (LIMIT is literal)
// ORDER BY and LIMIT to be evaluated in leading query
}

@@ -736,3 +742,3 @@ else { // with parse error (`select from <EOF>`, `select from E { *, ( select }`)

setLink( query, '_$next',
(art.kind === '$tableAlias' ? art._parent._$next : art) );
(art.kind === '$tableAlias' ? art._parent._$next : art ) );
setLink( query, '_block', art._block );

@@ -761,3 +767,3 @@ query.kind = 'select';

const last = table.path[table.path.length - 1];
const dot = last?.id?.lastIndexOf('.');
const dot = last?.id?.lastIndexOf( '.' );
const id = (dot >= 0) ? last.id.substring( dot + 1 ) : last.id || '';

@@ -815,2 +821,3 @@ // TODO: if we have too much time, we can calculate the real location with '.'

table.name.param = aliases[1] || aliases[0] || '<unknown>';
setLink( table, '_user', query ); // TODO: do not set kind/name
setLink( table, '_$next', query._$next );

@@ -830,3 +837,3 @@ // TODO: probably set this to query if we switch to name restriction in JOIN

error( 'name-missing-alias', [ tableAlias.location, semanticLoc ],
{ '#': 'duplicate', code: 'as ‹alias›' });
{ '#': 'duplicate', code: 'as ‹alias›' } );
}

@@ -867,3 +874,3 @@ else {

// TODO: use traverseExpr()
if (Array.isArray(expr)) { // TODO: old-style $parens ?
if (Array.isArray( expr )) { // TODO: old-style $parens ?
expr.forEach( e => initExprForQuery( e, query ) );

@@ -878,3 +885,3 @@ }

else if (expr.args) {
const args = Array.isArray(expr.args) ? expr.args : Object.values( expr.args );
const args = Array.isArray( expr.args ) ? expr.args : Object.values( expr.args );
args.forEach( e => initExprForQuery( e, query ) );

@@ -885,3 +892,3 @@ }

expr.$expected = 'approved-exists';
approveExistsInChildren(expr);
approveExistsInChildren( expr );
}

@@ -963,3 +970,3 @@ }

// Warning about first annotation should be enough to avoid spam.
const firstAnno = Object.keys(col).find(key => key.startsWith('@'));
const firstAnno = Object.keys( col ).find( key => key.startsWith( '@' ) );
if (firstAnno) {

@@ -982,3 +989,3 @@ warning( 'syntax-ignoring-anno', [ col[firstAnno].name.location, col ],

{ prop: '*' },
'Excluding elements without wildcard $(PROP) has no effect');
'Excluding elements without wildcard $(PROP) has no effect' );
}

@@ -1010,7 +1017,7 @@ }

if (exprOrPathElement.args)
exprOrPathElement.args.forEach(elem => approveExistsInChildren(elem));
exprOrPathElement.args.forEach( elem => approveExistsInChildren( elem ) );
else if (exprOrPathElement.where && exprOrPathElement.where.args)
exprOrPathElement.where.args.forEach(elem => approveExistsInChildren(elem));
exprOrPathElement.where.args.forEach( elem => approveExistsInChildren( elem ) );
else if (exprOrPathElement.path)
exprOrPathElement.path.forEach(elem => approveExistsInChildren(elem));
exprOrPathElement.path.forEach( elem => approveExistsInChildren( elem ) );
}

@@ -1247,5 +1254,2 @@ // TODO: we might issue 'expr-unexpected-exists' and 'expr-no-subquery' already in

return false;
const names = Object.keys( dict );
if (!names.length) // TODO: re-check - really allow empty dict if no other?
return false;
const feature = kindProperties[parent.kind][prop];

@@ -1256,6 +1260,16 @@ if (feature &&

const location = dict[$location];
// TODO: a bit inconsistent = not a simple switch on `prop`…
if (prop === 'actions') {
error( 'def-unexpected-actions', [ location, construct ], {},
'Actions and functions only exist top-level and for entities' );
if (Object.keys( dict ).length) {
error( 'def-unexpected-actions', [ location, construct ], {}, // TODO: ext-
'Actions and functions only exist top-level and for entities' ); // or aspects
}
else {
warning( 'ext-ignoring-actions', [ location, construct ], {},
'Actions and functions only exist top-level and for entities' );
return false;
}
}
//
else if (parent.kind === 'action' || parent.kind === 'function') {

@@ -1282,6 +1296,11 @@ error( 'ext-unexpected-action', [ construct.location, construct ], { '#': parent.kind }, {

else if (feature) { // allowed in principle, but not with extend
if (parent.$inferred === 'include') { // special case for better error message
if (!Object.keys( dict ).length) {
warning( 'ext-ignoring-elements', [ location, construct ], {},
'Only structures with directly specified elements can be extended by elements' );
return false;
}
else if (parent.$inferred === 'include') { // special case for better error message
const variant = (construct.enum || construct.elements) ? 'elements' : 'std';
error( 'ref-expected-direct-structure', [ location, construct ],
{ '#': variant, art: parent });
{ '#': variant, art: parent } );
}

@@ -1307,4 +1326,6 @@ else {

// Return whether the `target` is actually a `targetAspect`
// TODO: really do that here and not in kick-start.js?
/**
* Return whether the `target` is actually a `targetAspect`
* TODO: really do that here and not in kick-start.js?
*/
function targetIsTargetAspect( elem ) {

@@ -1311,0 +1332,0 @@ const { target } = elem;

@@ -28,3 +28,3 @@ // Extend

const $location = Symbol.for('cds.$location');
const $location = Symbol.for( 'cds.$location' );

@@ -57,6 +57,6 @@ // attach stupid location - TODO: remove in v5

const includesNonShadowedFirst = isDeprecatedEnabled(model.options, 'includesNonShadowedFirst');
const includesNonShadowedFirst = isDeprecatedEnabled( model.options, 'includesNonShadowedFirst' );
sortModelSources();
const extensionsDict = Object.create(null); // TODO TMP
const extensionsDict = Object.create( null ); // TODO TMP
forEachDefinition( model, tagIncludes ); // TODO TMP

@@ -139,17 +139,23 @@

if (art.kind === 'annotate' && !art.returns && extensionsMap.returns)
if (art.kind === 'annotate' && !art.returns && extensionsMap.returns && !art._parent?.returns)
annotateCreate( art, '', art, 'returns' );
moveDictExtensions( art, extensionsMap, 'actions' );
moveDictExtensions( art, extensionsMap, 'params' );
moveReturnsExtensions( art, extensionsMap );
const sub = art.returns || art.items || art.targetAspect?.elements && art.targetAspect;
if (sub) {
if (art.returns) { // after having applied params!
if (art.returns) {
pushToDict( art.returns, '_extensions', ...extensionsMap.elements || [] );
pushToDict( art.returns, '_extensions', ...extensionsMap.enum || [] );
if (art.kind !== 'annotate') {
extendHandleReturns( extensionsMap.elements, art );
extendHandleReturns( extensionsMap.enum, art );
return;
}
// care about 'ext-unexpected-returns' in a later change if we have XSN returns
pushToDict( sub, '_extensions', ...avoidRecursiveReturns(extensionsMap, 'elements'));
pushToDict( sub, '_extensions', ...avoidRecursiveReturns(extensionsMap, 'enum') );
}
const sub = art.items || art.targetAspect?.elements && art.targetAspect;
if (sub) {
pushToDict( sub, '_extensions', ...extensionsMap.elements || [] );
pushToDict( sub, '_extensions', ...extensionsMap.enum || [] );
}
else {

@@ -160,25 +166,5 @@ moveDictExtensions( art, extensionsMap,

}
moveDictExtensions( art, extensionsMap, 'actions' );
}
/**
* FIXME: Remove this workaround. This workaround avoids endless recursion for annotate statements
* that have both "returns" and "elements". The endless recursion happens due to the
* pushDict on `sub`. The `elements` extensions will point to the same extension on which
* `returns` exist.
*
* @param extensionsMap
* @param {string} prop
* @return {XSN.Extension[]}
*/
function avoidRecursiveReturns( extensionsMap, prop ) {
if (!extensionsMap[prop])
return [];
const exts = [];
for (const ext of extensionsMap[prop])
exts.push({ ...ext, returns: undefined });
return exts;
}
/**
* Create super annotate statements for remaining extensions

@@ -231,3 +217,3 @@ */

service: 'Artifact $(ART) is not of kind $(KIND), use $(CODE) or $(KEYWORD) instead',
});
} );
}

@@ -242,3 +228,3 @@ // TODO: Use similar checks for EXTEND ENTITY etc - 'ext-ignoring-kind'

const hasOnlySubExtensions = art._outer; // items, anonymous aspects
const dict = Object.create(null);
const dict = Object.create( null );
for (const ext of art._extensions) {

@@ -444,3 +430,3 @@ for (const prop in ext) {

result.push( prevItem );
if (upToSpec && prevItem && equalUpTo( prevItem, item.upTo)) {
if (upToSpec && prevItem && equalUpTo( prevItem, item.upTo )) {
upToSpec = false;

@@ -616,3 +602,3 @@ break;

const elem = artDict[name] || annotateFor( art, extProp, name );
setLink( elemExt.name, '_artifact', (elem.kind !== 'annotate' ? elem : null) );
setLink( elemExt.name, '_artifact', (elem.kind !== 'annotate' ? elem : null ) );
pushToDict( elem, '_extensions', elemExt );

@@ -624,10 +610,13 @@ }

function moveReturnsExtensions( art, extensionsMap ) {
if (!extensionsMap.returns)
const extensions = extensionsMap.returns;
if (!extensions)
return;
const artReturns = art.returns;
let extReturns = artReturns;
const isAction = art.kind === 'action' || art.kind === 'function';
for (const ext of extensionsMap.returns) {
if (!art.returns && art.kind !== 'annotate') { // no check in super annotate statement
const variant = art.kind === 'action' || art.kind === 'function' ? art.kind : 'std';
for (const ext of extensions) {
if (!artReturns && art.kind !== 'annotate') {
warning( 'ext-unexpected-returns', [ ext.returns.location, ext ], {
'#': variant, keyword: 'returns',
'#': (isAction ? art.kind : 'std'), keyword: 'returns',
}, {

@@ -638,8 +627,14 @@ std: 'Unexpected $(KEYWORD); only actions and functions have return parameters',

function: 'Unexpected $(KEYWORD) for function without return parameter',
});
} );
// Do not put completely wrong returns into a “super annotate” statement;
// this could induce consequential errors with [..., …]:
if (!isAction)
continue; // do not put into 'extensions'
// add to 'extensions' for action/function without returns:
extReturns ??= annotateFor( art, 'params', '' );
}
// If `!art.returns`, then it could be CSN from SQL, where actions are replaced by dummies.
const elem = art.returns || annotateFor( art, 'params', '' );
setLink( ext.name, '_artifact', (elem.kind !== 'annotate' ? elem : null) );
pushToDict(elem, '_extensions', ext.returns);
if (extReturns) {
setLink( ext.name, '_artifact', (isAction ? artReturns : null ) );
pushToDict( extReturns, '_extensions', ext.returns );
}
}

@@ -733,3 +728,3 @@ }

entity: 'Elements of entity types can\'t be annotated',
});
} );
break;

@@ -822,4 +817,2 @@ case 'params':

return;
// else if (ext.kind === 'extend') { // TODO v4 - add error
// }
if (art?.kind === 'namespace') {

@@ -849,3 +842,3 @@ // TODO: not at all different to having no definition

(art.kind === 'action' || art.kind === 'function') && construct.elements) {
warning('ext-expecting-returns', [ construct.name.location, construct ], {
warning( 'ext-expecting-returns', [ construct.name.location, construct ], {
'#': art.kind, keyword: 'returns', code: 'annotate ‹name› with returns { … }',

@@ -856,3 +849,3 @@ }, {

function: 'Expected $(KEYWORD) when annotating function return structure, i.e. $(CODE)',
});
} );
}

@@ -900,3 +893,3 @@ }

if (!noIncludes && !(canApplyIncludes( art, art ) &&
extensions.every( ext => canApplyIncludes(ext, art) )))
extensions.every( ext => canApplyIncludes( ext, art ) )))
return false;

@@ -909,3 +902,3 @@ if (Array.isArray( noIncludes )) {

!(canApplyIncludes( art, art ) &&
extensions.every( ext => canApplyIncludes( ext, art) ))) {
extensions.every( ext => canApplyIncludes( ext, art ) ))) {
// console.log( 'FALSE:',art.name, extensions.map( e => e.name ) )

@@ -961,3 +954,3 @@ return false;

if (art.includes)
art.includes.push(...ext.includes);
art.includes.push( ...ext.includes );
else

@@ -971,3 +964,3 @@ art.includes = [ ...ext.includes ];

initMembers( ext, art, ext._block ); // might set _extend, _annotate
dependsOnSilent(art, ext); // art depends silently on ext (inverse to normal dep!)
dependsOnSilent( art, ext ); // art depends silently on ext (inverse to normal dep!)
}

@@ -986,3 +979,3 @@ for (const name in ext.elements) {

// This whole function will be removed with a next change - no need to have nice code here:
const dict = Object.create(null);
const dict = Object.create( null );
// actions cannot be extended anyway. TODO: there should be a message

@@ -1029,3 +1022,4 @@ // (possible with CSN input), but that was missing before this change, too.

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

@@ -1078,3 +1072,3 @@ lastExt = ext;

} );
attachAndEmitValidNames(msg, validDict);
attachAndEmitValidNames( msg, validDict );
}

@@ -1121,4 +1115,7 @@ }

* If `ext !== art`, applies includes on the extensions, not artifact.
* Sets `_ancestor` links on `art`.
* Sets `_ancestors` links on `art`.
*
* TODO: try to set `_ancestors` only to entities (but beware “intermediate”
* non-entities).
*
* Examples:

@@ -1151,3 +1148,14 @@ * ext === art: `entity E : F {}` => add elements of F to E

includeMembers( ext, art, 'elements' );
includeMembers( ext, art, 'actions' );
if (art.kind !== 'type') {
includeMembers( ext, art, 'actions' );
}
else {
for (const ref of ext.includes) {
const template = ref._artifact; // already resolved
if (template?.actions && Object.keys( template.actions ).length) {
warning( 'ref-ignoring-actions', [ ref.location, ext ], { art: template },
'The actions of $(ART) are not added to the type' );
}
}
}
}

@@ -1169,4 +1177,4 @@

const members = ext[prop];
if (members || art[prop])
ext[prop] = Object.create(null);
if (members)
ext[prop] = Object.create( null );
let hasNewElement = false;

@@ -1178,3 +1186,3 @@

if (template[prop] && !ext[prop])
ext[prop] = Object.create(null);
ext[prop] = Object.create( null );
// eslint-disable-next-line no-loop-func

@@ -1193,3 +1201,3 @@ forEachInOrder( template, prop, ( origin, name ) => {

elem.$inferred = 'include';
if (origin.masked)
if (origin.masked) // TODO(v5): remove 'masked'
elem.masked = Object.assign( { $inferred: 'include' }, origin.masked );

@@ -1207,3 +1215,3 @@ if (origin.key)

// TODO: also complain if elem is just defined in art
});
} );
}

@@ -1224,3 +1232,3 @@ }

dictAdd( ext[prop], name, elem );
});
} );
}

@@ -1238,9 +1246,9 @@ }

return;
forEachInOrder(parent, prop, ( member, name ) => {
if (member.$inferred === 'include' && Array.isArray(member.$duplicates)) {
const includes = [ member, ...member.$duplicates ].map(dup => dup._origin._main);
forEachInOrder( parent, prop, ( member, name ) => {
if (member.$inferred === 'include' && Array.isArray( member.$duplicates )) {
const includes = [ member, ...member.$duplicates ].map( dup => dup._origin._main );
error( 'duplicate-definition', [ parent.name.location, member ],
{ '#': `include-${ prop }`, name, sorted_arts: includes } );
}
});
} );
}

@@ -1257,3 +1265,3 @@ }

function layeredExtensions( extensions ) {
const layered = Object.create(null);
const layered = Object.create( null );
for (const ext of extensions) {

@@ -1290,2 +1298,3 @@ const layer = (ext.kind === 'annotate' || ext.kind === 'extend' || ext.kind === 'source') &&

const highest = layered[name]?.extensions || [];
highest.sort( compareExtensions );
delete layered[name];

@@ -1296,3 +1305,3 @@ return { highest, issue: inMoreThanOneFile( highest ) };

// collect all layers which are lower than another layer
const allExtends = Object.create(null);
const allExtends = Object.create( null );
allExtends[''] = {}; // the "Define" layer

@@ -1315,3 +1324,3 @@ for (const name of layerNames) {

highest.sort( compareExtensions );
const good = highestLayers.every( layer => !inMoreThanOneFile( layer.extensions ));
const good = highestLayers.every( layer => !inMoreThanOneFile( layer.extensions ) );
// TODO: use layer.file instead

@@ -1327,3 +1336,3 @@ const issue = !good || highestLayers.length > 1 && 'unrelated';

const file = extensions[0].location?.file;
return !file || extensions.slice(1).some( e => e.location?.file !== file );
return !file || extensions.some( e => e.location?.file !== file );
}

@@ -1330,0 +1339,0 @@

@@ -50,4 +50,4 @@ // Things which needs to done for parse.cdl after define()

forEachGeneric(model, 'definitions', art => resolveTypesForParseCdl(art, art));
forEachGeneric(model, 'vocabularies', art => resolveTypesForParseCdl(art, art));
forEachGeneric( model, 'definitions', art => resolveTypesForParseCdl( art, art ) );
forEachGeneric( model, 'vocabularies', art => resolveTypesForParseCdl( art, art ) );

@@ -58,3 +58,3 @@ if (extensions.length > 0) {

model.extensions = extensions;
model.extensions.forEach(ext => resolveTypesForParseCdl(ext, ext));
model.extensions.forEach( ext => resolveTypesForParseCdl( ext, ext ) );
}

@@ -74,5 +74,5 @@ }

if (Array.isArray(artifact)) {
if (Array.isArray( artifact )) {
// e.g. `args` array
artifact.forEach(art => resolveTypesForParseCdl(art, main));
artifact.forEach( art => resolveTypesForParseCdl( art, main ) );
return;

@@ -86,6 +86,6 @@ }

if (artifact.type)
resolveTypeUnchecked(artifact, main);
resolveTypeUnchecked( artifact, main );
for (const include of artifact.includes || [])
resolveUncheckedPath(include, 'include', main);
resolveUncheckedPath( include, 'include', main );

@@ -95,10 +95,10 @@ // define.js takes care that `target` is a ref and

if (artifact.target)
resolveUncheckedPath(artifact.target, 'target', main);
resolveUncheckedPath( artifact.target, 'target', main );
if (artifact.targetAspect)
resolveTypesForParseCdl(artifact.targetAspect, main);
resolveTypesForParseCdl( artifact.targetAspect, main );
if (artifact.from) {
const { from } = artifact;
resolveUncheckedPath(from, 'from', main);
resolveTypesForParseCdl(from, main);
resolveUncheckedPath( from, 'from', main );
resolveTypesForParseCdl( from, main );
}

@@ -109,11 +109,11 @@

for (const prop in artifact) {
if (artifact[prop] === undefined || parseCdlSpeciallyHandledXsnProps.includes(prop) ||
parseCdlIgnoredXsnProps.includes(prop))
if (artifact[prop] === undefined || parseCdlSpeciallyHandledXsnProps.includes( prop ) ||
parseCdlIgnoredXsnProps.includes( prop ))
continue;
if (artifact[prop] && Object.getPrototypeOf(artifact[prop]) === null)
if (artifact[prop] && Object.getPrototypeOf( artifact[prop] ) === null)
// Dictionary in XSN
forEachGeneric(artifact, prop, art => resolveTypesForParseCdl(art, art));
forEachGeneric( artifact, prop, art => resolveTypesForParseCdl( art, art ) );
else
resolveTypesForParseCdl(artifact[prop], main);
resolveTypesForParseCdl( artifact[prop], main );
}

@@ -125,8 +125,8 @@

// TODO: If resolveTypeUnchecked is reworked, we may be able to simplify this coding.
(artifact.$queries || []).forEach( art => resolveTypesForParseCdl(art, artifact) );
(artifact.columns || []).forEach( art => resolveTypesForParseCdl(art, artifact) );
forEachGeneric( artifact, 'mixin', art => resolveTypesForParseCdl(art, artifact) );
(artifact.$queries || []).forEach( art => resolveTypesForParseCdl( art, artifact ) );
(artifact.columns || []).forEach( art => resolveTypesForParseCdl( art, artifact ) );
forEachGeneric( artifact, 'mixin', art => resolveTypesForParseCdl( art, artifact ) );
// For better error messages for `type of`s in `returns`, we pass the object as the new main.
resolveTypesForParseCdl(artifact.returns, artifact.returns);
resolveTypesForParseCdl( artifact.returns, artifact.returns );

@@ -136,16 +136,16 @@ // Because `args` can be used in different contexts with different semantics,

if (artifact.args && typeof artifact.args === 'object') {
if (Array.isArray(artifact.args)) {
if (Array.isArray( artifact.args )) {
// `args` may either be an array (e.g. query 'from' args) ...
artifact.args.forEach((from) => {
artifact.args.forEach( (from) => {
// ... and could be either inside a `from` ...
if (from && from.kind === '$tableAlias')
resolveUncheckedPath(from, 'from', from._main);
resolveUncheckedPath( from, 'from', from._main );
// ... or only params ...
resolveTypesForParseCdl(from, main);
});
resolveTypesForParseCdl( from, main );
} );
}
else {
// ... or dictionary (e.g. params)
forEachGeneric(artifact, 'args', obj => resolveTypesForParseCdl(obj, obj));
forEachGeneric( artifact, 'args', obj => resolveTypesForParseCdl( obj, obj ) );
}

@@ -152,0 +152,0 @@ }

@@ -21,2 +21,3 @@ // Generate: localized data and managed compositions

} = require('./utils');
const { weakLocation } = require('../base/messages');

@@ -37,3 +38,3 @@ function generate( model ) {

const addTextsLanguageAssoc = checkTextsLanguageAssocOption(model, options);
const addTextsLanguageAssoc = checkTextsLanguageAssocOption( model, options );
const useTextsAspect = checkTextsAspect();

@@ -68,10 +69,10 @@

const processed = new WeakSet();
forEachDefinition(model, processCompositionPersistence);
forEachDefinition( model, processCompositionPersistence );
function processCompositionPersistence( def ) {
if (def.$inferred === 'composition-entity' && !processed.has(def)) {
if (def.$inferred === 'composition-entity' && !processed.has( def )) {
if (def._parent)
processCompositionPersistence(def._parent);
processCompositionPersistence( def._parent );
copyPersistenceAnnotations( def, def._parent );
processed.add(def);
processed.add( def );
}

@@ -96,4 +97,4 @@ }

if (textsAspect.kind !== 'aspect' || !textsAspect.elements) {
error('def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
{ '#': 'no-aspect', art: textsAspect });
error( 'def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
{ '#': 'no-aspect', art: textsAspect } );
return false;

@@ -105,6 +106,6 @@ }

const lang = textsAspect.elements.language;
error('def-unexpected-element', [ lang.name.location, lang ],
{ option: 'addTextsLanguageAssoc', art: textsAspect, name: 'language' },
// eslint-disable-next-line max-len
'$(ART) is not used because option $(OPTION) conflicts with existing element $(NAME); remove either option or element' );
error( 'def-unexpected-element', [ lang.name.location, lang ],
{ option: 'addTextsLanguageAssoc', art: textsAspect, name: 'language' },
// eslint-disable-next-line max-len
'$(ART) is not used because option $(OPTION) conflicts with existing element $(NAME); remove either option or element' );
hasError = true;

@@ -117,4 +118,4 @@ }

if (!elem) {
error('def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
{ '#': 'missing', art: textsAspect, name });
error( 'def-invalid-texts-aspect', [ textsAspect.name.location, textsAspect ],
{ '#': 'missing', art: textsAspect, name } );
hasError = true;

@@ -124,4 +125,4 @@ }

const loc = elem.key?.location || elem.name?.location || textsAspect.name.location;
error('def-invalid-texts-aspect', [ loc, elem ],
{ '#': expected.key ? 'key' : 'no-key', art: elem });
error( 'def-invalid-texts-aspect', [ loc, elem ],
{ '#': expected.key ? 'key' : 'no-key', art: elem } );
hasError = true;

@@ -203,5 +204,5 @@ }

if (fioriEnabled)
protectedElements.push('ID_texts');
protectedElements.push( 'ID_texts' );
if (addTextsLanguageAssoc)
protectedElements.push('language');
protectedElements.push( 'language' );

@@ -225,2 +226,9 @@ for (const name in art.elements) {

}
if (isKey && isLocalized) {
const errpos = elem.localized || elem.type || elem.name;
warning( 'def-ignoring-localized', [ errpos.location, elem ],
{ keyword: 'localized' },
'Keyword $(KEYWORD) is ignored for primary keys' );
}
}

@@ -348,4 +356,4 @@ if (textElems.length <= keys)

const { elements } = art;
art.elements = Object.create(null);
const names = [ 'ID_texts', 'locale', ...Object.keys(elements) ];
art.elements = Object.create( null );
const names = [ 'ID_texts', 'locale', ...Object.keys( elements ) ];
for (const name of names)

@@ -380,3 +388,3 @@ art.elements[name] = elements[name];

const textsAspect = model.definitions['sap.common.TextsAspect'];
const elements = Object.create(null);
const elements = Object.create( null );
const { location } = base.name;

@@ -429,3 +437,3 @@ const art = {

function createTextsEntityWithDefaultElements( base, absolute, fioriEnabled ) {
const elements = Object.create(null);
const elements = Object.create( null );
const { location } = base.name;

@@ -543,3 +551,3 @@ const art = {

function hasTruthyProp( art, prop ) {
const processed = Object.create(null); // avoid infloops with circular refs
const processed = Object.create( null ); // avoid infloops with circular refs
let name = art.name.absolute; // is ok, since no recursive type possible

@@ -584,3 +592,3 @@ while (art && !processed[name]) {

function baseKeys() {
const k = Object.create(null);
const k = Object.create( null );
for (const name in base.elements) {

@@ -671,3 +679,3 @@ const elem = base.elements[name];

const names = Object.keys( target.elements )
.filter( n => n.startsWith('up__') && keyNames.includes( n.substring(4) ) );
.filter( n => n.startsWith( 'up__' ) && keyNames.includes( n.substring(4) ) );
if (names.length) {

@@ -678,7 +686,7 @@ // FUTURE: if named type, add sub info with location of "up_" element

one: 'Key element $(NAMES) can\'t be added to $(TARGET) as it already exist',
});
} );
return false;
}
if (elem.type && !isDirectComposition(elem)) {
if (elem.type && !isDirectComposition( elem )) {
// Only issue warning for direct usages, not for projections, includes, etc.

@@ -688,3 +696,3 @@ // TODO: Make it configurable error; v4: error

{ prop: 'Composition of', otherprop: 'Association to' },
'Expected $(PROP), but found $(OTHERPROP) for composition of aspect');
'Expected $(PROP), but found $(OTHERPROP) for composition of aspect' );
}

@@ -709,5 +717,10 @@

kind: 'entity',
name: { path: splitIntoPath( location, entityName ), absolute: entityName, location },
name: {
path: splitIntoPath( location, entityName ),
absolute: entityName,
// for code navigation (e.g. via `extend`s): point to the element's name
location: elem.name.location,
},
location,
elements: Object.create(null),
elements: Object.create( null ),
$inferred: 'composition-entity',

@@ -725,13 +738,15 @@ };

// Since there is no user-written up_ element, use a weak location to the beginning of {…}.
const upLocation = weakLocation( location );
const up = { // elements.up_ = ...
name: { location, id: 'up_' },
name: { location: upLocation, id: 'up_' },
kind: 'element',
location,
location: upLocation,
$inferred: 'aspect-composition',
type: augmentPath( location, 'cds.Association' ),
target: augmentPath( location, base.name.absolute ),
type: augmentPath( upLocation, 'cds.Association' ),
target: augmentPath( upLocation, base.name.absolute ),
cardinality: {
targetMin: { val: 1, literal: 'number', location },
targetMax: { val: 1, literal: 'number', location },
location,
targetMin: { val: 1, literal: 'number', location: upLocation },
targetMax: { val: 1, literal: 'number', location: upLocation },
location: upLocation,
},

@@ -745,12 +760,13 @@ };

'up__', '@odata.containment.ignore' );
up.on = augmentEqual( location, 'up_', Object.values( keys ), 'up__' );
up.on = augmentEqual( upLocation, 'up_', Object.values( keys ), 'up__' );
}
else {
up.key = { location, val: true };
up.key = { location: upLocation, val: true };
// managed associations must be explicitly set to not null
// even if target cardinality is 1..1
up.notNull = { location, val: true };
up.notNull = { location: upLocation, val: true };
}
dictAdd( art.elements, 'up_', up);
dictAdd( art.elements, 'up_', up );
// Only for named aspects, use a new location; otherwise use the origin's one.
addProxyElements( art, target.elements, 'aspect-composition', target.name && location );

@@ -773,3 +789,3 @@

const origin = elements[name];
const proxy = linkToOrigin( origin, pname, null, null, location || origin.location );
const proxy = linkToOrigin( origin, pname, null, null, location, true );
setLink( proxy, '_block', origin._block );

@@ -825,3 +841,3 @@ proxy.$inferred = inferred;

function eq( refs ) {
if (Array.isArray(refs))
if (Array.isArray( refs ))
return { op: { val: '=', location }, args: refs.map( ref ), location };

@@ -840,3 +856,3 @@

function ref( path ) {
return { path: path.split('.').map( id => ({ id, location }) ), location };
return { path: path.split( '.' ).map( id => ({ id, location }) ), location };
}

@@ -852,3 +868,3 @@ }

const loc = model.definitions['sap.common.Languages']?.name?.location || null;
model.$messageFunctions.info('api-ignoring-language-assoc', loc, {
model.$messageFunctions.info( 'api-ignoring-language-assoc', loc, {
'#': variant, option: 'addTextsLanguageAssoc', art: 'sap.common.Languages', name: 'code',

@@ -858,3 +874,3 @@ }, {

code: 'Ignoring option $(OPTION) because entity $(ART) is missing element $(NAME)',
});
} );
}

@@ -861,0 +877,0 @@

@@ -15,3 +15,3 @@ // Main XSN-based compiler functions

const { resolveModule, resolveModuleSync } = require('../utils/moduleResolve');
const { makeModuleResolver, makeModuleResolverSync } = require('../utils/moduleResolve');
const parseLanguage = require('../language/antlrParser'); // TODO: should we do some lazyload here?

@@ -57,3 +57,3 @@ const parseCsn = require('../json/from-csn');

constructor( errs, ...args ) {
super(...args);
super( ...args );
this.code = 'ERR_CDS_COMPILER_INVOCATION';

@@ -69,3 +69,3 @@ this.errors = errs;

constructor( arg, ...args ) {
super(...args);
super( ...args );
this.code = 'ERR_CDS_COMPILER_ARGUMENT';

@@ -92,5 +92,5 @@ this.argument = arg;

const parser = options.fallbackParser === 'auto!'
? (source?.startsWith('{') ? parseCsn.parse : parseLanguage)
? (source?.startsWith( '{' ) ? parseCsn.parse : parseLanguage)
: (extensionParsers[ext] || extensionParsers[options.fallbackParser] ||
source.startsWith('{') && parseCsn.parse);
source.startsWith( '{' ) && parseCsn.parse);
if (parser)

@@ -101,3 +101,3 @@ return parser( source, filename, options, messageFunctions );

model.location = new CsnLocation( filename );
messageFunctions.error( 'file-unknown-ext', emptyWeakLocation(filename),
messageFunctions.error( 'file-unknown-ext', emptyWeakLocation( filename ),
{ file: ext, '#': !ext && 'none' }, {

@@ -139,3 +139,3 @@ std: 'Unknown file extension $(FILE)',

//
function compileX( filenames, dir = '', options = {}, fileCache = Object.create(null) ) {
function compileX( filenames, dir = '', options = {}, fileCache = Object.create( null ) ) {
// A non-proper dictionary (i.e. with prototype) is safe if the keys are

@@ -145,13 +145,14 @@ // absolute file names - they start with `/` or `\` or similar

// fileCache = Object.assign( Object.create(null), fileCache );
dir = path.resolve(dir);
dir = path.resolve( dir );
const model = { sources: null, options };
model.$messageFunctions = createMessageFunctions( options, 'compile', model );
const { resolveModule } = makeModuleResolver( options, fileCache, model.$messageFunctions );
let input = null;
let all = processFilenames( filenames, dir )
.then((processedInput) => {
.then( (processedInput) => {
input = processedInput;
model.sources = input.sources;
})
.then(() => promiseAllDoNotRejectImmediately( input.files.map(readAndParse) ))
} )
.then( () => promiseAllDoNotRejectImmediately( input.files.map( readAndParse ) ) )
.then( testInvocation, (reason) => {

@@ -163,3 +164,3 @@ // do not reject with PromiseAllError, use InvocationError:

new InvocationError( [ ...input.repeated, ...errs ]) );
});
} );

@@ -172,3 +173,3 @@ if (!options.parseOnly && !options.parseCdl)

return compileDoX( model );
});
} );

@@ -222,6 +223,4 @@ // Read file `filename` and parse its content, return messages

// Promise executor is called immediately with `new`:
for (const module in dependencies) {
promises.push( resolveModule( dependencies[module], fileCache, options,
model.$messageFunctions ) );
}
for (const module in dependencies)
promises.push( resolveModule( dependencies[module] ) );
}

@@ -249,6 +248,6 @@ if (!promises.length)

*/
function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.create(null) ) {
function compileSyncX( filenames, dir = '', options = {}, fileCache = Object.create( null ) ) {
// A non-proper dictionary (i.e. with prototype) is safe if the keys are
// absolute file names - they start with `/` or `\` or similar
dir = path.resolve(dir);
dir = path.resolve( dir );
const a = processFilenamesSync( filenames, dir );

@@ -258,2 +257,4 @@

model.$messageFunctions = createMessageFunctions( options, 'compile', model );
const { resolveModuleSync } = makeModuleResolverSync( options, fileCache,
model.$messageFunctions );

@@ -264,6 +265,6 @@ const asts = [];

if (err)
errors.push(err);
errors.push( err );
if (ast)
asts.push(ast);
}));
asts.push( ast );
} ) );

@@ -282,3 +283,3 @@ if (errors.length || a.repeated.length) {

for (const fileName of fileNames) {
readAndParseSync(fileName, ( err, ast ) => {
readAndParseSync( fileName, ( err, ast ) => {
if (err)

@@ -288,3 +289,3 @@ throw err;

asts.push( ast );
});
} );
}

@@ -300,3 +301,3 @@ }

if ( filename === false ) { // module which has not been found
cb(null, null);
cb( null, null );
return;

@@ -306,3 +307,3 @@ }

if (typeof rel === 'object') { // already parsed
cb(null, null);
cb( null, null );
return; // no further dependency processing

@@ -316,3 +317,3 @@ }

if (err) {
cb(err, null);
cb( err, null );
}

@@ -329,6 +330,6 @@ else {

catch (e) {
cb(e, null);
cb( e, null );
}
}
});
} );
}

@@ -353,6 +354,4 @@

// Promise executor is called immediately with `new`:
for (const module in dependencies) {
fileNames.push( resolveModuleSync( dependencies[module], fileCache, options,
model.$messageFunctions ) );
}
for (const module in dependencies)
fileNames.push( resolveModuleSync( dependencies[module] ) );
}

@@ -389,3 +388,3 @@ if (!fileNames.length)

sourcesDict = { '<stdin>.cds': sourcesDict };
const sources = Object.create(null);
const sources = Object.create( null );
const model = { sources, options };

@@ -448,5 +447,5 @@ model.$messageFunctions = createMessageFunctions( options, 'compile', model );

const file = csn.$location && csn.$location.file &&
csn.$location.file.replace(/[.]cds$/, '.cds.csn') || '<recompile>.csn';
csn.$location.file.replace( /[.]cds$/, '.cds.csn' ) || '<recompile>.csn';
const sources = Object.create(null);
const sources = Object.create( null );
const model = { sources, options };

@@ -460,3 +459,3 @@ model.$messageFunctions = createMessageFunctions( options, 'compile', model );

if (options.messages) // does not help with exception in compileDoX()
deduplicateMessages(options.messages); // TODO: do better
deduplicateMessages( options.messages ); // TODO: do better
return compiled;

@@ -506,2 +505,3 @@ }

assertConsistency( model );
check( model );
throwWithError();

@@ -511,4 +511,2 @@ if (options.lintMode)

check(model);
throwWithError();
return propagator.propagate( model );

@@ -528,3 +526,3 @@ }

async function processFilenames( filenames, dir ) {
const filenameMap = Object.create(null);
const filenameMap = Object.create( null );

@@ -539,8 +537,8 @@ const promises = [];

// already handles non-existent files.
const promise = fs.promises.realpath(path.resolve(dir, originalName))
.then(setName, () => setName(originalName));
promises.push(promise);
const promise = fs.promises.realpath( path.resolve( dir, originalName ) )
.then( setName, () => setName( originalName ) );
promises.push( promise );
}
await Promise.all(promises);
await Promise.all( promises );
return createSourcesDict( filenames, filenameMap, dir );

@@ -553,6 +551,6 @@ }

function processFilenamesSync( filenames, dir ) {
const filenameMap = Object.create(null);
const filenameMap = Object.create( null );
for (const originalName of filenames) {
let name = path.resolve(dir, originalName);
let name = path.resolve( dir, originalName );
try {

@@ -562,3 +560,3 @@ // Resolve possible symbolic link; if the file does not exist

// already handles non-existent files.
name = fs.realpathSync.native(name);
name = fs.realpathSync.native( name );
}

@@ -585,3 +583,3 @@ catch (e) {

function createSourcesDict( filenames, filenameMap, dir ) {
const sources = Object.create(null);
const sources = Object.create( null );
const files = [];

@@ -594,3 +592,3 @@ const repeated = [];

sources[name] = path.relative( dir, name );
files.push(name);
files.push( name );
}

@@ -597,0 +595,0 @@ else if (typeof sources[name] === 'string') { // not specified more than twice

@@ -36,3 +36,3 @@ // Kick-start: prepare to resolve all references

return; // nothing to do for builtins and redefinitions
if (art.query && art._ancestors === undefined)
if (art.query && art._ancestors === undefined && art.kind === 'entity')
setProjectionAncestors( art );

@@ -48,3 +48,3 @@

// To be removed when nested services are allowed
if (!isBetaEnabled(options, 'nestedServices') && art.kind === 'service') {
if (!isBetaEnabled( options, 'nestedServices' ) && art.kind === 'service') {
while (parent.kind !== 'service')

@@ -71,5 +71,7 @@ parent = parent._parent;

// Service1.E = projection on E
// Remark: _ancestors are also set with includes, and there also for aspects,
// types and events.
const chain = [];
const autoexposed = annotationVal( art['@cds.autoexposed'] );
const preferredRedirectionTarget = annotationVal( art['@cds.redirection.target'] );
// no need to set preferredRedirectionTarget in the while loop as we would

@@ -79,4 +81,3 @@ // use the projection having @cds.redirection.target anyhow instead of

while (art?.query?.from?.path && // direct select with one source
art._ancestors !== 0 && // prevent inf-loop
(preferredRedirectionTarget || !annotationIsFalse( art['@cds.redirection.target'] ) )) {
art._ancestors !== 0) { // prevent inf-loop
chain.push( art );

@@ -116,6 +117,6 @@ setLink( art, '_ancestors', 0 ); // avoid infloop with cyclic from

function expose( ancestor ) {
if (ancestor._service === service)
if (ancestor._service === service || annotationIsFalse( art['@cds.redirection.target'] ))
return;
const desc = ancestor._descendants ||
setLink( ancestor, '_descendants', Object.create(null) );
setLink( ancestor, '_descendants', Object.create( null ) );
if (!desc[sname])

@@ -122,0 +123,0 @@ desc[sname] = [ art ];

@@ -26,3 +26,3 @@ // Module handling, layers and packages

// dependent SCCs are called first
function setExtends( node, representative, sccDeps = Object.create(null) ) {
function setExtends( node, representative, sccDeps = Object.create( null ) ) {
setLink( node, '_layerRepresentative', representative );

@@ -29,0 +29,0 @@ if (layerRepresentative !== representative) {

@@ -51,4 +51,4 @@ // Populate views with elements, elements with association targets, ...

const $inferred = Symbol.for('cds.$inferred');
const $location = Symbol.for('cds.$location');
const $inferred = Symbol.for( 'cds.$inferred' );
const $location = Symbol.for( 'cds.$location' );

@@ -170,3 +170,3 @@ /**

* target entity (including redirections), but not induce calculating the
* target's elements. Calculating an effective structure (entities, …) does
* target's elements. Calculating an effective structure (entities, …) does
* not imply calculating the effective types of its elements. Calculating an

@@ -220,3 +220,3 @@ * effective array does not imply calculating its effective line type.

if (a.typeProps$)
setSpecifiedElementTypeProperties(a);
setSpecifiedElementTypeProperties( a );
}

@@ -399,3 +399,3 @@ // console.log( 'ET-END:', art?.kind, art?.name )

}
if (art.elements || art.kind === '$tableAlias' || art.kind === '$inline' ||
if (art.elements || art.kind === '$inline' ||
// no element expansions for "non-proper" types like

@@ -414,3 +414,4 @@ // entities (as parameter types) etc:

// .toString(), Object.keys(struct.elements))
proxyCopyMembers( art, 'elements', struct.elements, weakLocation( location ) );
proxyCopyMembers( art, 'elements', struct.elements, weakLocation( location ),
null, isDeprecatedEnabled( options, 'noKeyPropagationWithExpansions' ) );
// Set elements expansion status (the if condition is always true, as no

@@ -549,3 +550,3 @@ // elements expansion will take place on artifact with existing other

if (!ielem.typeProps$)
setLink(ielem, 'typeProps$', Object.create(null));
setLink( ielem, 'typeProps$', Object.create( null ) );
// Note: At this point in time, effectiveType() was likely not called on the

@@ -559,5 +560,5 @@ // element, yet. Setting it here, we can't compare it to its value from _origin.

if (selem.elements)
setLink(ielem, 'elements$', selem.elements);
setLink( ielem, 'elements$', selem.elements );
if (selem.enum)
setLink(ielem, 'enum$', selem.enum);
setLink( ielem, 'enum$', selem.enum );
}

@@ -567,3 +568,3 @@ }

if (wasAnnotated)
setExpandStatusAnnotate(art, 'annotate');
setExpandStatusAnnotate( art, 'annotate' );

@@ -588,4 +589,4 @@ // TODO: We don't check enum$, yet! We first need to fix expansion for

if (o._effectiveType !== 0) { // cyclic
while (!o[prop] && getOrigin(o))
o = getOrigin(o);
while (!o[prop] && getOrigin( o ))
o = getOrigin( o );
}

@@ -607,3 +608,3 @@

return query;
setLink( query, '_combined', Object.create(null) );
setLink( query, '_combined', Object.create( null ) );
query.$inlines = [];

@@ -634,3 +635,3 @@ forEachGeneric( query, '$tableAliases', resolveTabRef );

dictAddArray( query._combined, name, elem, null ); // not dictAdd()
});
} );
}

@@ -680,3 +681,3 @@ }

if (!inlineHead) {
elemsParent.elements = Object.create(null);
elemsParent.elements = Object.create( null );
if (query._main._leadingQuery === query) // never the case for 'expand'

@@ -715,3 +716,3 @@ query._main.elements = elemsParent.elements;

error( 'duplicate-definition', [ location, query ], { name, '#': 'element' } );
});
} );
setMemberParent( col, id, query );

@@ -789,3 +790,3 @@ }

function wildcardSiblings( columns, query ) {
const siblings = Object.create(null);
const siblings = Object.create( null );
if (!columns)

@@ -815,3 +816,3 @@ return siblings;

const inferred = query._main.$inferred;
const excludingDict = (colParent || query).excludingDict || Object.create(null);
const excludingDict = (colParent || query).excludingDict || Object.create( null );

@@ -843,3 +844,3 @@ const envParent = wildcard._pathHead; // TODO: rename _pathHead to _columnParent

error( 'duplicate-definition', [ loc, query ], { name, '#': 'element' } );
});
} );
setMemberParent( sibling, name, query );

@@ -851,4 +852,4 @@ }

}
else if (Array.isArray(navElem)) {
const names = navElem.filter( e => !e.$duplicates)
else if (Array.isArray( navElem )) {
const names = navElem.filter( e => !e.$duplicates )
.map( e => `${ e.name.alias }.${ e.name.element }` );

@@ -868,3 +869,3 @@ if (names.length) {

error( 'duplicate-definition', [ loc, query ], { name, '#': 'element' } );
});
} );
elem.$inferred = '*';

@@ -916,3 +917,3 @@ elem.name.$inferred = '*';

const { id } = sibling.name;
if (Array.isArray(navElem)) {
if (Array.isArray( navElem )) {
// ID published! Used in stakeholder project; if renamed, add to oldMessageIds

@@ -1022,2 +1023,8 @@ info( 'wildcard-excluding-many', [ sibling.name.location, query ],

// console.log('ES:',elem.name.absolute,elem.name.element);
if (assoc._main === target && elem._main?.kind === 'entity' &&
elem._main?._ancestors?.includes( target )) {
// source and target of the model association are the same entity, and
// the current main artifact is a suitable auto-redirection target → return it
return elem._main;
}
const elemScope = scopedRedirections && // null if no scoped redirections

@@ -1032,3 +1039,3 @@ preferredElemScope( target, service, elem, assoc._main || assoc );

const desc = origTarget._descendants ||
setLink( origTarget, '_descendants', Object.create(null) );
setLink( origTarget, '_descendants', Object.create( null ) );
if (!desc[service.name.absolute]) // could be the target itself (no repeated msgs)!

@@ -1055,3 +1062,3 @@ desc[service.name.absolute] = [ target ];

two: 'Replace target $(TARGET) by $(SORTED_ARTS) or $(SECOND); can\'t auto-redirect this association if multiple projections exist in this service',
});
} );
// continuation semantics: no auto-redirection

@@ -1062,6 +1069,6 @@ }

// HINT: consider bin/cdsv2m.js when changing the following message text
// No grouped and sub messages yet (TODO v3): mention at all target places with all assocs
// No grouped and sub messages yet (TODO v5): mention at all target places with all assocs
const withAnno = annotationVal( exposed[0]['@cds.redirection.target'] );
for (const proj of exposed) {
// TODO: def-ambiguous-target (just v3, as the current is infamous and used in options),
// TODO: def-ambiguous-target (just v5, as the current is infamous and used in options),
message( 'redirected-implicitly-ambiguous',

@@ -1090,3 +1097,3 @@ [ weakLocation( proj.name.location ), proj ],

// Return projections of `target` in `service`. Shorted by
// Return projections of `target` in `service`. Sorted by
// - first, only consider projections with @cds.redirection.target=true

@@ -1108,3 +1115,3 @@ // - exclude all indirect projections, i.e. those which are projection on others in list

for (const e of exposed) {
if (min.every( m => m._ancestors?.includes( e ))) {
if (min.every( m => m._ancestors?.includes( e ) )) {
min = [ e ];

@@ -1256,3 +1263,3 @@ }

if (base === target)
return `${ service.name.absolute }.${ absolute.substring( absolute.lastIndexOf('.') + 1 ) }`;
return `${ service.name.absolute }.${ absolute.substring( absolute.lastIndexOf( '.' ) + 1 ) }`;
// for scoped (e.g. calculated) entities, use exposed name of base:

@@ -1329,7 +1336,7 @@ const exposed = minimalExposure( base, service, elemScope );

if (target.params) {
art.params = Object.create(null);
art.params = Object.create( null );
// is art.query.from.path[0].$syntax: ':' required?
art.query.from.path[0].args = Object.create(null);
forEachGeneric(target, 'params', (p, pn) => {
art.params[pn] = linkToOrigin(p, pn, art, 'params', p.location);
art.query.from.path[0].args = Object.create( null );
forEachGeneric( target, 'params', (p, pn) => {
art.params[pn] = linkToOrigin( p, pn, art, 'params', p.location );
art.query.from.path[0].args[pn] = {

@@ -1341,3 +1348,3 @@ name: { id: p.name.id, location: p.location },

};
});
} );
}

@@ -1344,0 +1351,0 @@ // TODO: do we need to tag the generated entity with elemScope = 'auto'?

@@ -23,3 +23,3 @@ // Propagate properties in XSN

} = require('./utils');
const $inferred = Symbol.for('cds.$inferred');
const $inferred = Symbol.for( 'cds.$inferred' );
// const { refString } = require( '../base/messages')

@@ -108,3 +108,3 @@

if (checkAndSetStatus( target.value._artifact ))
news.push(target.value._artifact);
news.push( target.value._artifact );

@@ -247,7 +247,7 @@ if (target.value?._artifact.$inferred !== 'include') {

const dict = source[prop];
target[prop] = Object.create(null); // also propagate empty elements
target[prop] = Object.create( null ); // also propagate empty elements
for (const name in dict) {
const member = linkToOrigin( dict[name], name, target, prop, location );
member.$inferred = 'proxy';
setEffectiveType(member, dict[name]);
setEffectiveType( member, dict[name] );
}

@@ -292,3 +292,3 @@ target[prop][$inferred] = 'prop';

if (target.kind)
always(prop, target, source); // not in 'items'
always( prop, target, source ); // not in 'items'
}

@@ -386,3 +386,3 @@

if (source._effectiveType !== undefined)
setLink( target, '_effectiveType', source._effectiveType);
setLink( target, '_effectiveType', source._effectiveType );
}

@@ -389,0 +389,0 @@

@@ -65,2 +65,3 @@ // Compiler phase "resolve": resolve all references

traverseQueryPost,
linkToOrigin,
} = require('./utils');

@@ -70,5 +71,5 @@

const $location = Symbol.for('cds.$location');
const $location = Symbol.for( 'cds.$location' );
const $inferred = Symbol.for('cds.$inferred');
const $inferred = Symbol.for( 'cds.$inferred' );

@@ -104,3 +105,3 @@ // TODO: make this part of specExpected in shared.js

const ignoreSpecifiedElements
= isDeprecatedEnabled(model.options, 'ignoreSpecifiedQueryElements');
= isDeprecatedEnabled( model.options, 'ignoreSpecifiedQueryElements' );

@@ -140,5 +141,5 @@ return doResolve();

target: 'Illegal circular reference to target $(ART)',
});
} );
}
});
} );
if (model.$assert) {

@@ -164,12 +165,8 @@ error( '$internal-expecting-cyclic', null, {},

// TODO: or should we push elems with `expand` sibling to extra list for
// better messages? (Whatever that means exactly.)
// better messages? (Whatever that means exactly.)
const nav = pathNavigation( elem.value );
const { path } = elem.value;
const item = path[path.length - 1];
if (nav.navigation && nav.item === item) {
// sourceElem, alias.sourceElem, mixin:
// redirectImplicitly( elem, origin );
pushLink( nav.navigation, '_projections', elem );
}
else if (elem._pathHead?.kind === '$inline' && path.length === 1) {
if (elem._pathHead?.kind === '$inline' && path.length === 1) {
const item = path[0];
const hpath = elem._pathHead.value?.path;

@@ -181,2 +178,36 @@ const head = hpath?.length === 1 && hpath[0]._navigation;

}
else if (nav.navigation) { // not set for $self.…
// Path could start with table alias; get start index
let index = path.indexOf(nav.item);
if (index === -1)
return;
let navItem = nav.navigation;
if (path[index].where || path[index].args)
return;
++index;
while (navItem && index < path.length) {
const step = path[index];
if (!step?.id || step.where || step.args)
break;
if (!navItem.elements?.[step.id]) {
const elements = navItem._origin?.elements ||
navItem._origin?.target?._artifact?.elements;
if (!elements)
break;
// Only link available path steps (navigation tree).
const origin = elements[step.id];
const member = linkToOrigin( origin, step.id, navItem, 'elements',
navItem.path?.location, true );
member.$inferred = 'expanded';
member.kind = '$navElement';
}
navItem = navItem.elements[step.id];
setLink( step, '_navigation', navItem );
++index;
}
// Last path step, if found, is a simple projection
if (index === path.length && navItem)
pushLink( navItem, '_projections', elem );
}
} );

@@ -370,3 +401,3 @@ }

if (struct && struct.kind !== 'type' && struct.elements &&
Object.values( struct.elements ).some( e => e.targetAspect)) {
Object.values( struct.elements ).some( e => e.targetAspect )) {
message( 'type-managed-composition', [ include.location, art ],

@@ -395,3 +426,3 @@ { '#': struct.kind, art: struct } );

obj = obj.items || obj; // the object which has type properties
effectiveType(obj);
effectiveType( obj );
}

@@ -423,9 +454,9 @@ if (obj.type) { // TODO: && !obj.type.$inferred ?

const isCsn = (obj._block && obj._block.$frontend === 'json');
error('type-missing-target', [ obj.type.location, obj ],
{ '#': isCsn ? 'csn' : 'std', type: elemtype.name.absolute }, {
// We don't say "use 'association to <target>" because the type could be used
// in action parameters, etc. as well.
std: 'The type $(TYPE) can\'t be used directly because it\'s compiler internal',
csn: 'Type $(TYPE) is missing a target',
});
error( 'type-missing-target', [ obj.type.location, obj ],
{ '#': isCsn ? 'csn' : 'std', type: elemtype.name.absolute }, {
// We don't say "use 'association to <target>" because the type could be used
// in action parameters, etc. as well.
std: 'The type $(TYPE) can\'t be used directly because it\'s compiler internal',
csn: 'Type $(TYPE) is missing a target',
} );
}

@@ -466,3 +497,3 @@ }

// Avoid strange ref-cyclic if managed composition is key (check comes later)
// TODO: the following is already done by addimplicitforeignkeys()!
// TODO: the following is already done by addImplicitForeignKeys()!
if (obj.$inferred !== 'aspect-composition') {

@@ -483,5 +514,5 @@ forEachGeneric( obj, 'foreignKeys', (elem) => {

if (art.$syntax === 'calc')
checkCalculatedElement(art);
checkCalculatedElement( art );
else if (art.type && !art.$inferred )
checkStructureCast(art);
checkStructureCast( art );
}

@@ -496,4 +527,4 @@

for (const id in art.elements$) {
resolveRefs(art.elements$[id]);
checkSpecifiedElement(art.elements[id], art.elements$[id]);
resolveRefs( art.elements$[id] );
checkSpecifiedElement( art.elements[id], art.elements$[id] );
}

@@ -523,3 +554,3 @@ }

const sType = specifiedElement.type?._artifact;
const iType = getInferredPropFromOrigin('type')?._artifact || inferredElement;
const iType = getInferredPropFromOrigin( 'type' )?._artifact || inferredElement;

@@ -529,5 +560,5 @@ // xor: could be missing a type;

if (!specifiedElement.type && inferredElement.type) {
error('query-mismatched-element', [ specifiedElement.location, user ], {
error( 'query-mismatched-element', [ specifiedElement.location, user ], {
'#': !specifiedElement.type ? 'missing' : 'extra', name: user.name.id, prop: 'type',
});
} );
return;

@@ -538,3 +569,4 @@ }

const typeName = iType?.name && sType?.name ? 'typeName' : 'type';
error('query-mismatched-element', [
const othertype = typeName !== 'type' && iType || '';
error( 'query-mismatched-element', [
specifiedElement.type.location || specifiedElement.location, user,

@@ -545,4 +577,4 @@ ], {

type: sType,
othertype: iType,
});
othertype,
} );
return;

@@ -553,4 +585,4 @@ }

// On the inferred side, they are likely expanded.
if (!hasXorPropMismatch('elements') && !hasXorPropMismatch('items') &&
!hasXorPropMismatch('target') && !hasXorPropMismatch('enum')) {
if (!hasXorPropMismatch( 'elements' ) && !hasXorPropMismatch( 'items' ) &&
!hasXorPropMismatch( 'target' ) && !hasXorPropMismatch( 'enum' )) {
// Element are already traversed via elements$ merging.

@@ -560,7 +592,7 @@

if (specifiedElement.items && !specifiedElement.items.$inferred)
checkSpecifiedElement(inferredElement.items, specifiedElement.items, specifiedElement);
checkSpecifiedElement( inferredElement.items, specifiedElement.items, specifiedElement );
if (specifiedElement.target &&
if (specifiedElement.target?._artifact && inferredElement.target?._artifact &&
specifiedElement.target._artifact !== inferredElement.target._artifact) {
error('query-mismatched-element', [
error( 'query-mismatched-element', [
specifiedElement.target.location || specifiedElement.location, user,

@@ -572,7 +604,7 @@ ], {

art: inferredElement.target,
});
} );
}
if (specifiedElement.foreignKeys) {
const sKeys = Object.keys(specifiedElement.foreignKeys);
const sKeys = Object.keys( specifiedElement.foreignKeys );
/** @type {any} */

@@ -584,5 +616,5 @@ let iKeys = inferredElement;

}
iKeys = Object.keys(iKeys.foreignKeys || {});
if (sKeys.length !== iKeys.length || sKeys.some( fkey => !iKeys.includes(fkey))) {
error('query-mismatched-element', [
iKeys = Object.keys( iKeys.foreignKeys || {} );
if (sKeys.length !== iKeys.length || sKeys.some( fkey => !iKeys.includes( fkey ) )) {
error( 'query-mismatched-element', [
specifiedElement.foreignKeys.location || specifiedElement.location, user,

@@ -594,3 +626,3 @@ ], {

art: inferredElement.target,
});
} );
}

@@ -600,9 +632,9 @@ }

if (specifiedElement.virtual) {
const iVirtual = getInferredPropFromOrigin('virtual')?.val || false;
const iVirtual = getInferredPropFromOrigin( 'virtual' )?.val || false;
if (!specifiedElement.virtual.val !== !iVirtual) {
error('query-mismatched-element', [
error( 'query-mismatched-element', [
specifiedElement.virtual.location || specifiedElement.location, user,
], {
'#': 'prop', prop: 'virtual', name: user.name.id,
});
} );
}

@@ -614,5 +646,5 @@ }

const sCardinality = specifiedElement.cardinality;
const iCardinality = getInferredPropFromOrigin('cardinality');
const iCardinality = getInferredPropFromOrigin( 'cardinality' );
if (!iCardinality) {
error('query-mismatched-element', [
error( 'query-mismatched-element', [
sCardinality.location || specifiedElement.location, user,

@@ -623,3 +655,3 @@ ], {

name: user.name.id,
});
} );
}

@@ -636,3 +668,3 @@ else {

continue;
error('query-mismatched-element', [
error( 'query-mismatched-element', [
sCardinality[prop]?.location || sCardinality.location || specifiedElement.location,

@@ -645,3 +677,3 @@ user,

name: user.name.id,
});
} );
}

@@ -652,7 +684,7 @@ }

if (specifiedElement.value) {
error('query-unexpected-property', [
error( 'query-unexpected-property', [
specifiedElement.value.location || specifiedElement.location, user,
], {
'#': 'calculatedElement', prop: 'value', name: user.name.id,
});
} );
}

@@ -662,10 +694,10 @@

// TODO: Do not use _origin chain for key; has been propagated in propagateKeyProps().
const iKey = getInferredPropFromOrigin('key')?.val;
const iKey = getInferredPropFromOrigin( 'key' )?.val;
// If "key" is specified or truthy in the inferred element, the values must match.
if (!iKey !== !specifiedElement.key?.val) {
error('query-mismatched-element', [
error( 'query-mismatched-element', [
specifiedElement.key?.location || specifiedElement.location, user,
], {
'#': specifiedElement.key ? 'prop' : 'missing', prop: 'key', name: user.name.id,
});
} );
}

@@ -684,5 +716,5 @@ }

if (!iEnumEntry) {
error('query-mismatched-element', [ specifiedElement.location, user ], {
error( 'query-mismatched-element', [ specifiedElement.location, user ], {
'#': 'enumExtra', name: user.name.id, id: name,
});
} );
break;

@@ -695,5 +727,5 @@ }

if (iVal !== sVal) {
error('query-mismatched-element', [ specifiedElement.location, user ], {
error( 'query-mismatched-element', [ specifiedElement.location, user ], {
'#': 'enumVal', name: user.name.id, id: name,
});
} );
break;

@@ -715,3 +747,3 @@ }

'#': specifiedElement[prop] ? 'extra' : 'missing', name: user.name.id, prop,
});
} );
return true;

@@ -726,4 +758,4 @@ }

if (element._effectiveType !== 0) {
while (getOrigin(element) && !element[prop])
element = getOrigin(element);
while (getOrigin( element ) && !element[prop])
element = getOrigin( element );
}

@@ -766,8 +798,8 @@ return element[prop];

if (!allowedInKind.includes(art._main.kind)) {
if (!allowedInKind.includes( art._main.kind )) {
if (art.$inferred === 'include') {
// even for include-chains, we find the correct ref due to element-expansion.
const include = art._main.includes.find(i => i._artifact === art._origin._main);
error('ref-invalid-calc-elem', [ include.location || art.value.location, art ],
{ '#': art._main.kind });
const include = art._main.includes.find( i => i._artifact === art._origin._main );
error( 'ref-invalid-calc-elem', [ include.location || art.value.location, art ],
{ '#': art._main.kind } );
}

@@ -778,6 +810,6 @@ else {

}
else if (!allowedInKind.includes(parent.kind)) {
else if (!allowedInKind.includes( parent.kind )) {
error( 'def-invalid-calc-elem', loc, { '#': parent.kind } );
}
else if (effectiveType(art)?.elements) {
else if (effectiveType( art )?.elements) {
// For inferred (e.g. included) calc elements, this error is already emitted at the origin.

@@ -791,3 +823,3 @@ if (!art.$inferred) {

}
else if (effectiveType(art)?.items) {
else if (effectiveType( art )?.items) {
// For inferred (e.g. included) calc elements, this error is already emitted at the origin.

@@ -808,3 +840,3 @@ if (!art.$inferred) {

// also appears with parse-cdl:
error('def-invalid-calc-elem', loc, { '#': prop });
error( 'def-invalid-calc-elem', loc, { '#': prop } );
return; // one error is enough

@@ -821,18 +853,15 @@ }

if (elem && art.type) { // has explicit type
if (art.type._artifact?.elements) {
error('type-cast-to-structured', [ art.type.location, art ], {},
'Can\'t cast to structured element');
}
else if (elem.elements) { // TODO: calc elements
error('type-cast-structured', [ art.type.location, art ], {},
'Structured elements can\'t be cast to a different type');
}
if (art.type._artifact?.elements)
error( 'type-invalid-cast', [ art.type.location, art ], { '#': 'to-structure' } );
else if (elem.elements) // TODO: calc elements
error( 'type-invalid-cast', [ art.type.location, art ], { '#': 'from-structure' } );
}
}
// Return type containing the assoc spec (keys, on); note that no
// propagation/rewrite has been done yet, cyclic dependency must have been
// checked before!
/**
* Return type containing the assoc spec (keys, on); note that no
* propagation/rewrite has been done yet, cyclic dependency must have been
* checked before!
*/
function getAssocSpec( type ) {
// only to be called without cycles
let unmanaged = null;

@@ -897,2 +926,7 @@ while (type) {

}
for (const limit of query.$limit || []) // LIMIT from UNION:
resolveLimit( limit );
if (query.limit)
resolveLimit( query.limit );
return;

@@ -905,17 +939,18 @@

if (join.on)
resolveExpr( join.on, 'join-on', query );
// TODO: check restrictions according to join "query"
resolveExpr( join.on, 'join-on', join );
}
}
// Note the strange name resolution (dynamic part) for ORDER BY: the same
// as for select items if it is an expression, but first look at select
// item alias (i.e. like `$projection.NAME` if it is a path. If it is an
// ORDER BY of an UNION, do not allow any dynamic path in an expression,
// and only allow the elements of the leading query if it is a path.
//
// This seems to be similar, but different in SQLite 3.22.0: ORDER BY seems
// to bind stronger than UNION (see <SQLite>/src/parse.y), and the name
// resolution seems to use select item aliases from all SELECTs of the
// UNION (see <SQLite>/test/tkt2822.test).
/**
* Note the strange name resolution (dynamic part) for ORDER BY: the same
* as for select items if it is an expression, but first look at select
* item alias (i.e. like `$projection.NAME` if it is a path. If it is an
* ORDER BY of an UNION, do not allow any dynamic path in an expression,
* and only allow the elements of the leading query if it is a path.
*
* This seems to be similar, but different in SQLite 3.22.0: ORDER BY seems
* to bind stronger than UNION (see <SQLite>/src/parse.y), and the name
* resolution seems to use select item aliases from all SELECTs of the
* UNION (see <SQLite>/test/tkt2822.test).
*/
function resolveBy( array, refMode, exprMode ) {

@@ -927,2 +962,9 @@ for (const value of array ) {

}
function resolveLimit( limit ) {
if (limit.rows)
resolveExpr( limit.rows, 'limit-rows', query );
if (limit.offset)
resolveExpr( limit.offset, 'limit-offset', query );
}
}

@@ -958,3 +1000,3 @@

comp: 'An unmanaged composition can\'t be defined as type',
});
} );
// TODO: also warning if inside structure

@@ -1017,3 +1059,3 @@ }

if (modelKeys.length !== serviceKeys.length) {
issue.id = modelKeys.find( id => !serviceKeys.includes( id ));
issue.id = modelKeys.find( id => !serviceKeys.includes( id ) );
issue['#'] = 'missing';

@@ -1071,3 +1113,3 @@ }

function addImplicitForeignKeys( art, obj, target ) {
obj.foreignKeys = Object.create(null);
obj.foreignKeys = Object.create( null );
forEachInOrder( target, 'elements', ( elem, name ) => {

@@ -1088,4 +1130,4 @@ if (elem.key && elem.key.val) {

setArtifactLink( key.targetElement.path[0], elem );
setLink( key, '_effectiveType', effectiveType(elem) );
dependsOn(key, elem, location);
setLink( key, '_effectiveType', effectiveType( elem ) );
dependsOn( key, elem, location );
// TODO TMP: instead, make managed composition of aspects and unmanaged

@@ -1096,3 +1138,3 @@ // assocs not depend on their `on` condition (empty `_deps` after resolve)

}
});
} );
obj.foreignKeys[$inferred] = 'keys';

@@ -1126,3 +1168,3 @@ }

function addForeignKeyNavigations( art, silent = false ) {
art.$keysNavigation = Object.create(null);
art.$keysNavigation = Object.create( null );
const keys = [];

@@ -1138,3 +1180,3 @@ // Basically sort foreign keys according to length of target element ref.

}
});
} );
for (const key of keys.flat()) {

@@ -1152,3 +1194,3 @@ let dict = art.$keysNavigation;

else
nav.$keysNavigation = Object.create(null);
nav.$keysNavigation = Object.create( null );
}

@@ -1238,3 +1280,3 @@ else if (item === last || nav._artifact) {

targetOp: 'The redirected target $(ART) is a complex view with $(KEYWORD)',
});
} );
break;

@@ -1255,3 +1297,3 @@ }

let news = [ { chain, sources: [ target ] } ];
const dict = Object.create(null);
const dict = Object.create( null );
while (news.length) {

@@ -1381,3 +1423,3 @@ const outer = news;

const type = alias || effectiveType( step._artifact );
const art = (type && type.target) ? type.target._artifact : type;
const art = type?.target ? type.target._artifact : type;
if (!art)

@@ -1415,4 +1457,4 @@ return;

if (!entity || !entity.params) {
let first = dict[Object.keys(dict)[0]];
if (Array.isArray(first))
let first = dict[Object.keys( dict )[0]];
if (Array.isArray( first ))
first = first[0];

@@ -1432,3 +1474,3 @@ error( 'expr-unexpected-argument',

const exp = (expected === 'from') ? 'from-args' : expected;
if (Array.isArray(dict)) {
if (Array.isArray( dict )) {
const loc = [ dict[0] && dict[0].location || stepLocation, user ];

@@ -1445,3 +1487,3 @@ error( 'expr-expected-named-argument', loc, {},

const arg = dict[name];
for (const a of Array.isArray(arg) ? arg : [ arg ]) {
for (const a of Array.isArray( arg ) ? arg : [ arg ]) {
setArtifactLink( a.name, param );

@@ -1458,16 +1500,18 @@ if (!param) {

// Return condensed info about reference in select item
// - tableAlias.elem -> { navigation: navElem, item: path[1], tableAlias }
// - sourceElem (in query) -> { navigation: navElem, item: path[0], tableAlias }
// - mixinElem -> { navigation: mixinElement, item: path[0] }
// - $projection.elem -> also $self.item -> { item: path[1], tableAlias: $self }
// - $self -> { item: undefined, tableAlias: $self }
// - $parameters.P, :P -> {}
// - $now, current_date -> {}
// - undef, redef -> {}
// With 'navigation': store that navigation._artifact is projected
// With 'navigation': rewrite its ON condition
// With navigation: Do KEY propagation
//
// TODO: re-think this function, copied in populate.js and tweak-assocs.js
/**
* Return condensed info about reference in select item
* - tableAlias.elem -> { navigation: navElem, item: path[1], tableAlias }
* - sourceElem (in query) -> { navigation: navElem, item: path[0], tableAlias }
* - mixinElem -> { navigation: mixinElement, item: path[0] }
* - $projection.elem -> also $self.item -> { item: path[1], tableAlias: $self }
* - $self -> { item: undefined, tableAlias: $self }
* - $parameters.P, :P -> {}
* - $now, current_date -> {}
* - undef, redef -> {}
* With 'navigation': store that navigation._artifact is projected
* With 'navigation': rewrite its ON condition
* With navigation: Do KEY propagation
*
* TODO: re-think this function, copied in populate.js and tweak-assocs.js
*/
function pathNavigation( ref ) {

@@ -1474,0 +1518,0 @@ // currently, indirectly projectable elements are not included - we might

@@ -23,4 +23,4 @@ // Compiler functions and utilities shared across all phases

const $inferred = Symbol.for('cds.$inferred');
const $location = Symbol.for('cds.$location');
const $inferred = Symbol.for( 'cds.$inferred' );
const $location = Symbol.for( 'cds.$location' );

@@ -65,2 +65,3 @@ /**

notFound: undefinedDefinition,
accept: acceptRealArtifact,
},

@@ -158,5 +159,13 @@ _extensions: {

},
'limit-rows': {
lexical: null,
dollar: true,
dynamic: () => Object.create( null ),
notFound: undefinedVariable,
param: paramSemantics,
},
'limit-offset': 'limit-rows',
// general element references -----------------------------------------------
where: {
lexical: tableAliasesIfNotExtendAndSelf,
lexical: tableAliasesAndSelf,
dollar: true,

@@ -204,3 +213,4 @@ dynamic: combinedSourcesOrParentElements,

dollar: true,
dynamic: combinedSourcesOrParentElements, // TODO: source alone...
dynamic: combinedSourcesOrParentElements,
rejectRoot: rejectOwnExceptVisibleAliases,
notFound: undefinedSourceElement,

@@ -256,2 +266,3 @@ param: paramSemantics,

dynamic: queryElements,
rejectRoot: rejectOwnAliasesAndMixins,
notFound: undefinedParentElement,

@@ -265,2 +276,3 @@ check: checkOrderByRef,

dynamic: () => Object.create( null ),
rejectRoot: rejectAllOwn,
notFound: undefinedVariable,

@@ -299,3 +311,3 @@ check: checkRefInQuery,

if (expr.args) {
const args = Array.isArray(expr.args) ? expr.args : Object.values( expr.args );
const args = Array.isArray( expr.args ) ? expr.args : Object.values( expr.args );
// TODO: re-think $expected

@@ -336,6 +348,8 @@ if (!callback.traverse?.( args, exprCtx, user, callback ))

// Return artifact or element referred by the path in `ref`. The first
// environment we search in is `env`. If no such artifact or element exist,
// complain with message and return `undefined`. Record a dependency from
// `user` to the found artifact if `user` is provided.
/**
* Return artifact or element referred by the path in `ref`. The first
* environment we search in is `env`. If no such artifact or element exist,
* complain with message and return `undefined`. Record a dependency from
* `user` to the found artifact if `user` is provided.
*/
function resolvePath( ref, expected, user ) {

@@ -359,3 +373,3 @@ const origUser = user;

const r = getPathRoot( ref, semantics, origUser );
const root = r && acceptPathRoot( r, ref, semantics, user );
const root = r && acceptPathRoot( r, ref, semantics, origUser );
if (!root)

@@ -385,3 +399,3 @@ return setArtifactLink( ref, root );

else if (art._main && art.kind !== 'select' || path[0]._navigation?.kind !== '$self') {
// no real dependency to bare $self (or actually: the underlaying query)
// no real dependency to bare $self (or actually: the underlying query)
dependsOn( user, art, location );

@@ -428,3 +442,3 @@ // Without on-demand resolve, we can simply signal 'undefined "x"'

}
args = args.slice(parameters.length);
args = args.slice( parameters.length );
}

@@ -494,3 +508,3 @@ else if (args.length > 0 && !typeArtifact?.builtin) {

for (let env = lexical; env; env = env[nextProp]) {
const dict = env[dictProp] || Object.create(null);
const dict = env[dictProp] || Object.create( null );
const r = dict[head.id];

@@ -557,3 +571,3 @@ if (acceptLexical( r, path, semantics, user ))

const found = env && env[item.id]; // not env?.[item.id] ! …we want to keep the 0
// Reject `$self.$_column_1`:
// Reject `$self.$_column_1`: TODO: necessary to do here again?
art = setArtifactLink( item, (found?.name?.$inferred === '$internal') ? undefined : found );

@@ -595,10 +609,9 @@

// TODO: test table alias and mixin named `$projection`
if (path.length === 1 && !user.expand && !user.inline) { // accept lonely…
// allow mixins, and `up_` in anonymous target aspect:
if (art.kind !== '$self' || path[0].id !== '$self')
return art.kind === 'mixin' || art.kind === '$navElement';
return true;
}
// return !art.$internal && art;
return art.name?.$inferred !== '$internal'; // not a compiler-generated internal alias
if (path.length !== 1 || user.expand || user.inline)
return art.name?.$inferred !== '$internal'; // not a compiler-generated internal alias
// allow mixins, $self, and `up_` in anonymous target aspect (is $navElement):
return art.kind === 'mixin' ||
art.kind === '$self' && path[0].id === '$self' ||
art.kind === '$navElement';
}

@@ -611,2 +624,5 @@

return getAmbiguousRefLink( art, head, user );
if (semantics.rejectRoot?.( art, user, ref, semantics ))
return null;
switch (art.kind) {

@@ -625,4 +641,4 @@ case 'using': {

case '$navElement': {
if (head.id === user.$extended)
path.$prefix = user.$extended;
if (head.id === (user._user || user).$extended)
path.$prefix = head.id;
setLink( head, '_navigation', art );

@@ -641,3 +657,3 @@ return setArtifactLink( head, art._origin );

if (path.length === 1 && art.kind === '$tableAlias')
user.$noOrigin = true;
(user._user || user).$noOrigin = true;
return art;

@@ -664,3 +680,3 @@ }

// duplicate table aliases, only mention non-ambiguous source elems
const uniqueNames = arr.filter( e => !e.$duplicates);
const uniqueNames = arr.filter( e => !e.$duplicates );
if (uniqueNames.length) {

@@ -672,3 +688,3 @@ const names = uniqueNames.filter( e => e._parent.$inferred !== '$internal' )

variant = 'none';
error( 'ref-ambiguous', [ head.location, user ], { '#': variant, id: head.id, names });
error( 'ref-ambiguous', [ head.location, user ], { '#': variant, id: head.id, names } );
}

@@ -715,3 +731,3 @@ return false;

const aliases = query.$tableAliases;
const r = Object.create(null);
const r = Object.create( null );
if (aliases.$self.kind === '$self')

@@ -796,3 +812,3 @@ r.$self = aliases.$self;

function artifactsEnv( art ) {
return art._subArtifacts || Object.create(null);
return art._subArtifacts || Object.create( null );
}

@@ -811,3 +827,3 @@

}
return env?.elements || Object.create(null);
return env?.elements || Object.create( null );
}

@@ -829,3 +845,3 @@

return 0;
return env?.elements || Object.create(null);
return env?.elements || Object.create( null );
}

@@ -837,3 +853,3 @@

return 0;
return env?.elements || Object.create(null);
return env?.elements || Object.create( null );
}

@@ -852,3 +868,3 @@

return 0;
return env?.elements || Object.create(null);
return env?.elements || Object.create( null );
}

@@ -928,3 +944,3 @@

// otherwise, use s/th like ref-unexpected-element
signalNotFound( (isVar ? 'ref-undefined-var' : 'ref-expecting-const'),
signalNotFound( ( isVar ? 'ref-undefined-var' : 'ref-expecting-const'),
[ head.location, user ],

@@ -978,3 +994,3 @@ valid, { '#': 'std', id } );

const src = id.charAt( 0 ) !== '$' && user._combined?.[id];
if (src && !Array.isArray(src)) {
if (src && !Array.isArray( src )) {
path.$prefix = src.name.alias; // pushing it to path directly could be problematic

@@ -1013,3 +1029,3 @@ // configurable error:

: pathName( path.slice( path.indexOf( item ) ) );
signalNotFound( (art.$uncheckedElements ? 'ref-unknown-var' : 'ref-undefined-var'),
signalNotFound( ( art.$uncheckedElements ? 'ref-unknown-var' : 'ref-undefined-var'),
[ item.location, user ], valid,

@@ -1034,2 +1050,52 @@ { id: `${ art.name.element }.${ id }` } );

function rejectOwnAliasesAndMixins( art, user, ref, semantics ) { // orderBy-set-ref
switch (art.kind) {
case '$tableAlias':
case 'mixin':
if (art._parent !== user)
return false;
break;
case '$self':
if (!semantics) // orderBy-set-expr
break;
// FALLTHROUGH
default:
return false;
}
error( 'ref-invalid-element', [ ref.path[0].location, user._user ],
{ '#': art.kind, id: art.name.id } );
return true;
}
function rejectAllOwn( art, user, ref ) { // orderBy-set-expr
return rejectOwnAliasesAndMixins( art, user, ref, null );
}
function rejectOwnExceptVisibleAliases( art, user, ref ) { // for join-on
switch (art.kind) {
case '$navElement':
art = art._parent;
// FALLTHROUGH
case '$tableAlias':
case 'mixin':
if (art._parent !== user._user || user.$tableAliases[art.name.id])
return false;
break;
case '$self':
// in the SQL backend, the $self.elem references are replaced by the
// corresponding column expression; this might have references to elements
// of invisible table aliases; at least one stakeholder uses this,
// so it can't be an error (yet).
warning( 'ref-deprecated-self-element', [ ref.path[0].location, user._user ], {},
// eslint-disable-next-line max-len
'Referring to the query\'s own elements here might lead to invalid SQL references; use source elements only' );
return false;
default:
return false;
}
error( 'ref-invalid-element', [ ref.path[0].location, user._user ],
{ '#': art.kind, id: art.name.id } );
return true;
}
function acceptElemOrVarOrSelf( art, user, ref ) {

@@ -1052,3 +1118,3 @@ // TODO: make $self._artifact point to the $self alias, not the entity

// Path with only one item, but we expect an element, e.g. `$at.from`.
signalMissingElementAccess(art, [ path[0].location, user ]);
signalMissingElementAccess( art, [ path[0].location, user ] );
return null;

@@ -1078,2 +1144,11 @@ }

function acceptRealArtifact( art, user, ref ) {
// For compatibility, we accept `extend Unknown` without elements/actions/includes
if (art.kind !== 'namespace' || !(user.elements || user.actions || user.includes))
return art;
const { location } = ref.path[ref.path.length - 1];
signalNotFound( 'ref-undefined-def', [ location, user ], null, { art } );
return false;
}
function acceptStructOrBare( art, user, ref ) { // for includes[]

@@ -1103,5 +1178,7 @@ // It had been checked before that `includes` is already forbidden for

// Including aspects with elements is forbidden for aspects without the
// `elements` property (TODO: ensure that, see #11346). Testing for the length of
// `art.elements` requires that we have applied potential `includes` of
// `art` before!
// `elements` property. Testing for the length of `art.elements` requires
// that we have applied potential `includes` of `art` before!
// We might allow includes with elements in the future, they'd probably
// count as specified elements with lower priority, i.e. annos, types, key
// etc on columns beat those inherited from the include.
if (art.kind === 'aspect' &&

@@ -1517,6 +1594,6 @@ (!art.elements || base.query && !Object.keys( art.elements ).length))

// Mapping for better valid names: from -> $at.from
const valid = Object.keys(art.elements || {}).reduce((prev, curr) => {
const valid = Object.keys( art.elements || {} ).reduce( (prev, curr) => {
prev[`${ art.name.id }.${ curr }`] = true;
return prev;
}, Object.create(null));
}, Object.create( null ) );
attachAndEmitValidNames( err, valid );

@@ -1544,3 +1621,3 @@ }

if (!art.internal && !art.deprecated && art.name?.$inferred !== '$internal' &&
(viaCdl ? art._main || !name.includes('.') : art.kind !== 'namespace'))
(viaCdl ? art._main || !name.includes( '.' ) : art.kind !== 'namespace'))
msg.validNames[name] = art;

@@ -1552,3 +1629,3 @@ }

const loc = msg.$location[0] || msg.$location;
const names = Object.keys(msg.validNames);
const names = Object.keys( msg.validNames );
names.sort();

@@ -1561,3 +1638,3 @@ if (names.length > 22) {

{ '#': !names.length ? 'zero' : 'std' },
{ std: `Valid: ${ names.join(', ') }`, zero: 'No valid names' });
{ std: `Valid: ${ names.join( ', ' ) }`, zero: 'No valid names' } );
}

@@ -1564,0 +1641,0 @@ }

@@ -25,4 +25,4 @@ // Tweak associations: rewrite keys and on conditions

const $location = Symbol.for('cds.$location');
const $inferred = Symbol.for('cds.$inferred');
const $location = Symbol.for( 'cds.$location' );
const $inferred = Symbol.for( 'cds.$inferred' );

@@ -122,3 +122,3 @@ // Export function of this file.

function rewriteAssociationCheck( element ) {
const elem = element.items || element; // TODO v2: nested items
const elem = element.items || element; // TODO v5: nested items
if (elem.elements)

@@ -206,3 +206,3 @@ forEachGeneric( elem, 'elements', rewriteAssociationCheck );

function assocWithExplicitSpec( assoc ) {
while (assoc.foreignKeys && inferredForeignKeys( assoc.foreignKeys, 'keys') ||
while (assoc.foreignKeys && inferredForeignKeys( assoc.foreignKeys, 'keys' ) ||
assoc.on && assoc.on.$inferred)

@@ -270,3 +270,3 @@ assoc = getOrigin( assoc );

if (orig._effectiveType !== undefined)
setLink( fk, '_effectiveType', orig._effectiveType);
setLink( fk, '_effectiveType', orig._effectiveType );
const te = copyExpr( orig.targetElement, elem.location );

@@ -280,3 +280,3 @@ if (elem._redirected) {

fk.targetElement = te;
});
} );
if (elem.foreignKeys) // Possibly no fk was set

@@ -296,3 +296,3 @@ elem.foreignKeys[$inferred] = 'rewrite';

setExpandStatus( elem, 'target' );
if (elem._parent && elem._parent.kind === 'element') {
if (elem._parent?.kind === 'element') {
// managed association as sub element not supported yet

@@ -304,3 +304,3 @@ error( null, [ elem.location, elem ], {},

}
const nav = (elem._main && elem._main.query && elem.value)
const nav = (elem._main?.query && elem.value)
? pathNavigation( elem.value ) // redirected source elem or mixin

@@ -357,4 +357,4 @@ : { navigation: assoc }; // redirected user-provided

const root = expr.path[0]._navigation || expr.path[0]._artifact;
// console.log( info(null, [assoc.name.location, assoc],
// { names: expr.path.map(i=>i.id), art: root }, 'TA').toString())
// console.log( info(null, [ assoc.name.location, assoc ],
// { names: expr.path.map(i => i.id), art: root }, 'TA').toString());
if (!root || root._main !== source)

@@ -364,5 +364,3 @@ return; // not $self or source element

return; // are not allowed anyway - there was an error before
const item = expr.path[root.kind === '$self' ? 1 : 0];
// console.log('YE', assoc.name, item, root.name, expr.path)
const elem = navProjection( item && tableAlias.elements[item.id], assoc );
const { item, elem } = firstProjectionForPath( expr.path, tableAlias, assoc );
rewritePath( expr, item, assoc, elem, assoc.value.location );

@@ -398,3 +396,3 @@ }

const elem = (assoc._main.items || assoc._main).elements[item.id];
if (!(Array.isArray(elem) || // no msg for redefs
if (!(Array.isArray( elem ) || // no msg for redefs
elem === item._artifact || // redirection for explicit def

@@ -412,3 +410,3 @@ elem._origin === item._artifact)) {

}
rewritePath( expr, item, assoc, (Array.isArray(elem) ? false : elem), null );
rewritePath( expr, item, assoc, (Array.isArray( elem ) ? false : elem), null );
}

@@ -422,9 +420,8 @@ }

if (location) {
error( 'rewrite-not-projected', [ location, assoc ],
{ name: assoc.name.id, art: item._artifact }, {
// eslint-disable-next-line max-len
std: 'Projected association $(NAME) uses non-projected element $(ART)',
// eslint-disable-next-line max-len
element: 'Projected association $(NAME) uses non-projected element $(MEMBER) of $(ART)',
} );
error( 'rewrite-not-projected', [ location, assoc ], {
name: assoc.name.id, art: item._artifact, elemref: { ref: path },
}, {
std: 'Projected association $(NAME) uses non-projected element $(ELEMREF)',
element: 'Projected association $(NAME) uses non-projected element $(MEMBER) of $(ART)',
} );
}

@@ -437,5 +434,11 @@ delete root._navigation;

if (item !== root) {
// e.g. mixin ON-condition: Base.foo -> $self.foo or multi-path projection,
// $projection -> $self
root.id = '$self';
setLink( root, '_navigation', assoc._parent.$tableAliases.$self );
setArtifactLink( root, assoc._parent );
if (item) {
const i = path.indexOf(item);
ref.path = [ root, ...path.slice( i, path.length ) ];
}
}

@@ -486,3 +489,3 @@ else if (elem.name.id.charAt(0) === '$') {

const proj = navProjection( alias.elements[name], assoc );
name = proj && proj.name && proj.name.id;
name = proj?.name?.id;
if (!name) {

@@ -493,4 +496,3 @@ if (!forKeys)

const culprit = elem.target && !elem.target.$inferred && elem.target ||
(elem.value && elem.value.path &&
elem.value.path[elem.value.path.length - 1]) ||
elem.value?.path?.[elem.value.path.length - 1] ||
elem;

@@ -513,3 +515,3 @@ // TODO: probably better to collect the non-projected foreign keys

if (elem && !Array.isArray(elem))
if (elem && !Array.isArray( elem ))
return elem;

@@ -535,16 +537,67 @@ // TODO: better (extra message), TODO: do it

// Return condensed info about reference in select item
// - tableAlias.elem -> { navigation: navElem, item: path[1], tableAlias }
// - sourceElem (in query) -> { navigation: navElem, item: path[0], tableAlias }
// - mixinElem -> { navigation: mixinElement, item: path[0] }
// - $projection.elem -> also $self.item -> { item: path[1], tableAlias: $self }
// - $self -> { item: undefined, tableAlias: $self }
// - $parameters.P, :P -> {}
// - $now, current_date -> {}
// - undef, redef -> {}
// With 'navigation': store that navigation._artifact is projected
// With 'navigation': rewrite its ON condition
// With navigation: Do KEY propagation
//
// TODO: re-think this function, copied in populate.js and tweak-assocs.js
/**
* For a path `a.b.c.d`, return a projection for the first path item that is projected.
* For example, if a query has multiple projections such as `a.b, a, a.b.c`, the
* _first_ possible projection will be used and the caller can rewrite `a.b.c.d` to `b.c.d`.
* This avoids that `extend`s affect the ON-condition.
*
* The returned object `ret` has `ret.item`, which is the path item that is projected.
* `ret.elem` is the element projection.
*
* @param {any[]} path
* @param {object} tableAlias
* @param {object} assoc Preferred association that should be used if projected.
* @return {{elem: object, item: object}|null}
*/
function firstProjectionForPath( path, tableAlias, assoc ) {
const viaSelf = (path[0]._navigation || path[0]._artifact).kind === '$self';
const root = viaSelf ? 1 : 0;
if (root >= path.length) // e.g. just `$self` path item
return { item: undefined, elem: {} };
// We want to use the _first_ valid projection that is written by the user (if the preferred
// `assoc` is not directly projected). To achieve that, look into the table alias' elements.
const selectedElements = Object.values(tableAlias._parent.elements);
const proj = [];
let navItem = tableAlias;
for (const item of path.slice(root)) {
navItem = item?.id && navItem.elements?.[item.id];
if (!navItem) {
break;
}
else if (navItem._projections) {
const elem = navProjection( navItem, assoc );
if (elem && elem === assoc) {
// in case the specified association is found, _always_ use it.
return { item, elem };
}
else if (elem) {
const index = selectedElements.indexOf(elem);
proj.push({ item, elem, index });
}
}
}
return (proj.length === 0)
? { item: path[root], elem: null }
: proj.reduce( (acc, curr) => (acc.index > curr.index ? curr : acc), proj[0] ); // first
}
/**
* Return condensed info about reference in select item
* - tableAlias.elem -> { navigation: navElem, item: path[1], tableAlias }
* - sourceElem (in query) -> { navigation: navElem, item: path[0], tableAlias }
* - mixinElem -> { navigation: mixinElement, item: path[0] }
* - $projection.elem -> also $self.item -> { item: path[1], tableAlias: $self }
* - $self -> { item: undefined, tableAlias: $self }
* - $parameters.P, :P -> {}
* - $now, current_date -> {}
* - undef, redef -> {}
* With 'navigation': store that navigation._artifact is projected
* With 'navigation': rewrite its ON condition
* With navigation: Do KEY propagation
*
* TODO: re-think this function, copied in populate.js and tweak-assocs.js
*/
function pathNavigation( ref ) {

@@ -551,0 +604,0 @@ // currently, indirectly projectable elements are not included - we might

@@ -17,3 +17,3 @@ // Simple compiler utility functions

const $inferred = Symbol.for('cds.$inferred');
const $inferred = Symbol.for( 'cds.$inferred' );

@@ -113,3 +113,3 @@ // for links, i.e., properties starting with an underscore '_':

function proxyCopyMembers( art, dictProp, originDict, location, kind ) {
function proxyCopyMembers( art, dictProp, originDict, location, kind, tmpDeprecated ) {
art[dictProp] = Object.create( null );

@@ -123,4 +123,7 @@ // TODO: set $inferred ? for dict?

member.$inferred = 'expanded';
// TODO throughout the compiler: do not set art.‹prop›.$inferred if art.$inferred
if (kind)
member.kind = kind;
else if (origin.key && !tmpDeprecated) // TODO(v5/v6): remove tmpDeprecated
member.key = Object.assign( { $inferred: 'expanded' }, origin.key );
if (kind && origin.masked) // TODO: remove!

@@ -145,3 +148,3 @@ member.masked = Object.assign( { $inferred: 'nav' }, origin.masked );

if (p[prop] === undefined)
p[prop] = Object.create(null);
p[prop] = Object.create( null );
dictAdd( p[prop], name, elem );

@@ -168,3 +171,3 @@ }

delete elem.name[kind];
});
} );
// try { throw new CompilerAssertion('Foo') } catch (e) { elem.name.stack = e; };

@@ -215,3 +218,3 @@ }

if (!parent[kind][prop])
parent[kind][prop] = Object.create(null);
parent[kind][prop] = Object.create( null );
pushToDict( parent[kind][prop], name, elem );

@@ -242,3 +245,3 @@ }

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

@@ -255,3 +258,3 @@

function splitIntoPath( location, name ) {
return name.split('.').map( id => ({ id, location }) );
return name.split( '.' ).map( id => ({ id, location }) );
}

@@ -270,3 +273,3 @@

return expr;
else if (Array.isArray(expr))
else if (Array.isArray( expr ))
return expr.map( e => copyExpr( e, location, skipUnderscored, rewritePath ) );

@@ -308,3 +311,3 @@

}
else if (Array.isArray(expr)) {
else if (Array.isArray( expr )) {
return expr.some( e => testExpr( e, pathTest, queryTest, user ) );

@@ -320,6 +323,6 @@ }

// unnamed args => array
if (Array.isArray(expr.args))
if (Array.isArray( expr.args ))
return expr.args.some( e => testExpr( e, pathTest, queryTest, user ) );
// named args => dictionary
for (const namedArg of Object.keys(expr.args)) {
for (const namedArg of Object.keys( expr.args )) {
if (testExpr( expr.args[namedArg], pathTest, queryTest, user ))

@@ -417,3 +420,3 @@ return true;

callback( obj, name, prop );
if (Array.isArray(obj.$duplicates)) // redefinitions
if (Array.isArray( obj.$duplicates )) // redefinitions
obj.$duplicates.forEach( o => callback( o, name, prop ) );

@@ -420,0 +423,0 @@ }

{
"root": true,
//"plugins": ["sonarjs", "jsdoc"],
//"extends": ["plugin:jsdoc/recommended", "../../../.eslintrc-ydkjsi.json", "plugin:sonarjs/recommended"],
"plugins": ["sonarjs"],
"extends": ["../../.eslintrc-ydkjsi.json", "plugin:sonarjs/recommended"],
"rules": {
"no-shadow": "off"
"prefer-const": "error",
"quotes": ["error", "single", "avoid-escape"],
"prefer-template": "error",
"no-trailing-spaces": "error",
"template-curly-spacing":["error", "never"],
"complexity": ["warn", 50],
"max-len": "off",
// there seem to be false positives
"jsdoc/require-returns-check": "off",
// Don't enforce stupid descriptions
"jsdoc/require-param-description": "off",
"jsdoc/require-returns-description": "off",
// Sometimes if-else's are more specific
"sonarjs/prefer-single-boolean-return": "off",
// Very whiny and nitpicky
"sonarjs/cognitive-complexity": "off",
"sonarjs/no-duplicate-string": "off",
// Does not recognize TS types
"jsdoc/no-undefined-types": "off",
"jsdoc/tag-lines": "off",
"no-nested-ternary": "off",
"sonarjs/no-nested-template-literals": "off"
},
"parserOptions": {
"ecmaVersion": 2022,
"sourceType": "script"
},
"env": {
"es2022": true,
"node": true
},
"settings": {
"jsdoc": {
"mode": "typescript"
}
}
}

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

/**************************************************************************************************
/** ************************************************************************************************
* preprocessAnnotations

@@ -18,5 +18,5 @@ *

*/
function preprocessAnnotations(csn, serviceName, options) {
function preprocessAnnotations( csn, serviceName, options ) {
const { message } = makeMessageFunction(csn, options);
let fkSeparator = '_';
const fkSeparator = '_';

@@ -37,6 +37,6 @@ resolveShortcuts();

// return value can be null is target has no key
function getKeyOfTargetOfManagedAssoc(anno, assoc) {
function getKeyOfTargetOfManagedAssoc( anno, assoc ) {
// assoc.target can be the name of the target or the object itself
const targetName = (typeof assoc.target === 'object') ? assoc.target.name : assoc.target;
const target = (typeof assoc.target === 'object') ? assoc.target : csn.definitions[assoc.target];
const target = (typeof assoc.target === 'object') ? assoc.target : csn.definitions[assoc.target];

@@ -47,6 +47,7 @@ const keyNames = Object.keys(target.elements).filter(x => target.elements[x].key && !target.elements[x].target);

message('odata-anno-preproc', null, { anno, name: targetName, '#': 'nokey' },
'target $(NAME) has no key');
'target $(NAME) has no key');
}
else if (keyNames.length > 1)
else if (keyNames.length > 1) {
message('odata-anno-preproc', null, { anno, name: targetName, '#': 'multkeys' });
}

@@ -65,14 +66,20 @@ return keyNames[0];

const location = [ 'definitions', artifactName ];
if(artifactName === serviceName || artifactName.startsWith(serviceName + '.')) {
if (artifactName === serviceName || artifactName.startsWith(`${serviceName}.`)) {
handleAnnotations(artifactName, artifactName, artifact, location);
artifact.elements && Object.entries(artifact.elements).forEach(([elementName, element]) => {
handleAnnotations(artifactName, elementName, element, [ ...location, 'elements', elementName ]);
});
artifact.params && Object.entries(artifact.params).forEach(([paramName, param]) => {
handleAnnotations(artifactName, paramName, param, [ ...location, 'actions', artifactName, 'params', paramName ]);
});
if (artifact.elements) {
Object.entries(artifact.elements).forEach(([ elementName, element ]) => {
handleAnnotations(artifactName, elementName, element, [ ...location, 'elements', elementName ]);
});
}
if (artifact.params) {
Object.entries(artifact.params).forEach(([ paramName, param ]) => {
handleAnnotations(artifactName, paramName, param, [ ...location, 'actions', artifactName, 'params', paramName ]);
});
}
forEachGeneric(artifact, 'actions', (action, actionName) => {
action.params && Object.entries(action.params).forEach(([paramName, param]) => {
handleAnnotations(actionName, paramName, param, [ ...location, 'actions', actionName, 'params', paramName ]);
});
if (action.params) {
Object.entries(action.params).forEach(([ paramName, param ]) => {
handleAnnotations(actionName, paramName, param, [ ...location, 'actions', actionName, 'params', paramName ]);
});
}
});

@@ -82,8 +89,7 @@ }

function handleAnnotations(defName, carrierName, carrier, location) {
function handleAnnotations( defName, carrierName, carrier, location ) {
// collect the names of the carrier's annotation properties
const annoNames = Object.keys(carrier).filter( x => x[0] === '@')
const annoNames = Object.keys(carrier).filter( x => x[0] === '@');
annoNames.forEach(aName => {
annoNames.forEach((aName) => {
const aNameWithoutQualifier = aName.split('#')[0];

@@ -106,3 +112,3 @@

// inner functions
function draftAnnotations(aName, aNameWithoutQualifier) {
function draftAnnotations( aName, aNameWithoutQualifier ) {
if ((carrier.kind === 'entity') &&

@@ -113,9 +119,10 @@ (aNameWithoutQualifier === '@Common.DraftRoot.PreparationAction' ||

aNameWithoutQualifier === '@Common.DraftNode.PreparationAction')
) {
) {
let value = carrier[aName];
// prefix with service name, if not already done
if (value === 'draftPrepare' || value === 'draftActivate' || value === 'draftEdit') {
if (value === 'draftPrepare' || value === 'draftActivate' || value === 'draftEdit') {
// mocha test has no whatsMySchemaName
const schemaName = options.whatsMySchemaName && options.whatsMySchemaName(carrierName) || serviceName;
value = carrier[aName] = schemaName + '.' + value;
carrier[aName] = `${schemaName}.${value}`;
value = carrier[aName];
}

@@ -125,4 +132,4 @@ // for v2: function imports live inside EntityContainer -> path needs to contain "EntityContainer/"

if (isV2()) {
let entityNameShort = carrierName.split('.').pop();
carrier[aName] = value.replace(/(draft(Prepare|Activate|Edit))$/, (match, p1) => 'EntityContainer/' + entityNameShort + '_' + p1)
const entityNameShort = carrierName.split('.').pop();
carrier[aName] = value.replace(/(draft(Prepare|Activate|Edit))$/, (match, p1) => `EntityContainer/${entityNameShort}_${p1}`);
}

@@ -132,6 +139,5 @@ }

function fixedValueListShortCut(anno) {
function fixedValueListShortCut( anno ) {
if (anno === '@Common.ValueList.entity' ||
anno === '@Common.ValueList.viaAssociation') {
const _fixedValueListShortCut = () => {

@@ -143,8 +149,8 @@ // note: we loop over all annotations that were originally present, even if they are

// if CollectionPath is explicitly given, no shortcut expansion is made
if (carrier['@Common.ValueList.CollectionPath']) {
if (carrier['@Common.ValueList.CollectionPath'])
return false;
}
if (carrier.kind === 'entity') {
message('odata-anno-preproc', [...location, anno], { anno, '#': 'notforentity' });
message('odata-anno-preproc', [ ...location, anno ], { anno, '#': 'notforentity' });
return false;

@@ -163,3 +169,3 @@ }

if (!assocName) {
message('odata-anno-preproc', [...location, anno], { anno, '#': 'viaassoc' });
message('odata-anno-preproc', [ ...location, anno ], { anno, '#': 'viaassoc' });
return false;

@@ -169,3 +175,3 @@ }

if (!assoc || !assoc.target) {
message('odata-anno-preproc', [...location, anno], { anno, id: assocName, '#': 'noassoc' });
message('odata-anno-preproc', [ ...location, anno ], { anno, id: assocName, '#': 'noassoc' });
return false;

@@ -179,8 +185,11 @@ }

// if both annotations are present, ignore 'entity' and raise a message
if (annoNames.map(x=>x.split('#')[0]).find(x=>(x==='@Common.ValueList.viaAssociation'))) {
message('odata-anno-preproc', [...location, anno],
{
name: '@Common.ValueList.entity', anno: '@Common.ValueList',
value: 'entity', code: 'viaAssociation', '#': 'vallistignored'
});
if (annoNames.map(x => x.split('#')[0]).find(x => (x === '@Common.ValueList.viaAssociation'))) {
message('odata-anno-preproc', [ ...location, anno ],
{
name: '@Common.ValueList.entity',
anno: '@Common.ValueList',
value: 'entity',
code: 'viaAssociation',
'#': 'vallistignored',
});
return false;

@@ -190,10 +199,9 @@ }

const annoVal = carrier['@Common.ValueList.entity']; // name of value list entity
if (annoVal['=']) {
message('odata-anno-preproc', [...location, anno], { anno, '#': 'notastring' },
);
}
if (annoVal['='])
message('odata-anno-preproc', [ ...location, anno ], { anno, '#': 'notastring' });
// mocha test has no whatsMySchemaName
const schemaName = options.whatsMySchemaName && options.whatsMySchemaName(defName) || serviceName
const schemaName = options.whatsMySchemaName && options.whatsMySchemaName(defName) || serviceName;
enameShort = annoVal['='] || annoVal;
enameFull = schemaName + '.' + enameShort;
enameFull = `${schemaName}.${enameShort}`;
}

@@ -203,3 +211,3 @@

if (!vlEntity) {
message('odata-anno-preproc', [...location, anno ], { anno, id: enameFull, '#': 'notexist' });
message('odata-anno-preproc', [ ...location, anno ], { anno, id: enameFull, '#': 'notexist' });
return false;

@@ -224,7 +232,6 @@ }

// rename the localDataProp to be 'assocName/key'
if(carrier['@cds.api.ignore']) {
if (carrier['@cds.api.ignore']) {
const assocName = carrier['@odata.foreignKey4'];
if(assocName && options.isV4()) {
localDataProp = localDataProp.replace(assocName+fkSeparator, assocName+'/');
}
if (assocName && options.isV4())
localDataProp = localDataProp.replace(assocName + fkSeparator, `${assocName}/`);
}

@@ -237,7 +244,8 @@

if (keys.length === 0) {
message('odata-anno-preproc', [...location, anno], { anno, name: enameFull, '#': 'vhlnokey' });
message('odata-anno-preproc', [ ...location, anno ], { anno, name: enameFull, '#': 'vhlnokey' });
return false;
}
else if (keys.length > 1)
message('odata-anno-preproc', [...location, anno], { anno, name: enameFull, '#': 'vhlmultkeys' });
else if (keys.length > 1) {
message('odata-anno-preproc', [ ...location, anno ], { anno, name: enameFull, '#': 'vhlmultkeys' });
}
valueListProp = keys[0];

@@ -255,7 +263,10 @@

textField = Identification[0]['='];
} else if (Identification && Identification[0] && Identification[0]['Value'] && Identification[0]['Value']['=']) {
textField = Identification[0]['Value']['='];
} else {
}
else if (Identification && Identification[0] && Identification[0].Value && Identification[0].Value['=']) {
textField = Identification[0].Value['='];
}
else {
const stringFields = Object.keys(vlEntity.elements).filter(
x => !vlEntity.elements[x].key && vlEntity.elements[x].type === 'cds.String')
x => !vlEntity.elements[x].key && vlEntity.elements[x].type === 'cds.String'
);
if (stringFields.length === 1)

@@ -268,11 +279,11 @@ textField = stringFields[0];

if (!parameters) {
parameters = [{
'$Type': 'Common.ValueListParameterInOut',
'LocalDataProperty' : { '=' : localDataProp },
'ValueListProperty' : valueListProp
}];
parameters = [ {
$Type: 'Common.ValueListParameterInOut',
LocalDataProperty: { '=': localDataProp },
ValueListProperty: valueListProp,
} ];
if (textField) {
parameters[1] = {
'$Type': 'Common.ValueListParameterDisplayOnly',
'ValueListProperty' : textField
$Type: 'Common.ValueListParameterDisplayOnly',
ValueListProperty: textField,
};

@@ -283,3 +294,3 @@ }

const newObj = Object.create( Object.getPrototypeOf(carrier) );
Object.keys(carrier).forEach( e => {
Object.keys(carrier).forEach( (e) => {
if (e === '@Common.ValueList.entity' || e === '@Common.ValueList.viaAssociation') {

@@ -302,3 +313,3 @@ newObj['@Common.ValueList.Label'] = label;

return true;
}
};

@@ -314,12 +325,12 @@ const success = _fixedValueListShortCut();

function textArrangementReordering(aName, aNameWithoutQualifier) {
function textArrangementReordering( aName, aNameWithoutQualifier ) {
if (aNameWithoutQualifier === '@Common.TextArrangement') {
let value = carrier[aName];
let textAnno = carrier['@Common.Text'];
const value = carrier[aName];
const textAnno = carrier['@Common.Text'];
// can only occur if there is a @Common.Text annotation at the same target
if (!textAnno) {
message('odata-anno-preproc', [...location, '@Common.TextArrangement'], { anno: '@Common.TextArrangement', name: '@Common.Text', '#': 'txtarr' });
}
if (!textAnno)
message('odata-anno-preproc', [ ...location, '@Common.TextArrangement' ], { anno: '@Common.TextArrangement', name: '@Common.Text', '#': 'txtarr' });
//change the scalar anno into a "pseudo-structured" one
// change the scalar anno into a "pseudo-structured" one
// TODO should be flattened, but then alphabetical order is destroyed

@@ -329,5 +340,5 @@

// nested annotation precedence and remove outer annotation (always)
if(!carrier['@Common.Text.@UI.TextArrangement'] && textAnno) {
carrier['@Common.Text'] = { '$value': textAnno, '@UI.TextArrangement': value };
}
if (!carrier['@Common.Text.@UI.TextArrangement'] && textAnno)
carrier['@Common.Text'] = { $value: textAnno, '@UI.TextArrangement': value };
delete carrier[aName];

@@ -334,0 +345,0 @@ }

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

const edmUtils = require('./edmUtils.js')
const edmUtils = require('./edmUtils.js');
const { initializeModel } = require('./edmPreprocessor.js');

@@ -17,3 +17,5 @@ const translate = require('./annotations/genericTranslation.js');

const { makeMessageFunction } = require('../base/messages');
const { EdmTypeFacetMap, EdmTypeFacetNames, EdmPrimitiveTypeMap, getEdm } = require('./edm.js');
const {
EdmTypeFacetMap, EdmTypeFacetNames, EdmPrimitiveTypeMap, getEdm,
} = require('./edm.js');

@@ -25,7 +27,7 @@ /*

function csn2edm(_csn, serviceName, _options) {
return csn2edmAll(_csn, _options, [ serviceName ])[ serviceName ];
function csn2edm( _csn, serviceName, _options ) {
return csn2edmAll(_csn, _options, [ serviceName ])[serviceName];
}
function csn2edmAll(_csn, _options, serviceNames=undefined) {
function csn2edmAll( _csn, _options, serviceNames = undefined ) {
// get us a fresh model copy that we can work with

@@ -37,3 +39,5 @@ const csn = cloneCsnNonDict(_csn, _options);

const messageFunctions = makeMessageFunction(csn, _options, 'to.edmx');
const { info, warning, error, message, throwWithError } = messageFunctions;
const {
info, warning, error, message, throwWithError,
} = messageFunctions;
checkCSNVersion(csn, _options);

@@ -47,3 +51,4 @@

if (_csn.meta && _csn.meta.transformation === 'odata' && _csn.meta.options) {
if (!csn.meta) setProp(csn, 'meta', Object.create(null));
if (!csn.meta)
setProp(csn, 'meta', Object.create(null));
setProp(csn.meta, 'options', _csn.meta.options);

@@ -58,3 +63,4 @@ }

fallBackSchemaName,
options ] = initializeModel(csn, _options, messageFunctions, serviceNames);
options,
] = initializeModel(csn, _options, messageFunctions, serviceNames);

@@ -65,4 +71,4 @@ const mergedVocabularies = translate.mergeOdataVocabularies(options, message);

const v = options.v;
if(Object.keys(allServices).length === 0) {
const { v } = options;
if (Object.keys(allServices).length === 0) {
info(null, null, 'No Services in model');

@@ -72,13 +78,12 @@ return rc;

if(serviceNames === undefined)
if (serviceNames === undefined)
serviceNames = options.serviceNames;
if(serviceNames) {
serviceNames.forEach(name => {
let serviceCsn = allServices[name];
if(serviceCsn == undefined) {
if (serviceNames) {
serviceNames.forEach((name) => {
const serviceCsn = allServices[name];
if (!serviceCsn)
warning(null, null, { name }, 'No service definition with name $(NAME) found in the model');
}
else {
else
rc[name] = createEdm(serviceCsn);
}
});

@@ -89,3 +94,4 @@ }

services[serviceCsn.name] = createEdm(serviceCsn);
return services; }, rc);
return services;
}, rc);
}

@@ -99,7 +105,9 @@

//--------------------------------------------------------------------------------
function createEdm(serviceCsn) {
function createEdm( serviceCsn ) {
// eslint-disable-next-line no-unused-vars
function baseName( str, del ) {
const l = str.lastIndexOf(del);
return (l >= 0) ? str.slice(l + del.length, str.length) : str;
}
function baseName(str, del) { let l = str.lastIndexOf(del); // eslint-disable-line no-unused-vars
return (l >= 0) ? str.slice(l+del.length, str.length) : str; }
// if we have a real alias take it, otherwise use basename of service

@@ -109,3 +117,5 @@ // let alias = serviceCsn.alias || baseName(baseName(serviceCsn.name, '::'), '.');

function markRendered(def) { setProp(def, '$isRendered', true); }
function markRendered( def ) {
setProp(def, '$isRendered', true);
}

@@ -151,6 +161,4 @@ const service = new Edm.DataServices(v);

let LeadSchema;
const fqSchemaXRef = [serviceCsn.name];
const whatsMySchemaName = function(n) {
return fqSchemaXRef.reduce((rc, sn) => !rc && n && n.startsWith(sn + '.') ? sn : rc, undefined);
}
const fqSchemaXRef = [ serviceCsn.name ];
const whatsMySchemaName = n => fqSchemaXRef.reduce((acc, sn) => (!acc && n && n.startsWith(`${sn}.`) ? sn : acc), undefined);

@@ -160,3 +168,3 @@ // tunnel schema xref and servicename in options to edm.Typebase to rectify

options.serviceName = serviceCsn.name;
// List of all schema names in this service, including the service itself
// List of all schema names in this service, including the service itself
options.whatsMySchemaName = whatsMySchemaName;

@@ -167,8 +175,8 @@ options.whatsMyServiceRootName = whatsMyServiceRootName;

const UsedTypes = {};
function collectUsedType(csn, typeName = (csn.items?.type || csn.type)) {
if(typeName) {
if(UsedTypes[typeName])
UsedTypes[typeName].push(csn)
function collectUsedType( def, typeName = (def.items?.type || def.type) ) {
if (typeName) {
if (UsedTypes[typeName])
UsedTypes[typeName].push(def);
else
UsedTypes[typeName] = [ csn ];
UsedTypes[typeName] = [ def ];
}

@@ -184,17 +192,17 @@ }

container: true,
definitions: Object.create(null)
}
definitions: Object.create(null),
},
};
if(options.isV4()) {
if (options.isV4()) {
// Add additional schema containers as sub contexts to the service
Object.entries(allSchemas).forEach(([fqName, art]) => {
if(serviceCsn.name === whatsMyServiceRootName(fqName) &&
fqName.startsWith(serviceCsn.name + '.')) {
if(art.kind === 'reference')
Object.entries(allSchemas).forEach(([ fqName, art ]) => {
if (serviceCsn.name === whatsMyServiceRootName(fqName) &&
fqName.startsWith(`${serviceCsn.name}.`)) {
if (art.kind === 'reference')
fqSchemaXRef.push(fqName);
if(art.kind === 'schema') {
if (art.kind === 'schema') {
fqSchemaXRef.push(fqName);
// Strip the toplevel service schema name (see comment above)
const name = fqName.replace(serviceCsn.name + '.', '');
const name = fqName.replace(`${serviceCsn.name}.`, '');
subSchemaDictionary[name] = {

@@ -205,3 +213,3 @@ name,

container: false,
definitions: Object.create(null)
definitions: Object.create(null),
};

@@ -213,3 +221,3 @@ }

// Sort schema names in reverse order to allow longest match
fqSchemaXRef.sort((a,b) => b.length-a.length);
fqSchemaXRef.sort((a, b) => b.length - a.length);

@@ -222,3 +230,3 @@ // Fill the schemas and references, fqSchemaXRef must be complete

const sortedSchemaNames = Object.keys(subSchemaDictionary).filter(n => n !== fallBackSchemaName && n !== serviceCsn.name).sort();
if(subSchemaDictionary[fallBackSchemaName])
if (subSchemaDictionary[fallBackSchemaName])
sortedSchemaNames.push(fallBackSchemaName);

@@ -230,3 +238,3 @@

sortedSchemaNames.forEach(name => {
sortedSchemaNames.forEach((name) => {
const schema = subSchemaDictionary[name];

@@ -245,26 +253,28 @@ service.registerSchema(schema.fqName, createSchema(schema));

*/
service._children.forEach(c => {
c._ec && Object.entries(c._ec._registry).forEach((([setName, arr]) => {
if(arr.length > 1) {
error(null, null, {
name: c._edmAttributes.Namespace,
id: setName,
names: arr.map(a => a.getDuplicateMessage())
}, 'Namespace $(NAME): Duplicate entries in EntityContainer with Name=$(ID) for $(NAMES)');
}
}));
service._children.forEach((c) => {
if (c._ec) {
Object.entries(c._ec._registry).forEach((([ setName, arr ]) => {
if (arr.length > 1) {
error(null, null, {
name: c._edmAttributes.Namespace,
id: setName,
names: arr.map(a => a.getDuplicateMessage()),
}, 'Namespace $(NAME): Duplicate entries in EntityContainer with Name=$(ID) for $(NAMES)');
}
}));
}
});
// Create annotations and distribute into Schemas, merge vocabulary cross refs into xServiceRefs
addAnnotations(xServiceRefs);
addAnnotations2XServiceRefs();
// Finally add cross service references into the EDM and extract the targetSchemaNames
// for the type cross check
Object.values(xServiceRefs).forEach(ref => {
let r = new Edm.Reference(v, ref.ref);
r.append(new Edm.Include(v, ref.inc))
Object.values(xServiceRefs).forEach((ref) => {
const r = new Edm.Reference(v, ref.ref);
r.append(new Edm.Include(v, ref.inc));
edm._defaultRefs.push(r);
});
for(let typeName in UsedTypes) {
if(!isBuiltinType(typeName)) {
for (const typeName in UsedTypes) {
if (!isBuiltinType(typeName)) {
let iTypeName = typeName;

@@ -278,18 +288,17 @@ /*

*/
if(!typeName.startsWith(serviceCsn.name + '.'))
iTypeName = serviceCsn.name + '.' + typeName;
if (!typeName.startsWith(`${serviceCsn.name}.`))
iTypeName = `${serviceCsn.name}.${typeName}`;
const def = reqDefs.definitions[iTypeName];
const usages = UsedTypes[typeName].filter(u => !u.$NameClashReported);
if(usages.length > 0 && def && !def.$isRendered && def['@cds.external']) {
if (usages.length > 0 && def && !def.$isRendered && def['@cds.external']) {
message('odata-spec-violation-type', usages[0].$location,
{
type: typeName,
anno: '@cds.external',
name: serviceCsn.name,
code: def.elements ? 'Edm.ComplexType' : 'Edm.TypeDefinition',
version: options.isV4() ? '4.0' : '2.0',
'#': 'external'
}
);
{
type: typeName,
anno: '@cds.external',
name: serviceCsn.name,
code: def.elements ? 'Edm.ComplexType' : 'Edm.TypeDefinition',
version: options.isV4() ? '4.0' : '2.0',
'#': 'external',
});
}

@@ -299,7 +308,7 @@ }

return edm
return edm;
// Sort definitions into their schema container
function populateSchemas(schemas) {
Object.entries(reqDefs.definitions).forEach(([fqName, art]) => {
function populateSchemas( schemas ) {
Object.entries(reqDefs.definitions).forEach(([ fqName, art ]) => {
// Identify service members by their definition name only, this allows

@@ -317,12 +326,12 @@ // to let the internal object.name have the sub-schema name.

// not marked to be ignored as schema member
if(mySchemaName &&
if (mySchemaName &&
serviceCsn.name === whatsMyServiceRootName(fqName, false) &&
art.kind !== 'context' && art.kind !== 'service') {
// Strip the toplevel serviceName from object.name
// except if the schema name is the service name itself.
// Proxy names are not prefixed, as they need to be reused.
if(mySchemaName !== serviceCsn.name) {
fqName = art.name = fqName.replace(serviceCsn.name + '.', '');
mySchemaName = mySchemaName.replace(serviceCsn.name + '.', '');
if (mySchemaName !== serviceCsn.name) {
art.name = fqName.replace(`${serviceCsn.name}.`, '');
fqName = art.name;
mySchemaName = mySchemaName.replace(`${serviceCsn.name}.`, '');
}

@@ -358,9 +367,9 @@ schemas[mySchemaName].definitions[fqName] = art;

return Object.entries(allSchemas).reduce((references, [fqName, art]) => {
return Object.entries(allSchemas).reduce((references, [ fqName, art ]) => {
// add references
if(art.kind === 'reference' &&
if (art.kind === 'reference' &&
whatsMySchemaName(fqName) &&
serviceCsn.name === whatsMyServiceRootName(fqName, false)) {
serviceCsn.name === whatsMyServiceRootName(fqName, false))
references[art.inc.Namespace] = art;
}
return references;

@@ -371,14 +380,15 @@ }, {});

// Main schema creator function
function createSchema(schema) {
function createSchema( schema ) {
/** @type {object} */
// Same check for alias (if supported by us)
const reservedNames = ['Edm', 'odata', 'System', 'Transient'];
const loc = ['definitions', schema.name];
if(reservedNames.includes(schema.name))
const reservedNames = [ 'Edm', 'odata', 'System', 'Transient' ];
const loc = [ 'definitions', schema.name ];
if (reservedNames.includes(schema.name))
message('odata-spec-violation-namespace', loc, { names: reservedNames });
if (schema.name.length > 511)
if (schema.name.length > 511) {
message('odata-spec-violation-namespace', loc, { '#': 'length' });
}
else {
schema.name.split('.').forEach(id => {
schema.name.split('.').forEach((id) => {
if (!edmUtils.isODataSimpleIdentifier(id))

@@ -393,3 +403,3 @@ message('odata-spec-violation-id', loc, { id });

// now namespace and alias are used to create the fullQualified(name)
const schemaNamePrefix = schema.name + '.'
const schemaNamePrefix = `${schema.name}.`;
const schemaAliasPrefix = schemaNamePrefix;

@@ -405,28 +415,26 @@ const schemaCsn = schema;

edmUtils.foreach(schemaCsn.definitions,
a => a.kind === 'entity' && !a.abstract && a.name.startsWith(schemaNamePrefix),
[createEntityTypeAndSet, markRendered]
);
a => a.kind === 'entity' && !a.abstract && a.name.startsWith(schemaNamePrefix),
[ createEntityTypeAndSet, markRendered ]);
// create unbound actions/functions
edmUtils.foreach(schemaCsn.definitions,
a => (a.kind === 'action' || a.kind === 'function') && a.name.startsWith(schemaNamePrefix),
[(options.isV4()) ? createActionV4 : createActionV2, markRendered]);
a => (a.kind === 'action' || a.kind === 'function') && a.name.startsWith(schemaNamePrefix),
[ (options.isV4()) ? createActionV4 : createActionV2, markRendered ]);
// create the complex types
// create the complex types
edmUtils.foreach(schemaCsn.definitions,
a => edmUtils.isStructuredType(a) && a.name.startsWith(schemaNamePrefix),
[createComplexType, markRendered]);
a => edmUtils.isStructuredType(a) && a.name.startsWith(schemaNamePrefix),
[ createComplexType, markRendered ]);
if(options.isV4())
{
if (options.isV4()) {
edmUtils.foreach(schemaCsn.definitions,
artifact => edmUtils.isDerivedType(artifact) &&
!artifact.target&&
artifact => edmUtils.isDerivedType(artifact) &&
!artifact.target &&
artifact.name.startsWith(schemaNamePrefix),
[createTypeDefinitionV4, markRendered]);
[ createTypeDefinitionV4, markRendered ]);
}
if(isBetaEnabled(options, 'odataTerms')) {
if (isBetaEnabled(options, 'odataTerms')) {
edmUtils.foreach(schemaCsn.definitions,
a => a.kind === 'annotation' && a.name.startsWith(schemaNamePrefix),
createTerm);
a => a.kind === 'annotation' && a.name.startsWith(schemaNamePrefix),
createTerm);
}

@@ -437,14 +445,15 @@

const name = cur._edmAttributes.Name;
if(acc[name] === undefined) {
if (acc[name] === undefined)
acc[name] = [ cur ];
} else {
else
acc[name].push(cur);
}
return acc;
}, Object.create(null) );
navigationProperties.forEach(np => {
if(options.isV4()) {
navigationProperties.forEach((np) => {
if (options.isV4()) {
// V4: No referential constraints for Containment Relationships
if((!np.isContainment() || (options.renderForeignKeys)) && !np.isToMany())
if ((!np.isContainment() || (options.renderForeignKeys)) && !np.isToMany())
np.addReferentialConstraintNodes();

@@ -471,10 +480,10 @@ }

*/
if(Schema._ec && Schema._ec._children.length === 0) {
if (Schema._ec && Schema._ec._children.length === 0)
Schema._children.splice(Schema._children.indexOf(Schema._ec), 1);
}
Object.entries(NamesInSchemaXRef).forEach(([name, refs]) => {
if(refs.length > 1) {
error(null, ['definitions', `${Schema._edmAttributes.Namespace}.${name}`], { name: Schema._edmAttributes.Namespace },
'Duplicate name in Schema $(NAME)');
Object.entries(NamesInSchemaXRef).forEach(([ name, refs ]) => {
if (refs.length > 1) {
error(null, [ 'definitions', `${Schema._edmAttributes.Namespace}.${name}` ], { name: Schema._edmAttributes.Namespace },
'Duplicate name in Schema $(NAME)');
}

@@ -485,4 +494,3 @@ });

function createEntityTypeAndSet(entityCsn)
{
function createEntityTypeAndSet( entityCsn ) {
const EntityTypeName = entityCsn.name.replace(schemaNamePrefix, '');

@@ -493,27 +501,30 @@ const EntitySetName = edmUtils.getBaseName(entityCsn.$entitySetName || entityCsn.name);

const loc = ['definitions', entityCsn.name];
const location = [ 'definitions', entityCsn.name ];
const type = `${schema.name}.${EntityTypeName}`;
if(properties.length === 0)
warning(null, loc, { type }, 'EDM EntityType $(TYPE) has no properties');
else if(entityCsn.$edmKeyPaths.length === 0 && !isSingleton)
message('odata-spec-violation-no-key', loc);
if (properties.length === 0)
warning(null, location, { type }, 'EDM EntityType $(TYPE) has no properties');
else if (entityCsn.$edmKeyPaths.length === 0 && !isSingleton)
message('odata-spec-violation-no-key', location);
if(!edmUtils.isODataSimpleIdentifier(EntityTypeName))
message('odata-spec-violation-id', loc, { id: EntityTypeName });
if (!edmUtils.isODataSimpleIdentifier(EntityTypeName))
message('odata-spec-violation-id', location, { id: EntityTypeName });
properties.forEach(p => {
const pLoc = [...loc, 'elements', p._edmAttributes.Name];
properties.forEach((p) => {
const pLoc = [ ...location, 'elements', p._edmAttributes.Name ];
edmTypeCompatibilityCheck(p, pLoc);
if(p._edmAttributes.Name === EntityTypeName)
if (p._edmAttributes.Name === EntityTypeName)
message('odata-spec-violation-property-name', pLoc, { meta: entityCsn.kind });
if(options.isV2() && p._isCollection && !p._csn.target)
if (options.isV2() && p._isCollection && !p._csn.target)
message('odata-spec-violation-array', pLoc, { version: '2.0' });
if(!edmUtils.isODataSimpleIdentifier(p._edmAttributes.Name))
if (!edmUtils.isODataSimpleIdentifier(p._edmAttributes.Name)) {
message('odata-spec-violation-id', pLoc, { id: p._edmAttributes.Name });
}
else if (options.isV2() && /^(_|\d)/.test(p._edmAttributes.Name)) {
// FIXME: Rewrite signalIllegalIdentifier function to be more flexible
message('odata-spec-violation-id', pLoc,
{ prop: p._edmAttributes.Name[0], id: p._edmAttributes.Name, version: '2.0', '#': 'v2firstchar' });
{
prop: p._edmAttributes.Name[0], id: p._edmAttributes.Name, version: '2.0', '#': 'v2firstchar',
});
}

@@ -523,6 +534,6 @@ });

// construct EntityType attributes
const attributes = { Name : EntityTypeName };
const attributes = { Name: EntityTypeName };
// CDXCORE-CDXCORE-173
if(options.isV2() && hasStream) {
if (options.isV2() && hasStream) {
attributes['m:HasStream'] = true;

@@ -534,11 +545,10 @@ edmUtils.assignAnnotation(entityCsn, '@Core.MediaType', hasStream);

if (EntityContainer && entityCsn.$hasEntitySet)
{
if (EntityContainer && entityCsn.$hasEntitySet) {
/** @type {object} */
let containerEntry;
if(edmUtils.isSingleton(entityCsn) && options.isV4()) {
if (edmUtils.isSingleton(entityCsn) && options.isV4()) {
containerEntry = new Edm.Singleton(v, { Name: EntitySetName, Type: fullQualified(EntityTypeName) }, entityCsn);
if(entityCsn['@odata.singleton.nullable'])
containerEntry._edmAttributes.Nullable= true;
if (entityCsn['@odata.singleton.nullable'])
containerEntry._edmAttributes.Nullable = true;
}

@@ -550,5 +560,5 @@ else {

// V4: Create NavigationPropertyBinding in EntitySet
if(options.isV4()) {
entityCsn.$edmNPBs.forEach(npb => {
containerEntry.append(new Edm.NavigationPropertyBinding(v, npb))
if (options.isV4()) {
entityCsn.$edmNPBs.forEach((npb) => {
containerEntry.append(new Edm.NavigationPropertyBinding(v, npb));
});

@@ -560,10 +570,13 @@ }

// put actions behind entity types in Schema/EntityContainer
entityCsn.actions && Object.entries(entityCsn.actions).forEach(([ n, a ]) => {
(options.isV4()) ? createActionV4(a, n, entityCsn)
: createActionV2(a, n, entityCsn)
});
if (entityCsn.actions) {
Object.entries(entityCsn.actions).forEach(([ n, a ]) => {
if (options.isV4())
createActionV4(a, n, entityCsn);
else
createActionV2(a, n, entityCsn);
});
}
}
function createComplexType(structuredTypeCsn)
{
function createComplexType( structuredTypeCsn ) {
// V4 attributes: Name, BaseType, Abstract, OpenType

@@ -575,21 +588,21 @@ const attributes = { Name: structuredTypeCsn.name.replace(schemaNamePrefix, '') };

const properties = createProperties(elementsCsn, structuredTypeCsn)[0];
const loc = ['definitions', structuredTypeCsn.name];
const location = [ 'definitions', structuredTypeCsn.name ];
if(!edmUtils.isODataSimpleIdentifier(attributes.Name))
message('odata-spec-violation-id', loc, { id: attributes.Name });
if (!edmUtils.isODataSimpleIdentifier(attributes.Name))
message('odata-spec-violation-id', location, { id: attributes.Name });
properties.forEach(p => {
const pLoc = [ ...loc, ...(structuredTypeCsn.items ? ['items', 'elements'] : [ 'elements' ]), p._edmAttributes.Name ];
properties.forEach((p) => {
const pLoc = [ ...location, ...(structuredTypeCsn.items ? [ 'items', 'elements' ] : [ 'elements' ]), p._edmAttributes.Name ];
edmTypeCompatibilityCheck(p, pLoc);
if(p._edmAttributes.Name === complexType._edmAttributes.Name)
if (p._edmAttributes.Name === complexType._edmAttributes.Name)
message('odata-spec-violation-property-name', pLoc, { meta: structuredTypeCsn.kind });
if(!edmUtils.isODataSimpleIdentifier(p._edmAttributes.Name))
if (!edmUtils.isODataSimpleIdentifier(p._edmAttributes.Name))
message('odata-spec-violation-id', pLoc, { id: p._edmAttributes.Name });
if(options.isV2()) {
if(p._isCollection && !p._csn.target)
if (options.isV2()) {
if (p._isCollection && !p._csn.target)
message('odata-spec-violation-array', pLoc, { version: '2.0' });
if(p._csn.target)
if (p._csn.target)
message('odata-spec-violation-assoc', pLoc, { version: '2.0' });

@@ -612,4 +625,3 @@ }

*/
function createProperties(elementsCsn, edmParentCsn=elementsCsn)
{
function createProperties( elementsCsn, edmParentCsn = elementsCsn ) {
const props = [];

@@ -619,9 +631,9 @@ let hasStream = false;

elementsCsn.elements && Object.entries(elementsCsn.elements).forEach(([elementName, elementCsn]) =>
{
if(elementCsn._edmParentCsn == undefined)
setProp(elementCsn, '_edmParentCsn', edmParentCsn);
if (elementsCsn.elements) {
Object.entries(elementsCsn.elements).forEach(([ elementName, elementCsn ]) => {
if (!elementCsn._edmParentCsn)
setProp(elementCsn, '_edmParentCsn', edmParentCsn);
if(elementCsn.target) {
// Foreign keys are part of the generic elementCsn.elements property creation
if (elementCsn.target) {
// Foreign keys are part of the generic elementCsn.elements property creation

@@ -633,14 +645,13 @@ // This is the V4 edmx:NavigationProperty

// (undefined !== false) still evaluates to true
if (!elementCsn._target.abstract && elementCsn['@odata.navigable'] !== false)
{
const navProp = new Edm.NavigationProperty(v, {
Name: elementName,
Type: elementCsn._target.name
}, elementCsn);
collectUsedType(elementCsn, elementCsn._target.name);
props.push(navProp);
if (!elementCsn._target.abstract && elementCsn['@odata.navigable'] !== false) {
const navProp = new Edm.NavigationProperty(v, {
Name: elementName,
Type: elementCsn._target.name,
}, elementCsn);
collectUsedType(elementCsn, elementCsn._target.name);
props.push(navProp);
// save the navProp in the global array for late constraint building
navigationProperties.push(navProp);
navigationProperties.push(navProp);
}
}
}
// render ordinary property if element is NOT ...

@@ -650,30 +661,29 @@ // 1) ... annotated @cds.api.ignore

else if(isEdmPropertyRendered(elementCsn, options))
{
// CDXCORE-CDXCORE-173
// V2: filter @Core.MediaType
if ( options.isV2() && elementCsn['@Core.MediaType']) {
hasStream = elementCsn['@Core.MediaType'];
delete elementCsn['@Core.MediaType'];
else if (isEdmPropertyRendered(elementCsn, options)) {
// CDXCORE-CDXCORE-173
// V2: filter @Core.MediaType
if ( options.isV2() && elementCsn['@Core.MediaType']) {
hasStream = elementCsn['@Core.MediaType'];
delete elementCsn['@Core.MediaType'];
// CDXCORE-CDXCORE-177:
// V2: don't render element but add attribute 'm:HasStream="true' to EntityType
// V4: render property type 'Edm.Stream'
streamProps.push(elementName);
} else {
collectUsedType(elementCsn);
props.push(new Edm.Property(v, { Name: elementName }, elementCsn));
streamProps.push(elementName);
}
else {
collectUsedType(elementCsn);
props.push(new Edm.Property(v, { Name: elementName }, elementCsn));
}
}
});
}
if (options.isV2()) {
if (streamProps.length > 1) { // TODO: why not mention 2.0 in text?
error(null, [ 'definitions', elementsCsn.name ], { names: streamProps, version: '2.0', anno: '@Core.MediaType' },
'Expected only one element to be annotated with $(ANNO) for OData $(VERSION) but found $(NAMES)');
}
});
if(options.isV2()) {
if(streamProps.length > 1) { // TODO: why not mention 2.0 in text?
error(null, ['definitions', elementsCsn.name], { names: streamProps, version: '2.0', anno: '@Core.MediaType' },
'Expected only one element to be annotated with $(ANNO) for OData $(VERSION) but found $(NAMES)');
else if (streamProps.length === 1) {
info(null, [ 'definitions', elementsCsn.name ], { id: streamProps[0], version: '2.0', anno: '@Core.MediaType' },
'Property $(ID) annotated with $(ANNO) is removed from EDM for OData $(VERSION)');
}
else if(streamProps.length === 1) {
info(null, ['definitions', elementsCsn.name], { id: streamProps[0], version: '2.0', anno: '@Core.MediaType' },
'Property $(ID) annotated with $(ANNO) is removed from EDM for OData $(VERSION)');
}
}

@@ -683,3 +693,3 @@ return [ props, hasStream ];

function createTerm(termCsn) {
function createTerm( termCsn ) {
const attributes = { Name: termCsn.name.replace(schemaNamePrefix, '') };

@@ -691,8 +701,7 @@ const term = new Edm.Term(v, attributes, termCsn);

// V4 <TypeDefintion>
function createTypeDefinitionV4(typeCsn)
{
function createTypeDefinitionV4( typeCsn ) {
// derived types are already resolved to base types
const attributes = { Name: typeCsn.name.replace(schemaNamePrefix, '') };
if(!edmUtils.isODataSimpleIdentifier(attributes.Name))
message('odata-spec-violation-id', ['definitions', typeCsn.name], { id: attributes.Name });
if (!edmUtils.isODataSimpleIdentifier(attributes.Name))
message('odata-spec-violation-id', [ 'definitions', typeCsn.name ], { id: attributes.Name });

@@ -705,16 +714,15 @@ const typeDef = new Edm.TypeDefinition(v, attributes, typeCsn );

// add bound/unbound actions/functions for V4
function createActionV4(actionCsn, _name, entityCsn=undefined)
{
function createActionV4( actionCsn, _name, entityCsn = undefined ) {
const iAmAnAction = actionCsn.kind === 'action';
const actionName = edmUtils.getBaseName(actionCsn.name);
const attributes = { Name: actionName, IsBound : false };
const attributes = { Name: actionName, IsBound: false };
const loc = entityCsn
? [ 'definitions', entityCsn.name, 'actions', actionCsn.name ]
: [ 'definitions', actionCsn.name ];
const location = entityCsn
? [ 'definitions', entityCsn.name, 'actions', actionCsn.name ]
: [ 'definitions', actionCsn.name ];
if(!edmUtils.isODataSimpleIdentifier(attributes.Name))
message('odata-spec-violation-id', loc, { id: attributes.Name });
if (!edmUtils.isODataSimpleIdentifier(attributes.Name))
message('odata-spec-violation-id', location, { id: attributes.Name });
if(!iAmAnAction)
if (!iAmAnAction)
attributes.IsComposable = false;

@@ -724,3 +732,3 @@

const actionNode = (iAmAnAction) ? new Edm.Action(v, attributes)
: new Edm.FunctionDefinition(v, attributes);
: new Edm.FunctionDefinition(v, attributes);

@@ -736,17 +744,17 @@ const bpType = entityCsn ? fullQualified(entityCsn.name) : undefined;

let bpName = 'in';
if(actionCsn.params) {
const entries = Object.entries(actionCsn.params);
if (actionCsn.params) {
const entries = Object.entries(actionCsn.params);
const firstParam = entries[0][1];
const type = firstParam?.items?.type || firstParam?.type;
if(type === special$self) {
if (type === special$self) {
bpName = entries[0][0];
setProp(actionCsn, '$bindingParam', firstParam);
if(bpType) {
if(firstParam.items?.type)
if (bpType) {
if (firstParam.items?.type)
firstParam.items.type = bpType;
if(firstParam.type)
if (firstParam.type)
firstParam.type = bpType;
}
if(!edmUtils.isODataSimpleIdentifier(bpName))
message('odata-spec-violation-id', [ ...loc, 'params', bpName ], { id: bpName });
if (!edmUtils.isODataSimpleIdentifier(bpName))
message('odata-spec-violation-id', [ ...location, 'params', bpName ], { id: bpName });
}

@@ -757,42 +765,36 @@ }

// No explicit binding parameter, check (user defined) annotation value)
if(!actionCsn.$bindingParam) {
if (!actionCsn.$bindingParam) {
const bpNameAnno = actionCsn['@cds.odata.bindingparameter.name'];
if(bpNameAnno != null) {
if(typeof bpNameAnno === 'string')
if (bpNameAnno != null) {
if (typeof bpNameAnno === 'string')
bpName = bpNameAnno;
if(typeof bpNameAnno === 'object' && bpNameAnno['='])
if (typeof bpNameAnno === 'object' && bpNameAnno['='])
bpName = bpNameAnno['='];
}
if(!edmUtils.isODataSimpleIdentifier(bpName))
message('odata-spec-violation-id', [...loc, '@cds.odata.bindingparameter.name'], { id: bpName });
if(actionCsn.params && actionCsn.params[bpName]) {
error('duplicate-definition', [...loc, '@cds.odata.bindingparameter.name'], { '#': 'param', name: bpName });
}
if (!edmUtils.isODataSimpleIdentifier(bpName))
message('odata-spec-violation-id', [ ...location, '@cds.odata.bindingparameter.name' ], { id: bpName });
if (actionCsn.params && actionCsn.params[bpName])
error('duplicate-definition', [ ...location, '@cds.odata.bindingparameter.name' ], { '#': 'param', name: bpName });
}
if(entityCsn != undefined)
{
if (entityCsn) {
actionNode.setEdmAttribute('IsBound', true);
if(!actionCsn.$bindingParam) {
if (!actionCsn.$bindingParam) {
// Binding Parameter: 'in' at first position in sequence, this is decisive!
if(actionCsn['@cds.odata.bindingparameter.collection'])
actionNode.append(new Edm.Parameter(v, { Name: bpName, Type: bpType, Collection:true/*, Nullable: false*/ } ));
if (actionCsn['@cds.odata.bindingparameter.collection'])
actionNode.append(new Edm.Parameter(v, { Name: bpName, Type: bpType, Collection: true/* , Nullable: false */ } ));
else
actionNode.append(new Edm.Parameter(v, { Name: bpName, Type: bpType } ));
actionNode.append(new Edm.Parameter(v, { Name: bpName, Type: bpType } ));
}
}
else if(EntityContainer)// unbound => produce Action/FunctionImport
{
else if (EntityContainer) { // unbound => produce Action/FunctionImport
/** @type {object} */
const actionImport = iAmAnAction
? new Edm.ActionImport(v, { Name: actionName, Action : fullQualified(actionName) })
: new Edm.FunctionImport(v, { Name: actionName, Function : fullQualified(actionName) });
? new Edm.ActionImport(v, { Name: actionName, Action: fullQualified(actionName) })
: new Edm.FunctionImport(v, { Name: actionName, Function: fullQualified(actionName) });
const rt = actionCsn.returns && ((actionCsn.returns.items && actionCsn.returns.items.type) || actionCsn.returns.type);
if(rt) // add EntitySet attribute only if return type is a non abstract entity
{
if (rt) { // add EntitySet attribute only if return type is a non abstract entity
const definition = schemaCsn.definitions[rt];
if(definition && definition.kind === 'entity' && !definition.abstract)
{
if (definition && definition.kind === 'entity' && !definition.abstract)
actionImport.setEdmAttribute('EntitySet', edmUtils.getBaseName(rt));
}
}

@@ -803,21 +805,22 @@ EntityContainer.register(actionImport);

// Parameter Nodes
actionCsn.params && Object.entries(actionCsn.params).forEach(([parameterName, parameterCsn]) => {
const p = new Edm.Parameter(v, { Name: parameterName }, parameterCsn );
const pLoc = [ ...loc, 'params', p._edmAttributes.Name ];
if(!edmUtils.isODataSimpleIdentifier(parameterName))
message('odata-spec-violation-id', pLoc, { id: parameterName });
collectUsedType(parameterCsn);
edmTypeCompatibilityCheck(p, pLoc);
actionNode.append(p);
});
if (actionCsn.params) {
Object.entries(actionCsn.params).forEach(([ parameterName, parameterCsn ]) => {
const p = new Edm.Parameter(v, { Name: parameterName }, parameterCsn );
const pLoc = [ ...location, 'params', p._edmAttributes.Name ];
if (!edmUtils.isODataSimpleIdentifier(parameterName))
message('odata-spec-violation-id', pLoc, { id: parameterName });
collectUsedType(parameterCsn);
edmTypeCompatibilityCheck(p, pLoc);
actionNode.append(p);
});
}
// return type if any
if(actionCsn.returns) {
if (actionCsn.returns) {
actionNode._returnType = new Edm.ReturnType(v, actionCsn.returns);
collectUsedType(actionCsn.returns);
edmTypeCompatibilityCheck(actionNode._returnType, [ ...loc, 'returns' ]);
edmTypeCompatibilityCheck(actionNode._returnType, [ ...location, 'returns' ]);
// if binding type matches return type add attribute EntitySetPath
if(entityCsn != undefined && fullQualified(entityCsn.name) === actionNode._returnType._type) {
if (entityCsn && fullQualified(entityCsn.name) === actionNode._returnType._type)
actionNode.setEdmAttribute('EntitySetPath', bpName);
}
}

@@ -828,4 +831,3 @@ Schema.addAction(actionNode);

// add bound/unbound actions/functions for V2
function createActionV2(actionCsn, name, entityCsn=undefined)
{
function createActionV2( actionCsn, name, entityCsn = undefined ) {
/** @type {object} */

@@ -846,33 +848,29 @@ const attributes = { Name: name.replace(schemaNamePrefix, '') };

const loc = entityCsn
const location = entityCsn
? [ 'definitions', entityCsn.name, 'actions', actionCsn.name ]
: [ 'definitions', actionCsn.name ];
if(!edmUtils.isODataSimpleIdentifier(attributes.Name))
message('odata-spec-violation-id', loc, { id: attributes.Name });
if (!edmUtils.isODataSimpleIdentifier(attributes.Name))
message('odata-spec-violation-id', location, { id: attributes.Name });
const rt = actionCsn.returns && ((actionCsn.returns.items && actionCsn.returns.items.type) || actionCsn.returns.type);
if(rt) // add EntitySet attribute only if return type is an entity
{
if (rt) { // add EntitySet attribute only if return type is an entity
const definition = schemaCsn.definitions[rt];
if(definition && definition.kind === 'entity')
{
if (definition && definition.kind === 'entity')
functionImport.setEdmAttribute('EntitySet', rt.replace(schemaNamePrefix, ''));
}
}
if(actionCsn.returns)
if (actionCsn.returns)
functionImport.setEdmAttribute('ReturnType', getReturnType(actionCsn));
if(actionCsn.kind === 'function')
functionImport.setXml( {'m:HttpMethod': 'GET' });
else if(actionCsn.kind === 'action')
functionImport.setXml( {'m:HttpMethod': 'POST'});
if (actionCsn.kind === 'function')
functionImport.setXml( { 'm:HttpMethod': 'GET' });
else if (actionCsn.kind === 'action')
functionImport.setXml( { 'm:HttpMethod': 'POST' });
if(entityCsn != undefined)
{
if (entityCsn) {
// Make bound function names always unique as per Ralf's recommendation
functionImport.setXml( {'sap:action-for': fullQualified(entityCsn.name) } );
const name = entityCsn.name.replace(schemaNamePrefix, '') + '_' + functionImport._edmAttributes.Name;
functionImport.setEdmAttribute('Name', name);
functionImport.setXml( { 'sap:action-for': fullQualified(entityCsn.name) } );
const entityName = `${entityCsn.name.replace(schemaNamePrefix, '')}_${functionImport._edmAttributes.Name}`;
functionImport.setEdmAttribute('Name', entityName);

@@ -882,13 +880,12 @@ // Binding Parameter: Primary Keys at first position in sequence, this is decisive!

edmUtils.foreach(entityCsn.elements,
elementCsn => elementCsn.key && !elementCsn.target,
(elementCsn, elementName) => {
functionImport.append(new Edm.Parameter(v, { Name: elementName }, elementCsn, 'In' ));
}
);
elementCsn => elementCsn.key && !elementCsn.target,
(elementCsn, elementName) => {
functionImport.append(new Edm.Parameter(v, { Name: elementName }, elementCsn, 'In' ));
});
}
// is this still required?
Object.entries(actionCsn).forEach(([p, v]) => {
if (p.match(/^@sap\./))
functionImport.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : v });
Object.entries(actionCsn).forEach(([ key, val ]) => {
if (key.match(/^@sap\./))
functionImport.setXml( { [`sap:${key.slice(5).replace(/\./g, '-')}`]: val });
});

@@ -899,41 +896,42 @@ // then append all other parameters

// the client assume that Nullable is false.... Correct Nullable Handling is done inside Parameter constructor
actionCsn.params && Object.entries(actionCsn.params).forEach(([parameterName, parameterCsn], i) => {
const type = parameterCsn?.items?.type || parameterCsn?.type;
if(i === 0 && type === special$self) {
if (actionCsn.params) {
Object.entries(actionCsn.params).forEach(([ parameterName, parameterCsn ], i) => {
const type = parameterCsn?.items?.type || parameterCsn?.type;
if (i === 0 && type === special$self) {
// skip and remove the first parameter if it is a $self binding parameter to
// omit annotation rendering later on
delete actionCsn.params[parameterName];
}
else {
const pLoc = [...loc, 'params', parameterName];
const param = new Edm.Parameter(v, { Name: parameterName }, parameterCsn, 'In' );
collectUsedType(parameterCsn);
edmTypeCompatibilityCheck(param, pLoc);
if(!edmUtils.isODataSimpleIdentifier(parameterName))
message('odata-spec-violation-id', pLoc, { id: parameterName });
delete actionCsn.params[parameterName];
}
else {
const pLoc = [ ...location, 'params', parameterName ];
const param = new Edm.Parameter(v, { Name: parameterName }, parameterCsn, 'In' );
collectUsedType(parameterCsn);
edmTypeCompatibilityCheck(param, pLoc);
if (!edmUtils.isODataSimpleIdentifier(parameterName))
message('odata-spec-violation-id', pLoc, { id: parameterName });
// only scalar or structured type in V2 (not entity)
if(param._type &&
// only scalar or structured type in V2 (not entity)
if (param._type &&
!param._type.startsWith('Edm.') &&
csn.definitions[param._type] &&
!edmUtils.isStructuredType(csn.definitions[param._type]))
message('odata-spec-violation-param', pLoc, { version: '2.0' });
message('odata-spec-violation-param', pLoc, { version: '2.0' });
if(param._isCollection)
message('odata-spec-violation-array', pLoc, { version: '2.0' });
if (param._isCollection)
message('odata-spec-violation-array', pLoc, { version: '2.0' });
functionImport.append(param);
}
});
functionImport.append(param);
}
});
}
if(EntityContainer)
if (EntityContainer)
EntityContainer.register(functionImport);
function getReturnType(action)
{
function getReturnType( action ) {
// it is safe to assume that either type or items.type are set
const returnsLoc = [ ...loc, 'returns'];
const returnsLoc = [ ...location, 'returns' ];
const returns = action.returns.items || action.returns;
let type = returns['@odata.Type'];
if(!type) {
if (!type) {
type = returns.type;

@@ -945,9 +943,9 @@ if (type) {

}
else if(isBuiltinType(type)) {
else if (isBuiltinType(type)) {
type = edmUtils.mapCdsToEdmType(returns, messageFunctions, true);
if(type) {
if (type) {
const td = EdmPrimitiveTypeMap[type];
if(td && !td.v2) {
if (td && !td.v2) {
message('odata-spec-violation-type', returnsLoc,
{ type, version: '2.0', '#': 'incompatible' });
{ type, version: '2.0', '#': 'incompatible' });
}

@@ -959,4 +957,4 @@ }

}
if(action.returns._isCollection)
type = `Collection(${type})`
if (action.returns._isCollection)
type = `Collection(${type})`;
}

@@ -986,4 +984,3 @@ else {

*/
function addAssociationV2(navigationProperty)
{
function addAssociationV2( navigationProperty ) {
let constraints = navigationProperty._csn._constraints;

@@ -994,6 +991,6 @@ let parentName = navigationProperty._csn._edmParentCsn.name.replace(schemaNamePrefix, '');

let i = 1;
while(NamesInSchemaXRef[assocName] !== undefined) {
assocName = plainAssocName + '_' + i++;
}
while (NamesInSchemaXRef[assocName] !== undefined)
assocName = `${plainAssocName}_${i++}`;
let fromRole = parentName;

@@ -1012,4 +1009,4 @@ let toRole = navigationProperty._edmAttributes.Type.replace(schemaAliasPrefix, ''); // <= navprops type should be prefixed with alias

if(fromRole === toRole) {
if(constraints._partnerCsn)
if (fromRole === toRole) {
if (constraints._partnerCsn)
fromRole += '1';

@@ -1039,5 +1036,4 @@ else

let reuseAssoc = false;
let forwardAssocCsn = constraints._partnerCsn;
if(forwardAssocCsn)
{
const forwardAssocCsn = constraints._partnerCsn;
if (forwardAssocCsn) {
// This is a backlink, swap the roles and types, rewrite assocName

@@ -1049,8 +1045,9 @@ [ fromRole, toRole ] = [ toRole, fromRole ];

parentName = forwardAssocCsn._edmParentCsn.name.replace(schemaNamePrefix, '');
assocName = plainAssocName = parentName + NAVPROP_TRENNER + forwardAssocCsn.name.replace(VALUELIST_NAVPROP_PREFIX, '');
plainAssocName = parentName + NAVPROP_TRENNER + forwardAssocCsn.name.replace(VALUELIST_NAVPROP_PREFIX, '');
assocName = plainAssocName;
i = 1;
while(NamesInSchemaXRef[assocName] !== undefined && !(NamesInSchemaXRef[assocName][0] instanceof Edm.Association)) {
assocName = plainAssocName + '_' + i++;
}
while (NamesInSchemaXRef[assocName] !== undefined && !(NamesInSchemaXRef[assocName][0] instanceof Edm.Association))
assocName = `${plainAssocName}_${i++}`;
navigationProperty.setEdmAttribute('Relationship', fullQualified(assocName));

@@ -1063,3 +1060,3 @@

if(reuseAssoc)
if (reuseAssoc)
return;

@@ -1071,21 +1068,21 @@

const edmAssociation = new Edm.Association(v, { Name: assocName }, navigationProperty,
[ fromRole, fullQualified(fromEntityType) ],
[ toRole, fullQualified(toEntityType) ],
constraints._multiplicity );
if(NamesInSchemaXRef[assocName] === undefined) {
[ fromRole, fullQualified(fromEntityType) ],
[ toRole, fullQualified(toEntityType) ],
constraints._multiplicity );
if (NamesInSchemaXRef[assocName] === undefined)
NamesInSchemaXRef[assocName] = [ edmAssociation ];
}
else {
else
NamesInSchemaXRef[assocName].push(edmAssociation);
}
// Add ReferentialConstraints if any
if(!navigationProperty._isCollection && Object.keys(constraints.constraints).length > 0) {
if (!navigationProperty._isCollection && Object.keys(constraints.constraints).length > 0) {
// A managed composition is treated as association
if(navigationProperty._csn.type === 'cds.Composition' && navigationProperty._csn.on) {
if (navigationProperty._csn.type === 'cds.Composition' && navigationProperty._csn.on) {
edmAssociation.append(Edm.ReferentialConstraint.createV2(v,
toRole, fromRole, constraints.constraints));
toRole, fromRole, constraints.constraints));
}
else {
edmAssociation.append(Edm.ReferentialConstraint.createV2(v,
fromRole, toRole, constraints.constraints));
fromRole, toRole, constraints.constraints));
}

@@ -1095,6 +1092,6 @@ }

Schema.append(edmAssociation);
if(EntityContainer && !navigationProperty._targetCsn.$proxy) {
const assocSet = new Edm.AssociationSet(v, { Name: assocName, Association: fullQualified(assocName) },
fromRole, toRole, fromEntitySet, toEntitySet);
if(navigationProperty._csn._SetAttributes)
if (EntityContainer && !navigationProperty._targetCsn.$proxy) {
const assocSet = new Edm.AssociationSet(v, { Name: assocName, Association: fullQualified(assocName) },
fromRole, toRole, fromEntitySet, toEntitySet);
if (navigationProperty._csn._SetAttributes)
assocSet.setSapVocabularyAsAttributes(navigationProperty._csn._SetAttributes);

@@ -1106,5 +1103,4 @@ EntityContainer.register(assocSet);

// produce a full qualified name replacing the namespace with the alias (if provided)
function fullQualified(name)
{
return schemaAliasPrefix + name.replace(schemaNamePrefix, '')
function fullQualified( name ) {
return schemaAliasPrefix + name.replace(schemaNamePrefix, '');
}

@@ -1114,13 +1110,13 @@ }

// generate the Edm.Annotations tree and append it to the corresponding schema
function addAnnotations(xServiceRefs) {
let { annos, usedVocabularies, xrefs } = translate.csn2annotationEdm(reqDefs, csn.vocabularies, serviceCsn.name, Edm, options, messageFunctions, mergedVocabularies);
function addAnnotations2XServiceRefs( ) {
const { annos, usedVocabularies, xrefs } = translate.csn2annotationEdm(reqDefs, csn.vocabularies, serviceCsn.name, Edm, options, messageFunctions, mergedVocabularies);
// distribute edm:Annotations into the schemas
// Distribute each anno into Schema
annos.forEach(anno => {
annos.forEach((anno) => {
let targetSchema = whatsMySchemaName(anno._edmAttributes.Target);
// if no target schema has been found, it's a service annotation that applies to the service schema
if(targetSchema === undefined)
if (targetSchema === undefined)
targetSchema = serviceCsn.name;
if(targetSchema !== serviceCsn.name) {
const newTarget = anno._edmAttributes.Target.replace(serviceCsn.name + '.', '');
if (targetSchema !== serviceCsn.name) {
const newTarget = anno._edmAttributes.Target.replace(`${serviceCsn.name}.`, '');
anno.setEdmAttribute('Target', newTarget);

@@ -1132,6 +1128,6 @@ }

// create service cross reference and merge it into xServiceRefs
xrefs.forEach(xr => {
if(xr !== serviceCsn.name) {
xrefs.forEach((xr) => {
if (xr !== serviceCsn.name) {
const art = edmUtils.createSchemaRef(allServices, xr);
if(xServiceRefs[art.inc.Namespace] === undefined)
if (xServiceRefs[art.inc.Namespace] === undefined)
xServiceRefs[art.inc.Namespace] = art;

@@ -1141,26 +1137,29 @@ }

// merge vocabulary cross references into xServiceRefs
usedVocabularies.forEach(art => xServiceRefs[art.inc.Namespace] = art);
usedVocabularies.forEach((art) => {
xServiceRefs[art.inc.Namespace] = art;
} );
}
function edmTypeCompatibilityCheck(p, pLoc) {
function edmTypeCompatibilityCheck( p, pLoc ) {
const edmType = p._type;
if(!edmType) {
if (!edmType) {
message('odata-spec-violation-type', pLoc);
}
else if(p._scalarType) {
else if (p._scalarType) {
const td = EdmPrimitiveTypeMap[edmType];
if(td) {
if (td) {
// The renderer/type mapper doesn't/shouldn't produce incompatible types and facets.
// Only the unknown type warning may be triggered by an unknown @odata.Type override.
if(td.v2 !== p.v2 && td.v4 !== p.v4)
if (td.v2 !== p.v2 && td.v4 !== p.v4) {
message('odata-spec-violation-type', pLoc,
{ type: edmType, version: (p.v4 ? '4.0' : '2.0'), '#': 'incompatible' });
EdmTypeFacetNames.forEach(name => {
{ type: edmType, version: (p.v4 ? '4.0' : '2.0'), '#': 'incompatible' });
}
EdmTypeFacetNames.forEach((name) => {
const facet = EdmTypeFacetMap[name];
const optional =
(facet.optional !== undefined)
const optional
= (facet.optional !== undefined)
? (Array.isArray(facet.optional)
? facet.optional.includes(edmType)
: facet.optional)
: false;
: false;

@@ -1170,7 +1169,19 @@ // facet is not in attributes

// node and facet version match
if(!p._edmAttributes[name] && td[name] && !optional && (p.v2 === facet.v2 || p.v4 === facet.v4)) {
if (!p._edmAttributes[name] && td[name] && !optional && (p.v2 === facet.v2 || p.v4 === facet.v4)) {
message('odata-spec-violation-type', pLoc,
{ type: edmType, name, version: (p.v4 ? '4.0' : '2.0'), '#': 'facet' });
{
type: edmType, name, version: (p.v4 ? '4.0' : '2.0'), '#': 'facet',
});
}
});
if (edmType === 'Edm.Decimal') {
const precision = Number.parseInt(p._edmAttributes.Precision, 10);
const scale = Number.parseInt(p._edmAttributes.Scale, 10);
if (!Number.isNaN(precision) && !Number.isNaN(scale) && scale > precision) {
message('odata-spec-violation-type', pLoc,
{
type: edmType, number: scale, rawvalue: precision, ersion: (p.v4 ? '4.0' : '2.0'), '#': 'scale',
});
}
}
}

@@ -1177,0 +1188,0 @@ else {

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

'use strict'
'use strict';

@@ -10,9 +10,15 @@ const edmUtils = require('./edmUtils.js');

const EdmTypeFacetMap = {
'MaxLength': { v2: true, v4: true, remove: true, optional: true },
'Precision': { v2: true, v4: true, remove: true, optional: true },
'Scale': { v2: true, v4: true, remove: true, optional: true, extra: 'sap:variable-scale' },
'SRID': { v4: true, remove: true, optional: true },
//'FixedLength': { v2: true },
//'Collation': { v2: true },
//'Unicode': { v2: true, v4: true },
MaxLength: {
v2: true, v4: true, remove: true, optional: true,
},
Precision: {
v2: true, v4: true, remove: true, optional: true,
},
Scale: {
v2: true, v4: true, remove: true, optional: true, extra: 'sap:variable-scale',
},
SRID: { v4: true, remove: true, optional: true },
// 'FixedLength': { v2: true },
// 'Collation': { v2: true },
// 'Unicode': { v2: true, v4: true },
};

@@ -23,3 +29,5 @@ const EdmTypeFacetNames = Object.keys(EdmTypeFacetMap);

const EdmPrimitiveTypeMap = {
'Edm.Binary': { v2: true, v4: true, MaxLength: true, FixedLength: true, desc: 'Binary data' },
'Edm.Binary': {
v2: true, v4: true, MaxLength: true, FixedLength: true, desc: 'Binary data',
},
'Edm.Boolean': { v2: true, v4: true, desc: 'Binary-valued logic' },

@@ -29,4 +37,8 @@ 'Edm.Byte': { v2: true, v4: true, desc: 'Unsigned 8-bit integer' },

'Edm.DateTime': { v2: true, Precision: true, desc: 'Date and time with values ranging from 12:00:00 midnight, January 1, 1753 A.D. through 11:59:59 P.M, December 31, 9999 A.D.' },
'Edm.DateTimeOffset': { v2: true, v4: true, Precision: true, desc: 'Date and time with a time-zone offset, no leap seconds' },
'Edm.Decimal': { v2: true, v4: true, Precision: true, Scale: true, desc: 'Numeric values with decimal representation' },
'Edm.DateTimeOffset': {
v2: true, v4: true, Precision: true, desc: 'Date and time with a time-zone offset, no leap seconds',
},
'Edm.Decimal': {
v2: true, v4: true, Precision: true, Scale: true, desc: 'Numeric values with decimal representation',
},
'Edm.Double': { v2: true, v4: true, desc: 'IEEE 754 binary64 floating-point number (15-17 decimal digits)' },

@@ -41,3 +53,5 @@ 'Edm.Duration': { v4: true, Precision: true, desc: 'Signed duration in days, hours, minutes, and (sub)seconds' },

'Edm.Stream': { v4: true, MaxLength: true, desc: 'Binary data stream' },
'Edm.String': { v2: true, v4: true, MaxLength: true, FixedLength: true, Collation: true, Unicode: true, desc: 'Sequence of characters' },
'Edm.String': {
v2: true, v4: true, MaxLength: true, FixedLength: true, Collation: true, Unicode: true, desc: 'Sequence of characters',
},
'Edm.Time': { v2: true, Precision: true, desc: 'time of day with values ranging from 0:00:00.x to 23:59:59.y, where x and y depend upon the precision' },

@@ -62,21 +76,19 @@ 'Edm.TimeOfDay': { v4: true, Precision: true, desc: 'Clock time 00:00-23:59:59.999999999999' },

'Edm.PrimitiveType': { v4: true, desc: 'Abstract meta type' },
//'Edm.Untyped': { v4: true, desc: 'Abstract void type' },
// 'Edm.Untyped': { v4: true, desc: 'Abstract void type' },
};
function getEdm(options, messageFunctions) {
const { error } = messageFunctions || { error: ()=>true, warning: ()=>true };
class Node
{
function getEdm( options, messageFunctions ) {
const { error } = messageFunctions || { error: () => true, warning: () => true };
class Node {
/**
* @param {boolean[]} v Versions in the form of [<v2>, <v4>].
* @param {boolean[]} version Versions in the form of [<v2>, <v4>].
* @param {object} attributes
* @param {CSN.Model} csn
*/
constructor(v, attributes = Object.create(null), csn=undefined)
{
if(!attributes || typeof attributes !== 'object')
constructor(version, attributes = Object.create(null), csn = undefined) {
if (!attributes || typeof attributes !== 'object')
error(null, 'Please debug me: attributes must be a dictionary');
if(!Array.isArray(v))
error(null, 'Please debug me: v is either undefined or not an array: ' + v);
if(v.filter(v => v).length !== 1)
if (!Array.isArray(version))
error(null, `Please debug me: v is either undefined or not an array: ${version}`);
if (version.filter(v => v).length !== 1)
error(null, 'Please debug me: exactly one version must be set');

@@ -93,13 +105,17 @@

this._ignoreChildren = false;
this._v = v;
this._v = version;
if(this.v2)
if (this.v2)
this.setSapVocabularyAsAttributes(csn);
}
get v2() { return this._v[0] }
get v4() { return this._v[1] }
get v2() {
return this._v[0];
}
get v4() {
return this._v[1];
}
get kind() {
return this.constructor.name
return this.constructor.name;
}

@@ -113,3 +129,3 @@

setEdmAttribute(key, value) {
if(key !== undefined && key !== null && value !== undefined && value !== null)
if (key !== undefined && key !== null && value !== undefined && value !== null)
this._edmAttributes[key] = value;

@@ -134,4 +150,3 @@ }

*/
setXml(attributes)
{
setXml(attributes) {
return Object.assign(this._xmlOnlyAttributes, attributes);

@@ -147,9 +162,7 @@ }

*/
setJSON(attributes)
{
setJSON(attributes) {
return Object.assign(this._jsonOnlyAttributes, attributes);
}
prepend(...children)
{
prepend(...children) {
this._children.splice(0, 0, ...children.filter(c => c));

@@ -159,28 +172,25 @@ return this;

append(...children)
{
append(...children) {
// remove undefined entries
this._children.push(...children.filter(c => c));
return this
return this;
}
// virtual
toJSON()
{
toJSON() {
const json = Object.create(null);
// $kind Property MAY be omitted in JSON for performance reasons
if(!(this.kind in Node.noJsonKinds))
json['$Kind'] = this.kind;
if (!(this.kind in Node.noJsonKinds))
json.$Kind = this.kind;
this.toJSONattributes(json);
this.toJSONchildren(json);
return json
return json;
}
// virtual
toJSONattributes(json)
{
toJSONattributes(json) {
forEach(this._edmAttributes, (p, v) => {
if (p !== 'Name')
json[p[0] === '@' ? p : '$' + p] = v;
json[p[0] === '@' ? p : `$${p}`] = v;
});

@@ -191,18 +201,18 @@ return json;

// virtual
toJSONchildren(json)
{
toJSONchildren(json) {
// any child with a Name should be added by its name into the JSON object
// all others must overload toJSONchildren()
this._children.filter(c => c._edmAttributes.Name).forEach(c => json[c._edmAttributes.Name] = c.toJSON());
this._children.filter(c => c._edmAttributes.Name).forEach((c) => {
json[c._edmAttributes.Name] = c.toJSON();
});
}
// virtual
toXML(indent = '', what='all')
{
let kind = this.kind;
let head = indent + '<' + kind;
toXML(indent = '', what = 'all') {
const { kind } = this;
let head = `${indent}<${kind}`;
if(kind==='Parameter' && this._edmAttributes.Collection) {
if (kind === 'Parameter' && this._edmAttributes.Collection) {
delete this._edmAttributes.Collection;
this._edmAttributes.Type=`Collection(${this._edmAttributes.Type})`;
this._edmAttributes.Type = `Collection(${this._edmAttributes.Type})`;
}

@@ -212,11 +222,12 @@

let inner = this.innerXML(indent + ' ', what)
if (inner.length < 1) {
head += '/>'
}
else if (inner.length < 77 && inner.indexOf('<') < 0) {
head += '>' + inner.slice(indent.length + 1, -1) + '</' + kind + '>'
} else {
head += '>\n' + inner + indent + '</' + kind + '>'
}
const inner = this.innerXML(`${indent} `, what);
if (inner.length < 1)
head += '/>';
else if (inner.length < 77 && inner.indexOf('<') < 0)
head += `>${inner.slice(indent.length + 1, -1)}</${kind}>`;
else
head += `>\n${inner}${indent}</${kind}>`;
return head;

@@ -226,12 +237,11 @@ }

// virtual
toXMLattributes()
{
toXMLattributes() {
let tmpStr = '';
forEach(this._edmAttributes, (p, v) => {
if (v !== undefined && typeof v !== 'object')
tmpStr += ' ' + p + '="' + edmUtils.escapeStringForAttributeValue(v) + '"'
tmpStr += ` ${p}="${edmUtils.escapeStringForAttributeValue(v)}"`;
});
forEach(this._xmlOnlyAttributes, (p, v) => {
if (v !== undefined && typeof v !== 'object')
tmpStr += ' ' + p + '="' + edmUtils.escapeStringForAttributeValue(v) + '"'
tmpStr += ` ${p}="${edmUtils.escapeStringForAttributeValue(v)}"`;
});

@@ -242,8 +252,8 @@ return tmpStr;

// virtual
innerXML(indent, what='all')
{
innerXML(indent, what = 'all') {
let xml = '';
this._children.forEach(e =>
xml += e.toXML(indent, what) + '\n');
this._children.forEach((e) => {
xml += `${e.toXML(indent, what)}\n`;
});
return xml;

@@ -253,11 +263,11 @@ }

// virtual
setSapVocabularyAsAttributes(csn, useSetAttributes=false)
{
if(csn)
{
setSapVocabularyAsAttributes(csn, useSetAttributes = false) {
if (csn) {
const attr = (useSetAttributes ? csn._SetAttributes : csn);
attr && Object.entries(attr).forEach(([p, v]) => {
if (p.match(/^@sap./))
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : v } );
});
if (attr) {
Object.entries(attr).forEach(([ p, v ]) => {
if (p.match(/^@sap./))
this.setXml( { [`sap:${p.slice(5).replace(/\./g, '-')}`]: v } );
});
}
}

@@ -268,23 +278,24 @@ }

// $kind Property MAY be omitted in JSON for performance reasons
Node.noJsonKinds = {'Property':1, 'EntitySet':1, 'ActionImport':1, 'FunctionImport':1, 'Singleton':1, 'Schema':1};
Node.noJsonKinds = {
Property: 1, EntitySet: 1, ActionImport: 1, FunctionImport: 1, Singleton: 1, Schema: 1,
};
class Reference extends Node
{
constructor(v, details)
{
super(v, details);
if(this.v2)
class Reference extends Node {
constructor(version, details) {
super(version, details);
if (this.v2)
this._edmAttributes['xmlns:edmx'] = 'http://docs.oasis-open.org/odata/ns/edmx';
}
get kind() { return 'edmx:Reference' }
get kind() {
return 'edmx:Reference';
}
toJSON()
{
let json = Object.create(null);
let includes = [];
toJSON() {
const json = Object.create(null);
const includes = [];
this._children.forEach(c => includes.push(c.toJSON()));
if(includes.length > 0)
json['$Include'] = includes;
if (includes.length > 0)
json.$Include = includes;
return json;

@@ -294,8 +305,8 @@ }

class Include extends Node
{
get kind() { return 'edmx:Include' }
toJSON()
{
let json = Object.create(null);
class Include extends Node {
get kind() {
return 'edmx:Include';
}
toJSON() {
const json = Object.create(null);
return this.toJSONattributes(json);

@@ -305,10 +316,27 @@ }

class Schema extends Node
{
constructor(v, ns, alias=undefined, serviceCsn=null, annotations=[], withEntityContainer=true)
{
class EntityContainer extends Node {
constructor(version, attributes, csn) {
super(version, attributes, csn);
this._registry = Object.create(null);
}
// use the _SetAttributes
setSapVocabularyAsAttributes(csn) {
super.setSapVocabularyAsAttributes(csn, true);
}
register(entry) {
if (!this._registry[entry._edmAttributes.Name])
this._registry[entry._edmAttributes.Name] = [ entry ];
else
this._registry[entry._edmAttributes.Name].push(entry);
this.append(entry);
}
}
class Schema extends Node {
constructor(version, ns, alias = undefined, serviceCsn = null, annotations = [], withEntityContainer = true) {
const props = { Namespace: ns };
if(alias !== undefined)
if (alias !== undefined)
props.Alias = alias;
super(v, props);
super(version, props);
this._annotations = annotations;

@@ -318,11 +346,10 @@ this._actions = Object.create(null);

if(this.v2 && serviceCsn)
if (this.v2 && serviceCsn)
this.setSapVocabularyAsAttributes(serviceCsn);
if(withEntityContainer)
{
let ecprops = { Name: 'EntityContainer' };
let ec = new EntityContainer(v, ecprops, serviceCsn );
if(this.v2)
ec.setXml( { 'm:IsDefaultEntityContainer': true } );
if (withEntityContainer) {
const ecprops = { Name: 'EntityContainer' };
const ec = new EntityContainer(version, ecprops, serviceCsn );
if (this.v2)
ec.setXml( { 'm:IsDefaultEntityContainer': true } );
// append for rendering, ok ec has Name

@@ -336,34 +363,34 @@ this.append(ec);

// hold actions and functions in V4
addAction(action)
{
if(this._actions[action._edmAttributes.Name])
addAction(action) {
if (this._actions[action._edmAttributes.Name])
this._actions[action._edmAttributes.Name].push(action);
else
this._actions[action._edmAttributes.Name] = [action];
this._actions[action._edmAttributes.Name] = [ action ];
}
setAnnotations(annotations)
{
if(Array.isArray(annotations) && annotations.length > 0)
setAnnotations(annotations) {
if (Array.isArray(annotations) && annotations.length > 0)
this._annotations.push(...annotations);
}
innerXML(indent, what)
{
innerXML(indent, what) {
let xml = '';
if(what==='metadata' || what==='all')
{
if (what === 'metadata' || what === 'all') {
xml += super.innerXML(indent);
this._actions && Object.values(this._actions).forEach(actionArray => {
actionArray.forEach(action => {
xml += action.toXML(indent, what) + '\n'; });
if (this._actions) {
Object.values(this._actions).forEach((actionArray) => {
actionArray.forEach((action) => {
xml += `${action.toXML(indent, what)}\n`;
});
});
}
}
if ((what === 'annotations' || what === 'all') && this._annotations.length > 0) {
this._annotations.filter(a => a._edmAttributes.Term).forEach((a) => {
xml += `${a.toXML(indent)}\n`;
});
this._annotations.filter(a => a._edmAttributes.Target).forEach((a) => {
xml += `${a.toXML(indent)}\n`;
});
}
if(what==='annotations' || what==='all')
{
if(this._annotations.length > 0) {
this._annotations.filter(a => a._edmAttributes.Term).forEach(a => xml += a.toXML(indent) + '\n');
this._annotations.filter(a => a._edmAttributes.Target).forEach(a => xml += a.toXML(indent) + '\n');
}
}
return xml;

@@ -373,53 +400,55 @@ }

// no $Namespace
toJSONattributes(json)
{
this._edmAttributes && Object.entries(this._edmAttributes).forEach(([p, v]) => {
if (p !== 'Name' && p !== 'Namespace')
json[p[0] === '@' ? p : '$' + p] = v;
});
toJSONattributes(json) {
if (this._edmAttributes) {
Object.entries(this._edmAttributes).forEach(([ p, v ]) => {
if (p !== 'Name' && p !== 'Namespace')
json[p[0] === '@' ? p : `$${p}`] = v;
});
}
}
toJSONchildren(json)
{
toJSONchildren(json) {
// 'edmx:DataServices' should not appear in JSON
super.toJSONchildren(json);
if(this._annotations.length > 0) {
this._annotations.filter(a => a._edmAttributes.Term).forEach(a => {
Object.entries(a.toJSON()).forEach(([n, v]) => {
if (this._annotations.length > 0) {
this._annotations.filter(a => a._edmAttributes.Term).forEach((a) => {
Object.entries(a.toJSON()).forEach(([ n, v ]) => {
json[n] = v;
});
});
let json_Annotations = Object.create(null);
this._annotations.filter(a => a._edmAttributes.Target).forEach(a => json_Annotations[a._edmAttributes.Target] = a.toJSON());
if(Object.keys(json_Annotations).length)
json['$Annotations'] = json_Annotations;
const jsonAnnotations = Object.create(null);
this._annotations.filter(a => a._edmAttributes.Target).forEach((a) => {
jsonAnnotations[a._edmAttributes.Target] = a.toJSON();
});
if (Object.keys(jsonAnnotations).length)
json.$Annotations = jsonAnnotations;
}
this._actions && Object.entries(this._actions).forEach(([actionName, actionArray]) => {
json[actionName] = [];
actionArray.forEach(action => {
json[actionName].push(action.toJSON());
if (this._actions) {
Object.entries(this._actions).forEach(([ actionName, actionArray ]) => {
json[actionName] = [];
actionArray.forEach((action) => {
json[actionName].push(action.toJSON());
});
});
});
}
return json;
}
}
class DataServices extends Node
{
constructor(v)
{
class DataServices extends Node {
constructor(v) {
super(v);
this._schemas = Object.create(null);
if(this.v2)
this.setXml( { 'm:DataServiceVersion': '2.0' } )
if (this.v2)
this.setXml( { 'm:DataServiceVersion': '2.0' } );
}
get kind() { return 'edmx:DataServices'; }
get kind() {
return 'edmx:DataServices';
}
registerSchema(fqName, schema)
{
if(!this._schemas[fqName]) {
registerSchema(fqName, schema) {
if (!this._schemas[fqName]) {
this._schemas[fqName] = schema;

@@ -430,6 +459,7 @@ super.append(schema);

toJSONchildren(json)
{
toJSONchildren(json) {
// 'edmx:DataServices' should not appear in JSON
this._children.forEach(s => json[s._edmAttributes.Namespace] = s.toJSON());
this._children.forEach((s) => {
json[s._edmAttributes.Namespace] = s.toJSON();
});
return json;

@@ -439,3 +469,3 @@ }

/* <edmx:Edmx> must contain exactly one <edmx:DataServices> with 1..n <edm:Schema> elements
/* <edmx:Edmx> must contain exactly one <edmx:DataServices> with 1..n <edm:Schema> elements
may contain 0..n <edmx:Reference> elements

@@ -450,7 +480,5 @@

class Edm extends Node
{
constructor(v, service)
{
super(v, { Version : (v[1]) ? '4.0' : '1.0' });
class Edm extends Node {
constructor(version, service) {
super(version, { Version: (version[1]) ? '4.0' : '1.0' });
this._service = service;

@@ -460,13 +488,11 @@ this._defaultRefs = [];

const xmlProps = Object.create(null);
if(this.v4)
{
if (this.v4) {
xmlProps['xmlns:edmx'] = 'http://docs.oasis-open.org/odata/ns/edmx';
xmlProps['xmlns:m'] = undefined;
xmlProps['xmlns:sap'] = undefined;
xmlProps['xmlns:m'] = undefined;
xmlProps['xmlns:sap'] = undefined;
}
else
{
else {
xmlProps['xmlns:edmx'] = 'http://schemas.microsoft.com/ado/2007/06/edmx';
xmlProps['xmlns:m'] = 'http://schemas.microsoft.com/ado/2007/08/dataservices/metadata';
xmlProps['xmlns:sap'] = 'http://www.sap.com/Protocols/SAPData';
xmlProps['xmlns:m'] = 'http://schemas.microsoft.com/ado/2007/08/dataservices/metadata';
xmlProps['xmlns:sap'] = 'http://www.sap.com/Protocols/SAPData';
}

@@ -476,32 +502,34 @@ this.setXml(xmlProps);

get kind() { return 'edmx:Edmx' }
get kind() {
return 'edmx:Edmx';
}
getAnnotations(schemaIndex=0)
{
if(this._service && this._service._children[schemaIndex])
getAnnotations(schemaIndex = 0) {
if (this._service && this._service._children[schemaIndex])
return this._service._children[schemaIndex]._annotations;
else
return undefined;
return undefined;
}
setAnnotations(annotations, schemaIndex=0)
{
if(this._service && this._service._children[schemaIndex])
setAnnotations(annotations, schemaIndex = 0) {
if (this._service && this._service._children[schemaIndex])
this._service._children[schemaIndex]._annotations = annotations;
}
toJSON()
{
let schema = this._service._children[0];
toJSON() {
const schema = this._service._children[0];
let json = Object.create(null);
json['$Version'] = this._edmAttributes.Version;
json['$EntityContainer'] = schema._edmAttributes.Namespace + '.' + schema._ec._edmAttributes.Name;
const json = Object.create(null);
json.$Version = this._edmAttributes.Version;
json.$EntityContainer = `${schema._edmAttributes.Namespace}.${schema._ec._edmAttributes.Name}`;
let reference_json = Object.create(null);
this._defaultRefs.forEach(r => reference_json[r._edmAttributes.Uri] = r.toJSON());
this._children.forEach(r => reference_json[r._edmAttributes.Uri] = r.toJSON());
const referenceJson = Object.create(null);
this._defaultRefs.forEach((r) => {
referenceJson[r._edmAttributes.Uri] = r.toJSON();
});
this._children.forEach((r) => {
referenceJson[r._edmAttributes.Uri] = r.toJSON();
});
if(Object.keys(reference_json).length)
json['$Reference'] = reference_json;
if (Object.keys(referenceJson).length)
json.$Reference = referenceJson;

@@ -514,15 +542,18 @@ this._service.toJSONattributes(json);

// all(default), metadata, annotations
toXML(what='all')
{
return '<?xml version="1.0" encoding="utf-8"?>\n' + super.toXML('', what);
toXML(what = 'all') {
return `<?xml version="1.0" encoding="utf-8"?>\n${super.toXML('', what)}`;
}
innerXML(indent, what)
{
innerXML(indent, what) {
let xml = '';
if(this.v4 || (this.v2 && (what === 'all' || what === 'annotations')))
this._defaultRefs.forEach(r => xml += r.toXML(indent) + '\n');
this._children.forEach(e => xml += e.toXML(indent) + '\n');
xml += this._service.toXML(indent, what) + '\n';
if (this.v4 || (this.v2 && (what === 'all' || what === 'annotations'))) {
this._defaultRefs.forEach((r) => {
xml += `${r.toXML(indent)}\n`;
});
}
this._children.forEach((e) => {
xml += `${e.toXML(indent)}\n`;
});
xml += `${this._service.toXML(indent, what)}\n`;
return xml;

@@ -532,34 +563,10 @@ }

class EntityContainer extends Node
{
constructor(v, attributes, csn) {
super(v, attributes, csn);
this._registry = Object.create(null);
}
// use the _SetAttributes
setSapVocabularyAsAttributes(csn)
{
super.setSapVocabularyAsAttributes(csn, true);
}
register(entry) {
if(!this._registry[entry._edmAttributes.Name])
this._registry[entry._edmAttributes.Name] = [entry];
else
this._registry[entry._edmAttributes.Name].push(entry);
this.append(entry);
}
}
class Singleton extends Node
{
toJSONattributes(json)
{
class Singleton extends Node {
toJSONattributes(json) {
forEach(this._edmAttributes, (p, v) => {
if (p !== 'Name') {
if(p === 'EntityType') // it's $Type in json
json['$Type'] = v;
if (p === 'EntityType') // it's $Type in json
json.$Type = v;
else
json[p[0] === '@' ? p : '$' + p] = v;
json[p[0] === '@' ? p : `$${p}`] = v;
}

@@ -570,8 +577,9 @@ });

toJSONchildren(json)
{
let json_navPropBinding = Object.create(null);
this._children.forEach(npb => json_navPropBinding[npb._edmAttributes.Path] = npb._edmAttributes.Target);
if(Object.keys(json_navPropBinding).length > 0)
json['$NavigationPropertyBinding'] = json_navPropBinding;
toJSONchildren(json) {
const jsonNavPropBinding = Object.create(null);
this._children.forEach((npb) => {
jsonNavPropBinding[npb._edmAttributes.Path] = npb._edmAttributes.Target;
});
if (Object.keys(jsonNavPropBinding).length > 0)
json.$NavigationPropertyBinding = jsonNavPropBinding;

@@ -582,18 +590,15 @@ return json;

getDuplicateMessage() {
return `EntityType "${this._edmAttributes.EntityType}"`
return `EntityType "${this._edmAttributes.EntityType}"`;
}
}
class EntitySet extends Singleton
{
class EntitySet extends Singleton {
// use the _SetAttributes
setSapVocabularyAsAttributes(csn)
{
setSapVocabularyAsAttributes(csn) {
super.setSapVocabularyAsAttributes(csn, true);
}
toJSONattributes(json)
{
toJSONattributes(json) {
// OASIS ODATA-1231 $Collection=true
json['$Collection']=true;
json.$Collection = true;
return super.toJSONattributes(json);

@@ -603,17 +608,22 @@ }

class Key extends Node
{
class PropertyRef extends Node {
constructor(version, Name, Alias) {
super(version, (Alias) ? { Name, Alias } : { Name });
}
toJSON() {
return this._edmAttributes.Alias ? { [this._edmAttributes.Alias]: this._edmAttributes.Name } : this._edmAttributes.Name;
}
}
class Key extends Node {
// keys is an array of [name] or [name, alias]
constructor(v, keys)
{
super(v);
constructor(version, keys) {
super(version);
if (keys && keys.length > 0)
{
keys.forEach(k => this.append(new PropertyRef(v, ...k)));
}
keys.forEach(k => this.append(new PropertyRef(version, ...k)));
}
toJSON()
{
let json = [];
toJSON() {
const json = [];
this._children.forEach(c => json.push(c.toJSON()));

@@ -632,28 +642,23 @@ return json;

class ActionFunctionBase extends Node
{
constructor(v, details)
{
super(v, details);
class ActionFunctionBase extends Node {
constructor(version, details) {
super(version, details);
this._returnType = undefined;
}
innerXML(indent)
{
innerXML(indent) {
let xml = super.innerXML(indent);
if(this._returnType !== undefined)
xml += this._returnType.toXML(indent) + '\n';
return xml
if (this._returnType !== undefined)
xml += `${this._returnType.toXML(indent)}\n`;
return xml;
}
toJSONchildren(json)
{
let json_parameters = [];
this._children.forEach(p => json_parameters.push(p.toJSON()));
if(json_parameters.length > 0)
json['$Parameter'] = json_parameters;
if(this._returnType)
{
json['$ReturnType'] = this._returnType.toJSON();
}
toJSONchildren(json) {
const jsonParameters = [];
this._children.forEach(p => jsonParameters.push(p.toJSON()));
if (jsonParameters.length > 0)
json.$Parameter = jsonParameters;
if (this._returnType)
json.$ReturnType = this._returnType.toJSON();
return json;

@@ -664,5 +669,6 @@ }

// collide with a method 'Function' of the Istanbul/NYC tool
class FunctionDefinition extends ActionFunctionBase
{
get kind() { return 'Function'; }
class FunctionDefinition extends ActionFunctionBase {
get kind() {
return 'Function';
}
}

@@ -678,34 +684,31 @@ class Action extends ActionFunctionBase {}

getDuplicateMessage() {
return `Function "${this._edmAttributes.Name}"`
return `Function "${this._edmAttributes.Name}"`;
}
} //ActionFunctionBase {}
} // ActionFunctionBase {}
class ActionImport extends Node {
getDuplicateMessage() {
return `Action "${this._edmAttributes.Name}"`
return `Action "${this._edmAttributes.Name}"`;
}
}
class TypeBase extends Node
{
constructor(v, attributes, csn, typeName='Type')
{
class TypeBase extends Node {
constructor(version, attributes, csn, typeName = 'Type') {
// ??? Is CSN still required? NavProp?
super(v, attributes, csn);
super(version, attributes, csn);
this._typeName = typeName;
this._scalarType = undefined;
if(this._edmAttributes[typeName] === undefined)
{
let typecsn = csn.type ? csn : (csn.items && csn.items.type ? csn.items : csn);
if (this._edmAttributes[typeName] === undefined) {
const typecsn = csn.type ? csn : (csn.items && csn.items.type ? csn.items : csn);
// Complex/EntityType are derived from TypeBase
// but have no type attribute in their CSN
if(typecsn.type) { // this thing has a type
if (typecsn.type) { // this thing has a type
// check whether this is a scalar type (or array of scalar type) or a named type
if(typecsn.items && typecsn.items.type &&
isBuiltinType(typecsn.items.type)) {
if (typecsn.items && typecsn.items.type &&
isBuiltinType(typecsn.items.type))
this._scalarType = typecsn.items;
}
else if(isBuiltinType(typecsn.type)) {
else if (isBuiltinType(typecsn.type))
this._scalarType = typecsn;
}
if(this._scalarType) {
if (this._scalarType) {
this._edmAttributes[typeName] = csn._edmType;

@@ -717,4 +720,4 @@ // CDXCORE-CDXCORE-173 ignore type facets for Edm.Stream

// multi-byte characters.
if(!(this._edmAttributes[typeName] === 'Edm.Stream' &&
!( /*scalarType.type === 'cds.String' ||*/ this._scalarType.type === 'cds.Binary')))
if (!(this._edmAttributes[typeName] === 'Edm.Stream' &&
!( /* scalarType.type === 'cds.String' || */ this._scalarType.type === 'cds.Binary')))
edmUtils.addTypeFacets(this, this._scalarType);

@@ -731,5 +734,5 @@ }

// Allow to override type only on scalar and undefined types
if((this._scalarType || typecsn.type == null) && !csn.elements) {
if ((this._scalarType || typecsn.type == null) && !csn.elements) {
const odataType = csn['@odata.Type'];
if(odataType) {
if (odataType) {
const td = EdmPrimitiveTypeMap[odataType];

@@ -740,18 +743,17 @@ // If type is known, it must be available in the current version

// produce an unrecoverable error.
if(td && (td.v2 === this.v2 || td.v4 === this.v4)) {
if (td && (td.v2 === this.v2 || td.v4 === this.v4)) {
this.setEdmAttribute(typeName, odataType);
EdmTypeFacetNames.forEach(facetName => {
EdmTypeFacetNames.forEach((facetName) => {
const facet = EdmTypeFacetMap[facetName];
if(facet.remove) {
if (facet.remove) {
this.removeEdmAttribute(facetName);
this.removeEdmAttribute(facet.extra);
}
if(td[facetName] !== undefined &&
if (td[facetName] !== undefined &&
(facet.v2 === this.v2 ||
facet.v4 === this.v4))
{
if(this.v2 && facetName === 'Scale' && csn['@odata.'+facetName] === 'variable')
facet.v4 === this.v4)) {
if (this.v2 && facetName === 'Scale' && csn[`@odata.${facetName}`] === 'variable')
this.setXml({ [facet.extra]: true });
else
this.setEdmAttribute(facetName, csn['@odata.'+facetName]);
this.setEdmAttribute(facetName, csn[`@odata.${facetName}`]);
}

@@ -767,7 +769,6 @@ });

if(options.whatsMySchemaName && this._edmAttributes[typeName]) {
let schemaName = options.whatsMySchemaName(this._edmAttributes[typeName]);
if(schemaName && schemaName !== options.serviceName) {
this._edmAttributes[typeName] = this._edmAttributes[typeName].replace(options.serviceName + '.', '');
}
if (options.whatsMySchemaName && this._edmAttributes[typeName]) {
const schemaName = options.whatsMySchemaName(this._edmAttributes[typeName]);
if (schemaName && schemaName !== options.serviceName)
this._edmAttributes[typeName] = this._edmAttributes[typeName].replace(`${options.serviceName}.`, '');
}

@@ -778,27 +779,25 @@

// decorate for XML (not for Complex/EntityType)
if(this._isCollection && this._edmAttributes[typeName])
this._edmAttributes[typeName] = `Collection(${this._edmAttributes[typeName]})`
if (this._isCollection && this._edmAttributes[typeName])
this._edmAttributes[typeName] = `Collection(${this._edmAttributes[typeName]})`;
}
toJSONattributes(json)
{
toJSONattributes(json) {
// $Type Edm.String, $Nullable=false MAY be omitted
// @ property and parameter for performance reasons
if(this._type !== 'Edm.String' && this._type) // Edm.String is default)
json['$'+this._typeName] = this._type;
if (this._type !== 'Edm.String' && this._type) // Edm.String is default)
json[`$${this._typeName}`] = this._type;
this._edmAttributes && Object.entries(this._edmAttributes).forEach(([p, v]) => {
if (p !== 'Name' && p !== this._typeName
if (this._edmAttributes) {
Object.entries(this._edmAttributes).forEach(([ p, v ]) => {
if (p !== 'Name' && p !== this._typeName &&
// remove this line if Nullable=true becomes default
&& !(p === 'Nullable' && !v))
{
json[p[0] === '@' ? p : '$' + p] = v;
}
});
!(p === 'Nullable' && !v))
json[p[0] === '@' ? p : `$${p}`] = v;
});
}
if(this._isCollection)
json['$Collection'] = this._isCollection;
if (this._isCollection)
json.$Collection = this._isCollection;
return json;
}

@@ -808,21 +807,18 @@ }

class ComplexType extends TypeBase {
constructor(v, details, csn) {
super(v, details, csn);
if(this.v4 && !!csn['@open']) {
this._edmAttributes['OpenType'] = true;
}
constructor(version, details, csn) {
super(version, details, csn);
if (this.v4 && !!csn['@open'])
this._edmAttributes.OpenType = true;
}
}
class EntityType extends ComplexType
{
constructor(v, details, properties, csn)
{
super(v, details, csn);
class EntityType extends ComplexType {
constructor(version, details, properties, csn) {
super(version, details, csn);
this.append(...properties);
const aliasXref = Object.create(null);
csn.$edmKeyPaths.forEach(p => {
const [alias, ...tail] = p[0].split('/').reverse();
csn.$edmKeyPaths.forEach((p) => {
const [ alias, ...tail ] = p[0].split('/').reverse();
if(aliasXref[alias] === undefined)
if (aliasXref[alias] === undefined)
aliasXref[alias] = 0;

@@ -832,19 +828,18 @@ else

// if it's a path, push the alias
if(tail.length > 0)
if (tail.length > 0)
p.push(alias);
});
csn.$edmKeyPaths.slice().reverse().forEach(p => {
csn.$edmKeyPaths.slice().reverse().forEach((p) => {
let alias = p[1];
if(alias)
{
if (alias) {
const c = aliasXref[alias]--;
// Limit Key length to 32 characters
if(c > 0) {
if(alias.length > 28) {
alias = alias.substr(0, 13)+ '__' +alias.substr(alias.length-13, alias.length);
}
alias = alias+'_'+c.toString().padStart(3, '0');
if (c > 0) {
if (alias.length > 28)
alias = `${alias.substr(0, 13)}__${alias.substr(alias.length - 13, alias.length)}`;
alias = `${alias}_${c.toString().padStart(3, '0')}`;
}
else if(alias.length > 32) {
alias = alias.substr(0, 15)+ '__' +alias.substr(alias.length-15, alias.length);
else if (alias.length > 32) {
alias = `${alias.substr(0, 15)}__${alias.substr(alias.length - 15, alias.length)}`;
}

@@ -855,11 +850,11 @@ p[1] = alias;

if(csn.$edmKeyPaths && csn.$edmKeyPaths.length)
this._keys = new Key(v, csn.$edmKeyPaths);
if (csn.$edmKeyPaths && csn.$edmKeyPaths.length)
this._keys = new Key(version, csn.$edmKeyPaths);
else
this._keys = undefined;
if(options.odataOpenapiHints) {
if(csn['@cds.autoexpose'])
if (options.odataOpenapiHints) {
if (csn['@cds.autoexpose'])
this.setJSON({ '@cds.autoexpose': true });
if(csn['@cds.autoexposed'])
if (csn['@cds.autoexposed'])
this.setJSON({ '@cds.autoexposed': true });

@@ -869,20 +864,19 @@ }

innerXML(indent)
{
innerXML(indent) {
let xml = '';
if(this._keys)
xml += this._keys.toXML(indent) + '\n';
if (this._keys)
xml += `${this._keys.toXML(indent)}\n`;
return xml + super.innerXML(indent);
}
toJSONattributes(json)
{
toJSONattributes(json) {
super.toJSONattributes(json);
this._jsonOnlyAttributes && Object.entries(this._jsonOnlyAttributes).forEach(([p, v]) => {
json[p[0] === '@' ? p : '$' + p] = v;
});
if(this._keys)
{
json['$Key'] = this._keys.toJSON();
if (this._jsonOnlyAttributes) {
Object.entries(this._jsonOnlyAttributes).forEach(([ p, v ]) => {
json[p[0] === '@' ? p : `$${p}`] = v;
});
}
if (this._keys)
json.$Key = this._keys.toJSON();
return json;

@@ -892,25 +886,19 @@ }

class Term extends TypeBase
{
constructor(v, attributes, csn)
{
super(v, attributes, csn);
class Term extends TypeBase {
constructor(version, attributes, csn) {
super(version, attributes, csn);
const appliesTo = csn['@odata.term.AppliesTo'];
if(appliesTo) {
this.setEdmAttribute('AppliesTo', Array.isArray(appliesTo) ? appliesTo.map(v=>v['=']||v).join(' ') : appliesTo['='] || appliesTo);
}
if (appliesTo)
this.setEdmAttribute('AppliesTo', Array.isArray(appliesTo) ? appliesTo.map(v => v['='] || v).join(' ') : appliesTo['='] || appliesTo);
}
}
}
class TypeDefinition extends TypeBase
{
constructor(v, attributes, csn)
{
super(v, attributes, csn, 'UnderlyingType');
class TypeDefinition extends TypeBase {
constructor(version, attributes, csn) {
super(version, attributes, csn, 'UnderlyingType');
}
toJSONattributes(json)
{
toJSONattributes(json) {
super.toJSONattributes(json);
json['$UnderlyingType'] = this._type;
json.$UnderlyingType = this._type;
return json;

@@ -920,17 +908,23 @@ }

class EnumType extends TypeDefinition
{
constructor(v, attributes, csn)
{
super(v, attributes, csn);
class Member extends Node {
toJSONattributes(json) {
json[this._edmAttributes.Name] = this._edmAttributes.Value;
return json;
}
}
class EnumType extends TypeDefinition {
constructor(version, attributes, csn) {
super(version, attributes, csn);
// array of enum not yet allowed
const enumValues = /*(csn.items && csn.items.enum) ||*/ csn.enum;
enumValues && Object.entries(enumValues).forEach(([en, e]) => {
this.append(new Member(v, { Name: en, Value: e.val } ));
});
const enumValues = /* (csn.items && csn.items.enum) || */ csn.enum;
if (enumValues) {
Object.entries(enumValues).forEach(([ en, e ]) => {
this.append(new Member(version, { Name: en, Value: e.val } ));
});
}
}
toJSONattributes(json)
{
toJSONattributes(json) {
super.toJSONattributes(json);

@@ -940,4 +934,3 @@ return json;

toJSONchildren(json)
{
toJSONchildren(json) {
this._children.forEach(c => c.toJSONattributes(json));

@@ -948,20 +941,8 @@ return json;

class Member extends Node
{
toJSONattributes(json)
{
json[this._edmAttributes.Name] = this._edmAttributes.Value;
return json;
}
}
class PropertyBase extends TypeBase
{
constructor(v, attributes, csn)
{
super(v, attributes, csn);
class PropertyBase extends TypeBase {
constructor(version, attributes, csn) {
super(version, attributes, csn);
this._csn = csn;
if(this.v2)
{
let typecsn = csn.items || csn;
if (this.v2) {
const typecsn = csn.items || csn;

@@ -971,6 +952,5 @@ // see edmUtils.mapsCdsToEdmType => add sap:display-format annotation

// but not if Edm.DateTime is the result of a regular cds type mapping
if(this._edmAttributes.Type === 'Edm.DateTime'
&& (typecsn.type !== 'cds.DateTime' && typecsn.type !== 'cds.Timestamp'))
this.setXml( { 'sap:display-format' : 'Date' } );
if (this._edmAttributes.Type === 'Edm.DateTime' &&
(typecsn.type !== 'cds.DateTime' && typecsn.type !== 'cds.Timestamp'))
this.setXml( { 'sap:display-format': 'Date' } );
}

@@ -980,34 +960,29 @@ this.setNullable();

setNullable()
{
setNullable() {
// From the Spec: In OData 4.01 responses a collection-valued property MUST specify a value for the Nullable attribute.
if(this._isCollection) {
if (this._isCollection)
this._edmAttributes.Nullable = !this.isNotNullable();
}
// Nullable=true is default, mention Nullable=false only in XML
// Nullable=false is default for EDM JSON representation 4.01
// When a key explicitly (!) has 'notNull = false', it stays nullable
else if(this.isNotNullable())
{
else if (this.isNotNullable())
this._edmAttributes.Nullable = false;
}
}
isNotNullable(csn=undefined) {
let nodeCsn = csn || this._csn;
isNotNullable(csn = undefined) {
const nodeCsn = csn || this._csn;
// Nullable=true is default, mention Nullable=false only in XML
// Nullable=false is default for EDM JSON representation 4.01
// When a key explicitly (!) has 'notNull = false', it stays nullable
return (nodeCsn._NotNullCollection !== undefined ? nodeCsn._NotNullCollection :
(nodeCsn.key && nodeCsn.notNull !== false) || nodeCsn.notNull === true);
return (nodeCsn._NotNullCollection !== undefined ? nodeCsn._NotNullCollection
: (nodeCsn.key && nodeCsn.notNull !== false) || nodeCsn.notNull === true);
}
toJSONattributes(json)
{
toJSONattributes(json) {
super.toJSONattributes(json);
// mention all nullable elements explicitly, remove if Nullable=true becomes default
if(this._edmAttributes.Nullable === undefined || this._edmAttributes.Nullable === true)
{
json['$Nullable'] = true;
}
if (this._edmAttributes.Nullable === undefined || this._edmAttributes.Nullable === true)
json.$Nullable = true;
return json;

@@ -1019,13 +994,10 @@ }

called with V2=false */
class ReturnType extends PropertyBase
{
constructor(v, csn)
{
super(v, {}, csn);
class ReturnType extends PropertyBase {
constructor(version, csn) {
super(version, {}, csn);
}
// we need Name but NO $kind, can't use standard to JSON()
toJSON()
{
let json = Object.create(null);
toJSON() {
const json = Object.create(null);
this.toJSONattributes(json);

@@ -1036,13 +1008,10 @@ return json;

class Property extends PropertyBase
{
constructor(v, attributes, csn)
{
super(v, attributes, csn);
class Property extends PropertyBase {
constructor(version, attributes, csn) {
super(version, attributes, csn);
// TIPHANACDS-4180
if(this.v2)
{
if (this.v2) {
// eslint-disable-next-line sonarjs/no-redundant-boolean
if(csn['@odata.etag'] == true || csn['@cds.etag'] == true)
this._edmAttributes.ConcurrencyMode='Fixed'
if (csn['@odata.etag'] || csn['@cds.etag'])
this._edmAttributes.ConcurrencyMode = 'Fixed';

@@ -1052,3 +1021,3 @@ // translate the following @sap annos as xml attributes to the Property

if (p in Property.SAP_Annotation_Attributes)
this.setXml( { ['sap:' + p.slice(5).replace(/\./g, '-')] : v });
this.setXml( { [`sap:${p.slice(5).replace(/\./g, '-')}`]: v });
});

@@ -1061,3 +1030,2 @@ }

if (csn.default && !csn['@Core.ComputedDefaultValue']) {
const def = csn.default;

@@ -1067,6 +1035,6 @@ // if def has a value, it's a simple value

// if it's a simple value with signs, produce a string representation
if(csn.default.xpr) {
defVal = csn.default.xpr.map(i => {
if(i.val !== undefined) {
if(csn.type === 'cds.Boolean')
if (csn.default.xpr) {
defVal = csn.default.xpr.map((i) => {
if (i.val !== undefined) {
if (csn.type === 'cds.Boolean')
return i.val ? 'true' : 'false';

@@ -1079,3 +1047,3 @@ return i.val;

// complex values should be marked with @Core.ComputedDefaultValue already in the edmPreprocessor
if(defVal !== undefined) {
if (this.v4 && defVal !== undefined) {
/* No Default Value rendering in V2 (or only with future flag).

@@ -1085,4 +1053,3 @@ Reason: Fiori UI5 expects 'Default' under extension namespace 'sap:'

*/
if(this.v4)
this._edmAttributes[`Default${this.v4 ? 'Value' : ''}`] = defVal;
this._edmAttributes[`Default${this.v4 ? 'Value' : ''}`] = defVal;
}

@@ -1099,28 +1066,15 @@ }

Property.SAP_Annotation_Attributes = {
'@sap.hierarchy.node.for':1, // -> sap:hierarchy-node-for
'@sap.hierarchy.parent.node.for':1, // -> sap:hierarchy-parent-node-for
'@sap.hierarchy.level.for':1, // -> sap:hierarchy-level-for
'@sap.hierarchy.drill.state.for':1, // -> sap:hierarchy-drill-state-for
'@sap.hierarchy.node.descendant.count.for':1, // -> sap:hierarchy-node-descendant-count-for
'@sap.parameter':1
'@sap.hierarchy.node.for': 1, // -> sap:hierarchy-node-for
'@sap.hierarchy.parent.node.for': 1, // -> sap:hierarchy-parent-node-for
'@sap.hierarchy.level.for': 1, // -> sap:hierarchy-level-for
'@sap.hierarchy.drill.state.for': 1, // -> sap:hierarchy-drill-state-for
'@sap.hierarchy.node.descendant.count.for': 1, // -> sap:hierarchy-node-descendant-count-for
'@sap.parameter': 1,
};
class PropertyRef extends Node
{
constructor(v, Name, Alias) {
super(v, (Alias) ? { Name, Alias } : { Name });
}
class Parameter extends PropertyBase {
constructor(version, attributes, csn = {}, mode = null) {
super(version, attributes, csn);
toJSON() {
return this._edmAttributes.Alias ? { [this._edmAttributes.Alias]:this._edmAttributes.Name } : this._edmAttributes.Name;
}
}
class Parameter extends PropertyBase
{
constructor(v, attributes, csn={}, mode=null)
{
super(v, attributes, csn);
if(mode != null)
if (mode != null)
this._edmAttributes.Mode = mode;

@@ -1131,11 +1085,10 @@

// the client assume that Nullable is false.... Correct Nullable Handling is done inside Parameter constructor
if(this.v2 && this._edmAttributes.Nullable === undefined)
this.setXml({Nullable: true});
if (this.v2 && this._edmAttributes.Nullable === undefined)
this.setXml({ Nullable: true });
}
toJSON()
{
toJSON() {
// we need Name but NO $kind, can't use standard to JSON()
let json = Object.create(null);
json['$Name'] = this._edmAttributes.Name;
const json = Object.create(null);
json.$Name = this._edmAttributes.Name;
return this.toJSONattributes(json);

@@ -1147,10 +1100,25 @@ }

class NavigationProperty extends Property
{
constructor(v, attributes, csn)
{
super(v, attributes, csn);
class OnDelete extends Node {}
let [src, tgt] = edmUtils.determineMultiplicity(csn._constraints._partnerCsn || csn);
csn._constraints._multiplicity = csn._constraints._partnerCsn ? [tgt, src] : [src, tgt];
class ReferentialConstraint extends Node {
constructor(version, attributes, csn) {
super(version, attributes, csn);
this._d = null;
this._p = null;
}
innerXML(indent) {
if (this._d && this._p)
return `${this._p.toXML(indent)}\n${this._d.toXML(indent)}\n`;
return super.innerXML(indent);
}
}
class NavigationProperty extends Property {
constructor(version, attributes, csn) {
super(version, attributes, csn);
const [ src, tgt ] = edmUtils.determineMultiplicity(csn._constraints._partnerCsn || csn);
csn._constraints._multiplicity = csn._constraints._partnerCsn ? [ tgt, src ] : [ src, tgt ];
this._type = attributes.Type;

@@ -1160,10 +1128,9 @@ this._isCollection = this.isToMany();

if (this.v4)
{
if(options.isStructFormat && this._csn.key)
if (this.v4) {
if (options.isStructFormat && this._csn.key)
this._edmAttributes.Nullable = false;
// either csn has multiplicity or we have to use the multiplicity of the backlink
if(this._isCollection) {
this._edmAttributes.Type = `Collection(${attributes.Type})`
if (this._isCollection) {
this._edmAttributes.Type = `Collection(${attributes.Type})`;
// attribute Nullable is not allowed in combination with Collection (see Spec)

@@ -1176,11 +1143,11 @@ // Even if min cardinality is > 0, remove Nullable, because the implicit OData contract

// we have exactly one selfReference or the default partner
let partner =
!csn.$noPartner ?
csn._selfReferences.length === 1
const partner
= !csn.$noPartner
? csn._selfReferences.length === 1
? csn._selfReferences[0]
: csn._constraints._partnerCsn
: undefined;
if(partner && partner['@odata.navigable'] !== false && this._csn._edmParentCsn.kind !== 'type') {
if (partner && partner['@odata.navigable'] !== false && this._csn._edmParentCsn.kind !== 'type') {
// $abspath[0] is main entity
this._edmAttributes.Partner = partner.$abspath.slice(1).join(options.pathDelimiter);
this._edmAttributes.Partner = partner.$abspath.slice(1).join('/');
}

@@ -1198,16 +1165,15 @@

// eslint-disable-next-line sonarjs/no-redundant-boolean
if(csn['@odata.contained'] == true || csn.containsTarget) {
if (csn['@odata.contained'] || csn.containsTarget)
this._edmAttributes.ContainsTarget = true;
}
if(this._edmAttributes.ContainsTarget === undefined && csn.type === 'cds.Composition') {
if (this._edmAttributes.ContainsTarget === undefined && csn.type === 'cds.Composition') {
// Delete is redundant in containment
// TODO: to be specified via @sap.on.delete
this.append(new OnDelete(v, { Action: 'Cascade' } ) );
this.append(new OnDelete(version, { Action: 'Cascade' } ) );
}
}
if (this.v2 && this.isNotNullable()) {
// in V2 not null must be expressed with target cardinality of 1 or more,
// store Nullable=false and evaluate in determineMultiplicity()
// in V2 not null must be expressed with target cardinality of 1 or more,
// store Nullable=false and evaluate in determineMultiplicity()
delete this._edmAttributes.Nullable;

@@ -1228,4 +1194,4 @@ }

isNotNullable(csn=undefined) {
let nodeCsn = csn || this._csn;
isNotNullable(csn = undefined) {
const nodeCsn = csn || this._csn;
// Set Nullable=false only if 'NOT NULL' was specified in the model

@@ -1248,34 +1214,31 @@ // Do not derive Nullable=false from key attribute.

toJSONattributes(json)
{
toJSONattributes(json) {
// use the original type, not the decorated one
super.toJSONattributes(json);
json['$Type'] = this._type;
json.$Type = this._type;
// attribute Nullable is not allowed in combination with Collection (see Spec)
if(json['$Collection'])
delete json['$Nullable'];
if (json.$Collection)
delete json.$Nullable;
return json;
}
toJSONchildren(json)
{
let json_constraints = Object.create(null);
this._children.forEach(c => {
switch(c.kind) {
toJSONchildren(json) {
const jsonConstraints = Object.create(null);
this._children.forEach((c) => {
switch (c.kind) {
case 'ReferentialConstraint':
// collect ref constraints in dictionary
json_constraints[c._edmAttributes.Property] = c._edmAttributes.ReferencedProperty;
jsonConstraints[c._edmAttributes.Property] = c._edmAttributes.ReferencedProperty;
break;
case 'OnDelete':
json['$OnDelete'] = c._edmAttributes.Action;
json.$OnDelete = c._edmAttributes.Action;
break;
default:
error(null, 'Please debug me: Unhandled NavProp child: ' + c.kind);
error(null, `Please debug me: Unhandled NavProp child: ${c.kind}`);
}
});
// TODO Annotations
if(Object.keys(json_constraints).length > 0)
json['$ReferentialConstraint'] = json_constraints;
if (Object.keys(jsonConstraints).length > 0)
json.$ReferentialConstraint = jsonConstraints;
return json;

@@ -1285,61 +1248,42 @@ }

// V4 referential constraints!
addReferentialConstraintNodes()
{
addReferentialConstraintNodes() {
// flip the constrains if this is a $self partner
let _constraints = this._csn._constraints;
let [i,j] = [0,1];
if(this._csn._constraints._partnerCsn) {
let { _constraints } = this._csn;
let [ i, j ] = [ 0, 1 ];
if (this._csn._constraints._partnerCsn) {
_constraints = this._csn._constraints._partnerCsn._constraints;
[i,j] = [1,0];
[ i, j ] = [ 1, 0 ];
}
_constraints.constraints && Object.values(_constraints.constraints).forEach(c =>
this.append(new ReferentialConstraint(this._v,
{ Property: c[i].join(options.pathDelimiter), ReferencedProperty: c[j].join(options.pathDelimiter) } ) )
);
}
}
class ReferentialConstraint extends Node
{
constructor(v, attributes, csn) {
super(v, attributes, csn);
this._d = null;
this._p = null;
}
innerXML(indent)
{
if(this._d && this._p)
{
return this._p.toXML(indent) + '\n' + this._d.toXML(indent) + '\n';
if (_constraints.constraints) {
Object.values(_constraints.constraints)
.forEach(c => this.append(
new ReferentialConstraint(this._v,
{
Property: c[i].join(options.pathDelimiter),
ReferencedProperty: c[j].join(options.pathDelimiter),
} )
));
}
else
return super.innerXML(indent);
}
}
class OnDelete extends Node {}
// Annotations below
class AnnotationBase extends Node
{
class AnnotationBase extends Node {
// No Kind: AnnotationBase is base class for Thing and ValueThing with dynamic kinds,
// this requires an explicit constructor as the kinds cannot be blacklisted in
// Node.toJSON()
toJSON()
{
let json = Object.create(null);
toJSON() {
const json = Object.create(null);
this.toJSONattributes(json);
this.toJSONchildren(json);
return json
return json;
}
getConstantExpressionValue()
{
getConstantExpressionValue() {
// short form: key: value
const inlineConstExpr =
[ 'Edm.Binary', 'Edm.Boolean', 'Edm.Byte', 'Edm.Date', 'Edm.DateTimeOffset', 'Edm.Decimal', 'Edm.Double', 'Edm.Duration', 'Edm.Guid',
'Edm.Int16', 'Edm.Int32', 'Edm.Int64', 'Edm.SByte','Edm.Single', 'Edm.Stream', 'Edm.String', 'Edm.TimeOfDay',
const inlineConstExpr
= [ 'Edm.Binary', 'Edm.Boolean', 'Edm.Byte', 'Edm.Date', 'Edm.DateTimeOffset', 'Edm.Decimal', 'Edm.Double', 'Edm.Duration', 'Edm.Guid',
'Edm.Int16', 'Edm.Int32', 'Edm.Int64', 'Edm.SByte', 'Edm.Single', 'Edm.Stream', 'Edm.String', 'Edm.TimeOfDay',
// Edm.Geo* according to https://issues.oasis-open.org/browse/ODATA-1323
/* 'Edm.Geography', 'Edm.GeographyPoint', 'Edm.GeographyLineString', 'Edm.GeographyPolygon', 'Edm.GeographyMultiPoint',
/* 'Edm.Geography', 'Edm.GeographyPoint', 'Edm.GeographyLineString', 'Edm.GeographyPolygon', 'Edm.GeographyMultiPoint',
'Edm.GeographyMultiLineString', 'Edm.GeographyMultiPolygon', 'Edm.GeographyCollection', 'Edm.Geometry', 'Edm.GeometryPoint',

@@ -1352,57 +1296,46 @@ 'Edm.GeometryLineString', 'Edm.GeometryPolygon', 'Edm.GeometryMultiPoint', 'Edm.GeometryMultiLineString', 'Edm.GeometryMultiPolygon',

// Official JSON V4.01 Spec defines these paths as constant inline expression:
'AnnotationPath', 'ModelElementPath', 'NavigationPropertyPath', 'PropertyPath',
];
'AnnotationPath', 'ModelElementPath', 'NavigationPropertyPath', 'PropertyPath' ];
const dict = this._jsonOnlyAttributes;
const inline = edmUtils.intersect(Object.keys(dict), inlineConstExpr);
if(inline.length === 1)
{
let v = dict[inline[0]];
switch(inline[0])
{
/* short notation for Edm.Boolean, Edm.String and Edm.Float, see internal project:
edmx2csn-npm/edm-converters/blob/835d92a1aa6b0be25c56cef85e260c9188187429/lib/edmxV40ToJsonV40/README.md
*/
case 'Edm.Boolean':
v = (v=='true'?true:(v=='false'?false:v));
// eslint-no-fallthrough
default:
return v;
}
if (inline.length === 1) {
const v = dict[inline[0]];
/* short notation for Edm.Boolean, Edm.String and Edm.Float, see internal project:
edmx2csn-npm/edm-converters/blob/835d92a1aa6b0be25c56cef85e260c9188187429/lib/edmxV40ToJsonV40/README.md
*/
if (inline[0] === 'Edm.Boolean')
return (v === 'true' ? true : (v === 'false' ? false : v));
return v;
}
else
{
// if this is not a constant expression shortcut, render key/value pair verbatim
// without filtering non-spec-compliant constExpr
let json = Object.create(null);
Object.entries(dict).forEach(([k,v]) => {
json['$'+k] = v;
});
return json;
}
// if this is not a constant expression shortcut, render key/value pair verbatim
// without filtering non-spec-compliant constExpr
const json = Object.create(null);
Object.entries(dict).forEach(([ k, v ]) => {
json[`$${k}`] = v;
});
return json;
}
mergeJSONAnnotations(prefix='') {
mergeJSONAnnotations(prefix = '') {
return this._children.filter(c => c.kind === 'Annotation').reduce((o, a) => {
Object.entries(a.toJSON()).forEach(([n, v]) => {
o[prefix+n] = v;
Object.entries(a.toJSON()).forEach(([ n, v ]) => {
o[prefix + n] = v;
});
return o; },
Object.create(null));
return o;
},
Object.create(null));
}
}
class Annotations extends AnnotationBase
{
constructor(v, target)
{
super(v, { Target: target });
this.setXml( { xmlns : this.v2 ? 'http://docs.oasis-open.org/odata/ns/edm' : undefined } );
class Annotations extends AnnotationBase {
constructor(version, target) {
super(version, { Target: target });
this.setXml( { xmlns: this.v2 ? 'http://docs.oasis-open.org/odata/ns/edm' : undefined } );
}
toJSONattributes(json)
{
toJSONattributes(json) {
forEach(this._edmAttributes, (p, v) => {
if (p !== 'Target')
json[p[0] === '@' ? p : '$' + p] = v;
json[p[0] === '@' ? p : `$${p}`] = v;
});

@@ -1412,9 +1345,8 @@ return json;

toJSONchildren(json)
{
this._children.forEach(a => {
Object.entries(a.toJSON()).forEach(([n, v]) => {
toJSONchildren(json) {
this._children.forEach((a) => {
Object.entries(a.toJSON()).forEach(([ n, v ]) => {
json[n] = v;
});
})
});
}

@@ -1435,14 +1367,11 @@ }

// transported this is no longer the case....
class Annotation extends AnnotationBase
{
constructor(v, termName)
{
super(v, { Term: termName } );
class Annotation extends AnnotationBase {
constructor(version, termName) {
super(version, { Term: termName } );
}
toJSON()
{
toJSON() {
const json = super.mergeJSONAnnotations(this.getJsonFQTermName());
const e = this._children.filter(c => c.kind !== 'Annotation');
if(e.length === 0 || this._ignoreChildren) // must be a constant expression
if (e.length === 0 || this._ignoreChildren) // must be a constant expression
json[this.getJsonFQTermName()] = this.getConstantExpressionValue();

@@ -1456,10 +1385,8 @@ else

getJsonFQTermName() {
return '@' + this._edmAttributes.Term + (this._edmAttributes.Qualifier ? '#' + this._edmAttributes.Qualifier : '');
return `@${this._edmAttributes.Term}${this._edmAttributes.Qualifier ? `#${this._edmAttributes.Qualifier}` : ''}`;
}
}
class Collection extends AnnotationBase
{
toJSON()
{
class Collection extends AnnotationBase {
toJSON() {
// EDM JSON doesn't mention annotations on collections

@@ -1470,20 +1397,16 @@ return this._children.map(a => a.toJSON());

class Record extends AnnotationBase
{
toJSONattributes(json)
{
if(this._jsonOnlyAttributes.Type)
class Record extends AnnotationBase {
toJSONattributes(json) {
if (this._jsonOnlyAttributes.Type)
json['@type'] = this._jsonOnlyAttributes.Type;
let keys = Object.keys(this._edmAttributes).filter(k => k !== 'Type');
for(const key of keys)
json['$'+key] = this._edmAttributes[key];
const keys = Object.keys(this._edmAttributes).filter(k => k !== 'Type');
for (const key of keys)
json[`$${key}`] = this._edmAttributes[key];
}
toJSONchildren(json)
{
this._children.forEach(c => {
switch(c.kind)
{
toJSONchildren(json) {
this._children.forEach((c) => {
switch (c.kind) {
case 'Annotation': {
Object.entries(c.toJSON()).forEach(([n, v]) => {
Object.entries(c.toJSON()).forEach(([ n, v ]) => {
json[n] = v;

@@ -1495,3 +1418,3 @@ });

// plus property annotations as [a.Property]@anno: val
Object.entries(c.mergeJSONannotations()).forEach(([n, a]) => {
Object.entries(c.mergeJSONannotations()).forEach(([ n, a ]) => {
json[n] = a;

@@ -1504,3 +1427,3 @@ });

default:
error(null, 'Pease debug me: Unhandled Record child: ' + c.kind);
error(null, `Pease debug me: Unhandled Record child: ${c.kind}`);
}

@@ -1511,19 +1434,14 @@ });

class PropertyValue extends AnnotationBase
{
constructor(v, property)
{
super(v);
class PropertyValue extends AnnotationBase {
constructor(version, property) {
super(version);
this._edmAttributes.Property = property;
}
toJSON()
{
const c = this._children.filter(c => c.kind !== 'Annotation')
if(c.length === 0 || this._ignoreChildren)
toJSON() {
const children = this._children.filter(child => child.kind !== 'Annotation');
if (children.length === 0 || this._ignoreChildren)
return this.getConstantExpressionValue();
else
{
return c[0].toJSON();
}
return children[0].toJSON();
}

@@ -1535,36 +1453,30 @@ mergeJSONannotations() {

class Thing extends AnnotationBase
{
constructor(v, kind, details)
{
super(v, details);
class Thing extends AnnotationBase {
constructor(version, kind, details) {
super(version, details);
this._kind = kind;
}
get kind() { return this._kind; }
get kind() {
return this._kind;
}
}
class ValueThing extends Thing
{
constructor(v, kind, value)
{
super(v, kind, undefined);
class ValueThing extends Thing {
constructor(version, kind, value) {
super(version, kind, undefined);
this._value = value;
}
toXML(indent='')
{
let kind = this.kind;
let xml = indent + '<' + kind + this.toXMLattributes();
xml += (this._value !== undefined ? '>' + edmUtils.escapeStringForText(this._value) + '</' + kind + '>' : '/>');
toXML(indent = '') {
const { kind } = this;
let xml = `${indent}<${kind}${this.toXMLattributes()}`;
xml += (this._value !== undefined ? `>${edmUtils.escapeStringForText(this._value)}</${kind}>` : '/>');
return xml;
}
toJSON()
{
if(this._children.length === 0 || this._ignoreChildren) // must be a constant expression
toJSON() {
if (this._children.length === 0 || this._ignoreChildren) // must be a constant expression
return this.getConstantExpressionValue();
else
// annotation must have exactly one child (=record or collection)
return this._children[0].toJSON();
return this._children[0].toJSON();
}

@@ -1575,17 +1487,12 @@ }

class Expr extends Thing {
constructor(v, kind, details) {
super(v, kind, details);
}
toJSON()
{
toJSON() {
// toJSON: depending on number of children unary or n-ary expr
const json = this.mergeJSONAnnotations();
const e = this._children.filter(c=>c.kind !== 'Annotation');
if(e.length === 1) {
json['$'+this.kind] = e[0].toJSON();
}
else {
json['$'+this.kind] = e.map(c => c.toJSON());
}
const e = this._children.filter(c => c.kind !== 'Annotation');
if (e.length === 1)
json[`$${this.kind}`] = e[0].toJSON();
else
json[`$${this.kind}`] = e.map(c => c.toJSON());
return json;

@@ -1601,3 +1508,3 @@ }

const json = this.mergeJSONAnnotations();
json['$'+this.kind] = null;
json[`$${this.kind}`] = null;
return json;

@@ -1609,3 +1516,3 @@ }

const json = this.mergeJSONAnnotations();
json['$'+this.kind] = this._children.filter(c=>c.kind !== 'Annotation').map(c => c.toJSON());
json[`$${this.kind}`] = this._children.filter(c => c.kind !== 'Annotation').map(c => c.toJSON());
this.toJSONattributes(json);

@@ -1618,6 +1525,5 @@ return json;

// TODO: Why json?
if(this._jsonOnlyAttributes['Collection'])
return ` Type="Collection(${this._edmAttributes.Type})"`
else
return ` Type="${this._edmAttributes.Type}"`
if (this._jsonOnlyAttributes.Collection)
return ` Type="Collection(${this._edmAttributes.Type})"`;
return ` Type="${this._edmAttributes.Type}"`;
}

@@ -1627,4 +1533,4 @@ toJSON() {

// first expression only, if any
const c = this._children.filter(c=>c.kind !== 'Annotation');
json['$'+this.kind] = c.length ? c[0].toJSON() : {};
const children = this._children.filter(child => child.kind !== 'Annotation');
json[`$${this.kind}`] = children.length ? children[0].toJSON() : {};
this.toJSONattributes(json);

@@ -1635,5 +1541,7 @@ return json;

super.toJSONattributes(json);
this._jsonOnlyAttributes && Object.entries(this._jsonOnlyAttributes).forEach(([p, v]) => {
json[p[0] === '@' ? p : '$' + p] = v;
});
if (this._jsonOnlyAttributes) {
Object.entries(this._jsonOnlyAttributes).forEach(([ p, v ]) => {
json[p[0] === '@' ? p : `$${p}`] = v;
});
}
return json;

@@ -1647,3 +1555,3 @@ }

const json = this.mergeJSONAnnotations();
json['$'+this.kind] = this._children.filter(c=>c.kind !== 'Annotation').map(c => c.toJSON());
json[`$${this.kind}`] = this._children.filter(c => c.kind !== 'Annotation').map(c => c.toJSON());
return json;

@@ -1656,4 +1564,4 @@ }

// first expression only, if any
const c = this._children.filter(c=>c.kind !== 'Annotation');
json['$'+this.kind] = c.length ? c[0].toJSON() : '';
const children = this._children.filter(child => child.kind !== 'Annotation');
json[`$${this.kind}`] = children.length ? children[0].toJSON() : '';
this.toJSONattributes(json);

@@ -1663,6 +1571,5 @@ return json;

toJSONattributes(json) // including Name
{
toJSONattributes(json) { // including Name
forEach(this._edmAttributes, (p, v) => {
json[p[0] === '@' ? p : '$' + p] = v;
json[p[0] === '@' ? p : `$${p}`] = v;
});

@@ -1674,4 +1581,4 @@ return json;

class LabeledElementReference extends ValueThing {
constructor(v, val) {
super(v, 'LabeledElementReference', val);
constructor(version, val) {
super(version, 'LabeledElementReference', val);
}

@@ -1683,4 +1590,4 @@ }

// first expression only, if any
const c = this._children.filter(c=>c.kind !== 'Annotation');
json['$'+this.kind] = c.length ? c[0].toJSON() : {};
const children = this._children.filter(child => child.kind !== 'Annotation');
json[`$${this.kind}`] = children.length ? children[0].toJSON() : {};
return json;

@@ -1692,25 +1599,24 @@ }

class End extends Node {}
class Association extends Node
{
constructor(v, details, navProp, fromRole, toRole, multiplicity)
{
super(v, details);
class Association extends Node {
constructor(version, details, navProp, fromRole, toRole, multiplicity) {
super(version, details);
this._end = [
new End(v, { Role: fromRole[0], Type: fromRole[1], Multiplicity: multiplicity[0] } ),
new End(v, { Role: toRole[0], Type: toRole[1], Multiplicity: multiplicity[1] } )
new End(version, { Role: fromRole[0], Type: fromRole[1], Multiplicity: multiplicity[0] } ),
new End(version, { Role: toRole[0], Type: toRole[1], Multiplicity: multiplicity[1] } ),
];
// set Delete:Cascade on composition end
if(navProp._csn.type === 'cds.Composition')
this._end[0].append(new OnDelete(v, { Action: 'Cascade' }));
if (navProp._csn.type === 'cds.Composition')
this._end[0].append(new OnDelete(version, { Action: 'Cascade' }));
if(navProp._csn._selfReferences && navProp._csn._selfReferences.length &&
if (navProp._csn._selfReferences && navProp._csn._selfReferences.length &&
navProp._csn._selfReferences[0].type === 'cds.Composition')
this._end[1].append(new OnDelete(v, { Action: 'Cascade' }));
this._end[1].append(new OnDelete(version, { Action: 'Cascade' }));
}
innerXML(indent)
{
innerXML(indent) {
let xml = '';
this._end.forEach(e => xml += e.toXML(indent) + '\n');
this._end.forEach((e) => {
xml += `${e.toXML(indent)}\n`;
});
xml += super.innerXML(indent);

@@ -1721,16 +1627,14 @@ return xml;

class AssociationSet extends Node
{
constructor(v, details, fromRole, toRole, fromEntitySet, toEntitySet)
{
super(v, details);
class AssociationSet extends Node {
constructor(version, details, fromRole, toRole, fromEntitySet, toEntitySet) {
super(version, details);
this.append(
new End(v, { Role: fromRole, EntitySet: fromEntitySet } ),
new End(v, { Role: toRole, EntitySet: toEntitySet } )
);
new End(version, { Role: fromRole, EntitySet: fromEntitySet } ),
new End(version, { Role: toRole, EntitySet: toEntitySet } )
);
}
getDuplicateMessage() {
return `Association "${this._edmAttributes.Association}"`
return `Association "${this._edmAttributes.Association}"`;
}
}
}

@@ -1740,15 +1644,16 @@ class Dependent extends Node {}

ReferentialConstraint.createV2 =
function(v, from, to, c)
{
let node = new ReferentialConstraint(v, {});
ReferentialConstraint.createV2
= (v, from, to, c) => {
const node = new ReferentialConstraint(v, {});
node._d = new Dependent(v, { Role: from } );
node._p = new Principal(v, { Role: to } );
c && Object.values(c).forEach(cv => {
node._d.append(new PropertyRef(v, cv[0].join(options.pathDelimiter)));
node._p.append(new PropertyRef(v, cv[1].join(options.pathDelimiter)));
});
if (c) {
Object.values(c).forEach((cv) => {
node._d.append(new PropertyRef(v, cv[0].join(options.pathDelimiter)));
node._p.append(new PropertyRef(v, cv[1].join(options.pathDelimiter)));
});
}
return node;
}
};

@@ -1771,3 +1676,3 @@ return {

Key,
//ActionFunctionBase,
// ActionFunctionBase,
FunctionDefinition,

@@ -1809,7 +1714,8 @@ Action,

Dependent,
Principal
}
Principal,
};
} // instance function
module.exports = { EdmTypeFacetMap, EdmTypeFacetNames, EdmPrimitiveTypeMap, getEdm };
module.exports = {
EdmTypeFacetMap, EdmTypeFacetNames, EdmPrimitiveTypeMap, getEdm,
};

@@ -26,18 +26,15 @@ 'use strict';

function mapAnnotationAssignment(artifact, parent, mappingDictionary)
{
let props = edmUtils.intersect(Object.keys(mappingDictionary), Object.keys(artifact));
function mapAnnotationAssignment( artifact, parent, mappingDictionary ) {
const props = edmUtils.intersect(Object.keys(mappingDictionary), Object.keys(artifact));
// now start the substitution
props.forEach(prop => {
let [ mapping, value, remove_original ] = mappingDictionary[prop];
if(mapping instanceof Function)
{
props.forEach((prop) => {
const [ mapping, value, removeOriginal ] = mappingDictionary[prop];
if (mapping instanceof Function)
mapping(artifact, parent, prop);
}
else
{
edmUtils.assignAnnotation(artifact, mapping, value || artifact[prop]['='] || artifact[prop]);
}
if(remove_original)
if (removeOriginal)
delete artifact[prop];

@@ -47,20 +44,14 @@ });

function addToSetAttr(carrier, propName, propValue, removeFromType=true) {
function addToSetAttr( carrier, propName, propValue, removeFromType = true ) {
edmUtils.assignProp(carrier, '_SetAttributes', Object.create(null));
edmUtils.assignAnnotation(carrier._SetAttributes, propName, propValue);
if(removeFromType) {
if (removeFromType)
delete carrier[propName];
}
}
function applyAppSpecificLateCsnTransformationOnElement(options, element, struct, error)
{
if(options.isV2())
{
if(struct['@Aggregation.ApplySupported.PropertyRestrictions'])
{
mapAnnotationAssignment(element, struct, AnalyticalAnnotations());
}
}
function applyAppSpecificLateCsnTransformationOnElement( options, element, struct, error ) {
if (options.isV2() && struct['@Aggregation.ApplySupported.PropertyRestrictions'])
mapAnnotationAssignment(element, struct, AnalyticalAnnotations());
// etag requires Core.OptimisticConcurrency to be set in V4 (cap/issues#2641)

@@ -72,47 +63,41 @@ // Oliver Heinrich mentions in the issue that the Okra runtime must be set to a

// for @[odata|cds].etag annotations...
if(options.isV4())
{
if (options.isV4() && (element['@odata.etag'] || element['@cds.etag'])) {
// eslint-disable-next-line sonarjs/no-redundant-boolean
if(element['@odata.etag'] == true || element['@cds.etag'] == true) {
// don't put element name into collection as per advice from Ralf Handl, as
// no runtime is interested in the property itself, it is sufficient to mark
// the entity set.
edmUtils.assignAnnotation(struct, '@Core.OptimisticConcurrency',
(struct['@Core.OptimisticConcurrency'] || [])/*.push(element.name)*/);
}
// don't put element name into collection as per advice from Ralf Handl, as
// no runtime is interested in the property itself, it is sufficient to mark
// the entity set.
edmUtils.assignAnnotation(struct, '@Core.OptimisticConcurrency',
(struct['@Core.OptimisticConcurrency'] || [])/* .push(element.name) */);
}
function AnalyticalAnnotations()
{
function mapCommonAttributes(element, struct, prop)
{
let CommonAttributes = element[prop];
if(!Array.isArray(CommonAttributes)) {
error(null, ['definitions', struct.name, 'elements', element.name],
{ anno: '@Common.Attributes', code: JSON.stringify(CommonAttributes) },
'Expecting array value for $(ANNO): $(CODE)');
function AnalyticalAnnotations() {
function mapCommonAttributes( elt, structure, prop ) {
const CommonAttributes = elt[prop];
if (!Array.isArray(CommonAttributes)) {
error(null, [ 'definitions', structure.name, 'elements', elt.name ],
{ anno: '@Common.Attributes', code: JSON.stringify(CommonAttributes) },
'Expecting array value for $(ANNO): $(CODE)');
return;
}
let targets = edmUtils.intersect(CommonAttributes, Object.keys(struct.elements));
targets.forEach(tgt => {
edmUtils.assignAnnotation(struct.elements[tgt], '@sap.attribute-for', element.name);
const targets = edmUtils.intersect(CommonAttributes, Object.keys(structure.elements));
targets.forEach((tgt) => {
edmUtils.assignAnnotation(structure.elements[tgt], '@sap.attribute-for', elt.name);
});
}
function mapContextDefiningProperties(element, struct, prop)
{
let ContextDefiningProperties = element[prop];
if(!Array.isArray(ContextDefiningProperties)) {
error(null, ['definitions', struct.name, 'elements', element.name],
{ anno: '@Aggregation.ContextDefiningProperties', code: JSON.stringify(ContextDefiningProperties) },
'Expecting array value for $(ANNO): $(CODE)');
function mapContextDefiningProperties( elt, structure, prop ) {
const ContextDefiningProperties = elt[prop];
if (!Array.isArray(ContextDefiningProperties)) {
error(null, [ 'definitions', structure.name, 'elements', elt.name ],
{ anno: '@Aggregation.ContextDefiningProperties', code: JSON.stringify(ContextDefiningProperties) },
'Expecting array value for $(ANNO): $(CODE)');
return;
}
if(ContextDefiningProperties.length > 0)
edmUtils.assignAnnotation(element, '@sap.super-ordinate', ContextDefiningProperties[ContextDefiningProperties.length-1]);
if (ContextDefiningProperties.length > 0)
edmUtils.assignAnnotation(elt, '@sap.super-ordinate', ContextDefiningProperties[ContextDefiningProperties.length - 1]);
}
let dict = Object.create(null);
//analytics term definition unknown, lower case
const dict = Object.create(null);
// analytics term definition unknown, lower case
dict['@Analytics.Measure'] = [ '@sap.aggregation-role', 'measure' ];

@@ -132,3 +117,5 @@ dict['@Analytics.Dimension'] = [ '@sap.aggregation-role', 'dimension' ];

// respect flattened annotation $value
Object.entries(dict).forEach(([k, v]) => dict[k+'.$value'] = v);
Object.entries(dict).forEach(([ k, v ]) => {
dict[`${k}.$value`] = v;
});
return dict;

@@ -138,53 +125,49 @@ }

function applyAppSpecificLateCsnTransformationOnStructure(options, struct, error)
{
if(options.isV2())
{
if(struct['@Aggregation.ApplySupported.PropertyRestrictions'])
{
transformAnalyticalModel(struct);
mapAnnotationAssignment(struct, undefined, AnalyticalAnnotations());
}
function applyAppSpecificLateCsnTransformationOnStructure( options, struct, error ) {
if (options.isV2() && struct['@Aggregation.ApplySupported.PropertyRestrictions']) {
transformAnalyticalModel(struct);
mapAnnotationAssignment(struct, undefined, AnalyticalAnnotations());
}
// nested functions begin
function transformAnalyticalModel(struct)
{
let keyName = 'ID__';
if(struct == undefined || struct.elements == undefined || struct.elements[keyName] != undefined)
function transformAnalyticalModel( structure ) {
const keyName = 'ID__';
if (!structure?.elements || structure.elements[keyName])
return;
// remove key prop from elements, add new key to elements
let elements = Object.create(null);
let key = { name: keyName, key : true, type : 'cds.String', '@sap.sortable':false, '@sap.filterable':false, '@UI.Hidden': true };
const elements = Object.create(null);
const key = {
name: keyName, key: true, type: 'cds.String', '@sap.sortable': false, '@sap.filterable': false, '@UI.Hidden': true,
};
elements[keyName] = key;
setProp(struct, '$keys',{ [keyName] : key } );
forEachGeneric(struct.items || struct, 'elements', (e,n) =>
{
if(e.key) delete e.key;
setProp(structure, '$keys', { [keyName]: key } );
forEachGeneric(structure.items || structure, 'elements', (e, n) => {
if (e.key)
delete e.key;
elements[n] = e;
});
struct.elements = elements;
structure.elements = elements;
}
function AnalyticalAnnotations()
{
function mapFilterRestrictions(struct, parent, prop)
{
let stringDict = Object.create(null);
stringDict['SingleValue'] = 'single-value';
stringDict['MultiValue'] = 'multi-value';
stringDict['SingleRange'] = 'interval';
function AnalyticalAnnotations() {
function mapFilterRestrictions( structure, parent, prop ) {
const stringDict = Object.create(null);
stringDict.SingleValue = 'single-value';
stringDict.MultiValue = 'multi-value';
stringDict.SingleRange = 'interval';
let filterRestrictions = struct[prop];
if(!Array.isArray(filterRestrictions)) {
error(null, ['definitions', struct.name ],
{ anno: '@Capabilities.FilterRestrictions.FilterExpressionRestrictions',
code: JSON.stringify(filterRestrictions) },
'Expected array value for $(ANNO): $(CODE)');
const filterRestrictions = structure[prop];
if (!Array.isArray(filterRestrictions)) {
error(null, [ 'definitions', structure.name ],
{
anno: '@Capabilities.FilterRestrictions.FilterExpressionRestrictions',
code: JSON.stringify(filterRestrictions),
},
'Expected array value for $(ANNO): $(CODE)');
return;
}
filterRestrictions.forEach(v => {
let e = struct.elements[v.Property];
if(e)
filterRestrictions.forEach((v) => {
const e = structure.elements[v.Property];
if (e)
edmUtils.assignAnnotation(e, '@sap.filter-restriction', stringDict[v.AllowedExpressions]);

@@ -194,28 +177,28 @@ });

function mapRequiredProperties(struct, parent, prop)
{
let requiredProperties = struct[prop];
if(!Array.isArray(requiredProperties)) {
error(null, ['definitions', struct.name],
{ anno: '@Capabilities.FilterRestrictions.RequiredProperties',
code: JSON.stringify(requiredProperties) },
'Expecting array value for $(ANNO): $(CODE)');
function mapRequiredProperties( structure, parent, prop ) {
const requiredProperties = structure[prop];
if (!Array.isArray(requiredProperties)) {
error(null, [ 'definitions', structure.name ],
{
anno: '@Capabilities.FilterRestrictions.RequiredProperties',
code: JSON.stringify(requiredProperties),
},
'Expecting array value for $(ANNO): $(CODE)');
return;
}
let props = edmUtils.intersect(Object.keys(struct.elements), requiredProperties)
props.forEach(p => {
edmUtils.assignAnnotation(struct.elements[p], '@sap.required-in-filter', true);
const props = edmUtils.intersect(Object.keys(structure.elements), requiredProperties);
props.forEach((p) => {
edmUtils.assignAnnotation(structure.elements[p], '@sap.required-in-filter', true);
});
}
function mapRequiresFilter(struct, parent, prop)
{
let requiresFilter = struct[prop];
if(requiresFilter)
edmUtils.assignAnnotation(struct._SetAttributes, '@sap.requires-filter', requiresFilter);
function mapRequiresFilter( structure, parent, prop ) {
const requiresFilter = structure[prop];
if (requiresFilter)
edmUtils.assignAnnotation(structure._SetAttributes, '@sap.requires-filter', requiresFilter);
}
// Entity Props
let dict = Object.create(null);
// Entity Props
const dict = Object.create(null);
dict['@Aggregation.ApplySupported.PropertyRestrictions'] = [ '@sap.semantics', 'aggregate' ];

@@ -228,3 +211,5 @@ dict['@Common.Label'] = [ '@sap.label' ];

// respect flattened annotation $value
Object.keys(dict).forEach(k => dict[k+'.$value'] = dict[k]);
Object.keys(dict).forEach((k) => {
dict[`${k}.$value`] = dict[k];
});

@@ -235,4 +220,4 @@ return dict;

function setSAPSpecificV2AnnotationsToEntityContainer(options, carrier) {
if(!options.isV2())
function setSAPSpecificV2AnnotationsToEntityContainer( options, carrier ) {
if (!options.isV2())
return;

@@ -242,3 +227,3 @@ // documented in https://wiki.scn.sap.com/wiki/display/EmTech/SAP+Annotations+for+OData+Version+2.0#SAPAnnotationsforODataVersion2.0-Elementedm:EntityContainer

// EntityContainer only
'@sap.supported.formats' : addToSetAttr,
'@sap.supported.formats': addToSetAttr,
'@sap.use.batch': addToSetAttr,

@@ -248,9 +233,9 @@ '@sap.message.scope.supported': addToSetAttr,

Object.entries(carrier).forEach(([p, v]) => {
(SetAttributes[p] || function() { /* no-op */ })(carrier, p, v);
Object.entries(carrier).forEach(([ p, v ]) => {
(SetAttributes[p] || function () { /* no-op */ })(carrier, p, v); // eslint-disable-line func-names
});
}
function setSAPSpecificV2AnnotationsToEntitySet(options, carrier) {
if(!options.isV2())
function setSAPSpecificV2AnnotationsToEntitySet( options, carrier ) {
if (!options.isV2())
return;

@@ -260,11 +245,13 @@ // documented in https://wiki.scn.sap.com/wiki/display/EmTech/SAP+Annotations+for+OData+Version+2.0#SAPAnnotationsforODataVersion2.0-Elementedm:EntitySet

// EntitySet, EntityType
'@sap.label' : (s,pn, pv) => { addToSetAttr(s, pn, pv, false); },
'@sap.label': (s, pn, pv) => {
addToSetAttr(s, pn, pv, false);
},
'@sap.semantics': checkSemantics,
// EntitySet only
'@sap.creatable' : addToSetAttr,
'@sap.updatable' : addToSetAttr,
'@sap.creatable': addToSetAttr,
'@sap.updatable': addToSetAttr,
'@sap.deletable': addToSetAttr,
'@sap.updatable.path': addToSetAttr,
'@sap.deletable.path': addToSetAttr,
'@sap.searchable' : addToSetAttr,
'@sap.searchable': addToSetAttr,
'@sap.pagable': addToSetAttr,

@@ -280,8 +267,8 @@ '@sap.topable': addToSetAttr,

Object.entries(carrier).forEach(([p, v]) => {
(SetAttributes[p] || function() { /* no-op */ })(carrier, p, v);
Object.entries(carrier).forEach(([ p, v ]) => {
(SetAttributes[p] || function () { /* no-op */ })(carrier, p, v); // eslint-disable-line func-names
});
function checkSemantics(struct, propName, propValue) {
if(propValue === 'timeseries' || propValue === 'aggregate') {
function checkSemantics( struct, propName, propValue ) {
if (propValue === 'timeseries' || propValue === 'aggregate') {
// aggregate is forwarded to Set and must remain on Type

@@ -293,9 +280,11 @@ addToSetAttr(struct, propName, propValue, propValue !== 'aggregate');

function setSAPSpecificV2AnnotationsToAssociation(carrier) {
function setSAPSpecificV2AnnotationsToAssociation( carrier ) {
// documented in https://wiki.scn.sap.com/wiki/display/EmTech/SAP+Annotations+for+OData+Version+2.0
const SetAttributes = {
// Applicable to NavProp and foreign keys, add to AssociationSet
'@sap.creatable' : (c, pn, pv) => { addToAssociationSet(c, pn, pv, false); },
'@sap.creatable': (c, pn, pv) => {
addToAssociationSet(c, pn, pv, false);
},
// Not applicable to NavProp, applicable to foreign keys, add to AssociationSet
'@sap.updatable' : addToAssociationSet,
'@sap.updatable': addToAssociationSet,
// Not applicable to NavProp, not applicable to foreign key, add to AssociationSet

@@ -311,20 +300,18 @@ '@sap.deletable': (c, pn, pv) => {

Object.entries(carrier).forEach(([p, v]) => {
(SetAttributes[p] || function() {/* no-op */})(carrier, p, v);
Object.entries(carrier).forEach(([ p, v ]) => {
(SetAttributes[p] || function () { /* no-op */ })(carrier, p, v); // eslint-disable-line func-names
});
function addToAssociationSet(carrier, propName, propValue, removeFromType=true) {
if(carrier.target) {
edmUtils.assignProp(carrier, '_SetAttributes', Object.create(null));
edmUtils.assignAnnotation(carrier._SetAttributes, propName, propValue);
if(removeFromType) {
delete carrier[propName];
}
function addToAssociationSet( target, propName, propValue, removeFromType = true ) {
if (target.target) {
edmUtils.assignProp(target, '_SetAttributes', Object.create(null));
edmUtils.assignAnnotation(target._SetAttributes, propName, propValue);
if (removeFromType)
delete target[propName];
}
}
function removeFromForeignKey(carrier, propName) {
if(carrier['@odata.foreignKey4'] && carrier[propName] !== undefined) {
delete carrier[propName];
}
function removeFromForeignKey( target, propName ) {
if (target['@odata.foreignKey4'] && target[propName] !== undefined)
delete target[propName];
}

@@ -334,3 +321,2 @@ }

module.exports = {

@@ -341,3 +327,3 @@ applyAppSpecificLateCsnTransformationOnElement,

setSAPSpecificV2AnnotationsToEntitySet,
setSAPSpecificV2AnnotationsToAssociation
setSAPSpecificV2AnnotationsToAssociation,
};

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

// eslint-disable-next-line no-unused-vars
function resolveForeignKeyRefs(csn, csnUtils) {
function resolveForeignKeyRefs( csn, csnUtils ) {
forEachDefinition(csn, (def, defName) => {
let currPath = ['definitions', defName ];
const currPath = [ 'definitions', defName ];
forEachMemberRecursively(def, (construct, _constructName, _prop, path) => {
if(construct.target && construct.keys) {
if (construct.target && construct.keys) {
construct.keys.forEach((fk, i) => {
setProp(fk, '_artifact', csnUtils.inspectRef([...path, 'keys', i]).art);
setProp(fk, '_artifact', csnUtils.inspectRef([ ...path, 'keys', i ]).art);
});

@@ -24,4 +24,4 @@ }

function inboundQualificationChecks(csn, options, messageFunctions,
serviceRootNames, requestedServiceNames, isMyServiceRequested, whatsMyServiceRootName, csnUtils) {
function inboundQualificationChecks( csn, options, messageFunctions,
serviceRootNames, requestedServiceNames, isMyServiceRequested, whatsMyServiceRootName, csnUtils ) {
const { message, throwWithError } = messageFunctions;

@@ -33,22 +33,22 @@

// attach $path to all
function attach$path(def, defName) {
// attach $path to all
function attach$path( def, defName ) {
setProp(def, '$path', [ 'definitions', defName ]);
forEachMemberRecursively(def,
(member, _memberName, _prop, path) => {
setProp(member, '$path', path);
}, [ 'definitions', defName ]);
(member, _memberName, _prop, path) => {
setProp(member, '$path', path);
}, [ 'definitions', defName ]);
}
function checkProperArrayUsage(def, defName) {
function checkProperArrayUsage( def, defName ) {
if (!isMyServiceRequested(defName))
return;
let currPath = ['definitions', defName];
const currPath = [ 'definitions', defName ];
checkIfItemsOfItems(def, undefined, undefined, currPath);
forEachMemberRecursively(def, checkIfItemsOfItems, currPath);
function checkIfItemsOfItems(construct, _constructName, _prop, path) {
function checkIfItemsOfItems( construct, _constructName, _prop, path ) {
const constructType = csnUtils.effectiveType(construct);
if (constructType.items) {
if(constructType.items.target) {
if (constructType.items.target) {
const isComp = constructType.items.type === 'cds.Composition';

@@ -71,16 +71,18 @@ message('type-invalid-items', path, { '#': isComp ? 'comp' : 'assoc', prop: 'items' });

function checkNestedContextsAndServices() {
!isBetaEnabled(options, 'nestedServices') && serviceRootNames.forEach(sn => {
const parent = whatsMyServiceRootName(sn, false);
if(parent && requestedServiceNames.includes(parent) && parent !== sn) {
message( 'service-nested-service', [ 'definitions', sn ], { art: parent },
'A service can\'t be nested within a service $(ART)' );
}
});
if (!isBetaEnabled(options, 'nestedServices')) {
serviceRootNames.forEach((sn) => {
const parent = whatsMyServiceRootName(sn, false);
if (parent && requestedServiceNames.includes(parent) && parent !== sn) {
message( 'service-nested-service', [ 'definitions', sn ], { art: parent },
'A service can\'t be nested within a service $(ART)' );
}
});
}
Object.entries(csn.definitions).forEach(([fqName, art]) => {
if(art.kind === 'context') {
Object.entries(csn.definitions).forEach(([ fqName, art ]) => {
if (art.kind === 'context') {
const parent = whatsMyServiceRootName(fqName);
if(requestedServiceNames.includes(parent)) {
if (requestedServiceNames.includes(parent)) {
message( 'service-nested-context', [ 'definitions', fqName ], { art: parent },
'A context can\'t be nested within a service $(ART)' );
'A context can\'t be nested within a service $(ART)' );
}

@@ -92,2 +94,2 @@ }

module.exports = { inboundQualificationChecks }
module.exports = { inboundQualificationChecks };
'use strict';
const { setProp } = require('../base/model');
const { isBuiltinType, isEdmPropertyRendered, applyTransformations, cloneAnnotationValue } = require('../model/csnUtils');
const {
isBuiltinType, isEdmPropertyRendered, applyTransformations, cloneAnnotationValue,
} = require('../model/csnUtils');
const { escapeString, hasControlCharacters, hasUnpairedUnicodeSurrogate } = require('../render/utils/stringEscapes');
const {CompilerAssertion} = require('../base/error');
const { CompilerAssertion } = require('../base/error');
/* eslint max-statements-per-line:off */
function validateOptions(_options)
{
if(!_options.isV2 && !_options.isV4)
{
function validateOptions( _options ) {
if (!_options.isV2 && !_options.isV4) {
// csn2edm expects "odataVersion" to be a top-level property of options
// set to 'v4' as default, override with value from incoming options
const options = Object.assign({ odataVersion: 'v4'}, _options);
const options = Object.assign({ odataVersion: 'v4' }, _options);
// global flag that indicates whether or not FKs shall be rendered in general
// V2/V4 flat: yes
// V4/struct: depending on odataForeignKeys
options.renderForeignKeys =
options.odataVersion === 'v4' ? options.odataFormat === 'structured' && !!options.odataForeignKeys : true;
options.renderForeignKeys
= options.odataVersion === 'v4' ? options.odataFormat === 'structured' && !!options.odataForeignKeys : true;

@@ -24,3 +25,3 @@ const v2 = options.odataVersion.match(/v2/i) !== null;

options.v = [v2, v4];
options.v = [ v2, v4 ];
options.isStructFormat = options.odataFormat && options.odataFormat === 'structured';

@@ -36,22 +37,22 @@ options.isFlatFormat = !options.isStructFormat;

}
else
return _options;
return _options;
}
// returns intersection of two arrays
function intersect(a,b)
{
return [...new Set(a)].filter(x => new Set(b).has(x));
function intersect( a, b ) {
return [ ...new Set(a) ].filter(x => new Set(b).has(x));
}
// Call func(art, name) for each artifact 'art' with name 'name' in 'dictionary' that returns true for 'filter(art)'
function foreach(dictionary, filter, func) {
dictionary && Object.entries(dictionary).forEach(([name, value]) => {
if (filter(value)) {
if(Array.isArray(func))
func.forEach(f=>f(value, name));
else
function foreach( dictionary, filter, func ) {
if (dictionary) {
Object.entries(dictionary).forEach(([ name, value ]) => {
if (filter(value)) {
if (Array.isArray(func))
func.forEach(f => f(value, name));
else
func(value, name);
}
});
}
});
}
}

@@ -61,24 +62,24 @@

// or if artifact belongs to an artificial parameter entity
function isContainee(artifact) {
function isContainee( artifact ) {
// if $containerNames is present, it is guaranteed that it has at least one entry
return (artifact.$containerNames && (artifact.$containerNames.length > 1 || artifact.$containerNames[0] != artifact.name));
return (artifact.$containerNames && (artifact.$containerNames.length > 1 || artifact.$containerNames[0] !== artifact.name));
}
// Return true if the association 'assoc' has cardinality 'to-many'
function isToMany(assoc) {
if (!assoc.cardinality) {
function isToMany( assoc ) {
if (!assoc.cardinality)
return false;
}
// Different representations possible: array or targetMax property
let targetMax = assoc.cardinality[1] ||assoc.cardinality.max;
if (!targetMax) {
const targetMax = assoc.cardinality[1] || assoc.cardinality.max;
if (!targetMax)
return false;
}
return targetMax === '*' || Number(targetMax) > 1;
}
function isNavigable(assoc) {
function isNavigable( assoc ) {
return (assoc.target && (assoc['@odata.navigable'] == null || assoc['@odata.navigable']));
}
function isSingleton(entityCsn) {
function isSingleton( entityCsn ) {
const singleton = entityCsn['@odata.singleton'];

@@ -89,3 +90,3 @@ const hasNullable = entityCsn['@odata.singleton.nullable'] != null;

function isParameterizedEntity(artifact) {
function isParameterizedEntity( artifact ) {
return artifact.kind === 'entity' && artifact.params;

@@ -95,3 +96,3 @@ }

// Return true if 'artifact' is structured (i.e. has elements, like a structured type or an entity)
function isStructuredArtifact(artifact) {
function isStructuredArtifact( artifact ) {
// FIXME: No derived types etc yet

@@ -102,15 +103,14 @@ return (artifact.items && artifact.items.elements || artifact.elements);

// Return true if 'artifact' is a real structured type (not an entity)
function isStructuredType(artifact) {
function isStructuredType( artifact ) {
return artifact.kind === 'type' && isStructuredArtifact(artifact);
}
function isDerivedType(artifact) {
function isDerivedType( artifact ) {
return artifact.kind === 'type' && !isStructuredArtifact(artifact);
}
function resolveOnConditionAndPrepareConstraints(csn, assocCsn, messageFunctions) {
function resolveOnConditionAndPrepareConstraints( csn, assocCsn, messageFunctions ) {
const { info, warning } = messageFunctions;
if(assocCsn.on)
{
if (assocCsn.on) {
// fill constraint array with [prop, depProp]

@@ -133,3 +133,3 @@ getExpressionArguments(assocCsn.on);

*/
assocCsn._constraints.selfs.filter(p => p).forEach(partnerPath => {
assocCsn._constraints.selfs.filter(p => p).forEach((partnerPath) => {
// resolve partner path in target

@@ -139,28 +139,27 @@ const originAssocCsn = resolveOriginAssoc(csn, (assocCsn._originalTarget || assocCsn._target), partnerPath);

const parent = csn.definitions[parentName];
if(originAssocCsn && originAssocCsn.$abspath) {
if (originAssocCsn && originAssocCsn.$abspath) {
const originParentName = originAssocCsn.$abspath[0];
if(parent.$mySchemaName && originAssocCsn._originalTarget !== parent && originAssocCsn._target !== parent) {
if (parent.$mySchemaName && originAssocCsn._originalTarget !== parent && originAssocCsn._target !== parent) {
isBacklink = false;
// Partnership is ambiguous
setProp(originAssocCsn, '$noPartner', true);
info(null, ['definitions', parentName, 'elements', assocCsn.name],
`"${originParentName}:${partnerPath.join('.')}" with target "${originAssocCsn._target.name}" is compared with $self which represents "${parentName}"`);
info(null, [ 'definitions', parentName, 'elements', assocCsn.name ],
`"${originParentName}:${partnerPath.join('.')}" with target "${originAssocCsn._target.name}" is compared with $self which represents "${parentName}"`);
}
if(originAssocCsn.target) {
if (originAssocCsn.target) {
// Mark this association as backlink if $self appears exactly once
// to suppress edm:Association generation in V2 mode
if(isBacklink) {
if (isBacklink) {
// establish partnership with origin assoc but only if this association is the first one
if(originAssocCsn._selfReferences.length === 0) {
if (originAssocCsn._selfReferences.length === 0)
assocCsn._constraints._partnerCsn = originAssocCsn;
}
else {
else
isBacklink = false;
}
}
// store all backlinks at forward, required to calculate rendering of foreign keys
// if the termCount != 1 or more than one $self compare this is not a backlink
if(parent.$mySchemaName && assocCsn._constraints.selfs.length === 1 && assocCsn._constraints.termCount === 1) {
if (parent.$mySchemaName && assocCsn._constraints.selfs.length === 1 && assocCsn._constraints.termCount === 1)
originAssocCsn._selfReferences.push(assocCsn);
}
assocCsn._constraints._origins.push(originAssocCsn);

@@ -174,10 +173,9 @@ }

*/
throw new CompilerAssertion('Backlink association element is not an association or composition: "' + originAssocCsn.name);
throw new CompilerAssertion(`Backlink association element is not an association or composition: "${originAssocCsn.name}`);
}
}
else
{
warning(null, ['definitions', parentName],
{ partner: `${assocCsn._target.name}/${partnerPath}`, name: `${parentName}/${assocCsn.name}` },
'Can\'t resolve backlink to $(PARTNER) from $(NAME)');
else {
warning(null, [ 'definitions', parentName ],
{ partner: `${assocCsn._target.name}/${partnerPath}`, name: `${parentName}/${assocCsn.name}` },
'Can\'t resolve backlink to $(PARTNER) from $(NAME)');
}

@@ -188,16 +186,14 @@ });

// nested functions
function getExpressionArguments(expr)
{
let allowedTokens = [ '=', 'and', '(', ')' ];
if(expr && Array.isArray(expr))
// if some returns true, this term is not usable as a constraint term
if(!expr.some(isNotAConstraintTerm))
expr.forEach(fillConstraints)
function getExpressionArguments( expr ) {
const allowedTokens = [ '=', 'and', '(', ')' ];
if (expr && Array.isArray(expr) && !expr.some(isNotAConstraintTerm))
// if some returns true, this term is not usable as a constraint term
expr.forEach(fillConstraints);
// return true if token is not one of '=', 'and', '(', ')' or object
function isNotAConstraintTerm(tok)
{
if(tok.xpr)
function isNotAConstraintTerm( tok ) {
if (tok.xpr)
return tok.xpr.some(isNotAConstraintTerm);
if(Array.isArray(tok))
if (Array.isArray(tok))
return tok.some(isNotAConstraintTerm);

@@ -208,15 +204,12 @@ return !(typeof tok === 'object' && tok != null || allowedTokens.includes(tok));

// fill constraints object with [dependent, principal] pairs and collect all forward assocs for $self terms
function fillConstraints(arg, pos)
{
if(arg.xpr)
function fillConstraints( arg, pos ) {
if (arg.xpr) {
getExpressionArguments(arg.xpr);
else if(pos > 0 && pos < expr.length)
{
let lhs = expr[pos-1];
let rhs = expr[pos+1];
if(arg === '=')
{
}
else if (pos > 0 && pos < expr.length) {
let lhs = expr[pos - 1];
let rhs = expr[pos + 1];
if (arg === '=') {
assocCsn._constraints.termCount++;
if(lhs.ref && rhs.ref) // ref is a path
{
if (lhs.ref && rhs.ref) { // ref is a path
lhs = lhs.ref;

@@ -227,24 +220,24 @@ rhs = rhs.ref;

// strip of prefix '$self's
if(lhs[0] === '$self' && lhs.length > 1)
if (lhs[0] === '$self' && lhs.length > 1)
lhs = lhs.slice(1);
if(rhs[0] === '$self' && rhs.length > 1)
if (rhs[0] === '$self' && rhs.length > 1)
rhs = rhs.slice(1);
if((lhs[0] === assocCsn.name && rhs[0] !== assocCsn.name) ||
(lhs[0] !== assocCsn.name && rhs[0] === assocCsn.name))
{
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 ]
// backlink [ self, assocName ]
let c;
if(lhs[0] === assocCsn.name)
c = [rhs, lhs.slice(1)];
if (lhs[0] === assocCsn.name)
c = [ rhs, lhs.slice(1) ];
else
c = [lhs, rhs.slice(1)];
c = [ lhs, rhs.slice(1) ];
// do we have a $self id?
// if so, store partner in selfs array
if(c[0][0] === '$self' && c[0].length === 1) {
if (c[0][0] === '$self' && c[0].length === 1) {
assocCsn._constraints.selfs.push(c[1]);
} else {
}
else {
const key = c.join(',');

@@ -261,4 +254,3 @@ assocCsn._constraints.constraints[key] = c;

function finalizeReferentialConstraints(csn, assocCsn, options, info)
{
function finalizeReferentialConstraints( csn, assocCsn, options, info ) {
if (assocCsn.on) {

@@ -276,11 +268,10 @@ /* example for originalTarget:

*/
assocCsn._constraints._origins.forEach(originAssocCsn => {
assocCsn._constraints._origins.forEach((originAssocCsn) => {
// if the origin assoc is marked as primary key and if it's managed, add all its foreign keys as constraint
// as they are also primary keys of the origin entity as well
if(!assocCsn._target.$isParamEntity && originAssocCsn.key && originAssocCsn.keys) {
for(let fk of originAssocCsn.keys) {
let realFk = originAssocCsn._parent.elements[fk.$generatedFieldName];
let pk = assocCsn._parent.elements[fk.ref[0]];
if(isConstraintCandidate(pk) && isConstraintCandidate(realFk))
{
if (!assocCsn._target.$isParamEntity && originAssocCsn.key && originAssocCsn.keys) {
for (const fk of originAssocCsn.keys) {
const realFk = originAssocCsn._parent.elements[fk.$generatedFieldName];
const pk = assocCsn._parent.elements[fk.ref[0]];
if (isConstraintCandidate(pk) && isConstraintCandidate(realFk)) {
const c = [ [ fk.ref[0] ], [ fk.$generatedFieldName ] ];

@@ -294,10 +285,10 @@ const key = c.join(',');

if(!assocCsn._target.$isParamEntity) {
if (!assocCsn._target.$isParamEntity) {
// Use $path to identify main artifact in case assocs parent was a nested type and deanonymized
// Some (draft) associations don't have a $path, use _parent as last resort
let dependentEntity = assocCsn.$path ? csn.definitions[assocCsn.$path[1]] : assocCsn._parent;
let localDepEntity = assocCsn._parent;
let localDepEntity = assocCsn._parent;
// _target must always be a main artifact
let principalEntity = assocCsn._target;
if(assocCsn.type === 'cds.Composition') {
if (assocCsn.type === 'cds.Composition') {
// Header is composed of Items => Cds.Composition: Header is principal => use header's primary keys

@@ -308,4 +299,5 @@ principalEntity = dependentEntity;

// Swap the constraint elements to be correct on Composition [principal, dependent] => [dependent, principal]
Object.keys(assocCsn._constraints.constraints).forEach(cn => {
assocCsn._constraints.constraints[cn] = [ assocCsn._constraints.constraints[cn][1], assocCsn._constraints.constraints[cn][0] ] } );
Object.keys(assocCsn._constraints.constraints).forEach((cn) => {
assocCsn._constraints.constraints[cn] = [ assocCsn._constraints.constraints[cn][1], assocCsn._constraints.constraints[cn][0] ];
} );
}

@@ -316,52 +308,53 @@ // Remove all target elements that are not key in the principal entity

foreach(assocCsn._constraints.constraints,
c => {
// rc === true will remove the constraint (positive filter expression)
let rc = true;
// concatenate all paths in flat mode to identify the correct element
// in structured mode only resolve top level element (path rewriting is done elsewhere)
const depEltName = ( options.isFlatFormat ? c[0].join('_') : c[0][0] );
const principalEltName = ( options.isFlatFormat ? c[1].join('_') : c[1][0] );
const fk = (dependentEntity.kind === 'entity' && dependentEntity.elements[ depEltName ]) ||
(localDepEntity && localDepEntity.elements && localDepEntity.elements[ depEltName ]);
const pk = principalEntity.$keys && principalEntity.$keys[ principalEltName ];
if(isConstraintCandidate(fk) && isConstraintCandidate(pk)) {
if(options.isStructFormat) {
// In structured mode it might be the association has a new _parent due to
// type de-anonymization.
// There are three cases for dependent ON condition paths:
// 1) path is relative to assoc in same sub structure
// 2) path is absolute and ends up in a different environment
// 3) path is absolute and touches in assoc's environment
(c) => {
// rc === true will remove the constraint (positive filter expression)
let rc = true;
// concatenate all paths in flat mode to identify the correct element
// in structured mode only resolve top level element (path rewriting is done elsewhere)
const depEltName = ( options.isFlatFormat ? c[0].join('_') : c[0][0] );
const principalEltName = ( options.isFlatFormat ? c[1].join('_') : c[1][0] );
const fk = (dependentEntity.kind === 'entity' && dependentEntity.elements[depEltName]) ||
(localDepEntity && localDepEntity.elements && localDepEntity.elements[depEltName]);
const pk = principalEntity.$keys && principalEntity.$keys[principalEltName];
if (isConstraintCandidate(fk) && isConstraintCandidate(pk)) {
if (options.isStructFormat) {
// In structured mode it might be the association has a new _parent due to
// type de-anonymization.
// There are three cases for dependent ON condition paths:
// 1) path is relative to assoc in same sub structure
// 2) path is absolute and ends up in a different environment
// 3) path is absolute and touches in assoc's environment
// => 1) if _parents are equal, fk path is relative to assoc
if(fk._parent === assocCsn._parent) {
rc = false;
}
// => 2) & 3) if path is not relative to assoc, remove main entity (pos=0) and assoc (pos=n-1)
// and check path identity: If absolute path touches assoc's _parent, add it
else if(!assocCsn.$abspath.slice(1, assocCsn.$abspath.length-1).some((p,i) => c[0][i] !== p)) {
// this was an absolute addressed path, remove environment prefix
c[0].splice(0, assocCsn.$abspath.length-2);
rc = false;
}
}
else {
// for flat mode isConstraintCandidate(fk) && isConstraintCandidate(pk) is sufficient
rc = false;
}
}
if(!rc)
remainingPrincipalRefs.push(principalEltName);
return rc;
},
(c, cn) => { delete assocCsn._constraints.constraints[cn]; }
);
// => 1) if _parents are equal, fk path is relative to assoc
if (fk._parent === assocCsn._parent) {
rc = false;
}
// => 2) & 3) if path is not relative to assoc, remove main entity (pos=0) and assoc (pos=n-1)
// and check path identity: If absolute path touches assoc's _parent, add it
else if (!assocCsn.$abspath.slice(1, assocCsn.$abspath.length - 1).some((p, i) => c[0][i] !== p)) {
// this was an absolute addressed path, remove environment prefix
c[0].splice(0, assocCsn.$abspath.length - 2);
rc = false;
}
}
else {
// for flat mode isConstraintCandidate(fk) && isConstraintCandidate(pk) is sufficient
rc = false;
}
}
if (!rc)
remainingPrincipalRefs.push(principalEltName);
return rc;
},
(c, cn) => {
delete assocCsn._constraints.constraints[cn];
});
// V2 check that ALL primary keys are constraints
if(principalEntity.$keys) {
const renderedKeys = Object.values(principalEntity.$keys).filter(isConstraintCandidate).map(v=>v.name);
if(options.isV2() && intersect(renderedKeys, remainingPrincipalRefs).length !== renderedKeys.length)
if(options.odataV2PartialConstr) {
if (principalEntity.$keys) {
const renderedKeys = Object.values(principalEntity.$keys).filter(isConstraintCandidate).map(v => v.name);
if (options.isV2() && intersect(renderedKeys, remainingPrincipalRefs).length !== renderedKeys.length) {
if (options.odataV2PartialConstr) {
info('odata-spec-violation-constraints',
['definitions', assocCsn._parent.name, 'elements', assocCsn.name], { version: '2.0' });
[ 'definitions', assocCsn._parent.name, 'elements', assocCsn.name ], { version: '2.0' });
}

@@ -371,9 +364,8 @@ else {

}
}
}
}
}
// Handle managed association, a managed composition is treated as association
else
{
else if (!assocCsn._target.$isParamEntity && assocCsn.keys) {
// If FK is key in target => constraint

@@ -386,27 +378,24 @@ // Don't consider primary key associations (fks become keys on the source entity) as

// there are no constraints for them.
if(!assocCsn._target.$isParamEntity && assocCsn.keys) {
const remainingPrincipalRefs = [];
for(let fk of assocCsn.keys) {
let realFk = assocCsn._parent.items ? assocCsn._parent.items.elements[fk.$generatedFieldName] : assocCsn._parent.elements[fk.$generatedFieldName];
let pk = assocCsn._target.elements[fk.ref[0]];
if(pk && pk.key && isConstraintCandidate(pk) && isConstraintCandidate(realFk))
{
remainingPrincipalRefs.push(fk.ref[0]);
const c = [ [ fk.$generatedFieldName ], [ fk.ref[0] ] ];
const key = c.join(',');
assocCsn._constraints.constraints[key] = c;
}
const remainingPrincipalRefs = [];
for (const fk of assocCsn.keys) {
const realFk = assocCsn._parent.items ? assocCsn._parent.items.elements[fk.$generatedFieldName] : assocCsn._parent.elements[fk.$generatedFieldName];
const pk = assocCsn._target.elements[fk.ref[0]];
if (pk && pk.key && isConstraintCandidate(pk) && isConstraintCandidate(realFk)) {
remainingPrincipalRefs.push(fk.ref[0]);
const c = [ [ fk.$generatedFieldName ], [ fk.ref[0] ] ];
const key = c.join(',');
assocCsn._constraints.constraints[key] = c;
}
}
// V2 check that ALL primary keys are constraints
const renderedKeys = Object.values(assocCsn._target.$keys).filter(isConstraintCandidate).map(v=>v.name);
if(options.isV2() && intersect(renderedKeys, remainingPrincipalRefs).length !== renderedKeys.length) {
if(options.odataV2PartialConstr) {
info('odata-spec-violation-constraints',
['definitions', assocCsn._parent.name, 'elements', assocCsn.name], { version: '2.0' } );
}
else {
assocCsn._constraints.constraints = {};
}
// V2 check that ALL primary keys are constraints
const renderedKeys = Object.values(assocCsn._target.$keys).filter(isConstraintCandidate).map(v => v.name);
if (options.isV2() && intersect(renderedKeys, remainingPrincipalRefs).length !== renderedKeys.length) {
if (options.odataV2PartialConstr) {
info('odata-spec-violation-constraints',
[ 'definitions', assocCsn._parent.name, 'elements', assocCsn.name ], { version: '2.0' } );
}
else {
assocCsn._constraints.constraints = {};
}
}

@@ -417,6 +406,5 @@ }

// continue with multiplicity
if(assocCsn._target.$isParamEntity)
{
if (assocCsn._target.$isParamEntity)
assocCsn._constraints.constraints = Object.create(null);
}
return assocCsn._constraints;

@@ -431,3 +419,3 @@

*/
function isConstraintCandidate(elt) {
function isConstraintCandidate( elt ) {
return (elt &&

@@ -441,4 +429,3 @@ elt.type &&

function determineMultiplicity(csn)
{
function determineMultiplicity( csn ) {
/*

@@ -475,26 +462,26 @@ => SRC Cardinality

const isAssoc = csn.type === 'cds.Association';
if(!csn.cardinality)
if (!csn.cardinality)
csn.cardinality = Object.create(null);
if(!csn.cardinality.src)
if (!csn.cardinality.src)
csn.cardinality.src = isAssoc ? '*' : '1';
if(!csn.cardinality.min)
if (!csn.cardinality.min)
csn.cardinality.min = 0;
if(!csn.cardinality.max)
if (!csn.cardinality.max)
csn.cardinality.max = 1;
const srcCardinality =
(csn.cardinality.src == 1)
? (!isAssoc || csn.cardinality.srcmin == 1)
? '1'
: '0..1'
const srcCardinality
= (csn.cardinality.src == 1) // eslint-disable-line eqeqeq
? (!isAssoc || csn.cardinality.srcmin == 1) // eslint-disable-line eqeqeq
? '1'
: '0..1'
: '*';
const tgtCardinality =
(csn.cardinality.max > 1 || csn.cardinality.max === '*')
const tgtCardinality
= (csn.cardinality.max > 1 || csn.cardinality.max === '*')
? '*'
: (csn.cardinality.min == 1)
: (csn.cardinality.min == 1) // eslint-disable-line eqeqeq
? '1'
: '0..1';
return [srcCardinality, tgtCardinality];
return [ srcCardinality, tgtCardinality ];
}

@@ -506,18 +493,18 @@

// This function works only after finalizeConstraints
function getEffectiveTargetCardinality(csn) {
function getEffectiveTargetCardinality( csn ) {
const rc = { min: 0, max: 1 };
if(!csn._constraints || !csn._constraints.$finalized)
throw new CompilerAssertion('_constraints missing or not finalized: "' + csn.name);
if (!csn._constraints || !csn._constraints.$finalized)
throw new CompilerAssertion(`_constraints missing or not finalized: "${csn.name}`);
// partner (forward) cardinality has precedence
if(csn._constraints._partnerCsn) {
if(csn._constraints._partnerCsn.cardinality?.srcmin)
if (csn._constraints._partnerCsn) {
if (csn._constraints._partnerCsn.cardinality?.srcmin)
rc.min = csn._constraints._partnerCsn.cardinality.srcmin;
if(csn._constraints._partnerCsn.cardinality?.src)
if (csn._constraints._partnerCsn.cardinality?.src)
rc.max = csn._constraints._partnerCsn.cardinality.src;
}
else if(csn.cardinality) {
if(csn.cardinality.min)
else if (csn.cardinality) {
if (csn.cardinality.min)
rc.min = csn.cardinality.min;
if(csn.cardinality.max)
rc.max = csn.cardinality.max
if (csn.cardinality.max)
rc.max = csn.cardinality.max;
}

@@ -527,13 +514,12 @@ return rc;

function mapCdsToEdmType(csn, messageFunctions, isV2=false, isMediaType=false, location=undefined)
{
if(location === undefined)
function mapCdsToEdmType( csn, messageFunctions, isV2 = false, isMediaType = false, location = undefined ) {
if (location === undefined)
location = csn.$path;
const { error } = messageFunctions || { error: ()=>true };
let cdsType = csn.type;
if(cdsType === undefined) {
const { error } = messageFunctions || { error: () => true };
const cdsType = csn.type;
if (cdsType === undefined) {
error(null, location, 'no type found');
return '<NOTYPE>';
}
if(!isBuiltinType(cdsType))
if (!isBuiltinType(cdsType))
return cdsType;

@@ -593,7 +579,6 @@

}[cdsType];
if (edmType == undefined) {
if (!edmType)
error(null, location, { type: cdsType }, 'No EDM type available for $(TYPE)');
}
if(isV2)
{
if (isV2) {
if (edmType === 'Edm.Date')

@@ -604,7 +589,5 @@ edmType = 'Edm.DateTime';

}
else // isV4
{
// CDXCORE-CDXCORE-173
if(isMediaType)
edmType = 'Edm.Stream';
else if (isMediaType) { // isV4
// CDXCORE-CDXCORE-173
edmType = 'Edm.Stream';
}

@@ -614,6 +597,5 @@ return edmType;

function addTypeFacets(node, csn)
{
function addTypeFacets( node, csn ) {
const isV2 = node.v2;
const decimalTypes = {'cds.Decimal':1, 'cds.DecimalFloat':1, 'cds.hana.SMALLDECIMAL':1};
const decimalTypes = { 'cds.Decimal': 1, 'cds.DecimalFloat': 1, 'cds.hana.SMALLDECIMAL': 1 };
if (csn.length != null)

@@ -632,6 +614,6 @@ node.setEdmAttribute('MaxLength', csn.length);

node.setEdmAttribute('Precision', 7);
if(csn.type in decimalTypes) {
if(isV2) {
if (csn.type in decimalTypes) {
if (isV2) {
// no prec/scale or scale is 'floating'/'variable'
if(!(csn.precision || csn.scale) || (csn.scale === 'floating' || csn.scale === 'variable')) {
if (!(csn.precision || csn.scale) || (csn.scale === 'floating' || csn.scale === 'variable')) {
node.setXml( { 'sap:variable-scale': true } );

@@ -643,5 +625,5 @@ node.removeEdmAttribute('Scale');

// map both floating and variable to => variable
if(node._edmAttributes.Scale === 'floating')
if (node._edmAttributes.Scale === 'floating')
node.setEdmAttribute('Scale', 'variable');
if(!csn.precision && !csn.scale)
if (csn.precision == null && csn.scale == null)
// if Decimal has no p, s set scale 'variable'

@@ -652,5 +634,5 @@ node.setXml( { Scale: 'variable' } ); // floating is V4.01

// Unicode unused today
if(csn.unicode)
if (csn.unicode)
node.setEdmAttribute('Unicode', csn.unicode);
if(csn.srid)
if (csn.srid)
node.setEdmAttribute('SRID', csn.srid);

@@ -660,3 +642,3 @@ }

/**
/**
* A simple identifier is a Unicode character sequence with the following restrictions:

@@ -676,5 +658,5 @@ * - The first character MUST be the underscore character (U+005F) or any character in the Unicode category “Letter (L)” or “Letter number (Nl)”

*/
function isODataSimpleIdentifier(identifier){
function isODataSimpleIdentifier( identifier ) {
// this regular expression reflects the specification from above
const regex = /^[\p{Letter}\p{Nl}_][_\p{Letter}\p{Nl}\p{Nd}\p{Mn}\p{Mc}\p{Pc}\p{Cf}]{0,127}$/gu
const regex = /^[\p{Letter}\p{Nl}_][_\p{Letter}\p{Nl}\p{Nd}\p{Mn}\p{Mc}\p{Pc}\p{Cf}]{0,127}$/gu;
return identifier && identifier.match(regex);

@@ -705,3 +687,3 @@ }

*/
function escapeStringForAttributeValue(str) {
function escapeStringForAttributeValue( str ) {
if (typeof str !== 'string')

@@ -755,3 +737,3 @@ return str;

*/
function escapeStringForText(str) {
function escapeStringForText( str ) {
if (typeof str !== 'string')

@@ -787,3 +769,3 @@ return str;

*/
function encodeNonCharacters(codePoint) {
function encodeNonCharacters( codePoint ) {
const hex = codePoint.toString(16).toUpperCase();

@@ -794,3 +776,3 @@ return `&#x${hex};`;

// return the path prefix of a given name or if no prefix available 'root'
function getSchemaPrefix(name) {
function getSchemaPrefix( name ) {
const lastDotIdx = name.lastIndexOf('.');

@@ -801,16 +783,16 @@ return (lastDotIdx > 0 ) ? name.substring(0, lastDotIdx) : 'root';

// get artifacts base name
function getBaseName(name) {
function getBaseName( name ) {
const lastDotIdx = name.lastIndexOf('.');
return (lastDotIdx > 0 ) ? name.substring(lastDotIdx+1, name.length) : name;
return (lastDotIdx > 0 ) ? name.substring(lastDotIdx + 1, name.length) : name;
}
// This is a poor mans path resolver for $self partner paths only
function resolveOriginAssoc(csn, env, path) {
for(const segment of path) {
let elements = (env.items && env.items.elements || env.elements);
if(elements)
env = env.elements[segment];
let type = (env.items && env.items.type || env.type);
if(type && !isBuiltinType(type) && !(env.items && env.items.elements || env.elements))
env = csn.definitions[env.type];
function resolveOriginAssoc( csn, env, path ) {
for (const segment of path) {
const elements = (env?.items?.elements || env?.elements);
if (elements)
env = elements[segment];
const type = (env?.items?.type || env?.type);
if (type && !isBuiltinType(type) && !(env?.items?.elements || env?.elements))
env = csn.definitions[type];
}

@@ -820,9 +802,9 @@ return env;

function mergeIntoNavPropEntry(annoPrefix, navPropEntry, prefix, props) {
function mergeIntoNavPropEntry( annoPrefix, navPropEntry, prefix, props ) {
let newEntry = false;
// Filter properties with prefix and reduce them into a new dictionary
const o = props.filter(p => p[0].startsWith(annoPrefix+'.')).reduce((a,c) => {
const o = props.filter(p => p[0].startsWith(`${annoPrefix}.`)).reduce((a, c) => {
// clone the annotation value to avoid side effects with rewritten paths
a[c[0].replace(annoPrefix+'.', '')] = cloneAnnotationValue(c[1]);
a[c[0].replace(`${annoPrefix}.`, '')] = cloneAnnotationValue(c[1]);
return a;

@@ -832,25 +814,25 @@ }, { });

// BEFORE merging found capabilities, prefix the paths
applyTransformations({ definitions: { o }}, {
applyTransformations({ definitions: { o } }, {
'=': (parent, prop, value) => {
parent[prop] = prefix.concat(value).join('.');
}
},
});
// don't overwrite existing restrictions
const prop = annoPrefix.split('.')[1];
if(!navPropEntry[prop]) {
if (!navPropEntry[prop]) {
// if dictionary has entries, add them to navPropEntry
if(Object.keys(o).length) {
if (Object.keys(o).length) {
// ReadRestrictions may have sub type ReadByKeyRestrictions { Description, LongDescription }
// chop annotations into dictionaries
if(annoPrefix === '@Capabilities.ReadRestrictions' &&
if (annoPrefix === '@Capabilities.ReadRestrictions' &&
Object.keys(o).some(k => k.startsWith('ReadByKeyRestrictions.'))) {
const no = {};
Object.entries(o).forEach(([k,v]) => {
const [head, ...tail] = k.split('.');
if(head === 'ReadByKeyRestrictions' && tail.length) {
if(!no['ReadByKeyRestrictions'])
no['ReadByKeyRestrictions'] = {};
// Don't try to add entry into non object
if(typeof no['ReadByKeyRestrictions'] === 'object')
no['ReadByKeyRestrictions'][tail.join('.')] = v;
Object.entries(o).forEach(([ k, v ]) => {
const [ head, ...tail ] = k.split('.');
if (head === 'ReadByKeyRestrictions' && tail.length) {
if (!no.ReadByKeyRestrictions)
no.ReadByKeyRestrictions = {};
// Don't try to add entry into non object
if (typeof no.ReadByKeyRestrictions === 'object')
no.ReadByKeyRestrictions[tail.join('.')] = v;
}

@@ -871,4 +853,4 @@ else {

// merge but don't overwrite into existing navprop
Object.entries(o).forEach(([k,v]) => {
if(!navPropEntry[prop][k])
Object.entries(o).forEach(([ k, v ]) => {
if (!navPropEntry[prop][k])
navPropEntry[prop][k] = v;

@@ -881,4 +863,4 @@ });

// Assign but not overwrite annotation
function assignAnnotation(node, name, value) {
if(value !== undefined &&
function assignAnnotation( node, name, value ) {
if (value !== undefined &&
name !== undefined && name[0] === '@')

@@ -889,24 +871,24 @@ node[name] ??= value;

// Set non enumerable property if it doesn't exist yet
function assignProp(obj, prop, value) {
if(obj[prop] === undefined) {
function assignProp( obj, prop, value ) {
if (obj[prop] === undefined)
setProp(obj, prop, value);
}
}
//
// create Cross Schema Reference object
//
function createSchemaRef(serviceRoots, targetSchemaName) {
// prepend as many path ups '..' as there are path steps in the service ref
let serviceRef = path4(serviceRoots[targetSchemaName]).split('/').filter(c=>c.length);
// create Cross Schema Reference object
//
function createSchemaRef( serviceRoots, targetSchemaName ) {
// prepend as many path ups '..' as there are path steps in the service ref
const serviceRef = path4(serviceRoots[targetSchemaName]).split('/').filter(c => c.length);
serviceRef.splice(0, 0, ...Array(serviceRef.length).fill('..'));
// uncomment this to make $metadata absolute
// if(serviceRef.length===0)
// serviceRef.push('');
if(serviceRef[serviceRef.length-1] !== '$metadata')
// uncomment this to make $metadata absolute
// if(serviceRef.length===0)
// serviceRef.push('');
if (serviceRef[serviceRef.length - 1] !== '$metadata')
serviceRef.push('$metadata');
let sc = { kind: 'reference',
const sc = {
kind: 'reference',
name: targetSchemaName,
ref: { Uri: serviceRef.join('/') },
inc: { Namespace: targetSchemaName }
inc: { Namespace: targetSchemaName },
};

@@ -916,3 +898,3 @@ setProp(sc, '$mySchemaName', targetSchemaName);

/**
/**
* Resolve a service endpoint path to mount it to as follows...

@@ -922,13 +904,12 @@ * Use _path or def[@path] if given (and remove leading '/')

*/
function path4 (def, _path = def['@path']) {
function path4( def, _path = def['@path'] ) {
if (_path)
return _path.replace(/^\//, '');
else
return ( // generate one from the service's name
/[^.]+$/.exec(def.name)[0] //> my.very.CatalogService --> CatalogService
.replace(/Service$/,'') //> CatalogService --> Catalog
.replace(/([a-z0-9])([A-Z])/g, (_,c,C) => c+'-'+C.toLowerCase()) //> ODataFooBarX9 --> odata-foo-bar-x9
.replace(/_/g,'-') //> foo_bar_baz --> foo-bar-baz
.toLowerCase() //> FOO --> foo
)
return ( // generate one from the service's name
/[^.]+$/.exec(def.name)[0] // > my.very.CatalogService --> CatalogService
.replace(/Service$/, '') // > CatalogService --> Catalog
.replace(/([a-z0-9])([A-Z])/g, (_, c, C) => `${c}-${C.toLowerCase()}`) // > ODataFooBarX9 --> odata-foo-bar-x9
.replace(/_/g, '-') // > foo_bar_baz --> foo-bar-baz
.toLowerCase() // > FOO --> foo
);
}

@@ -964,3 +945,3 @@ }

getBaseName,
mergeIntoNavPropEntry
}
mergeIntoNavPropEntry,
};

@@ -118,3 +118,2 @@ // Transform XSN (augmented CSN) into CSN

// definitions, extensions, members ----------------------------------------
returns, // storing the return type of actions
notNull: value,

@@ -127,2 +126,3 @@ default: expression,

actions, // TODO: just normal dictionary
returns, // storing the return type of actions
// special: top-level, cardinality -----------------------------------------

@@ -470,3 +470,3 @@ sources,

attachAnnotations( sub, 'params', entry.params, inf );
const obj = entry.returns || entry; // TODO: create returns !
const obj = entry.returns || entry;
const many = obj.items || obj;

@@ -476,4 +476,4 @@ const elems = (many.targetAspect || many).elements;

attachAnnotations( sub, 'elements', elems, inf, entry.returns );
if (many.enum)
attachAnnotations( sub, 'enum', many.enum, inf );
else if (many.enum) // make 'enum' annotations appear in 'elements' annotate
attachAnnotations( sub, 'elements', many.enum, inf, entry.returns );
}

@@ -480,0 +480,0 @@ if (Object.keys( sub ).length)

@@ -73,2 +73,14 @@ // Official cds-compiler API.

csnFlavor?: string | 'client' | 'gensrc' | 'universal'
/**
* If set, backends will not create localized convenience views for those views,
* that only have an association to a localized entity/view. Views will only get
* a convenience view, if they themselves contain localized elements (i.e. either
* have simple projection on localized elements and CDL-casts to a localized element).
*
* The OData backend will not set `$localized: true` markers for such cases.
*
* Does not work for backends to.hdi(), to.hdbcds() or to.sql() with `sqlDialect: 'hana'`,
* since in all those dialects, associations still exist in generated artifacts.
*/
fewerLocalizedViews?: boolean
}

@@ -91,2 +103,26 @@

/**
* "Doc comments" (documentation comments) are those comments starting with `/**` in CDL
* or the `doc` property in CSN. This option is an _output_ option, which can have three
* values:
*
* - `true`:
* Doc comments will appear in the compiled CSN. Basic sanity checks are performed:
* In CDL, if a doc comment appears at a not-defined position, where it has no impact,
* an info message is emitted. For CSN input, it is checked that the `doc` property
* is a string or `null`.
*
* - `false`:
* Doc comments will not be parsed for CDL, and will be stripped from input CSN,
* i.e. the compiled CSN (output) does not contain `doc` properties. No checks
* are performed on doc comments.
*
* - `undefined`:
* Doc comments are checked (see value `true`). For CDL, doc comments are not parsed,
* i.e. will not appear in the compiled CSN (output).
* For CSN input, all `doc` properties remain in the CSN.
*
* The CDL equivalent of the CSN value `doc: null`, is an empty doc comment.
*/
docComment?: boolean
/**
* When set to `true`, and the model contains an entity `sap.common.Languages`

@@ -101,2 +137,13 @@ * with an element `code`, all generated texts entities additionally contain

/**
* An array of directory names that are used for CDS module lookups.
* Lookup directory `node_modules/` is appended if not set explicitly.
*
* All directories in this array follow the same lookup-pattern as `node_modules/`.
*
* See <https://cap.cloud.sap/docs/cds/cdl#model-resolution>
*
* @since v4.2.0
*/
moduleLookupDirectories?: string[]
/**
* Option for {@link compileSources}. If set, all objects inside the

@@ -474,5 +521,6 @@ * provided sources dictionary are interpreted as XSN structures instead

* String to identify this class. Can be used instead of relying on `instanceof`.
* Always `ERR_CDS_COMPILATION_FAILURE`.
* @since v4.0.0
*/
code = 'ERR_CDS_COMPILATION_FAILURE';
code: string;
messages: CompileMessage[];

@@ -810,2 +858,5 @@ toString(): string;

*
* NOTE: If `name` contains newline characters, the resulting delimited identifier
* will not be parsable by the compiler!
*
* Example:

@@ -825,2 +876,5 @@ * ```js

* @param [insideFunction=null]
* Inside special functions such as SAP HANA's `OCCURRENCES_REGEXPR`, there are more
* keywords than in other places. Set this value to a function name, if you want to
* handle those additional keywords as well.
*/

@@ -833,2 +887,5 @@ function smartId(name: string, insideFunction?: string|null) : string;

*
* NOTE: If `name` contains newline characters, the resulting delimited identifier
* will not be parsable by the compiler!
*
* Example:

@@ -849,2 +906,5 @@ * ```js

*
* NOTE: If `name` contains newline characters, the resulting delimited identifier
* will not be parsable by the compiler!
*
* Example:

@@ -851,0 +911,0 @@ * ```js

@@ -1084,2 +1084,5 @@ 'use strict';

*
* IMPORTANT: Consider using copyAnnotationsAndDoc() instead!
* Don't forget about doc comments!
*
* @param {object} fromNode

@@ -1129,2 +1132,28 @@ * @param {object} toNode

/**
* Same as `copyAnnotationsAndDoc()` but deletes the annotations on source
* side after copying them. Useful when applying annotations from `cds.extensions`.
*
* Overwrite existing ones only if 'overwrite' is true.
*
* @param {object} sourceNode
* @param {object} targetNode
* @param {boolean} [overwrite]
*/
function moveAnnotationsAndDoc( sourceNode, targetNode, overwrite = false ) {
// Ignore if no targetNode (in case of errors)
if (!targetNode)
return;
const annotations = Object.keys(sourceNode)
.filter(key => key.startsWith('@') || key === 'doc');
for (const anno of annotations) {
if (targetNode[anno] === undefined || overwrite) {
targetNode[anno] = sourceNode[anno];
delete sourceNode[anno];
}
}
}
/**
* Applies annotations from `csn.extensions` to definitions and their elements.

@@ -1156,3 +1185,3 @@ *

if (def) {
copyAnnotationsAndDoc(ext, def, config.override);
moveAnnotationsAndDoc(ext, def, config.override);
applyAnnotationsToElements(ext, def);

@@ -1179,3 +1208,3 @@ }

if (targetElem) {
copyAnnotationsAndDoc(sourceElem, targetElem, config.override);
moveAnnotationsAndDoc(sourceElem, targetElem, config.override);
applyAnnotationsToElements(sourceElem, targetElem);

@@ -1182,0 +1211,0 @@ }

@@ -20,3 +20,3 @@ // Make internal properties of the XSN / augmented CSN visible

class NOT_A_DICTIONARY {} // used for consol.log display
class NOT_A_DICTIONARY {} // used for console.log display

@@ -23,0 +23,0 @@ function locationString( loc ) {

@@ -252,6 +252,41 @@ 'use strict';

function relevantTypeChange(type, otherType) {
return otherType !== type && ![type, otherType].every(t => ['cds.Association', 'cds.Composition'].includes(t));
return otherType !== type && !isSuperflousHanaTypeChange(otherType, type) && ![type, otherType].every(t => ['cds.Association', 'cds.Composition'].includes(t));
}
const superflousTypeChanges = {
// turn it into a real dict
__proto__: null,
// We used to put these types into the CSN, although they are just internal
// so we need to be robust against them now.
'cds.UTCDateTime': 'cds.DateTime',
'cds.UTCTimestamp': 'cds.Timestamp',
'cds.LocalDate': 'cds.Date',
'cds.LocalTime': 'cds.Time' ,
};
/**
* We removed some old SAP HANA types from the CSN and we now need to not
* detect them as changes.
*
* @param {string} before Type before
* @param {string} after Type after
* @returns {boolean}
*/
function isSuperflousHanaTypeChange( before, after ) {
return superflousTypeChanges[before] ? superflousTypeChanges[before] === after : false;
}
/**
* If the element has one of the superflous types, do the change so we don't accidentally
* pass such an old type into the SQL renderer.
* @param {CSN.Element} element
* @returns {CSN.Element}
*/
function remapType( element ) {
if(element?.type && superflousTypeChanges[element.type])
element.type = superflousTypeChanges[element.type];
return element;
}
/**
* Returns whether two things are deeply equal.

@@ -314,3 +349,3 @@ * Function-type things are compared in terms of identity,

return {
old: otherElement,
old: remapType(otherElement),
new: element

@@ -317,0 +352,0 @@ };

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

forEachKey(artifact, (key) => {
if (key.startsWith('@') && !annosToKeep[key])
if (key.startsWith('@') && !key.startsWith('@cds.persistence.') && !annosToKeep[key])
delete artifact[key];

@@ -165,0 +165,0 @@ });

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

.option(' --cds-home <dir>')
.option(' --fuzzy-csn-error')
.option(' --module-lookup-directories <list>')
.option(' --trace-fs')

@@ -82,3 +82,4 @@ .option(' --error <id-list>')

--cds-home <dir> When set, modules starting with '@sap/cds/' are searched in <dir>
--fuzzy-csn-error Report free-style CSN properties as errors
--module-lookup-directories <list> Comma separated list of directories to look
for CDS modules. Default is 'node_modules/'.
-- Indicate the end of options (helpful if source names start with "-")

@@ -233,2 +234,3 @@

.option('-s, --service-names <list>')
.option(' --fewer-localized-views')
.help(`

@@ -270,2 +272,4 @@ Usage: cdsc toOdata [options] <files...>

(default) empty, all services are rendered
--fewer-localized-views If set, the backends will not create localized convenience views for
those views, that only have an association to a localized entity/view.
`);

@@ -303,2 +307,3 @@

.option(' --better-sqlite-session-variables')
.option(' --fewer-localized-views')
.help(`

@@ -355,3 +360,6 @@ Usage: cdsc toSql [options] <files...>

--generated-by-comment Enable rendering of the initial SQL comment for HDI-based artifacts
--better-sqlite-session-variables Enable better-sqlite compatible rendering of $user. Only active if sqlDialect is \`sqlite\`
--better-sqlite-session-variables Enable better-sqlite compatible rendering of $user. Only
active if sqlDialect is \`sqlite\`
--fewer-localized-views If set, the backends will not create localized convenience views for
those views, that only have an association to a localized entity/view.

@@ -434,2 +442,3 @@ `);

.option(' --struct-xpr')
.option(' --fewer-localized-views')
.help(`

@@ -451,2 +460,5 @@ Usage: cdsc toCsn [options] <files...>

$location is an object with 'file', 'line' and 'col' properties.
--fewer-localized-views If --with-locations and this option are set, the backends
will not create localized convenience views for those views,
that only have an association to a localized entity/view.

@@ -453,0 +465,0 @@ Internal options (for testing only, may be changed/removed at any time)

@@ -261,12 +261,10 @@ // Common render functions for toCdl.js, toHdbcds.js and toSql.js

'cds.Boolean': 'BOOLEAN',
// (TODO: do it later; TODO: why not CHAR or at least VARCHAR?)
'cds.UUID': 'NVARCHAR', // changed to cds.String earlier
// (TODO: do it later; TODO: why not CHAR or at least VARCHAR?)
'cds.hana.ST_POINT': 'CHAR', // CHAR is implicit fallback used in toSql - make it explicit here
'cds.hana.ST_GEOMETRY': 'CHAR', // CHAR is implicit fallback used in toSql - make it explicit here
},
hana: {
'cds.hana.SMALLDECIMAL': 'SMALLDECIMAL',
'cds.LocalDate': 'DATE',
'cds.LocalTime': 'TIME',
'cds.DateTime': 'SECONDDATE',
'cds.UTCDateTime': 'SECONDDATE',
'cds.UTCTimestamp': 'TIMESTAMP',
'cds.hana.ST_POINT': 'ST_POINT',

@@ -279,3 +277,3 @@ 'cds.hana.ST_GEOMETRY': 'ST_GEOMETRY',

'cds.Timestamp': 'TIMESTAMP_TEXT',
'cds.DateTime': 'TIMESTAMP_TEXT',
'cds.DateTime': 'DATETIME_TEXT',
'cds.Binary': 'BINARY_BLOB',

@@ -315,2 +313,6 @@ 'cds.hana.BINARY': 'BINARY_BLOB',

'cds.Int64': 'cds.Integer64',
'cds.Timestamp': 'cds.UTCTimestamp',
'cds.DateTime': 'cds.UTCDateTime',
'cds.Date': 'cds.LocalDate',
'cds.Time': 'cds.LocalTime',
};

@@ -317,0 +319,0 @@

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

* @param {object | Array} _parent the thing that has _prop
* @param {string|number} _prop the name of the current property
* @param {string|number} _prop the name of the current property or index
* @param {object} node The value of node[_prop]

@@ -67,0 +67,0 @@ */

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

{ target: member.target, anno: '@cds.persistence.skip' },
'Association has been removed as it\'s target $(TARGET) is annotated with $(ANNO)');
'Association has been removed, as its target $(TARGET) is annotated with $(ANNO) and can\'t be rendered in SAP HANA SQL');
member.$ignore = true;

@@ -92,0 +92,0 @@ }

@@ -59,2 +59,21 @@ 'use strict';

}
// for `texts` compositions, we may generate foreign key constraints even w/o `up_`
else if (elementName === 'texts' && element.target === `${path[path.length - 1]}.texts`) {
const { on } = element;
const target = csn.definitions[element.target];
// `texts` entities have a key named "locale"
const targetSideHasLocaleKey = target.elements.locale?.key;
if (targetSideHasLocaleKey && !skipConstraintGeneration(parent, target, { /* there is no assoc */ })) {
const sourceElements = Array.from(elementsOfSourceSide(on, elements));
const targetElements = Array.from(elementsOfTargetSide(on, target.elements));
// `texts` entities have all the keys the original entity has
const allElementsAreKeysAndHaveTheSameName = targetElements.length &&
targetElements.every(
([ targetKey, e ]) => e.key &&
sourceElements.some(([ sourceKey, sourceElement ]) => sourceElement.key && targetKey === sourceKey )
);
if (allElementsAreKeysAndHaveTheSameName)
attachConstraintsToDependentKeys(targetElements, sourceElements, path[path.length - 1], 'texts', { texts: true });
}
}
}

@@ -138,3 +157,3 @@ },

* @param {CSN.PathSegment} sourceAssociation the name of the association from which the constraint originates
* @param {CSN.PathSegment} upLinkFor the name of the composition which used this association in a `$self = <comp>.<up_>` comparison
* @param {CSN.PathSegment | object} upLinkFor the name of the composition which used this association in a `$self = <comp>.<up_>` comparison
* it is used for a comment in the constraint, which is only printed out in test-mode

@@ -445,9 +464,12 @@ */

const elements = new Map();
on.filter(element => typeof element === 'object' &&
element.ref.length > 1 &&
targetElements[element.ref[element.ref.length - 1]])
.forEach((element) => {
elements.set(element.ref[element.ref.length - 1], targetElements[element.ref[element.ref.length - 1]]);
});
const findElements = (tokenStream) => {
tokenStream
.forEach((element) => {
if (typeof element === 'object' && element.ref?.length > 1 && targetElements[element.ref[element.ref.length - 1]])
elements.set(element.ref[element.ref.length - 1], targetElements[element.ref[element.ref.length - 1]]);
else if (element.xpr)
findElements(element.xpr);
});
};
findElements(on);
return elements;

@@ -465,8 +487,12 @@ }

const elements = new Map();
on.filter(element => typeof element === 'object' &&
element.ref.length === 1 &&
sourceElements[element.ref[0]])
.forEach((element) => {
elements.set(element.ref[0], sourceElements[element.ref[0]]);
});
const findElements = (tokenStream) => {
tokenStream
.forEach((element) => {
if (typeof element === 'object' && element.ref?.length === 1 && sourceElements[element.ref[0]])
elements.set(element.ref[0], sourceElements[element.ref[0]]);
else if (element.xpr)
findElements(element.xpr);
});
};
findElements(on);
return elements;

@@ -514,8 +540,12 @@ }

});
// onDelete Rule is the "weakest" rule applicable. Precedence: RESTRICT > SET NULL > CASCADE
// onDelete Rule is the "weakest" rule applicable. Precedence: RESTRICT > CASCADE
const onDelete = onDeleteRules.has('RESTRICT') ? 'RESTRICT' : 'CASCADE';
let onDeleteRemark = null;
// comments in sqlite files are causing the JDBC driver to throw an error on deployment
if (options.testMode && onDelete === 'CASCADE')
onDeleteRemark = `Up_ link for Composition "${$foreignKeyConstraint.upLinkFor}" implies existential dependency`;
if (options.testMode && onDelete === 'CASCADE') {
if ($foreignKeyConstraint.upLinkFor?.texts)
onDeleteRemark = `Constraint originates from localized composition ”${$foreignKeyConstraint.parentTable}:texts“`;
else
onDeleteRemark = `Up_ link for Composition "${$foreignKeyConstraint.upLinkFor}" implies existential dependency`;
}
// constraint identifier usually start with `c__` to avoid name clashes

@@ -522,0 +552,0 @@ let identifier = options.pre2134ReferentialConstraintNames ? '' : 'c__';

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

walkCsnPath,
getUtils,
} = require('../../model/csnUtils');
const { csnRefs, implicitAs } = require('../../model/csnRefs');
const { implicitAs, columnAlias } = require('../../model/csnRefs');
const { setProp } = require('../../base/model');

@@ -22,17 +23,10 @@ const { forEach } = require('../../utils/objectUtils');

* @param {object} messageFunctions
* @param {Function} messageFunctions.error
* @param {Function} messageFunctions.info
* @param {Function} messageFunctions.throwWithAnyError
* @param {object} csnUtils
* @param {object} [iterateOptions]
*/
function expandStructureReferences( csn, options, pathDelimiter, { error, info, throwWithAnyError }, csnUtils, iterateOptions = {} ) {
const {
isStructured, get$combined, getFinalTypeInfo,
} = csnUtils;
let { effectiveType, inspectRef } = csnUtils;
function expandStructureReferences( csn, options, pathDelimiter, messageFunctions, csnUtils, iterateOptions = {} ) {
const { error, info, throwWithAnyError } = messageFunctions;
rewriteExpandInline();
applyTransformations(csn, {

@@ -44,4 +38,5 @@ keys: (parent, name, keys, path) => {

const artifact = csn.definitions[path[1]];
csnUtils.initDefinition(artifact); // potentially no initialized, yet
if (!hasAnnotationValue(artifact, '@cds.persistence.table')) {
const root = get$combined({ SELECT: parent });
const root = csnUtils.get$combined({ SELECT: parent });
// TODO: replace with the correct options.transformation?

@@ -51,3 +46,3 @@ // Do not expand the * in OData for a moment, not to introduce changes

if (!options.toOdata)
parent.columns = replaceStar(root, columns, parent.excluding);
parent.columns = replaceStar(root, columns, parent.excluding, parent.from.join !== undefined);
parent.columns = expand(parent.columns, path.concat('columns'), true);

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

// We can directly use SELECT here, as only projections and SELECT can have .columns
const root = get$combined({ SELECT: parent });
const root = csnUtils.get$combined({ SELECT: parent });
if (!hasAnnotationValue(artifact, '@cds.persistence.table')) {

@@ -110,3 +105,3 @@ // Make root look like normal .elements - we never cared about conflict afaik anyway

({ effectiveType, inspectRef } = csnRefs(csn));
csnUtils = getUtils(csn);

@@ -153,3 +148,3 @@ const publishing = [];

const links = obj._links || inspectRef(path.concat([ name, i ])).links;
const links = obj._links || csnUtils.inspectRef(path.concat([ name, i ])).links;

@@ -254,3 +249,3 @@ if (!links)

if (parent.ref) {
const finalBaseType = getFinalTypeInfo(parent._art.type);
const finalBaseType = csnUtils.getFinalTypeInfo(parent._art.type);
const art = parent._art;

@@ -308,3 +303,3 @@

return false;
const eType = effectiveType(obj._art);
const eType = csnUtils.effectiveType(obj._art);
return (eType.type === 'cds.Association' || eType.type === 'cds.Composition') && eType.cardinality && eType.cardinality.max !== 1;

@@ -528,3 +523,3 @@ }

* @param {CSN.Path} path
* @param {boolean} [withAlias=false] Whether to "expand" the (implicit) alias aswell.
* @param {boolean} [withAlias=false] Whether to "expand" the (implicit) alias as well.
* @returns {Array} New array - with all structured things expanded

@@ -537,6 +532,5 @@ */

if (col.ref && col.$scope !== '$magic') {
const _art = col._art || inspectRef(path.concat(i)).art;
if (_art && isStructured(_art))
const _art = col._art || csnUtils.inspectRef(path.concat(i)).art;
if (_art && csnUtils.isStructured(_art))
newThing.push(...expandRef(_art, col, withAlias));
else

@@ -549,2 +543,19 @@ newThing.push(col);

}
else if (col.cast?.type) {
const _art = col.cast._type || csnUtils.inspectRef(path.concat(i, 'cast', 'type')).art;
if (_art && csnUtils.isStructured(_art)) {
// special case for `null as name : Struct`
if (col.val === null) {
newThing.push(...expandValAsStructure(_art, col, withAlias));
}
else {
error('type-invalid-cast', path.concat(i, 'cast', 'type'), {
'#': col.val !== undefined ? 'val-to-structure' : 'expr-to-structure', value: col.val,
});
}
}
else {
newThing.push(col);
}
}
else {

@@ -559,34 +570,31 @@ newThing.push(col);

/**
* Expand the ref and - if requested - expand the alias with it.
* Expands a column, and calls leafCallback() when a leaf node is reached.
*
* Iterative, to not run into stack overflow.
*
* @param {CSN.Element} art
* @param {object} root Column, ref in order by, etc.
* @param {boolean} withAlias
* @returns {Array}
* Structured Artifact which is used for expansion (and names, etc.). For a ref, it's the
* underlying type or a cast-type, for a value, it's always the cast-type.
* @param {string} colName
* Name of the column, that is used as the first name segment, e.g. a column `a` may end up in
* leafs `a_b` and `a_c`, if `art` has elements `b` and `c`.
* @param {string[]} colTypeRef
* Expanded type for the column. Basically the path to the to-be-expanded `art`.
* @param {(currentRef: any[], currentAlias: string[]) => object} leafCallback
* Callback when leaf nodes are reached. currentRef is the type reference for the expanded
* column. currentAlias is the columns calculated alias.
* @returns {object[]}
*/
function expandRef( art, root, withAlias ) {
function _expandStructCol( art, colName, colTypeRef, leafCallback ) {
const expanded = [];
/** @type {Array<[CSN.Element, any[], any[]]>} */
const stack = [ [ art, root.ref, [ root.as || implicitAs(root.ref) ] ] ];
/** @type {Array<[CSN.Element, any[], string[]]>} */
const stack = [ [ art, colTypeRef, [ colName ] ] ];
while (stack.length > 0) {
const [ current, currentRef, currentAlias ] = stack.pop();
if (isStructured(current)) {
for (const [ n, e ] of Object.entries(current.elements || effectiveType(current).elements).reverse())
stack.push([ e, currentRef.concat(n), currentAlias.concat(n) ]);
if (csnUtils.isStructured(current)) {
const elements = Object.entries(current.elements || csnUtils.effectiveType(current).elements).reverse();
for (const [ name, elem ] of elements)
stack.push([ elem, currentRef.concat(name), currentAlias.concat(name) ]);
}
else {
const obj = { ...root, ...{ ref: currentRef } };
if (withAlias) {
const newAlias = currentAlias.join(pathDelimiter);
// if (alias !== undefined) // explicit alias
obj.as = newAlias;
// alias was implicit - to later distinguish expanded s -> s.a from explicitly written s.a
if (root.as === undefined)
setProp(obj, '$implicitAlias', true);
}
if (root.key)
obj.key = true;
expanded.push(obj);
const newCol = leafCallback(currentRef, currentAlias);
expanded.push(newCol);
}

@@ -599,2 +607,63 @@ }

/**
* Expand the ref and - if requested - expand/set the alias with it.
*
* @param {CSN.Element} art
* @param {object} root Column, ref in order by, etc.
* @param {boolean} withAlias Whether to add an explicit flattened alias to the expanded columns/references.
* @returns {Array}
*/
function expandRef( art, root, withAlias ) {
return _expandStructCol(art, columnAlias(root), root.ref, ( currentRef, currentAlias) => {
const obj = { ...root, ref: currentRef };
if (withAlias) {
// TODO: Remove this line in case foreign key annotations should
// be adressed via full path into target instead of using alias
// names. See flattening.js::flattenAllStructStepsInRefs()
// apply transformations on `ref` counterpart comment.
setProp(obj, '$structRef', currentAlias);
obj.as = currentAlias.join(pathDelimiter);
// alias was implicit - to later distinguish expanded s -> s.a from explicitly written s.a
if (root.as === undefined)
setProp(obj, '$implicitAlias', true);
}
if (typeof root.$env === 'string')
obj.ref = [ root.$env, ...obj.ref ];
return obj;
});
}
/**
* Expand `null` columns which were cast to a structure, that is: `null as name : Struct`.
* Requires that `col` has an alias.
*
* @param {CSN.Element} art
* @param {object} col
* @param {boolean} withAlias Whether to add an explicit flattened alias to the expanded columns/references.
* @returns {Array}
*/
function expandValAsStructure( art, col, withAlias ) {
const colName = col.as || '';
// Expression-columns may have an internal name such as `$_column_N`. If the name is internal,
// we should not publish names based upon the internal name.
const isInternal = !col.as || !Object.prototype.propertyIsEnumerable.call(col, 'as');
return _expandStructCol(art, colName, col.cast.type?.ref || [ col.cast.type ], ( currentRef, currentAlias) => {
const newCol = {
...col,
val: col.val,
cast: { type: { ref: currentRef } },
};
if (withAlias) {
if (!isInternal)
newCol.as = currentAlias.join(pathDelimiter);
else
setProp(newCol, 'as', currentAlias.join(pathDelimiter));
}
return newCol;
});
}
/**
* Get the effective name produced by the object

@@ -621,5 +690,6 @@ *

* @param {string[]} [excluding=[]]
* @param {boolean} [isComplexQuery=false] Wether the query is a single source select or something more complex
* @returns {Array} If there was a star, expand it and handle shadowing/excluding, else just return subs
*/
function replaceStar( base, subs, excluding = [] ) {
function replaceStar( base, subs, excluding = [], isComplexQuery = false ) {
const stars = [];

@@ -656,3 +726,7 @@ const names = Object.create(null);

else { // the thing is not shadowed - use the name from the base
star.push({ ref: [ part ] });
const col = { ref: [ part ] };
if (isComplexQuery) // $env: tableAlias
setProp(col, '$env', base[part][0].parent);
star.push(col);
}

@@ -659,0 +733,0 @@ }

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

const { csnRefs } = require('../../model/csnRefs');
const { setProp } = require('../../base/model');
const { setProp, isBetaEnabled } = require('../../base/model');
const { forEach } = require('../../utils/objectUtils');

@@ -212,3 +212,6 @@ const { cardinality2str } = require('../../model/csnUtils');

const scopedPath = [ ...parent.$path ];
// TODO: If foreign key annotations should be assigned via
// full path into target, uncomment this line and
// comment/remove setProp in expansion.js
// setProp(parent, '$structRef', parent.ref);
parent.ref = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes);

@@ -224,3 +227,5 @@ resolved.set(parent, { links, art, scope });

// To handle explicitly written s.a - add implicit alias a, since after flattening it would otherwise be s_a
else if (parent.ref[parent.ref.length - 1] !== lastRef && (insideColumns(scopedPath) || insideKeys(scopedPath)) && !parent.as) {
else if (parent.ref[parent.ref.length - 1] !== lastRef &&
(insideColumns(scopedPath) || insideKeys(scopedPath)) &&
!parent.as) {
parent.as = lastRef;

@@ -403,3 +408,33 @@ }

/**
* Link annotate extensions to managed associations as a preparational step
* for later annotation assignment on the final foreignkeys
* This function must be applied on an unmodified, structured CSN in order to
* traverse both the extensions and dictionary trees in corresponding order.
*
* @param {CSN.Model} csn
* @param {object} options
*/
function linkForeignKeyAnnotationExtensionsToAssociation( csn, options ) {
if (isBetaEnabled(options, 'annotateForeignKeys')) {
csn.extensions?.forEach(( ext ) => {
const defName = ext.annotate;
const traverseExtensions = (env, enode) => {
if (env?.target && env?.keys) {
setProp(env, '$fkExtensions', enode);
}
else {
const elements = env?.items?.elements || env?.elements;
if (enode?.elements && elements)
Object.keys(enode.elements).forEach(en => traverseExtensions(elements[en], enode.elements[en]));
}
};
if (ext.annotate)
traverseExtensions(csn.definitions[defName], ext);
});
}
}
/**
* @param {CSN.Model} csn
* @param {CSN.Options} options

@@ -537,3 +572,3 @@ * @param {Function} error

function cloneAndExtendRef( key, base, ref ) {
const clone = cloneCsnNonDict(base, options);
const clone = cloneCsnNonDict(base, { ...options, hiddenPropertiesToClone: [ '$structRef', '$fkExtensions' ] } );
if (key.ref) {

@@ -554,2 +589,4 @@ // We build a ref that contains the aliased fk - that element will be created later on, so this ref is not resolvable yet

clone.ref = clone.ref.concat(key.ref);
if (clone.$structRef && key.$structRef)
clone.$structRef = clone.$structRef.concat(key.$structRef);
}

@@ -655,2 +692,30 @@

}
// assign annotations from fkExtension tree to foreign keys
if (isBetaEnabled(options, 'annotateForeignKeys')) {
const extCollector = {};
fks.forEach(([ _fkn, fk ]) => {
let ext = element.$fkExtensions;
let extKey = elementName;
for (const step of fk.$extensionPath) {
extKey += `.${step}`;
ext = ext?.elements?.[step];
if (!ext)
break;
// collect annotations, lowest wins
// eslint-disable-next-line no-loop-func
Object.entries(ext).forEach(([ k, v ]) => {
if (k[0] === '@') {
fk[k] = v;
extCollector[extKey] = ext;
}
});
}
});
// remove consumed annotations after applying the annotation hierarchy to each fk!
Object.values(extCollector).forEach(ext => Object.keys(ext).forEach((k) => {
if (k[0] === '@')
delete ext[k];
}));
}
orderedElements.push(...fks);

@@ -679,6 +744,7 @@ });

* @param {string} pathDelimiter
* @param {object} extensionPath
* @param {number} lvl
* @returns {Array[]} First element of every sub-array is the foreign key name, second is the foreign key definition
*/
function createForeignKeys( csnUtils, path, element, prefix, csn, options, pathDelimiter, lvl = 0 ) {
function createForeignKeys( csnUtils, path, element, prefix, csn, options, pathDelimiter, extensionPath = [], lvl = 0 ) {
const special$self = !csn?.definitions?.$self && '$self';

@@ -729,3 +795,3 @@ const isInspectRefResult = !Array.isArray(path);

const result = csnUtils.inspectRef(continuePath);
fks = fks.concat(createForeignKeys(csnUtils, result, result.art, alias, csn, options, pathDelimiter, lvl + 1));
fks = fks.concat(createForeignKeys(csnUtils, result, result.art, alias, csn, options, pathDelimiter, extensionPath.concat(key.$structRef), lvl + 1));
});

@@ -744,3 +810,3 @@ if (!hasKeys)

const continuePath = getContinuePath([ 'elements', elemName ]);
fks = fks.concat(createForeignKeys(csnUtils, continuePath, elem, elemName, csn, options, pathDelimiter, lvl + 1));
fks = fks.concat(createForeignKeys(csnUtils, continuePath, elem, elemName, csn, options, pathDelimiter, extensionPath.concat(elemName), lvl + 1));
}

@@ -752,2 +818,3 @@ });

const newFk = Object.create(null);
setProp(newFk, '$extensionPath', extensionPath);
for (const prop of [ 'type', 'length', 'scale', 'precision', 'srid', 'default', '@odata.Type' ]) {

@@ -808,4 +875,5 @@ // copy props from original element to preserve derived types!

removeLeadingSelf,
linkForeignKeyAnnotationExtensionsToAssociation,
handleManagedAssociationsAndCreateForeignKeys,
getBranches,
};

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

flattening.linkForeignKeyAnnotationExtensionsToAssociation(csn, options);
// All type refs must be resolved, including external APIs.

@@ -173,2 +175,3 @@ // OData has no 'type of' so 'real' imported OData APIs marked @cds.external are safe.

// No refs with struct-steps exist anymore
flattening.flattenAllStructStepsInRefs(csn, options, resolved, '_', { skipArtifact: isExternalServiceMember });

@@ -394,3 +397,3 @@ // No type references exist anymore

if(node.kind == null) {
if (node['@mandatory']&& node['@Common.FieldControl'] === undefined) {
if (node['@mandatory'] && !Object.entries(node).some(([k,v]) => k === '@Common.FieldControl' || k.startsWith('@Common.FieldControl.') && v != null)) {
setAnnotation(node, '@Common.FieldControl', { '#': 'Mandatory' });

@@ -397,0 +400,0 @@ }

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

const { translateAssocsToJoinsCSN } = require('./translateAssocsToJoins');
const { csnRefs, pathId, traverseQuery } = require('../model/csnRefs');
const { csnRefs, pathId, traverseQuery, columnAlias} = require('../model/csnRefs');
const { checkCSNVersion } = require('../json/csnVersion');

@@ -167,2 +167,4 @@ const validate = require('../checks/validator');

doA2J && flattening.linkForeignKeyAnnotationExtensionsToAssociation(csn, options);
// Check if structured elements and managed associations are compared in an expression

@@ -468,66 +470,67 @@ // and expand these structured elements. This tuple expansion allows all other

// For non-A2J only
function handleMixinOnConditions(artifact, artifactName) {
if (!artifact.query)
if (!artifact.query) // projections can't have mixins
return;
forAllQueries(artifact.query, (query, path) => {
const { mixin } = query.SELECT || {};
if(mixin) {
const { mixin } = query.SELECT || {};
if (mixin) {
query.SELECT.columns
// filter for associations which are used in the SELECT
.filter((c) => {
return c.ref && c.ref.length > 1;
})
.forEach((usedAssoc) => {
const assocName = pathId(usedAssoc.ref[0]);
const mixinAssociation = mixin[assocName];
if(mixinAssociation){
mixinAssociation.on = getResolvedMixinOnCondition(csn, mixinAssociation, query, assocName, path.concat(['mixin', assocName]));
}
})
// filter for associations which are used in the SELECT
.filter((c) => {
return c.ref && c.ref.length > 1;
})
.forEach((usedAssoc) => {
const assocName = pathId(usedAssoc.ref[0]);
const mixinAssociation = mixin[assocName];
if (mixinAssociation)
mixinAssociation.on = getResolvedMixinOnCondition(mixinAssociation, query, assocName, path.concat(['mixin', assocName]));
})
}
}, [ 'definitions', artifactName, 'query' ]);
}, ['definitions', artifactName, 'query']);
}
function getResolvedMixinOnCondition(csn, mixinAssociation, query, assocName, path){
const { inspectRef } = csnRefs(csn);
const referencedThroughStar = query.SELECT.columns.some((column) => column === '*');
return mixinAssociation.on
.map((onConditionPart, i) => {
let columnToReplace;
if(onConditionPart.ref && (onConditionPart.ref[0] === '$projection' || onConditionPart.ref[0] === '$self')){
const { links } = inspectRef(path.concat(['on', i]));
if(links){
columnToReplace = onConditionPart.ref[links.length - 1];
}
}
if (!columnToReplace)
return onConditionPart;
// For non-A2J only
function getResolvedMixinOnCondition(mixinAssociation, query, assocName, path) {
const referencedThroughStar = query.SELECT.columns.some((column) => column === '*');
return mixinAssociation.on.map(handeMixinOnConditionPart);
const replaceWith = query.SELECT.columns.find((column) =>
column.as && column.as === columnToReplace ||
column.ref && column.ref[0] === columnToReplace
);
if (!replaceWith && referencedThroughStar) {
// not explicitly in column list, check query sources
// get$combined also includes elements which are part of "excluding {}"
// this shouldn't be an issue here, as such references get rejected
const elementsOfQuerySources = csnUtils.get$combined(query);
forEach(elementsOfQuerySources, (id, element) => {
// if the ref points to an element which is not explicitly exposed in the column list,
// but through the '*' operator -> replace the $projection / $self with the correct source entity
if(id === columnToReplace)
onConditionPart.ref[0] = element[0].parent;
});
}
function handeMixinOnConditionPart(onConditionPart, i) {
let columnToReplace;
if (onConditionPart.ref && (onConditionPart.ref[0] === '$projection' || onConditionPart.ref[0] === '$self')){
const { links } = csnUtils.inspectRef(path.concat(['on', i]));
if (links) {
columnToReplace = onConditionPart.ref[links.length - 1];
}
}
if (!columnToReplace)
return onConditionPart;
// No implicit CAST in on-condition
if(replaceWith && replaceWith.cast) {
const clone = cloneCsnNonDict(replaceWith, options);
delete clone.cast;
return clone;
}
return replaceWith || onConditionPart;
});
const replaceWith = query.SELECT.columns.find(col => columnAlias(col) === columnToReplace);
if (!replaceWith && referencedThroughStar) {
// not explicitly in column list, check query sources
// get$combined also includes elements which are part of "excluding {}"
// this shouldn't be an issue here, as such references get rejected
const elementsOfQuerySources = csnUtils.get$combined(query);
forEach(elementsOfQuerySources, (id, element) => {
// if the ref points to an element which is not explicitly exposed in the column list,
// but through the '*' operator -> replace the $projection / $self with the correct source entity
if(id === columnToReplace)
onConditionPart.ref[0] = element[0].parent;
});
return onConditionPart;
}
else if (replaceWith) {
const clone = cloneCsnNonDict(replaceWith, options);
delete clone.cast; // No implicit CAST in on-condition
delete clone.as;
return clone;
}
else {
return onConditionPart
}
}
}
/**

@@ -707,3 +710,5 @@ * @param {CSN.Artifact} artifact

// simply make it invisible and copy it over to the result csn
forEachDefinition(csn, art => art.technicalConfig && setProp(art, 'technicalConfig', art.technicalConfig));
forEachDefinition(csn,
art => art.technicalConfig && setProp(art, 'technicalConfig',
art.technicalConfig));

@@ -721,2 +726,19 @@ const newCsn = translateAssocsToJoinsCSN(csn, options);

});
// restore $fkExtensions and $structRef for foreign key annotations
if (isBetaEnabled(options, 'annotateForeignKeys')) {
forEachDefinition(csn, (oldDef, artName) => {
const newDef = newCsn.definitions[artName];
if(oldDef?.elements) {
Object.entries(oldDef.elements).forEach(([eltName, oldElt]) => {
const newElt = newDef.elements[eltName];
if(oldElt.$fkExtensions)
setProp(newElt, '$fkExtensions', oldElt.$fkExtensions);
oldElt.keys?.forEach((fk, i) => {
if(fk.$structRef && newElt.keys?.[i])
setProp(newElt.keys[i], '$structRef', fk.$structRef);
})
})
}
});
}
csn = newCsn;

@@ -756,7 +778,3 @@ }

const hanaNamesMap = createDict({
'cds.DateTime': 'cds.UTCDateTime',
'cds.Timestamp': 'cds.UTCTimestamp',
'cds.Date': 'cds.LocalDate',
'cds.Time': 'cds.LocalTime',
'cds.UUID': 'cds.String',
'cds.UUID': 'cds.String'
});

@@ -763,0 +781,0 @@ node[key] = hanaNamesMap[val] || val;

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

} = require('../model/csnUtils');
const {CompilerAssertion} = require('../base/error');

@@ -38,6 +39,7 @@ /**

const _targetFor = Symbol('_targetFor');
const annoPersistenceSkip = '@cds.persistence.skip';
/**
* Callback function returning `true` if the localization view should be created.
* @callback acceptLocalizedView
* @callback AcceptLocalizedViewCallback
* @param {string} viewName localization view name

@@ -50,3 +52,3 @@ * @param {string} originalName Artifact name of the original view

*
* A convenience view is created if the entity/view has a localized element
* A convenience view is created if the entity/view has a localized element[^1]
* or if it exposes an association leading to a localized-tagged target.

@@ -58,10 +60,12 @@ *

* 1. "direct ones" using coalesce() for the table entities with localized
* elements: as projection on the original (created in extend.js)
* 2. for table entities with associations to entities which have a localized
* convenience views or redirections thereon: as projection on the original
* 3. for view entities with associations to entities which have a localized
* convenience views or redirections thereon: as entity using the same
* query as the original, but replacing all sources by their localized
* convenience view variant if present
* elements[^1]: as projection on the original and '.texts' entity (created in extend.js)
* 2. for _table_ entities with associations to entities which have a localized
* convenience: as projection on the original
* 3. for _view_ entities with either localized elements[^1] or associations
* to entities which have a localized convenience view:
* as view using a copy of the original query, but replacing all sources by
* their localized convenience view variant if present
*
* [^1]: That is, the element has `localized: true`.
*
* First, all "direct ones" are built (1). Then we build all 2 and 3

@@ -75,17 +79,42 @@ * transitively (i.e. as long as an entity has an association which directly or

* @param {CSN.Model} csn
* @param {CSN.Options} options
* @param {boolean} useJoins If true, rewrite the "localized" association to a
* join in direct convenience views.
* Input CSN model. Should not have existing convenience views.
*
* @param {object} options
* CSN options. Only few options are used, see below for important ones.
* Options such as `testMode` or `testSortCsn` can also be set.
*
* @param {string} [options.localizedLanguageFallback]
* Valid values (if set): 'none', 'coalesce' (default)
* Whether to use a `coalesce()` function when selecting from `.texts` entities.
* If not set, untranslated strings may not return any value. If 'coalesce'
* is used, it will fall back to the original string.
*
* @param {boolean} [options.localizedWithoutCoalesce]
* Deprecated version of localizedLanguageFallback. Do not use.
*
* @param {boolean} [options.fewerLocalizedViews]
*
* @param {object} config
* Configuration for creating convenience views. Non-user visible options.
*
* @param {boolean} [config.useJoins]
* If true, rewrite the "localized" association to a join in direct convenience views.
*
* @param {AcceptLocalizedViewCallback} [config.acceptLocalizedView]
* A callback that can be used to suppress the creation of localized convenience views
* if desired. For example, if you want to know which definitions get a convenience view
* but don't actually want to create them.
*
* @param {boolean} [config.ignoreUnknownExtensions]
* If true, do not emit a warning for annotations on unknown `localized.*` views.
*/
function _addLocalizationViews(csn, options, useJoins, config) {
function _addLocalizationViews(csn, options, config) {
const messageFunctions = makeMessageFunction(csn, options);
if (checkExistingLocalizationViews(csn, options, messageFunctions)) {
messageFunctions.throwWithError();
if (checkExistingLocalizationViews(csn, options, messageFunctions))
return csn;
}
const { acceptLocalizedView, ignoreUnknownExtensions } = config;
const { useJoins, acceptLocalizedView, ignoreUnknownExtensions } = config;
const noCoalesce = (options.localizedLanguageFallback === 'none' ||
options.localizedWithoutCoalesce);
const ignoreAssocToLocalized = !!options.fewerLocalizedViews;

@@ -187,6 +216,6 @@ createDirectConvenienceViews(); // 1

if (shouldUseJoin)
// Expand elements:
// Expand elements; (variant 1)
columns.push( ...columnsForEntityWithExcludeList( entity, 'L_0', textElements ) )
else
columns.push( '*' );
columns.push( '*' ); // (variant 2)

@@ -339,3 +368,3 @@ for (const originalElement of textElements) {

forEachGeneric(art, 'elements', (elem, elemName , _prop, path) => {
forEachGeneric(art, 'elements', (elem, elemName , _prop) => {
if (elem.$ignore) // from SAP HANA backend

@@ -349,7 +378,2 @@ return;

textElements.push( elemName );
if ((elem.key|| elem.$key) && elem.localized) {
messageFunctions.warning('def-ignoring-localized', path, { keyword: 'localized' },
'Keyword $(KEYWORD) is ignored for primary keys');
}
}, artPath);

@@ -375,3 +399,2 @@

}
if (!isValidTextsEntity( textsEntity )) {

@@ -382,2 +405,8 @@ messageFunctions.info( null, [ 'definitions', textsName ], { name: artName },

}
if (!art[annoPersistenceSkip] && textsEntity[annoPersistenceSkip]) {
messageFunctions.message( 'anno-unexpected-localized-skip', artPath,
{ name: textsName, art: artName, anno: annoPersistenceSkip },
'Compiler generated entity $(NAME) must not be annotated with $(ANNO) if $(ART) is not skipped' );
return null;
}

@@ -404,18 +433,19 @@ // There may be keys in the original artifact that were added by the core compiler,

* Transitively create convenience views for entities/views that have
* associations to localized entities or to views that themselves have such
* a dependency.
* associations to localized entities, views that themselves have such
* a dependency or views that contain projections on localized elements.
*
* The algorithm is as follows:
*
* 1. For each view/entity with associations:
* - If target is NOT localized => add view/entity to target's `_targetFor` property
* - If target is localized => add view/entity to array `entities`
* 1. For each view with elements that have `localized: true` markers:
* => add view to array `entities`
* For each view/entity with associations:
* - If target is NOT localized => add view/entity to target's `_targetFor` property
* - If target is localized => add view/entity to array `entities`
* 2. As long as `entities` has entries:
* a. For each entry in `entities`
* - Create a convenience view
* - If the entry has a `_targetFor` property, add its entries to
* `nextEntities` because they now have a transitive dependency on a
* localized view.
* b. Copy all entries from `nextEntities` to `entities`.
* c. Clear `nextEntities`.
* a. For each entry in `entities`
* - Create a convenience view
* - If the entry has a `_targetFor` property, add its entries to `nextEntities`
* because they now have a transitive dependency on a localized view.
* b. Copy all entries from `nextEntities` to `entities`.
* c. Clear `nextEntities`.
* 3. Rewrite all references to the localized variants.

@@ -462,6 +492,5 @@ */

}
else if (elem.target) {
else if (!ignoreAssocToLocalized && elem.target) {
// If the target has a localized view then we are localized as well.
const def = csn.definitions[elem.target];
// TODO: What if elem.target cannot be found? Could this happen after flattening, ...?
if (!def)

@@ -505,3 +534,3 @@ continue;

if (art[_targetFor])
if (!ignoreAssocToLocalized && art[_targetFor])
nextEntities.push(...art[_targetFor]);

@@ -594,10 +623,18 @@ delete art[_targetFor];

const ref = Array.isArray(obj.ref) ? obj.ref[0] : obj.ref;
if (typeof ref !== 'string')
return;
const def = csn.definitions[ref];
if (def && def[_hasLocalizedView]) {
if (Array.isArray(obj.ref))
obj.ref[0] = def[_hasLocalizedView];
else
if (typeof ref === 'string') {
const def = csn.definitions[ref];
if (def && def[_hasLocalizedView]) {
if (Array.isArray(obj.ref))
obj.ref[0] = def[_hasLocalizedView];
else
obj.ref = def[_hasLocalizedView];
}
} else if (ref.id) {
const def = csn.definitions[ref.id];
if (def && def[_hasLocalizedView])
obj.ref[0].id = def[_hasLocalizedView];
} else if (options.testMode) {
throw new CompilerAssertion('Debug me: Unhandled reference during localized-rewrite!');
}

@@ -610,3 +647,3 @@ }

function textsEntityName(artName) {
// We can assume, that the element exists. This is checked in isEntityPreprocessed()
// We can assume that the element exists. This is checked in isEntityPreprocessed().
return csn.definitions[artName].elements.texts.target;

@@ -644,6 +681,6 @@ }

* @param {CSN.Options} options
* @param [config] config.acceptLocalizedView: optional callback function returning true if the localized view name and its parent name provided as parameter should be created
* @param [config]
*/
function addLocalizationViews(csn, options, config = {}) {
return _addLocalizationViews(csn, options, false, config);
return _addLocalizationViews(csn, options, { ...config, useJoins: false });
}

@@ -654,10 +691,10 @@

* rewrite the "localized" association to joins in direct entity convenience
* views. This is needed by e.g. SQL for SQLite where A2J is used.
* views. This is needed e.g. by SQL for SQLite where A2J is used.
*
* @param {CSN.Model} csn
* @param {CSN.Options} options
* @param [config] config.acceptLocalizedView: optional callback function returning true if the localized view name and its parent name provided as parameter should be created
* @param [config]
*/
function addLocalizationViewsWithJoins(csn, options, config = {}) {
return _addLocalizationViews(csn, options, true, config);
return _addLocalizationViews(csn, options, { ...config, useJoins: true });
}

@@ -664,0 +701,0 @@

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

let flatElemName = elemName + pathDelimiter + childName;
let flatElem = cloneCsnNonDict(childElem, options);
let flatElem = cloneCsnNonDict(childElem, { ...options, hiddenPropertiesToClone: [ '$structRef', '$fkExtensions' ] } );
// Don't take over notNull from leaf elements

@@ -540,3 +540,3 @@ delete flatElem.notNull;

// CreationDateTime : Timestamp;
const creationDateTime = createScalarElement('CreationDateTime', hanaMode ? 'cds.UTCTimestamp' : 'cds.Timestamp');
const creationDateTime = createScalarElement('CreationDateTime', 'cds.Timestamp');
creationDateTime.CreationDateTime['@Common.Label'] = '{i18n>Draft_CreationDateTime}';

@@ -558,3 +558,3 @@ addElement(creationDateTime, artifact, artifactName);

// LastChangeDateTime : Timestamp;
const lastChangeDateTime = createScalarElement('LastChangeDateTime', hanaMode ? 'cds.UTCTimestamp' : 'cds.Timestamp');
const lastChangeDateTime = createScalarElement('LastChangeDateTime', 'cds.Timestamp');
lastChangeDateTime.LastChangeDateTime['@Common.Label'] = '{i18n>Draft_LastChangeDateTime}';

@@ -1181,9 +1181,8 @@ addElement(lastChangeDateTime, artifact, artifactName);

if(xn.length) {
error(null, location,
{
prefix: prefix(lhs, op, rhs),
name: xn,
alias: (x.lhs ? rhs : lhs).ref.join('.')
},
'$(PREFIX): Sub path $(NAME) not found in $(ALIAS)');
error('expr-invalid-expansion', location, {
value: prefix(lhs, op, rhs),
name: xn,
alias: (x.lhs ? rhs : lhs).ref.join('.')
},
'Missing sub path $(NAME) in $(ALIAS) for tuple expansion of $(VALUE); both sides must expand to the same sub paths');
}

@@ -1190,0 +1189,0 @@ else {

@@ -104,7 +104,7 @@ // Util functions for operations usually used with files.

// created in different execution env
traceFS( 'READFILE:cache-error:', filename, body.message );
traceFS( 'READFILE:cache-err:', filename, body.message );
cb( body ); // no need for process.nextTick( cb, body ) with moduleResolve
}
else {
traceFS( 'READFILE:cache:', filename, body );
traceFS( 'READFILE:cache: ', filename, body );
cb( null, body );

@@ -114,3 +114,3 @@ }

else {
traceFS( 'READFILE:start:', filename );
traceFS( 'READFILE:start: ', filename );
// TODO: set cache directly to some "delay" - store error differently?

@@ -121,3 +121,3 @@ // e.g. an error of callback functions!

fileCache[filename] = err || data;
traceFS('READFILE:data:', filename, err || data);
traceFS('READFILE:data: ', filename, err || data);
cb(err, data);

@@ -143,3 +143,3 @@ });

if (body !== undefined) {
traceFS( 'ISFILE:cache:', filename, body );
traceFS( 'ISFILE:cache: ', filename, body );
if (body instanceof Error)

@@ -151,3 +151,3 @@ cb( body ); // no need for process.nextTick( cb, body ) with moduleResolve

else {
traceFS( 'ISFILE:start:', filename, body );
traceFS( 'ISFILE:start: ', filename, body );
// in the future (if we do module resolve ourselves with just readFile),

@@ -164,3 +164,3 @@ // we avoid parallel readFile by storing having an array of `cb`s in

fileCache[filename] = body;
traceFS('ISFILE:data:', filename, body);
traceFS('ISFILE:data: ', filename, body);
if (body instanceof Error)

@@ -167,0 +167,0 @@ cb(err);

// Custom resolve functionality for the CDS compiler
//
// See `internalDoc/ModuleResolution.md` for details on the algorithm.
// See also <https://cap.cloud.sap/docs/cds/cdl#model-resolution>.
// The algorithm is based on NodeJS's `require()`.
//
// For debugging purpose, if the option variable `options.traceFs` is set,
// we log file lookups. This makes it easier to debug why a file was not
// found when setting custom search directories.
//
// You can set this option via your `.cdsrc.json` or `package.json`:
// `{ "cds": { "cdsc": { "traceFs": true } } }`.

@@ -17,14 +25,20 @@ 'use strict';

* "./index.cds" is checked first, then "index.csn" and so on.
*
* Keep in sync with documentation!
*/
const extensions = [ '.cds', '.csn', '.json' ];
/**
* Default module-lookup directories. Used when resolving modules.
* Since version v4.2, this option can be configured. Before that,
* only node_modules/ was a valid module-lookup directory.
*
* @type {string[]}
*/
const defaultLookupDirectories = [ 'node_modules/' ];
/**
* A global cds.home configuration can be set that forces the cds-compiler to
* use a certain directory for all @sap/cds/ includes.
* A global `cds.home` or local `options.cdsHome` configuration can be set that
* forces the cds-compiler to use a certain directory for all @sap/cds/ includes.
* This function handles such module paths.
*
* @todo Re-think:
* - Why can't a JAVA installation set a (symbolic) link?
* - Preferred to a local installation? Not the node-way!
*
* @param {string} modulePath

@@ -47,62 +61,66 @@ * @param {CSN.Options} options

/**
* @param {object} dep
* Create the module resolver, namely `resolveModule(dep)`.
*
* @param {CSN.Options} options
* @param {object} fileCache
* @param {CSN.Options} options
* @param {object} messageFunctions
*/
function resolveModule( dep, fileCache, options, messageFunctions ) {
function makeModuleResolver( options, fileCache, messageFunctions ) {
const _fs = cdsFs(fileCache, options.traceFs);
// let opts = { extensions, basedir: dep.basedir, preserveSymlinks: false };
// `preserveSymlinks` option does not really work -> provide workaround anyway...
// Hm, the resolve package also does not follow the node recommendation:
// "Using fs.stat() to check for the existence of a file before calling
// fs.open(), fs.readFile() or fs.writeFile() is not recommended"
/** @type {ResolveConfig} */
const opts = {
extensions,
basedir: dep.basedir,
isFile: _fs.isFile,
readFile: _fs.readFile,
realpath: _fs.realpath,
lookupDirs: _getLookupDirectories( options, messageFunctions ),
};
return new Promise( (fulfill, reject) => {
const lookupPath = adaptCdsModule(dep.module, options);
resolveCDS( lookupPath, opts, (err, res) => {
// console.log('RESOLVE', dep, res, err)
if (err) {
reject(err);
}
else {
const body = fileCache[res];
if (body === undefined || body === true) { // use fs if no or just temp entry
dep.absname = res;
_fs.realpath( res, cb );
return {
resolveModule,
};
function resolveModule( dep ) {
return new Promise( (fulfill, reject) => {
const lookupPath = adaptCdsModule( dep.module, options );
_resolveCDS( lookupPath, dep.basedir, opts, (err, res) => {
// console.log('RESOLVE', dep, res, err)
if (err) {
reject(err);
}
else if (body && typeof body === 'object' && body.realname) {
// dep.absname = body.realname;
cb( null, body.realname ); // use fs.realpath name
else {
const body = fileCache[res];
if (body === undefined || body === true) { // use fs if no or just temp entry
dep.absname = res;
_fs.realpath( res, cb );
}
else if (body && typeof body === 'object' && body.realname) {
// dep.absname = body.realname;
cb( null, body.realname ); // use fs.realpath name
}
else {
// dep.absname = res;
cb( null, res );
}
}
});
function cb( err, res ) {
if (err) {
reject(err);
}
else {
// dep.absname = res;
cb( null, res );
if (dep.absname)
fileCache[dep.absname] = (dep.absname === res) || { realname: res };
dep.resolved = res; // store in dep that module resolve was successful
for (const from of dep.usingFroms)
from.realname = res;
fulfill(res);
}
}
}).catch( () => {
_errorFileNotFound(dep, options, messageFunctions);
return false;
});
function cb( err, res ) {
if (err) {
reject(err);
}
else {
if (dep.absname)
fileCache[dep.absname] = (dep.absname === res) || { realname: res };
dep.resolved = res; // store in dep that module resolve was successful
for (const from of dep.usingFroms)
from.realname = res;
fulfill(res);
}
}
}).catch( () => {
_errorFileNotFound(dep, options, messageFunctions);
return false;
});
}
}

@@ -112,59 +130,121 @@

/**
* @param {object} dep
* Create the synchronous module resolver, namely `resolveModuleSync(dep)`.
*
* @param {CSN.Options} options
* @param {object} fileCache
* @param {CSN.Options} options
* @param {object} messageFunctions
*/
function resolveModuleSync( dep, fileCache, options, messageFunctions ) {
function makeModuleResolverSync( options, fileCache, messageFunctions ) {
const _fs = cdsFs(fileCache, options.traceFs);
/** @type {ResolveConfig} */
const opts = {
extensions,
basedir: dep.basedir,
isFile: _fs.isFileSync,
readFile: _fs.readFileSync,
realpath: _fs.realpathSync,
lookupDirs: _getLookupDirectories( options, messageFunctions ),
};
let result = null;
let error = null;
const lookupPath = adaptCdsModule(dep.module, options);
return {
resolveModuleSync,
};
resolveCDS( lookupPath, opts, (err, res) => {
if (err)
error = err;
if (res)
result = res;
});
function resolveModuleSync( dep ) {
let result = null;
let error = null;
const lookupPath = adaptCdsModule(dep.module, options);
if (error) {
_errorFileNotFound(dep, options, messageFunctions);
return false;
}
const body = result ? fileCache[result] : undefined;
if (body === undefined || body === true) { // use fs if no or just temp entry
dep.absname = result;
_fs.realpathSync( result, (err, modulePath) => {
_resolveCDS( lookupPath, dep.basedir, opts, (err, res) => {
if (err)
error = err;
else
result = modulePath;
if (res)
result = res;
});
if (error) {
_errorFileNotFound(dep, options, messageFunctions);
return false;
}
const body = result ? fileCache[result] : undefined;
if (body === undefined || body === true) { // use fs if no or just temp entry
dep.absname = result;
_fs.realpathSync( result, (err, modulePath) => {
if (err)
error = err;
else
result = modulePath;
});
}
else if (body && typeof body === 'object' && body.realname) {
result = body.realname;
}
if (error) {
_errorFileNotFound(dep, options, messageFunctions);
return false;
}
if (dep.absname)
fileCache[dep.absname] = (dep.absname === result) || { realname: result };
dep.resolved = result; // store in dep that module resolve was successful
for (const from of dep.usingFroms)
from.realname = result;
return result;
}
else if (body && typeof body === 'object' && body.realname) {
result = body.realname;
}
/**
* Get a list of module-lookup directories and ensure that user-provided lookup
* directories match our expectations / validate them.
*
* In case of errors (e.g. invalid options), returns the default list.
*
* @param {CSN.Options} options
* @param {object} messageFunctions
* @return {string[]}
*/
function _getLookupDirectories( options, messageFunctions ) {
const dirs = options?.moduleLookupDirectories || defaultLookupDirectories;
if (!Array.isArray(dirs)) {
messageFunctions.error('api-invalid-option', null, {
'#': 'type',
option: 'moduleLookupDirectories',
value: 'string[]',
othervalue: typeof options.moduleLookupDirectories,
});
return defaultLookupDirectories;
}
if (error) {
_errorFileNotFound(dep, options, messageFunctions);
return false;
if (!dirs.includes('node_modules/')) {
// Special case of call-side-errors and to ensure old behavior.
dirs.push('node_modules/');
}
if (dep.absname)
fileCache[dep.absname] = (dep.absname === result) || { realname: result };
dep.resolved = result; // store in dep that module resolve was successful
for (const from of dep.usingFroms)
from.realname = result;
for (const dir of dirs) {
if (!dir.endsWith('/')) {
messageFunctions.error('api-invalid-lookup-dir', null, {
'#': 'slash',
option: 'moduleLookupDirectories',
value: dir,
othervalue: '/',
} );
return defaultLookupDirectories;
}
if (dir.startsWith('./') || dir.startsWith('../')) {
// Avoid relative directories, as we don't want to give the impression that they
// are resolved relative to the caller.
messageFunctions.error('api-invalid-lookup-dir', null, {
'#': 'relative',
option: 'moduleLookupDirectories',
value: dir,
othervalue: './',
});
return defaultLookupDirectories;
}
}
return result;
return dirs;
}

@@ -207,12 +287,14 @@

* @param {string} moduleName Module to load, e.g. `./Include.cds` or `@sap/cds/common`.
* @param {ResolveOptions} options
* @param {ResolveConfig} config
* @param {string} baseDir
* @param {(err, result) => void} callback
*/
function resolveCDS( moduleName, options, callback ) {
function _resolveCDS( moduleName, baseDir, config, callback ) {
const isWindows = (process.platform === 'win32');
let resolvedBaseDir = path.resolve(options.basedir);
let resolvedBaseDir = path.resolve(baseDir);
const lookupDirs = [ ...config.lookupDirs ];
// NodeJS does not preserve symbolic links when resolving modules.
// So neither do we.
options.realpath(resolvedBaseDir, (realPathErr, realPath) => {
config.realpath(resolvedBaseDir, (realPathErr, realPath) => {
// There may be an error in resolving the symlink.

@@ -231,3 +313,3 @@ // We ignore the error and simply use the original path.

else
loadNodeModules(resolvedBaseDir);
loadAsModule(resolvedBaseDir);
}

@@ -277,3 +359,3 @@

function loadAsFile( absoluteModulePath, cb ) {
const extensionsToTry = [ '' ].concat(options.extensions);
const extensionsToTry = [ '' ].concat(config.extensions);
loadFileWithExtensions(extensionsToTry);

@@ -293,3 +375,3 @@

const file = absoluteModulePath + exts.shift();
options.isFile(file, (err, foundAndIsFile) => {
config.isFile(file, (err, foundAndIsFile) => {
if (!err && foundAndIsFile)

@@ -342,14 +424,15 @@ cb(null, file);

/**
* Try to load the module from a node_modules directory.
* Start at absoluteDir and go through all parent directories.
* Try to load the module from a specified module-lookup directory such
* as `node_modules/`.
* Start at `absoluteDir` and go through all parent directories.
*
* @param {string} absoluteDir
*/
function loadNodeModules( absoluteDir ) {
const dirs = nodeModulesPaths(absoluteDir);
loadFromNodeDirs(dirs);
function loadAsModule( absoluteDir ) {
const dirGen = modulePaths(absoluteDir);
loadNextDir();
function loadFromNodeDirs( nodeDirs ) {
const dir = nodeDirs.shift();
if (!dir) {
function loadNextDir() {
const dir = dirGen.next();
if (dir.done) {
// We're at root

@@ -359,3 +442,3 @@ callback(makeNotFoundError(), null);

}
const file = path.join(dir, moduleName);
const file = path.join(dir.value, moduleName);
loadAsLocalFileOrDirectory(file, (err, filepath) => {

@@ -365,3 +448,3 @@ if (!err && filepath)

else
loadFromNodeDirs(nodeDirs);
loadNextDir();
});

@@ -381,3 +464,3 @@ }

options.readFile(file, DEFAULT_ENCODING, (err, content) => {
config.readFile(file, DEFAULT_ENCODING, (err, content) => {
if (err) {

@@ -399,12 +482,10 @@ cb(err, null);

* Get a list of all `node_modules` directories that MAY exist.
* Starting from absoluteStart upwards until at root.
* Starting from `absoluteStart` upwards until at root.
*
* @param {string} absoluteStart
* @returns {string[]} Array of possible "node_modules" folders for the given path.
* @param {string} absoluteStart *
* @return {Generator<string>} Possible module directories for the given path.
*/
function nodeModulesPaths( absoluteStart ) {
function* modulePaths( absoluteStart ) {
// Use platform-dependent separator. All NodeJS `path` methods use the system's path separator.
const parts = absoluteStart.split(path.sep);
// Do NOT use global node_modules directories.
const dirs = [];

@@ -418,8 +499,14 @@ // If we're on *nix systems, the first part is just an empty string ''

for (let i = parts.length - 1; i >= 0; i--) {
if (parts[i] === 'node_modules')
continue;
const dir = path.join(...parts.slice(0, i + 1), 'node_modules');
dirs.push(dir);
for (let j = 0; j < lookupDirs.length; ++j) {
const dir = lookupDirs[j];
if (dir && path.isAbsolute(dir)) {
// Only look up absolute paths once.
lookupDirs[j] = null;
yield dir;
}
else if (dir && parts[i] && !lookupDirs.includes(`${ parts[i] }/`)) {
yield path.join(...parts.slice(0, i + 1), dir);
}
}
}
return dirs;
}

@@ -433,3 +520,3 @@

function makeNotFoundError() {
const moduleError = new Error(`Can't find module '${ moduleName }' from '${ options.basedir }'`);
const moduleError = new Error(`Can't find module '${ moduleName }' from '${ config.basedir }'`);
// eslint-disable-next-line

@@ -454,3 +541,3 @@ moduleError['code'] = 'MODULE_NOT_FOUND';

/**
* Get the cds.main entry of the package.json
* Get the `cds.main` entry of the package.json
* @param {object} pkg

@@ -465,4 +552,5 @@ */

/**
* @typedef {object} ResolveOptions
* @property {string} basedir
* @typedef {object} ResolveConfig
* @property {string[]} lookupDirs
* Directories to look in for modules, e.g. node_modules/.
* @property {string[]} extensions

@@ -476,8 +564,9 @@ * @property {(path: string, callback: (err, foundAndIsFile) => void) => void} isFile

module.exports = {
resolveModule,
resolveModuleSync,
makeModuleResolver,
makeModuleResolverSync,
// exported for unit tests
resolveCDS,
_resolveCDS,
_getLookupDirectories,
isLocalFile,
extensions,
};

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

* Takes an object and creates a dictionary out of it.
* This avoid cases where e.g. properties named "toString" are interpreted
* This avoids cases where e.g. properties named "toString" are interpreted
* as JS internal functions.

@@ -21,0 +21,0 @@ *

{
"name": "@sap/cds-compiler",
"version": "4.1.2",
"version": "4.2.2",
"description": "CDS (Core Data Services) compiler and backends",

@@ -25,6 +25,5 @@ "homepage": "https://cap.cloud.sap/",

"test3": "node scripts/verifyGrammarChecksum.js && mocha --reporter-option maxDiffSize=0 test3/",
"deployTest3SQL": "deployRefs=true mocha --reporter-option maxDiffSize=0 test3/test.deploy.hana-sql.js",
"deployTest3": "deployRefs=true mocha --reporter-option maxDiffSize=0 test3/testDeployment.js",
"deployHanaDeltaSQL": "CDS_COMPILER_DEPLOY_HANA=1 mocha test3/test.deploy.hana-sql.delta.js",
"deployDiffs": "deployRefs=true mocha --reporter-option maxDiffSize=0 test3/deployDiffs.js",
"deployHanaSql": "CDS_COMPILER_DEPLOY_HANA=1 mocha --reporter-option maxDiffSize=0 test3/test.deploy.hana-sql.js",
"deployHdiHdbcds": "CDS_COMPILER_DEPLOY_HANA=1 mocha --reporter-option maxDiffSize=0 test3/test.deploy.hdi.hdbcds.js",
"deployGitDiffs": "CDS_COMPILER_DEPLOY_HANA=1 mocha --reporter-option maxDiffSize=0 test3/test.deploy.git-diffs.js",
"gentest3": "cross-env MAKEREFS=${MAKEREFS:-'true'} mocha --reporter-option maxDiffSize=0 test3/testRefFiles.js",

@@ -35,2 +34,3 @@ "coverage": "cross-env nyc mocha --reporter-option maxDiffSize=0 test/ test3/ && nyc report --reporter=lcov",

"updateVocs": "node scripts/odataAnnotations/generateDictMain.js && npm run generateAllRefs",
"updateTocs": "node scripts/update-toc.js",
"generateCompilerRefs": "cross-env MAKEREFS='true' mocha test/testCompiler.js",

@@ -37,0 +37,0 @@ "generateEdmRefs": "cross-env MAKEREFS='true' mocha test/testEdmPositive.js",

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is 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 too big to display

Sorry, the diff of this file is not supported yet

Sorry, the diff of this file is too big to display

Sorry, the diff of this file is too big to display

SocketSocket SOC 2 Logo

Product

  • Package Alerts
  • Integrations
  • Docs
  • Pricing
  • FAQ
  • Roadmap
  • Changelog

Packages

npm

Stay in touch

Get open source security insights delivered straight into your inbox.


  • Terms
  • Privacy
  • Security

Made with ⚡️ by Socket Inc