@sap/cds-compiler 6.2.2 → 6.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +35 -0
- package/bin/cdsc.js +11 -4
- package/lib/api/options.js +1 -1
- package/lib/base/message-registry.js +36 -7
- package/lib/base/messages.js +11 -4
- package/lib/base/model.js +0 -1
- package/lib/checks/assocOutsideService.js +17 -30
- package/lib/checks/checkForTypes.js +0 -18
- package/lib/checks/checkPathsInStoredCalcElement.js +2 -1
- package/lib/checks/onConditions.js +2 -2
- package/lib/checks/queryNoDbArtifacts.js +16 -15
- package/lib/checks/types.js +1 -1
- package/lib/checks/utils.js +30 -6
- package/lib/checks/validator.js +4 -5
- package/lib/compiler/checks.js +47 -18
- package/lib/compiler/index.js +88 -6
- package/lib/compiler/resolve.js +7 -7
- package/lib/compiler/tweak-assocs.js +47 -25
- package/lib/gen/BaseParser.js +1 -1
- package/lib/gen/CdlGrammar.checksum +1 -1
- package/lib/gen/CdlParser.js +381 -378
- package/lib/gen/Dictionary.json +0 -2
- package/lib/model/csnRefs.js +9 -4
- package/lib/model/csnUtils.js +67 -2
- package/lib/optionProcessor.js +2 -3
- package/lib/parsers/AstBuildingParser.js +5 -6
- package/lib/render/toCdl.js +10 -4
- package/lib/render/utils/common.js +4 -2
- package/lib/transform/db/assertUnique.js +2 -1
- package/lib/transform/db/associations.js +37 -1
- package/lib/transform/db/assocsToQueries/transformExists.js +21 -32
- package/lib/transform/db/assocsToQueries/utils.js +1 -1
- package/lib/transform/db/cdsPersistence.js +1 -1
- package/lib/transform/db/expansion.js +37 -36
- package/lib/transform/draft/db.js +20 -20
- package/lib/transform/draft/odata.js +38 -40
- package/lib/transform/effective/associations.js +1 -1
- package/lib/transform/effective/flattening.js +40 -47
- package/lib/transform/effective/main.js +6 -4
- package/lib/transform/forOdata.js +135 -115
- package/lib/transform/forRelationalDB.js +151 -142
- package/lib/transform/localized.js +116 -109
- package/lib/transform/odata/adaptAnnotationRefs.js +21 -16
- package/lib/transform/odata/createForeignKeys.js +73 -70
- package/lib/transform/odata/flattening.js +216 -200
- package/lib/transform/odata/foreignKeyRefsInXprAnnos.js +47 -45
- package/lib/transform/odata/toFinalBaseType.js +40 -39
- package/lib/transform/odata/typesExposure.js +151 -133
- package/lib/transform/odata/utils.js +7 -6
- package/lib/transform/parseExpr.js +165 -162
- package/lib/transform/transformUtils.js +184 -551
- package/lib/transform/translateAssocsToJoins.js +510 -571
- package/lib/transform/tupleExpansion.js +495 -0
- package/lib/transform/universalCsn/universalCsnEnricher.js +1 -0
- package/package.json +1 -1
- package/lib/base/cleanSymbols.js +0 -17
- package/lib/checks/nonexpandableStructured.js +0 -39
package/CHANGELOG.md
CHANGED
|
@@ -8,6 +8,41 @@ Note: `beta` fixes, changes and features are usually not listed in this ChangeLo
|
|
|
8
8
|
but in [doc/CHANGELOG_BETA.md](doc/CHANGELOG_BETA.md).
|
|
9
9
|
The compiler behavior concerning `beta` features can change at any time without notice.
|
|
10
10
|
|
|
11
|
+
## Version 6.3.0 - 2025-08-28
|
|
12
|
+
|
|
13
|
+
### Added
|
|
14
|
+
|
|
15
|
+
- compiler: Column casts can now use more modifiers such as `default` directly.
|
|
16
|
+
- for.odata/to.edm(x):
|
|
17
|
+
+ New option `draftUserDescription` is now available. It adds the fields `CreatedByUserDescription`,
|
|
18
|
+
`LastChangedByUserDescription`, `InProcessByUserDescription` to the `DraftAdministrativeData` entity.
|
|
19
|
+
- to.sql:
|
|
20
|
+
+ Structures with only one element can now be compared to scalar values.
|
|
21
|
+
This also applies to associations with only one foreign key.
|
|
22
|
+
+ `cds.UInt8` can now be used in SQL dialects "h2" and "postgres".
|
|
23
|
+
+ Managed associations can now be used in comparisons, e.g. `assoc = struct`.
|
|
24
|
+
+ Structures and managed associations with only one element can be compared with scalars, e.g. `struct = 1`.
|
|
25
|
+
+ In the draft use case, the `DRAFT.DraftAdministrativeData` entity now includes the following fields by default:
|
|
26
|
+
`CreatedByUserDescription`, `LastChangedByUserDescription`, `InProcessByUserDescription`, and `DraftMessages`.
|
|
27
|
+
|
|
28
|
+
### Changed
|
|
29
|
+
|
|
30
|
+
- Update OData vocabularies: Common
|
|
31
|
+
- cdsc: EDMX output uses XML comments as service separators instead of `//`.
|
|
32
|
+
If there is only one service, no header is printed, allowing piping the output to a file.
|
|
33
|
+
- to.sql: path expressions which end in a foreign key are now always optimized to use the element of the source side.
|
|
34
|
+
|
|
35
|
+
### Fixed
|
|
36
|
+
|
|
37
|
+
- compiler: Redirecting associations to non-query entities was fixed.
|
|
38
|
+
- to.sql/to.edm(x): References to associations can now be compared to other associations and structures.
|
|
39
|
+
- to.sql: Referencing a foreign key of an `@cds.persistence.skip` entity previously caused an
|
|
40
|
+
error in queries. Now the foreign key in the source entity is resolved and rendered.
|
|
41
|
+
|
|
42
|
+
### Removed
|
|
43
|
+
|
|
44
|
+
- for.odata/to.edm(x): The `addAnnotationAddressViaNavigationPath` option has been removed. Its functionality is included in the `draftMessages` option.
|
|
45
|
+
|
|
11
46
|
## Version 6.2.2 - 2025-07-28
|
|
12
47
|
|
|
13
48
|
### Fixed
|
package/bin/cdsc.js
CHANGED
|
@@ -462,13 +462,15 @@ async function executeCommandLine( command, options, args ) {
|
|
|
462
462
|
}
|
|
463
463
|
else if (options.json) {
|
|
464
464
|
const result = main.to.edm.all(csn, options);
|
|
465
|
+
const omitHeadline = Object.keys(result).length === 1;
|
|
465
466
|
for (const serviceName in result)
|
|
466
|
-
writeToFileOrDisplay(options.out, `${ serviceName }.json`, result[serviceName]);
|
|
467
|
+
writeToFileOrDisplay(options.out, `${ serviceName }.json`, result[serviceName], omitHeadline);
|
|
467
468
|
}
|
|
468
469
|
else {
|
|
469
470
|
const result = main.to.edmx.all(csn, options);
|
|
471
|
+
const omitHeadline = Object.keys(result).length === 1;
|
|
470
472
|
for (const serviceName in result)
|
|
471
|
-
writeToFileOrDisplay(options.out, `${ serviceName }.xml`, result[serviceName]);
|
|
473
|
+
writeToFileOrDisplay(options.out, `${ serviceName }.xml`, result[serviceName], omitHeadline);
|
|
472
474
|
}
|
|
473
475
|
return model;
|
|
474
476
|
}
|
|
@@ -710,8 +712,13 @@ async function executeCommandLine( command, options, args ) {
|
|
|
710
712
|
hdbview: true,
|
|
711
713
|
hdbprojectionview: true,
|
|
712
714
|
};
|
|
713
|
-
|
|
714
|
-
|
|
715
|
+
if (fileName.endsWith('.xml')) {
|
|
716
|
+
process.stdout.write(`<!-- ------------------- ${ fileName.replaceAll('-->', '-- >') } ------------------- -->\n`);
|
|
717
|
+
}
|
|
718
|
+
else {
|
|
719
|
+
const commentStarter = fileName.split('.').pop() in sqlTypes ? '--$' : '//';
|
|
720
|
+
process.stdout.write(`${ commentStarter } ------------------- ${ fileName } -------------------\n`);
|
|
721
|
+
}
|
|
715
722
|
}
|
|
716
723
|
|
|
717
724
|
process.stdout.write(`${ content }\n`);
|
package/lib/api/options.js
CHANGED
|
@@ -36,7 +36,6 @@ const publicOptionsNewAPI = [
|
|
|
36
36
|
'booleanEquality',
|
|
37
37
|
'dollarNowAsTimestamp',
|
|
38
38
|
// ODATA
|
|
39
|
-
'addAnnotationAddressViaNavigationPath',
|
|
40
39
|
'odataOpenapiHints',
|
|
41
40
|
'edm4OpenAPI',
|
|
42
41
|
'odataVersion',
|
|
@@ -50,6 +49,7 @@ const publicOptionsNewAPI = [
|
|
|
50
49
|
'odataVocabularies',
|
|
51
50
|
'odataNoCreator',
|
|
52
51
|
'draftMessages',
|
|
52
|
+
'draftUserDescription',
|
|
53
53
|
'service',
|
|
54
54
|
'serviceNames',
|
|
55
55
|
// to.cdl
|
|
@@ -178,7 +178,7 @@ const centralMessages = {
|
|
|
178
178
|
'service-nested-context': { severity: 'Error', configurableFor: true }, // does not hurt compile, TODO
|
|
179
179
|
'service-nested-service': { severity: 'Error' }, // not supported yet; TODO: configurableFor:'test'?
|
|
180
180
|
|
|
181
|
-
'expr-unexpected-operator': { severity: 'Error'
|
|
181
|
+
'expr-unexpected-operator': { severity: 'Error' },
|
|
182
182
|
|
|
183
183
|
// Published! Used in @sap/cds-lsp; if renamed, add to oldMessageIds and contact colleagues
|
|
184
184
|
// Also used by other projects that rely on double-quotes for delimited identifiers.
|
|
@@ -196,7 +196,7 @@ const centralMessages = {
|
|
|
196
196
|
'syntax-missing-as': { severity: 'Error', configurableFor: true },
|
|
197
197
|
'syntax-missing-proj-semicolon': { severity: 'Warning' },
|
|
198
198
|
'syntax-unexpected-after': { severity: 'Error' },
|
|
199
|
-
'syntax-unexpected-filter': { severity: 'Error', configurableFor:
|
|
199
|
+
'syntax-unexpected-filter': { severity: 'Error', configurableFor: 'v7' },
|
|
200
200
|
'syntax-unexpected-many-one': { severity: 'Error' },
|
|
201
201
|
'syntax-deprecated-ref-virtual': { severity: 'Error' },
|
|
202
202
|
'syntax-unexpected-reserved-word': { severity: 'Error', configurableFor: true },
|
|
@@ -786,8 +786,11 @@ const centralMessageTexts = {
|
|
|
786
786
|
calc: 'Calculated elements can\'t use parameter references',
|
|
787
787
|
},
|
|
788
788
|
'ref-unexpected-structured': {
|
|
789
|
-
std: 'Unexpected usage of structured
|
|
790
|
-
|
|
789
|
+
std: 'Unexpected usage of structured element $(ELEMREF)',
|
|
790
|
+
assoc: 'Unexpected usage of managed association $(ELEMREF)',
|
|
791
|
+
'struct-expr': 'Structured element $(ELEMREF) can\'t be used in expressions with scalars; only possible for structures with one leaf-element',
|
|
792
|
+
'assoc-expr': 'Associations $(ELEMREF) can\'t be used in expressions with scalars; only possible for association with one foreign key',
|
|
793
|
+
complexExpr: 'Unexpected reference to a structured element $(ELEMREF) in expression $(VALUE)',
|
|
791
794
|
},
|
|
792
795
|
'ref-unexpected-virtual': {
|
|
793
796
|
std: 'Unexpected reference to virtual element $(NAME)', // "std" currently unused
|
|
@@ -801,8 +804,14 @@ const centralMessageTexts = {
|
|
|
801
804
|
'with-filter': 'Unexpected reference to an association with filter',
|
|
802
805
|
'self-with-filter': 'Unexpected column reference starting with $(ALIAS) to an association with filter',
|
|
803
806
|
self: 'A reference to an unmanaged association is only valid when compared via $(CODE)',
|
|
807
|
+
|
|
804
808
|
expr: 'Associations can\'t be used as values in expressions',
|
|
805
809
|
'expr-comp': 'Compositions can\'t be used as values in expressions',
|
|
810
|
+
'anno-expr': 'Associations can\'t be used in expressions for annotation values',
|
|
811
|
+
'anno-expr-comp': 'Compositions can\'t be used in expressions for annotation values',
|
|
812
|
+
'query-expr': 'Unmanaged associations can\'t be used in expressions in queries',
|
|
813
|
+
'query-expr-comp': 'Unmanaged compositions can\'t be used in expressions in queries',
|
|
814
|
+
|
|
806
815
|
'assoc-stored': 'Associations and compositions can\'t be used as values in stored calculated elements',
|
|
807
816
|
|
|
808
817
|
'managed-filter': 'Unexpected managed association $(NAME) in filter expression of $(ID)',
|
|
@@ -831,6 +840,7 @@ const centralMessageTexts = {
|
|
|
831
840
|
unmanagedleaf: 'Unexpected unmanaged association as final path step of $(ELEMREF) in an ON-condition',
|
|
832
841
|
'calc-non-fk': 'Can\'t follow association $(ID) in path $(ELEMREF) in a stored calculated element; only foreign keys can be referred to, but not $(NAME)',
|
|
833
842
|
'calc-unmanaged': 'Can\'t follow association $(ID) in path $(ELEMREF) in a stored calculated element',
|
|
843
|
+
'calc-missing': 'Missing foreign key access for association $(ID) in path $(ELEMREF) in a stored calculated element',
|
|
834
844
|
},
|
|
835
845
|
'ref-unexpected-filter': {
|
|
836
846
|
std: 'Unexpected filter in path $(ELEMREF)', // unused
|
|
@@ -983,10 +993,16 @@ const centralMessageTexts = {
|
|
|
983
993
|
},
|
|
984
994
|
'def-unexpected-key': {
|
|
985
995
|
std: '$(ART) can\'t have additional keys',
|
|
986
|
-
virtual: 'Unexpected $(
|
|
996
|
+
virtual: 'Unexpected $(KEYWORD) for virtual element',
|
|
987
997
|
// TODO: Better message?
|
|
988
998
|
include: '$(ART) can\'t have additional keys (through include)',
|
|
989
|
-
invalidType: 'Unexpected $(
|
|
999
|
+
invalidType: 'Unexpected $(KEYWORD) for element of type $(TYPE)',
|
|
1000
|
+
},
|
|
1001
|
+
'def-unsupported-key': {
|
|
1002
|
+
std: '$(KEYWORD) is not supported here', // unused variant
|
|
1003
|
+
kind: '$(KEYWORD) is only supported for elements in an entity or an aspect',
|
|
1004
|
+
sub: '$(KEYWORD) is only supported for top-level elements',
|
|
1005
|
+
type: '$(KEYWORD) is not supported for elements of type $(TYPE)',
|
|
990
1006
|
},
|
|
991
1007
|
'def-unexpected-localized': {
|
|
992
1008
|
std: 'Unexpected $(KEYWORD)',
|
|
@@ -1046,6 +1062,9 @@ const centralMessageTexts = {
|
|
|
1046
1062
|
'include-elements': 'Duplicate element $(NAME) through multiple includes $(SORTED_ARTS)',
|
|
1047
1063
|
'include-actions': 'Duplicate action or function $(NAME) through multiple includes $(SORTED_ARTS)',
|
|
1048
1064
|
},
|
|
1065
|
+
'ref-invalid-assoc-navigation': {
|
|
1066
|
+
std: 'Invalid navigation along association $(ID) in path $(ELEMREF) to target $(NAME) having annotation $(ANNO)',
|
|
1067
|
+
},
|
|
1049
1068
|
'ref-invalid-element': {
|
|
1050
1069
|
std: 'Invalid element reference',
|
|
1051
1070
|
$tableAlias: 'Can\'t refer to source elements of table alias $(ID)',
|
|
@@ -1254,7 +1273,17 @@ const centralMessageTexts = {
|
|
|
1254
1273
|
publishingFilter: 'Can\'t publish managed association $(ID) with filter, as it must have at least one foreign key',
|
|
1255
1274
|
},
|
|
1256
1275
|
|
|
1257
|
-
|
|
1276
|
+
'expr-invalid-expansion': {
|
|
1277
|
+
std: 'Path $(NAME) in expression $(VALUE) can\'t be expanded',
|
|
1278
|
+
'path-mismatch': 'Missing sub path $(NAME) in $(ALIAS) for tuple expansion of $(VALUE); both sides must expand to the same sub paths',
|
|
1279
|
+
'non-scalar': 'Path $(NAME) in expression $(VALUE) can\'t be expanded as it does not contain any scalar element',
|
|
1280
|
+
},
|
|
1281
|
+
'expr-unsupported-expansion': {
|
|
1282
|
+
std: 'Unsupported $(ELEMREF) in structural expression $(VALUE)',
|
|
1283
|
+
scalarRef: 'Unsupported scalar reference $(ELEMREF) in structural expression $(VALUE)',
|
|
1284
|
+
},
|
|
1285
|
+
|
|
1286
|
+
// tenant isolation via discriminator column:
|
|
1258
1287
|
'tenant-invalid-alias-name': {
|
|
1259
1288
|
std: 'Can\'t have a table alias named $(NAME) in a tenant-dependent entity',
|
|
1260
1289
|
implicit: 'Provide an explicit table alias name; do not use $(NAME)',
|
package/lib/base/messages.js
CHANGED
|
@@ -911,14 +911,20 @@ function transformElementRef( arg ) {
|
|
|
911
911
|
if (!ref)
|
|
912
912
|
return quoted( arg );
|
|
913
913
|
// Can be used by CSN backends or compiler to create a simple path such as E:elem
|
|
914
|
-
return quoted(
|
|
915
|
-
|
|
914
|
+
return quoted( pathToMessageString( arg ) );
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
function pathToMessageString( arg ) {
|
|
918
|
+
const ref = arg?.ref || arg?.path || arg; // support CSN and XSN
|
|
919
|
+
if (!ref)
|
|
920
|
+
return null;
|
|
921
|
+
|
|
922
|
+
return ((arg.scope === 'param' || arg.param) ? ':' : '') +
|
|
916
923
|
ref.map(
|
|
917
924
|
item => (typeof item !== 'string'
|
|
918
925
|
? `${ item.id }${ item.args ? '(…)' : '' }${ item.where ? '[…]' : '' }`
|
|
919
926
|
: item)
|
|
920
|
-
).join('.')
|
|
921
|
-
);
|
|
927
|
+
).join('.');
|
|
922
928
|
}
|
|
923
929
|
|
|
924
930
|
function transformArg( arg, r, args, texts ) {
|
|
@@ -1958,4 +1964,5 @@ module.exports = {
|
|
|
1958
1964
|
// for tests only
|
|
1959
1965
|
constructSemanticLocationFromCsnPath,
|
|
1960
1966
|
homeName,
|
|
1967
|
+
pathToMessageString,
|
|
1961
1968
|
};
|
package/lib/base/model.js
CHANGED
|
@@ -1,42 +1,29 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
+
// Only to be used with validator.js - a correct this value needs to be provided!
|
|
4
|
+
const { forEachMemberRecursively } = require('../model/csnUtils');
|
|
5
|
+
|
|
3
6
|
/**
|
|
4
7
|
* Asserts that there is no association usage outside of the specified service.
|
|
5
8
|
* We do not check in type-ofs - we resolve them, so they are not a problem.
|
|
6
9
|
*
|
|
7
|
-
* @param {
|
|
8
|
-
* @param {string}
|
|
9
|
-
* @param {object} ref - The reference object.
|
|
10
|
-
* @param {Array} path - The path array indicating the location in the CSN.
|
|
11
|
-
* @param {object} grandparent - The grandparent object in the CSN.
|
|
12
|
-
* @param {string} parentProp - The property name of the grandparent object.
|
|
10
|
+
* @param {CSN.Artifact} artifact Artifact to validate
|
|
11
|
+
* @param {string} artifactName Name of the artifact
|
|
13
12
|
*/
|
|
14
|
-
function assertNoAssocUsageOutsideOfService(
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
return;
|
|
18
|
-
|
|
19
|
-
if (this.csn.definitions[this.options.effectiveServiceName]?.kind !== 'service' ||
|
|
20
|
-
!artifactName.startsWith(`${ this.options.effectiveServiceName }.`))
|
|
21
|
-
return;
|
|
22
|
-
|
|
23
|
-
const { _links } = parent;
|
|
24
|
-
// session variables can't have assoc steps, _links of 1 can't have assoc steps
|
|
25
|
-
// TODO: (typeof parentProp === 'number' && path[path.length - 2] === 'on') - ignore on-conditions, as they are cut off anyway
|
|
26
|
-
if (parent.$scope === '$magic' || _links?.length <= 1 )
|
|
13
|
+
function assertNoAssocUsageOutsideOfService( artifact, artifactName ) {
|
|
14
|
+
if (artifact.kind !== 'entity' || this.csn.definitions[this.options.effectiveServiceName]?.kind !== 'service' ||
|
|
15
|
+
!artifactName.startsWith(`${ this.options.effectiveServiceName }.`))
|
|
27
16
|
return;
|
|
28
17
|
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
}
|
|
18
|
+
if (artifact.kind === 'entity' && (artifact.query || artifact.projection)) {
|
|
19
|
+
forEachMemberRecursively(artifact, (element, elementName, prop, path) => {
|
|
20
|
+
if (element && element.target && !element.target.startsWith(`${ this.options.effectiveServiceName }.`)) {
|
|
21
|
+
this.error('assoc-invalid-outside-service', path,
|
|
22
|
+
{ name: this.options.effectiveServiceName, id: elementName },
|
|
23
|
+
'Association $(ID) pointing outside of service $(NAME) must not be published');
|
|
24
|
+
}
|
|
25
|
+
}, [ 'definitions', artifactName ], true, { elementsOnly: true });
|
|
37
26
|
}
|
|
38
27
|
}
|
|
39
28
|
|
|
40
|
-
module.exports =
|
|
41
|
-
ref: assertNoAssocUsageOutsideOfService,
|
|
42
|
-
};
|
|
29
|
+
module.exports = assertNoAssocUsageOutsideOfService;
|
|
@@ -18,22 +18,6 @@ function checkForHanaTypes( parent, name, type, path ) {
|
|
|
18
18
|
}
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
-
/**
|
|
22
|
-
* Check that `cds.UInt8` is not used - we don't have a clear idea how to represent it on postgres and h2
|
|
23
|
-
*
|
|
24
|
-
* @param {object} parent Object with a type
|
|
25
|
-
* @param {string} name Name of the type property on parent
|
|
26
|
-
* @param {Array} type type to check
|
|
27
|
-
* @param {CSN.Path} path
|
|
28
|
-
*/
|
|
29
|
-
function CheckForUInt8( parent, name, type, path ) {
|
|
30
|
-
const artifact = this.csn.definitions[path[1]];
|
|
31
|
-
if (artifact.kind === 'entity' && isPersistedOnDatabase(artifact) && parent.type === 'cds.UInt8') {
|
|
32
|
-
this.error('ref-unexpected-type', [ ...path, 'type' ], { type: 'cds.UInt8', value: this.options.sqlDialect },
|
|
33
|
-
'Type $(TYPE) can\'t be used with sqlDialect $(VALUE)');
|
|
34
|
-
}
|
|
35
|
-
}
|
|
36
|
-
|
|
37
21
|
/**
|
|
38
22
|
* Check types - specifically for postgres and h2
|
|
39
23
|
*
|
|
@@ -44,8 +28,6 @@ function CheckForUInt8( parent, name, type, path ) {
|
|
|
44
28
|
*/
|
|
45
29
|
function checkTypes( parent, name, type, path ) {
|
|
46
30
|
checkForHanaTypes.bind(this)(parent, name, type, path);
|
|
47
|
-
if (this.options.sqlDialect === 'postgres' || this.options.sqlDialect === 'h2')
|
|
48
|
-
CheckForUInt8.bind(this)(parent, name, type, path);
|
|
49
31
|
}
|
|
50
32
|
|
|
51
33
|
module.exports = {
|
|
@@ -56,8 +56,9 @@ function _checkPathsInStoredCalcElement( parent, value, csnPath ) {
|
|
|
56
56
|
else {
|
|
57
57
|
// It's a managed association - access of the foreign keys is allowed
|
|
58
58
|
requireForeignKeyAccess(parent, i, (errorIndex) => {
|
|
59
|
+
const variant = errorIndex >= value.length ? 'calc-missing' : 'calc-non-fk';
|
|
59
60
|
this.error('ref-unexpected-navigation', csnPath, {
|
|
60
|
-
'#':
|
|
61
|
+
'#': variant, id, elemref: parent, name: value[errorIndex]?.id || value[errorIndex],
|
|
61
62
|
});
|
|
62
63
|
hasPathError = true;
|
|
63
64
|
});
|
|
@@ -135,7 +135,7 @@ function validateOnCondition( member, memberName, property, path ) {
|
|
|
135
135
|
((type.target && type.keys || type.elements) && validStructuredElement ||
|
|
136
136
|
(type.target && validDollarSelf)) && !type.virtual
|
|
137
137
|
) {
|
|
138
|
-
// Do nothing - handled by
|
|
138
|
+
// Do nothing - handled by tuple expansion
|
|
139
139
|
}
|
|
140
140
|
else if (type.items && !type.virtual) {
|
|
141
141
|
this.error(null, onPath, { elemref: { ref } },
|
|
@@ -183,7 +183,7 @@ function requireForeignKeyAccess( parent, refIndex, noForeignKeyCallback ) {
|
|
|
183
183
|
ref.splice(refIndex + 1, 1, ...resolved);
|
|
184
184
|
}
|
|
185
185
|
|
|
186
|
-
const next = pathId(ref[refIndex + 1]);
|
|
186
|
+
const next = ref[refIndex + 1] && pathId(ref[refIndex + 1]);
|
|
187
187
|
let possibleKeys = next && assoc.keys.filter(r => r.ref[0] === next);
|
|
188
188
|
if (!possibleKeys || possibleKeys.length === 0) {
|
|
189
189
|
noForeignKeyCallback(refIndex + 1);
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { isPersistedOnDatabase,
|
|
3
|
+
const { isPersistedOnDatabase, applyTransformationsOnNonDictionary } = require('../model/csnUtils');
|
|
4
4
|
const { isBuiltinType } = require('../base/builtins');
|
|
5
5
|
const { requireForeignKeyAccess } = require('./onConditions');
|
|
6
6
|
const { pathId } = require('../model/csnRefs');
|
|
@@ -26,8 +26,13 @@ function checkQueryForNoDBArtifacts( query ) {
|
|
|
26
26
|
for (const prop of generalQueryProperties) {
|
|
27
27
|
const queryPart = (query.SELECT || query.SET)[prop];
|
|
28
28
|
if (Array.isArray(queryPart)) {
|
|
29
|
-
|
|
30
|
-
|
|
29
|
+
const that = this;
|
|
30
|
+
applyTransformationsOnNonDictionary((query.SELECT || query.SET), prop, {
|
|
31
|
+
ref: (parent, _name, val, csnPath, tokenStream, refIndex) => {
|
|
32
|
+
const danglingAssocAllowed = tokenStream[refIndex - 1] !== 'exists';
|
|
33
|
+
checkQueryRef.call(that, parent, danglingAssocAllowed);
|
|
34
|
+
},
|
|
35
|
+
}, { skipStandard: { on: true } });
|
|
31
36
|
}
|
|
32
37
|
else if (typeof queryPart === 'object') {
|
|
33
38
|
checkQueryRef.call(this, queryPart, prop === 'columns');
|
|
@@ -119,9 +124,10 @@ function _checkExpandInline( obj, previousRefs = [], previousLinks = [] ) {
|
|
|
119
124
|
* @param {CSN.Path} ref
|
|
120
125
|
* @param {object[]} _links
|
|
121
126
|
* @param {CSN.Path} $path
|
|
122
|
-
* @param {boolean}
|
|
127
|
+
* @param {boolean} danglingAssocAllowed usually optimised to foreign key hence allowed even if target is skipped,
|
|
128
|
+
* except in from or after exists
|
|
123
129
|
*/
|
|
124
|
-
function _checkRef( ref, _links, $path,
|
|
130
|
+
function _checkRef( ref, _links, $path, danglingAssocAllowed ) {
|
|
125
131
|
if (!ref || !_links )
|
|
126
132
|
return;
|
|
127
133
|
|
|
@@ -129,7 +135,7 @@ function _checkRef( ref, _links, $path, inColumns ) {
|
|
|
129
135
|
const isPublishedAssoc = this.csnUtils.isAssocOrComposition(_links[_links.length - 1].art);
|
|
130
136
|
|
|
131
137
|
// Don't check the last element - to allow association publishing in columns
|
|
132
|
-
for (let i = 0; i < (
|
|
138
|
+
for (let i = 0; i < (danglingAssocAllowed ? _links.length - 1 : _links.length); i++) {
|
|
133
139
|
const link = _links[i];
|
|
134
140
|
if (!link)
|
|
135
141
|
continue;
|
|
@@ -153,8 +159,8 @@ function _checkRef( ref, _links, $path, inColumns ) {
|
|
|
153
159
|
if (nonPersistedTarget) {
|
|
154
160
|
let isJoinRelevant = isPublishedAssoc || // publishing associations is always join relevant
|
|
155
161
|
isLast || // e.g. FROM targets are always join relevant.
|
|
156
|
-
isUnmanagedOrNoKeys
|
|
157
|
-
|
|
162
|
+
isUnmanagedOrNoKeys || // unmanaged associations are always join relevant -> no FKs
|
|
163
|
+
ref.slice(i).some(s => s.where || s.args); // function calls or filters are always join relevant
|
|
158
164
|
if (!isJoinRelevant) {
|
|
159
165
|
// for managed, published associations with more than one $path-step, only FK
|
|
160
166
|
// access is allowed.
|
|
@@ -164,17 +170,12 @@ function _checkRef( ref, _links, $path, inColumns ) {
|
|
|
164
170
|
}
|
|
165
171
|
|
|
166
172
|
if (isJoinRelevant) {
|
|
167
|
-
|
|
168
|
-
this.error( null, $path, {
|
|
169
|
-
'#': cdsPersistenceSkipped ? 'std' : 'abstract',
|
|
173
|
+
this.error('ref-invalid-assoc-navigation', $path, {
|
|
170
174
|
anno: '@cds.persistence.skip',
|
|
171
175
|
id: nonPersistedTarget.pathStep,
|
|
172
176
|
elemref: { ref },
|
|
173
177
|
name: nonPersistedTarget.name,
|
|
174
|
-
}
|
|
175
|
-
std: 'Unexpected $(ANNO) annotation on association target $(NAME) of $(ID) in path $(ELEMREF)',
|
|
176
|
-
abstract: 'Unexpected abstract association target $(NAME) of $(ID) in path $(ELEMREF)',
|
|
177
|
-
} );
|
|
178
|
+
});
|
|
178
179
|
break; // only one error per path
|
|
179
180
|
}
|
|
180
181
|
}
|
package/lib/checks/types.js
CHANGED
|
@@ -152,7 +152,7 @@ function checkTypeOfHasProperType( artOrElement, name, model, error, path, deriv
|
|
|
152
152
|
|
|
153
153
|
|
|
154
154
|
/**
|
|
155
|
-
* Can happen in CSN, e.g. `{ a: { kind: "type" } }`
|
|
155
|
+
* Can happen in CSN, e.g. `{ a: { kind: "type" } }` or via `elem;` in CDL.
|
|
156
156
|
*
|
|
157
157
|
* @param {Function} error the error function
|
|
158
158
|
* @param {CSN.Path} path the path to the element or the artifact
|
package/lib/checks/utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { isBuiltinType } = require('../base/builtins');
|
|
4
|
-
const { RelationalOperators } = require('../transform/
|
|
4
|
+
const { RelationalOperators } = require('../transform/tupleExpansion');
|
|
5
5
|
/**
|
|
6
6
|
* Prepare the ref steps so that they are loggable
|
|
7
7
|
*
|
|
@@ -17,7 +17,9 @@ function logReady( refStep ) {
|
|
|
17
17
|
* structured that can be used for tuple expansion. This can either be a
|
|
18
18
|
* real 'elements' thing or a managed association/composition with foreign keys.
|
|
19
19
|
*
|
|
20
|
-
*
|
|
20
|
+
* @TODO: This function also allows `is null` on the right-hand-side.
|
|
21
|
+
* We should move these checks to the actual tuple expansion, because
|
|
22
|
+
* if we're missing cases here, it currently results in incorrect expansion.
|
|
21
23
|
*
|
|
22
24
|
* @param {Array} on the on condition which to check
|
|
23
25
|
* @param {number} startIndex the index of the relational term in the on condition array
|
|
@@ -26,8 +28,7 @@ function logReady( refStep ) {
|
|
|
26
28
|
function otherSideIsExpandableStructure( on, startIndex ) {
|
|
27
29
|
if (on[startIndex - 1] && RelationalOperators.includes(on[startIndex - 1])) {
|
|
28
30
|
const lhs = on[startIndex - 2];
|
|
29
|
-
|
|
30
|
-
return /* lhs?.val !== undefined || */ isOk(resolveArtifactType.call(this, lhs?._art));
|
|
31
|
+
return isOk(resolveArtifactType.call(this, lhs?._art));
|
|
31
32
|
}
|
|
32
33
|
else if (on[startIndex + 1] && RelationalOperators.includes(on[startIndex + 1])) {
|
|
33
34
|
const op = on[startIndex + 1];
|
|
@@ -35,8 +36,7 @@ function otherSideIsExpandableStructure( on, startIndex ) {
|
|
|
35
36
|
if (op === 'is')
|
|
36
37
|
// check for unary operator 'is [not] null' as token stream
|
|
37
38
|
return rhs === 'null' || (rhs === 'not' && on[startIndex + 3] === 'null');
|
|
38
|
-
|
|
39
|
-
return /* rhs?.val !== undefined || */ isOk(resolveArtifactType.call(this, rhs?._art));
|
|
39
|
+
return isOk(resolveArtifactType.call(this, rhs?._art));
|
|
40
40
|
}
|
|
41
41
|
return false;
|
|
42
42
|
|
|
@@ -51,6 +51,29 @@ function otherSideIsExpandableStructure( on, startIndex ) {
|
|
|
51
51
|
}
|
|
52
52
|
}
|
|
53
53
|
|
|
54
|
+
/**
|
|
55
|
+
* Check that the opposite operand to a relational term is s value or "is null".
|
|
56
|
+
*
|
|
57
|
+
* @param {Array} expr the expression which to check
|
|
58
|
+
* @param {number} startIndex the index of the relational term in the expression array
|
|
59
|
+
* @returns {boolean} indicates whether the other side of a relational term is scalar
|
|
60
|
+
*/
|
|
61
|
+
function otherSideIsValue( expr, startIndex ) {
|
|
62
|
+
if (expr[startIndex - 1] && RelationalOperators.includes(expr[startIndex - 1]))
|
|
63
|
+
return expr[startIndex - 2]?.val !== undefined;
|
|
64
|
+
|
|
65
|
+
if (expr[startIndex + 1] && RelationalOperators.includes(expr[startIndex + 1])) {
|
|
66
|
+
const op = expr[startIndex + 1];
|
|
67
|
+
const rhs = expr[startIndex + 2];
|
|
68
|
+
if (op === 'is')
|
|
69
|
+
// check for unary operator 'is [not] null' as token stream
|
|
70
|
+
return rhs === 'null' || (rhs === 'not' && expr[startIndex + 3] === 'null');
|
|
71
|
+
return rhs?.val !== undefined;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return false;
|
|
75
|
+
}
|
|
76
|
+
|
|
54
77
|
/**
|
|
55
78
|
* Get the real type of an artifact
|
|
56
79
|
*
|
|
@@ -68,5 +91,6 @@ function resolveArtifactType( art ) {
|
|
|
68
91
|
module.exports = {
|
|
69
92
|
logReady,
|
|
70
93
|
otherSideIsExpandableStructure,
|
|
94
|
+
otherSideIsValue,
|
|
71
95
|
resolveArtifactType,
|
|
72
96
|
};
|
package/lib/checks/validator.js
CHANGED
|
@@ -42,7 +42,6 @@ const checkForInvalidTarget = require('./invalidTarget');
|
|
|
42
42
|
const { validateAssociationsInItems } = require('./arrayOfs');
|
|
43
43
|
const checkQueryForNoDBArtifacts = require('./queryNoDbArtifacts');
|
|
44
44
|
const checkExplicitlyNullableKeys = require('./nullableKeys');
|
|
45
|
-
const nonexpandableStructuredInExpression = require('./nonexpandableStructured');
|
|
46
45
|
const existsMustEndInAssoc = require('./existsMustEndInAssoc');
|
|
47
46
|
const forbidAssocInExists = require('./existsExpressionsOnlyForeignKeys');
|
|
48
47
|
const checkPathsInStoredCalcElement = require('./checkPathsInStoredCalcElement');
|
|
@@ -87,7 +86,6 @@ const forRelationalDBCsnValidators = [
|
|
|
87
86
|
checkCdsMap,
|
|
88
87
|
existsMustEndInAssoc,
|
|
89
88
|
forbidAssocInExists,
|
|
90
|
-
nonexpandableStructuredInExpression,
|
|
91
89
|
navigationIntoMany,
|
|
92
90
|
checkPathsInStoredCalcElement,
|
|
93
91
|
featureFlags,
|
|
@@ -124,7 +122,7 @@ const forOdataArtifactValidators
|
|
|
124
122
|
checkReadOnlyAndInsertOnly,
|
|
125
123
|
];
|
|
126
124
|
|
|
127
|
-
const forOdataCsnValidators = [ checkCdsMap
|
|
125
|
+
const forOdataCsnValidators = [ checkCdsMap ];
|
|
128
126
|
|
|
129
127
|
const forOdataQueryValidators = [];
|
|
130
128
|
|
|
@@ -202,8 +200,6 @@ function getDBCsnValidators( options ) {
|
|
|
202
200
|
validations.push(checkForParams.csnValidator);
|
|
203
201
|
if (options.sqlDialect === 'h2' || options.sqlDialect === 'postgres')
|
|
204
202
|
validations.push(checkForHanaTypes);
|
|
205
|
-
if (options.transformation === 'effective' && options.effectiveServiceName)
|
|
206
|
-
validations.push(assertNoAssocUsageOutsideOfService);
|
|
207
203
|
|
|
208
204
|
return validations;
|
|
209
205
|
}
|
|
@@ -232,6 +228,9 @@ function forRelationalDB( csn, that ) {
|
|
|
232
228
|
},
|
|
233
229
|
(artifact, artifactName) => {
|
|
234
230
|
if (that.options.transformation === 'effective') {
|
|
231
|
+
if (that.options.effectiveServiceName)
|
|
232
|
+
assertNoAssocUsageOutsideOfService.bind(that)(artifact, artifactName);
|
|
233
|
+
|
|
235
234
|
forEachMemberRecursively(artifact, checkAnnotationExpression.bind(that), [ 'definitions', artifactName ], false, {
|
|
236
235
|
skipArtifact: a => a.returns || (a.params && !a.query),
|
|
237
236
|
});
|