@sap/cds-compiler 4.1.2 → 4.2.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 +107 -1
- package/bin/cdsc.js +6 -3
- package/doc/CHANGELOG_BETA.md +5 -0
- package/doc/CHANGELOG_DEPRECATED.md +15 -0
- package/lib/api/main.js +2 -2
- package/lib/api/options.js +2 -2
- package/lib/api/validate.js +24 -24
- package/lib/base/message-registry.js +41 -6
- package/lib/base/messages.js +7 -0
- package/lib/base/model.js +38 -8
- package/lib/checks/elements.js +11 -10
- package/lib/checks/manyNavigations.js +33 -0
- package/lib/checks/onConditions.js +5 -2
- package/lib/checks/queryNoDbArtifacts.js +2 -3
- package/lib/checks/selectItems.js +4 -55
- package/lib/checks/utils.js +3 -2
- package/lib/checks/validator.js +3 -1
- package/lib/compiler/.eslintrc.json +2 -1
- package/lib/compiler/assert-consistency.js +27 -24
- package/lib/compiler/base.js +6 -2
- package/lib/compiler/builtins.js +34 -34
- package/lib/compiler/checks.js +179 -208
- package/lib/compiler/classes.js +2 -2
- package/lib/compiler/cycle-detector.js +6 -6
- package/lib/compiler/define.js +66 -45
- package/lib/compiler/extend.js +81 -72
- package/lib/compiler/finalize-parse-cdl.js +26 -26
- package/lib/compiler/generate.js +61 -45
- package/lib/compiler/index.js +47 -49
- package/lib/compiler/kick-start.js +8 -7
- package/lib/compiler/moduleLayers.js +1 -1
- package/lib/compiler/populate.js +42 -35
- package/lib/compiler/propagator.js +6 -6
- package/lib/compiler/resolve.js +170 -126
- package/lib/compiler/shared.js +122 -45
- package/lib/compiler/tweak-assocs.js +93 -40
- package/lib/compiler/utils.js +15 -12
- package/lib/edm/.eslintrc.json +40 -1
- package/lib/edm/annotations/genericTranslation.js +721 -707
- package/lib/edm/annotations/preprocessAnnotations.js +88 -77
- package/lib/edm/csn2edm.js +389 -378
- package/lib/edm/edm.js +678 -772
- package/lib/edm/edmAnnoPreprocessor.js +132 -146
- package/lib/edm/edmInboundChecks.js +29 -27
- package/lib/edm/edmPreprocessor.js +686 -646
- package/lib/edm/edmUtils.js +277 -296
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +1 -1
- package/lib/gen/languageParser.js +1253 -1276
- package/lib/json/from-csn.js +34 -4
- package/lib/json/to-csn.js +4 -4
- package/lib/language/language.g4 +2 -5
- package/lib/main.d.ts +61 -1
- package/lib/model/csnUtils.js +31 -2
- package/lib/model/revealInternalProperties.js +1 -1
- package/lib/modelCompare/compare.js +37 -2
- package/lib/modelCompare/utils/filter.js +1 -1
- package/lib/optionProcessor.js +15 -3
- package/lib/render/toCdl.js +30 -4
- package/lib/render/toSql.js +5 -9
- package/lib/render/utils/common.js +8 -6
- package/lib/transform/db/applyTransformations.js +1 -1
- package/lib/transform/db/cdsPersistence.js +1 -1
- package/lib/transform/db/constraints.js +47 -17
- package/lib/transform/db/expansion.js +133 -50
- package/lib/transform/db/flattening.js +75 -7
- package/lib/transform/forOdata.js +4 -1
- package/lib/transform/forRelationalDB.js +80 -62
- package/lib/transform/localized.js +91 -54
- package/lib/transform/transformUtils.js +9 -10
- package/lib/utils/file.js +7 -7
- package/lib/utils/moduleResolve.js +210 -121
- package/lib/utils/objectUtils.js +1 -1
- package/package.json +5 -5
package/lib/compiler/shared.js
CHANGED
|
@@ -20,8 +20,8 @@ const {
|
|
|
20
20
|
artifactRefLocation,
|
|
21
21
|
} = require('./utils');
|
|
22
22
|
|
|
23
|
-
const $inferred = Symbol.for('cds.$inferred');
|
|
24
|
-
const $location = Symbol.for('cds.$location');
|
|
23
|
+
const $inferred = Symbol.for( 'cds.$inferred' );
|
|
24
|
+
const $location = Symbol.for( 'cds.$location' );
|
|
25
25
|
|
|
26
26
|
/**
|
|
27
27
|
* Main export function of this file. Attach "resolve" functions shared for phase
|
|
@@ -61,6 +61,7 @@ function fns( model ) {
|
|
|
61
61
|
lexical: userBlock,
|
|
62
62
|
dynamic: modelDefinitions,
|
|
63
63
|
notFound: undefinedDefinition,
|
|
64
|
+
accept: acceptRealArtifact,
|
|
64
65
|
},
|
|
65
66
|
_extensions: {
|
|
66
67
|
isMainRef: 'all',
|
|
@@ -153,9 +154,17 @@ function fns( model ) {
|
|
|
153
154
|
notFound: undefinedVariable,
|
|
154
155
|
param: paramUnsupported,
|
|
155
156
|
},
|
|
157
|
+
'limit-rows': {
|
|
158
|
+
lexical: null,
|
|
159
|
+
dollar: true,
|
|
160
|
+
dynamic: () => Object.create( null ),
|
|
161
|
+
notFound: undefinedVariable,
|
|
162
|
+
param: paramSemantics,
|
|
163
|
+
},
|
|
164
|
+
'limit-offset': 'limit-rows',
|
|
156
165
|
// general element references -----------------------------------------------
|
|
157
166
|
where: {
|
|
158
|
-
lexical:
|
|
167
|
+
lexical: tableAliasesAndSelf,
|
|
159
168
|
dollar: true,
|
|
160
169
|
dynamic: combinedSourcesOrParentElements,
|
|
161
170
|
notFound: undefinedSourceElement,
|
|
@@ -198,7 +207,8 @@ function fns( model ) {
|
|
|
198
207
|
'join-on': {
|
|
199
208
|
lexical: tableAliasesAndSelf,
|
|
200
209
|
dollar: true,
|
|
201
|
-
dynamic: combinedSourcesOrParentElements,
|
|
210
|
+
dynamic: combinedSourcesOrParentElements,
|
|
211
|
+
rejectRoot: rejectOwnExceptVisibleAliases,
|
|
202
212
|
notFound: undefinedSourceElement,
|
|
203
213
|
param: paramSemantics,
|
|
204
214
|
},
|
|
@@ -249,6 +259,7 @@ function fns( model ) {
|
|
|
249
259
|
lexical: tableAliasesAndSelf, // TODO: reject own tab aliases
|
|
250
260
|
dollar: true,
|
|
251
261
|
dynamic: queryElements,
|
|
262
|
+
rejectRoot: rejectOwnAliasesAndMixins,
|
|
252
263
|
notFound: undefinedParentElement,
|
|
253
264
|
check: checkOrderByRef,
|
|
254
265
|
param: paramSemantics,
|
|
@@ -257,6 +268,7 @@ function fns( model ) {
|
|
|
257
268
|
lexical: tableAliasesAndSelf, // TODO: reject own tab aliases
|
|
258
269
|
dollar: true,
|
|
259
270
|
dynamic: () => Object.create( null ),
|
|
271
|
+
rejectRoot: rejectAllOwn,
|
|
260
272
|
notFound: undefinedVariable,
|
|
261
273
|
check: checkRefInQuery,
|
|
262
274
|
param: paramSemantics,
|
|
@@ -290,7 +302,7 @@ function fns( model ) {
|
|
|
290
302
|
}
|
|
291
303
|
|
|
292
304
|
if (expr.args) {
|
|
293
|
-
const args = Array.isArray(expr.args) ? expr.args : Object.values( expr.args );
|
|
305
|
+
const args = Array.isArray( expr.args ) ? expr.args : Object.values( expr.args );
|
|
294
306
|
// TODO: re-think $expected
|
|
295
307
|
if (!callback.traverse?.( args, exprCtx, user, callback ))
|
|
296
308
|
args.forEach( e => traverseExpr( e, exprCtx, user, callback ) );
|
|
@@ -326,10 +338,12 @@ function fns( model ) {
|
|
|
326
338
|
return `${ art.name.absolute }.${ pathName( ref.path.slice(1) ) }`;
|
|
327
339
|
}
|
|
328
340
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
341
|
+
/**
|
|
342
|
+
* Return artifact or element referred by the path in `ref`. The first
|
|
343
|
+
* environment we search in is `env`. If no such artifact or element exist,
|
|
344
|
+
* complain with message and return `undefined`. Record a dependency from
|
|
345
|
+
* `user` to the found artifact if `user` is provided.
|
|
346
|
+
*/
|
|
333
347
|
function resolvePath( ref, expected, user ) {
|
|
334
348
|
const origUser = user;
|
|
335
349
|
user = user._user || user;
|
|
@@ -348,7 +362,7 @@ function fns( model ) {
|
|
|
348
362
|
const semantics = (typeof s === 'string') ? referenceSemantics[s] : s;
|
|
349
363
|
|
|
350
364
|
const r = getPathRoot( ref, semantics, origUser );
|
|
351
|
-
const root = r && acceptPathRoot( r, ref, semantics,
|
|
365
|
+
const root = r && acceptPathRoot( r, ref, semantics, origUser );
|
|
352
366
|
if (!root)
|
|
353
367
|
return setArtifactLink( ref, root );
|
|
354
368
|
|
|
@@ -373,7 +387,7 @@ function fns( model ) {
|
|
|
373
387
|
dependsOn( user._main, target, location, user );
|
|
374
388
|
}
|
|
375
389
|
else if (art._main && art.kind !== 'select' || path[0]._navigation?.kind !== '$self') {
|
|
376
|
-
// no real dependency to bare $self (or actually: the
|
|
390
|
+
// no real dependency to bare $self (or actually: the underlying query)
|
|
377
391
|
dependsOn( user, art, location );
|
|
378
392
|
// Without on-demand resolve, we can simply signal 'undefined "x"'
|
|
379
393
|
// instead of 'illegal cycle' in the following case:
|
|
@@ -415,7 +429,7 @@ function fns( model ) {
|
|
|
415
429
|
if (!artifact[par] && i < args.length)
|
|
416
430
|
artifact[par] = args[i];
|
|
417
431
|
}
|
|
418
|
-
args = args.slice(parameters.length);
|
|
432
|
+
args = args.slice( parameters.length );
|
|
419
433
|
}
|
|
420
434
|
else if (args.length > 0 && !typeArtifact?.builtin) {
|
|
421
435
|
// One or two arguments are interpreted as either length or precision/scale.
|
|
@@ -480,7 +494,7 @@ function fns( model ) {
|
|
|
480
494
|
: [ '_$next', '$tableAliases' ];
|
|
481
495
|
// let notApplicable = ...; // for table aliases in JOIN-ON and UNION orderBy
|
|
482
496
|
for (let env = lexical; env; env = env[nextProp]) {
|
|
483
|
-
const dict = env[dictProp] || Object.create(null);
|
|
497
|
+
const dict = env[dictProp] || Object.create( null );
|
|
484
498
|
const r = dict[head.id];
|
|
485
499
|
if (acceptLexical( r, path, semantics, user ))
|
|
486
500
|
return setArtifactLink( head, r );
|
|
@@ -542,7 +556,7 @@ function fns( model ) {
|
|
|
542
556
|
// TOOD: call envFn with location of last item (for dependency error)
|
|
543
557
|
const env = envFn( art, path[index - 1].location, user );
|
|
544
558
|
const found = env && env[item.id]; // not env?.[item.id] ! …we want to keep the 0
|
|
545
|
-
// Reject `$self.$_column_1`:
|
|
559
|
+
// Reject `$self.$_column_1`: TODO: necessary to do here again?
|
|
546
560
|
art = setArtifactLink( item, (found?.name?.$inferred === '$internal') ? undefined : found );
|
|
547
561
|
|
|
548
562
|
if (!art) {
|
|
@@ -579,14 +593,13 @@ function fns( model ) {
|
|
|
579
593
|
// Non-global lexical are table aliases, mixins and $self, $projection, $parameters,
|
|
580
594
|
// Do not accept a lonely table alias and `$projection`
|
|
581
595
|
// TODO: test table alias and mixin named `$projection`
|
|
582
|
-
if (path.length
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
return art.name?.$inferred !== '$internal'; // not a compiler-generated internal alias
|
|
596
|
+
if (path.length !== 1 || user.expand || user.inline)
|
|
597
|
+
return art.name?.$inferred !== '$internal'; // not a compiler-generated internal alias
|
|
598
|
+
|
|
599
|
+
// allow mixins, $self, and `up_` in anonymous target aspect (is $navElement):
|
|
600
|
+
return art.kind === 'mixin' ||
|
|
601
|
+
art.kind === '$self' && path[0].id === '$self' ||
|
|
602
|
+
art.kind === '$navElement';
|
|
590
603
|
}
|
|
591
604
|
|
|
592
605
|
function acceptPathRoot( art, ref, semantics, user ) {
|
|
@@ -594,6 +607,9 @@ function fns( model ) {
|
|
|
594
607
|
const [ head ] = path;
|
|
595
608
|
if (Array.isArray( art ))
|
|
596
609
|
return getAmbiguousRefLink( art, head, user );
|
|
610
|
+
if (semantics.rejectRoot?.( art, user, ref, semantics ))
|
|
611
|
+
return null;
|
|
612
|
+
|
|
597
613
|
switch (art.kind) {
|
|
598
614
|
case 'using': {
|
|
599
615
|
const def = model.definitions[art.name.absolute];
|
|
@@ -607,8 +623,8 @@ function fns( model ) {
|
|
|
607
623
|
return setLink( head, '_navigation', art );
|
|
608
624
|
}
|
|
609
625
|
case '$navElement': {
|
|
610
|
-
if (head.id === user.$extended)
|
|
611
|
-
path.$prefix =
|
|
626
|
+
if (head.id === (user._user || user).$extended)
|
|
627
|
+
path.$prefix = head.id;
|
|
612
628
|
setLink( head, '_navigation', art );
|
|
613
629
|
return setArtifactLink( head, art._origin );
|
|
614
630
|
}
|
|
@@ -622,7 +638,7 @@ function fns( model ) {
|
|
|
622
638
|
// that the corresponding entity should not be put as $origin into the CSN.
|
|
623
639
|
// TODO: remove again, should be easy enough in to-csn without.
|
|
624
640
|
if (path.length === 1 && art.kind === '$tableAlias')
|
|
625
|
-
user.$noOrigin = true;
|
|
641
|
+
(user._user || user).$noOrigin = true;
|
|
626
642
|
return art;
|
|
627
643
|
}
|
|
628
644
|
case '$parameters': { // TODO: remove from CC
|
|
@@ -644,14 +660,14 @@ function fns( model ) {
|
|
|
644
660
|
return false;
|
|
645
661
|
// only complain about ambiguous source elements if we do not have
|
|
646
662
|
// duplicate table aliases, only mention non-ambiguous source elems
|
|
647
|
-
const uniqueNames = arr.filter( e => !e.$duplicates);
|
|
663
|
+
const uniqueNames = arr.filter( e => !e.$duplicates );
|
|
648
664
|
if (uniqueNames.length) {
|
|
649
665
|
const names = uniqueNames.filter( e => e._parent.$inferred !== '$internal' )
|
|
650
666
|
.map( e => `${ e.name.alias }.${ e.name.element }` );
|
|
651
667
|
let variant = names.length === uniqueNames.length ? 'std' : 'few';
|
|
652
668
|
if (names.length === 0)
|
|
653
669
|
variant = 'none';
|
|
654
|
-
error( 'ref-ambiguous', [ head.location, user ], { '#': variant, id: head.id, names });
|
|
670
|
+
error( 'ref-ambiguous', [ head.location, user ], { '#': variant, id: head.id, names } );
|
|
655
671
|
}
|
|
656
672
|
return false;
|
|
657
673
|
}
|
|
@@ -693,7 +709,7 @@ function fns( model ) {
|
|
|
693
709
|
return user._main || user;
|
|
694
710
|
// query.$tableAliases contains both aliases and $self/$projection
|
|
695
711
|
const aliases = query.$tableAliases;
|
|
696
|
-
const r = Object.create(null);
|
|
712
|
+
const r = Object.create( null );
|
|
697
713
|
if (aliases.$self.kind === '$self')
|
|
698
714
|
r.$self = aliases.$self;
|
|
699
715
|
// TODO: disallow $projection for ON conditions all together
|
|
@@ -773,7 +789,7 @@ function fns( model ) {
|
|
|
773
789
|
// default is function `environment`
|
|
774
790
|
|
|
775
791
|
function artifactsEnv( art ) {
|
|
776
|
-
return art._subArtifacts || Object.create(null);
|
|
792
|
+
return art._subArtifacts || Object.create( null );
|
|
777
793
|
}
|
|
778
794
|
|
|
779
795
|
function staticTarget( prev ) {
|
|
@@ -787,7 +803,7 @@ function fns( model ) {
|
|
|
787
803
|
return target.elements;
|
|
788
804
|
env = resolvePath( env.targetAspect, 'targetAspect', env );
|
|
789
805
|
}
|
|
790
|
-
return env?.elements || Object.create(null);
|
|
806
|
+
return env?.elements || Object.create( null );
|
|
791
807
|
}
|
|
792
808
|
|
|
793
809
|
function targetNavigation( art, location, user ) {
|
|
@@ -804,14 +820,14 @@ function fns( model ) {
|
|
|
804
820
|
// This way (not here though, but later in resolve.js)
|
|
805
821
|
if (env === 0)
|
|
806
822
|
return 0;
|
|
807
|
-
return env?.elements || Object.create(null);
|
|
823
|
+
return env?.elements || Object.create( null );
|
|
808
824
|
}
|
|
809
825
|
|
|
810
826
|
function calcElemNavigation( art, location, user ) {
|
|
811
827
|
const env = navigationEnv( art, location, user, 'calc' );
|
|
812
828
|
if (env === 0)
|
|
813
829
|
return 0;
|
|
814
|
-
return env?.elements || Object.create(null);
|
|
830
|
+
return env?.elements || Object.create( null );
|
|
815
831
|
}
|
|
816
832
|
|
|
817
833
|
// Return effective search environment provided by artifact `art`, i.e. the
|
|
@@ -825,7 +841,7 @@ function fns( model ) {
|
|
|
825
841
|
const env = navigationEnv( art, location, user, 'nav' );
|
|
826
842
|
if (env === 0)
|
|
827
843
|
return 0;
|
|
828
|
-
return env?.elements || Object.create(null);
|
|
844
|
+
return env?.elements || Object.create( null );
|
|
829
845
|
}
|
|
830
846
|
|
|
831
847
|
function navigationEnv( art, location, user, assocSpec ) {
|
|
@@ -900,7 +916,7 @@ function fns( model ) {
|
|
|
900
916
|
const isVar = id.charAt( 0 ) === '$' && id !== '$self';
|
|
901
917
|
// TODO: for wrong $self, also use ref-undefined-var, but with extra msg id
|
|
902
918
|
// otherwise, use s/th like ref-unexpected-element
|
|
903
|
-
signalNotFound( (isVar ? 'ref-undefined-var' : 'ref-expecting-const'),
|
|
919
|
+
signalNotFound( ( isVar ? 'ref-undefined-var' : 'ref-expecting-const'),
|
|
904
920
|
[ head.location, user ],
|
|
905
921
|
valid, { '#': 'std', id } );
|
|
906
922
|
// TODO: use s/th better than 'ref-expecting-const' !!
|
|
@@ -949,7 +965,7 @@ function fns( model ) {
|
|
|
949
965
|
function undefinedOrderByElement( user, head, valid, dynamicDict, _art, path ) {
|
|
950
966
|
const { id } = head;
|
|
951
967
|
const src = id.charAt( 0 ) !== '$' && user._combined?.[id];
|
|
952
|
-
if (src && !Array.isArray(src)) {
|
|
968
|
+
if (src && !Array.isArray( src )) {
|
|
953
969
|
path.$prefix = src.name.alias; // pushing it to path directly could be problematic
|
|
954
970
|
// configurable error:
|
|
955
971
|
signalNotFound( 'ref-deprecated-orderby', [ head.location, user ], valid,
|
|
@@ -983,7 +999,7 @@ function fns( model ) {
|
|
|
983
999
|
const id = (item === path[path.length - 1])
|
|
984
1000
|
? item.id
|
|
985
1001
|
: pathName( path.slice( path.indexOf( item ) ) );
|
|
986
|
-
signalNotFound( (art.$uncheckedElements ? 'ref-unknown-var' : 'ref-undefined-var'),
|
|
1002
|
+
signalNotFound( ( art.$uncheckedElements ? 'ref-unknown-var' : 'ref-undefined-var'),
|
|
987
1003
|
[ item.location, user ], valid,
|
|
988
1004
|
{ id: `${ art.name.element }.${ id }` } );
|
|
989
1005
|
}
|
|
@@ -1003,6 +1019,56 @@ function fns( model ) {
|
|
|
1003
1019
|
// function arguments ( art, user, ref, semantics ),
|
|
1004
1020
|
// default (for elements only): acceptElemOrVar
|
|
1005
1021
|
|
|
1022
|
+
function rejectOwnAliasesAndMixins( art, user, ref, semantics ) { // orderBy-set-ref
|
|
1023
|
+
switch (art.kind) {
|
|
1024
|
+
case '$tableAlias':
|
|
1025
|
+
case 'mixin':
|
|
1026
|
+
if (art._parent !== user)
|
|
1027
|
+
return false;
|
|
1028
|
+
break;
|
|
1029
|
+
case '$self':
|
|
1030
|
+
if (!semantics) // orderBy-set-expr
|
|
1031
|
+
break;
|
|
1032
|
+
// FALLTHROUGH
|
|
1033
|
+
default:
|
|
1034
|
+
return false;
|
|
1035
|
+
}
|
|
1036
|
+
error( 'ref-invalid-element', [ ref.path[0].location, user._user ],
|
|
1037
|
+
{ '#': art.kind, id: art.name.id } );
|
|
1038
|
+
return true;
|
|
1039
|
+
}
|
|
1040
|
+
|
|
1041
|
+
function rejectAllOwn( art, user, ref ) { // orderBy-set-expr
|
|
1042
|
+
return rejectOwnAliasesAndMixins( art, user, ref, null );
|
|
1043
|
+
}
|
|
1044
|
+
|
|
1045
|
+
function rejectOwnExceptVisibleAliases( art, user, ref ) { // for join-on
|
|
1046
|
+
switch (art.kind) {
|
|
1047
|
+
case '$navElement':
|
|
1048
|
+
art = art._parent;
|
|
1049
|
+
// FALLTHROUGH
|
|
1050
|
+
case '$tableAlias':
|
|
1051
|
+
case 'mixin':
|
|
1052
|
+
if (art._parent !== user._user || user.$tableAliases[art.name.id])
|
|
1053
|
+
return false;
|
|
1054
|
+
break;
|
|
1055
|
+
case '$self':
|
|
1056
|
+
// in the SQL backend, the $self.elem references are replaced by the
|
|
1057
|
+
// corresponding column expression; this might have references to elements
|
|
1058
|
+
// of invisible table aliases; at least one stakeholder uses this,
|
|
1059
|
+
// so it can't be an error (yet).
|
|
1060
|
+
warning( 'ref-deprecated-self-element', [ ref.path[0].location, user._user ], {},
|
|
1061
|
+
// eslint-disable-next-line max-len
|
|
1062
|
+
'Referring to the query\'s own elements here might lead to invalid SQL references; use source elements only' );
|
|
1063
|
+
return false;
|
|
1064
|
+
default:
|
|
1065
|
+
return false;
|
|
1066
|
+
}
|
|
1067
|
+
error( 'ref-invalid-element', [ ref.path[0].location, user._user ],
|
|
1068
|
+
{ '#': art.kind, id: art.name.id } );
|
|
1069
|
+
return true;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1006
1072
|
function acceptElemOrVarOrSelf( art, user, ref ) {
|
|
1007
1073
|
// TODO: make $self._artifact point to the $self alias, not the entity
|
|
1008
1074
|
return (!(art._main && art.kind !== 'select') && ref.path[0]._navigation?.kind === '$self')
|
|
@@ -1020,7 +1086,7 @@ function fns( model ) {
|
|
|
1020
1086
|
}
|
|
1021
1087
|
else if (art.$requireElementAccess) { // on some CDS variables
|
|
1022
1088
|
// Path with only one item, but we expect an element, e.g. `$at.from`.
|
|
1023
|
-
signalMissingElementAccess(art, [ path[0].location, user ]);
|
|
1089
|
+
signalMissingElementAccess( art, [ path[0].location, user ] );
|
|
1024
1090
|
return null;
|
|
1025
1091
|
}
|
|
1026
1092
|
else if (art.$autoElement) {
|
|
@@ -1045,6 +1111,15 @@ function fns( model ) {
|
|
|
1045
1111
|
return art;
|
|
1046
1112
|
}
|
|
1047
1113
|
|
|
1114
|
+
function acceptRealArtifact( art, user, ref ) {
|
|
1115
|
+
// For compatibility, we accept `extend Unknown` without elements/actions/includes
|
|
1116
|
+
if (art.kind !== 'namespace' || !(user.elements || user.actions || user.includes))
|
|
1117
|
+
return art;
|
|
1118
|
+
const { location } = ref.path[ref.path.length - 1];
|
|
1119
|
+
signalNotFound( 'ref-undefined-def', [ location, user ], null, { art } );
|
|
1120
|
+
return false;
|
|
1121
|
+
}
|
|
1122
|
+
|
|
1048
1123
|
function acceptStructOrBare( art, user, ref ) { // for includes[]
|
|
1049
1124
|
// It had been checked before that `includes` is already forbidden for
|
|
1050
1125
|
// non-entity/aspect/type/event.
|
|
@@ -1069,9 +1144,11 @@ function fns( model ) {
|
|
|
1069
1144
|
// Remark: it is not necessary to test for user.elements[$inferred], because
|
|
1070
1145
|
// the type could only have inferred elements if it has a type expression.
|
|
1071
1146
|
// Including aspects with elements is forbidden for aspects without the
|
|
1072
|
-
// `elements` property
|
|
1073
|
-
//
|
|
1074
|
-
//
|
|
1147
|
+
// `elements` property. Testing for the length of `art.elements` requires
|
|
1148
|
+
// that we have applied potential `includes` of `art` before!
|
|
1149
|
+
// We might allow includes with elements in the future, they'd probably
|
|
1150
|
+
// count as specified elements with lower priority, i.e. annos, types, key
|
|
1151
|
+
// etc on columns beat those inherited from the include.
|
|
1075
1152
|
if (art.kind === 'aspect' &&
|
|
1076
1153
|
(!art.elements || base.query && !Object.keys( art.elements ).length))
|
|
1077
1154
|
return art;
|
|
@@ -1482,10 +1559,10 @@ function fns( model ) {
|
|
|
1482
1559
|
const err = message( 'ref-expected-element', location,
|
|
1483
1560
|
{ '#': 'magicVar', id: art.name.id } );
|
|
1484
1561
|
// Mapping for better valid names: from -> $at.from
|
|
1485
|
-
const valid = Object.keys(art.elements || {}).reduce((prev, curr) => {
|
|
1562
|
+
const valid = Object.keys( art.elements || {} ).reduce( (prev, curr) => {
|
|
1486
1563
|
prev[`${ art.name.id }.${ curr }`] = true;
|
|
1487
1564
|
return prev;
|
|
1488
|
-
}, Object.create(null));
|
|
1565
|
+
}, Object.create( null ) );
|
|
1489
1566
|
attachAndEmitValidNames( err, valid );
|
|
1490
1567
|
}
|
|
1491
1568
|
|
|
@@ -1508,14 +1585,14 @@ function fns( model ) {
|
|
|
1508
1585
|
// ignore internal types such as cds.Association, ignore names with dot for
|
|
1509
1586
|
// CDL references to main artifacts:
|
|
1510
1587
|
if (!art.internal && !art.deprecated && art.name?.$inferred !== '$internal' &&
|
|
1511
|
-
(viaCdl ? art._main || !name.includes('.') : art.kind !== 'namespace'))
|
|
1588
|
+
(viaCdl ? art._main || !name.includes( '.' ) : art.kind !== 'namespace'))
|
|
1512
1589
|
msg.validNames[name] = art;
|
|
1513
1590
|
}
|
|
1514
1591
|
|
|
1515
1592
|
if (options.testMode && !options.$recompile) {
|
|
1516
1593
|
// no semantic location => either first of [loc, semantic loc] pair or just location.
|
|
1517
1594
|
const loc = msg.$location[0] || msg.$location;
|
|
1518
|
-
const names = Object.keys(msg.validNames);
|
|
1595
|
+
const names = Object.keys( msg.validNames );
|
|
1519
1596
|
names.sort();
|
|
1520
1597
|
if (names.length > 22) {
|
|
1521
1598
|
names.length = 20;
|
|
@@ -1523,7 +1600,7 @@ function fns( model ) {
|
|
|
1523
1600
|
}
|
|
1524
1601
|
info( null, [ loc, null ],
|
|
1525
1602
|
{ '#': !names.length ? 'zero' : 'std' },
|
|
1526
|
-
{ std: `Valid: ${ names.join(', ') }`, zero: 'No valid names' });
|
|
1603
|
+
{ std: `Valid: ${ names.join( ', ' ) }`, zero: 'No valid names' } );
|
|
1527
1604
|
}
|
|
1528
1605
|
}
|
|
1529
1606
|
}
|
|
@@ -22,8 +22,8 @@ const {
|
|
|
22
22
|
} = require('./utils');
|
|
23
23
|
const { CsnLocation } = require('./classes');
|
|
24
24
|
|
|
25
|
-
const $location = Symbol.for('cds.$location');
|
|
26
|
-
const $inferred = Symbol.for('cds.$inferred');
|
|
25
|
+
const $location = Symbol.for( 'cds.$location' );
|
|
26
|
+
const $inferred = Symbol.for( 'cds.$inferred' );
|
|
27
27
|
|
|
28
28
|
// Export function of this file.
|
|
29
29
|
function tweakAssocs( model ) {
|
|
@@ -118,7 +118,7 @@ function tweakAssocs( model ) {
|
|
|
118
118
|
}
|
|
119
119
|
|
|
120
120
|
function rewriteAssociationCheck( element ) {
|
|
121
|
-
const elem = element.items || element; // TODO
|
|
121
|
+
const elem = element.items || element; // TODO v5: nested items
|
|
122
122
|
if (elem.elements)
|
|
123
123
|
forEachGeneric( elem, 'elements', rewriteAssociationCheck );
|
|
124
124
|
if (!elem.target)
|
|
@@ -201,7 +201,7 @@ function tweakAssocs( model ) {
|
|
|
201
201
|
}
|
|
202
202
|
|
|
203
203
|
function assocWithExplicitSpec( assoc ) {
|
|
204
|
-
while (assoc.foreignKeys && inferredForeignKeys( assoc.foreignKeys, 'keys') ||
|
|
204
|
+
while (assoc.foreignKeys && inferredForeignKeys( assoc.foreignKeys, 'keys' ) ||
|
|
205
205
|
assoc.on && assoc.on.$inferred)
|
|
206
206
|
assoc = getOrigin( assoc );
|
|
207
207
|
return assoc;
|
|
@@ -264,7 +264,7 @@ function tweakAssocs( model ) {
|
|
|
264
264
|
fk.$inferred = 'rewrite'; // Override existing value; TODO: other $inferred value?
|
|
265
265
|
// TODO: re-check for case that foreign key is managed association
|
|
266
266
|
if (orig._effectiveType !== undefined)
|
|
267
|
-
setLink( fk, '_effectiveType', orig._effectiveType);
|
|
267
|
+
setLink( fk, '_effectiveType', orig._effectiveType );
|
|
268
268
|
const te = copyExpr( orig.targetElement, elem.location );
|
|
269
269
|
if (elem._redirected) {
|
|
270
270
|
const i = te.path[0]; // TODO: or also follow path like for ON?
|
|
@@ -273,7 +273,7 @@ function tweakAssocs( model ) {
|
|
|
273
273
|
setArtifactLink( te, state );
|
|
274
274
|
}
|
|
275
275
|
fk.targetElement = te;
|
|
276
|
-
});
|
|
276
|
+
} );
|
|
277
277
|
if (elem.foreignKeys) // Possibly no fk was set
|
|
278
278
|
elem.foreignKeys[$inferred] = 'rewrite';
|
|
279
279
|
}
|
|
@@ -288,14 +288,14 @@ function tweakAssocs( model ) {
|
|
|
288
288
|
// same (TODO later: set status whether rewrite changes anything),
|
|
289
289
|
// especially problematic are refs starting with $self:
|
|
290
290
|
setExpandStatus( elem, 'target' );
|
|
291
|
-
if (elem._parent
|
|
291
|
+
if (elem._parent?.kind === 'element') {
|
|
292
292
|
// managed association as sub element not supported yet
|
|
293
293
|
error( null, [ elem.location, elem ], {},
|
|
294
294
|
// eslint-disable-next-line max-len
|
|
295
295
|
'Rewriting the ON-condition of unmanaged association in sub element is not supported' );
|
|
296
296
|
return;
|
|
297
297
|
}
|
|
298
|
-
const nav = (elem._main
|
|
298
|
+
const nav = (elem._main?.query && elem.value)
|
|
299
299
|
? pathNavigation( elem.value ) // redirected source elem or mixin
|
|
300
300
|
: { navigation: assoc }; // redirected user-provided
|
|
301
301
|
const cond = copyExpr( assoc.on,
|
|
@@ -347,15 +347,13 @@ function tweakAssocs( model ) {
|
|
|
347
347
|
if (tableAlias) { // from ON cond of element in source ref/d by table alias
|
|
348
348
|
const source = tableAlias._origin;
|
|
349
349
|
const root = expr.path[0]._navigation || expr.path[0]._artifact;
|
|
350
|
-
// console.log( info(null, [assoc.name.location, assoc],
|
|
351
|
-
// { names: expr.path.map(i=>i.id), art: root }, 'TA').toString())
|
|
350
|
+
// console.log( info(null, [ assoc.name.location, assoc ],
|
|
351
|
+
// { names: expr.path.map(i => i.id), art: root }, 'TA').toString());
|
|
352
352
|
if (!root || root._main !== source)
|
|
353
353
|
return; // not $self or source element
|
|
354
354
|
if (expr.scope === 'param' || root.kind === '$parameters')
|
|
355
355
|
return; // are not allowed anyway - there was an error before
|
|
356
|
-
const item = expr.path
|
|
357
|
-
// console.log('YE', assoc.name, item, root.name, expr.path)
|
|
358
|
-
const elem = navProjection( item && tableAlias.elements[item.id], assoc );
|
|
356
|
+
const { item, elem } = firstProjectionForPath( expr.path, tableAlias, assoc );
|
|
359
357
|
rewritePath( expr, item, assoc, elem, assoc.value.location );
|
|
360
358
|
}
|
|
361
359
|
else if (assoc._main.query) { // from ON cond of mixin element in query
|
|
@@ -386,7 +384,7 @@ function tweakAssocs( model ) {
|
|
|
386
384
|
return; // just $self
|
|
387
385
|
// corresponding elem in including structure
|
|
388
386
|
const elem = (assoc._main.items || assoc._main).elements[item.id];
|
|
389
|
-
if (!(Array.isArray(elem) ||
|
|
387
|
+
if (!(Array.isArray( elem ) || // no msg for redefs
|
|
390
388
|
elem === item._artifact || // redirection for explicit def
|
|
391
389
|
elem._origin === item._artifact)) {
|
|
392
390
|
const art = assoc._origin;
|
|
@@ -399,7 +397,7 @@ function tweakAssocs( model ) {
|
|
|
399
397
|
element: 'This element is not originally referred to in the ON-condition of association $(MEMBER) of $(ART)',
|
|
400
398
|
} );
|
|
401
399
|
}
|
|
402
|
-
rewritePath( expr, item, assoc, (Array.isArray(elem) ? false : elem), null );
|
|
400
|
+
rewritePath( expr, item, assoc, (Array.isArray( elem ) ? false : elem), null );
|
|
403
401
|
}
|
|
404
402
|
}
|
|
405
403
|
|
|
@@ -408,13 +406,12 @@ function tweakAssocs( model ) {
|
|
|
408
406
|
let root = path[0];
|
|
409
407
|
if (!elem) {
|
|
410
408
|
if (location) {
|
|
411
|
-
error( 'rewrite-not-projected', [ location, assoc ],
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
} );
|
|
409
|
+
error( 'rewrite-not-projected', [ location, assoc ], {
|
|
410
|
+
name: assoc.name.id, art: item._artifact, elemref: { ref: path },
|
|
411
|
+
}, {
|
|
412
|
+
std: 'Projected association $(NAME) uses non-projected element $(ELEMREF)',
|
|
413
|
+
element: 'Projected association $(NAME) uses non-projected element $(MEMBER) of $(ART)',
|
|
414
|
+
} );
|
|
418
415
|
}
|
|
419
416
|
delete root._navigation;
|
|
420
417
|
setArtifactLink( root, elem );
|
|
@@ -422,9 +419,15 @@ function tweakAssocs( model ) {
|
|
|
422
419
|
return;
|
|
423
420
|
}
|
|
424
421
|
if (item !== root) {
|
|
422
|
+
// e.g. mixin ON-condition: Base.foo -> $self.foo or multi-path projection,
|
|
423
|
+
// $projection -> $self
|
|
425
424
|
root.id = '$self';
|
|
426
425
|
setLink( root, '_navigation', assoc._parent.$tableAliases.$self );
|
|
427
426
|
setArtifactLink( root, assoc._parent );
|
|
427
|
+
if (item) {
|
|
428
|
+
const i = path.indexOf(item);
|
|
429
|
+
ref.path = [ root, ...path.slice( i, path.length ) ];
|
|
430
|
+
}
|
|
428
431
|
}
|
|
429
432
|
else if (elem.name.id.charAt(0) === '$') {
|
|
430
433
|
root = { id: '$self', location: item.location };
|
|
@@ -470,14 +473,13 @@ function tweakAssocs( model ) {
|
|
|
470
473
|
// consider intermediate "preferred" elements - not just `assoc`,
|
|
471
474
|
// but its origins, too.
|
|
472
475
|
const proj = navProjection( alias.elements[name], assoc );
|
|
473
|
-
name = proj
|
|
476
|
+
name = proj?.name?.id;
|
|
474
477
|
if (!name) {
|
|
475
478
|
if (!forKeys)
|
|
476
479
|
break;
|
|
477
480
|
setArtifactLink( item, null );
|
|
478
481
|
const culprit = elem.target && !elem.target.$inferred && elem.target ||
|
|
479
|
-
|
|
480
|
-
elem.value.path[elem.value.path.length - 1]) ||
|
|
482
|
+
elem.value?.path?.[elem.value.path.length - 1] ||
|
|
481
483
|
elem;
|
|
482
484
|
// TODO: probably better to collect the non-projected foreign keys
|
|
483
485
|
// and have one message for all
|
|
@@ -495,7 +497,7 @@ function tweakAssocs( model ) {
|
|
|
495
497
|
env = env.target._artifact?._effectiveType;
|
|
496
498
|
elem = setArtifactLink( item, env?.elements?.[name] );
|
|
497
499
|
|
|
498
|
-
if (elem && !Array.isArray(elem))
|
|
500
|
+
if (elem && !Array.isArray( elem ))
|
|
499
501
|
return elem;
|
|
500
502
|
// TODO: better (extra message), TODO: do it
|
|
501
503
|
error( 'query-undefined-element', [ item.location, assoc ],
|
|
@@ -516,20 +518,71 @@ function navProjection( navigation, preferred ) {
|
|
|
516
518
|
: navigation._projections[0] || null;
|
|
517
519
|
}
|
|
518
520
|
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
521
|
+
|
|
522
|
+
/**
|
|
523
|
+
* For a path `a.b.c.d`, return a projection for the first path item that is projected.
|
|
524
|
+
* For example, if a query has multiple projections such as `a.b, a, a.b.c`, the
|
|
525
|
+
* _first_ possible projection will be used and the caller can rewrite `a.b.c.d` to `b.c.d`.
|
|
526
|
+
* This avoids that `extend`s affect the ON-condition.
|
|
527
|
+
*
|
|
528
|
+
* The returned object `ret` has `ret.item`, which is the path item that is projected.
|
|
529
|
+
* `ret.elem` is the element projection.
|
|
530
|
+
*
|
|
531
|
+
* @param {any[]} path
|
|
532
|
+
* @param {object} tableAlias
|
|
533
|
+
* @param {object} assoc Preferred association that should be used if projected.
|
|
534
|
+
* @return {{elem: object, item: object}|null}
|
|
535
|
+
*/
|
|
536
|
+
function firstProjectionForPath( path, tableAlias, assoc ) {
|
|
537
|
+
const viaSelf = (path[0]._navigation || path[0]._artifact).kind === '$self';
|
|
538
|
+
const root = viaSelf ? 1 : 0;
|
|
539
|
+
if (root >= path.length) // e.g. just `$self` path item
|
|
540
|
+
return { item: undefined, elem: {} };
|
|
541
|
+
|
|
542
|
+
// We want to use the _first_ valid projection that is written by the user (if the preferred
|
|
543
|
+
// `assoc` is not directly projected). To achieve that, look into the table alias' elements.
|
|
544
|
+
const selectedElements = Object.values(tableAlias._parent.elements);
|
|
545
|
+
const proj = [];
|
|
546
|
+
let navItem = tableAlias;
|
|
547
|
+
for (const item of path.slice(root)) {
|
|
548
|
+
navItem = item?.id && navItem.elements?.[item.id];
|
|
549
|
+
if (!navItem) {
|
|
550
|
+
break;
|
|
551
|
+
}
|
|
552
|
+
else if (navItem._projections) {
|
|
553
|
+
const elem = navProjection( navItem, assoc );
|
|
554
|
+
if (elem && elem === assoc) {
|
|
555
|
+
// in case the specified association is found, _always_ use it.
|
|
556
|
+
return { item, elem };
|
|
557
|
+
}
|
|
558
|
+
else if (elem) {
|
|
559
|
+
const index = selectedElements.indexOf(elem);
|
|
560
|
+
proj.push({ item, elem, index });
|
|
561
|
+
}
|
|
562
|
+
}
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
return (proj.length === 0)
|
|
566
|
+
? { item: path[root], elem: null }
|
|
567
|
+
: proj.reduce( (acc, curr) => (acc.index > curr.index ? curr : acc), proj[0] ); // first
|
|
568
|
+
}
|
|
569
|
+
|
|
570
|
+
/**
|
|
571
|
+
* Return condensed info about reference in select item
|
|
572
|
+
* - tableAlias.elem -> { navigation: navElem, item: path[1], tableAlias }
|
|
573
|
+
* - sourceElem (in query) -> { navigation: navElem, item: path[0], tableAlias }
|
|
574
|
+
* - mixinElem -> { navigation: mixinElement, item: path[0] }
|
|
575
|
+
* - $projection.elem -> also $self.item -> { item: path[1], tableAlias: $self }
|
|
576
|
+
* - $self -> { item: undefined, tableAlias: $self }
|
|
577
|
+
* - $parameters.P, :P -> {}
|
|
578
|
+
* - $now, current_date -> {}
|
|
579
|
+
* - undef, redef -> {}
|
|
580
|
+
* With 'navigation': store that navigation._artifact is projected
|
|
581
|
+
* With 'navigation': rewrite its ON condition
|
|
582
|
+
* With navigation: Do KEY propagation
|
|
583
|
+
*
|
|
584
|
+
* TODO: re-think this function, copied in populate.js and tweak-assocs.js
|
|
585
|
+
*/
|
|
533
586
|
function pathNavigation( ref ) {
|
|
534
587
|
// currently, indirectly projectable elements are not included - we might
|
|
535
588
|
// keep it this way! If we want them to be included - be aware: cycles
|