@sap/cds-compiler 5.9.4 → 6.0.12
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 +117 -319
- package/README.md +1 -1
- package/bin/cds_update_identifiers.js +3 -5
- package/bin/cdsc.js +24 -9
- package/bin/cdshi.js +1 -1
- package/bin/cdsse.js +4 -4
- package/doc/CHANGELOG_BETA.md +11 -0
- package/doc/CHANGELOG_DEPRECATED.md +29 -0
- package/lib/api/main.js +8 -5
- package/lib/api/options.js +12 -10
- package/lib/base/builtins.js +1 -0
- package/lib/base/message-registry.js +191 -99
- package/lib/base/messages.js +35 -21
- package/lib/base/model.js +14 -24
- package/lib/checks/actionsFunctions.js +10 -20
- package/lib/checks/annotationsOData.js +1 -1
- package/lib/checks/elements.js +35 -10
- package/lib/checks/enums.js +31 -0
- package/lib/checks/foreignKeys.js +2 -2
- package/lib/checks/hasPersistedElements.js +5 -0
- package/lib/checks/invalidTarget.js +1 -1
- package/lib/checks/managedWithoutKeys.js +5 -4
- package/lib/checks/queryNoDbArtifacts.js +10 -8
- package/lib/checks/types.js +5 -5
- package/lib/checks/validator.js +6 -4
- package/lib/compiler/assert-consistency.js +13 -9
- package/lib/compiler/checks.js +20 -52
- package/lib/compiler/define.js +31 -6
- package/lib/compiler/extend.js +5 -1
- package/lib/compiler/generate.js +14 -17
- package/lib/compiler/populate.js +8 -31
- package/lib/compiler/propagator.js +21 -35
- package/lib/compiler/resolve.js +64 -29
- package/lib/compiler/shared.js +16 -4
- package/lib/compiler/tweak-assocs.js +1 -1
- package/lib/compiler/utils.js +1 -1
- package/lib/edm/annotations/edmJson.js +23 -20
- package/lib/edm/annotations/genericTranslation.js +12 -10
- package/lib/edm/csn2edm.js +50 -56
- package/lib/edm/edm.js +33 -28
- package/lib/edm/edmInboundChecks.js +2 -2
- package/lib/edm/edmPreprocessor.js +54 -88
- package/lib/edm/edmUtils.js +9 -12
- package/lib/gen/BaseParser.js +63 -52
- package/lib/gen/CdlGrammar.checksum +1 -1
- package/lib/gen/CdlParser.js +1153 -1165
- package/lib/gen/Dictionary.json +21 -1
- package/lib/json/from-csn.js +70 -43
- package/lib/json/to-csn.js +6 -8
- package/lib/language/multiLineStringParser.js +3 -2
- package/lib/main.d.ts +58 -24
- package/lib/model/cloneCsn.js +3 -0
- package/lib/model/csnUtils.js +28 -39
- package/lib/model/xprAsTree.js +23 -9
- package/lib/modelCompare/compare.js +5 -4
- package/lib/optionProcessor.js +24 -17
- package/lib/parsers/AstBuildingParser.js +81 -25
- package/lib/parsers/XprTree.js +57 -3
- package/lib/parsers/identifiers.js +1 -1
- package/lib/parsers/index.js +0 -3
- package/lib/render/manageConstraints.js +25 -25
- package/lib/render/toCdl.js +173 -170
- package/lib/render/toHdbcds.js +126 -128
- package/lib/render/toRename.js +7 -7
- package/lib/render/toSql.js +128 -125
- package/lib/render/utils/common.js +47 -22
- package/lib/render/utils/delta.js +25 -25
- package/lib/render/utils/operators.js +2 -2
- package/lib/render/utils/pretty.js +5 -5
- package/lib/render/utils/sql.js +13 -13
- package/lib/render/utils/standardDatabaseFunctions.js +115 -103
- package/lib/render/utils/unique.js +4 -4
- package/lib/transform/db/applyTransformations.js +1 -1
- package/lib/transform/db/assertUnique.js +2 -2
- package/lib/transform/db/associations.js +6 -7
- package/lib/transform/db/assocsToQueries/utils.js +4 -5
- package/lib/transform/db/backlinks.js +12 -9
- package/lib/transform/db/cdsPersistence.js +8 -7
- package/lib/transform/db/constraints.js +13 -10
- package/lib/transform/db/expansion.js +7 -3
- package/lib/transform/db/flattening.js +4 -14
- package/lib/transform/db/processSqlServices.js +2 -1
- package/lib/transform/db/temporal.js +5 -7
- package/lib/transform/db/views.js +2 -4
- package/lib/transform/draft/db.js +8 -8
- package/lib/transform/draft/odata.js +10 -7
- package/lib/transform/forOdata.js +10 -5
- package/lib/transform/forRelationalDB.js +5 -75
- package/lib/transform/localized.js +1 -1
- package/lib/transform/odata/createForeignKeys.js +11 -10
- package/lib/transform/odata/flattening.js +8 -4
- package/lib/transform/odata/foreignKeyRefsInXprAnnos.js +96 -0
- package/lib/transform/odata/typesExposure.js +3 -3
- package/lib/transform/transformUtils.js +4 -8
- package/lib/transform/translateAssocsToJoins.js +14 -7
- package/lib/transform/universalCsn/universalCsnEnricher.js +10 -4
- package/lib/utils/objectUtils.js +0 -17
- package/package.json +10 -13
- package/share/messages/def-upcoming-virtual-change.md +1 -1
- package/LICENSE +0 -37
- package/bin/cds_remove_invalid_whitespace.js +0 -138
- package/doc/CHANGELOG_ARCHIVE.md +0 -3604
- package/lib/gen/genericAntlrParser.js +0 -3
- package/lib/gen/language.checksum +0 -1
- package/lib/gen/language.interp +0 -456
- package/lib/gen/language.tokens +0 -180
- package/lib/gen/languageLexer.interp +0 -439
- package/lib/gen/languageLexer.js +0 -1483
- package/lib/gen/languageLexer.tokens +0 -167
- package/lib/gen/languageParser.js +0 -24941
- package/lib/language/antlrParser.js +0 -205
- package/lib/language/errorStrategy.js +0 -646
- package/lib/language/genericAntlrParser.js +0 -1572
- package/lib/parsers/CdlGrammar.g4 +0 -2070
package/lib/base/messages.js
CHANGED
|
@@ -6,8 +6,13 @@
|
|
|
6
6
|
|
|
7
7
|
const { term } = require('../utils/term');
|
|
8
8
|
const { Location, locationString } = require('./location');
|
|
9
|
-
const {
|
|
10
|
-
const {
|
|
9
|
+
const { isBetaEnabled } = require('./model');
|
|
10
|
+
const {
|
|
11
|
+
centralMessages,
|
|
12
|
+
configurableForValidValues,
|
|
13
|
+
centralMessageTexts,
|
|
14
|
+
oldMessageIds,
|
|
15
|
+
} = require('./message-registry');
|
|
11
16
|
const _messageIdsWithExplanation = require('../../share/messages/message-explanations.json').messages;
|
|
12
17
|
const { analyseCsnPath, traverseQuery } = require('../model/csnRefs');
|
|
13
18
|
const { CompilerAssertion } = require('./error');
|
|
@@ -54,8 +59,10 @@ function hasNonDowngradableErrors( messages, moduleName, options ) {
|
|
|
54
59
|
|
|
55
60
|
/**
|
|
56
61
|
* Returns true if the given message id exist in the central message register and is
|
|
57
|
-
* downgradable
|
|
58
|
-
*
|
|
62
|
+
* downgradable
|
|
63
|
+
* - Non-errors are not downgradable if the moduleName is listed in the
|
|
64
|
+
* `errorFor` property
|
|
65
|
+
* - Errors might be downgradable according to the `configurableFor` property,
|
|
59
66
|
*
|
|
60
67
|
* @param {string} messageId
|
|
61
68
|
* @param {string} moduleName
|
|
@@ -70,17 +77,12 @@ function isDowngradable( messageId, moduleName, options ) {
|
|
|
70
77
|
|
|
71
78
|
// errorFor has the highest priority. If the message is an error for
|
|
72
79
|
// the module, it is NEVER downgradable.
|
|
73
|
-
if (msg.errorFor && msg.errorFor.includes(moduleName))
|
|
74
|
-
return false;
|
|
75
80
|
if (msg.severity !== 'Error')
|
|
76
|
-
return
|
|
77
|
-
// v6 messages are downgradable (except if errorFor also contains the current module).
|
|
78
|
-
if (msg.errorFor && msg.errorFor.includes('v6'))
|
|
79
|
-
return true;
|
|
81
|
+
return !msg.errorFor?.includes( moduleName );
|
|
80
82
|
const { configurableFor } = msg;
|
|
81
83
|
return (Array.isArray( configurableFor ))
|
|
82
84
|
? configurableFor.includes( moduleName )
|
|
83
|
-
: configurableFor &&
|
|
85
|
+
: configurableFor && configurableForValidValues[configurableFor]?.( options );
|
|
84
86
|
}
|
|
85
87
|
|
|
86
88
|
/**
|
|
@@ -90,14 +92,16 @@ function isDowngradable( messageId, moduleName, options ) {
|
|
|
90
92
|
* @returns {string}
|
|
91
93
|
*/
|
|
92
94
|
function severityChangeMarker( msg, config ) {
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
isDowngradable( msg.messageId, config.moduleForMarker,
|
|
95
|
+
if (config.moduleForMarker && msg.messageId) {
|
|
96
|
+
if (!msg.severity || msg.severity === 'Error') {
|
|
97
|
+
if (isDowngradable( msg.messageId, config.moduleForMarker,
|
|
97
98
|
{ deprecated: { downgradableErrors: true } }))
|
|
98
|
-
|
|
99
|
-
|
|
99
|
+
// Do not include testMode here, no marker for configurableFor: 'test'
|
|
100
|
+
return '‹↓›';
|
|
101
|
+
}
|
|
102
|
+
else if (centralMessages[msg.messageId]?.errorFor?.includes( 'v7' )) {
|
|
100
103
|
return '‹↑›';
|
|
104
|
+
}
|
|
101
105
|
}
|
|
102
106
|
return '';
|
|
103
107
|
}
|
|
@@ -246,7 +250,7 @@ function reclassifiedSeverity( msg, options, moduleName ) {
|
|
|
246
250
|
if (errorFor.includes(moduleName))
|
|
247
251
|
return 'Error';
|
|
248
252
|
|
|
249
|
-
if (errorFor.includes('
|
|
253
|
+
if (errorFor.includes('v7') && isBetaEnabled(options, 'v7preview')) {
|
|
250
254
|
severity = 'Error';
|
|
251
255
|
if (!isDowngradable(msg.messageId, moduleName, options))
|
|
252
256
|
return severity;
|
|
@@ -755,7 +759,7 @@ const quote = { // could be an option in the future
|
|
|
755
759
|
single: p => `‘${ p }’`, // for other things cited from or expected in the model
|
|
756
760
|
angle: p => `‹${ p }›`, // for tokens like ‹Identifier›, and similar
|
|
757
761
|
direct: p => p, // e.g. for numbers _not cited from or expected in_ the source
|
|
758
|
-
upper: p => p.toUpperCase(),
|
|
762
|
+
upper: p => p.toUpperCase(),
|
|
759
763
|
};
|
|
760
764
|
|
|
761
765
|
const paramsTransform = {
|
|
@@ -774,6 +778,7 @@ const paramsTransform = {
|
|
|
774
778
|
subprop: quote.single,
|
|
775
779
|
otherprop: quote.single,
|
|
776
780
|
code: quote.single,
|
|
781
|
+
enum: sym => quote.single( `#${ sym }`),
|
|
777
782
|
newcode: quote.single,
|
|
778
783
|
kind: quote.single,
|
|
779
784
|
meta: quote.angle,
|
|
@@ -1489,12 +1494,17 @@ function homeName( art, absoluteOnly ) {
|
|
|
1489
1494
|
return art;
|
|
1490
1495
|
if (art._user) // when providing a path item with filter as “user”
|
|
1491
1496
|
return homeName( art._user, absoluteOnly );
|
|
1492
|
-
if (art._outer) {
|
|
1497
|
+
if (art._outer) { // in items property, or annotation with path
|
|
1498
|
+
if (art._outer?.kind === '$annotation')
|
|
1499
|
+
art = art._outer;
|
|
1493
1500
|
const outer = homeName( art._outer, absoluteOnly );
|
|
1494
1501
|
if (art.kind === '$annotation') // eslint-disable-next-line sonarjs/no-nested-template-literals
|
|
1495
1502
|
return `${ outer }/${ quoted( `@${ art.name.id }` ) }`;
|
|
1496
1503
|
return outer;
|
|
1497
1504
|
}
|
|
1505
|
+
else if (art.kind === '$annotation') { // in items property, or annotation with path
|
|
1506
|
+
return quoted( `@${ art.name.id }` );
|
|
1507
|
+
}
|
|
1498
1508
|
else if (art.kind === 'source' || !art.name) { // error reported in parser or on source level
|
|
1499
1509
|
return null;
|
|
1500
1510
|
}
|
|
@@ -1899,6 +1909,8 @@ function sanitizeCsnPath( csnPath ) {
|
|
|
1899
1909
|
*/
|
|
1900
1910
|
function explainMessage( messageId ) {
|
|
1901
1911
|
messageId = oldMessageIds[messageId] || messageId;
|
|
1912
|
+
if (Array.isArray(messageId))
|
|
1913
|
+
messageId = messageId[0]; // take first just in case
|
|
1902
1914
|
const filename = path.join(__dirname, '..', '..', 'share', 'messages', `${ messageId }.md`);
|
|
1903
1915
|
return fs.readFileSync(filename, 'utf8');
|
|
1904
1916
|
}
|
|
@@ -1912,7 +1924,9 @@ function explainMessage( messageId ) {
|
|
|
1912
1924
|
* @returns {boolean}
|
|
1913
1925
|
*/
|
|
1914
1926
|
function hasMessageExplanation( messageId ) {
|
|
1915
|
-
|
|
1927
|
+
let id = oldMessageIds[messageId] || messageId || false;
|
|
1928
|
+
if (Array.isArray(id))
|
|
1929
|
+
id = id[0];
|
|
1916
1930
|
return id && _messageIdsWithExplanation.includes(id);
|
|
1917
1931
|
}
|
|
1918
1932
|
|
package/lib/base/model.js
CHANGED
|
@@ -25,7 +25,7 @@ const availableBetaFlags = {
|
|
|
25
25
|
tenantVariable: true,
|
|
26
26
|
calcAssoc: true,
|
|
27
27
|
temporalRawProjection: true,
|
|
28
|
-
|
|
28
|
+
v7preview: true,
|
|
29
29
|
draftMessages: true,
|
|
30
30
|
rewriteAnnotationExpressionsViaType: true,
|
|
31
31
|
sqlServiceDummies: true,
|
|
@@ -36,31 +36,21 @@ const availableBetaFlags = {
|
|
|
36
36
|
// Used by isDeprecatedEnabled() to check if any flag ist set.
|
|
37
37
|
const availableDeprecatedFlags = {
|
|
38
38
|
// the old ones starting with _, : false
|
|
39
|
+
noPersistenceJournalForGeneratedEntities: true, // since v6
|
|
39
40
|
downgradableErrors: true,
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
41
|
+
noCompositionIncludes: true, // since v6; was an option with inverted meaning in v5
|
|
42
|
+
noQuasiVirtualAssocs: true, // since v6
|
|
43
|
+
_includesNonShadowedFirst: true,
|
|
44
|
+
_eagerPersistenceForGeneratedEntities: true,
|
|
45
|
+
_noKeyPropagationWithExpansions: true,
|
|
43
46
|
ignoreSpecifiedQueryElements: true,
|
|
44
47
|
};
|
|
45
48
|
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
'
|
|
50
|
-
'
|
|
51
|
-
'noElementsExpansion',
|
|
52
|
-
'noInheritedAutoexposeViaComposition',
|
|
53
|
-
'noScopedRedirections',
|
|
54
|
-
'oldVirtualNotNullPropagation',
|
|
55
|
-
'parensAsStrings',
|
|
56
|
-
'projectionAsQuery',
|
|
57
|
-
'redirectInSubQueries',
|
|
58
|
-
'renderVirtualElements',
|
|
59
|
-
'shortAutoexposed',
|
|
60
|
-
'unmanagedUpInComponent',
|
|
61
|
-
'v1KeysForTemporal',
|
|
62
|
-
// do not add old deprecated flags which should not lead to an error:
|
|
63
|
-
// autoCorrectOrderBySourceRefs - just info would be ok
|
|
49
|
+
// Deprecated flags that were removed in v5.
|
|
50
|
+
const oldDeprecatedFlagsV5 = [
|
|
51
|
+
'includesNonShadowedFirst',
|
|
52
|
+
'eagerPersistenceForGeneratedEntities',
|
|
53
|
+
'noKeyPropagationWithExpansions',
|
|
64
54
|
];
|
|
65
55
|
|
|
66
56
|
/**
|
|
@@ -120,9 +110,9 @@ function checkRemovedDeprecatedFlags( options, { error } ) {
|
|
|
120
110
|
return;
|
|
121
111
|
|
|
122
112
|
forEach(options.deprecated, (key, val) => {
|
|
123
|
-
if (val &&
|
|
113
|
+
if (val && oldDeprecatedFlagsV5.includes(key)) {
|
|
124
114
|
error('api-invalid-deprecated', null, { name: key },
|
|
125
|
-
'Deprecated flag $(NAME) has been removed in CDS compiler
|
|
115
|
+
'Deprecated flag $(NAME) has been removed in CDS compiler v6');
|
|
126
116
|
}
|
|
127
117
|
});
|
|
128
118
|
}
|
|
@@ -68,13 +68,8 @@ function checkActionOrFunction( art, artName, prop, path ) {
|
|
|
68
68
|
if (!paramType)
|
|
69
69
|
return; // no type could be resolved
|
|
70
70
|
|
|
71
|
-
if (param.type && this.csnUtils.isAssocOrComposition(param))
|
|
72
|
-
this.error(
|
|
73
|
-
std: 'An association is not allowed as this artifact\'s parameter type', // Not used
|
|
74
|
-
action: 'An association is not allowed as action\'s parameter type',
|
|
75
|
-
function: 'An association is not allowed as function\'s parameter type',
|
|
76
|
-
});
|
|
77
|
-
}
|
|
71
|
+
if (param.type && this.csnUtils.isAssocOrComposition(param))
|
|
72
|
+
this.error('ref-unexpected-assoc-type', currPath, { '#': `${ actKind }-param` });
|
|
78
73
|
|
|
79
74
|
if (paramType.items?.type)
|
|
80
75
|
checkActionOrFunctionParameter.call(this, paramType.items, currPath.concat('items'), actKind);
|
|
@@ -95,14 +90,8 @@ function checkActionOrFunction( art, artName, prop, path ) {
|
|
|
95
90
|
if (!finalReturnType)
|
|
96
91
|
return; // no type, e.g. `type of V:calculated`; already an error in `checkTypeOfHasProperType()`
|
|
97
92
|
|
|
98
|
-
if (this.csnUtils.isAssocOrComposition(returns))
|
|
99
|
-
this.error(
|
|
100
|
-
{
|
|
101
|
-
std: 'An association is not allowed as this artifact\'s return type', // Not used
|
|
102
|
-
action: 'An association is not allowed as action\'s return type',
|
|
103
|
-
function: 'An association is not allowed as function\'s return type',
|
|
104
|
-
});
|
|
105
|
-
}
|
|
93
|
+
if (this.csnUtils.isAssocOrComposition(returns))
|
|
94
|
+
this.error('ref-unexpected-assoc-type', currPath, { '#': `${ actKind }-returns` });
|
|
106
95
|
|
|
107
96
|
if (finalReturnType.items) // check array return type
|
|
108
97
|
checkReturns.call(this, finalReturnType.items, currPath.concat('items'), actKind);
|
|
@@ -118,14 +107,15 @@ function checkActionOrFunction( art, artName, prop, path ) {
|
|
|
118
107
|
* @param {CSN.Path} currPath The current path
|
|
119
108
|
*/
|
|
120
109
|
function checkUserDefinedType( type, typeName, currPath ) {
|
|
121
|
-
|
|
122
|
-
if (!isBuiltinType(type.type) && type.kind && type.kind !== 'type') {
|
|
110
|
+
if (type.kind && type.kind !== 'type' && type.type && !isBuiltinType(this.csnUtils.getFinalTypeInfo(type.type)?.type)) {
|
|
123
111
|
const serviceOfType = this.csnUtils.getServiceName(typeName);
|
|
124
112
|
if (serviceName && serviceName !== serviceOfType) {
|
|
125
113
|
// if (!(isMultiSchema && serviceOfType)) {
|
|
126
|
-
this.error(
|
|
127
|
-
|
|
128
|
-
|
|
114
|
+
this.error('ref-expected-service-type', currPath, { type: typeName, kind: type.kind, service: serviceName }, {
|
|
115
|
+
std: 'Referenced $(KIND) $(TYPE) can\'t be used in service $(SERVICE) because it is defined outside the service',
|
|
116
|
+
entity: 'Referenced entity $(TYPE) can\'t be used in service $(SERVICE) because it is defined outside the service',
|
|
117
|
+
type: 'Referenced type $(TYPE) can\'t be used in service $(SERVICE) because it is defined outside the service',
|
|
118
|
+
});
|
|
129
119
|
// }
|
|
130
120
|
}
|
|
131
121
|
}
|
|
@@ -108,7 +108,7 @@ function checkTemporalAnnotationsAssignment( artifact, artifactName ) {
|
|
|
108
108
|
* @param {CSN.Path} path
|
|
109
109
|
*/
|
|
110
110
|
function checkForAnnoAssignmentAndApplicability( annoIdentifier, member, path ) {
|
|
111
|
-
if (
|
|
111
|
+
if (member[`@cds.valid.${ annoIdentifier }`]) {
|
|
112
112
|
valid[annoIdentifier].push(path);
|
|
113
113
|
// check whether annotation is not assigned to not allowed element type, these are: association, structured elements, leaf element of a structure
|
|
114
114
|
if (this.csnUtils.isAssocOrComposition(member) || this.csnUtils.isStructured(member) || path.length > 5)
|
package/lib/checks/elements.js
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
|
-
forEachMember,
|
|
4
|
+
forEachMember,
|
|
5
|
+
forEachMemberRecursively,
|
|
6
|
+
cardinality2str,
|
|
7
|
+
hasPersistenceSkipAnnotation,
|
|
5
8
|
} = require('../model/csnUtils');
|
|
6
9
|
const { isBuiltinType } = require('../base/builtins');
|
|
7
10
|
const { isGeoTypeName } = require('../compiler/builtins');
|
|
@@ -41,9 +44,8 @@ function checkPrimaryKey( art ) {
|
|
|
41
44
|
if (member.key || parentIsKey) {
|
|
42
45
|
const finalBaseType = this.csnUtils.getFinalTypeInfo(member.type);
|
|
43
46
|
if (isGeoTypeName(finalBaseType?.type)) {
|
|
44
|
-
this.error(
|
|
45
|
-
{ type: finalBaseType.type, name: elemFqName }
|
|
46
|
-
'Type $(TYPE) can\'t be used as primary key in element $(NAME)');
|
|
47
|
+
this.error('ref-unsupported-type', parentPath || member.$path,
|
|
48
|
+
{ '#': 'key', type: finalBaseType.type, name: elemFqName });
|
|
47
49
|
}
|
|
48
50
|
else if (finalBaseType && this.csnUtils.isStructured(finalBaseType) && !finalBaseType.$visited) {
|
|
49
51
|
setProp(finalBaseType, '$visited', true);
|
|
@@ -105,7 +107,7 @@ function checkPrimaryKey( art ) {
|
|
|
105
107
|
function checkVirtualElement( member ) {
|
|
106
108
|
if (member.virtual) {
|
|
107
109
|
if (this.csnUtils.isAssocOrComposition(member))
|
|
108
|
-
this.error(
|
|
110
|
+
this.error('def-unexpected-virtual', member.$path, { keyword: 'virtual' });
|
|
109
111
|
}
|
|
110
112
|
}
|
|
111
113
|
|
|
@@ -119,12 +121,36 @@ function checkVirtualElement( member ) {
|
|
|
119
121
|
function checkManagedAssoc( member ) {
|
|
120
122
|
if (!member.target || isManagedComposition.bind(this)(member))
|
|
121
123
|
return;
|
|
124
|
+
|
|
125
|
+
const targetMax = member.cardinality?.max ?? 1;
|
|
126
|
+
if (targetMax === 1 || member.on)
|
|
127
|
+
return;
|
|
128
|
+
|
|
129
|
+
// Special case for "--with-mocks" of the cds cli: For testing databases such as H2 and SQLite, we allow
|
|
130
|
+
// associations with neither on-condition nor foreign keys if the CSN is mocked.
|
|
131
|
+
// See #13916 for details.
|
|
132
|
+
const allowForMocked = this.csn._mocked && (this.options.sqlDialect === 'h2' || this.options.sqlDialect === 'sqlite');
|
|
133
|
+
|
|
134
|
+
const isPersisted = !hasPersistenceSkipAnnotation(this.artifact) && !this.artifact['@cds.persistence.exists'];
|
|
135
|
+
if (isPersisted && !allowForMocked && !member.keys && (targetMax === '*' || Number(targetMax) > 1) && this.options.transformation === 'sql') {
|
|
136
|
+
// Since cds-compiler v6, managed to-many no longer get 'keys'.
|
|
137
|
+
// As this would lead to DROP COLUMNs, emit an error instead.
|
|
138
|
+
this.error('type-missing-on-condition', member.cardinality?.$path || member.$path, {
|
|
139
|
+
value: cardinality2str(member, false),
|
|
140
|
+
'#': this.csnUtils.isComposition(member) ? 'comp' : 'std',
|
|
141
|
+
}, {
|
|
142
|
+
// same as 'to-many-no-on', but as error
|
|
143
|
+
std: 'Expected association with target cardinality $(VALUE) to have an ON-condition',
|
|
144
|
+
comp: 'Expected composition with target cardinality $(VALUE) to have an ON-condition',
|
|
145
|
+
});
|
|
146
|
+
}
|
|
147
|
+
|
|
122
148
|
// Implementation note: Imported services (i.e. external ones) may contain to-many associations
|
|
123
149
|
// with an empty foreign key list. If the user (in this case importer) explicitly sets an empty
|
|
124
150
|
// foreign key array, we won't emit a warning to avoid spamming the user.
|
|
125
|
-
|
|
126
|
-
if (max === 1 || member.on || !member.keys || member.keys.length === 0)
|
|
151
|
+
if (!member.keys || member.keys.length === 0)
|
|
127
152
|
return;
|
|
153
|
+
|
|
128
154
|
// We use the fact that `key` is only supported top-level (warning otherwise).
|
|
129
155
|
// And if an element of a structured _type_ is "key", we get a warning for the key.
|
|
130
156
|
// However, we may get false negatives for our warning, which is acceptable, e.g. for
|
|
@@ -135,8 +161,7 @@ function checkManagedAssoc( member ) {
|
|
|
135
161
|
if (!coversAllTargetKeys.call(this, foreignKeys, targetKeys))
|
|
136
162
|
return; // foreign key does not cover at least one target key -> can be to-many
|
|
137
163
|
|
|
138
|
-
|
|
139
|
-
this.warning(isNoDb ? 'to-many-no-on-noDB' : 'to-many-no-on', member.cardinality?.$path || member.$path, {
|
|
164
|
+
this.warning(!isPersisted ? 'to-many-no-on-noDB' : 'to-many-no-on', member.cardinality?.$path || member.$path, {
|
|
140
165
|
value: cardinality2str(member, false),
|
|
141
166
|
'#': this.csnUtils.isComposition(member) ? 'comp' : 'std',
|
|
142
167
|
}, {
|
|
@@ -256,7 +281,7 @@ function checkRecursiveTypeUsage( art ) {
|
|
|
256
281
|
if (!isDeref && this.options.odataVersion === 'v4' && this.options.odataFormat === 'structured')
|
|
257
282
|
return;
|
|
258
283
|
if (!def.$recErr) {
|
|
259
|
-
this.error(
|
|
284
|
+
this.error('ref-cyclic', loc, { '#': 'type', type: type ?? prevType });
|
|
260
285
|
setProp(def, '$recErr', true);
|
|
261
286
|
}
|
|
262
287
|
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { ModelError } = require('../base/error');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Removes the `enum` property - the compiler already resolved them.
|
|
7
|
+
*
|
|
8
|
+
* @param {Object} parent
|
|
9
|
+
*/
|
|
10
|
+
function removeEnum( parent ) {
|
|
11
|
+
delete parent.enum;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Check if '#' has a `val` property. If val is undefined, an error is logged as this means that the compiler could not resolve the reference.
|
|
16
|
+
*
|
|
17
|
+
* If the reference was resolved, remove # and keep the .val.
|
|
18
|
+
*
|
|
19
|
+
* @param {Object} parent
|
|
20
|
+
*/
|
|
21
|
+
function checkAndRemoveHash( parent ) {
|
|
22
|
+
if (parent.val === undefined)
|
|
23
|
+
throw new ModelError('Expected value to be resolved by the compiler - throwing exception to trigger recompilation.');
|
|
24
|
+
|
|
25
|
+
delete parent['#'];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
module.exports = {
|
|
29
|
+
enum: removeEnum,
|
|
30
|
+
'#': checkAndRemoveHash,
|
|
31
|
+
};
|
|
@@ -42,7 +42,7 @@ function validateForeignKeys( member, memberName ) {
|
|
|
42
42
|
// Declared as arrow-function to keep scope the same (this value)
|
|
43
43
|
const checkForItemsOrMissingType = (mem, memName) => {
|
|
44
44
|
if (mem.items) {
|
|
45
|
-
this.error(
|
|
45
|
+
this.error('type-invalid-foreign-key', member.$path, { '#': 'items' });
|
|
46
46
|
}
|
|
47
47
|
else if (mem.keys) {
|
|
48
48
|
handleAssociation(mem);
|
|
@@ -52,7 +52,7 @@ function validateForeignKeys( member, memberName ) {
|
|
|
52
52
|
}
|
|
53
53
|
else if (mem.type) {
|
|
54
54
|
if (mem.type === 'cds.Map') {
|
|
55
|
-
this.error(
|
|
55
|
+
this.error('type-invalid-foreign-key', member.$path, { '#': 'std', type: mem.type } );
|
|
56
56
|
}
|
|
57
57
|
else {
|
|
58
58
|
const type = mem.type.ref
|
|
@@ -35,6 +35,11 @@ function hasRealElements( elements ) {
|
|
|
35
35
|
if (hasRealElements(element.elements))
|
|
36
36
|
return true;
|
|
37
37
|
}
|
|
38
|
+
else if (element.target) {
|
|
39
|
+
if (element.keys?.length > 0)
|
|
40
|
+
return true;
|
|
41
|
+
// else: either unmanaged or no keys
|
|
42
|
+
}
|
|
38
43
|
else {
|
|
39
44
|
return true;
|
|
40
45
|
}
|
|
@@ -24,7 +24,7 @@ function invalidTarget( member ) {
|
|
|
24
24
|
const checkForInvalidTarget = (mem) => {
|
|
25
25
|
if (mem.target) {
|
|
26
26
|
const target = this.csn.definitions[mem.target];
|
|
27
|
-
if (!target)
|
|
27
|
+
if (!target) // `[object Object]` → anonymous target aspect
|
|
28
28
|
throw new ModelError(`Expected target ${ mem.target }`);
|
|
29
29
|
if (target.kind !== 'entity') {
|
|
30
30
|
const isAssoc = this.csnUtils.getFinalTypeInfo(member.type)?.type !== 'cds.Composition';
|
|
@@ -1,18 +1,19 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
|
|
4
3
|
const { ModelError } = require('../base/error');
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
|
-
* Trigger a recompilation in case of an association without .keys and without .on
|
|
6
|
+
* Trigger a recompilation in case of an to-one association without .keys and without .on
|
|
8
7
|
*
|
|
9
8
|
* @param {CSN.Element} member the element to be checked
|
|
10
9
|
* @param {string} memberName the elements name
|
|
11
10
|
* @param {string} prop which kind of member are we looking at -> only prop "elements"
|
|
12
11
|
*/
|
|
13
12
|
function managedWithoutKeys( member, memberName, prop ) {
|
|
14
|
-
if (prop === 'elements' && member.target && !member.keys && !member.on) {
|
|
15
|
-
|
|
13
|
+
if (prop === 'elements' && member.target && !member.keys && !member.on) {
|
|
14
|
+
const targetMax = member.cardinality?.max;
|
|
15
|
+
if (!targetMax || targetMax === 1)
|
|
16
|
+
throw new ModelError('Expected association to have either an on-condition or foreign keys.');
|
|
16
17
|
}
|
|
17
18
|
}
|
|
18
19
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const {
|
|
3
|
+
const { isPersistedOnDatabase, hasPersistenceSkipAnnotation } = require('../model/csnUtils');
|
|
4
4
|
const { isBuiltinType } = require('../base/builtins');
|
|
5
5
|
const { requireForeignKeyAccess } = require('./onConditions');
|
|
6
6
|
const { pathId } = require('../model/csnRefs');
|
|
@@ -22,7 +22,7 @@ const generalQueryProperties = [ 'from', 'columns', 'where', 'groupBy', 'orderBy
|
|
|
22
22
|
* @param {CSN.Query} query Query to check
|
|
23
23
|
*/
|
|
24
24
|
function checkQueryForNoDBArtifacts( query ) {
|
|
25
|
-
if (isPersistedOnDatabase(this.artifact) && !
|
|
25
|
+
if (isPersistedOnDatabase(this.artifact) && !this.artifact['@cds.persistence.table']) {
|
|
26
26
|
for (const prop of generalQueryProperties) {
|
|
27
27
|
const queryPart = (query.SELECT || query.SET)[prop];
|
|
28
28
|
if (Array.isArray(queryPart)) {
|
|
@@ -164,7 +164,7 @@ function _checkRef( ref, _links, $path, inColumns ) {
|
|
|
164
164
|
}
|
|
165
165
|
|
|
166
166
|
if (isJoinRelevant) {
|
|
167
|
-
const cdsPersistenceSkipped =
|
|
167
|
+
const cdsPersistenceSkipped = hasPersistenceSkipAnnotation(targetArt);
|
|
168
168
|
this.error( null, $path, {
|
|
169
169
|
'#': cdsPersistenceSkipped ? 'std' : 'abstract',
|
|
170
170
|
anno: '@cds.persistence.skip',
|
|
@@ -180,11 +180,7 @@ function _checkRef( ref, _links, $path, inColumns ) {
|
|
|
180
180
|
}
|
|
181
181
|
|
|
182
182
|
// check managed association to have foreign keys array filled
|
|
183
|
-
if (art.
|
|
184
|
-
this.error('expr-missing-foreign-key', $path, { id: pathStep, elemref: { ref } } );
|
|
185
|
-
break; // only one error per path
|
|
186
|
-
}
|
|
187
|
-
else if (art.on) {
|
|
183
|
+
if (art.target && art.on) {
|
|
188
184
|
for (let j = 0; j < art.on.length - 2; j++) {
|
|
189
185
|
if (art.on[j].ref && art.on[j + 1] === '=' && art.on[j + 2].ref) {
|
|
190
186
|
const [ fwdAssoc, fwdPath ] = getForwardAssociation(pathStep, art.on[j], art.on[j + 2]);
|
|
@@ -196,6 +192,12 @@ function _checkRef( ref, _links, $path, inColumns ) {
|
|
|
196
192
|
}
|
|
197
193
|
}
|
|
198
194
|
}
|
|
195
|
+
else if (art.target && !hasForeignKeys.call(this, art)) {
|
|
196
|
+
// Either no 'keys' array or an empty one. Since v6, to-many associations
|
|
197
|
+
// may have neither ON-condition nor foreign keys.
|
|
198
|
+
this.error('expr-missing-foreign-key', $path, { id: pathStep, elemref: { ref } } );
|
|
199
|
+
break; // only one error per path
|
|
200
|
+
}
|
|
199
201
|
}
|
|
200
202
|
}
|
|
201
203
|
|
package/lib/checks/types.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
|
-
const { hasAnnotationValue } = require('../model/csnUtils');
|
|
4
|
-
|
|
5
3
|
// Only to be used with validator.js - a correct this value needs to be provided!
|
|
6
4
|
|
|
5
|
+
const { hasPersistenceSkipAnnotation } = require('../model/csnUtils');
|
|
6
|
+
|
|
7
7
|
/**
|
|
8
8
|
* Scale must not be 'variable' or 'floating'
|
|
9
9
|
*
|
|
@@ -15,9 +15,9 @@ const { hasAnnotationValue } = require('../model/csnUtils');
|
|
|
15
15
|
* @param {CSN.Path} path the path to the member
|
|
16
16
|
*/
|
|
17
17
|
function checkDecimalScale( member, memberName, prop, path ) {
|
|
18
|
-
if (
|
|
19
|
-
|
|
20
|
-
|
|
18
|
+
if (this.artifact['@cds.persistence.exists'] ||
|
|
19
|
+
// skip is already filtered in validator, here for completeness
|
|
20
|
+
hasPersistenceSkipAnnotation(this.artifact))
|
|
21
21
|
return;
|
|
22
22
|
if (member.scale && (member.scale === 'variable' || member.scale === 'floating'))
|
|
23
23
|
this.error(null, path, { name: member.scale }, 'Unexpected scale $(NAME)');
|
package/lib/checks/validator.js
CHANGED
|
@@ -2,8 +2,8 @@
|
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
4
|
forEachDefinition, forEachMemberRecursively, forAllQueries,
|
|
5
|
-
forEachMember, getNormalizedQuery,
|
|
6
|
-
applyTransformations, functionList, mergeTransformers,
|
|
5
|
+
forEachMember, getNormalizedQuery,
|
|
6
|
+
applyTransformations, functionList, mergeTransformers, hasPersistenceSkipAnnotation,
|
|
7
7
|
} = require('../model/csnUtils');
|
|
8
8
|
const enrichCsn = require('./enricher');
|
|
9
9
|
|
|
@@ -17,6 +17,7 @@ const validateHasPersistedElements = require('./hasPersistedElements');
|
|
|
17
17
|
const checkForHanaTypes = require('./checkForTypes');
|
|
18
18
|
const { checkAnnotationExpression } = require('./structuredAnnoExpressions');
|
|
19
19
|
const checkForParams = require('./parameters');
|
|
20
|
+
const checkAndRemoveEnums = require('./enums');
|
|
20
21
|
// forOdata
|
|
21
22
|
const { validateDefaultValues } = require('./defaultValues');
|
|
22
23
|
const { checkActionOrFunction } = require('./actionsFunctions');
|
|
@@ -90,6 +91,7 @@ const forRelationalDBCsnValidators = [
|
|
|
90
91
|
navigationIntoMany,
|
|
91
92
|
checkPathsInStoredCalcElement,
|
|
92
93
|
featureFlags,
|
|
94
|
+
checkAndRemoveEnums,
|
|
93
95
|
];
|
|
94
96
|
/**
|
|
95
97
|
* @type {Array<(query: CSN.Query, path: CSN.Path) => void>}
|
|
@@ -239,8 +241,8 @@ function forRelationalDB( csn, that ) {
|
|
|
239
241
|
forRelationalDBQueryValidators.concat(commonQueryValidators),
|
|
240
242
|
{
|
|
241
243
|
skipArtifact: artifact => artifact.abstract ||
|
|
242
|
-
|
|
243
|
-
|
|
244
|
+
hasPersistenceSkipAnnotation(artifact) ||
|
|
245
|
+
artifact['@cds.persistence.exists'] ||
|
|
244
246
|
[ 'action', 'function', 'event' ].includes(artifact.kind),
|
|
245
247
|
});
|
|
246
248
|
}
|
|
@@ -261,7 +261,12 @@ function assertConsistency( model, stage ) {
|
|
|
261
261
|
$contains: { kind: true, test: TODO },
|
|
262
262
|
actions: { kind: true, inherits: 'definitions' },
|
|
263
263
|
enum: { kind: true, inherits: 'definitions' },
|
|
264
|
-
foreignKeys: {
|
|
264
|
+
foreignKeys: {
|
|
265
|
+
kind: true,
|
|
266
|
+
inherits: 'definitions',
|
|
267
|
+
instanceOf: 'ignore',
|
|
268
|
+
also: [ false ],
|
|
269
|
+
},
|
|
265
270
|
$keysNavigation: { kind: true, test: TODO },
|
|
266
271
|
$filtered: { kind: true, inherits: 'value' }, // for assoc+filter
|
|
267
272
|
$enclosed: { kind: true, inherits: 'value' }, // for comp+filter
|
|
@@ -334,7 +339,7 @@ function assertConsistency( model, stage ) {
|
|
|
334
339
|
test: isArray( column ),
|
|
335
340
|
instanceOf: XsnArtifact,
|
|
336
341
|
optional: thoseWithKind,
|
|
337
|
-
enum: [ '*' ],
|
|
342
|
+
enum: [ '*', '**' ], // '**' = duplicate wildcard
|
|
338
343
|
requires: [ 'location' ],
|
|
339
344
|
// schema: { kind: { isRequired: () => {} } } // kind not required
|
|
340
345
|
},
|
|
@@ -433,9 +438,6 @@ function assertConsistency( model, stage ) {
|
|
|
433
438
|
optional: [
|
|
434
439
|
'location', '$inferred', 'sort', 'nulls',
|
|
435
440
|
'param', 'scope', // for dynamic parameter '?'
|
|
436
|
-
// before recompilation for A2J, named types are replaced by the base
|
|
437
|
-
// definition (TODO v6 remove, we can probably remove elements+items now)
|
|
438
|
-
'elements', 'items', 'enum',
|
|
439
441
|
'args', 'op', 'func', 'suffix',
|
|
440
442
|
// calculated elements on-write - TODO: outside `value`
|
|
441
443
|
'stored',
|
|
@@ -472,7 +474,6 @@ function assertConsistency( model, stage ) {
|
|
|
472
474
|
'$parens',
|
|
473
475
|
'_artifact', // _artifact with "localized data"s 'coalesce'
|
|
474
476
|
'sort', 'nulls', // if used in GROUP BY
|
|
475
|
-
'enum', // for CAST in A2J recompilation (TODO v6: remove)
|
|
476
477
|
// `elements` of type is not put into cast, many will be cds.LargeString
|
|
477
478
|
...typeProperties, // for CAST
|
|
478
479
|
],
|
|
@@ -496,6 +497,7 @@ function assertConsistency( model, stage ) {
|
|
|
496
497
|
// expressions as annotation values
|
|
497
498
|
'$tokenTexts', 'op', 'args', 'func', '_artifact', 'type', '$typeArgs',
|
|
498
499
|
'scale', 'srid', 'length', 'precision', 'scope', '$parens',
|
|
500
|
+
'_block', '_outer', // for annotation assignments
|
|
499
501
|
],
|
|
500
502
|
// TODO: restrict path to #simplePath
|
|
501
503
|
},
|
|
@@ -877,9 +879,11 @@ function assertConsistency( model, stage ) {
|
|
|
877
879
|
if (node !== null || noSyntaxErrors()) {
|
|
878
880
|
isObject( node, parent, prop, spec, idx );
|
|
879
881
|
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
882
|
+
const choice = (!noSyntaxErrors()) ? 'none'
|
|
883
|
+
: (node.path) && 'ref' ||
|
|
884
|
+
(node.join) && 'join' ||
|
|
885
|
+
(node.query) && 'query' ||
|
|
886
|
+
'none';
|
|
883
887
|
if (spec[choice])
|
|
884
888
|
assertProp( node, parent, prop, spec[choice], choice );
|
|
885
889
|
else
|