@sap/cds-compiler 4.0.2 → 4.1.2
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 +100 -5
- package/bin/cdsc.js +12 -12
- package/doc/CHANGELOG_BETA.md +11 -0
- package/lib/api/main.js +31 -11
- package/lib/api/validate.js +1 -1
- package/lib/base/location.js +6 -7
- package/lib/base/message-registry.js +84 -38
- package/lib/base/messages.js +11 -10
- package/lib/base/model.js +6 -2
- package/lib/checks/defaultValues.js +6 -6
- package/lib/checks/foreignKeys.js +0 -5
- package/lib/checks/onConditions.js +17 -12
- package/lib/checks/queryNoDbArtifacts.js +132 -72
- package/lib/checks/sql-snippets.js +15 -4
- package/lib/checks/types.js +3 -3
- package/lib/checks/utils.js +1 -1
- package/lib/compiler/assert-consistency.js +44 -16
- package/lib/compiler/base.js +1 -0
- package/lib/compiler/builtins.js +7 -8
- package/lib/compiler/checks.js +274 -197
- package/lib/compiler/classes.js +62 -0
- package/lib/compiler/cycle-detector.js +3 -3
- package/lib/compiler/define.js +63 -50
- package/lib/compiler/extend.js +38 -20
- package/lib/compiler/finalize-parse-cdl.js +2 -1
- package/lib/compiler/generate.js +0 -8
- package/lib/compiler/index.js +9 -7
- package/lib/compiler/kick-start.js +2 -0
- package/lib/compiler/populate.js +139 -110
- package/lib/compiler/propagator.js +4 -3
- package/lib/compiler/resolve.js +157 -126
- package/lib/compiler/shared.js +706 -404
- package/lib/compiler/tweak-assocs.js +21 -10
- package/lib/compiler/utils.js +228 -36
- package/lib/edm/annotations/genericTranslation.js +1 -1
- package/lib/edm/edm.js +4 -1
- package/lib/edm/edmPreprocessor.js +5 -4
- package/lib/edm/edmUtils.js +2 -4
- package/lib/gen/Dictionary.json +34 -10
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +1 -1
- package/lib/gen/languageParser.js +3987 -3963
- package/lib/json/from-csn.js +43 -47
- package/lib/json/to-csn.js +11 -11
- package/lib/language/antlrParser.js +2 -1
- package/lib/language/genericAntlrParser.js +52 -43
- package/lib/language/language.g4 +59 -59
- package/lib/language/multiLineStringParser.js +2 -0
- package/lib/main.d.ts +5 -0
- package/lib/model/csnRefs.js +37 -19
- package/lib/model/csnUtils.js +20 -16
- package/lib/model/revealInternalProperties.js +29 -21
- package/lib/modelCompare/compare.js +112 -39
- package/lib/modelCompare/utils/filter.js +54 -24
- package/lib/optionProcessor.js +6 -6
- package/lib/render/manageConstraints.js +20 -17
- package/lib/render/toCdl.js +34 -20
- package/lib/render/toHdbcds.js +2 -2
- package/lib/render/toRename.js +4 -9
- package/lib/render/toSql.js +77 -26
- package/lib/render/utils/common.js +3 -3
- package/lib/render/utils/unique.js +52 -0
- package/lib/transform/db/applyTransformations.js +61 -20
- package/lib/transform/db/assertUnique.js +7 -8
- package/lib/transform/db/associations.js +2 -2
- package/lib/transform/db/cdsPersistence.js +8 -8
- package/lib/transform/db/expansion.js +17 -21
- package/lib/transform/db/flattening.js +23 -23
- package/lib/transform/db/rewriteCalculatedElements.js +20 -14
- package/lib/transform/db/temporal.js +1 -1
- package/lib/transform/db/transformExists.js +8 -7
- package/lib/transform/db/views.js +73 -33
- package/lib/transform/draft/db.js +11 -9
- package/lib/transform/draft/odata.js +1 -1
- package/lib/transform/{forOdataNew.js → forOdata.js} +6 -6
- package/lib/transform/forRelationalDB.js +69 -75
- package/lib/transform/localized.js +6 -5
- package/lib/transform/odata/toFinalBaseType.js +3 -3
- package/lib/transform/{transformUtilsNew.js → transformUtils.js} +4 -101
- package/lib/transform/translateAssocsToJoins.js +14 -28
- package/package.json +1 -1
- package/share/messages/check-proper-type-of.md +1 -1
- package/share/messages/{check-proper-type.md → def-missing-type.md} +3 -5
- package/share/messages/message-explanations.json +1 -1
package/lib/base/messages.js
CHANGED
|
@@ -17,6 +17,7 @@ const { cdlNewLineRegEx } = require('../language/textUtils');
|
|
|
17
17
|
const fs = require('fs');
|
|
18
18
|
const path = require('path');
|
|
19
19
|
const { inspect } = require('util')
|
|
20
|
+
const { CsnLocation } = require('../compiler/classes');
|
|
20
21
|
|
|
21
22
|
// term instance for messages
|
|
22
23
|
const colorTerm = term();
|
|
@@ -94,7 +95,7 @@ class CompilationError extends Error {
|
|
|
94
95
|
super( `CDS compilation failed\n${messages?.map( m => m.toString() ).join('\n') || ''}` );
|
|
95
96
|
/** @since v4.0.0 */
|
|
96
97
|
this.code = 'ERR_CDS_COMPILATION_FAILURE';
|
|
97
|
-
this.messages = messages;
|
|
98
|
+
this.messages = [ ...messages ].sort(compareMessageSeverityAware);
|
|
98
99
|
|
|
99
100
|
// TODO: remove this bin/cdsc.js specifics
|
|
100
101
|
Object.defineProperty( this, 'hasBeenReported', { value: false, configurable: true, writable: true, enumerable: false } );
|
|
@@ -128,7 +129,7 @@ class CompilationError extends Error {
|
|
|
128
129
|
class CompileMessage {
|
|
129
130
|
/**
|
|
130
131
|
* Creates an instance of CompileMessage.
|
|
131
|
-
* @param {
|
|
132
|
+
* @param {CSN.Location} location Location of the message
|
|
132
133
|
* @param {string} msg The message text
|
|
133
134
|
* @param {MessageSeverity} [severity='Error'] Severity: Debug, Info, Warning, Error
|
|
134
135
|
* @param {string} [id] The ID of the message - visible as property messageId
|
|
@@ -139,7 +140,7 @@ class CompileMessage {
|
|
|
139
140
|
*/
|
|
140
141
|
constructor(location, msg, severity = 'Error', id = null, home = null, moduleName = null) {
|
|
141
142
|
this.message = msg;
|
|
142
|
-
this.$location = { ...location, address: undefined };
|
|
143
|
+
this.$location = { __proto__: CsnLocation.prototype, ...location, address: undefined };
|
|
143
144
|
this.validNames = null;
|
|
144
145
|
this.home = home; // semantic location, e.g. 'entity:"E"/element:"x"'
|
|
145
146
|
this.severity = severity;
|
|
@@ -892,14 +893,14 @@ function replaceInString( text, params ) {
|
|
|
892
893
|
}
|
|
893
894
|
|
|
894
895
|
/**
|
|
895
|
-
* @param {
|
|
896
|
-
* @returns {
|
|
896
|
+
* @param {CsnLocation} loc
|
|
897
|
+
* @returns {CsnLocation}
|
|
897
898
|
*/
|
|
898
899
|
function weakLocation( loc ) {
|
|
899
900
|
if (!loc)
|
|
900
|
-
return
|
|
901
|
+
return new CsnLocation();
|
|
901
902
|
// no endLine/endCol
|
|
902
|
-
return
|
|
903
|
+
return new CsnLocation( loc.file, loc.line, loc.col, undefined, undefined );
|
|
903
904
|
}
|
|
904
905
|
|
|
905
906
|
/**
|
|
@@ -1310,14 +1311,14 @@ function artName( art, omit ) {
|
|
|
1310
1311
|
const r = (name.absolute) ? [ quoted( name.absolute ) ] : [];
|
|
1311
1312
|
if (name.select && name.select > 1 || name.select != null && art.kind !== 'element') // Yes, omit select:1 for element - TODO: re-check
|
|
1312
1313
|
r.push( (art.kind === 'extend' ? 'block:' : 'query:') + name.select ); // TODO: rename to 'select:1' and consider whether there are more selects
|
|
1313
|
-
if (name.action && omit !== 'action')
|
|
1314
|
+
if (name.action != null && omit !== 'action')
|
|
1314
1315
|
r.push( `${ memberActionName(art) }:${ quoted( name.action ) }` );
|
|
1315
|
-
if (name.alias && art.kind !== '$self' && name.$inferred !== '$internal')
|
|
1316
|
+
if (name.alias != null && art.kind !== '$self' && name.$inferred !== '$internal')
|
|
1316
1317
|
r.push( (art.kind === 'mixin' ? 'mixin:' : 'alias:') + quoted( name.alias ) );
|
|
1317
1318
|
if (name.param != null && omit !== 'param')
|
|
1318
1319
|
r.push( name.param ? `param:${ quoted( name.param ) }` : 'returns' ); // TODO: join
|
|
1319
1320
|
|
|
1320
|
-
if (name.element && omit !== 'element') {
|
|
1321
|
+
if (name.element != null && omit !== 'element') {
|
|
1321
1322
|
if (name.select != null && !art.$inferred)
|
|
1322
1323
|
r.push( 'column:' + quoted( name.element ) );
|
|
1323
1324
|
else
|
package/lib/base/model.js
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
// module- and csn/XSN-independent definitions
|
|
2
|
+
|
|
3
|
+
// TODO: move XSN-specific things to lib/compiler/utils/
|
|
4
|
+
// TODO: move CSN-specific things to ???
|
|
5
|
+
|
|
1
6
|
'use strict';
|
|
2
7
|
|
|
3
8
|
const { forEach } = require('../utils/objectUtils');
|
|
@@ -22,14 +27,13 @@ const queryOps = {
|
|
|
22
27
|
const availableBetaFlags = {
|
|
23
28
|
// enabled by --beta-mode
|
|
24
29
|
annotationExpressions: true,
|
|
25
|
-
toRename: true, // Removes once it's publicly documented
|
|
26
30
|
assocsWithParams: true, // beta, because runtimes don't support it, yet.
|
|
27
31
|
hanaAssocRealCardinality: true,
|
|
28
32
|
mapAssocToJoinCardinality: true, // only SAP HANA HEX engine supports it
|
|
29
33
|
enableUniversalCsn: true,
|
|
30
|
-
aspectWithoutElements: true, // TODO: do not just remove beta flag - remove elements, too!
|
|
31
34
|
odataTerms: true,
|
|
32
35
|
optionalActionFunctionParameters: true, // not supported by runtime, yet.
|
|
36
|
+
associationDefault: true,
|
|
33
37
|
// disabled by --beta-mode
|
|
34
38
|
nestedServices: false,
|
|
35
39
|
};
|
|
@@ -53,12 +53,12 @@ function rejectParamDefaultsInHanaCds( member, memberName, prop, path ) {
|
|
|
53
53
|
*/
|
|
54
54
|
function warnAboutDefaultOnAssociationForHanaCds( member, memberName, prop, path ) {
|
|
55
55
|
const art = this.csn.definitions[path[1]];
|
|
56
|
-
if (!art.query && !art.projection &&
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
if (this.options.transformation === 'hdbcds' && !art.query && !art.projection && member.target && member.default) {
|
|
57
|
+
const type = member._type?.type || member.type || 'cds.Association';
|
|
58
|
+
this.warning('type-invalid-default', path, { '#': type === 'cds.Association' ? 'std' : 'comp' }, {
|
|
59
|
+
std: 'Default on associations is not supported for HDBCDS',
|
|
60
|
+
comp: 'Default on compositions is not supported for HDBCDS',
|
|
61
|
+
});
|
|
62
62
|
}
|
|
63
63
|
}
|
|
64
64
|
|
|
@@ -16,8 +16,6 @@ const { setProp } = require('../base/model');
|
|
|
16
16
|
function validateForeignKeys( member, memberName ) {
|
|
17
17
|
// We have a managed association
|
|
18
18
|
const isManagedAssoc = mem => mem && mem.target && !mem.on;
|
|
19
|
-
// We have an unmanaged association
|
|
20
|
-
const isUnmanagedAssoc = mem => mem && mem.target && mem.on && !mem.keys;
|
|
21
19
|
|
|
22
20
|
// Declared as arrow-function to keep scope the same (this value)
|
|
23
21
|
const handleAssociation = (mem) => {
|
|
@@ -46,9 +44,6 @@ function validateForeignKeys( member, memberName ) {
|
|
|
46
44
|
if (mem.items) {
|
|
47
45
|
this.error(null, member.$path, {}, 'Array-like properties must not be foreign keys');
|
|
48
46
|
}
|
|
49
|
-
else if (isUnmanagedAssoc(mem)) {
|
|
50
|
-
this.error(null, member.$path, {}, 'Unmanaged association must not be a foreign key');
|
|
51
|
-
}
|
|
52
47
|
else if (mem.keys) {
|
|
53
48
|
handleAssociation(mem);
|
|
54
49
|
}
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { forEachGeneric } = require('../model/csnUtils');
|
|
4
4
|
const { otherSideIsExpandableStructure, resolveArtifactType } = require('./utils');
|
|
5
|
+
const { pathId } = require('../model/csnRefs');
|
|
5
6
|
|
|
6
7
|
// Only to be used with validator.js - a correct this value needs to be provided!
|
|
7
8
|
|
|
@@ -93,7 +94,7 @@ function validateOnCondition( member, memberName, property, path ) {
|
|
|
93
94
|
}
|
|
94
95
|
else {
|
|
95
96
|
// It's a managed association - access of the foreign keys is allowed
|
|
96
|
-
|
|
97
|
+
requireForeignKeyAccess(member.on[i], j, (errorIndex) => {
|
|
97
98
|
this.error('ref-unexpected-navigation', csnPath, {
|
|
98
99
|
'#': 'std', id, elemref, name: ref[errorIndex].id || ref[errorIndex],
|
|
99
100
|
});
|
|
@@ -158,22 +159,26 @@ function validateOnCondition( member, memberName, property, path ) {
|
|
|
158
159
|
|
|
159
160
|
/**
|
|
160
161
|
* Ensure that only foreign keys of the association `parent.ref[refIndex]` are accessed in `parent.ref`.
|
|
161
|
-
* If a non-fk field is accessed, `
|
|
162
|
+
* If a non-fk field is accessed, `noForeignKeyCallback` is invoked.
|
|
162
163
|
*
|
|
163
|
-
* @param {object} parent
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
* @param {
|
|
167
|
-
*
|
|
164
|
+
* @param {object} parent
|
|
165
|
+
* Object containing `ref` and `_links` from csnRefs.
|
|
166
|
+
*
|
|
167
|
+
* @param {number} refIndex
|
|
168
|
+
* Index of the to-be-checked association in `parent.ref`
|
|
169
|
+
*
|
|
170
|
+
* @param {(errorIndex: number) => void} noForeignKeyCallback
|
|
171
|
+
* Called if there are non-fk path steps. Argument is index in `parent.ref` that is faulty.
|
|
172
|
+
* If a fk-step is missing, `errorIndex` will be `> parent.ref.length`.
|
|
168
173
|
*/
|
|
169
|
-
function
|
|
174
|
+
function requireForeignKeyAccess( parent, refIndex, noForeignKeyCallback ) {
|
|
170
175
|
const { ref, _links } = parent;
|
|
171
176
|
const assoc = _links[refIndex].art;
|
|
172
177
|
|
|
173
|
-
const next = ref[refIndex + 1]
|
|
178
|
+
const next = pathId(ref[refIndex + 1]);
|
|
174
179
|
let possibleKeys = next && assoc.keys.filter(r => r.ref[0] === next);
|
|
175
180
|
if (!possibleKeys || possibleKeys.length === 0) {
|
|
176
|
-
|
|
181
|
+
noForeignKeyCallback(refIndex + 1);
|
|
177
182
|
}
|
|
178
183
|
else {
|
|
179
184
|
// For cases where `Association to T { struct.one, struct.two };` is used instead of `{ struct }`.
|
|
@@ -195,7 +200,7 @@ function checkForeignKeyAccess( parent, refIndex, csnPath, callback ) {
|
|
|
195
200
|
++fkIndex;
|
|
196
201
|
}
|
|
197
202
|
if (!success)
|
|
198
|
-
|
|
203
|
+
noForeignKeyCallback(refIndex + fkIndex);
|
|
199
204
|
}
|
|
200
205
|
}
|
|
201
206
|
|
|
@@ -211,4 +216,4 @@ function validateMixinOnCondition( query, path ) {
|
|
|
211
216
|
forEachGeneric( query.SELECT, 'mixin', validateOnCondition.bind(this), path );
|
|
212
217
|
}
|
|
213
218
|
|
|
214
|
-
module.exports = { validateOnCondition, validateMixinOnCondition,
|
|
219
|
+
module.exports = { validateOnCondition, validateMixinOnCondition, requireForeignKeyAccess };
|
|
@@ -1,136 +1,196 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { hasAnnotationValue, isPersistedOnDatabase, isBuiltinType } = require('../model/csnUtils');
|
|
4
|
+
const { requireForeignKeyAccess } = require('./onConditions');
|
|
5
|
+
const { pathId } = require('../model/csnRefs');
|
|
6
|
+
|
|
7
|
+
const generalQueryProperties = [ 'from', 'columns', 'where', 'groupBy', 'orderBy', 'having', 'limit' ];
|
|
8
|
+
|
|
4
9
|
/**
|
|
5
|
-
*
|
|
6
|
-
*
|
|
10
|
+
* Ensure that all source artifacts and association targets are persisted on the database.
|
|
11
|
+
* Otherwise, we would end up with a JOIN against a non-existent table.
|
|
7
12
|
*
|
|
8
13
|
* Check the given query for:
|
|
9
|
-
* -
|
|
14
|
+
* - Association-traversal over skipped/abstract things
|
|
10
15
|
* - Associations (indirectly) using managed associations without foreign keys
|
|
11
16
|
*
|
|
12
17
|
* Currently checked:
|
|
13
|
-
* - "columns" for something like toF.
|
|
18
|
+
* - "columns" for something like toF.field, where F is skipped. But publishing toF is fine, will be ignored later on
|
|
14
19
|
* - "from" for something like "select from E.toF" where E, F or E AND F are no-db.
|
|
15
20
|
*
|
|
16
|
-
*
|
|
17
21
|
* @param {CSN.Query} query Query to check
|
|
18
22
|
*/
|
|
19
23
|
function checkQueryForNoDBArtifacts( query ) {
|
|
20
24
|
if (isPersistedOnDatabase(this.artifact) && !hasAnnotationValue(this.artifact, '@cds.persistence.table')) {
|
|
21
|
-
const generalQueryProperties = [ 'from', 'columns', 'where', 'groupBy', 'orderBy', 'having', 'limit' ];
|
|
22
25
|
for (const prop of generalQueryProperties) {
|
|
23
26
|
const queryPart = (query.SELECT || query.SET)[prop];
|
|
24
27
|
if (Array.isArray(queryPart)) {
|
|
25
28
|
for (const part of queryPart)
|
|
26
|
-
|
|
29
|
+
checkQueryRef.call(this, part, prop === 'columns');
|
|
27
30
|
}
|
|
28
31
|
else if (typeof queryPart === 'object') {
|
|
29
|
-
|
|
32
|
+
checkQueryRef.call(this, queryPart, prop === 'columns');
|
|
30
33
|
}
|
|
31
34
|
}
|
|
32
35
|
}
|
|
33
36
|
}
|
|
34
37
|
|
|
35
38
|
/**
|
|
36
|
-
*
|
|
39
|
+
* @param {CSN.Element} assoc Definition to check
|
|
40
|
+
* @returns {boolean} True, if there are any foreign keys.
|
|
41
|
+
*/
|
|
42
|
+
function hasForeignKeys( assoc ) {
|
|
43
|
+
if (!assoc || !assoc.keys)
|
|
44
|
+
return false;
|
|
45
|
+
return _hasForeignKeyOrElements.call(this, assoc);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Returns true if the given definition has at least one foreign key or element leaf node.
|
|
37
50
|
*
|
|
38
|
-
* @param {CSN.
|
|
39
|
-
* @returns {
|
|
51
|
+
* @param {CSN.Artifact} def
|
|
52
|
+
* @returns {boolean} True if there are FKs/element leaves.
|
|
40
53
|
*/
|
|
41
|
-
function
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
return c;
|
|
45
|
-
if (def.elements) {
|
|
46
|
-
c += Object.values(def.elements).reduce((acc, e) => {
|
|
47
|
-
acc += leafCount.call(this, e);
|
|
48
|
-
return acc;
|
|
49
|
-
}, 0);
|
|
54
|
+
function _hasForeignKeyOrElements( def ) {
|
|
55
|
+
if (!def) {
|
|
56
|
+
return false;
|
|
50
57
|
}
|
|
51
58
|
else if (def.keys) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
59
|
+
return def.keys.some(e => _hasForeignKeyOrElements.call(this, e._art));
|
|
60
|
+
}
|
|
61
|
+
else if (def.elements) {
|
|
62
|
+
return Object.values(def.elements).some( e => _hasForeignKeyOrElements.call(this, e));
|
|
56
63
|
}
|
|
57
64
|
else if (def.type) {
|
|
58
65
|
if (isBuiltinType(def.type) && !(def.target))
|
|
59
|
-
return
|
|
60
|
-
|
|
66
|
+
return true;
|
|
67
|
+
return _hasForeignKeyOrElements.call(this, this.artifactRef(def.type, null));
|
|
61
68
|
}
|
|
62
|
-
return
|
|
69
|
+
return false;
|
|
63
70
|
}
|
|
64
71
|
|
|
65
72
|
/**
|
|
66
|
-
* Check the given ref for usage of skipped/abstract assoc targets
|
|
73
|
+
* Check the given `obj.ref` for usage of skipped/abstract assoc targets
|
|
67
74
|
*
|
|
68
|
-
* @param {
|
|
75
|
+
* @param {CSN.Column} obj CSN "thing" to check
|
|
69
76
|
* @param {boolean} inColumns True if the ref is part of a from
|
|
70
77
|
*/
|
|
71
|
-
function
|
|
72
|
-
if (!
|
|
78
|
+
function checkQueryRef( obj, inColumns ) {
|
|
79
|
+
if (!obj || obj.$scope === 'alias')
|
|
73
80
|
return;
|
|
74
81
|
|
|
75
|
-
|
|
82
|
+
if (obj.expand || obj.inline)
|
|
83
|
+
_checkExpandInline.call(this, obj);
|
|
84
|
+
|
|
85
|
+
else if (obj.ref && obj._links)
|
|
86
|
+
_checkRef.call(this, obj.ref, obj._links, obj.$path, inColumns);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Run _checkRef on all expand/inline structure leaf nodes.
|
|
91
|
+
* We do so by creating artificial paths that follow expand/inline nodes to their leaves.
|
|
92
|
+
*
|
|
93
|
+
* @param {CSN.Column} obj
|
|
94
|
+
* @param {CSN.Path} previousRefs
|
|
95
|
+
* @param {object[]} previousLinks
|
|
96
|
+
*/
|
|
97
|
+
function _checkExpandInline( obj, previousRefs = [], previousLinks = [] ) {
|
|
98
|
+
if (obj.ref && obj._links) { // There could be anonymous nested "expand".
|
|
99
|
+
previousRefs = previousRefs.concat(obj.ref);
|
|
100
|
+
previousLinks = previousLinks.concat(obj._links);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
if (!obj.expand && !obj.inline) {
|
|
104
|
+
if (obj.ref && obj._links) {
|
|
105
|
+
// `inColumns: true` for expand/inline
|
|
106
|
+
_checkRef.call(this, previousRefs, previousLinks, obj.$path, true);
|
|
107
|
+
}
|
|
108
|
+
return;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
for (const col of obj.expand || obj.inline)
|
|
112
|
+
_checkExpandInline.call(this, col, previousRefs, previousLinks);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* Implementation of checkQueryRef() that works on ref/links arrays instead of a column.
|
|
117
|
+
*
|
|
118
|
+
* @param {CSN.Path} ref
|
|
119
|
+
* @param {object[]} _links
|
|
120
|
+
* @param {CSN.Path} $path
|
|
121
|
+
* @param {boolean} inColumns
|
|
122
|
+
*/
|
|
123
|
+
function _checkRef( ref, _links, $path, inColumns ) {
|
|
124
|
+
if (!ref || !_links )
|
|
125
|
+
return;
|
|
126
|
+
|
|
127
|
+
let nonPersistedTarget = null;
|
|
128
|
+
const isPublishedAssoc = this.csnUtils.isAssocOrComposition(_links[_links.length - 1].art);
|
|
76
129
|
|
|
77
130
|
// Don't check the last element - to allow association publishing in columns
|
|
78
|
-
for (let i = 0; i < (inColumns ?
|
|
79
|
-
const link =
|
|
131
|
+
for (let i = 0; i < (inColumns ? _links.length - 1 : _links.length); i++) {
|
|
132
|
+
const link = _links[i];
|
|
80
133
|
if (!link)
|
|
81
134
|
continue;
|
|
82
|
-
|
|
83
135
|
const { art } = link;
|
|
84
136
|
if (!art)
|
|
85
137
|
continue;
|
|
86
138
|
|
|
87
|
-
const
|
|
88
|
-
const
|
|
89
|
-
const
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
139
|
+
const isLast = i >= _links.length - 1;
|
|
140
|
+
const isUnmanagedOrNoKeys = !art.keys;
|
|
141
|
+
const targetArt = art.target ? this.artifactRef(art.target) : art;
|
|
142
|
+
const pathStep = pathId(ref[i]);
|
|
143
|
+
const name = art.target || pathStep;
|
|
144
|
+
|
|
145
|
+
// If any path-step is not persisted, then all following path steps must only access foreign keys.
|
|
146
|
+
// For example, it could be toF.toG.field, where toG is FK of toF; the FK-only-check would succeed,
|
|
147
|
+
// but we only check "field" in the next iteration, where it is seen as access on a non-skipped
|
|
148
|
+
// entity, hence the need to store if any target is skipped.
|
|
149
|
+
if (!isPersistedOnDatabase(targetArt))
|
|
150
|
+
nonPersistedTarget = { name, pathStep };
|
|
151
|
+
|
|
152
|
+
if (nonPersistedTarget) {
|
|
153
|
+
let isJoinRelevant = isPublishedAssoc || // publishing associations is always join relevant
|
|
154
|
+
isLast || // e.g. FROM targets are always join relevant.
|
|
155
|
+
isUnmanagedOrNoKeys; // unmanaged associations are always join relevant -> no FKs
|
|
156
|
+
|
|
157
|
+
if (!isJoinRelevant) {
|
|
158
|
+
// for managed, published associations with more than one $path-step, only FK
|
|
159
|
+
// access is allowed.
|
|
160
|
+
requireForeignKeyAccess({ ref, _links }, i, () => {
|
|
161
|
+
isJoinRelevant = true;
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
if (isJoinRelevant) {
|
|
166
|
+
const cdsPersistenceSkipped = hasAnnotationValue(targetArt, '@cds.persistence.skip');
|
|
167
|
+
this.error( null, $path, {
|
|
168
|
+
'#': cdsPersistenceSkipped ? 'std' : 'abstract',
|
|
169
|
+
id: nonPersistedTarget.pathStep,
|
|
170
|
+
elemref: { ref },
|
|
171
|
+
name: nonPersistedTarget.name,
|
|
111
172
|
}, {
|
|
112
173
|
std: 'Unexpected “@cds.persistence.skip” annotation on association target $(NAME) of $(ID) in path $(ELEMREF)',
|
|
113
174
|
abstract: 'Unexpected “abstract” association target $(NAME) of $(ID) in path $(ELEMREF)',
|
|
114
175
|
} );
|
|
176
|
+
break; // only one error per path
|
|
115
177
|
}
|
|
116
178
|
}
|
|
179
|
+
|
|
117
180
|
// check managed association to have foreign keys array filled
|
|
118
|
-
if (art.keys &&
|
|
119
|
-
this.error(null,
|
|
120
|
-
obj.$path,
|
|
121
|
-
{ id: pathStep, elemref: obj },
|
|
181
|
+
if (art.keys && !hasForeignKeys.call(this, art)) {
|
|
182
|
+
this.error(null, $path, { id: pathStep, elemref: { ref } },
|
|
122
183
|
'Path step $(ID) of $(ELEMREF) has no foreign keys');
|
|
184
|
+
break; // only one error per path
|
|
123
185
|
}
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
if (j < art.on.length - 2 && art.on[j].ref && art.on[j + 1] === '=' && art.on[j + 2].ref) {
|
|
186
|
+
else if (art.on) {
|
|
187
|
+
for (let j = 0; j < art.on.length - 2; j++) {
|
|
188
|
+
if (art.on[j].ref && art.on[j + 1] === '=' && art.on[j + 2].ref) {
|
|
128
189
|
const [ fwdAssoc, fwdPath ] = getForwardAssociation(pathStep, art.on[j], art.on[j + 2]);
|
|
129
|
-
if (fwdAssoc
|
|
130
|
-
this.error(null,
|
|
131
|
-
{ name: pathStep, elemref: obj, id: fwdPath },
|
|
190
|
+
if (fwdAssoc?.keys && !hasForeignKeys.call(this, fwdAssoc)) {
|
|
191
|
+
this.error(null, $path, { name: pathStep, elemref: { ref }, id: fwdPath },
|
|
132
192
|
'Path step $(NAME) of $(ELEMREF) is a $self comparison with $(ID) that has no foreign keys');
|
|
133
|
-
|
|
193
|
+
break; // only one error per path
|
|
134
194
|
}
|
|
135
195
|
}
|
|
136
196
|
}
|
|
@@ -139,7 +199,7 @@ function checkRef( obj, inColumns ) {
|
|
|
139
199
|
}
|
|
140
200
|
|
|
141
201
|
/**
|
|
142
|
-
* Get the forward association from a
|
|
202
|
+
* Get the forward association from a backlink $self association.
|
|
143
203
|
*
|
|
144
204
|
* @param {string} prefix Name of the association
|
|
145
205
|
* @param {object} lhs Left hand side of the on-condition part
|
|
@@ -18,14 +18,25 @@ function checkSqlAnnotationOnElement( member, memberName, prop, path ) {
|
|
|
18
18
|
this.message('anno-invalid-sql-element', path, { anno: 'sql.prepend' }, 'Annotation $(ANNO) can\'t be used on elements' );
|
|
19
19
|
|
|
20
20
|
if (member['@sql.append']) {
|
|
21
|
-
if (this.artifact.query || this.artifact.projection)
|
|
21
|
+
if (this.artifact.query || this.artifact.projection) {
|
|
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
|
+
}
|
|
24
|
+
else if (this.csnUtils.isStructured(member)) {
|
|
24
25
|
this.message('anno-invalid-sql-struct', path, { anno: 'sql.append' }, 'Annotation $(ANNO) can\'t be used on structured elements' );
|
|
25
|
-
|
|
26
|
+
}
|
|
27
|
+
else if (this.csnUtils.isManagedAssociation(member)) {
|
|
28
|
+
this.message('anno-invalid-sql-assoc', path, { anno: 'sql.append', '#': member.type }, {
|
|
29
|
+
std: 'Annotation $(ANNO) can\'t be used here',
|
|
30
|
+
'cds.Association': 'Annotation $(ANNO) can\'t be used on association elements',
|
|
31
|
+
'cds.Composition': 'Annotation $(ANNO) can\'t be used on composition elements',
|
|
32
|
+
} );
|
|
33
|
+
}
|
|
34
|
+
else if (member.value && !member.value.stored) {
|
|
26
35
|
this.message('anno-invalid-sql-calc', path, { anno: 'sql.append' }, 'Annotation $(ANNO) can\'t be used on calculated elements on read' );
|
|
27
|
-
|
|
36
|
+
}
|
|
37
|
+
else {
|
|
28
38
|
checkValidAnnoValue(member, '@sql.append', path, this.error, this.options);
|
|
39
|
+
}
|
|
29
40
|
}
|
|
30
41
|
}
|
|
31
42
|
|
package/lib/checks/types.js
CHANGED
|
@@ -162,9 +162,9 @@ function errorAboutMissingType( error, path, artifact, name,
|
|
|
162
162
|
let variant = isElement ? 'elm' : 'std';
|
|
163
163
|
if (artifact.value?.stored)
|
|
164
164
|
variant = 'calc';
|
|
165
|
-
error('
|
|
166
|
-
std: '
|
|
167
|
-
elm: '
|
|
165
|
+
error('def-missing-type', path, { art: name, '#': variant }, {
|
|
166
|
+
std: 'Missing type for $(ART)',
|
|
167
|
+
elm: 'Missing type for element $(ART)',
|
|
168
168
|
calc: 'A stored calculated element must have a type',
|
|
169
169
|
});
|
|
170
170
|
}
|
package/lib/checks/utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { isBuiltinType } = require('../model/csnUtils');
|
|
4
|
-
const { RelationalOperators } = require('../transform/
|
|
4
|
+
const { RelationalOperators } = require('../transform/transformUtils');
|
|
5
5
|
/**
|
|
6
6
|
* Prepare the ref steps so that they are loggable
|
|
7
7
|
*
|