@sap/cds-compiler 4.6.2 → 4.7.4
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 +37 -0
- package/bin/cds_update_identifiers.js +6 -2
- package/bin/cdsc.js +1 -1
- package/doc/CHANGELOG_ARCHIVE.md +9 -9
- package/doc/CHANGELOG_BETA.md +6 -0
- package/lib/api/main.js +56 -9
- package/lib/api/options.js +6 -3
- package/lib/api/validate.js +20 -29
- package/lib/base/message-registry.js +27 -3
- package/lib/base/messages.js +8 -3
- package/lib/base/model.js +2 -0
- package/lib/checks/dbFeatureFlags.js +28 -0
- package/lib/checks/elements.js +81 -13
- package/lib/checks/enricher.js +3 -2
- package/lib/checks/validator.js +38 -4
- package/lib/compiler/assert-consistency.js +4 -4
- package/lib/compiler/checks.js +5 -4
- package/lib/compiler/define.js +2 -2
- package/lib/compiler/generate.js +2 -1
- package/lib/compiler/propagator.js +3 -11
- package/lib/compiler/shared.js +2 -1
- package/lib/compiler/tweak-assocs.js +43 -24
- package/lib/edm/annotations/edmJson.js +3 -0
- package/lib/edm/annotations/genericTranslation.js +156 -106
- package/lib/edm/annotations/preprocessAnnotations.js +11 -14
- package/lib/edm/csn2edm.js +27 -24
- package/lib/edm/edm.js +8 -8
- package/lib/edm/edmPreprocessor.js +135 -37
- package/lib/edm/edmUtils.js +20 -7
- package/lib/gen/Dictionary.json +1 -0
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +9 -11
- package/lib/gen/languageParser.js +5942 -5446
- package/lib/json/to-csn.js +7 -114
- package/lib/language/genericAntlrParser.js +106 -48
- package/lib/model/cloneCsn.js +203 -0
- package/lib/model/csnRefs.js +11 -3
- package/lib/model/csnUtils.js +42 -85
- package/lib/optionProcessor.js +2 -2
- package/lib/render/manageConstraints.js +1 -1
- package/lib/render/toCdl.js +133 -88
- package/lib/render/toHdbcds.js +1 -5
- package/lib/render/toSql.js +7 -9
- package/lib/render/utils/common.js +9 -16
- package/lib/transform/addTenantFields.js +277 -102
- package/lib/transform/db/applyTransformations.js +14 -9
- package/lib/transform/db/backlinks.js +2 -1
- package/lib/transform/db/constraints.js +60 -82
- package/lib/transform/db/expansion.js +6 -6
- package/lib/transform/db/featureFlags.js +5 -0
- package/lib/transform/db/flattening.js +4 -4
- package/lib/transform/db/killAnnotations.js +1 -0
- package/lib/transform/db/rewriteCalculatedElements.js +2 -2
- package/lib/transform/db/transformExists.js +12 -0
- package/lib/transform/db/views.js +5 -2
- package/lib/transform/draft/odata.js +7 -6
- package/lib/transform/effective/associations.js +2 -1
- package/lib/transform/effective/main.js +3 -2
- package/lib/transform/effective/types.js +6 -3
- package/lib/transform/forOdata.js +39 -24
- package/lib/transform/forRelationalDB.js +34 -27
- package/lib/transform/localized.js +29 -9
- package/lib/transform/odata/flattening.js +419 -0
- package/lib/transform/odata/toFinalBaseType.js +95 -15
- package/lib/transform/odata/typesExposure.js +9 -7
- package/lib/transform/transformUtils.js +7 -6
- package/lib/transform/translateAssocsToJoins.js +3 -3
- package/lib/utils/objectUtils.js +14 -0
- package/package.json +1 -1
package/lib/render/toSql.js
CHANGED
|
@@ -24,7 +24,7 @@ const { checkCSNVersion } = require('../json/csnVersion');
|
|
|
24
24
|
const { timetrace } = require('../utils/timetrace');
|
|
25
25
|
const { isBetaEnabled, isDeprecatedEnabled } = require('../base/model');
|
|
26
26
|
const { smartFuncId } = require('../sql-identifier');
|
|
27
|
-
const { sortCsn } = require('../
|
|
27
|
+
const { sortCsn } = require('../model/cloneCsn');
|
|
28
28
|
const { manageConstraints, manageConstraint } = require('./manageConstraints');
|
|
29
29
|
const { renderUniqueConstraintString, renderUniqueConstraintDrop, renderUniqueConstraintAdd } = require('./utils/unique');
|
|
30
30
|
const { ModelError, CompilerAssertion } = require('../base/error');
|
|
@@ -104,6 +104,8 @@ class SqlRenderEnvironment {
|
|
|
104
104
|
* @returns {object} Dictionary of artifact-type:artifacts, where artifacts is a dictionary of name:content
|
|
105
105
|
*/
|
|
106
106
|
function toSqlDdl( csn, options, messageFunctions ) {
|
|
107
|
+
const withHanaAssociations = options.withHanaAssociations && options.sqlDialect === 'hana';
|
|
108
|
+
|
|
107
109
|
timetrace.start('SQL rendering');
|
|
108
110
|
const {
|
|
109
111
|
error, warning, info, throwWithAnyError,
|
|
@@ -146,7 +148,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
|
|
|
146
148
|
SET( x) {
|
|
147
149
|
return `(${renderQuery(x, this.env.withIncreasedIndent())})`;
|
|
148
150
|
},
|
|
149
|
-
}
|
|
151
|
+
});
|
|
150
152
|
|
|
151
153
|
function renderExpr( x, env ) {
|
|
152
154
|
return exprRenderer.renderExpr(x, env);
|
|
@@ -610,7 +612,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
|
|
|
610
612
|
result += renderTechnicalConfiguration(art.technicalConfig, childEnv);
|
|
611
613
|
|
|
612
614
|
|
|
613
|
-
if (
|
|
615
|
+
if (withHanaAssociations) {
|
|
614
616
|
const associations = Object.keys(art.elements)
|
|
615
617
|
.map(name => renderAssociationElement(name, art.elements[name], childEnv))
|
|
616
618
|
.filter(s => s !== '')
|
|
@@ -773,10 +775,6 @@ function toSqlDdl( csn, options, messageFunctions ) {
|
|
|
773
775
|
let result = '';
|
|
774
776
|
if (elm.target) {
|
|
775
777
|
result += env.indent;
|
|
776
|
-
const on = (!options.tenantAsColumn || elm.target['@cds.tenant.independent'])
|
|
777
|
-
? elm.on
|
|
778
|
-
: [ { ref: [ 'tenant' ] }, '=', { ref: [ elementName, 'tenant' ] },
|
|
779
|
-
'and', { xpr: elm.on } ];
|
|
780
778
|
if (elm.cardinality) {
|
|
781
779
|
if (isBetaEnabled(options, 'hanaAssocRealCardinality') && elm.cardinality.src && elm.cardinality.src === 1)
|
|
782
780
|
result += 'ONE TO ';
|
|
@@ -793,7 +791,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
|
|
|
793
791
|
}
|
|
794
792
|
result += ' JOIN ';
|
|
795
793
|
result += `${renderArtifactName(elm.target)} AS ${quoteSqlId(elementName)} ON (`;
|
|
796
|
-
result += `${renderExpr(on, env.withSubPath([ 'on' ]))})`;
|
|
794
|
+
result += `${renderExpr(elm.on, env.withSubPath([ 'on' ]))})`;
|
|
797
795
|
}
|
|
798
796
|
return result;
|
|
799
797
|
}
|
|
@@ -1154,7 +1152,7 @@ function toSqlDdl( csn, options, messageFunctions ) {
|
|
|
1154
1152
|
.map(name => renderAssociationElement(name, art.elements[name], childEnv))
|
|
1155
1153
|
.filter(s => s !== '')
|
|
1156
1154
|
.join(',\n');
|
|
1157
|
-
if (associations !== '' &&
|
|
1155
|
+
if (associations !== '' && withHanaAssociations) {
|
|
1158
1156
|
result += `${env.indent}\nWITH ASSOCIATIONS (\n${associations}\n`;
|
|
1159
1157
|
result += `${env.indent})`;
|
|
1160
1158
|
}
|
|
@@ -598,10 +598,9 @@ function withoutCast( xpr ) {
|
|
|
598
598
|
* (no trailing LF, don't indent if inline)
|
|
599
599
|
*
|
|
600
600
|
* @param {ExpressionConfiguration} rendererBase
|
|
601
|
-
* @param {boolean} [adaptPath] If true, `env.path` will be adapted for lists and subExpr.
|
|
602
601
|
* @returns {ExpressionRenderer} Expression rendering utility
|
|
603
602
|
*/
|
|
604
|
-
function createExpressionRenderer( rendererBase
|
|
603
|
+
function createExpressionRenderer( rendererBase ) {
|
|
605
604
|
const renderer = Object.create(rendererBase);
|
|
606
605
|
renderer.visitExpr = visitExpr;
|
|
607
606
|
/**
|
|
@@ -616,7 +615,6 @@ function createExpressionRenderer( rendererBase, adaptPath = false ) {
|
|
|
616
615
|
// are nested. This information is used for adding parentheses around
|
|
617
616
|
// expressions (see `this.xpr()`).
|
|
618
617
|
renderObj.isNestedXpr = false;
|
|
619
|
-
renderObj.adaptPath = adaptPath;
|
|
620
618
|
return renderObj.visitExpr(x);
|
|
621
619
|
};
|
|
622
620
|
/**
|
|
@@ -628,7 +626,6 @@ function createExpressionRenderer( rendererBase, adaptPath = false ) {
|
|
|
628
626
|
const renderObj = Object.create(renderer);
|
|
629
627
|
renderObj.env = env || this?.env;
|
|
630
628
|
renderObj.isNestedXpr = true;
|
|
631
|
-
renderObj.adaptPath = adaptPath;
|
|
632
629
|
return renderObj.visitExpr(x);
|
|
633
630
|
};
|
|
634
631
|
|
|
@@ -654,12 +651,10 @@ function visitExpr( x ) {
|
|
|
654
651
|
// If xpr is part of an array, it's always a nested xpr,
|
|
655
652
|
// e.g. CSN for `(1=1 or 2=2) and 3=3`.
|
|
656
653
|
const tokens = x.map((item, i) => {
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
}
|
|
662
|
-
return this.renderSubExpr(item);
|
|
654
|
+
this.env.path.push( i );
|
|
655
|
+
const result = this.renderSubExpr(item, this.env);
|
|
656
|
+
this.env.path.length -= 1;
|
|
657
|
+
return result;
|
|
663
658
|
});
|
|
664
659
|
return beautifyExprArray(tokens);
|
|
665
660
|
}
|
|
@@ -673,12 +668,10 @@ function visitExpr( x ) {
|
|
|
673
668
|
else if (x.list) {
|
|
674
669
|
// Render as non-nested expr.
|
|
675
670
|
return `(${x.list.map((item, i) => {
|
|
676
|
-
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
}
|
|
681
|
-
return this.renderExpr(item);
|
|
671
|
+
this.env.path.push('list', i);
|
|
672
|
+
const result = this.renderExpr(item, this.env);
|
|
673
|
+
this.env.path.length -= 2;
|
|
674
|
+
return result;
|
|
682
675
|
}).join(', ')})`;
|
|
683
676
|
}
|
|
684
677
|
else if (x.val !== undefined) {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Add tenant field
|
|
1
|
+
// Add tenant field to entities, check validity
|
|
2
2
|
|
|
3
3
|
// Prerequisites:
|
|
4
4
|
|
|
@@ -6,23 +6,23 @@
|
|
|
6
6
|
// - using structure types with unmanaged associations is not supported by the
|
|
7
7
|
// Core Compiler (due to missing ON-rewrite)
|
|
8
8
|
|
|
9
|
-
// TODO
|
|
10
|
-
|
|
11
|
-
// -
|
|
12
|
-
// - disallow use of such a type in entity without MANDT
|
|
9
|
+
// TODO clarify:
|
|
10
|
+
//
|
|
11
|
+
// - do we have to do something for secondary keys?
|
|
13
12
|
|
|
14
13
|
// Implementation remark:
|
|
15
|
-
|
|
14
|
+
//
|
|
16
15
|
// - the functions `forEachDefinition` & friends in csnUtils.js have become quite
|
|
17
16
|
// (too) general and are probably slow → not used here
|
|
18
17
|
|
|
19
18
|
'use strict';
|
|
20
19
|
|
|
21
20
|
const { createMessageFunctions } = require( '../base/messages' );
|
|
22
|
-
const { traverseQuery } = require( '../model/csnRefs' );
|
|
21
|
+
const { csnRefs, traverseQuery, implicitAs } = require( '../model/csnRefs' );
|
|
22
|
+
|
|
23
|
+
const annoTenantIndep = '@cds.tenant.independent';
|
|
23
24
|
|
|
24
|
-
const
|
|
25
|
-
const fieldDef = {
|
|
25
|
+
const tenantDef = {
|
|
26
26
|
key: true,
|
|
27
27
|
type: 'cds.String',
|
|
28
28
|
length: 36,
|
|
@@ -30,119 +30,198 @@ const fieldDef = {
|
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
function addTenantFields( csn, options ) {
|
|
33
|
+
const { error, throwWithError } = createMessageFunctions( options, 'tenant', csn );
|
|
34
|
+
const { tenantDiscriminator } = options;
|
|
35
|
+
const tenantName = tenantDiscriminator === true ? 'tenant' : tenantDiscriminator;
|
|
36
|
+
if (tenantName !== 'tenant') {
|
|
37
|
+
error( 'api-invalid-option', null, {
|
|
38
|
+
'#': 'value2',
|
|
39
|
+
option: 'tenantDiscriminator',
|
|
40
|
+
value: 'tenant',
|
|
41
|
+
rawvalue: true,
|
|
42
|
+
othervalue: tenantName,
|
|
43
|
+
} );
|
|
44
|
+
throwWithError();
|
|
45
|
+
}
|
|
46
|
+
|
|
33
47
|
const { definitions } = csn;
|
|
34
48
|
if (!definitions)
|
|
35
49
|
return csn;
|
|
36
|
-
const {
|
|
50
|
+
const { initDefinition, artifactRef, effectiveType } = csnRefs( csn );
|
|
37
51
|
|
|
52
|
+
const typeCache = new WeakMap();
|
|
38
53
|
const csnPath = [ 'definitions', '' ];
|
|
54
|
+
let independent;
|
|
39
55
|
let projection;
|
|
40
56
|
|
|
41
57
|
for (const name in definitions) {
|
|
42
58
|
const art = definitions[name];
|
|
43
|
-
|
|
44
|
-
continue;
|
|
59
|
+
initDefinition( art );
|
|
45
60
|
csnPath[1] = name;
|
|
61
|
+
independent = art[annoTenantIndep];
|
|
62
|
+
projection = art.query || art.projection && art;
|
|
46
63
|
|
|
47
|
-
if (art
|
|
48
|
-
|
|
49
|
-
|
|
64
|
+
if (art.kind === 'entity') {
|
|
65
|
+
independent = !!independent; // value should not influence message variant
|
|
66
|
+
if (independent && art.includes && !checkIncludes( art ))
|
|
67
|
+
continue;
|
|
68
|
+
handleElements( art );
|
|
69
|
+
if (projection)
|
|
70
|
+
traverseQuery( projection, null, null, handleQuery );
|
|
50
71
|
}
|
|
51
|
-
if (!
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
72
|
+
else if (!independent && independent != null) {
|
|
73
|
+
error( 'tenant-invalid-anno-value', csnPath, { anno: annoTenantIndep, value: independent },
|
|
74
|
+
// eslint-disable-next-line max-len
|
|
75
|
+
'Can\'t add $(ANNO) with value $(VALUE) to a non-entity, which is always tenant-independent' );
|
|
76
|
+
}
|
|
77
|
+
else if (art.includes) {
|
|
78
|
+
independent = art.kind; // might be used for message variant
|
|
79
|
+
checkIncludes( art ); // recompile should work
|
|
80
|
+
}
|
|
81
|
+
else if (projection) { // events - TODO: mention in doc
|
|
82
|
+
independent = art.kind; // might be used for message variant
|
|
83
|
+
// recompile should work: no new `tenant` source element for `select *`
|
|
55
84
|
traverseQuery( projection, null, null, handleQuery );
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
// Finally add the `tenant` element (do separately in order not to confuse
|
|
88
|
+
// the cache of csnRefs):
|
|
89
|
+
for (const name in definitions) {
|
|
90
|
+
const art = definitions[name];
|
|
91
|
+
if (isTenantDepEntity( art ))
|
|
92
|
+
art.elements = { [tenantName]: { ...tenantDef }, ...art.elements };
|
|
93
|
+
// consider non-enumerable `elements` of subqueries if that is supported
|
|
56
94
|
}
|
|
57
95
|
|
|
58
|
-
(csn.extensions || []).forEach( (ext, idx) => {
|
|
59
|
-
const tenant = ext.elements?.[
|
|
96
|
+
(csn.extensions || []).forEach( ( ext, idx ) => {
|
|
97
|
+
const tenant = ext.elements?.[tenantName];
|
|
60
98
|
const name = ext.annotate || ext.extend; // extend should not happen
|
|
61
|
-
if (tenant && definitions[name]
|
|
62
|
-
error(
|
|
63
|
-
{ name:
|
|
99
|
+
if (tenant && isTenantDepEntity( definitions[name] )) {
|
|
100
|
+
error( 'tenant-unexpected-ext', [ 'extensions', idx, 'elements', 'tenant' ],
|
|
101
|
+
{ name: tenantName },
|
|
64
102
|
'Can\'t annotate element $(NAME) of a tenant-dependent entity' );
|
|
65
103
|
}
|
|
66
104
|
} );
|
|
67
105
|
|
|
68
106
|
throwWithError();
|
|
107
|
+
csn.meta ??= {};
|
|
108
|
+
csn.meta.tenantDiscriminator = tenantName;
|
|
69
109
|
return csn; // input CSN changed by side effect
|
|
70
110
|
|
|
111
|
+
function checkIncludes( art ) {
|
|
112
|
+
const names = art.includes
|
|
113
|
+
.filter( name => isTenantDepEntity( csn.definitions[name] ) );
|
|
114
|
+
if (names.length) {
|
|
115
|
+
error( 'tenant-invalid-include', csnPath, { names }, {
|
|
116
|
+
// eslint-disable-next-line max-len
|
|
117
|
+
std: 'Can\'t include the tenant-dependent entities $(NAMES) into a tenant-independent definition',
|
|
118
|
+
// eslint-disable-next-line max-len
|
|
119
|
+
one: 'Can\'t include the tenant-dependent entity $(NAMES) into a tenant-independent definition',
|
|
120
|
+
} );
|
|
121
|
+
}
|
|
122
|
+
return !names.length;
|
|
123
|
+
}
|
|
124
|
+
|
|
71
125
|
function handleElements( art ) {
|
|
72
126
|
const { elements } = art;
|
|
73
|
-
if (elements[
|
|
74
|
-
error(
|
|
75
|
-
|
|
76
|
-
|
|
127
|
+
if (elements[tenantName]) {
|
|
128
|
+
error( 'tenant-unexpected-element', [ ...csnPath, 'elements', tenantName ],
|
|
129
|
+
{ name: tenantName, option: 'tenantDiscriminator' },
|
|
130
|
+
'Can\'t have entity with element $(NAME) when using option $(OPTION)' );
|
|
77
131
|
}
|
|
78
|
-
if (!Object.values( elements ).some( e => e.key )) {
|
|
79
|
-
error(
|
|
132
|
+
else if (!independent && !Object.values( elements ).some( e => e.key )) {
|
|
133
|
+
error( 'tenant-expecting-key', csnPath, {},
|
|
80
134
|
'There must be a key in a tenant-dependent entity' );
|
|
81
|
-
return false;
|
|
82
135
|
}
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
136
|
+
else {
|
|
137
|
+
traverse( art, handleAssociations );
|
|
138
|
+
}
|
|
86
139
|
}
|
|
87
140
|
|
|
141
|
+
// Queries --------------------------------------------------------------------
|
|
142
|
+
|
|
88
143
|
function handleQuery( query ) {
|
|
89
144
|
// TODO: errors are temporary: start with simple projections only = no better
|
|
90
|
-
// message $location necessary yet
|
|
91
|
-
if (!projection
|
|
92
|
-
return;
|
|
93
|
-
if (query.ref) {
|
|
94
|
-
if ((query.as || implicitAs( query.ref )) === fieldName) {
|
|
95
|
-
error( null, csnPath, { name: fieldName },
|
|
96
|
-
'Can\'t have a table alias named $(NAME) in a tenant-dependent entity' );
|
|
97
|
-
}
|
|
145
|
+
// message $location necessary yet - better: set `name` in csnRefs
|
|
146
|
+
if (!projection || query.ref && handleQuerySource( query ))
|
|
98
147
|
return;
|
|
99
|
-
}
|
|
100
148
|
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
'Can\'t add tenant columns to non-simple query entities' );
|
|
149
|
+
if (query !== projection && !independent) {
|
|
150
|
+
error( 'tenant-unsupported-query', csnPath, {},
|
|
151
|
+
'Can\'t yet have tenant-dependent non-simple query entities' );
|
|
105
152
|
projection = null;
|
|
106
153
|
return;
|
|
107
154
|
}
|
|
108
155
|
|
|
109
156
|
if (query.projection)
|
|
110
157
|
csnPath.push( 'projection' );
|
|
111
|
-
else
|
|
158
|
+
else if (query.SELECT)
|
|
112
159
|
csnPath.push( 'query', 'SELECT' );
|
|
160
|
+
else
|
|
161
|
+
return; // query.SET or query.join
|
|
113
162
|
|
|
163
|
+
const select = query.SELECT || query.projection;
|
|
114
164
|
if (select.mixin)
|
|
115
|
-
|
|
116
|
-
if (
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
165
|
+
checkMixins( select.mixin );
|
|
166
|
+
if (!independent) {
|
|
167
|
+
if (select.excluding)
|
|
168
|
+
checkExcluding( select.excluding );
|
|
169
|
+
if (select.columns)
|
|
170
|
+
handleColumns( select.columns );
|
|
171
|
+
// TODO: when we allow subqueries, we must also check for published in redirected assocs
|
|
172
|
+
// TODO: for subqueries, we might need to adapt the inferred elements
|
|
173
|
+
// TODO: where exists ref -
|
|
174
|
+
// TODO: select and query clauses, especially with aggregation functions
|
|
175
|
+
handleGroupBy( select );
|
|
176
|
+
}
|
|
177
|
+
else if (query !== projection && select.columns) {
|
|
178
|
+
checkColumnCasts( select.columns );
|
|
179
|
+
}
|
|
124
180
|
csnPath.length = 2;
|
|
125
181
|
}
|
|
126
182
|
|
|
127
|
-
function
|
|
183
|
+
function handleQuerySource( query ) {
|
|
184
|
+
if (independent) {
|
|
185
|
+
const art = query.ref[0]; // yes, the base
|
|
186
|
+
if (csn.definitions[art][annoTenantIndep])
|
|
187
|
+
return true;
|
|
188
|
+
error( 'tenant-invalid-query-source', csnPath, { art, '#': independent }, {
|
|
189
|
+
std: 'Can\'t use a tenant-dependent query source $(ART) in a tenant-independent entity',
|
|
190
|
+
event: 'Can\'t use a tenant-dependent query source $(ART) in an event',
|
|
191
|
+
} );
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
if (query !== (projection.SELECT || projection.projection)?.from) // with `join`
|
|
195
|
+
return false;
|
|
196
|
+
if ((query.as || implicitAs( query.ref )) === tenantName) {
|
|
197
|
+
error( 'tenant-invalid-alias-name', csnPath,
|
|
198
|
+
{ name: tenantName, '#': (query.as ? 'std' : 'implicit') } );
|
|
199
|
+
}
|
|
200
|
+
const art = artifactRef.from( query );
|
|
201
|
+
if (art[annoTenantIndep]) {
|
|
202
|
+
error( 'tenant-expecting-tenant-source', csnPath, { art: query },
|
|
203
|
+
// TODO: better the final entity name of assoc navigation in FROM
|
|
204
|
+
// eslint-disable-next-line max-len
|
|
205
|
+
'Expecting the query source $(ART) to be tenant-dependent for a tenant-dependent query entity' );
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function checkMixins( mixin ) {
|
|
128
211
|
csnPath.push( 'mixin', '' );
|
|
129
212
|
for (const name in mixin) {
|
|
130
213
|
csnPath[csnPath.length - 1] = name;
|
|
131
|
-
if (name
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
else {
|
|
135
|
-
error( null, csnPath, { name },
|
|
136
|
-
'Can\'t define a mixin named $(NAME) in a tenant-dependent entity' );
|
|
137
|
-
}
|
|
214
|
+
if (name === tenantName && !independent)
|
|
215
|
+
error( 'tenant-invalid-alias-name', csnPath, { name, '#': 'mixin' } );
|
|
216
|
+
handleAssociations( mixin[name], null );
|
|
138
217
|
}
|
|
139
218
|
csnPath.length -= 2;
|
|
140
219
|
}
|
|
141
220
|
|
|
142
221
|
function checkExcluding( excludeList ) {
|
|
143
|
-
if (excludeList.includes(
|
|
144
|
-
error(
|
|
145
|
-
'Can\'t exclude $(NAME) from query source' );
|
|
222
|
+
if (excludeList.includes( tenantName )) {
|
|
223
|
+
error( 'tenant-invalid-excluding', csnPath, { name: tenantName },
|
|
224
|
+
'Can\'t exclude $(NAME) from the query source of a tenant-dependent entity' );
|
|
146
225
|
}
|
|
147
226
|
}
|
|
148
227
|
|
|
@@ -153,7 +232,7 @@ function addTenantFields( csn, options ) {
|
|
|
153
232
|
// already contains a GROUP BY. And anyway: if we miss to add GROUP BY MANDT,
|
|
154
233
|
// the database will complain → no safetly risk.
|
|
155
234
|
if (select.groupBy)
|
|
156
|
-
select.groupBy.unshift( { ref: [
|
|
235
|
+
select.groupBy.unshift( { ref: [ tenantName ] } );
|
|
157
236
|
}
|
|
158
237
|
|
|
159
238
|
function handleColumns( columns ) {
|
|
@@ -162,65 +241,161 @@ function addTenantFields( csn, options ) {
|
|
|
162
241
|
for (const col of columns) {
|
|
163
242
|
++csnPath[csnPath.length - 1];
|
|
164
243
|
if (col.expand || col.inline) {
|
|
165
|
-
error(
|
|
244
|
+
error( 'tenant-unsupported-expand-inline', csnPath, {},
|
|
166
245
|
'Can\'t use expand/inline in a tenant-dependent entity' );
|
|
167
246
|
}
|
|
168
247
|
if (col.key != null) // yes, also with key: false
|
|
169
248
|
specifiedKey = true;
|
|
170
|
-
|
|
171
|
-
addToCondition( col.cast, col.as || implicitAs( col.ref ) );
|
|
249
|
+
// REDIRECTED TO: also check new target here? (main query: already checked via elements)
|
|
172
250
|
}
|
|
173
251
|
csnPath.length -= 2;
|
|
174
252
|
columns.unshift( specifiedKey
|
|
175
|
-
? { key: true, ref: [
|
|
176
|
-
: { ref: [
|
|
253
|
+
? { key: true, ref: [ tenantName ] }
|
|
254
|
+
: { ref: [ tenantName ] } );
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function checkColumnCasts( columns, prop = 'columns' ) {
|
|
258
|
+
csnPath.push( prop, -1 );
|
|
259
|
+
for (const col of columns) {
|
|
260
|
+
++csnPath[csnPath.length - 1];
|
|
261
|
+
if (col.cast?.target)
|
|
262
|
+
handleAssociations( col.cast, null );
|
|
263
|
+
else if (col.expand)
|
|
264
|
+
checkColumnCasts( col.expand, 'expand' );
|
|
265
|
+
else if (col.inline)
|
|
266
|
+
checkColumnCasts( col.inline, 'inline' );
|
|
267
|
+
}
|
|
268
|
+
csnPath.length -= 2;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
// Associations ---------------------------------------------------------------
|
|
272
|
+
|
|
273
|
+
function handleAssociations( elem, afterRecursion ) {
|
|
274
|
+
if (afterRecursion != null)
|
|
275
|
+
return null;
|
|
276
|
+
|
|
277
|
+
if (elem.target) {
|
|
278
|
+
if (!csn.definitions[elem.target][annoTenantIndep]) {
|
|
279
|
+
if (independent)
|
|
280
|
+
error( 'tenant-invalid-target', csnPath, { target: elem.target } );
|
|
281
|
+
}
|
|
282
|
+
else if (!independent && isComposition( elem )) {
|
|
283
|
+
error( 'tenant-invalid-composition', csnPath, { target: elem.target } );
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
else if (elem.type && (independent || !elem.elements && !elem.items)) {
|
|
287
|
+
// check type, but not with expanded elements in dependent entity, because
|
|
288
|
+
// composition could have redirected tenant-dependent target
|
|
289
|
+
const dep = typeDependency( artifactRef( elem.type, null ) );
|
|
290
|
+
if (independent) {
|
|
291
|
+
if (!dep || dep === 'Composition')
|
|
292
|
+
return true; // check elements (assocs could be redirected)
|
|
293
|
+
error( 'tenant-invalid-target', csnPath, { type: elem.type, '#': 'type' } );
|
|
294
|
+
}
|
|
295
|
+
else if (dep && dep !== 'dependent') {
|
|
296
|
+
error( 'tenant-invalid-composition', csnPath, { type: elem.type, '#': 'type' } );
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
return true;
|
|
301
|
+
}
|
|
302
|
+
return null;
|
|
177
303
|
}
|
|
178
304
|
|
|
179
|
-
|
|
305
|
+
/**
|
|
306
|
+
* Returns “type dependency”, a string, for type `assoc`:
|
|
307
|
+
*
|
|
308
|
+
* - '': type does not contain associations other than non-composition associations to
|
|
309
|
+
* tenant-independent entities
|
|
310
|
+
* - 'Composition': type contains associations (and at least one composition) to
|
|
311
|
+
* tenant-independent entities, and no associations to tenant-dependent entities
|
|
312
|
+
* - 'dependent': type contains associations, at least one to a tenant-dependent entity,
|
|
313
|
+
* but no compositions to tenant-independent entities
|
|
314
|
+
* - 'ERR': type contains associations, at least one to a tenant-dependent entity,
|
|
315
|
+
* and at least one composition to a tenant-independent entity
|
|
316
|
+
*
|
|
317
|
+
* Type references are followed, but only without sibling `elements` or `items`.
|
|
318
|
+
*/
|
|
319
|
+
function typeDependency( assoc ) {
|
|
320
|
+
if (!assoc || !(assoc = effectiveType( assoc )))
|
|
321
|
+
return '';
|
|
322
|
+
const assocDep = typeCache.get( assoc );
|
|
323
|
+
if (assocDep != null)
|
|
324
|
+
return assocDep;
|
|
325
|
+
let parentDep = '';
|
|
326
|
+
traverse( assoc, typeCallback );
|
|
327
|
+
return parentDep;
|
|
328
|
+
|
|
329
|
+
function typeCallback( type, savedDep ) {
|
|
330
|
+
let currentDep = typeCache.get( type );
|
|
331
|
+
if (currentDep != null) {
|
|
332
|
+
// nothing
|
|
333
|
+
}
|
|
334
|
+
else if (savedDep != null) {
|
|
335
|
+
currentDep = parentDep;
|
|
336
|
+
parentDep = savedDep;
|
|
337
|
+
}
|
|
338
|
+
else if (type.target) {
|
|
339
|
+
const annoDep = !csn.definitions[type.target][annoTenantIndep];
|
|
340
|
+
currentDep = (annoDep) ? 'dependent' : isComposition( type ) && 'Composition';
|
|
341
|
+
}
|
|
342
|
+
else if (type.elements || type.items) {
|
|
343
|
+
savedDep = parentDep;
|
|
344
|
+
parentDep = '';
|
|
345
|
+
return savedDep || ''; // recurse
|
|
346
|
+
}
|
|
347
|
+
else if (type.type) {
|
|
348
|
+
currentDep = typeDependency( artifactRef( type.type, null ) );
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
currentDep = '';
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
typeCache.set( type, currentDep );
|
|
355
|
+
if (!currentDep || !parentDep)
|
|
356
|
+
parentDep ||= currentDep;
|
|
357
|
+
else if (currentDep !== parentDep)
|
|
358
|
+
parentDep = 'ERR';
|
|
359
|
+
return null; // do not (further) recurse
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
// Generic functions ----------------------------------------------------------
|
|
364
|
+
|
|
365
|
+
function traverse( elem, callback ) {
|
|
366
|
+
const recurse = callback( elem, null );
|
|
367
|
+
if (recurse == null)
|
|
368
|
+
return;
|
|
180
369
|
const { elements } = elem;
|
|
181
370
|
if (elements) {
|
|
182
371
|
csnPath.push( 'elements', '' );
|
|
183
372
|
for (const name in elements) {
|
|
184
373
|
csnPath[csnPath.length - 1] = name;
|
|
185
|
-
|
|
374
|
+
traverse( elements[name], callback );
|
|
186
375
|
}
|
|
187
376
|
csnPath.length -= 2;
|
|
188
377
|
}
|
|
189
|
-
else if (elem.target) {
|
|
190
|
-
if (elem.on) {
|
|
191
|
-
addToCondition( elem, csnPath[csnPath.length - 1] );
|
|
192
|
-
}
|
|
193
|
-
else {
|
|
194
|
-
error( null, csnPath, {},
|
|
195
|
-
'Can\'t yet use managed associations in a tenant-dependent entity' );
|
|
196
|
-
}
|
|
197
|
-
}
|
|
198
378
|
else if (elem.items) {
|
|
199
379
|
csnPath.push( 'items' );
|
|
200
|
-
|
|
380
|
+
traverse( elem.items, callback );
|
|
201
381
|
--csnPath.length;
|
|
202
382
|
}
|
|
383
|
+
callback( elem, recurse );
|
|
203
384
|
}
|
|
204
385
|
|
|
205
|
-
function
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
'and',
|
|
214
|
-
// TODO: avoid (...) for standard AND-ed EQ-comparisons ?
|
|
215
|
-
{ xpr: elem.on },
|
|
216
|
-
];
|
|
386
|
+
function isComposition( assoc ) {
|
|
387
|
+
while (assoc && assoc.type !== 'cds.Association') {
|
|
388
|
+
const { type } = assoc;
|
|
389
|
+
if (type === 'cds.Composition')
|
|
390
|
+
return true;
|
|
391
|
+
assoc = artifactRef( type, null );
|
|
392
|
+
}
|
|
393
|
+
return false;
|
|
217
394
|
}
|
|
218
395
|
}
|
|
219
396
|
|
|
220
|
-
function
|
|
221
|
-
|
|
222
|
-
const id = (typeof item === 'string') ? item : item.id;
|
|
223
|
-
return id.substring( id.lastIndexOf('.') + 1 );
|
|
397
|
+
function isTenantDepEntity( art ) {
|
|
398
|
+
return art?.kind === 'entity' && !art[annoTenantIndep];
|
|
224
399
|
}
|
|
225
400
|
|
|
226
401
|
module.exports = {
|
|
@@ -307,6 +307,16 @@ function applyTransformationsOnDictionary( dictionary, customTransformers = {},
|
|
|
307
307
|
* @returns {object} transformed node
|
|
308
308
|
*/
|
|
309
309
|
function transformExpression( parent, propName, transformers, path = [] ) {
|
|
310
|
+
const callT = (t, cpn, child) => {
|
|
311
|
+
const ct = t[cpn];
|
|
312
|
+
if (ct) {
|
|
313
|
+
const ppn = propName;
|
|
314
|
+
if (Array.isArray(ct))
|
|
315
|
+
ct.forEach(cti => cti(child, cpn, child[cpn], path, parent, ppn));
|
|
316
|
+
else
|
|
317
|
+
ct(child, cpn, child[cpn], path, parent, ppn);
|
|
318
|
+
}
|
|
319
|
+
};
|
|
310
320
|
if (propName != null) {
|
|
311
321
|
const child = parent[propName];
|
|
312
322
|
if (!child || typeof child !== 'object' ||
|
|
@@ -319,15 +329,10 @@ function transformExpression( parent, propName, transformers, path = [] ) {
|
|
|
319
329
|
}
|
|
320
330
|
else {
|
|
321
331
|
for (const cpn of Object.getOwnPropertyNames( child )) {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
ct.forEach(cti => cti(child, cpn, child[cpn], path, parent, ppn));
|
|
327
|
-
|
|
328
|
-
else
|
|
329
|
-
ct(child, cpn, child[cpn], path, parent, ppn);
|
|
330
|
-
}
|
|
332
|
+
if (Array.isArray(transformers))
|
|
333
|
+
transformers.forEach(t => callT(t, cpn, child));
|
|
334
|
+
else
|
|
335
|
+
callT(transformers, cpn, child);
|
|
331
336
|
transformExpression(child, cpn, transformers, path);
|
|
332
337
|
}
|
|
333
338
|
}
|