@sap/cds-compiler
Advanced tools
Comparing version 1.39.0 to 1.42.2
@@ -128,2 +128,3 @@ #!/usr/bin/env node | ||
'dontRenderVirtualElements': true, | ||
'ignoreManagedAssocPublishingInUnion': true | ||
} | ||
@@ -130,0 +131,0 @@ } |
114
CHANGELOG.md
@@ -9,2 +9,116 @@ # ChangeLog for cdx compiler and backends | ||
## Version 1.42.2 - 2020-09-29 | ||
### Fixed | ||
- CDL: Action blocks can now be empty, e.g. `entity E {…} actions { }`. | ||
- An info message is emitted if builtin types are annotated. Use a custom type instead. | ||
Annotating builtins in CDL is possible but when transformed into CSN the annotation was silently lost. | ||
It is now put into the "extensions" property of the CSN. | ||
- Fixed `cast()` for simple values like numbers and strings. | ||
- to.sql: | ||
+ Remove simple default value checks and allow the database to reject default values upon activation. | ||
+ Render empty actual parameter list when selecting from a view with parameters which are fully covered with | ||
default values and no actual parameters are provided in the query itself. | ||
- OData: | ||
+ Correctly render unary operator of default values in EDM. | ||
## Version 1.42.0 - 2020-09-25 | ||
### Added | ||
- The compiler now supports the `cast(element as Type)` function in queries. | ||
Using this function will also result in a `CAST` SQL function call. | ||
- A top-level property `i18n` is now supported. The property can contain translated texts. | ||
The compiler expects its entries to be objects where each text value is a string. | ||
- CDL: Empty selection lists in views/projections are now allowed and make it possible to extend | ||
empty projections. Note that views/projections without any elements are not deployable. | ||
- For CSNs as input, the compiler returns properties as they are (without checks) | ||
if their name does not match the regexp `/[_$]?[a-zA-Z]+[0-9]*/` and does not start with `@`. | ||
With more than one CSN input, | ||
the compiler only returns the top-level CSN properties of the first source. | ||
### Changed | ||
- to.cdl: Smart type references are now explicitly rendered via ":"-syntax | ||
### Removed | ||
### Fixed | ||
- Annotating an _unknown_ element _twice_ now results in a duplicate annotation error instead | ||
of silently loosing the annotation. | ||
- Service/context extensions that reference a non-service/non-context now result in a compiler error | ||
instead of silently loosing the context/service extension. | ||
- to.hdbcds/sql/hdi: | ||
+ fix a bug, which resulted in a malformed on-condition, if an association key | ||
was another association pointing to an entitiy with a structured key. | ||
+ in conjunction with assoc-to-joins, the internal CSN reference broke | ||
causing missing locations and even internal errors when logging messages | ||
+ managed associations in UNION are now correctly processed | ||
- The parseCdl mode now correctly resolves type arguments of "many" types. | ||
- OData: The annotation `@Capabilities.Readable` is now correctly | ||
translated to `@Capabilities.ReadRestrictions.Readable`. | ||
## Version 1.41.4 - 2020-09-18 | ||
### Removed | ||
- The length of HANA identifiers are not checked anymore: no more warnings are issued for long identifiers. | ||
### Fixed | ||
- The check for ignored "localized" keywords in sub-elements has been extended to also | ||
include references to structured types. | ||
- A warning was added if views/projections are used as element types. | ||
- An info message is emitted if a namespace is annotated. | ||
Annotating namespaces is not possible. Previously the annotation was silently lost. | ||
It is now put into the "extensions" property of the CSN. | ||
## Version 1.41.2 - 2020-09-15 | ||
### Fixed | ||
- OData: correctly render primary key associations targeting a composition parent but are not | ||
the composition enabling association. | ||
- to.hdbcds/sql/hdi: Do not dump if artifact doesn't exist anymore after association to join translation | ||
- Only check for unmanaged associations inside of "many"/"array of" in the elements of views and entities, | ||
not inside of actions and other members. | ||
## Version 1.41.0 - 2020-09-11 | ||
### Added | ||
- OData: Allow the relational comparison of structures or managed associations in an ON condition as described in | ||
version 1.32.0 - 2020-07-10 (forHana). | ||
- Allow `Struct:elem` with and without preceeding `type of` as type reference. | ||
### Fixed | ||
- to.cdl: Only render enums if they were directly defined there | ||
- The parseCdl mode now checks for redefinitions to avoid generating invalid CSN. | ||
- OData: An error is thrown if a redirected target has fewer keys than the original one. | ||
- OData: Empty structured elements are now handled correctly in `flat` format. | ||
## Version 1.40.0 - 2020-09-04 | ||
### Added | ||
- to.hdi/sql: Support default values for view parameters. | ||
- OData: lower message severity from Error to Warning for | ||
`<entity type> has no primary key` and `<type> has no properties`. | ||
### Changed | ||
- OData: The foreign key references in associations are not flattened any more with format `structured`. | ||
### Fixed | ||
- parse.cdl: Properly handle type arguments, most likely relevant for HANA types. | ||
- OData: Multilevel anonymously defined `composition of <aspect>` is now processed successfully with the OData backend. | ||
- OData: Fix a bug in EDM generation that caused a dump. | ||
- Update ANTLR dependency to version 4.8. | ||
## Version 1.39.0 - 2020-08-26 | ||
@@ -11,0 +125,0 @@ |
@@ -6,12 +6,17 @@ # ChangeLog of Beta Features for cdx compiler and backends | ||
Note: `beta` fixes, changes and features are listed in this ChangeLog. | ||
Note: `beta` fixes, changes and features are listed in this ChangeLog just for information. | ||
The compiler behaviour concerning `beta` features can change at any time without notice. | ||
**Don't use `beta` fixes, changes and features in productive mode.** | ||
## Version 1.36.0 - 2020-08-xx | ||
## Version 1.42.0 | ||
### Added | ||
### Added `ignoreManagedAssocPublishingInUnion` | ||
#### `mapAssocToJoinCardinality` | ||
For `to.hdbcds`, with beta flag `ignoreManagedAssocPublishingInUnion` in conjunction with dialect | ||
`hanaJoins`, managed associations in UNIONs are replaced by their foreign keys and silently ignored | ||
## Version 1.36.0 - 2020-08-07 | ||
### Added `mapAssocToJoinCardinality` | ||
Analog to the feature `cardinality for explicit joins`, the association to | ||
@@ -22,7 +27,7 @@ join transformation algorithm now experimentally supports join cardinalities as well. | ||
#### `odataDefaultValues` | ||
### Added `odataDefaultValues` | ||
OData: Enables the rendering of default values for EntityType properties. | ||
#### `originalKeysForTemporal` | ||
### Added `originalKeysForTemporal` | ||
@@ -32,3 +37,3 @@ OData: The original entity keys are not enhanced with `@cds.valid.from` or replaced with | ||
#### `dontRenderVirtualElements` | ||
### Added `dontRenderVirtualElements` | ||
@@ -38,11 +43,51 @@ Virtual elements are no longer rendered in views as `null as <id>` or added to potentially generated | ||
### Removed | ||
### Removed `noJoinsForForeignKeys` | ||
#### `noJoinsForForeignKeys` | ||
The association to join transformation treats foreign key accesses with priority now. | ||
#### `uniqueconstraints` | ||
### Removed `uniqueconstraints` | ||
Unique constraints are now generally available. | ||
## Version 1.32.0 - 2020-07-10 | ||
### Removed `aspectCompositions` | ||
Aspect compositions aka managed compositions are now avaible without beta option. | ||
_Warning_: the CSN representation can still change. | ||
## Version 1.31.0 - 2020-06-26 | ||
### Changed `subElemRedirections` | ||
Signal an error | ||
if an unmanaged association as sub element is to be implicitly redirected, | ||
as we do not automatically rewrite the `on` condition in that situation yet. | ||
## Version 1.30.0 - 2020-06-12 | ||
### Added `subElemRedirections` | ||
When the beta option `subElemRedirections` is set to true, | ||
_all_ structure types are expanded when referenced: | ||
* managed associations (and compositions to entities) are implicitly redirected | ||
when necessary, | ||
* sub elements of referred structure types can be annotated individually, | ||
* the resulting CSN is bigger (will be reduced in the future if possible) | ||
as `type` references to structures will now have a sibling `elements`. | ||
This option does not enable: | ||
* rewriting the `on` conditions of associations in sub elements, | ||
* aspect compositions as sub elements, | ||
* `localized` sub elements, | ||
* `key` property on sub elements. | ||
## Version 1.23.0 | ||
### Added `keyRefError` | ||
Always signal an error (instead of just a warning in some cases), | ||
if not all references in the `keys` of an managed associations | ||
are projected in the new target. |
# Name Resolution in CDS | ||
> Status Oct 2019: TODOs must be filled, say more about name resolution in CSN. | ||
> Status Sep 2020: TODOs must be filled, say more about name resolution in CSN. | ||
@@ -50,3 +50,3 @@ Name resolution refers to the resolution of names (identifiers) within expressions of the source to the intended artifact or member in the model. | ||
``` | ||
nameprefix sap.core; | ||
namespace sap.core; | ||
@@ -59,14 +59,14 @@ context types { | ||
type Amount: Decimal(5,3); | ||
} | ||
}; | ||
type CurrencySymbol: String(3); | ||
view Products as select from ProductsInternal { | ||
productId, | ||
salesPrice | ||
}; | ||
entity ProductsInternal { | ||
productId: Integer; | ||
retailPrice: types.Price; | ||
salesPrice = retailPrice; // we give no reduction | ||
} | ||
view Products as select from ProductsInternal { | ||
productId, | ||
salesPrice | ||
} | ||
salesPrice = retailPrice; // calculated fields are not supported yet | ||
}; | ||
``` | ||
@@ -94,2 +94,7 @@ | ||
That being said, name resolution does **not depend on the order of definitions**. | ||
In the example above, the element `amount` has a type `Amount` | ||
which is defined _after_ the element definition. | ||
Similar for the view `Products` whose source entity `ProductsInternal` is defined after the view. | ||
In the view, we also refer to **elements** of (another) artifact. | ||
@@ -135,3 +140,3 @@ There is no special language construct for such references – | ||
an **extension cannot silently change the semantics of a model** – | ||
the name resolution is defined in such a way that a valid reference | ||
the name resolution is defined in such a way that a valid and potentially unrelated reference | ||
does not silently (without errors or warnings) point to another artifact | ||
@@ -189,3 +194,3 @@ when the extension is applied to the model. | ||
entity E : B { | ||
e = a; | ||
e = a; // calculated fields are not supported yet | ||
} | ||
@@ -200,3 +205,3 @@ ``` | ||
see the first example in Section ["Design Principles"](#design-principles). | ||
To avoid this situation in CDx/Language, we are a bit incompatible in this case. | ||
To avoid this situation in CDL, we are a bit incompatible in this case. | ||
@@ -352,6 +357,2 @@ | ||
In a future version of this project, | ||
we or others might provide a "use at your own risk" backend | ||
which produce SQL DDL statements without quoted identifiers | ||
--- | ||
@@ -402,3 +403,3 @@ | ||
following the lexical block structure of the source, | ||
or a small, fixed number of predefined names (e.g. `$projection`.) | ||
or a small, fixed number of predefined names (e.g. `$self`.) | ||
We will call such an environment a **lexical search environment**. | ||
@@ -429,6 +430,13 @@ | ||
annotation A : array of { e: Integer; }; | ||
annotate A with { | ||
annotate e with @lineElement; | ||
entity E { | ||
items: many { i: Integer; }; | ||
} | ||
annotation B: type of :A.e; // valid = Integer | ||
type T: type of E:items.i; // valid = Integer | ||
annotate E with { | ||
items { i @lineElement; }; // valid annotation | ||
} | ||
view V as select from E { | ||
items.i // not valid (yet) | ||
} | ||
``` | ||
@@ -443,3 +451,3 @@ | ||
annotate A with { | ||
@targetElem i; // error: do not follow associations | ||
@targetElem i; // err(info): do not follow associations | ||
} | ||
@@ -449,3 +457,3 @@ type S { e: Integer; } | ||
annotate T with { | ||
@derivedElem e; // ok: follow derived type | ||
@derivedElem e; // ok: follow derived type (not yet without beta) | ||
} | ||
@@ -487,4 +495,3 @@ ``` | ||
`Integer`, `Integer64`, `Binary`, `LargeBinary`, `Decimal`, `DecimalFloat`, `Double`, | ||
`Date`, `Time`, `Timestamp`, `DateTime`, and `Boolean`. | ||
More artifacts are defined with the options `--hana-flavor`. | ||
`Date`, `Time`, `Timestamp`, `DateTime`, and `Boolean` and `hana`, also the namespace `cds`. | ||
@@ -631,3 +638,4 @@ When searching for an annotation (after the initial `@`), the last search environment | ||
The reason for the `$self` references is visible in an example with subelements: | ||
The reason for the `$self` references is visible in an example with subelements | ||
(calculated fields are not supported yet): | ||
@@ -699,3 +707,3 @@ ``` | ||
The most visible differences in the name resolution semantics of CDx/Language compared to HANA CDS are: | ||
The most visible differences in the name resolution semantics of CDL compared to HANA CDS are: | ||
@@ -715,3 +723,3 @@ * Using constant values requires to prefix the path (referring to the constant) with a `:`. | ||
It is also compatible to the "pre-extension" name resolution semantics of HANA-CDS. | ||
This is nice! Why do we specify a different name resolution semantics for CDx/Language? | ||
This is nice! Why do we specify a different name resolution semantics for CDL? | ||
@@ -731,3 +739,3 @@ The reason is: | ||
These are properties which do not hold for consumers of the CDx Compiler. | ||
These are properties which do not hold for consumers of the CAP CDS Compiler. | ||
@@ -749,3 +757,3 @@ Additionally, while direct changes in base packages can always lead to semantic changes, | ||
extend b with { | ||
z = a; // in CDx/Language: $self.a | ||
z = a; // in CDL: $self.a, calculated fields are not supported yet | ||
} | ||
@@ -752,0 +760,0 @@ } |
@@ -599,3 +599,3 @@ /** @module API */ | ||
catch (err) { | ||
if (err instanceof CompilationError || options.testMode) | ||
if (err instanceof CompilationError || options.noRecompile) | ||
throw err; | ||
@@ -602,0 +602,0 @@ |
@@ -39,2 +39,3 @@ 'use strict'; | ||
'testMode', | ||
'noRecompile', | ||
'internalMsg', | ||
@@ -41,0 +42,0 @@ 'localizedWithoutCoalesce', // experiment version of 'localizedLanguageFallback', |
@@ -85,8 +85,7 @@ // Implementation of alerts | ||
* Link an alert to a location (if provided). If no explicit severity is given, the severity is taken | ||
* from the 'msg' itself (if provided). The alert is added to the list of alerts. If the alert is an | ||
* exception, the corresponding error is thrown. | ||
* from the 'msg' itself (if provided). The alert is added to the list of alerts. | ||
* | ||
* @param {any} msg Message text | ||
* @param {XSN.Location|CSN.Location|any[]} [location] Location information (possibly semantic location) | ||
* @param {string} [severity] severity: Info, Warning, Error | ||
* @param {CSN.MessageSeverity} [severity] severity: Info, Warning, Error, Debug | ||
* @param {string} [message_id=''] Message ID | ||
@@ -101,4 +100,2 @@ * @returns {Boolean} true, if no 'Error' alert was handled. | ||
switch (err.severity) { | ||
case 'Exception': | ||
throw err; | ||
case 'Error': | ||
@@ -105,0 +102,0 @@ return false; |
@@ -10,2 +10,6 @@ 'use strict'; | ||
function combinedLocation( start, end ) { | ||
if (!start) | ||
return end.location; | ||
else if (!end) | ||
return start.location; | ||
return { | ||
@@ -12,0 +16,0 @@ filename: start.location.filename, |
@@ -8,3 +8,3 @@ const { CompileMessage, DebugCompileMessage } = require('../base/messages'); | ||
* @param {XSN.Location|CSN.Location|CSN.Path} location | ||
* @param {string} severity | ||
* @param {CSN.MessageSeverity} severity | ||
* @param {string} message_id | ||
@@ -93,2 +93,5 @@ * @param {boolean} useDebugMsg | ||
let inCsnDict, inElement, inAction, inParam, inKeys, inRef, inEnum, inQuery, inColumn, inMixin, inItems = false; | ||
// for top level actions | ||
if(currentThing.kind === 'action') | ||
inAction = true; | ||
for(const [index, step] of csnPath.entries()){ | ||
@@ -217,4 +220,4 @@ currentThing = currentThing[step]; | ||
} | ||
if( inElement ) result += element(); | ||
if( inItems ) result += element() + '/items'; | ||
else if( inElement ) result += element(); | ||
return result; | ||
@@ -221,0 +224,0 @@ |
@@ -68,4 +68,7 @@ // Functions and classes for syntax messages | ||
'expected-type': 'A type or an element of a type is expected here', | ||
'expected-no-query': 'A type or an element of a type is expected here', | ||
'expected-entity': 'A non-abstract entity, projection or view is expected here', | ||
'expected-source': 'A query source must be a non-abstract entity or an association to an entity', | ||
'i18n-different-value': 'Different translation for key $(PROP) of language $(OTHERPROP) in unrelated layers' | ||
} | ||
@@ -76,4 +79,7 @@ | ||
'check-proper-type-of': true, | ||
'extend-repeated-intralayer': true, | ||
'extend-unrelated-layer': true, | ||
'redirected-to-ambiguous': true, | ||
'redirected-to-unrelated': true, | ||
'rewrite-not-supported': true, | ||
}; | ||
@@ -172,3 +178,3 @@ | ||
* @param {string} msg The message text | ||
* @param {string} [severity='Error'] Severity: Debug, Info, Warning, Error | ||
* @param {CSN.MessageSeverity} [severity='Error'] Severity: Debug, Info, Warning, Error | ||
* @param {string} [id] The ID of the message - visible as property messageId | ||
@@ -205,3 +211,3 @@ * @param {any} [home] | ||
* @param {string} msg The message text | ||
* @param {string} [severity='Error'] Severity: Debug, Info, Warning, Error | ||
* @param {CSN.MessageSeverity} [severity='Error'] Severity: Debug, Info, Warning, Error | ||
* @param {string} [id] The ID of the message - visible as property messageId | ||
@@ -421,4 +427,4 @@ * @param {any} [home] | ||
* Return message hash which is either the message string without the file location, | ||
* or the full message string if no semantic location is provided. | ||
* | ||
* or the full message string if no semantic location is provided. | ||
* | ||
* @param {CSN.Message} msg | ||
@@ -566,3 +572,3 @@ * @returns {string} can be used to uniquely identify a message | ||
// message already present, replace only if current msg is the more precise one | ||
if( msg.location.end.column > msg.location.start.column ) | ||
if( msg.location.end.column > msg.location.start.column ) | ||
seen.set(hash, msg); | ||
@@ -611,2 +617,4 @@ } | ||
return 'using:' + quoted( art.name.id ); | ||
else if (art.kind === 'extend') | ||
return homeNameForExtend ( art ); | ||
else if (art.name._artifact) // block, extend, annotate | ||
@@ -618,2 +626,34 @@ return homeName( art.name._artifact ); // use corresponding definition | ||
// The "home" for extensions is handled differently because `_artifact` is not | ||
// set for unknown extensions and we could have nested extensions. | ||
function homeNameForExtend( art ) { | ||
const absoluteName = (art.name.id ? art.name.id : | ||
art.name.path.map(s => s && s.id).join('.')); | ||
// Surrounding parent may be another extension. | ||
const parent = art._parent; | ||
if (!parent) | ||
return 'extend:' + quoted(absoluteName); | ||
// And that extension's artifact could have been found. | ||
const parentArt = parent.name && parent.name._artifact; | ||
if (!parentArt) | ||
return artName(parent) + '/' + quoted(absoluteName); | ||
let extensionName; | ||
if (parentArt.enum || parentArt.elements) { | ||
const fakeArt = { | ||
kind: parentArt.enum ? 'enum' : 'element', | ||
name: { element: absoluteName } | ||
}; | ||
extensionName = artName(fakeArt); | ||
} | ||
else { | ||
extensionName = 'extend:' + quoted(absoluteName); | ||
} | ||
// Even though the parent artifact was found, we use kind 'extend' | ||
// to make it clear that we are inside an (element) extension. | ||
return 'extend:' + artName(parentArt) + '/' + extensionName; | ||
} | ||
function quoted( name ) { | ||
@@ -620,0 +660,0 @@ return (name) ? '"' + name.replace( /"/g, '""' ) + '"' : '<?>'; // sync "; |
@@ -57,11 +57,2 @@ 'use strict'; | ||
function checkLocalizedSubElement(element) { | ||
if (!element.localized || element.localized.val !== true) | ||
return; | ||
if (element._parent && element._parent.kind === 'element') { | ||
message('localized-sub-element', element.localized.location, element, {}, 'Warning', 'Keyword "localized" is ignored for sub elements'); | ||
} | ||
} | ||
if(elem.key && elem.key.val === true){ | ||
@@ -75,5 +66,63 @@ if(['cds.hana.ST_POINT', 'cds.hana.ST_GEOMETRY'].includes(type)){ | ||
} | ||
checkLocalizedSubElement(elem); | ||
} | ||
/** | ||
* Non-recursive check if sub-elements have a "localized" keyword since this is | ||
* not yet supported. | ||
* | ||
* This check is not recursive to avoid a runtime overhead. Because of this it fails | ||
* to detect scenarios with indirections, e.g. | ||
* | ||
* type L : localized String; | ||
* type L1 : L; | ||
* type L2 : L1; | ||
* | ||
* entity E { | ||
* struct : { | ||
* subElement : L2; | ||
* } | ||
* } | ||
* | ||
* @param {XSN.Artifact} element | ||
* @param {XSN.Model} model | ||
*/ | ||
function checkLocalizedSubElement(element, model) { | ||
// TODO: Recursive check | ||
function hasTypeLocalizedElements(type) { | ||
if (!type) | ||
return false; | ||
for (const elementName in type.elements) { | ||
const element = type.elements[elementName]; | ||
if (element.localized && element.localized.val === true) | ||
return true; | ||
if (isTypeLocalized(element.type)) | ||
return true; | ||
} | ||
return false; | ||
} | ||
// TODO: Recursive check | ||
function isTypeLocalized(type) { | ||
return (type && type._artifact && type._artifact.localized && | ||
type._artifact.localized.val === true); | ||
} | ||
const message = getMessageFunction(model); | ||
const isSubElement = element._parent && element._parent.kind === 'element'; | ||
if (element.localized && element.localized.val === true && isSubElement) { | ||
message('localized-sub-element', element.localized.location, element, {}, | ||
'Warning', 'Keyword "localized" is ignored for sub elements'); | ||
return; | ||
} | ||
if (!element.type) | ||
return; | ||
if ((isTypeLocalized(element.type) && isSubElement) || hasTypeLocalizedElements(element._finalType)) { | ||
message('localized-sub-element', element.type.location, element, { type: element.type }, | ||
'Warning', 'Keyword "localized" in type $(TYPE) is ignored for sub elements'); | ||
} | ||
} | ||
// Perform checks for element (or type) 'elem' concerning managed associations, | ||
@@ -94,3 +143,3 @@ // only for managed assocs directly declared on 'elem', not those from derived | ||
if (!foreignKeys) { | ||
signal(error`The target "${target._artifact.name.absolute}" of the managed association "${elem.name.id}" does not have keys`, elem.location); | ||
signal(error`The target "${target._artifact.name.absolute}" of the managed association "${elem.name.id}" does not have keys`, target.location); | ||
} | ||
@@ -174,10 +223,15 @@ const targetMax = (elem.cardinality && elem.cardinality.targetMax && elem.cardinality.targetMax.val); | ||
function checkStructureCasting(elem, model) { | ||
const { error, signal } = alerts(model); | ||
if (elem.type && !elem.type.$inferred) { | ||
const message = getMessageFunction( model ); | ||
const loc = elem.type.location || elem.location; | ||
if (elem._finalType && elem._finalType.elements) | ||
signal(error`Cannot cast to structured element`, elem.location); | ||
message('type-cast-structured', loc, elem, {}, 'Error', `Cannot cast to structured element`); | ||
else if (elem.value && elem.value._artifact && elem.value._artifact._finalType && elem.value._artifact._finalType.elements) | ||
signal(error`Structured element cannot be casted to a different type`, elem.location); | ||
message('type-cast-structured', loc, elem, {}, 'Error', `Structured element cannot be casted to a different type`); | ||
} | ||
if (elem.value && elem.value.args) { | ||
elem.value.args.forEach(arg => checkStructureCasting(arg, model)); | ||
} | ||
} | ||
@@ -499,2 +553,3 @@ | ||
checkPrimaryKey, | ||
checkLocalizedSubElement, | ||
checkManagedAssoc, | ||
@@ -501,0 +556,0 @@ checkVirtualElement, |
@@ -49,3 +49,3 @@ 'use strict'; | ||
const { error, signal } = alerts(model); | ||
const { isAssociationOperand, isDollarSelfOperand } = transformUtils.getTransformers(model); | ||
const { isAssociationOperand, isDollarSelfOrProjectionOperand } = transformUtils.getTransformers(model); | ||
@@ -66,3 +66,3 @@ // No further checks regarding associations and $self required if this is a backlink-like expression | ||
} | ||
if (isDollarSelfOperand(arg)) { | ||
if (isDollarSelfOrProjectionOperand(arg)) { | ||
signal(error`"${arg.path[0].id}" can only be used as a value in a comparison to an association`, arg.location); | ||
@@ -90,4 +90,4 @@ } | ||
// Tree-ish expression from the compiler (not augmented) | ||
return (isAssociationOperand(xpr.args[0]) && isDollarSelfOperand(xpr.args[1]) | ||
|| isAssociationOperand(xpr.args[1]) && isDollarSelfOperand(xpr.args[0])); | ||
return (isAssociationOperand(xpr.args[0]) && isDollarSelfOrProjectionOperand(xpr.args[1]) | ||
|| isAssociationOperand(xpr.args[1]) && isDollarSelfOrProjectionOperand(xpr.args[0])); | ||
} | ||
@@ -94,0 +94,0 @@ |
@@ -32,3 +32,3 @@ 'use strict'; | ||
} | ||
if(this.artifact && ( this.artifact.kind === 'entity' || this.artifact.query ) && member && member.items){ | ||
if(this.artifact && ( this.artifact.kind === 'entity' || this.artifact.query ) && member && member.items && member.$path[2] == 'elements'){ | ||
if(member.items.type && member.items.type.ref){ | ||
@@ -35,0 +35,0 @@ validate(this.artifactRef(member.items.type)); |
@@ -14,3 +14,3 @@ 'use strict'; | ||
checkLocalizedElement, checkStructureCasting, checkForItemsChain, checkTypeStructure, | ||
checkElementHasValidTypeOf } = require('./checkElements'); | ||
checkElementHasValidTypeOf, checkLocalizedSubElement } = require('./checkElements'); | ||
const { checkExpression } = require('./checkExpressions'); | ||
@@ -270,7 +270,7 @@ const { foreachPath } = require('../model/modelUtils'); | ||
const finalTypeParams = targetFinalType ? targetFinalType.params : null; | ||
compareArgs(pathStep.namedArgs, pathStep.args, finalTypeParams); | ||
compareActualNamedArgsWithFormalNamedArgs(pathStep.namedArgs, finalTypeParams); | ||
} else { | ||
// Parameters can only be provided when navigating along associations, so because this path | ||
// is for non-associations, checking arguments along a navigation is unnecessary and faulty. | ||
compareArgs(pathStep.namedArgs, pathStep.args, pathStep._artifact.params); | ||
compareActualNamedArgsWithFormalNamedArgs(pathStep.namedArgs, pathStep._artifact.params); | ||
} | ||
@@ -280,32 +280,38 @@ | ||
* Compare two argument dictionaries for correct argument count. | ||
* @param {object} namedArgsGiven | ||
* @param {object[]} unnamedArgsGiven | ||
* @param {object} argsExpected | ||
* @param {object} actualArgs | ||
* @param {object} formalArgs | ||
*/ | ||
function compareArgs(namedArgsGiven, unnamedArgsGiven, argsExpected) { | ||
namedArgsGiven = namedArgsGiven || {}; | ||
unnamedArgsGiven = unnamedArgsGiven || []; | ||
argsExpected = argsExpected || {}; | ||
function compareActualNamedArgsWithFormalNamedArgs(actualArgs, formalArgs) { | ||
actualArgs = actualArgs || {}; | ||
formalArgs = formalArgs || {}; | ||
const givenNames = Object.keys(namedArgsGiven); | ||
const expectedNames = Object.keys(argsExpected); | ||
const aArgsCount = Object.keys(actualArgs).length; | ||
const expectedNames = Object.keys(formalArgs); | ||
if (unnamedArgsGiven.length) { | ||
if (unnamedArgsGiven.length != expectedNames.length) { | ||
message(undefined, pathStep.location, pathStep, | ||
{ expected: expectedNames.length, given: givenNames.length }, | ||
'Error', | ||
'Expected $(EXPECTED) arguments but $(GIVEN) given' | ||
); | ||
} | ||
} else { | ||
if (givenNames.length != expectedNames.length) { | ||
const missingArguments = expectedNames.filter((name) => !givenNames.includes(name)); | ||
message(undefined, pathStep.location, pathStep, | ||
{ names: missingArguments, expected: expectedNames.length, given: givenNames.length }, | ||
'Error', | ||
'Expected $(EXPECTED) arguments but $(GIVEN) given; missing: $(NAMES)' | ||
); | ||
} | ||
const missingArgs = []; | ||
const unknownArgs = []; | ||
for(const fAName in formalArgs) { | ||
if(!actualArgs[fAName] && !formalArgs[fAName].default) | ||
missingArgs.push(fAName); | ||
} | ||
for(const aAName in actualArgs) { | ||
if(!formalArgs[aAName]) | ||
unknownArgs.push(aAName); | ||
} | ||
if(missingArgs.length) { | ||
message(undefined, pathStep.location, pathStep, | ||
{ names: missingArgs, expected: expectedNames.length, given: aArgsCount }, | ||
'Error', | ||
'Expected $(EXPECTED) arguments but $(GIVEN) given; missing: $(NAMES)' | ||
); | ||
} | ||
// already checked in resolver | ||
if(unknownArgs.length) { | ||
message(undefined, pathStep.location, pathStep, | ||
{ names: unknownArgs, expected: expectedNames.length, given: aArgsCount }, | ||
'Error', | ||
'Expected $(EXPECTED) arguments but $(GIVEN) given; unknown: $(NAMES)' | ||
); | ||
} | ||
} | ||
@@ -325,2 +331,3 @@ } | ||
checkPrimaryKey(elem, model); | ||
checkLocalizedSubElement(elem, model); | ||
checkVirtualElement(elem, model); | ||
@@ -327,0 +334,0 @@ checkManagedAssoc(elem, model); |
@@ -72,2 +72,8 @@ // Consistency checker on model (XSN = augmented CSN) | ||
// Properties that can appear where a type can have type arguments. | ||
const typeProperties = [ | ||
'type', 'typeArguments', 'length', 'precision', 'scale', 'srid', | ||
]; | ||
function assertConsistency( model, stage ) { | ||
@@ -85,2 +91,3 @@ const stageParser = typeof stage === 'object'; | ||
'extensions', | ||
'i18n', | ||
'version', '$version', // version without --test-mode | ||
@@ -100,3 +107,4 @@ 'meta', | ||
optional: [ | ||
'messages', 'options', 'definitions', 'extensions', | ||
'messages', 'options', 'definitions', | ||
'extensions', 'i18n', | ||
'artifacts', 'artifacts_', 'namespace', 'usings', // CDL parser | ||
@@ -166,2 +174,11 @@ 'filename', 'dirname', // TODO: move filename into a normal location? Only in model.sources | ||
}, | ||
i18n: { | ||
test: isDictionary( ( val, parent, prop, spec, lang ) => { | ||
const textValueIsString = (v, p, textProp, s, textKey) => { | ||
isString(v.val, p, textKey, s); | ||
}; | ||
const innerDict = isDictionary( textValueIsString ); | ||
return innerDict( val, parent, lang, spec ); | ||
} ), | ||
}, | ||
_localized: { kind: true, test: TODO }, // true or artifact | ||
@@ -417,3 +434,7 @@ _assocSources: { kind: true, test: TODO }, // just null: isArray( inDefinitions ) during resolve | ||
struct: { inherits: 'val', test: isDictionary( definition ) }, // def because double @ | ||
args: { inherits: 'value', test: args }, | ||
args: { | ||
inherits: 'value', | ||
test: args, | ||
optional: [ '_typeIsExplicit', ...typeProperties ], // for cast() in expressions | ||
}, | ||
namedArgs: { inherits: 'value', optional: [ 'name', '$duplicate' ], test: args }, | ||
@@ -472,6 +493,9 @@ onCond: { kind: true, inherits: 'value' }, | ||
optional: [ | ||
'type', 'typeArguments', 'length', 'precision', 'scale', 'srid', 'enum', | ||
'enum', | ||
'elements', 'cardinality', 'target', 'on', 'onCond', 'foreignKeys', 'items', | ||
'_outer', '_finalType', 'notNull', | ||
'origin', '_block', '$inferred', '_deps', | ||
'elements_', '_elementsIndexNo', '$syntax', // TODO: remove | ||
'_foreignKeysIndexNo', '_status', | ||
...typeProperties, | ||
], | ||
@@ -478,0 +502,0 @@ }, |
@@ -145,5 +145,14 @@ // | ||
function always( prop, target, source ) { | ||
target[prop] = Object.assign( { $inferred: 'prop' }, source[prop] ); | ||
if ('_artifact' in source[prop]) | ||
setProp( target[prop], '_artifact', source[prop]._artifact ); | ||
const val = source[prop]; | ||
if (Array.isArray( val )) { | ||
target[prop] = [ ...val ]; | ||
target[prop].$inferred = 'prop'; | ||
} | ||
else if ('_artifact' in val) { | ||
target[prop] = Object.assign( { $inferred: 'prop' }, val ); | ||
setProp( target[prop], '_artifact', val._artifact ); | ||
} | ||
else { | ||
target[prop] = Object.assign( { $inferred: 'prop' }, val ); | ||
} | ||
} | ||
@@ -150,0 +159,0 @@ |
@@ -91,3 +91,7 @@ // Compiler functions and utilities shared across all phases | ||
}, | ||
type: { reject: rejectNonType }, // TODO: more detailed later (e.g. for enum base type?) | ||
type: { // TODO: more detailed later (e.g. for enum base type?) | ||
reject: rejectNonType, | ||
deprecateSmart: true, | ||
warn: warnAboutElementWithNonType, | ||
}, | ||
// if we want to disallow assoc nav for TYPE, do not do it her | ||
@@ -107,4 +111,9 @@ typeOf: { next: '_$next' }, | ||
filter: { next: '_$next', lexical: 'main' }, | ||
from: { reject: rejectNonSource, assoc: 'from', argsSpec: 'expr' }, | ||
const: { next: '_$next', reject: rejectNonConst }, | ||
from: { | ||
reject: rejectNonSource, | ||
assoc: 'from', | ||
argsSpec: 'expr', | ||
deprecateSmart: true, | ||
}, | ||
const: { next: '_$next', reject: rejectNonConst }, // DEFAULT | ||
expr: { // in: from-on, | ||
@@ -115,3 +124,2 @@ next: '_$next', escape: 'param', assoc: 'nav', | ||
noAliasOrMixin: true, // TODO: some headReject or similar | ||
escape: 'param', // meaning of ':' in front of path? search in 'params' | ||
next: '_$next', // TODO: lexical: ... how to find the (next) lexical environment | ||
@@ -122,3 +130,3 @@ rootEnv: 'elements', // the final environment for the path root | ||
'mixin-on': { | ||
escape: 'param', // meaning of ':' in front of path? search in 'params' | ||
escape: 'param', // TODO: extra check that assocs containing param in ON is not published | ||
next: '_$next', // TODO: lexical: ... how to find the (next) lexical environment | ||
@@ -181,2 +189,14 @@ noDep: true, // do not set dependency for circular-check | ||
/** | ||
* Warns about artifacts that cannot be used as an element type. | ||
* | ||
* @param {XSN.Artifact} art | ||
* @param {XSN.Artifact} user | ||
*/ | ||
function warnAboutElementWithNonType( art, user ) { | ||
return (user.kind === 'element' && art.query) | ||
? 'expected-no-query' | ||
: undefined; | ||
} | ||
function rejectNonEntity( art ) { | ||
@@ -254,8 +274,4 @@ return ([ 'view', 'entity' ].includes( art.kind ) && !(art.abstract && art.abstract.val)) | ||
if (!spec.escape) { | ||
const variant = (env.$frontend && env.$frontend !== 'cdl') ? 'std' : 'cdl'; | ||
message( 'ref-unexpected-scope', head.location, user, { name: head.id, '#': variant }, | ||
'Error', { | ||
std: 'Unexpected parameter scope for name $(NAME)', | ||
cdl: 'Unexpected `:` before name $(NAME)', | ||
} ); | ||
message( 'ref-unexpected-scope', ref.location, user, {}, | ||
'Error', 'Unexpected parameter reference' ); | ||
return setLink( ref, null ); | ||
@@ -362,2 +378,7 @@ } | ||
} | ||
if (spec.warn) { | ||
const msgId = spec.warn( art, user ); | ||
if (msgId) | ||
message( msgId, ref.location, user, {}, 'Warning' ); | ||
} | ||
if (user && (!spec.noDep || | ||
@@ -380,5 +401,31 @@ spec.noDep === 'only-entity' && art.kind !== 'entity' && art.kind !== 'view')) { | ||
} | ||
// Warning for CDL TYPE OF references without ':' or shifted ':' | ||
if (spec.deprecateSmart && typeof ref.scope === 'number' && | ||
!(env.$frontend && env.$frontend !== 'cdl')) | ||
deprecateSmart( ref, art, user ); | ||
return setLink( ref, art ); | ||
} | ||
// Issue warnings for deprecates "smart" element-in-artifact references | ||
// without a colon; and warnings for misplaced colons in references. | ||
// This function likely disappears again in cds-compiler v2.x. | ||
function deprecateSmart( ref, art, user ) { | ||
const { path } = ref; | ||
const scope = path.findIndex( i => i._artifact._main ); | ||
if (ref.scope) { // provided a ':' in the ref path | ||
if (scope === ref.scope) // correctly between main artifact and element | ||
return; | ||
const item = path[ref.scope]; | ||
message( 'ref-unexpected-colon', item.location, user, { id: item.id }, | ||
'Warning', 'Replace the colon before $(ID) by a dot' ); | ||
ref.scope = 0; // correct (otherwise CSN refs are wrong) | ||
} | ||
if (scope >= 0) { // we have a element-in-artifact reference | ||
const item = path[scope]; | ||
message( 'ref-missing-colon', item.location, user, { id: item.id }, | ||
'Warning', 'Replace the dot before $(ID) by a colon' ); | ||
ref.scope = scope; // no need to recalculate in to-csn.js | ||
} | ||
} | ||
// Resolve the type arguments provided with a type referenced for artifact or | ||
@@ -509,3 +556,3 @@ // element `artifact`. This function does nothing if the referred type | ||
if (spec.noMessage || msgArt === true && extDict === model.definitions) | ||
return setLink( head, null ); | ||
return null; | ||
@@ -690,6 +737,18 @@ const valid = []; | ||
function defineAnnotations( construct, art, block, priority ) { | ||
// namespaces cannot be annotated but we check for it because of | ||
// builtin contexts that appear as 'namespace' | ||
if (art.kind === 'namespace') | ||
return; | ||
if (!options.parseCdl && construct.kind === 'annotate') { | ||
// Namespaces cannot be annotated in CSN but because they exist as XSN artifacts | ||
// they can still be applied. Namespace annotations are extracted in to-csn.js | ||
// In parseCdl mode USINGs and other unknown references are generated as | ||
// namespaces which would lead to false positives. | ||
if (art.kind === 'namespace') { | ||
message( 'anno-namespace', construct.name.location, construct, {}, 'Info', | ||
'Namespaces cannot be annotated' ); | ||
} | ||
// Builtin annotations would also get lost. Same as for namespaces: | ||
// extracted in to-csn.js | ||
else if (art.builtin === true) { | ||
message( 'anno-builtin', construct.name.location, construct, {}, 'Info', | ||
'Builtin types should not be annotated. Use custom type instead.' ); | ||
} | ||
} | ||
// TODO: block should be construct._block | ||
@@ -701,2 +760,4 @@ if (construct.annotationAssignments && construct.annotationAssignments.doc ) | ||
return; | ||
// annotationAssignments is set if parsed from CDL but may not be set if | ||
// the input is CSN so we extract them. | ||
for (const annoProp in construct) { | ||
@@ -709,3 +770,3 @@ if (annoProp.charAt(0) === '@') { | ||
setProp( a, '_block', block ); | ||
addToDict( art, annoProp, a ); | ||
setAnnotation(art, annoProp, a, priority); | ||
} | ||
@@ -757,4 +818,2 @@ } | ||
}; | ||
if (priority) | ||
anno.priority = priority; | ||
setProp( anno, '_block', block ); | ||
@@ -809,2 +868,3 @@ // TODO: _parent, _main is set later (if we have ElementRef), or do we | ||
// TODO: make this just elem._origin, remove elem.origin | ||
setProp( elem, '_deps', [ { art: origin, location } ] ); | ||
return elem; | ||
@@ -811,0 +871,0 @@ } |
@@ -169,5 +169,5 @@ 'use strict'; | ||
if(properties.length === 0) { | ||
signal(signal.error`EntityType "${serviceName}.${EntityTypeName}" has no properties`, ['definitions',entityCsn.name]); | ||
signal(signal.warning`EntityType "${serviceName}.${EntityTypeName}" has no properties`, ['definitions',entityCsn.name]); | ||
} else if(entityCsn.$edmKeyPaths.length === 0) { | ||
signal(signal.error`EntityType "${serviceName}.${EntityTypeName}" has no primary key`, ['definitions',entityCsn.name]); | ||
signal(signal.warning`EntityType "${serviceName}.${EntityTypeName}" has no primary key`, ['definitions',entityCsn.name]); | ||
} | ||
@@ -440,3 +440,3 @@ | ||
if(properties.length === 0) { | ||
signal(signal.error`ComplexType "${structuredTypeCsn.name}" has no properties`, ['definitions', structuredTypeCsn.name]); | ||
signal(signal.warning`ComplexType "${structuredTypeCsn.name}" has no properties`, ['definitions', structuredTypeCsn.name]); | ||
} | ||
@@ -504,3 +504,3 @@ complexType.append(...(properties)); | ||
if(fromRole === toRole) { | ||
if(constraints._originAssocCsn) | ||
if(constraints._partnerCsn) | ||
fromRole += '1'; | ||
@@ -525,3 +525,3 @@ else | ||
/* | ||
If NavigationProperty is a backlink association (constraints._originAssocCsn is set), then there are two options: | ||
If NavigationProperty is a backlink association (constraints._partnerCsn is set), then there are two options: | ||
1) Counterpart NavigationProperty exists and is responsible to create the edm:Association element which needs to | ||
@@ -534,3 +534,3 @@ be reused by this backlink association. This is save because at this point of the processing all NavProps are created. | ||
let reuseAssoc = false; | ||
let forwardAssocCsn = constraints._originAssocCsn; | ||
let forwardAssocCsn = constraints._partnerCsn; | ||
if(forwardAssocCsn) | ||
@@ -553,3 +553,3 @@ { | ||
reuseAssoc = !!forwardAssocCsn._NavigationProperty; | ||
constraints = edmUtils.getReferentialConstraints(forwardAssocCsn, signal, options); | ||
constraints = forwardAssocCsn._constraints; | ||
constraints._multiplicity = edmUtils.determineMultiplicity(forwardAssocCsn); | ||
@@ -556,0 +556,0 @@ } |
@@ -834,6 +834,22 @@ // @ts-nocheck | ||
if (csn.default && isBetaEnabled(options, 'odataDefaultValues')) | ||
// OData only allows simple values, no complex expressions or function calls | ||
// This is a poor man's expr renderer, assuming that this value has passed | ||
// the defaultValues validator | ||
if (csn.default && isBetaEnabled(options, 'odataDefaultValues')) { | ||
let defVal = csn.default.val; | ||
if(csn.default.xpr) { | ||
defVal = csn.default.xpr.map(i => { | ||
if(i.val !== undefined) { | ||
if(csn.type === 'cds.Boolean') | ||
return i.val ? 'true' : 'false'; | ||
return i.val; | ||
} | ||
return i; | ||
}).join(''); | ||
} | ||
this[`Default${this.v4 ? 'Value' : ''}`] = ['cds.Boolean', 'cds.Binary', 'cds.LargeBinary', 'cds.Integer64', 'cds.Integer'].includes(csn.type) | ||
? csn.default.val | ||
: edmUtils.escapeString(csn.default.val); | ||
? defVal | ||
: edmUtils.escapeString(defVal); | ||
} | ||
} | ||
@@ -889,4 +905,4 @@ | ||
let [src, tgt] = edmUtils.determineMultiplicity(csn._constraints._originAssocCsn || csn); | ||
csn._constraints._multiplicity = csn._constraints._originAssocCsn ? [tgt, src] : [src, tgt]; | ||
let [src, tgt] = edmUtils.determineMultiplicity(csn._constraints._partnerCsn || csn); | ||
csn._constraints._multiplicity = csn._constraints._partnerCsn ? [tgt, src] : [src, tgt]; | ||
@@ -910,3 +926,3 @@ this.set( { | ||
} | ||
let partner = (csn._partnerCsn.length > 0) ? csn._partnerCsn[0] : csn._constraints._originAssocCsn; | ||
let partner = (csn._selfReferences.length > 0) ? csn._selfReferences[0] : csn._constraints._partnerCsn; | ||
if(partner && partner['@odata.navigable'] !== false) { | ||
@@ -913,0 +929,0 @@ this.Partner = partner.name; |
'use strict'; | ||
/* eslint max-statements-per-line:off */ | ||
const { setProp, isBetaEnabled } = require('../base/model'); | ||
const { forEachDefinition, isEdmPropertyRendered, forEachMemberRecursively, getUtils, cloneCsn, isBuiltinType } = require('../model/csnUtils'); | ||
const { forEachDefinition, forEachGeneric, forEachMember, forEachMemberRecursively, | ||
isEdmPropertyRendered, getUtils, cloneCsn, isBuiltinType } = require('../model/csnUtils'); | ||
const alerts = require('../base/alerts'); | ||
@@ -16,7 +17,7 @@ const edmUtils = require('./edmUtils.js') | ||
isStructuredArtifact, | ||
isEntityOrView, | ||
isParameterizedEntityOrView, | ||
isActionOrFunction, | ||
getReferentialConstraints, | ||
resolveOnConditionAndPrepareConstraints, | ||
finalizeReferentialConstraints, | ||
isSimpleIdentifier, | ||
isEntityOrView, | ||
} = require('./edmUtils.js'); | ||
@@ -52,10 +53,16 @@ | ||
// First attach names to all definitions in the model | ||
forAll(csn.definitions, (a, n) => { | ||
assignProp (a, 'name', n); | ||
}); | ||
foreach(csn.definitions, isActionOrFunction, a => { | ||
forAll(a.params, (p,n) => { | ||
setProp (p, 'name', n); | ||
// First attach names to all definitions (and actions/params) in the model | ||
// elements are done in initializeStruct | ||
forEachDefinition(csn, (def, defName) => { | ||
assignProp (def, 'name', defName); | ||
// Attach name to bound actions, functions and parameters | ||
forEachGeneric(def, 'actions', (a, n) => { | ||
assignProp(a, 'name', n); | ||
forEachGeneric(a, 'params', (p, n) => { | ||
assignProp(p, 'name', n); | ||
}); | ||
}); | ||
// Attach name unbound action parameters | ||
forEachGeneric(def, 'params', (p,n) => { | ||
assignProp(p, 'name', n); | ||
}) | ||
@@ -65,4 +72,4 @@ }); | ||
// Fetch service objects | ||
let services = Object.keys(csn.definitions).reduce((services, artName) => { | ||
let art = csn.definitions[artName]; | ||
const services = Object.keys(csn.definitions).reduce((services, artName) => { | ||
const art = csn.definitions[artName]; | ||
if(art.kind === 'service' && !art.abstract) { | ||
@@ -74,72 +81,62 @@ services.push(art); | ||
let serviceNames = services.map(s => s.name); | ||
function whatsMyServiceName(n) { | ||
return serviceNames.reduce((rc, sn) => n.startsWith(sn + '.') ? rc = sn : rc, undefined); | ||
} | ||
const serviceNames = services.map(s => s.name); | ||
if(serviceNames.length) { | ||
services.forEach(initializeService); | ||
// Set myServiceName for later reference and indication of a service member | ||
// First attach names to all definitions in the model | ||
// Link association targets and spray @odata.contained over untagged compositions | ||
foreach(csn.definitions, isStructuredArtifact, linkAssociationTarget); | ||
forEachDefinition(csn, [ (def, defName) => { | ||
setProp(def, '$myServiceName', whatsMyServiceName(defName)) }, linkAssociationTarget ]); | ||
// Create data structures for containments | ||
foreach(csn.definitions, isStructuredArtifact, initializeContainments); | ||
forEachDefinition(csn, initializeContainments); | ||
// Initialize entities with parameters (add Parameter entity) | ||
foreach(csn.definitions, isParameterizedEntityOrView, initializeParameterizedEntityOrView); | ||
forEachDefinition(csn, initializeParameterizedEntityOrView); | ||
// Initialize structures | ||
foreach(csn.definitions, isStructuredArtifact, initializeStructure); | ||
// Initialize associations | ||
foreach(csn.definitions, isStructuredArtifact, initializeAssociation); | ||
// get constraints for associations | ||
foreach(csn.definitions, isStructuredArtifact, initializeConstraints); | ||
forEachDefinition(csn, initializeStructure); | ||
// Initialize associations after _parent linking | ||
forEachDefinition(csn, prepareConstraints); | ||
// Mute V4 elements depending on constraint preparation | ||
if(options.isV4()) | ||
forEachDefinition(csn, ignoreProperties); | ||
// calculate constraints based on mutePropertiesForV4 and prepareConstraints | ||
forEachDefinition(csn, finalizeConstraints); | ||
// create association target proxies | ||
foreach(csn.definitions, isStructuredArtifact, redirectDanglingAssociationsToProxyTargets); | ||
// create edmKeyRefPaths | ||
foreach(csn.definitions, isEntityOrView, initializeEdmKeyRefPaths); | ||
forEachDefinition(csn, redirectDanglingAssociationsToProxyTargets); | ||
// decide if an entity set needs to be constructed or not | ||
foreach(csn.definitions, isStructuredArtifact, determineEntitySet); | ||
// Check the artifact identifier for compliance with the odata specification | ||
forAll(csn.definitions, checkArtifactIdentifier); | ||
// 1. let all doc props become @Core.Descriptions | ||
// 2. mark a member that will become a collection | ||
// 3. assign the edm primitive type to elements, to be used in the rendering later | ||
forEachDefinition(csn, artifact => { | ||
assignAnnotation(artifact, '@Core.Description', artifact.doc); | ||
markCollection(artifact); | ||
mapCdsToEdmProp(artifact); | ||
if (artifact.returns) { | ||
markCollection(artifact.returns); | ||
mapCdsToEdmProp(artifact.returns); | ||
} | ||
forEachMemberRecursively(artifact, | ||
member => { | ||
assignAnnotation(member, '@Core.Description', member.doc); | ||
markCollection(member); | ||
mapCdsToEdmProp(member); | ||
if (member.returns) { | ||
markCollection(member.returns); | ||
mapCdsToEdmProp(member.returns); | ||
} | ||
} | ||
); | ||
}); | ||
// Things that can be done in one pass | ||
// Create edmKeyRefPaths | ||
// Decide if an entity set needs to be constructed or not | ||
// Map /** doc comments */ to @CoreDescription | ||
// Artifact identifier spec compliance check (should be run last) | ||
forEachDefinition(csn, | ||
[ initializeEdmKeyRefPaths, determineEntitySet, mapDocCommentToCoreDescription, checkArtifactIdentifier ]); | ||
} | ||
return [services, options]; | ||
////////////////////////////////////////////////////////////////////// | ||
// | ||
// Service initialization starts here | ||
// | ||
// initialize the service itself | ||
function initializeService(service) { | ||
checkServiceIdentifier(service.name); | ||
setSAPSpecificV2AnnotationsToEntityContainer(options, service); | ||
} | ||
function checkServiceIdentifier(serviceName) { | ||
if (serviceName.length > 511) { | ||
// check service name | ||
if (service.name.length > 511) { | ||
// don't show long service name in message | ||
signalIllegalIdentifier(false, ['definitions', serviceName], 'namespace', 'must not exceed 511 characters'); | ||
signalIllegalIdentifier(false, ['definitions', service.name], 'namespace', 'must not exceed 511 characters'); | ||
} | ||
const simpleIdentifiers = serviceName.split('.'); | ||
const simpleIdentifiers = service.name.split('.'); | ||
simpleIdentifiers.forEach((identifier) => { | ||
if (!isSimpleIdentifier(identifier)) { | ||
// don't show long service name in message | ||
signalIllegalIdentifier(false, ['definitions', serviceName], 'namespace', | ||
signalIllegalIdentifier(false, ['definitions', service.name], 'namespace', | ||
'must consist of one or more dot separated simple identifiers (each starting with a letter or underscore, followed by at most 127 letters)'); | ||
} | ||
}); | ||
setSAPSpecificV2AnnotationsToEntityContainer(options, service); | ||
} | ||
@@ -149,20 +146,20 @@ | ||
function linkAssociationTarget(struct) { | ||
foreach(struct.elements, isAssociationOrComposition, (element, name) => { | ||
if (element._ignore) | ||
return; | ||
if(!element._target) { | ||
let target = csn.definitions[element.target]; | ||
if(target) { | ||
setProp(element, '_target', target); | ||
forEachMemberRecursively(struct, (element, name, prop, subpath) => { | ||
if(isAssociationOrComposition(element) && !element._ignore) { | ||
if(!element._target) { | ||
let target = csn.definitions[element.target]; | ||
if(target) { | ||
setProp(element, '_target', target); | ||
// If target has parameters, xref assoc at target for redirection | ||
if(isParameterizedEntityOrView(target)) { | ||
if(!target.$sources) { | ||
setProp(target, '$sources', Object.create(null)); | ||
if(isParameterizedEntityOrView(target)) { | ||
if(!target.$sources) { | ||
setProp(target, '$sources', Object.create(null)); | ||
} | ||
target.$sources[struct.name + '.' + name] = element; | ||
} | ||
target.$sources[struct.name + '.' + name] = element; | ||
} | ||
else { | ||
signal(signal.error`Target ${element.target} cannot be found in the model`, subpath); | ||
} | ||
} | ||
else { | ||
signal(signal.error`Target ${element.target} cannot be found in the model`, [ 'definitions', struct.name, 'elements', element.name ]); | ||
} | ||
} | ||
@@ -193,28 +190,28 @@ // in V4 tag all compositions to be containments | ||
function initializeContainments(container) { | ||
foreach(container.elements, isAssociationOrComposition, (element, elementName) => { | ||
if (element._ignore) | ||
return; | ||
if(element['@odata.contained']) { | ||
forEachMemberRecursively(container, (element, elementName) => { | ||
if(isAssociationOrComposition(element) && !element._ignore) { | ||
if(element['@odata.contained']) { | ||
// Let the containee know its container | ||
// (array because the contanee may contained more then once) | ||
let containee = element._target; | ||
if (!containee._containerEntity) { | ||
setProp(containee, '_containerEntity', []); | ||
} | ||
let containee = element._target; | ||
if (!containee._containerEntity) { | ||
setProp(containee, '_containerEntity', []); | ||
} | ||
// add container only once per containee | ||
if (!containee._containerEntity.includes(container.name)) { | ||
containee._containerEntity.push(container.name); | ||
if (!containee._containerEntity.includes(container.name)) { | ||
containee._containerEntity.push(container.name); | ||
// Mark associations in the containee pointing to the container (i.e. to this entity) | ||
for (let containeeElementName in containee.elements) { | ||
let containeeElement = containee.elements[containeeElementName]; | ||
if (containeeElement._target && containeeElement._target.name) { | ||
for (let containeeElementName in containee.elements) { | ||
let containeeElement = containee.elements[containeeElementName]; | ||
if (containeeElement._target && containeeElement._target.name) { | ||
// If this is an association that points to a container (but is not by itself contained, | ||
// which would indicate the top role in a hierarchy) mark it with '_isToContainer' | ||
if (containeeElement._target.name == container.name && !containeeElement['@odata.contained']) { | ||
setProp(containeeElement, '_isToContainer', true); | ||
if (containeeElement._target.name == container.name && !containeeElement['@odata.contained']) { | ||
setProp(containeeElement, '_isToContainer', true); | ||
} | ||
} | ||
} | ||
} | ||
rewriteContainmentAnnotations(container, containee, elementName); | ||
} | ||
rewriteContainmentAnnotations(container, containee, elementName); | ||
} | ||
@@ -224,32 +221,2 @@ }); | ||
function signalIllegalIdentifier(identifier, path, kind, msg) { | ||
signal(signal.warning`OData ${kind} ${identifier ? `: "${identifier}"` : ''} ${ msg ? msg : 'must start with a letter or underscore, followed by at most 127 letters, underscores or digits' }`, path); | ||
} | ||
// Check the artifact identifier for compliance with the odata specification | ||
function checkArtifactIdentifier(artifact) { | ||
const serviceName = whatsMyServiceName(artifact.name); | ||
if(serviceName) { | ||
const artifactName = artifact.name.replace(serviceName + '.', ''); | ||
if(artifact.kind === 'action' || artifact.kind === 'function'){ | ||
checkActionOrFunctionIdentifier(artifact, artifactName); | ||
} else if(!isSimpleIdentifier(artifactName)){ | ||
signalIllegalIdentifier(artifactName, ['definitions', artifact.name], 'entity name'); | ||
} | ||
} | ||
function checkActionOrFunctionIdentifier(actionOrFunction, actionOrFunctionName) { | ||
if(!isSimpleIdentifier(actionOrFunctionName)){ | ||
signalIllegalIdentifier(actionOrFunctionName, actionOrFunction.$path, 'function or action name'); | ||
} | ||
if(actionOrFunction.params) { | ||
forAll(actionOrFunction.params, (param) => { | ||
if(!isSimpleIdentifier(param.name)){ | ||
signalIllegalIdentifier(param.name, param.$path, 'function or action parameter name'); | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
// Split an entity with parameters into two entity types with their entity sets, | ||
@@ -262,4 +229,9 @@ // one named <name>Parameter and one named <name>Type. Parameter contains Type. | ||
// must be called. | ||
// As a param entity is a potential proxy candidate, this split must be performed on | ||
// all definitions | ||
function initializeParameterizedEntityOrView(entityCsn, entityName) { | ||
if(!isParameterizedEntityOrView(entityCsn)) | ||
return; | ||
// Naming rules for aggregated views with parameters | ||
@@ -308,2 +280,3 @@ // Parameters: EntityType <ViewName>Parameters, EntitySet <ViewName> | ||
setProp(parameterCsn, '$isParamEntity', true); | ||
setProp(parameterCsn, '$myServiceName', entityCsn.$myServiceName); | ||
@@ -319,3 +292,3 @@ // propagate containment information, if containment is recursive, use parameterCsn.name as _containerEntity | ||
forAll(entityCsn.params, (p,n) => { | ||
forEachGeneric(entityCsn, 'params', (p,n) => { | ||
let elt = cloneCsn(p); | ||
@@ -353,3 +326,3 @@ elt.name = n; | ||
}; | ||
setProp(entityCsn.elements[backlinkAssocName], '_partnerCsn', []); | ||
setProp(entityCsn.elements[backlinkAssocName], '_selfReferences', []); | ||
setProp(entityCsn.elements[backlinkAssocName], '_target', parameterCsn); | ||
@@ -369,2 +342,4 @@ } | ||
Object.keys(entityCsn.$sources || {}).forEach(n => { | ||
// preserve the original target for constraint calculation | ||
setProp(entityCsn.$sources[n], '_originalTarget', entityCsn.$sources[n]._target); | ||
entityCsn.$sources[n]._target = parameterCsn; | ||
@@ -376,9 +351,2 @@ entityCsn.$sources[n].target = parameterCsn.name; | ||
// Initialize structured artifact (type or entity) 'struct' by doing the | ||
// following: | ||
// - attach attributes 'name', 'Name' to elements (FIXME: We currently really require both 'Name' and 'name'!) | ||
// - create a property 'keys' with all its primary key elements | ||
// - optionally add the magic ValueList association | ||
// - call 'initializeAssociation' for each element that has an association type | ||
// - attach attribute 'name' to all actions and their parameters. | ||
@@ -390,3 +358,8 @@ function initElement(element, name, struct) { | ||
function initializeStructure(struct) { | ||
// Initialize a structured artifact | ||
function initializeStructure(def) { | ||
if(!isStructuredArtifact(def)) | ||
return; | ||
let keys = Object.create(null); | ||
@@ -396,4 +369,4 @@ let validFrom = [], validKey = []; | ||
// Iterate all struct elements | ||
forAll(struct.elements, (element, elementName) => { | ||
initElement(element, elementName, struct); | ||
forEachGeneric(def, 'elements', (element, elementName) => { | ||
initElement(element, elementName, def); | ||
@@ -403,6 +376,7 @@ if(!isSimpleIdentifier(elementName)) { | ||
signal.warning`OData property name: "${elementName}" must start with a letter or underscore, followed by at most 127 letters, underscores or digits`, | ||
['definitions', struct.name, 'elements', elementName] | ||
['definitions', def.name, 'elements', elementName] | ||
); | ||
} | ||
// collect temporal information | ||
if(element['@cds.valid.key']) { | ||
@@ -415,61 +389,23 @@ validKey.push(element); | ||
if(options.isV4()) { | ||
/* Do not expose | ||
1) foreign keys in structured mode (always) | ||
1a) foreign keys of associations to container | ||
(but only for containment establishing | ||
association, see FIXME below) | ||
2) elements that are tagged with @odata.containment.ignore | ||
and parent is containee | ||
*/ | ||
if(!element.target) { | ||
if(element['@odata.foreignKey4']) { | ||
let isContainerAssoc = false; | ||
let elements = struct.elements; | ||
let assoc = undefined; | ||
let paths = element['@odata.foreignKey4'].split('.') | ||
for(let p of paths) { | ||
assoc = elements[p]; | ||
if(assoc) // could be that the @odata.foreignKey4 was propagated... | ||
elements = assoc.elements; | ||
} | ||
// initialize an association | ||
if(isAssociationOrComposition(element) || element._ignore) { | ||
if(!element._target) { | ||
throw Error('Expect target to be resolved, parent: ' + def.name + ', assoc: ' + element.name + ', target: ' + element.target); | ||
} | ||
if(assoc) | ||
isContainerAssoc = assoc._isToContainer || assoc['@odata.contained']; | ||
/* FIXME: | ||
In combination flat/containment, keys of non-parent association shall | ||
be rendered: | ||
entity Orders { | ||
...; | ||
items: composition of many Items on $self = items.parent; | ||
} | ||
entity Items { | ||
...; | ||
parent: association to Orders; | ||
// FKs shall be rendered as this is not the assoc that establishes the composition | ||
// in flat mode only | ||
leadOrder: association to Orders; | ||
}; | ||
*/ | ||
// in case this is a forward assoc, store the backlink partneres here, _selfReferences.length > 1 => error | ||
assignProp(element, '_selfReferences', []); | ||
assignProp(element._target, '$proxies', []); | ||
/* | ||
If this foreign key is NOT a container fk, let isEdmPropertyRendered() decide | ||
Else, if fk is container fk, omit it if it wasn't requested in structured mode | ||
*/ | ||
if((!isContainerAssoc && !isEdmPropertyRendered(element, options)) || | ||
(isContainerAssoc && !options.renderForeignKeys)) | ||
assignAnnotation(element, '@cds.api.ignore', true); | ||
//forward annotations from managed association element to its foreign keys | ||
if(element.keys && options.isFlatFormat) { | ||
for(let fk of element.keys) { | ||
forAll(element, (attr, attrName) => { | ||
if(attrName[0] === '@') | ||
def.elements[fk.$generatedFieldName][attrName] = attr; | ||
}); | ||
} | ||
// if this is an containment ignore tagged element, | ||
// ignore it if option odataContainment is true and no foreign keys should be rendered | ||
if(element['@odata.containment.ignore'] && options.odataContainment && !options.renderForeignKeys) | ||
assignAnnotation(element, '@cds.api.ignore', true); | ||
} | ||
// it's an association | ||
else if(element['@odata.containment.ignore'] && options.odataContainment && !options.renderForeignKeys) { | ||
// if this is an explicitly containment ignore tagged association, | ||
// ignore it if option odataContainment is true and no foreign keys should be rendered | ||
assignAnnotation(element, '@odata.navigable', false); | ||
} | ||
// and afterwards eventually remove some :) | ||
setSAPSpecificV2AnnotationsToAssociation(options, element, def); | ||
} | ||
@@ -481,3 +417,3 @@ | ||
} | ||
applyAppSpecificLateCsnTransformationOnElement(options, element, struct); | ||
applyAppSpecificLateCsnTransformationOnElement(options, element, def); | ||
}); | ||
@@ -490,3 +426,3 @@ | ||
validKey.forEach(vk => altKeys[0].Key.push( { Name: vk.name, Alias: vk.name } ) ); | ||
assignAnnotation(struct, '@Core.AlternateKeys', altKeys); | ||
assignAnnotation(def, '@Core.AlternateKeys', altKeys); | ||
} | ||
@@ -507,3 +443,3 @@ } | ||
}); | ||
assignAnnotation(struct, '@Core.AlternateKeys', altKeys); | ||
assignAnnotation(def, '@Core.AlternateKeys', altKeys); | ||
keys = Object.create(null); | ||
@@ -522,87 +458,119 @@ validKey.forEach(e => { | ||
} | ||
assignProp(struct, '_SetAttributes', Object.create(null)); | ||
assignProp(struct, '$keys', keys); | ||
applyAppSpecificLateCsnTransformationOnStructure(options, struct); | ||
setSAPSpecificV2AnnotationsToEntitySet(options, struct); | ||
// initialize bound actions and functions | ||
// prepare the structure itself | ||
if(isEntityOrView(def)) { | ||
assignProp(def, '_SetAttributes', Object.create(null)); | ||
assignProp(def, '$keys', keys); | ||
applyAppSpecificLateCsnTransformationOnStructure(options, def); | ||
setSAPSpecificV2AnnotationsToEntitySet(options, def); | ||
} | ||
} | ||
// Attach name to actions and their parameters | ||
forAll(struct.actions, (a, n) => { | ||
a.name = n; | ||
forAll(a.params, (p, n) => { | ||
p.name = n; | ||
}); | ||
// Prepare the associations for the subsequent steps | ||
function prepareConstraints(struct) { | ||
forEachMember(struct, element => { | ||
if (isAssociationOrComposition(element) && !element._ignore) { | ||
// setup the constraints object | ||
setProp(element, '_constraints', { constraints: Object.create(null), selfs: [], _origins: [], termCount: 0 }); | ||
// and crack the ON condition | ||
resolveOnConditionAndPrepareConstraints(element, signal); | ||
} | ||
}); | ||
} | ||
// Resolve the association type of 'element' in 'struct' by doing the following: | ||
// - collect the foreign key elements for the target into attribute 'elements' | ||
function initializeAssociation(struct) { | ||
foreach(struct.elements, isAssociationOrComposition, element => { | ||
if (element._ignore) | ||
return; | ||
if(!element._target) { | ||
throw Error('Expect target to be resolved, parent: ' + struct.name + ', assoc: ' + element.name + ', target: ' + element.target); | ||
} | ||
/* | ||
Do not render (ignore) elements as properties | ||
In V4: | ||
1) If this is a foreign key of an association to a container which *is* used | ||
to establish the containment via composition and $self comparison, then | ||
do not render this foreign key. The $self comparison can only be evaluated | ||
after the ON conditions have been parsed in prepareConstraints(). | ||
2) For all other foreign keys let isEdmPropertyRendered() decide. | ||
3) If an element/association is annotated with @odata.containment.ignore and containment is | ||
active, assign @cds.api.ignore or @odata.navigable: false | ||
4) All of this can be revoked with options.renderForeignKeys. | ||
*/ | ||
function ignoreProperties(struct) { | ||
forEachGeneric(struct, 'elements', (element) => { | ||
if(!element.target) { | ||
if(element['@odata.foreignKey4']) { | ||
let isContainerAssoc = false; | ||
let elements = struct.elements; | ||
let assoc = undefined; | ||
let paths = element['@odata.foreignKey4'].split('.') | ||
for(let p of paths) { | ||
assoc = elements[p]; | ||
if(assoc) // could be that the @odata.foreignKey4 was propagated... | ||
elements = assoc.elements; | ||
} | ||
// in case this is a forward assoc, store the backlink partneres here, _partnerCsn.length > 1 => error | ||
setProp(element, '_partnerCsn', []); | ||
setProp(element._target, '$proxies', []); | ||
if(assoc) | ||
isContainerAssoc = assoc._isToContainer && assoc._selfReferences.length || assoc['@odata.contained']; | ||
/* | ||
If this foreign key is NOT a container fk, let isEdmPropertyRendered() decide | ||
Else, if fk is container fk, omit it if it wasn't requested in structured mode | ||
*/ | ||
if((!isContainerAssoc && !isEdmPropertyRendered(element, options)) || | ||
(isContainerAssoc && !options.renderForeignKeys)) | ||
assignAnnotation(element, '@cds.api.ignore', true); | ||
//forward annotations from managed association element to its foreign keys | ||
if(element.keys && options.isFlatFormat) { | ||
for(let fk of element.keys) { | ||
forAll(element, (attr, attrName) => { | ||
if(attrName[0] === '@') | ||
struct.elements[fk.$generatedFieldName][attrName] = attr; | ||
}); | ||
} | ||
// if this is an containment ignore tagged element, | ||
// ignore it if option odataContainment is true and no foreign keys should be rendered | ||
if(element['@odata.containment.ignore'] && options.odataContainment && !options.renderForeignKeys) | ||
assignAnnotation(element, '@cds.api.ignore', true); | ||
} | ||
// and afterwards eventually remove some :) | ||
setSAPSpecificV2AnnotationsToAssociation(options, element, struct); | ||
// it's an association | ||
else if(element['@odata.containment.ignore'] && options.odataContainment && !options.renderForeignKeys) { | ||
// if this is an explicitly containment ignore tagged association, | ||
// ignore it if option odataContainment is true and no foreign keys should be rendered | ||
assignAnnotation(element, '@odata.navigable', false); | ||
} | ||
}); | ||
} | ||
function initializeConstraints(struct) { | ||
foreach(struct.elements, isAssociationOrComposition, element => { | ||
if (element._ignore) return; | ||
setProp(element, '_constraints', getReferentialConstraints(element, signal, options)); | ||
/* | ||
Calculate the final referential constraints based on the assignments done in mutePropertiesForV4() | ||
It may be that now a number of properties are not rendered and cannot act as constraints (see isConstraintCandidate()) | ||
in edmUtils | ||
*/ | ||
function finalizeConstraints(struct) { | ||
forEachMember(struct, element => { | ||
if (isAssociationOrComposition(element) && !element._ignore) { | ||
finalizeReferentialConstraints(element, options); | ||
// only in V2 we must set the target cardinality of the backlink to the forward: | ||
if(element._constraints._originAssocCsn && element.cardinality && element.cardinality.max) { | ||
if(element._constraints._originAssocCsn.cardinality) { | ||
if(element._constraints._originAssocCsn.cardinality.src) { | ||
let srcMult = (element._constraints._originAssocCsn.cardinality.src == 1) ? '0..1' : '*'; | ||
let newMult = (element.cardinality.max > 1) ? '*' : '0..1'; | ||
if(options.isV2() && srcMult != newMult) { | ||
if(element._constraints._partnerCsn && element.cardinality && element.cardinality.max) { | ||
// if this is a partnership and this assoc has a set target cardinality, assign it as source cardinality to the partner | ||
if(element._constraints._partnerCsn.cardinality) { | ||
if(element._constraints._partnerCsn.cardinality.src) { | ||
let srcMult = (element._constraints._partnerCsn.cardinality.src == 1) ? '0..1' : '*'; | ||
let newMult = (element.cardinality.max > 1) ? '*' : '0..1'; | ||
if(options.isV2() && srcMult != newMult) { | ||
// Association 'E_toF': Multiplicity of Role='E' defined to '*', conflicting with target multiplicity '0..1' from | ||
signal(signal.warning`Source cardinality "${element._constraints._originAssocCsn.cardinality.src}" of "${element._constraints._originAssocCsn._parent.name}/${element._constraints._originAssocCsn.name}" conflicts with target cardinality "${element.cardinality.max}" of association "${element._parent.name}/${element.name}"`); | ||
signal(signal.warning`Source cardinality "${element._constraints._partnerCsn.cardinality.src}" of "${element._constraints._partnerCsn._parent.name}/${element._constraints._partnerCsn.name}" conflicts with target cardinality "${element.cardinality.max}" of association "${element._parent.name}/${element.name}"`); | ||
} | ||
} | ||
else { | ||
element._constraints._partnerCsn.cardinality.src = element.cardinality.max; | ||
} | ||
} | ||
else { | ||
element._constraints._originAssocCsn.cardinality.src = element.cardinality.max; | ||
element._constraints._partnerCsn.cardinality = { src: element.cardinality.max }; | ||
} | ||
} | ||
else { | ||
element._constraints._originAssocCsn.cardinality = { src: element.cardinality.max }; | ||
} | ||
} | ||
}); | ||
} | ||
function whatsMyServiceName(n) { | ||
return serviceNames.reduce((rc, sn) => n.startsWith(sn + '.') ? rc = sn : rc, undefined); | ||
} | ||
// For defining service create proxy target, if original target is outside of defining service | ||
function redirectDanglingAssociationsToProxyTargets(struct) { | ||
let myServiceName = whatsMyServiceName(struct.name); | ||
// myServiceName is used in closure | ||
const myServiceName = struct.$myServiceName; | ||
// if this artifact is a service member check its associations | ||
if(myServiceName) { | ||
foreach(struct.elements, isAssociationOrComposition, element => { | ||
if (element._ignore || element['@odata.navigable'] === false) { | ||
forEachGeneric(struct, 'elements', element => { | ||
if(!isAssociationOrComposition(element) || element._ignore || element['@odata.navigable'] === false) | ||
return; | ||
} | ||
/* | ||
* Consider everthing @cds.autoexpose: falsy to be a proxy candidate for now | ||
* Consider everything @cds.autoexpose: falsy to be a proxy candidate for now | ||
*/ | ||
@@ -631,2 +599,3 @@ /* | ||
proxy = { name, kind: 'entity', $proxy: true, elements: Object.create(null) }; | ||
setProp(proxy, '$myServiceName', myServiceName); | ||
setProp(proxy, '$proxy', true); | ||
@@ -798,23 +767,24 @@ setProp(proxy, '$keys', Object.create(null)); | ||
function initializeEdmKeyRefPaths(struct) { | ||
setProp(struct, '$edmKeyPaths', []); | ||
// for all key elements that shouldn't be ignored produce the paths | ||
foreach(struct.$keys, k => !k._ignore && !k._isToContainer, (k, kn) => { | ||
if(isEdmPropertyRendered(k, options) && | ||
if(struct.$myServiceName && struct.$keys) { | ||
setProp(struct, '$edmKeyPaths', []); | ||
// for all key elements that shouldn't be ignored produce the paths | ||
foreach(struct.$keys, k => !k._ignore && !(k._isToContainer && k._selfReferences.length), (k, kn) => { | ||
if(isEdmPropertyRendered(k, options) && | ||
!(options.isV2() && k['@Core.MediaType'])) { | ||
if(options.isV4() && options.isStructFormat) { | ||
if(options.isV4() && options.isStructFormat) { | ||
// This is structured OData ONLY | ||
// if the foreign keys are explictly requested, ignore associations and use the flat foreign keys instead | ||
if(options.renderForeignKeys && !k.target) | ||
struct.$edmKeyPaths.push([kn]); | ||
if(options.renderForeignKeys && !k.target) | ||
struct.$edmKeyPaths.push([kn]); | ||
// else produce paths (isEdmPropertyRendered() has filtered @odata.foreignKey4 already) | ||
else if(!options.renderForeignKeys) | ||
struct.$edmKeyPaths.push(...produceKeyRefPaths(k, kn)); | ||
} | ||
else if(!options.renderForeignKeys) | ||
struct.$edmKeyPaths.push(...produceKeyRefPaths(k, kn)); | ||
} | ||
// In v2/v4 flat, associations are never rendered | ||
else if(!k.target) { | ||
struct.$edmKeyPaths.push([kn]); | ||
else if(!k.target) { | ||
struct.$edmKeyPaths.push([kn]); | ||
} | ||
} | ||
} | ||
}); | ||
}); | ||
} | ||
/* | ||
@@ -880,4 +850,4 @@ Produce the list of paths for this element | ||
// No annos are rendered for non-existing EntitySet targets. | ||
if(struct.hasEntitySet === undefined) { | ||
let hasEntitySet = ['entity', 'view'].includes(struct.kind) && !(options.isV4() && edmUtils.isContainee(struct)) && !struct.$proxy; | ||
if(struct.$myServiceName && struct.hasEntitySet === undefined) { | ||
let hasEntitySet = isEntityOrView(struct) && !(options.isV4() && edmUtils.isContainee(struct)) && !struct.$proxy; | ||
setProp(struct, 'hasEntitySet', hasEntitySet); | ||
@@ -887,2 +857,74 @@ } | ||
// check Identifier length | ||
function signalIllegalIdentifier(identifier, path, kind, msg) { | ||
signal(signal.warning`OData ${kind} ${identifier ? `: "${identifier}"` : ''} ${ msg ? msg : 'must start with a letter or underscore, followed by at most 127 letters, underscores or digits' }`, path); | ||
} | ||
// Check the artifact identifier for compliance with the odata specification | ||
function checkArtifactIdentifier(artifact) { | ||
if(artifact.$myServiceName) { | ||
const artifactName = artifact.name.replace(artifact.$myServiceName + '.', ''); | ||
if(artifact.kind === 'action' || artifact.kind === 'function'){ | ||
checkActionOrFunctionIdentifier(artifact, artifactName); | ||
} else if(!isSimpleIdentifier(artifactName)){ | ||
signalIllegalIdentifier(artifactName, ['definitions', artifact.name], 'entity name'); | ||
} | ||
} | ||
function checkActionOrFunctionIdentifier(actionOrFunction, actionOrFunctionName) { | ||
if(!isSimpleIdentifier(actionOrFunctionName)){ | ||
signalIllegalIdentifier(actionOrFunctionName, actionOrFunction.$path, 'function or action name'); | ||
} | ||
if(actionOrFunction.params) { | ||
forEachGeneric(actionOrFunction, 'params', (param) => { | ||
if(!isSimpleIdentifier(param.name)){ | ||
signalIllegalIdentifier(param.name, param.$path, 'function or action parameter name'); | ||
} | ||
}); | ||
} | ||
} | ||
} | ||
function mapDocCommentToCoreDescription(artifact) { | ||
// 1. let all doc props become @Core.Descriptions | ||
// 2. mark a member that will become a collection | ||
// 3. assign the edm primitive type to elements, to be used in the rendering later | ||
assignAnnotation(artifact, '@Core.Description', artifact.doc); | ||
markCollection(artifact); | ||
mapCdsToEdmProp(artifact); | ||
if (artifact.returns) { | ||
markCollection(artifact.returns); | ||
mapCdsToEdmProp(artifact.returns); | ||
} | ||
forEachMemberRecursively(artifact,member => { | ||
assignAnnotation(member, '@Core.Description', member.doc); | ||
markCollection(member); | ||
mapCdsToEdmProp(member); | ||
if (member.returns) { | ||
markCollection(member.returns); | ||
mapCdsToEdmProp(member.returns); | ||
} | ||
}); | ||
// mark members that need to be rendered as collections | ||
function markCollection(obj) { | ||
const items = obj.items || csn.definitions[obj.type] && csn.definitions[obj.type].items; | ||
if (items) { | ||
assignProp(obj, '_NotNullCollection', items.notNull !== undefined ? items.notNull : true); | ||
assignProp(obj, '_isCollection', true); | ||
} | ||
} | ||
} | ||
// | ||
// Service initialization ends here | ||
// | ||
////////////////////////////////////////////////////////////////////// | ||
////////////////////////////////////////////////////////////////////// | ||
// | ||
// Helper section starts here | ||
// | ||
// If containment in V4 is active, annotations that would be assigned to the containees | ||
@@ -934,11 +976,2 @@ // entity set are not renderable anymore. In such a case try to reassign the annotations to | ||
// mark members that need to be rendered as collections | ||
function markCollection(obj) { | ||
const items = obj.items || csn.definitions[obj.type] && csn.definitions[obj.type].items; | ||
if (items) { | ||
assignProp(obj, '_NotNullCollection', items.notNull !== undefined ? items.notNull : true); | ||
assignProp(obj, '_isCollection', true); | ||
} | ||
} | ||
function mapCdsToEdmProp(obj) { | ||
@@ -1120,4 +1153,4 @@ if (obj.type && isBuiltinType(obj.type) && !isAssociationOrComposition(obj) && !obj.targetAspect) { | ||
elements[keyName] = key; | ||
struct.$keys = { [keyName] : key }; | ||
forAll(struct.elements, (e,n) => | ||
setProp(struct, '$keys',{ [keyName] : key } ); | ||
forEachGeneric(struct, 'elements', (e,n) => | ||
{ | ||
@@ -1124,0 +1157,0 @@ if(e.key) delete e.key; |
@@ -147,5 +147,5 @@ 'use strict'; | ||
function getReferentialConstraints(assocCsn, signal, options) | ||
{ | ||
let result = { constraints: Object.create(null), selfs: [], termCount: 0 }; | ||
function resolveOnConditionAndPrepareConstraints(assocCsn, signal) { | ||
if(!assocCsn._constraints) | ||
throw Error('Please debug me: need _constraints'); | ||
@@ -158,5 +158,5 @@ if(assocCsn.on) | ||
// for all $self conditions, fill constraints of partner (if any) | ||
let isBacklink = result.selfs.length == 1 && result.termCount == 1; | ||
let isBacklink = assocCsn._constraints.selfs.length == 1 && assocCsn._constraints.termCount == 1; | ||
/* example for originalTarget: | ||
/* example for _originalTarget: | ||
entity E (with parameters) { | ||
@@ -169,12 +169,10 @@ ... keys and all the stuff ... | ||
back target 'E' is also redirected to 'EParameters' (otherwise backlink would fail) | ||
ON Condition back.toE => parter=toE cannot be resolved in EParameters, originalTarget 'E' is | ||
ON Condition back.toE => parter=toE cannot be resolved in EParameters, _originalTarget 'E' is | ||
required for that | ||
*/ | ||
result.selfs.filter(p => p).forEach(partner => { | ||
let originAssocCsn = assocCsn._target.elements[partner]; | ||
if(originAssocCsn == undefined && assocCsn.originalTarget) | ||
originAssocCsn = assocCsn.originalTarget.elements[partner]; | ||
let parentArtifactName = assocCsn._parent.name; | ||
assocCsn._constraints.selfs.filter(p => p).forEach(partner => { | ||
const originAssocCsn = (assocCsn._originalTarget || assocCsn._target).elements[partner]; | ||
const parentArtifactName = assocCsn._parent.name; | ||
if(originAssocCsn) { | ||
if(originAssocCsn._target != assocCsn._parent) { | ||
if(originAssocCsn._originalTarget !== assocCsn._parent && originAssocCsn._target !== assocCsn._parent) { | ||
isBacklink = false; | ||
@@ -184,16 +182,2 @@ signal(signal.info`"${originAssocCsn._parent.name}:${partner}" with target "${originAssocCsn._target.name}" is compared with $self which represents "${parentArtifactName}"`, ['definitions', parentArtifactName, 'elements', assocCsn.name]); | ||
if(isAssociationOrComposition(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)) | ||
{ | ||
const c = [ [ fk.ref[0] ], [ fk.$generatedFieldName ] ]; | ||
const key = c.join(','); | ||
result.constraints[key] = c; | ||
} | ||
} | ||
} | ||
// Mark this association as backlink if $self appears exactly once | ||
@@ -203,4 +187,4 @@ // to surpress edm:Association generation in V2 mode | ||
// use first backlink as partner | ||
if(originAssocCsn._partnerCsn.length === 0) { | ||
result._originAssocCsn = originAssocCsn; | ||
if(originAssocCsn._selfReferences.length === 0) { | ||
assocCsn._constraints._partnerCsn = originAssocCsn; | ||
} | ||
@@ -211,4 +195,5 @@ else { | ||
// collect all backlinks at forward association | ||
originAssocCsn._partnerCsn.push(assocCsn); | ||
originAssocCsn._selfReferences.push(assocCsn); | ||
} | ||
assocCsn._constraints._origins.push(originAssocCsn); | ||
} | ||
@@ -229,75 +214,4 @@ else { | ||
}); | ||
if(!assocCsn._target.$isParamEntity) { | ||
// Header is composed of Items => Cds.Composition: Header is principal => use header's primary keys | ||
let dependentEntity = assocCsn._parent; | ||
let principalEntity = assocCsn._target; | ||
if(assocCsn.type === 'cds.Composition') { | ||
principalEntity = assocCsn._parent; | ||
dependentEntity = assocCsn._target; | ||
// Swap the constraint elements to be correct on Composition [principal, dependent] => [dependent, principal] | ||
Object.keys(result.constraints).forEach(cn => { | ||
result.constraints[cn] = [ result.constraints[cn][1], result.constraints[cn][0] ] } ); | ||
} | ||
// Remove all arget elements that are not key in the principal entity | ||
// and all elements that annotated with '@cds.api.ignore' | ||
foreach(result.constraints, | ||
c => { | ||
// concatenate all paths in flat mode to identify the correct element | ||
// in structured mode only resolve top level element (path rewriting is done elsewhere) | ||
let fk = dependentEntity.elements[ ( options.isFlatFormat ? c[0].join('_') : c[0][0] )]; | ||
let pk = principalEntity.$keys[ ( options.isFlatFormat ? c[1].join('_') : c[1][0] )]; | ||
return !(isConstraintCandidate(fk) && isConstraintCandidate(pk)); | ||
}, | ||
(c, cn) => { delete result.constraints[cn]; } ); | ||
} | ||
} | ||
// Handle managed association, a managed composition is treated as association | ||
else | ||
{ | ||
// If FK is key in target => constraint | ||
// Don't consider primary key associations (fks become keys on the source entity) as | ||
// this would impose a constraint against the target. | ||
// Filter out all elements that annotated with '@cds.api.ignore' | ||
// In structured format, foreign keys of managed associations are never rendered, so | ||
// there are no constraints for them. | ||
if(!assocCsn._target.$isParamEntity && assocCsn.keys) { | ||
for(let fk of assocCsn.keys) { | ||
let realFk = assocCsn._parent.elements[fk.$generatedFieldName]; | ||
let pk = assocCsn._target.elements[fk.ref[0]]; | ||
if(pk && pk.key && isConstraintCandidate(pk) && isConstraintCandidate(realFk)) | ||
{ | ||
const c = [ [ fk.$generatedFieldName ], [ fk.ref[0] ] ]; | ||
const key = c.join(','); | ||
result.constraints[key] = c; | ||
} | ||
} | ||
} | ||
} | ||
// If this association points to a redirected Parameter EntityType, do not calculate any constraints, | ||
// continue with multiplicity | ||
if(assocCsn._target.$isParamEntity) | ||
{ | ||
result.constraints = Object.create(null); | ||
} | ||
return result; | ||
/* | ||
* In Flat Mode an element is a constraint candidate if it is of scalar type. | ||
* In Structured mode, it eventually can be of a named type (which is | ||
* by the construction standards for OData either a complex type or a | ||
* type definition (alias to a scalar type). | ||
* The element must never be an association or composition and be renderable. | ||
*/ | ||
function isConstraintCandidate(elt) { | ||
let rc= (elt && | ||
elt.type && | ||
(!options.isFlatFormat || options.isFlatFormat && isBuiltinType(elt.type)) && | ||
!['cds.Association', 'cds.Composition'].includes(elt.type) && | ||
isEdmPropertyRendered(elt, options)); | ||
return rc; | ||
} | ||
// nested functions | ||
@@ -333,3 +247,3 @@ function getExpressionArguments(expr) | ||
{ | ||
result.termCount++; | ||
assocCsn._constraints.termCount++; | ||
if(lhs.ref && rhs.ref) // ref is a path | ||
@@ -362,6 +276,6 @@ { | ||
if(c[0][0] === '$self' && c[0].length === 1) { | ||
result.selfs.push(c[1][0]); | ||
assocCsn._constraints.selfs.push(c[1][0]); | ||
} else { | ||
const key = c.join(','); | ||
result.constraints[key] = c; | ||
assocCsn._constraints.constraints[key] = c; | ||
} | ||
@@ -376,2 +290,110 @@ } | ||
function finalizeReferentialConstraints(assocCsn, options) | ||
{ | ||
if(!assocCsn._constraints) | ||
throw Error('Please debug me: need _constraints'); | ||
if(assocCsn.on) | ||
{ | ||
/* example for originalTarget: | ||
entity E (with parameters) { | ||
... keys and all the stuff ... | ||
toE: association to E; | ||
back: association to E on back.toE = $self | ||
} | ||
toE target 'E' is redirected to 'EParameters' (must be as the new parameter list is required) | ||
back target 'E' is also redirected to 'EParameters' (otherwise backlink would fail) | ||
ON Condition back.toE => parter=toE cannot be resolved in EParameters, originalTarget 'E' is | ||
required for that | ||
*/ | ||
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)) | ||
{ | ||
const c = [ [ fk.ref[0] ], [ fk.$generatedFieldName ] ]; | ||
const key = c.join(','); | ||
assocCsn._constraints.constraints[key] = c; | ||
} | ||
} | ||
} | ||
}); | ||
if(!assocCsn._target.$isParamEntity) { | ||
// Header is composed of Items => Cds.Composition: Header is principal => use header's primary keys | ||
let dependentEntity = assocCsn._parent; | ||
let principalEntity = assocCsn._target; | ||
if(assocCsn.type === 'cds.Composition') { | ||
principalEntity = assocCsn._parent; | ||
dependentEntity = assocCsn._target; | ||
// 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] ] } ); | ||
} | ||
// Remove all arget elements that are not key in the principal entity | ||
// and all elements that annotated with '@cds.api.ignore' | ||
foreach(assocCsn._constraints.constraints, | ||
c => { | ||
// concatenate all paths in flat mode to identify the correct element | ||
// in structured mode only resolve top level element (path rewriting is done elsewhere) | ||
let fk = dependentEntity.elements[ ( options.isFlatFormat ? c[0].join('_') : c[0][0] )]; | ||
let pk = principalEntity.$keys[ ( options.isFlatFormat ? c[1].join('_') : c[1][0] )]; | ||
return !(isConstraintCandidate(fk) && isConstraintCandidate(pk)); | ||
}, | ||
(c, cn) => { delete assocCsn._constraints.constraints[cn]; } ); | ||
} | ||
} | ||
// Handle managed association, a managed composition is treated as association | ||
else | ||
{ | ||
// If FK is key in target => constraint | ||
// Don't consider primary key associations (fks become keys on the source entity) as | ||
// this would impose a constraint against the target. | ||
// Filter out all elements that annotated with '@cds.api.ignore' | ||
// In structured format, foreign keys of managed associations are never rendered, so | ||
// there are no constraints for them. | ||
if(!assocCsn._target.$isParamEntity && assocCsn.keys) { | ||
for(let fk of assocCsn.keys) { | ||
let realFk = assocCsn._parent.elements[fk.$generatedFieldName]; | ||
let pk = assocCsn._target.elements[fk.ref[0]]; | ||
if(pk && pk.key && isConstraintCandidate(pk) && isConstraintCandidate(realFk)) | ||
{ | ||
const c = [ [ fk.$generatedFieldName ], [ fk.ref[0] ] ]; | ||
const key = c.join(','); | ||
assocCsn._constraints.constraints[key] = c; | ||
} | ||
} | ||
} | ||
} | ||
// If this association points to a redirected Parameter EntityType, do not calculate any constraints, | ||
// continue with multiplicity | ||
if(assocCsn._target.$isParamEntity) | ||
{ | ||
assocCsn._constraints.constraints = Object.create(null); | ||
} | ||
return assocCsn._constraints; | ||
/* | ||
* In Flat Mode an element is a constraint candidate if it is of scalar type. | ||
* In Structured mode, it eventually can be of a named type (which is | ||
* by the construction standards for OData either a complex type or a | ||
* type definition (alias to a scalar type). | ||
* The element must never be an association or composition and be renderable. | ||
*/ | ||
function isConstraintCandidate(elt) { | ||
let rc= (elt && | ||
elt.type && | ||
(!options.isFlatFormat || options.isFlatFormat && isBuiltinType(elt.type)) && | ||
!['cds.Association', 'cds.Composition'].includes(elt.type) && | ||
isEdmPropertyRendered(elt, options)); | ||
return rc; | ||
} | ||
} | ||
function determineMultiplicity(csn) | ||
@@ -591,3 +613,4 @@ { | ||
isActionOrFunction, | ||
getReferentialConstraints, | ||
resolveOnConditionAndPrepareConstraints, | ||
finalizeReferentialConstraints, | ||
determineMultiplicity, | ||
@@ -594,0 +617,0 @@ mapCdsToEdmType, |
@@ -21,6 +21,10 @@ // CSN frontend - transform CSN into XSN | ||
* @property {Function} [arrayOf] Alternative to "type". The property should be an array. | ||
* Value is passed to arrayOf() | ||
* Value is passed to arrayOf(). | ||
* Value is ignored if "type" is set. Then it is only used | ||
* for better error messages. | ||
* @property {Function} [dictionaryOf] Alternative to "type". The property should be an object | ||
* in dictionary form (i.e. Object.<string, type>). | ||
* Value is passed to arrayOf() | ||
* Value is passed to dictionaryOf(). | ||
* Value is ignored if "type" is set. Then it is only used | ||
* for better error messages. | ||
* @property {Object.<string, SchemaSpec>} [schema] If some sub-properties have a different | ||
@@ -31,2 +35,4 @@ * semantic in this property than the default then | ||
* dictionary key by default. | ||
* @property {string} [msgProp] Display name of the property. compileSchema() sets it to | ||
* the dictionary key (+ optional '[]') by default. | ||
* @property {string} [msgId] Use this message id instead of the default one. | ||
@@ -92,2 +98,5 @@ * Allows more precise and detailed error messages. | ||
// CSN property names reserved for CAP | ||
const ourpropsRegex = /^[_$]?[a-zA-Z]+[0-9]*$/; | ||
// Sync with definition in to-csn.js: | ||
@@ -103,3 +112,3 @@ const typeProperties = [ | ||
'ref', 'xpr', 'val', '#', 'func', 'SELECT', 'SET', // Core Compiler checks SELECT/SET | ||
'param', 'global', 'literal', 'args', // only with 'ref'/'ref'/'val'/'func' | ||
'param', 'global', 'literal', 'args', 'cast', // only with 'ref'/'ref'/'val'/'func' | ||
]; | ||
@@ -146,2 +155,5 @@ | ||
}, | ||
i18n: { | ||
dictionaryOf: i18nLang, | ||
}, | ||
// definitions: ------------------------------------------------------------ | ||
@@ -175,4 +187,4 @@ definitions: { | ||
payload: { // keep it for a while, TODO: remove with v2 | ||
dictionaryOf: definition, | ||
type: renameTo( 'elements', dictionary ), | ||
dictionaryOf: definition, // duplicate of line below only for better error message | ||
type: renameTo( 'elements', dictionaryOf( definition ) ), | ||
defaultKind: 'element', | ||
@@ -529,3 +541,8 @@ validKinds: [], | ||
cast: { | ||
type: embed, | ||
type: cast, | ||
// cast can be: | ||
// 1. Inside "columns" => not in value | ||
// 2. Inside "xpr" => inside expressions | ||
// Because of (1) we have to set this property to false. | ||
inValue: false, | ||
optional: typeProperties, | ||
@@ -601,3 +618,3 @@ inKind: [ '$column' ], | ||
optional: [ | ||
'requires', 'definitions', 'extensions', | ||
'requires', 'definitions', 'extensions', 'i18n', | ||
'namespace', 'version', 'messages', 'meta', 'options', '@', '$location', | ||
@@ -656,3 +673,3 @@ ], | ||
else if (s.dictionaryOf) | ||
s.type = dictionary; | ||
s.type = dictionaryOf( s.dictionaryOf ); | ||
else | ||
@@ -722,8 +739,12 @@ throw new Error( `Missing type specification for property "${ p }` ); | ||
function embed( obj, spec, xsn, csn ) { | ||
if (spec.prop === 'cast') // XSN TODO: make sure that $inferred is enough | ||
xsn[csn.cast.target ? 'redirected' : '_typeIsExplicit'] = true; | ||
function embed( obj, spec, xsn ) { | ||
Object.assign( xsn, object( obj, spec ) ); // TODO: $location? | ||
} | ||
function cast( obj, spec, xsn, csn ) { | ||
// XSN TODO: make sure that $inferred is enough | ||
xsn[csn.cast.target ? 'redirected' : '_typeIsExplicit'] = true; | ||
// embed all other properties, e.g. "type" and type properties | ||
embed( obj, spec, xsn ); | ||
} | ||
function extra( node, spec, xsn ) { | ||
@@ -846,25 +867,28 @@ if (!xsn.$extra) | ||
function dictionary( dict, spec, xsn, csn ) { | ||
if (!dict || typeof dict !== 'object' || Array.isArray( dict )) { | ||
message( 'syntax-csn-expected-object', location(true), null, | ||
{ prop: spec.prop }, 'Error' ); // spec.prop, not spec.msgProp! | ||
return ignore( dict ); | ||
} | ||
if (csn.SELECT) // do not augment hidden 'elements' for 'SELECT' | ||
return undefined; | ||
const r = Object.create(null); | ||
const allNames = Object.keys( dict ); | ||
if (!allNames.length) | ||
return r; // {} in one JSON line | ||
++virtualLine; | ||
for (const name of allNames) { | ||
if (!name) { | ||
message( 'syntax-csn-empty-name', location(true), null, | ||
{ prop: spec.prop }, 'Warning', // TODO: Error | ||
'Property names in dictionary $(PROP) must not be empty' ); | ||
// A dictionary is expected. Uses spec.dictionaryOf. If unset, default is "definition". | ||
function dictionaryOf( elementFct ) { | ||
return function dictionary( dict, spec ) { | ||
if (!dict || typeof dict !== 'object' || Array.isArray( dict )) { | ||
message( 'syntax-csn-expected-object', location(true), null, | ||
{ prop: spec.prop }, 'Error' ); // spec.prop, not spec.msgProp! | ||
return ignore( dict ); | ||
} | ||
r[name] = definition( dict[name], spec, r, dict, name ); | ||
const r = Object.create(null); | ||
const allNames = Object.keys( dict ); | ||
if (!allNames.length) | ||
return r; // {} in one JSON line | ||
++virtualLine; | ||
} | ||
return r; | ||
for (const name of allNames) { | ||
if (!name) { | ||
message( 'syntax-csn-empty-name', location(true), null, | ||
{ prop: spec.prop }, 'Warning', // TODO: Error | ||
'Property names in dictionary $(PROP) must not be empty' ); | ||
} | ||
const val = elementFct( dict[name], spec, r, dict, name ); | ||
if (val !== undefined) | ||
r[name] = val; | ||
++virtualLine; | ||
} | ||
return r; | ||
}; | ||
} | ||
@@ -1255,2 +1279,19 @@ | ||
// i18n ------------------------------ | ||
function i18nLang( val, spec, xsn, csn, langKey ) { | ||
/** @type {SchemaSpec} */ | ||
const keySpec = { dictionaryOf: translations, prop: langKey }; | ||
return dictionaryOf( translations )( val, keySpec, xsn, csn ); | ||
} | ||
function translations( keyVal, spec, xsn, csn, textKey ) { | ||
if (typeof keyVal === 'string') // allow empty string | ||
return { val: keyVal, literal: 'string', location: location() }; | ||
message( 'syntax-csn-expected-translation', location(true), null, | ||
{ prop: textKey, otherprop: spec.prop }, 'Error', | ||
'Expected string for text key $(PROP) of language $(OTHERPROP)' ); | ||
return ignore( keyVal ); | ||
} | ||
// Helper functions for objects and definitions ------------------------------ | ||
@@ -1262,4 +1303,10 @@ | ||
if (!s || s.noPrefix && prop !== p0 ) { | ||
message( 'syntax-csn-unknown-property', location(true), null, { prop }, | ||
'Warning', 'Unknown CSN property $(PROP)' ); | ||
if (ourpropsRegex.test( prop )) { | ||
// TODO v2: Warning only with --sloppy | ||
message( 'syntax-csn-unknown-property', location(true), null, { prop }, | ||
'Warning', 'Unknown CSN property $(PROP)' ); | ||
} | ||
else { // TODO v2: always (i.e. also with message) add to $extra | ||
return { prop, type: extra }; | ||
} | ||
} | ||
@@ -1444,3 +1491,3 @@ else if (!expected( p0, s )) { | ||
message( 'syntax-csn-expected-object', location(true), null, { prop: '$location' }, | ||
'Error', 'Expected object for property $(PROP)' ); | ||
'Error' ); | ||
} | ||
@@ -1447,0 +1494,0 @@ // hidden feature: string $location |
@@ -7,2 +7,3 @@ // Transform augmented CSN into compact "official" CSN | ||
const { locationString } = require('../base/messages'); | ||
const { forEachGeneric } = require('../base/model'); | ||
@@ -43,3 +44,3 @@ const compilerVersion = require('../../package.json').version; | ||
localized: value, | ||
type: artifactRef, | ||
type: t => artifactRef( t, !t.$extra ), | ||
length: value, | ||
@@ -262,2 +263,4 @@ precision: value, | ||
csn.extensions = exts; | ||
if (model.i18n) | ||
csn.i18n = i18n( model.i18n ); | ||
set( 'messages', csn, model ); | ||
@@ -321,22 +324,77 @@ const [ src ] = Object.keys( model.sources ); | ||
); | ||
if (!gensrcFlavor) | ||
return exts; | ||
for (const name of Object.keys( model.definitions ).sort()) { | ||
const art = model.definitions[name]; | ||
// in definitions (without redef) with potential inferred elements: | ||
if (!(art instanceof Array) && art.elements && | ||
(art.query || art.includes || art.$inferred)) { | ||
const annos = art.$inferred && annotations( art, true ); | ||
const elems = inferred( art.elements, art.$inferred ); | ||
/** @type {object} */ | ||
const annotate = Object.assign( { annotate: name }, annos ); | ||
if (Object.keys( elems ).length) | ||
annotate.elements = elems; | ||
if (Object.keys( annotate ).length > 1) | ||
exts.push( annotate ); | ||
// For namespaces and builtins: Extract annotations since they cannot be represented | ||
// in CSN. For all other artifacts, check whether they may be auto-exposed, | ||
// $inferred, etc. and extract their annotations. | ||
// In parseCdl mode extensions were already put into "extensions". | ||
if (!model.options.parseCdl && art.kind === 'namespace') { | ||
extractAnnotationsToExtension( art ); | ||
if (art.builtin === 'reserved') | ||
forEachGeneric( art, 'artifacts', extractAnnotationsToExtension); | ||
} | ||
else if (gensrcFlavor) { | ||
// From definitions (without redefinitions) with potential inferred elements: | ||
if (!(art instanceof Array) && art.elements && | ||
(art.query || art.includes || art.$inferred)) { | ||
const annos = art.$inferred && annotations( art, true ); | ||
const elems = inferred( art.elements, art.$inferred ); | ||
/** @type {object} */ | ||
const annotate = Object.assign( { annotate: name }, annos ); | ||
if (Object.keys( elems ).length) | ||
annotate.elements = elems; | ||
if (Object.keys( annotate ).length > 1) | ||
exts.push( annotate ); | ||
} | ||
} | ||
} | ||
return exts; | ||
// extract namespace/builtin annotations | ||
function extractAnnotationsToExtension( art ) { | ||
const name = art.name.absolute; | ||
// 'true' because annotations on namespaces and builtins can only | ||
// happen through extensions. | ||
const annos = annotations( art, true ); | ||
const annotate = Object.assign( { annotate: name }, annos ); | ||
if (Object.keys( annotate ).length > 1) { | ||
const loc = locationForAnnotationExtension(); | ||
if (loc) | ||
location( loc, annotate, art ); | ||
exts.push( annotate ); | ||
} | ||
// Either the artifact's name's location or (for builtin types) the location | ||
// of its first annotation. | ||
function locationForAnnotationExtension() { | ||
if (art.location) | ||
return art.location; | ||
for (const key in art) { | ||
if (key.charAt(0) === '@' && art[key].name) | ||
return art[key].name.location; | ||
} | ||
return null; | ||
} | ||
} | ||
} | ||
/** | ||
* @param {XSN.i18n} i18nNode | ||
* @returns {CSN.i18n} | ||
*/ | ||
function i18n( i18nNode ) { | ||
const csn = Object.create( null ); | ||
for (const langKey in i18nNode) { | ||
const langDict = i18nNode[langKey]; | ||
if (!csn[langKey]) | ||
csn[langKey] = Object.create( null ); | ||
for (const textKey in langDict) | ||
csn[langKey][textKey] = langDict[textKey].val; | ||
} | ||
return csn; | ||
} | ||
function inferred( elems, inferredParent ) { | ||
@@ -416,3 +474,4 @@ const ext = Object.create(null); | ||
// for gensrcFlavor: return annotations from definition (annotated==false) | ||
// for gensrcFlavor and namespace/builtin annotation extraction: | ||
// return annotations from definition (annotated==false) | ||
// or annotations (annotated==true) | ||
@@ -425,3 +484,5 @@ function annotations( node, annotated ) { | ||
const val = node[prop]; | ||
if ((val.priority && val.priority !== 'define') === annotated) { | ||
// val.priority isn't set for computed annotations like @Core.Computed | ||
// and @odata.containment.ignore | ||
if (val.priority && (val.priority !== 'define') === annotated) { | ||
// transformer (= value) takes care to exclude $inferred annotation assignments | ||
@@ -546,3 +607,3 @@ const sub = transformer( val ); | ||
throw new Error( `Unexpected TYPE OF in ${ locationString(node.location) }`); | ||
return renderArtifactPath( path, terse, node.scope ); | ||
return renderArtifactPath( node, path, terse, node.scope ); | ||
} | ||
@@ -553,5 +614,6 @@ const { absolute } = root.name; | ||
if (absolute === path[0].id) // normal case (no localization view) | ||
return renderArtifactPath( path, terse ); // scope:param is not valid (and would be lost) | ||
return renderArtifactPath( node, path, terse ); | ||
// scope:param is not valid (and would be lost) | ||
const head = Object.assign( {}, path[0], { id: absolute } ); | ||
return renderArtifactPath( [ head, ...path.slice(1) ], terse ); | ||
return renderArtifactPath( node, [ head, ...path.slice(1) ], terse ); | ||
} | ||
@@ -565,7 +627,7 @@ if (node.scope === 'typeOf') { // TYPE OF without ':' in path | ||
// TODO: forbid TYPE OF elem / TYPE OF $self.elem in queries | ||
return renderArtifactPath( [ { id: absolute }, ...path.slice(1) ], terse ); | ||
return renderArtifactPath( node, [ { id: absolute }, ...path.slice(1) ], terse ); | ||
} | ||
const parent = root._parent; | ||
const structs = parent.name.element ? parent.name.element.split('.') : []; | ||
return { ref: [ absolute, ...structs, ...path.map( pathItem ) ] }; | ||
return extra( { ref: [ absolute, ...structs, ...path.map( pathItem ) ] }, node ); | ||
} | ||
@@ -586,6 +648,6 @@ let { scope } = node; | ||
const head = Object.assign( {}, path[0], { id: absolute } ); | ||
return renderArtifactPath( [ head, ...path.slice(1) ], terse, scope ); | ||
return renderArtifactPath( node, [ head, ...path.slice(1) ], terse, scope ); | ||
} | ||
function renderArtifactPath( path, terse, scope ) { | ||
function renderArtifactPath( node, path, terse, scope ) { | ||
if (scope === 0) { | ||
@@ -606,3 +668,3 @@ // try to find ':' position syntactically for FROM | ||
return (!terse || ref.length !== 1 || typeof ref[0] !== 'string') | ||
? { ref } | ||
? extra( { ref }, node ) | ||
: ref[0]; | ||
@@ -698,4 +760,4 @@ } | ||
if (node.path) | ||
return extra( { ref: node.path.map( pathItem ), param: true }, en ); | ||
return extra( { ref: [ node.param.val ], param: true }, en ); | ||
return extra( typeCast({ ref: node.path.map( pathItem ), param: true }, en), en ); | ||
return extra( typeCast({ ref: [ node.param.val ], param: true }, en), en ); | ||
} | ||
@@ -705,3 +767,3 @@ if (node.path) { | ||
if (node.path.length !== 1) | ||
return extra( pathRef( node.path ), en ); | ||
return extra( typeCast( pathRef( node.path ), en ), en ); | ||
const item = pathItem( node.path[0] ); | ||
@@ -711,15 +773,17 @@ if (typeof item === 'string' && !node.path[0].quoted && | ||
magicFunctions.includes( item.toUpperCase() )) | ||
return extra( { func: item }, en ); | ||
return extra( typeCast( { func: item }, en ), en ); | ||
return extra( pathRef( node.path ), en ); | ||
return extra( typeCast( pathRef( node.path ), en), en ); | ||
} | ||
if (node.literal) { | ||
if (typeof node.val === node.literal || node.val === null) | ||
return extra( { val: node.val }, en ); | ||
return extra(typeCast( { val: node.val }, en ), en ); | ||
else if (node.literal === 'enum') | ||
return extra( { '#': node.symbol.id }, en ); | ||
return extra(typeCast( { '#': node.symbol.id }, en ), en ); | ||
else if (node.literal === 'token') | ||
return node.val; // * in COUNT(*) | ||
return extra( { val: node.val, literal: (node.literal === 'hex') ? 'x' : node.literal }, | ||
en ); | ||
return extra( | ||
typeCast( { val: node.val, literal: (node.literal === 'hex') ? 'x' : node.literal }, en ), | ||
en | ||
); | ||
} | ||
@@ -748,4 +812,4 @@ if (node.func) { // TODO XSN: remove op: 'call', func is no path | ||
// do not use xpr() for xpr, as it would flatten inner xpr's (semantically ok) | ||
return extra( { xpr: node.args.map( expression ) }, node ); | ||
return { xpr: xpr( node ) }; | ||
return extra( typeCast({ xpr: node.args.map( expression ) }, node ), node ); | ||
return typeCast({ xpr: xpr( node ) }, node); | ||
} | ||
@@ -916,7 +980,3 @@ | ||
elem.name, neqPath( elem.value ) ); | ||
if (elem._typeIsExplicit || elem.redirected) { // TODO XSN: introduce $inferred | ||
col.cast = {}; // TODO: what about $extra in cast? | ||
for (const prop of typeProperties) | ||
set( prop, col.cast, elem ); | ||
} | ||
typeCast(col, elem); | ||
} | ||
@@ -953,13 +1013,17 @@ finally { | ||
function $extra( obj, csn ) { | ||
for (const prop of Object.keys( obj ).sort()) | ||
csn[prop] = obj[prop]; | ||
} | ||
function extra( csn, node ) { | ||
if (node && node.$extra) | ||
$extra( node.$extra, csn ); | ||
Object.assign( csn, node.$extra ); | ||
return csn; | ||
} | ||
function typeCast( csn, node ) { | ||
if (node._typeIsExplicit || node.redirected) { // TODO: XSN: introduce $inferred | ||
csn.cast = {}; // TODO: what about $extra in cast? | ||
for (const prop of typeProperties) | ||
set( prop, csn.cast, node ); | ||
} | ||
return csn; | ||
} | ||
function setHidden( obj, prop, val ) { | ||
@@ -966,0 +1030,0 @@ Object.defineProperty( obj, prop, { |
@@ -506,2 +506,9 @@ // Generic ANTLR parser class with AST-building functions | ||
function assignProps( target, annos = [], props, location ) { | ||
while (Array.isArray( props )) { | ||
// XSN TODO: change representation of parentheses around expressions | ||
// Then this check can be removed | ||
this.message( null, props.location || location || this.startLocation( this._ctx.start ), {}, | ||
'Error', 'Remove the parentheses around the expression' ); | ||
props = props[0]; | ||
} | ||
if (annos === true) | ||
@@ -508,0 +515,0 @@ return Object.assign( target, props ); |
@@ -91,6 +91,6 @@ // CSN functionality for resolving references | ||
return art; | ||
else if (!tail.length && notFound !== undefined) | ||
else if (notFound !== undefined) | ||
return notFound; | ||
} | ||
throw new Error( 'Undefined reference '); | ||
throw new Error( 'Undefined reference' ); | ||
} | ||
@@ -364,4 +364,5 @@ | ||
csnRefs.implicitAs = implicitAs; | ||
csnRefs.analyseCsnPath = analyseCsnPath | ||
csnRefs.analyseCsnPath = analyseCsnPath; | ||
csnRefs.pathId = pathId; | ||
module.exports = csnRefs; | ||
@@ -738,3 +738,3 @@ 'use strict' | ||
* Loop through the model, applying the custom transformations on the node's matching. | ||
* | ||
* | ||
* Each transformer gets: | ||
@@ -745,8 +745,10 @@ * - the parent having the property | ||
* - the path to the property | ||
* | ||
* | ||
* @param {object} csn CSN to enrich in-place | ||
* @param {Map} customTransformers Map of prop to transform and function to apply | ||
* @param {object} customTransformers Map of prop to transform and function to apply | ||
* @param {Function[]} artifactTransformers Transformations to run on the artifacts, like forEachDefinition | ||
* @param {Boolean} skipIgnore Wether to skip _ignore elements or not | ||
* @returns {object} CSN with transformations applied | ||
*/ | ||
function applyTransformations( csn, customTransformers={}, artifactTransformers=[] ) { | ||
function applyTransformations( csn, customTransformers={}, artifactTransformers=[], skipIgnore = true ) { | ||
const transformers = { | ||
@@ -771,3 +773,3 @@ elements: dictionary, | ||
function standard( parent, prop, node ) { | ||
if (!node || typeof node !== 'object' || !{}.propertyIsEnumerable.call( parent, prop ) || (typeof prop === 'string' && prop.startsWith('@')) || node._ignore) | ||
if (!node || typeof node !== 'object' || !{}.propertyIsEnumerable.call( parent, prop ) || (typeof prop === 'string' && prop.startsWith('@')) || (skipIgnore && node._ignore)) | ||
return; | ||
@@ -774,0 +776,0 @@ |
@@ -85,3 +85,7 @@ // For testing: reveal non-enumerable properties in CSN, display result of csnRefs | ||
function refLocation( art ) { | ||
return (!art) ? '<illegal link>' : art.$location || '<no location>'; | ||
if (art) | ||
return art.$location || '<no location>'; | ||
if (!options.testMode) | ||
return '<illegal link>'; | ||
throw new Error( 'Undefined reference' ); | ||
} | ||
@@ -88,0 +92,0 @@ |
@@ -621,4 +621,8 @@ | ||
// Even the first step might have parameters and/or a filter | ||
if (path.ref[0].args) { | ||
result += `(${renderArgs(path.ref[0].args, '=>', env, syntax)})`; | ||
// Render the actual parameter list. If the path has no actual parameters, | ||
// the ref is not rendered as { id: ...; args: } but as short form of ref[0] ;) | ||
// An empty actual parameter list is rendered as `()`. | ||
const ref = csn.definitions[path.ref[0].id] || csn.definitions[path.ref[0]]; | ||
if (ref && ref.params) { | ||
result += `(${renderArgs(path.ref[0].args || {}, '=>', env, syntax)})`; | ||
} | ||
@@ -715,6 +719,14 @@ else if (['udf'].includes(syntax)) { | ||
function renderParameterDefinitions(artifactName, params) { | ||
let result = Object.keys(params || {}).map(name => 'IN ' + quoteSqlId(name) + ' ' + renderTypeReference(artifactName, name, params[name])) | ||
.join(', '); | ||
if (result !== '') { | ||
result = '(' + result + ')'; | ||
let result = ''; | ||
if(params) { | ||
let parray = []; | ||
for(const pn in params) { | ||
const p = params[pn]; | ||
let pstr = 'IN ' + quoteSqlId(pn) + ' ' + renderTypeReference(artifactName, pn, p); | ||
if(p.default) { | ||
pstr += ' DEFAULT ' + renderExpr(p.default); | ||
} | ||
parray.push(pstr); | ||
} | ||
result = '(' + parray.join(', ') + ')'; | ||
} | ||
@@ -938,3 +950,3 @@ return result; | ||
// (no trailing LF, don't indent if inline) | ||
function renderExpr(x, env, inline=true) { | ||
function renderExpr(x, env, inline=true, nestedExpr=false) { | ||
// Compound expression | ||
@@ -944,3 +956,3 @@ if (x instanceof Array) { | ||
// FIXME: Take this for `toCdl`, too | ||
let tokens = x.map(item => renderExpr(item, env, inline)); | ||
let tokens = x.map(item => renderExpr(item, env, inline, nestedExpr)); | ||
let result = ''; | ||
@@ -955,6 +967,17 @@ for (let i = 0; i < tokens.length; i++) { | ||
return result; | ||
// return x.map(item => renderExpr(item, env, inline)).join(' '); | ||
// return x.map(item => renderExpr(item, env, inline, nestedExpr)).join(' '); | ||
} | ||
else if (typeof x === 'object' && x !== null) { | ||
if (options.forHana && nestedExpr && x.cast && x.cast.type) | ||
return renderExplicitTypeCast(renderExprObject()); | ||
return renderExprObject(); | ||
} | ||
// Not a literal value but part of an operator, function etc - just leave as it is | ||
// FIXME: For the sake of simplicity, we should get away from all this uppercasing in toSql | ||
else { | ||
return String(x).toUpperCase(); | ||
} | ||
// Various special cases represented as objects | ||
else if (typeof x === 'object' && x !== null) { | ||
function renderExprObject() { | ||
// Literal value, possibly with explicit 'literal' property | ||
@@ -1070,3 +1093,3 @@ if (x.val !== undefined) { | ||
else if (x.xpr) { | ||
return renderExpr(x.xpr, env); | ||
return renderExpr(x.xpr, env, inline, true); | ||
} | ||
@@ -1086,6 +1109,10 @@ // Sub-select | ||
} | ||
// Not a literal value but part of an operator, function etc - just leave as it is | ||
// FIXME: For the sake of simplicity, we should get away from all this uppercasing in toSql | ||
else { | ||
return String(x).toUpperCase(); | ||
/** | ||
* Renders an explicit `cast()` inside an 'xpr'. | ||
* @param {string} value | ||
*/ | ||
function renderExplicitTypeCast(value) { | ||
const typeRef = renderBuiltinType(x.cast.type) + renderTypeParameters(x.cast); | ||
return `CAST(${value} AS ${typeRef})`; | ||
} | ||
@@ -1092,0 +1119,0 @@ |
@@ -22,2 +22,3 @@ 'use strict'; | ||
const validateForeignKeys = require('../checks/csn/foreignKeys'); | ||
const { validateDefaultValues }= require('../checks/csn/defaultValues'); | ||
const validateAssociationsInArrayOf = require('../checks/csn/assocsInArrayOf'); | ||
@@ -109,3 +110,4 @@ | ||
setAnnotation, | ||
renameAnnotation | ||
renameAnnotation, | ||
expandStructsInOnConditions, | ||
} = transformers; | ||
@@ -181,6 +183,14 @@ | ||
}, | ||
/* Member Validators */ [ validateOnCondition, validateForeignKeys, validateAssociationsInArrayOf ], | ||
/* Member Validators */ [ validateOnCondition, validateForeignKeys, validateAssociationsInArrayOf, validateDefaultValues ], | ||
/* artifact validators */ [], | ||
/* query validators */ [ validateMixinOnCondition ]); | ||
// Check if structured elements and managed associations are compared in an ON condition | ||
// and expand these structured elements. This tuple expansion allows all other | ||
// subsequent procession steps (especially a2j) to see plain paths in ON conditions. | ||
// If errors are detected, handleMessages will return from further processing | ||
forEachDefinition(csn, expandStructsInOnConditions); | ||
// Throw exception in case of errors | ||
@@ -275,3 +285,4 @@ handleMessages(csn, options); | ||
// Process associations - expand, generate foreign keys | ||
processForeignKeys(csn, { referenceFlattener, csnUtils, transformers }) | ||
let flatKeys = !structuredOData || (structuredOData && options.toOdata.odataForeignKeys); | ||
processForeignKeys(csn, flatKeys, { referenceFlattener, csnUtils, transformers }) | ||
@@ -458,2 +469,4 @@ // Flatten on-conditions in unmanaged associations | ||
case 'check-proper-type-of': | ||
case 'rewrite-not-supported': | ||
case 'rewrite-undefined-key': | ||
message.severity = 'Error'; | ||
@@ -496,2 +509,3 @@ break; | ||
'@Capabilities.Updatable': '@Capabilities.UpdateRestrictions.Updatable', | ||
'@Capabilities.Readable': '@Capabilities.ReadRestrictions.Readable', | ||
} | ||
@@ -498,0 +512,0 @@ |
@@ -9,3 +9,3 @@ 'use strict'; | ||
const { forEach } = require('../udict'); | ||
const { collectAllManagedAssociations } = require('./utils'); | ||
const { forEachManagedAssociation } = require('./utils'); | ||
const sortByAssociationDependency = require('./sortByAssociationDependency'); | ||
@@ -115,3 +115,3 @@ | ||
function processSortedForeignKeys(sortedAssociations, functions) { | ||
function processSortedForeignKeys(sortedAssociations, flatKeys, functions) { | ||
@@ -128,3 +128,3 @@ const { csnUtils, transformers } = functions; | ||
if (csnUtils.isManagedAssociationElement(element) && element.keys) { | ||
takeoverForeignKeysOfTargetAssociations(element, path, generatedForeignKeyNamesForPath, functions); | ||
if (flatKeys) takeoverForeignKeysOfTargetAssociations(element, path, generatedForeignKeyNamesForPath, functions); | ||
fixCardinality(element); | ||
@@ -142,7 +142,3 @@ } | ||
let managedAssociations = collectAllManagedAssociations(csn); | ||
managedAssociations.forEach(item => { | ||
const { element } = item; | ||
forEachManagedAssociation(csn, (element) => { | ||
if (element.keys) { | ||
@@ -152,5 +148,6 @@ expandStructuredKeysForElement(element, referenceFlattener); | ||
}) | ||
} | ||
function processForeignKeys(csn, functions) { | ||
function processForeignKeys(csn, flatKeys, functions) { | ||
@@ -169,6 +166,5 @@ let { referenceFlattener, csnUtils, transformers } = functions; | ||
// generate foreign keys | ||
processSortedForeignKeys(sortedAssociations, { csnUtils, transformers, referenceFlattener }); | ||
processSortedForeignKeys(sortedAssociations, flatKeys, { csnUtils, transformers, referenceFlattener }); | ||
} | ||
module.exports = processForeignKeys; |
@@ -70,2 +70,5 @@ const { forEachRef } = require('../../model/csnUtils'); | ||
if (isNode) { | ||
const descr = Object.getOwnPropertyDescriptor(node,'$path') | ||
if(descr && descr.enumerable) // check if it is an element -> do not overwrite it | ||
return; | ||
if (!pathPrefix) | ||
@@ -72,0 +75,0 @@ setProp(node, '$path', path); |
@@ -29,3 +29,3 @@ 'use strict'; | ||
if (propertyName === 'elements') { | ||
exposeStructTypeOf(element, `${defName}.${elementName}`, getServiceOfArtifact(defName, services), `${defName.replace(/\./g, '_')}_${elementName}`); | ||
exposeStructTypeOf(element, `${defName}.${elementName}`, getServiceOfArtifact(defName, services), `${defName.replace(/\./g, '_')}_${elementName}`, structuredOData, path); | ||
// TODO: use the next line once the array of logic is reworked | ||
@@ -41,7 +41,7 @@ // exposeTypeOf(element, elementName, getServiceOfArtifact(defName, services), `${defName.replace(/\./g, '_')}_${elementName}`); | ||
if (def.kind === 'action' || def.kind === 'function') { | ||
exposeTypesOfAction(def, defName, serviceName); | ||
exposeTypesOfAction(def, defName, serviceName, path); | ||
} | ||
// bound actions | ||
for (let actionName in def.actions || {}) { | ||
exposeTypesOfAction(def.actions[actionName], `${defName}_${actionName}`, serviceName); | ||
exposeTypesOfAction(def.actions[actionName], `${defName}_${actionName}`, serviceName, path.concat(['actions', actionName])); | ||
} | ||
@@ -52,4 +52,4 @@ | ||
if (propertyName === 'elements') { | ||
if (structuredOData && csnUtils.isStructured(element)) { | ||
exposeStructTypeOf(element, elementName, serviceName, `${defNameWithoutServiceName(defName, serviceName).replace(/\./g, '_')}_${elementName}`); | ||
if (csnUtils.isStructured(element)) { | ||
exposeStructTypeOf(element, elementName, serviceName, `${defNameWithoutServiceName(defName, serviceName).replace(/\./g, '_')}_${elementName}`, structuredOData, path); | ||
// TODO: use the next line once the array of logic is reworked | ||
@@ -90,7 +90,7 @@ // exposeTypeOf(element, elementName, getServiceOfArtifact(defName, services), `${defName.replace(/\./g, '_')}_${elementName}`); | ||
// still WIP function | ||
function exposeTypeOf(node, memberName, service, artificialName) { | ||
function exposeTypeOf(node, memberName, service, artificialName, path) { | ||
if (isArrayed(node)) | ||
exposeArrayOfTypeOf(node, memberName, service, artificialName); | ||
exposeArrayOfTypeOf(node, memberName, service, artificialName, path); | ||
else | ||
exposeStructTypeOf(node, memberName, service, artificialName); | ||
exposeStructTypeOf(node, memberName, service, artificialName, structuredOData, path); | ||
} | ||
@@ -111,8 +111,8 @@ | ||
*/ | ||
function exposeTypesOfAction(action, actionName, service) { | ||
function exposeTypesOfAction(action, actionName, service, path) { | ||
if (action.returns) | ||
exposeTypeOf(action.returns, actionName, service, `return_${actionName.replace(/\./g, '_')}`); | ||
exposeTypeOf(action.returns, actionName, service, `return_${actionName.replace(/\./g, '_')}`, path.concat(['returns'])); | ||
for (let paramName in action.params || {}) { | ||
exposeTypeOf(action.params[paramName], actionName, service, `param_${actionName.replace(/\./g, '_')}_${paramName}`); | ||
exposeTypeOf(action.params[paramName], actionName, service, `param_${actionName.replace(/\./g, '_')}_${paramName}`, path.concat(['params', paramName])); | ||
} | ||
@@ -130,3 +130,3 @@ } | ||
*/ | ||
function exposeStructTypeOf(node, memberName, service, artificialName, deleteElems = structuredOData) { | ||
function exposeStructTypeOf(node, memberName, service, artificialName, deleteElems = structuredOData, path) { | ||
if (!node) { | ||
@@ -139,3 +139,3 @@ // TODO: when node will be undefined, if node is undefined this should not be reached | ||
// TODO: call exposure of Arrayed types? | ||
if (node.items) exposeStructTypeOf(node.items, memberName, service, artificialName, deleteElems); | ||
if (node.items) exposeStructTypeOf(node.items, memberName, service, artificialName, deleteElems, path); | ||
@@ -151,3 +151,3 @@ if (isExposableStructure(node)) { | ||
let newType = exposeStructType(newTypeFullName, newTypeElements, memberName); | ||
let newType = exposeStructType(newTypeFullName, newTypeElements, memberName, path); | ||
if (!newType) { | ||
@@ -163,3 +163,3 @@ // Error already reported | ||
if (node.elements && node.elements[elemName].$location) setProp(newType.elements[elemName], '$location', node.elements[elemName].$location); | ||
exposeStructTypeOf(newType.elements[elemName], memberName, service, `${newTypeId}_${elemName}`, deleteElems); | ||
exposeStructTypeOf(newType.elements[elemName], memberName, service, `${newTypeId}_${elemName}`, deleteElems, path); | ||
} | ||
@@ -194,3 +194,3 @@ typeDef.kind === 'type' ? copyAnnotations(typeDef, newType) : copyAnnotations(node, newType); | ||
*/ | ||
function exposeStructType(typeName, elements, parentName) { | ||
function exposeStructType(typeName, elements, parentName, path) { | ||
// If type already exists, reuse it (complain if not created here) | ||
@@ -200,3 +200,3 @@ let type = csn.definitions[typeName]; | ||
if (!exposedStructTypes.includes(typeName)) { | ||
signal(signal.error`Cannot create artificial type "${typeName}" for "${parentName}" because the name is already used`, ['definitions', parentName]); | ||
signal(signal.error`Cannot create artificial type "${typeName}" for "${parentName}" because the name is already used`, path); | ||
return null; | ||
@@ -234,3 +234,3 @@ } | ||
// like we expose structures in structured mode | ||
function exposeArrayOfTypeOf(node, memberName, service, artificialName) { | ||
function exposeArrayOfTypeOf(node, memberName, service, artificialName, path) { | ||
// if anonymously defined in place -> we always expose the type | ||
@@ -240,3 +240,3 @@ // this would be definition like 'elem: array of { ... }' | ||
if (node.items && !node.type) { | ||
exposeStructTypeOf(node, memberName, service, artificialName, true); | ||
exposeStructTypeOf(node, memberName, service, artificialName, true, path); | ||
} | ||
@@ -243,0 +243,0 @@ // we can have both of the 'type' and 'items' in the cases: |
@@ -25,12 +25,8 @@ const { | ||
function collectAllManagedAssociations(csn) { | ||
function forEachManagedAssociation(csn, callback) { | ||
let associations = []; | ||
forEachDefinition(csn, (def, definitionName) => { | ||
let root = ['definitions', definitionName]; | ||
forEachMemberRecursively(def, (element, elementName, _prop, subpath, _parent) => { | ||
forEachDefinition(csn, (def) => { | ||
forEachMemberRecursively(def, (element) => { | ||
if (isAssociationOrComposition(element) && !element.on) { | ||
let path = root.concat(subpath); | ||
associations.push({ definitionName, elementName, element, path }); | ||
callback(element) | ||
} | ||
@@ -40,3 +36,2 @@ }) | ||
return associations; | ||
} | ||
@@ -97,3 +92,3 @@ | ||
module.exports = { | ||
collectAllManagedAssociations, | ||
forEachManagedAssociation, | ||
defNameWithoutServiceName, | ||
@@ -100,0 +95,0 @@ getServiceOfArtifact, |
@@ -30,3 +30,3 @@ 'use strict'; | ||
isAssociationOperand, | ||
isDollarSelfOperand, | ||
isDollarSelfOrProjectionOperand, | ||
createExposingProjection, | ||
@@ -570,4 +570,4 @@ createAndAddDraftAdminDataProjection, | ||
// Return true if 'arg' is an expression argument denoting "$self" | ||
function isDollarSelfOperand(arg) { | ||
// Return true if 'arg' is an expression argument denoting "$self" || "$projection" | ||
function isDollarSelfOrProjectionOperand(arg) { | ||
return arg.path && arg.path.length == 1 && (arg.path[0].id === '$self' || arg.path[0].id === '$projection'); | ||
@@ -574,0 +574,0 @@ } |
@@ -10,5 +10,8 @@ 'use strict'; | ||
const { setProp } = require('../base/model'); | ||
const csnRefs = require('../model/csnRefs'); | ||
// eslint-disable-next-line no-unused-vars | ||
const { copyAnnotations, printableName, hasBoolAnnotation, forEachDefinition } = require('../model/modelUtils'); | ||
const { cloneCsn, forEachRef, getUtils, isBuiltinType } = require('../model/csnUtils'); | ||
const { cloneCsn, forEachMemberRecursively, forEachGeneric, forAllQueries, | ||
forEachRef, getUtils, isBuiltinType } = require('../model/csnUtils'); | ||
@@ -30,2 +33,7 @@ // Return the public functions of this module, with 'model' captured in a closure (for definitions, options etc). | ||
const { | ||
effectiveType, | ||
} = csnRefs(model); | ||
return { | ||
@@ -44,3 +52,3 @@ resolvePath, | ||
isAssociationOperand, | ||
isDollarSelfOperand, | ||
isDollarSelfOrProjectionOperand, | ||
getFinalBaseType, | ||
@@ -64,2 +72,3 @@ createExposingProjection, | ||
setAnnotation, | ||
expandStructsInOnConditions, | ||
}; | ||
@@ -111,3 +120,3 @@ | ||
newForeignKey(fkArtifact,foreignKeyElementName) | ||
newForeignKey(fkArtifact,foreignKeyElementName); | ||
@@ -122,3 +131,3 @@ function processAssociationOrComposition(fkArtifact,foreignKeyElementName) { | ||
if(iKey.ref.length>1) | ||
throw Error(`createForeignKeyElement(${artifactName},${assocName},${iKey.$path.join('/')}) unexpected reference: `+iKey.ref) | ||
throw Error(`createForeignKeyElement(${artifactName},${assocName},${iKey.$path.join('/')}) unexpected reference: `+ iKey.ref) | ||
newForeignKey(iKeyArtifact,foreignKeyElementName+'_'+iKey.ref[0]) | ||
@@ -129,5 +138,5 @@ }) | ||
// compose new foreign key out of 'fkArtifact' named 'foreignKeyElementName' | ||
function newForeignKey(fkArtifact,foreignKeyElementName) { | ||
if(fkArtifact.type=='cds.Association' || fkArtifact.type=='cds.Composition' ) { | ||
processAssociationOrComposition(fkArtifact,foreignKeyElementName) | ||
function newForeignKey(fkArtifact, foreignKeyElementName) { | ||
if (fkArtifact.type === 'cds.Association' || fkArtifact.type === 'cds.Composition') { | ||
processAssociationOrComposition(fkArtifact, foreignKeyElementName) | ||
return; | ||
@@ -158,3 +167,4 @@ } | ||
if (artifact.elements[foreignKeyElementName]) { | ||
signal(error`Generated foreign key element "${foreignKeyElementName}" for association "${assocName}" conflicts with existing element`, ['definitions', artifactName, 'elements', foreignKeyElementName]); | ||
signal(error`Generated foreign key element "${foreignKeyElementName}" for association "${assocName}" conflicts with existing element`, | ||
artifact.elements.$path ? artifact.elements.$path.concat([foreignKeyElementName]) : ['definitions', artifactName, 'elements', foreignKeyElementName]); | ||
} | ||
@@ -169,3 +179,3 @@ artifact.elements[foreignKeyElementName] = foreignKeyElement; | ||
setProp(foreignKeyElement, '$path', path); // attach $path to the newly created element - used for inspectRef in processAssociationOrComposition | ||
if(assoc.$location){ | ||
if (assoc.$location) { | ||
setProp(foreignKeyElement, '$location', assoc.$location); | ||
@@ -400,5 +410,6 @@ } | ||
// then the 'assocDef' does not have 'target' property, but only 'targetAspect' property | ||
// or the targetAspect might be an object, when the managed composition was defined anonymously | ||
let assocTarget = assocDef.target || assocDef.targetAspect; | ||
let assocTargetDef = getCsnDef(assocTarget); | ||
if (!assocDef._ignore && assocTarget && assocTargetDef && !assocTarget.startsWith(service)) { | ||
let assocTargetDef = typeof assocTarget === 'string' ? getCsnDef(assocTarget) : assocDef.targetAspect.elements; | ||
if (!assocDef._ignore && assocTarget && assocTargetDef && (typeof assocTarget === 'string' && !assocTarget.startsWith(service))) { | ||
// If we have a 'preserved dotted name' -> a result of flattening -> This scenario is not supported yet | ||
@@ -642,5 +653,5 @@ if (assocDef._flatElementNameWithDots) | ||
// Return true if 'arg' is an expression argument denoting "$self" | ||
function isDollarSelfOperand(arg) { | ||
return arg.ref && arg.ref.length == 1 && (arg.ref[0] === '$self'); | ||
// Return true if 'arg' is an expression argument denoting "$self" || "$projection" | ||
function isDollarSelfOrProjectionOperand(arg) { | ||
return arg.ref && arg.ref.length == 1 && (arg.ref[0] === '$self' || arg.ref[0] === '$projection'); | ||
} | ||
@@ -1060,4 +1071,4 @@ | ||
for(const k of art.keys) { | ||
const nps = { ref: k.ref.map(p => { | ||
return ( fullRef ? { id: p } : p ) } ), _art: k._art }; | ||
const nps = { ref: k.ref.map(p => fullRef ? { id: p } : p ) }; | ||
setProp(nps, '_art', k._art); | ||
const paths = flattenPath( nps, fullRef, followMgdAssoc ); | ||
@@ -1079,3 +1090,4 @@ // prepend prefix path | ||
for(const en in elements) { | ||
const nps = { ref: [ (fullRef ? { id: en, _art: elements[en] } : en )], _art: elements[en] }; | ||
const nps = { ref: [ (fullRef ? { id: en, _art: elements[en] } : en )] }; | ||
setProp(nps, '_art', elements[en]); | ||
const paths = flattenPath( nps, fullRef, followMgdAssoc ); | ||
@@ -1089,3 +1101,3 @@ // prepend prefix path | ||
else | ||
path._art = art; | ||
setProp(path, '_art', art); | ||
} | ||
@@ -1095,3 +1107,162 @@ return [path]; | ||
/* | ||
* Expand structured ON condition arguments to flat reference paths. | ||
* Structured elements are real sub element lists and managed associations. | ||
* All unmanaged association definitions are rewritten if applicable (elements/mixins). | ||
* | ||
* TODO: Check if can be skipped for abstract entity and or cds.persistence.skip ? | ||
*/ | ||
function expandStructsInOnConditions(artifact, artifactName, prop, path) { | ||
forEachMemberRecursively(artifact, | ||
(elem, elemName, prop, path) => { | ||
if(prop === 'elements') { | ||
if(elem.target && elem.on) { | ||
elem.on = expand(elem.on, path) | ||
} | ||
} | ||
}, path); | ||
if(artifact.query) { | ||
forAllQueries(artifact.query, (query) => { | ||
if(query.SELECT && query.SELECT.mixin) { | ||
forEachGeneric(query.SELECT, 'mixin', (mixin, mixinName, prop, path) => { | ||
if(mixin.target && mixin.on) { | ||
mixin.on = expand(mixin.on, path); | ||
} | ||
}, | ||
path); | ||
} | ||
}, path.concat([ 'query' ])); | ||
} | ||
/* | ||
flatten structured leaf types and return array of paths | ||
Flattening stops on all non-structured types. | ||
*/ | ||
function expand(expr, location) { | ||
let rc = []; | ||
for(let i = 0; i < expr.length; i++) | ||
{ | ||
if(Array.isArray(expr[i])) | ||
rc.push(expr[i].map(expand, location)); | ||
if(i < expr.length-2) | ||
{ | ||
const [lhs, op, rhs] = expr.slice(i); | ||
// lhs & rhs must be expandable types (structures or managed associations) | ||
if(lhs._art && rhs._art && | ||
lhs.ref && rhs.ref && | ||
isExpandable(lhs._art) && isExpandable(rhs._art) && | ||
['=', '<', '>', '>=', '<=', '!=', '<>'].includes(op) && | ||
!(isDollarSelfOrProjectionOperand(lhs) || isDollarSelfOrProjectionOperand(rhs))) { | ||
// if path is scalar and no assoc or has no type (@Core.Computed) use original expression | ||
// only do the expansion on (managed) assocs and (items.)elements, array of check in ON cond is done elsewhere | ||
const lhspaths = /*isScalarOrNoType(lhs._art) ? [ lhs ] : */ flattenPath({ _art: lhs._art, ref: lhs.ref }, false, true ); | ||
const rhspaths = /*isScalarOrNoType(rhs._art) ? [ rhs ] : */ flattenPath({ _art: rhs._art, ref: rhs.ref }, false, true ); | ||
// mapping dict for lhs/rhs for mismatch check | ||
// strip lhs/rhs prefix from flattened paths to check remaining common trailing path | ||
// if path is idempotent, it doesn't produce new flattened paths (ends on scalar type) | ||
// key is then empty string on both sides '' (=> equality) | ||
// Path matches if lhs/rhs are available | ||
const xref = lhspaths.reduce((a, v) => { | ||
a[v.ref.slice(lhs.ref.length).join('.')] = { lhs: v }; | ||
return a; | ||
}, Object.create(null)); | ||
rhspaths.forEach(v => { | ||
const k = v.ref.slice(rhs.ref.length).join('.'); | ||
if(xref[k]) | ||
xref[k].rhs = v; | ||
else | ||
xref[k] = { rhs: v }; | ||
}); | ||
let cont = true; | ||
for(const xn in xref) { | ||
const x = xref[xn]; | ||
// do the paths match? | ||
if(!(x.lhs && x.rhs)) { | ||
if(xn.length) | ||
signal(signal.error`'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Sub path '${xn}' not found in ${((x.lhs ? rhs : lhs).ref.join('.'))}`, location) | ||
else | ||
signal(signal.error`'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Path '${((x.lhs ? lhs : rhs).ref.join('.'))}' does not match ${((x.lhs ? rhs : lhs).ref.join('.'))}`, location) | ||
cont = false; | ||
} | ||
// lhs && rhs are present, consistency checks that affect both ends | ||
else { | ||
// is lhs scalar? | ||
if(!isScalarOrNoType(x.lhs._art)) { | ||
signal(signal.error`'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Path '${x.lhs.ref.join('.')}${(xn.length ? '.' + xn : '')}' must end on a scalar type`, location) | ||
cont = false; | ||
} | ||
// is rhs scalar? | ||
if(!isScalarOrNoType(x.rhs._art)) { | ||
signal(signal.error`'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Path '${x.rhs.ref.join('.')}${(xn.length ? '.' + xn : '')}' must end on a scalar type`, location) | ||
cont = false; | ||
} | ||
// info about type incompatibility if no other errors occured | ||
if(xn && cont) { | ||
const lhst = getType(x.lhs._art); | ||
const rhst = getType(x.rhs._art); | ||
if(lhst !== rhst) { | ||
signal(signal.info`'${lhs.ref.join('.')} ${op} ${rhs.ref.join('.')}': Types for sub path '${xn}' don't match`, location) | ||
} | ||
} | ||
} | ||
} | ||
// don't continue if there are path errors | ||
if(!cont) | ||
return expr; | ||
Object.keys(xref).forEach((k, i) => { | ||
const x = xref[k]; | ||
if(i>0) | ||
rc.push('and'); | ||
rc.push(x.lhs); | ||
rc.push(op); | ||
rc.push(x.rhs); | ||
}); | ||
i += 2; | ||
} | ||
else | ||
rc.push(expr[i]); | ||
} | ||
else | ||
rc.push(expr[i]); | ||
} | ||
return rc; | ||
function getType(art) { | ||
const effart = effectiveType(art); | ||
return Object.keys(effart).length ? effart : art.type; | ||
} | ||
function isExpandable(art) { | ||
art = effectiveType(art); | ||
if(art) { | ||
// items in ON conds are illegal but this should be checked elsewere | ||
const elements = art.elements || (art.items && art.items.elements); | ||
return (elements || art.target && art.keys) | ||
} | ||
return false; | ||
} | ||
function isScalarOrNoType(art) { | ||
art = effectiveType(art); | ||
if(art) { | ||
const type = art.type || (art.items && art.items.type); | ||
// items in ON conds are illegal but this should be checked elsewere | ||
const elements = art.elements || (art.items && art.items.elements); | ||
// @Core.Computed has no type | ||
return(!elements && !type || | ||
(type && isBuiltinType(type) && | ||
!['cds.Association', 'cds.Composition'].includes(type))) | ||
} | ||
return false; | ||
} | ||
} | ||
} | ||
} | ||
@@ -1098,0 +1269,0 @@ |
{ | ||
"name": "@sap/cds-compiler", | ||
"version": "1.39.0", | ||
"version": "1.42.2", | ||
"description": "CDS (Core Data Services) compiler and backends", | ||
@@ -18,3 +18,3 @@ "homepage": "https://cap.cloud.sap/", | ||
"dependencies": { | ||
"antlr4": "4.7.1", | ||
"antlr4": "4.8.0", | ||
"resolve": "1.8.1", | ||
@@ -21,0 +21,0 @@ "sax": "^1.2.4" |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is not supported yet
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
3741145
125
71507
+ Addedantlr4@4.8.0(transitive)
- Removedantlr4@4.7.1(transitive)
Updatedantlr4@4.8.0