@sap/cds-compiler 3.9.4 → 4.0.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 +92 -4
- package/README.md +0 -1
- package/bin/cdsc.js +11 -23
- package/bin/cdsse.js +3 -3
- package/doc/API.md +5 -0
- package/doc/CHANGELOG_ARCHIVE.md +1 -1
- package/doc/CHANGELOG_BETA.md +17 -1
- package/doc/CHANGELOG_DEPRECATED.md +28 -0
- package/lib/api/.eslintrc.json +1 -1
- package/lib/api/main.js +26 -8
- package/lib/api/options.js +2 -0
- package/lib/base/error.js +2 -0
- package/lib/base/message-registry.js +143 -64
- package/lib/base/messages.js +213 -107
- package/lib/base/model.js +11 -11
- package/lib/checks/.eslintrc.json +1 -1
- package/lib/checks/annotationsOData.js +2 -2
- package/lib/checks/elements.js +1 -1
- package/lib/checks/enricher.js +26 -3
- package/lib/checks/onConditions.js +67 -12
- package/lib/checks/queryNoDbArtifacts.js +106 -105
- package/lib/checks/sql-snippets.js +2 -0
- package/lib/checks/types.js +12 -6
- package/lib/checks/validator.js +2 -2
- package/lib/compiler/assert-consistency.js +10 -8
- package/lib/compiler/builtins.js +8 -2
- package/lib/compiler/checks.js +52 -35
- package/lib/compiler/define.js +31 -26
- package/lib/compiler/extend.js +120 -65
- package/lib/compiler/finalize-parse-cdl.js +12 -43
- package/lib/compiler/generate.js +16 -5
- package/lib/compiler/index.js +8 -5
- package/lib/compiler/kick-start.js +4 -3
- package/lib/compiler/populate.js +96 -95
- package/lib/compiler/propagator.js +7 -8
- package/lib/compiler/resolve.js +377 -103
- package/lib/compiler/shared.js +794 -517
- package/lib/compiler/tweak-assocs.js +8 -6
- package/lib/compiler/utils.js +44 -0
- package/lib/edm/annotations/genericTranslation.js +12 -4
- package/lib/edm/csn2edm.js +34 -32
- package/lib/edm/edm.js +34 -31
- package/lib/edm/edmAnnoPreprocessor.js +0 -23
- package/lib/edm/edmInboundChecks.js +7 -2
- package/lib/edm/edmPreprocessor.js +18 -17
- package/lib/edm/edmUtils.js +8 -4
- package/lib/gen/Dictionary.json +18 -0
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +4 -2
- package/lib/gen/languageParser.js +5006 -4582
- package/lib/json/from-csn.js +157 -112
- package/lib/json/to-csn.js +60 -89
- package/lib/language/antlrParser.js +17 -13
- package/lib/language/docCommentParser.js +11 -1
- package/lib/language/genericAntlrParser.js +13 -10
- package/lib/language/language.g4 +168 -97
- package/lib/main.d.ts +128 -36
- package/lib/main.js +1 -1
- package/lib/model/csnRefs.js +24 -5
- package/lib/model/csnUtils.js +9 -8
- package/lib/model/revealInternalProperties.js +7 -12
- package/lib/modelCompare/compare.js +1 -1
- package/lib/modelCompare/utils/filter.js +40 -2
- package/lib/optionProcessor.js +0 -3
- package/lib/render/toCdl.js +247 -214
- package/lib/render/toHdbcds.js +197 -181
- package/lib/render/toSql.js +325 -289
- package/lib/render/utils/common.js +42 -4
- package/lib/render/utils/delta.js +1 -1
- package/lib/render/utils/sql.js +3 -3
- package/lib/transform/braceExpression.js +2 -2
- package/lib/transform/db/.eslintrc.json +1 -1
- package/lib/transform/db/applyTransformations.js +3 -3
- package/lib/transform/db/associations.js +24 -12
- package/lib/transform/db/expansion.js +17 -18
- package/lib/transform/db/flattening.js +17 -21
- package/lib/transform/db/rewriteCalculatedElements.js +171 -64
- package/lib/transform/db/views.js +3 -4
- package/lib/transform/draft/db.js +21 -12
- package/lib/transform/draft/odata.js +4 -0
- package/lib/transform/forOdataNew.js +11 -10
- package/lib/transform/forRelationalDB.js +12 -7
- package/lib/transform/localized.js +4 -2
- package/lib/transform/odata/toFinalBaseType.js +5 -5
- package/lib/transform/odata/typesExposure.js +3 -3
- package/lib/transform/parseExpr.js +3 -0
- package/lib/transform/transformUtilsNew.js +43 -23
- package/lib/transform/translateAssocsToJoins.js +7 -6
- package/lib/transform/universalCsn/.eslintrc.json +1 -1
- package/lib/transform/universalCsn/coreComputed.js +7 -5
- package/lib/transform/universalCsn/universalCsnEnricher.js +12 -12
- package/package.json +2 -2
- package/share/messages/{duplicate-autoexposed.md → def-duplicate-autoexposed.md} +5 -1
- package/share/messages/message-explanations.json +1 -1
package/lib/checks/enricher.js
CHANGED
|
@@ -28,7 +28,7 @@ function enrichCsn( csn ) {
|
|
|
28
28
|
mixin: dictionary,
|
|
29
29
|
ref: pathRef,
|
|
30
30
|
type: simpleRef,
|
|
31
|
-
target
|
|
31
|
+
target,
|
|
32
32
|
includes: simpleRef,
|
|
33
33
|
columns,
|
|
34
34
|
// Annotations are ignored.
|
|
@@ -99,11 +99,10 @@ function enrichCsn( csn ) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
102
|
-
function simpleRef( node, prop ) {
|
|
102
|
+
function simpleRef( node, prop, ref ) {
|
|
103
103
|
setProp(node, '$path', [ ...csnPath ]);
|
|
104
104
|
cleanupCallbacks.push(() => delete node.$path);
|
|
105
105
|
|
|
106
|
-
const ref = node[prop];
|
|
107
106
|
if (typeof ref === 'string') {
|
|
108
107
|
const art = artifactRef( ref, null );
|
|
109
108
|
if (art || !ref.startsWith( 'cds.')) {
|
|
@@ -112,9 +111,19 @@ function enrichCsn( csn ) {
|
|
|
112
111
|
}
|
|
113
112
|
}
|
|
114
113
|
else if (Array.isArray( ref )) {
|
|
114
|
+
// e.g. `includes: [ 'E' ]`, which gets a parallel `_includes`.
|
|
115
115
|
setProp(node, `_${ prop }`, ref.map( r => artifactRef( r, null ) ));
|
|
116
116
|
cleanupCallbacks.push(() => delete node[`_${ prop }`]);
|
|
117
117
|
}
|
|
118
|
+
else if (typeof ref === 'object') {
|
|
119
|
+
// e.g. type refs via `{ type: { ref: [ 'E', 'field' ] } }
|
|
120
|
+
standard(node, prop, ref);
|
|
121
|
+
const art = artifactRef( ref, null );
|
|
122
|
+
if (art) {
|
|
123
|
+
setProp(node, `_${ prop }`, art);
|
|
124
|
+
cleanupCallbacks.push(() => delete node[`_${ prop }`]);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
// eslint-disable-next-line jsdoc/require-jsdoc
|
|
@@ -152,6 +161,20 @@ function enrichCsn( csn ) {
|
|
|
152
161
|
} );
|
|
153
162
|
csnPath.pop();
|
|
154
163
|
}
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* A target is either an anonymous aspect (with elements, etc.) via gensrc or a reference.
|
|
167
|
+
*
|
|
168
|
+
* @param {object} parent
|
|
169
|
+
* @param {string} prop
|
|
170
|
+
* @param {any} node
|
|
171
|
+
*/
|
|
172
|
+
function target( parent, prop, node ) {
|
|
173
|
+
if (node?.elements) // e.g. via gensrc
|
|
174
|
+
standard(parent, prop, node);
|
|
175
|
+
else
|
|
176
|
+
simpleRef(parent, prop, node);
|
|
177
|
+
}
|
|
155
178
|
}
|
|
156
179
|
|
|
157
180
|
module.exports = enrichCsn;
|
|
@@ -74,6 +74,7 @@ function validateOnCondition( member, memberName, property, path ) {
|
|
|
74
74
|
const validDollarSelf = otherSideIsValidDollarSelf(member.on, i);
|
|
75
75
|
const validStructuredElement = otherSideIsExpandableStructure.call(this, member.on, i);
|
|
76
76
|
for (let j = 0; j < _links.length - 1; j++) {
|
|
77
|
+
let hasPathError = false;
|
|
77
78
|
const csnPath = path.concat([ 'on', i, 'ref', j ]);
|
|
78
79
|
|
|
79
80
|
// For error messages
|
|
@@ -88,25 +89,34 @@ function validateOnCondition( member, memberName, property, path ) {
|
|
|
88
89
|
if (stepArt.on) {
|
|
89
90
|
// It's an unmanaged association - traversal is always forbidden
|
|
90
91
|
this.error('ref-unexpected-navigation', csnPath, { '#': 'unmanaged', id, elemref });
|
|
92
|
+
hasPathError = true;
|
|
91
93
|
}
|
|
92
94
|
else {
|
|
93
95
|
// It's a managed association - access of the foreign keys is allowed
|
|
94
|
-
|
|
95
|
-
if (!stepArt.keys.some(r => r.ref[0] === nextRef)) {
|
|
96
|
+
checkForeignKeyAccess(member.on[i], j, csnPath, (errorIndex) => {
|
|
96
97
|
this.error('ref-unexpected-navigation', csnPath, {
|
|
97
|
-
'#': 'std', id, elemref, name:
|
|
98
|
+
'#': 'std', id, elemref, name: ref[errorIndex].id || ref[errorIndex],
|
|
98
99
|
});
|
|
99
|
-
|
|
100
|
+
hasPathError = true;
|
|
101
|
+
});
|
|
100
102
|
}
|
|
101
103
|
}
|
|
102
|
-
if (stepArt.virtual)
|
|
103
|
-
this.error(null, csnPath, { id, elemref },
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
104
|
+
if (stepArt.virtual) {
|
|
105
|
+
this.error(null, csnPath, { id, elemref }, //
|
|
106
|
+
'Virtual elements can\'t be used in ON-conditions, step $(ID) of path $(ELEMREF)');
|
|
107
|
+
hasPathError = true;
|
|
108
|
+
}
|
|
109
|
+
if (ref[j].where) {
|
|
110
|
+
this.error('ref-unexpected-filter', csnPath, { '#': 'on-condition', id, elemref });
|
|
111
|
+
hasPathError = true;
|
|
112
|
+
}
|
|
113
|
+
if (ref[j].args) {
|
|
114
|
+
this.error('ref-unexpected-args', csnPath, { '#': 'on-condition', id, elemref });
|
|
115
|
+
hasPathError = true;
|
|
116
|
+
}
|
|
107
117
|
|
|
108
|
-
if (
|
|
109
|
-
|
|
118
|
+
if (hasPathError)
|
|
119
|
+
break; // avoid too many consequent errors
|
|
110
120
|
}
|
|
111
121
|
|
|
112
122
|
if (_art && !($scope === '$self' && ref.length === 1)) {
|
|
@@ -145,6 +155,51 @@ function validateOnCondition( member, memberName, property, path ) {
|
|
|
145
155
|
}
|
|
146
156
|
}
|
|
147
157
|
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Ensure that only foreign keys of the association `parent.ref[refIndex]` are accessed in `parent.ref`.
|
|
161
|
+
* If a non-fk field is accessed, `callback` is invoked.
|
|
162
|
+
*
|
|
163
|
+
* @param {object} parent Object containing `ref` and `_links` from csnRefs.
|
|
164
|
+
* @param {number} refIndex Index of the to-be-checked association in `parent.ref`
|
|
165
|
+
* @param {CSN.Path} csnPath
|
|
166
|
+
* @param {(errorIndex: number) => void} callback Called if there are non-fk path steps. Argument is index in
|
|
167
|
+
* `parent.ref` that is faulty. If a fk-step is missing, `errorIndex` will be `> parent.ref.length`.
|
|
168
|
+
*/
|
|
169
|
+
function checkForeignKeyAccess( parent, refIndex, csnPath, callback ) {
|
|
170
|
+
const { ref, _links } = parent;
|
|
171
|
+
const assoc = _links[refIndex].art;
|
|
172
|
+
|
|
173
|
+
const next = ref[refIndex + 1].id || ref[refIndex + 1];
|
|
174
|
+
let possibleKeys = next && assoc.keys.filter(r => r.ref[0] === next);
|
|
175
|
+
if (!possibleKeys || possibleKeys.length === 0) {
|
|
176
|
+
callback(refIndex + 1);
|
|
177
|
+
}
|
|
178
|
+
else {
|
|
179
|
+
// For cases where `Association to T { struct.one, struct.two };` is used instead of `{ struct }`.
|
|
180
|
+
// Note: We know that `{ struct, struct.one }` is not possible, so no prefix check required.
|
|
181
|
+
let fkIndex = 0;
|
|
182
|
+
let success = false;
|
|
183
|
+
while (!success && possibleKeys.length > 0) {
|
|
184
|
+
const pathStep = ref[refIndex + fkIndex + 1].id || ref[refIndex + fkIndex + 1];
|
|
185
|
+
|
|
186
|
+
// Function is immediately executed, before next iteration of loop. Access is fine.
|
|
187
|
+
// eslint-disable-next-line no-loop-func
|
|
188
|
+
possibleKeys = possibleKeys.filter((r) => {
|
|
189
|
+
const result = r.ref[fkIndex] === pathStep;
|
|
190
|
+
if (result && r.ref.length - 1 === fkIndex)
|
|
191
|
+
success = true; // full fk matched
|
|
192
|
+
|
|
193
|
+
return result;
|
|
194
|
+
});
|
|
195
|
+
++fkIndex;
|
|
196
|
+
}
|
|
197
|
+
if (!success)
|
|
198
|
+
callback(refIndex + fkIndex);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
|
|
148
203
|
/**
|
|
149
204
|
* Run the above validations also for mixins.
|
|
150
205
|
*
|
|
@@ -156,4 +211,4 @@ function validateMixinOnCondition( query, path ) {
|
|
|
156
211
|
forEachGeneric( query.SELECT, 'mixin', validateOnCondition.bind(this), path );
|
|
157
212
|
}
|
|
158
213
|
|
|
159
|
-
module.exports = { validateOnCondition, validateMixinOnCondition };
|
|
214
|
+
module.exports = { validateOnCondition, validateMixinOnCondition, checkForeignKeyAccess };
|
|
@@ -17,121 +17,122 @@ const { hasAnnotationValue, isPersistedOnDatabase, isBuiltinType } = require('..
|
|
|
17
17
|
* @param {CSN.Query} query Query to check
|
|
18
18
|
*/
|
|
19
19
|
function checkQueryForNoDBArtifacts( query ) {
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
c += Object.values(def.elements).reduce((acc, e) => {
|
|
32
|
-
acc += leafCount(e);
|
|
33
|
-
return acc;
|
|
34
|
-
}, 0);
|
|
35
|
-
}
|
|
36
|
-
else if (def.keys) {
|
|
37
|
-
c += def.keys.reduce((acc, e) => {
|
|
38
|
-
acc += leafCount(e._art);
|
|
39
|
-
return acc;
|
|
40
|
-
}, 0);
|
|
41
|
-
}
|
|
42
|
-
else if (def.type) {
|
|
43
|
-
if (isBuiltinType(def.type) && !(def.target))
|
|
44
|
-
return 1;
|
|
45
|
-
c += leafCount(this.csn.definitions[def.type]);
|
|
20
|
+
if (isPersistedOnDatabase(this.artifact) && !hasAnnotationValue(this.artifact, '@cds.persistence.table')) {
|
|
21
|
+
const generalQueryProperties = [ 'from', 'columns', 'where', 'groupBy', 'orderBy', 'having', 'limit' ];
|
|
22
|
+
for (const prop of generalQueryProperties) {
|
|
23
|
+
const queryPart = (query.SELECT || query.SET)[prop];
|
|
24
|
+
if (Array.isArray(queryPart)) {
|
|
25
|
+
for (const part of queryPart)
|
|
26
|
+
checkRef.call(this, part, prop === 'columns');
|
|
27
|
+
}
|
|
28
|
+
else if (typeof queryPart === 'object') {
|
|
29
|
+
checkRef.call(this, queryPart, prop === 'columns');
|
|
30
|
+
}
|
|
46
31
|
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Count the leaf-elements resulting from a given element.
|
|
37
|
+
*
|
|
38
|
+
* @param {CSN.Element} def Definition to check
|
|
39
|
+
* @returns {number} Number of leaf elements
|
|
40
|
+
*/
|
|
41
|
+
function leafCount( def ) {
|
|
42
|
+
let c = 0;
|
|
43
|
+
if (!def)
|
|
47
44
|
return c;
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
return;
|
|
45
|
+
if (def.elements) {
|
|
46
|
+
c += Object.values(def.elements).reduce((acc, e) => {
|
|
47
|
+
acc += leafCount.call(this, e);
|
|
48
|
+
return acc;
|
|
49
|
+
}, 0);
|
|
50
|
+
}
|
|
51
|
+
else if (def.keys) {
|
|
52
|
+
c += def.keys.reduce((acc, e) => {
|
|
53
|
+
acc += leafCount.call(this, e._art);
|
|
54
|
+
return acc;
|
|
55
|
+
}, 0);
|
|
56
|
+
}
|
|
57
|
+
else if (def.type) {
|
|
58
|
+
if (isBuiltinType(def.type) && !(def.target))
|
|
59
|
+
return 1;
|
|
60
|
+
c += leafCount.call(this, this.csn.definitions[def.type]);
|
|
61
|
+
}
|
|
62
|
+
return c;
|
|
63
|
+
}
|
|
58
64
|
|
|
59
|
-
|
|
65
|
+
/**
|
|
66
|
+
* Check the given ref for usage of skipped/abstract assoc targets
|
|
67
|
+
*
|
|
68
|
+
* @param {object} obj CSN "thing" to check
|
|
69
|
+
* @param {boolean} inColumns True if the ref is part of a from
|
|
70
|
+
*/
|
|
71
|
+
function checkRef( obj, inColumns ) {
|
|
72
|
+
if (!(obj && obj.ref) || !obj._links || obj.$scope === 'alias')
|
|
73
|
+
return;
|
|
60
74
|
|
|
61
|
-
|
|
62
|
-
for (let i = 0; i < (inColumns ? links.length - 1 : links.length); i++) {
|
|
63
|
-
const link = links[i];
|
|
64
|
-
if (!link)
|
|
65
|
-
continue;
|
|
75
|
+
const links = obj._links;
|
|
66
76
|
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
77
|
+
// Don't check the last element - to allow association publishing in columns
|
|
78
|
+
for (let i = 0; i < (inColumns ? links.length - 1 : links.length); i++) {
|
|
79
|
+
const link = links[i];
|
|
80
|
+
if (!link)
|
|
81
|
+
continue;
|
|
70
82
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
if (!isPersistedOnDatabase(endArtifact)) {
|
|
75
|
-
const nextElement = obj.ref[i + 1];
|
|
76
|
-
/**
|
|
77
|
-
* if we only navigate to foreign keys of the managed association in a view, we do not need to join,
|
|
78
|
-
* thus we can produce the view even if the target of the association is not persisted
|
|
79
|
-
*
|
|
80
|
-
* @param {CSN.Element} assoc association in ref
|
|
81
|
-
* @param {string} nextStep the ref step following the association
|
|
82
|
-
* @returns {boolean} true if no join will be generated
|
|
83
|
-
*/
|
|
84
|
-
const isJoinRelevant = (assoc, nextStep) => {
|
|
85
|
-
if (!assoc.keys)
|
|
86
|
-
return true;
|
|
87
|
-
const isExposedColumnAssocOrComposition = this.csnUtils.isAssocOrComposition(obj._art);
|
|
88
|
-
return !assoc.keys
|
|
89
|
-
.some(fk => fk.ref[0] === nextStep && !isExposedColumnAssocOrComposition);
|
|
90
|
-
};
|
|
91
|
-
if (isJoinRelevant(art, nextElement)) {
|
|
92
|
-
const cdsPersistenceSkipped = hasAnnotationValue(endArtifact, '@cds.persistence.skip');
|
|
93
|
-
this.error( null, obj.$path, {
|
|
94
|
-
id: pathStep, elemref: obj, name, '#': cdsPersistenceSkipped ? 'std' : 'abstract',
|
|
95
|
-
}, {
|
|
96
|
-
std: 'Unexpected “@cds.persistence.skip” annotation on association target $(NAME) of $(ID) in path $(ELEMREF)',
|
|
97
|
-
abstract: 'Unexpected “abstract” association target $(NAME) of $(ID) in path $(ELEMREF)',
|
|
98
|
-
} );
|
|
99
|
-
}
|
|
100
|
-
}
|
|
101
|
-
// check managed association to have foreign keys array filled
|
|
102
|
-
if (art.keys && leafCount(art) === 0) {
|
|
103
|
-
this.error(null,
|
|
104
|
-
obj.$path,
|
|
105
|
-
{ id: pathStep, elemref: obj },
|
|
106
|
-
'Path step $(ID) of $(ELEMREF) has no foreign keys');
|
|
107
|
-
}
|
|
83
|
+
const { art } = link;
|
|
84
|
+
if (!art)
|
|
85
|
+
continue;
|
|
108
86
|
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
87
|
+
const endArtifact = art.target ? this.csn.definitions[art.target] : art;
|
|
88
|
+
const pathStep = obj.ref[i].id ? obj.ref[i].id : obj.ref[i];
|
|
89
|
+
const name = art.target ? art.target : pathStep;
|
|
90
|
+
if (!isPersistedOnDatabase(endArtifact)) {
|
|
91
|
+
const nextElement = obj.ref[i + 1];
|
|
92
|
+
/**
|
|
93
|
+
* if we only navigate to foreign keys of the managed association in a view, we do not need to join,
|
|
94
|
+
* thus we can produce the view even if the target of the association is not persisted
|
|
95
|
+
*
|
|
96
|
+
* @param {CSN.Element} assoc association in ref
|
|
97
|
+
* @param {string} nextStep the ref step following the association
|
|
98
|
+
* @returns {boolean} true if no join will be generated
|
|
99
|
+
*/
|
|
100
|
+
const isJoinRelevant = (assoc, nextStep) => {
|
|
101
|
+
if (!assoc.keys)
|
|
102
|
+
return true;
|
|
103
|
+
const isExposedColumnAssocOrComposition = this.csnUtils.isAssocOrComposition(obj._art.type);
|
|
104
|
+
return !assoc.keys
|
|
105
|
+
.some(fk => fk.ref[0] === nextStep && !isExposedColumnAssocOrComposition);
|
|
106
|
+
};
|
|
107
|
+
if (isJoinRelevant(art, nextElement)) {
|
|
108
|
+
const cdsPersistenceSkipped = hasAnnotationValue(endArtifact, '@cds.persistence.skip');
|
|
109
|
+
this.error( null, obj.$path, {
|
|
110
|
+
id: pathStep, elemref: obj, name, '#': cdsPersistenceSkipped ? 'std' : 'abstract',
|
|
111
|
+
}, {
|
|
112
|
+
std: 'Unexpected “@cds.persistence.skip” annotation on association target $(NAME) of $(ID) in path $(ELEMREF)',
|
|
113
|
+
abstract: 'Unexpected “abstract” association target $(NAME) of $(ID) in path $(ELEMREF)',
|
|
114
|
+
} );
|
|
121
115
|
}
|
|
122
116
|
}
|
|
123
|
-
|
|
117
|
+
// check managed association to have foreign keys array filled
|
|
118
|
+
if (art.keys && leafCount.call(this, art) === 0) {
|
|
119
|
+
this.error(null,
|
|
120
|
+
obj.$path,
|
|
121
|
+
{ id: pathStep, elemref: obj },
|
|
122
|
+
'Path step $(ID) of $(ELEMREF) has no foreign keys');
|
|
123
|
+
}
|
|
124
124
|
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
125
|
+
if (art.on) {
|
|
126
|
+
for (let j = 0; j < art.on.length; j++) {
|
|
127
|
+
if (j < art.on.length - 2 && art.on[j].ref && art.on[j + 1] === '=' && art.on[j + 2].ref) {
|
|
128
|
+
const [ fwdAssoc, fwdPath ] = getForwardAssociation(pathStep, art.on[j], art.on[j + 2]);
|
|
129
|
+
if (fwdAssoc && fwdAssoc.keys && leafCount.call(this, fwdAssoc) === 0) {
|
|
130
|
+
this.error(null, obj.$path,
|
|
131
|
+
{ name: pathStep, elemref: obj, id: fwdPath },
|
|
132
|
+
'Path step $(NAME) of $(ELEMREF) is a $self comparison with $(ID) that has no foreign keys');
|
|
133
|
+
j += 2;
|
|
134
|
+
}
|
|
135
|
+
}
|
|
135
136
|
}
|
|
136
137
|
}
|
|
137
138
|
}
|
|
@@ -22,6 +22,8 @@ function checkSqlAnnotationOnElement( member, memberName, prop, path ) {
|
|
|
22
22
|
this.message('anno-invalid-sql-view-element', path, { anno: 'sql.append' }, 'Annotation $(ANNO) can\'t be used on elements in views' );
|
|
23
23
|
else if (this.csnUtils.isStructured(member))
|
|
24
24
|
this.message('anno-invalid-sql-struct', path, { anno: 'sql.append' }, 'Annotation $(ANNO) can\'t be used on structured elements' );
|
|
25
|
+
else if (member.value && !member.value.stored)
|
|
26
|
+
this.message('anno-invalid-sql-calc', path, { anno: 'sql.append' }, 'Annotation $(ANNO) can\'t be used on calculated elements on read' );
|
|
25
27
|
else
|
|
26
28
|
checkValidAnnoValue(member, '@sql.append', path, this.error, this.options);
|
|
27
29
|
}
|
package/lib/checks/types.js
CHANGED
|
@@ -53,10 +53,10 @@ function checkElementTypeDefinitionHasType( member, memberName, prop, path ) {
|
|
|
53
53
|
const parent = this.csn.definitions[path[1]];
|
|
54
54
|
|
|
55
55
|
// should only happen with csn input, not in cdl
|
|
56
|
-
// calculated elements may not have a .type (requires beta flag)
|
|
57
|
-
if (!member.value &&
|
|
56
|
+
// calculated elements on-read may not have a .type (requires beta flag)
|
|
57
|
+
if ((!member.value || member.value.stored) &&
|
|
58
58
|
!parent.projection && !parent.query && !hasArtifactTypeInformation(member)) {
|
|
59
|
-
errorAboutMissingType(this.error, path, memberName, true);
|
|
59
|
+
errorAboutMissingType(this.error, path, member, memberName, true);
|
|
60
60
|
return;
|
|
61
61
|
}
|
|
62
62
|
|
|
@@ -97,7 +97,7 @@ function checkTypeDefinitionHasType( artifact, artifactName, prop, path ) {
|
|
|
97
97
|
|
|
98
98
|
// should only happen with csn input, not in cdl
|
|
99
99
|
if (!hasArtifactTypeInformation(artifact)) {
|
|
100
|
-
errorAboutMissingType(this.error, path, artifactName);
|
|
100
|
+
errorAboutMissingType(this.error, path, artifact, artifactName);
|
|
101
101
|
return;
|
|
102
102
|
}
|
|
103
103
|
|
|
@@ -153,13 +153,19 @@ function checkTypeOfHasProperType( artOrElement, name, model, error, path, deriv
|
|
|
153
153
|
*
|
|
154
154
|
* @param {Function} error the error function
|
|
155
155
|
* @param {CSN.Path} path the path to the element or the artifact
|
|
156
|
+
* @param {CSN.Artifact} artifact Element or other member/definition.
|
|
156
157
|
* @param {string} name of the element or the artifact which is dubious
|
|
157
158
|
* @param {boolean} isElement indicates whether we are dealing with an element or an artifact
|
|
158
159
|
*/
|
|
159
|
-
function errorAboutMissingType( error, path, name,
|
|
160
|
-
|
|
160
|
+
function errorAboutMissingType( error, path, artifact, name,
|
|
161
|
+
isElement = false ) {
|
|
162
|
+
let variant = isElement ? 'elm' : 'std';
|
|
163
|
+
if (artifact.value?.stored)
|
|
164
|
+
variant = 'calc';
|
|
165
|
+
error('check-proper-type', path, { art: name, '#': variant }, {
|
|
161
166
|
std: 'Dubious type $(ART) without type information',
|
|
162
167
|
elm: 'Dubious element $(ART) without type information',
|
|
168
|
+
calc: 'A stored calculated element must have a type',
|
|
163
169
|
});
|
|
164
170
|
}
|
|
165
171
|
|
package/lib/checks/validator.js
CHANGED
|
@@ -19,7 +19,7 @@ const checkForParams = require('./parameters');
|
|
|
19
19
|
const { validateDefaultValues } = require('./defaultValues');
|
|
20
20
|
const { checkActionOrFunction } = require('./actionsFunctions');
|
|
21
21
|
const {
|
|
22
|
-
|
|
22
|
+
checkCoreMediaTypeAllowance, checkAnalytics,
|
|
23
23
|
checkAtSapAnnotations, checkReadOnlyAndInsertOnly,
|
|
24
24
|
} = require('./annotationsOData');
|
|
25
25
|
// both
|
|
@@ -251,7 +251,7 @@ function forOdata( csn, that ) {
|
|
|
251
251
|
if (that.csnUtils.getServiceName(artifactName)) {
|
|
252
252
|
checkAtSapAnnotations.bind(that)(artifact);
|
|
253
253
|
forEachMemberRecursively(artifact, [
|
|
254
|
-
|
|
254
|
+
checkCoreMediaTypeAllowance.bind(that),
|
|
255
255
|
checkAnalytics.bind(that),
|
|
256
256
|
checkAtSapAnnotations.bind(that),
|
|
257
257
|
]);
|
|
@@ -105,7 +105,6 @@ function assertConsistency( model, stage ) {
|
|
|
105
105
|
'$blocks',
|
|
106
106
|
'$messageFunctions',
|
|
107
107
|
'$functions',
|
|
108
|
-
'$volatileFunctions',
|
|
109
108
|
'_sortedSources',
|
|
110
109
|
],
|
|
111
110
|
},
|
|
@@ -238,6 +237,7 @@ function assertConsistency( model, stage ) {
|
|
|
238
237
|
// specified elements in query entities (TODO: introduce real "specified elements" instead):
|
|
239
238
|
elements$: { kind: true, enumerable: false, test: TODO },
|
|
240
239
|
enum$: { kind: true, enumerable: false, test: TODO },
|
|
240
|
+
typeProps$: { kind: true, enumerable: false, test: TODO },
|
|
241
241
|
actions: { kind: true, inherits: 'definitions' },
|
|
242
242
|
enum: { kind: true, inherits: 'definitions' },
|
|
243
243
|
foreignKeys: { kind: true, inherits: 'definitions' },
|
|
@@ -253,6 +253,7 @@ function assertConsistency( model, stage ) {
|
|
|
253
253
|
requires: [ 'op', 'location', 'args' ],
|
|
254
254
|
optional: [
|
|
255
255
|
'quantifier', 'orderBy', 'limit', 'name', '$parens', 'kind',
|
|
256
|
+
'_origin', // TODO tmp, see TODO in getOriginRaw()
|
|
256
257
|
'_parent', '_main', '_leadingQuery', '_effectiveType', '$effectiveSeqNo', // in FROM
|
|
257
258
|
],
|
|
258
259
|
},
|
|
@@ -359,8 +360,9 @@ function assertConsistency( model, stage ) {
|
|
|
359
360
|
optional: [
|
|
360
361
|
'$delimited', // TODO remove?
|
|
361
362
|
'args', '$syntax',
|
|
362
|
-
'where', '
|
|
363
|
-
'
|
|
363
|
+
'where', 'groupBy', 'limit', 'orderBy', 'having',
|
|
364
|
+
'cardinality',
|
|
365
|
+
'_artifact', '_navigation', '_user',
|
|
364
366
|
'$inferred',
|
|
365
367
|
],
|
|
366
368
|
},
|
|
@@ -386,6 +388,7 @@ function assertConsistency( model, stage ) {
|
|
|
386
388
|
// locations of parentheses pairs around expression:
|
|
387
389
|
$parens: { parser: true, test: TODO },
|
|
388
390
|
$prefix: { test: isString }, // compiler-corrected path prefix
|
|
391
|
+
$extended: { test: TODO, kind: [ 'element', '$inline' ] }, // `extend … with columns`
|
|
389
392
|
$syntax: {
|
|
390
393
|
parser: true,
|
|
391
394
|
kind: [ 'entity', 'view', 'type', 'aspect' ],
|
|
@@ -438,7 +441,7 @@ function assertConsistency( model, stage ) {
|
|
|
438
441
|
...typeProperties, // for CAST
|
|
439
442
|
],
|
|
440
443
|
},
|
|
441
|
-
query: { requires: [ 'query', 'location' ] },
|
|
444
|
+
query: { requires: [ 'query', 'location' ], optional: [ 'stored' ] },
|
|
442
445
|
},
|
|
443
446
|
literal: { // TODO: check value against literal
|
|
444
447
|
test: isString,
|
|
@@ -564,6 +567,7 @@ function assertConsistency( model, stage ) {
|
|
|
564
567
|
_parent: { kind: true, test: TODO },
|
|
565
568
|
_service: { kind: true, test: TODO },
|
|
566
569
|
_main: { kind: true, test: TODO },
|
|
570
|
+
_user: { kind: true, test: TODO },
|
|
567
571
|
_artifact: { test: TODO },
|
|
568
572
|
_navigation: { test: TODO },
|
|
569
573
|
_effectiveType: { kind: true, test: TODO },
|
|
@@ -631,13 +635,12 @@ function assertConsistency( model, stage ) {
|
|
|
631
635
|
parser: true,
|
|
632
636
|
kind: true,
|
|
633
637
|
test: isOneOf([
|
|
634
|
-
'',
|
|
638
|
+
'', // constructed “super annotate” statement, redirected user-provided target
|
|
635
639
|
// Uppercase values are used in logic, lowercase value are "just for us", i.e.
|
|
636
640
|
// debugging or to add properties such as $generated in Universal CSN.
|
|
637
641
|
// However, that is no longer true. For example, `autoexposed` is used in populate.js
|
|
638
642
|
// as well.
|
|
639
643
|
'IMPLICIT',
|
|
640
|
-
'REDIRECTED',
|
|
641
644
|
'NULL', // from propagator
|
|
642
645
|
'prop', // from propagator
|
|
643
646
|
|
|
@@ -651,7 +654,7 @@ function assertConsistency( model, stage ) {
|
|
|
651
654
|
'cast', // type from cast() function
|
|
652
655
|
'composition-entity',
|
|
653
656
|
'copy', // only used in rewriteCondition(): On-condition is copied
|
|
654
|
-
'duplicate-autoexposed', // just like `autoexposed`, but with `duplicate` error.
|
|
657
|
+
'def-duplicate-autoexposed', // just like `autoexposed`, but with `duplicate` error.
|
|
655
658
|
'expanded', // expanded elements, items, params
|
|
656
659
|
'include', // through includes, e.g. `entity E : F {}`
|
|
657
660
|
'keys',
|
|
@@ -683,7 +686,6 @@ function assertConsistency( model, stage ) {
|
|
|
683
686
|
$expected: { parser: true, test: isOneOf([ 'approved-exists', 'exists' ]) },
|
|
684
687
|
$messageFunctions: { test: TODO },
|
|
685
688
|
$functions: { test: TODO },
|
|
686
|
-
$volatileFunctions: { test: TODO },
|
|
687
689
|
};
|
|
688
690
|
let _noSyntaxErrors = null;
|
|
689
691
|
assertProp( model, null, stageParser ? ':parser' : ':model', null, true );
|
package/lib/compiler/builtins.js
CHANGED
|
@@ -433,6 +433,8 @@ function initBuiltins( model ) {
|
|
|
433
433
|
cds._subArtifacts.hana = hana;
|
|
434
434
|
env( coreHana, 'cds.hana.', hana );
|
|
435
435
|
model.$internal = { $frontend: '$internal' };
|
|
436
|
+
// namespace:"localized" reserved ---
|
|
437
|
+
model.definitions.localized = createNamespace( 'localized', 'reserved' );
|
|
436
438
|
return;
|
|
437
439
|
|
|
438
440
|
function createNamespace( name, builtin ) {
|
|
@@ -494,7 +496,7 @@ function initBuiltins( model ) {
|
|
|
494
496
|
art.$requireElementAccess = magic.$requireElementAccess;
|
|
495
497
|
|
|
496
498
|
createMagicElements( art, magic.elements );
|
|
497
|
-
if (options.variableReplacements)
|
|
499
|
+
if (options.variableReplacements?.[name])
|
|
498
500
|
createMagicElements( art, options.variableReplacements[name] );
|
|
499
501
|
// setProp( art, '_effectiveType', art );
|
|
500
502
|
}
|
|
@@ -512,7 +514,11 @@ function initBuiltins( model ) {
|
|
|
512
514
|
for (const n of names) {
|
|
513
515
|
const magic = {
|
|
514
516
|
kind: 'builtin',
|
|
515
|
-
name: {
|
|
517
|
+
name: {
|
|
518
|
+
id: n,
|
|
519
|
+
absolute: art.name.absolute,
|
|
520
|
+
element: art.name.element ? `${ art.name.element }.${ n }` : n,
|
|
521
|
+
},
|
|
516
522
|
};
|
|
517
523
|
// Propagate this property so that it is available for sub-elements.
|
|
518
524
|
if (art.$uncheckedElements)
|