@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.
Files changed (69) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/bin/cds_update_identifiers.js +6 -2
  3. package/bin/cdsc.js +1 -1
  4. package/doc/CHANGELOG_ARCHIVE.md +9 -9
  5. package/doc/CHANGELOG_BETA.md +6 -0
  6. package/lib/api/main.js +56 -9
  7. package/lib/api/options.js +6 -3
  8. package/lib/api/validate.js +20 -29
  9. package/lib/base/message-registry.js +27 -3
  10. package/lib/base/messages.js +8 -3
  11. package/lib/base/model.js +2 -0
  12. package/lib/checks/dbFeatureFlags.js +28 -0
  13. package/lib/checks/elements.js +81 -13
  14. package/lib/checks/enricher.js +3 -2
  15. package/lib/checks/validator.js +38 -4
  16. package/lib/compiler/assert-consistency.js +4 -4
  17. package/lib/compiler/checks.js +5 -4
  18. package/lib/compiler/define.js +2 -2
  19. package/lib/compiler/generate.js +2 -1
  20. package/lib/compiler/propagator.js +3 -11
  21. package/lib/compiler/shared.js +2 -1
  22. package/lib/compiler/tweak-assocs.js +43 -24
  23. package/lib/edm/annotations/edmJson.js +3 -0
  24. package/lib/edm/annotations/genericTranslation.js +156 -106
  25. package/lib/edm/annotations/preprocessAnnotations.js +11 -14
  26. package/lib/edm/csn2edm.js +27 -24
  27. package/lib/edm/edm.js +8 -8
  28. package/lib/edm/edmPreprocessor.js +135 -37
  29. package/lib/edm/edmUtils.js +20 -7
  30. package/lib/gen/Dictionary.json +1 -0
  31. package/lib/gen/language.checksum +1 -1
  32. package/lib/gen/language.interp +9 -11
  33. package/lib/gen/languageParser.js +5942 -5446
  34. package/lib/json/to-csn.js +7 -114
  35. package/lib/language/genericAntlrParser.js +106 -48
  36. package/lib/model/cloneCsn.js +203 -0
  37. package/lib/model/csnRefs.js +11 -3
  38. package/lib/model/csnUtils.js +42 -85
  39. package/lib/optionProcessor.js +2 -2
  40. package/lib/render/manageConstraints.js +1 -1
  41. package/lib/render/toCdl.js +133 -88
  42. package/lib/render/toHdbcds.js +1 -5
  43. package/lib/render/toSql.js +7 -9
  44. package/lib/render/utils/common.js +9 -16
  45. package/lib/transform/addTenantFields.js +277 -102
  46. package/lib/transform/db/applyTransformations.js +14 -9
  47. package/lib/transform/db/backlinks.js +2 -1
  48. package/lib/transform/db/constraints.js +60 -82
  49. package/lib/transform/db/expansion.js +6 -6
  50. package/lib/transform/db/featureFlags.js +5 -0
  51. package/lib/transform/db/flattening.js +4 -4
  52. package/lib/transform/db/killAnnotations.js +1 -0
  53. package/lib/transform/db/rewriteCalculatedElements.js +2 -2
  54. package/lib/transform/db/transformExists.js +12 -0
  55. package/lib/transform/db/views.js +5 -2
  56. package/lib/transform/draft/odata.js +7 -6
  57. package/lib/transform/effective/associations.js +2 -1
  58. package/lib/transform/effective/main.js +3 -2
  59. package/lib/transform/effective/types.js +6 -3
  60. package/lib/transform/forOdata.js +39 -24
  61. package/lib/transform/forRelationalDB.js +34 -27
  62. package/lib/transform/localized.js +29 -9
  63. package/lib/transform/odata/flattening.js +419 -0
  64. package/lib/transform/odata/toFinalBaseType.js +95 -15
  65. package/lib/transform/odata/typesExposure.js +9 -7
  66. package/lib/transform/transformUtils.js +7 -6
  67. package/lib/transform/translateAssocsToJoins.js +3 -3
  68. package/lib/utils/objectUtils.js +14 -0
  69. package/package.json +1 -1
@@ -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('../json/to-csn');
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
- }, true);
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 (options.sqlDialect === 'hana' && options.withHanaAssociations) {
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 !== '' && options.sqlDialect === 'hana' && options.withHanaAssociations) {
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, adaptPath = false ) {
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
- if (this.adaptPath) {
658
- // We want to keep the prototype of the original env.
659
- const env = Object.assign(Object.create(Object.getPrototypeOf(this.env || {})), this.env, { path: [ ...this.env.path, i ] });
660
- return this.renderSubExpr(item, env);
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
- if (this.adaptPath) {
677
- // We want to keep the prototype of the original env.
678
- const env = Object.assign(Object.create(Object.getPrototypeOf(this.env || {})), this.env, { path: [ ...this.env.path, 'list', i ] });
679
- return this.renderExpr(item, env);
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 MANDT to entities
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 entities without MANDT:
10
-
11
- // - cache whether structure type contains (managed) association to entity with MANDT
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 fieldName = 'tenant';
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 { error, throwWithError } = createMessageFunctions( options, 'tenant', csn );
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
- if (art?.kind !== 'entity')
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['@cds.tenant.independent'] != null) {
48
- error( null, csnPath, { anno: '@cds.tenant.independent' },
49
- 'Can\'t yet add annotation $(ANNO) to an entity' );
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 (!handleElements( art ))
52
- continue;
53
- projection = art.query || art.projection && art;
54
- if (projection)
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?.[fieldName];
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]?.kind === 'entity') { // TODO: ok for tenant-independent
62
- error( null, [ 'extensions', idx, 'elements', 'tenant' ],
63
- { name: fieldName },
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[fieldName]) {
74
- error( null, [ ...csnPath, 'elements', fieldName ], { name: fieldName },
75
- 'Can\'t add tenant field to entity having an element $(NAME)' );
76
- return false;
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( null, csnPath, {},
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
- handleAssociations( art );
84
- art.elements = { [fieldName]: { ...fieldDef }, ...elements };
85
- return true;
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) // error already reported
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
- const select = query.SELECT || query.projection;
102
- if (query.SET || query !== projection || !select?.from?.ref) {
103
- error( null, csnPath, {},
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
- handleMixins( select.mixin );
116
- if (select.excluding)
117
- checkExcluding( select.excluding );
118
- if (select.columns)
119
- handleColumns( select.columns );
120
- // TODO: for subqueries, we might need to adapt the inferred elements
121
- // TODO: where exists ref -
122
- // TODO: select and query clauses, especially with aggregation functions
123
- handleGroupBy( select );
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 handleMixins( mixin ) {
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 !== fieldName) {
132
- addToCondition( mixin[name], name );
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( fieldName )) {
144
- error( null, csnPath, { name: fieldName },
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: [ fieldName ] } );
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( null, csnPath, {},
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
- if (col.cast?.on) // REDIRECTED TO with explicit ON - TODO (low prio): less $self
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: [ fieldName ] }
176
- : { ref: [ fieldName ] } );
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
- function handleAssociations( elem ) {
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
- handleAssociations( elements[name] );
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
- handleAssociations( elem.items );
380
+ traverse( elem.items, callback );
201
381
  --csnPath.length;
202
382
  }
383
+ callback( elem, recurse );
203
384
  }
204
385
 
205
- function addToCondition( elem, assoc ) {
206
- if (!elem.on)
207
- return;
208
- const withSelf = csnPath.length > 4;
209
- elem.on = [
210
- { ref: [ assoc, fieldName ] }, // TODO: consider assoc name starting with '$'
211
- '=',
212
- { ref: (withSelf) ? [ '$self', fieldName ] : [ fieldName ] },
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 implicitAs( ref ) {
221
- const item = ref[ref.length - 1];
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
- const ct = transformers[cpn];
323
- if (ct) {
324
- const ppn = propName;
325
- if (Array.isArray(ct))
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
  }