@sap/cds-compiler 4.9.0 → 4.9.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +9 -0
- package/bin/cdsc.js +16 -16
- package/lib/base/message-registry.js +7 -3
- package/lib/base/messages.js +1 -1
- package/lib/compiler/assert-consistency.js +2 -1
- package/lib/compiler/define.js +4 -8
- package/lib/compiler/populate.js +0 -2
- package/lib/compiler/propagator.js +3 -2
- package/lib/compiler/resolve.js +5 -2
- package/lib/compiler/tweak-assocs.js +62 -31
- package/lib/compiler/xpr-rewrite.js +118 -45
- package/lib/edm/annotations/edmJson.js +24 -7
- package/lib/edm/annotations/genericTranslation.js +1 -1
- package/lib/json/to-csn.js +2 -1
- package/lib/model/csnRefs.js +11 -4
- package/lib/render/toCdl.js +1 -1
- package/lib/transform/db/constraints.js +74 -27
- package/lib/transform/db/flattening.js +2 -2
- package/lib/transform/forRelationalDB.js +2 -2
- package/lib/transform/odata/flattening.js +49 -35
- package/lib/transform/transformUtils.js +7 -5
- package/lib/transform/universalCsn/universalCsnEnricher.js +4 -3
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -7,6 +7,15 @@
|
|
|
7
7
|
Note: `beta` fixes, changes and features are usually not listed in this ChangeLog but [here](doc/CHANGELOG_BETA.md).
|
|
8
8
|
The compiler behavior concerning `beta` features can change at any time without notice.
|
|
9
9
|
|
|
10
|
+
## Version 4.9.2 - 2024-05-13
|
|
11
|
+
|
|
12
|
+
### Fixed
|
|
13
|
+
|
|
14
|
+
- compiler: Rewriting annotation expression paths in structures of projections has been improved.
|
|
15
|
+
- to.edm(x):
|
|
16
|
+
+ Operator `/` represents `DivBy` operator, explicit `DivBy` is replaced with `Div` as integer division.
|
|
17
|
+
- to.sql: consider all associations in tenant dependent entity for referential constraint generation
|
|
18
|
+
|
|
10
19
|
## Version 4.9.0 - 2024-04-25
|
|
11
20
|
|
|
12
21
|
### Added
|
package/bin/cdsc.js
CHANGED
|
@@ -578,22 +578,12 @@ function executeCommandLine( command, options, args ) {
|
|
|
578
578
|
// Depending on 'options.rawOutput', the model is either compacted to 'name.json' or
|
|
579
579
|
// written in raw form to '<name>_raw.txt'.
|
|
580
580
|
function displayNamedXsn( xsn, name ) {
|
|
581
|
-
if (options.rawOutput)
|
|
581
|
+
if (options.rawOutput)
|
|
582
582
|
writeToFileOrDisplay(options.out, `${name}_raw.txt`, util.inspect(reveal(xsn, options.rawOutput), false, null), true);
|
|
583
|
-
|
|
584
|
-
else if (options.internalMsg) {
|
|
583
|
+
else if (options.internalMsg)
|
|
585
584
|
writeToFileOrDisplay(options.out, `${name}_raw.txt`, util.inspect(reveal(xsn).messages, { depth: null, maxArrayLength: null }), true);
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
const csn = compactModel(xsn, options);
|
|
589
|
-
if (command === 'toCsn' && options.tenantDiscriminator)
|
|
590
|
-
addTenantFields(csn, options);
|
|
591
|
-
if (command === 'toCsn' && options.withLocalized)
|
|
592
|
-
addLocalizationViews(csn, options);
|
|
593
|
-
if (options.enrichCsn)
|
|
594
|
-
enrichCsn( csn, options );
|
|
595
|
-
writeToFileOrDisplay(options.out, `${name}.json`, csn, true);
|
|
596
|
-
}
|
|
585
|
+
else if (!options.parseOnly) // no output if parseOnly but not rawOutput
|
|
586
|
+
displayNamedCsn(compactModel(xsn, options), name);
|
|
597
587
|
}
|
|
598
588
|
|
|
599
589
|
/**
|
|
@@ -603,12 +593,22 @@ function executeCommandLine( command, options, args ) {
|
|
|
603
593
|
function displayNamedCsn( csn, name ) {
|
|
604
594
|
if (!csn) // only print CSN if it is set.
|
|
605
595
|
return;
|
|
596
|
+
|
|
597
|
+
if (command === 'toCsn' ) {
|
|
598
|
+
// If requested, run some CSN postprocessing.
|
|
599
|
+
if (options.tenantDiscriminator)
|
|
600
|
+
addTenantFields(csn, options); // always _before_ localized convenience views are added
|
|
601
|
+
if (options.withLocalized)
|
|
602
|
+
addLocalizationViews(csn, options);
|
|
603
|
+
}
|
|
604
|
+
|
|
605
|
+
if (options.enrichCsn)
|
|
606
|
+
enrichCsn( csn, options );
|
|
607
|
+
|
|
606
608
|
if (options.internalMsg) {
|
|
607
609
|
writeToFileOrDisplay(options.out, `${name}_raw.txt`, options.messages, true);
|
|
608
610
|
}
|
|
609
611
|
else if (!options.internalMsg) {
|
|
610
|
-
if (command === 'toCsn' && options.withLocalized)
|
|
611
|
-
addLocalizationViews(csn, options);
|
|
612
612
|
writeToFileOrDisplay(options.out, `${name}.json`, csn, true);
|
|
613
613
|
}
|
|
614
614
|
}
|
|
@@ -298,8 +298,9 @@ const centralMessageTexts = {
|
|
|
298
298
|
},
|
|
299
299
|
|
|
300
300
|
'anno-missing-rewrite': {
|
|
301
|
-
std: 'Assign a value for $(ANNO); the value inherited from $(ART)
|
|
301
|
+
std: 'Assign a value for $(ANNO); the value inherited from $(ART) can\'t be rewritten due to $(ELEMREF)',
|
|
302
302
|
unsupported: 'Assign a value for $(ANNO); the value inherited from $(ART) can\'t be rewritten due to unsupported $(ELEMREF)',
|
|
303
|
+
param: 'Assign a value for $(ANNO); the value inherited from $(ART) can\'t be rewritten due to parameter reference $(ELEMREF)',
|
|
303
304
|
},
|
|
304
305
|
|
|
305
306
|
'chained-array-of': '"Array of"/"many" must not be chained with another "array of"/"many" inside a service',
|
|
@@ -717,6 +718,8 @@ const centralMessageTexts = {
|
|
|
717
718
|
min: 'Expecting argument $(PROP) for type $(TYPE) to be greater than or equal to $(NUMBER)',
|
|
718
719
|
'incorrect-type': 'Expected $(NAMES) for argument $(PROP), but found $(CODE)',
|
|
719
720
|
},
|
|
721
|
+
'type-unexpected-foreign-keys': 'A managed aspect composition can\'t have a foreign keys specification. Use composition-of-entity or remove foreign keys',
|
|
722
|
+
'type-unexpected-on-condition': 'A managed aspect composition can\'t have a specified ON-condition. Use composition-of-entity or remove the ON-condition',
|
|
720
723
|
|
|
721
724
|
'type-invalid-items': {
|
|
722
725
|
std: 'Unexpected $(PROP)', // unused
|
|
@@ -876,7 +879,7 @@ const centralMessageTexts = {
|
|
|
876
879
|
sub: 'Expecting an entity as target; a target aspect can\'t be specified for a sub element',
|
|
877
880
|
},
|
|
878
881
|
'ref-invalid-include': {
|
|
879
|
-
std: '
|
|
882
|
+
std: 'An explicitly structured entity, type, aspect, or event is expected here',
|
|
880
883
|
bare : 'An aspect without elements is expected here',
|
|
881
884
|
param: 'A type, entity, aspect or event without parameters is expected here',
|
|
882
885
|
},
|
|
@@ -1166,7 +1169,8 @@ const centralMessageTexts = {
|
|
|
1166
1169
|
}
|
|
1167
1170
|
,
|
|
1168
1171
|
'odata-anno-xpr-type': {
|
|
1169
|
-
'std': 'Expected one qualified type name for $(OP) in $(ANNO)'
|
|
1172
|
+
'std': 'Expected one qualified type name for $(OP) in $(ANNO)',
|
|
1173
|
+
'edm': 'Expected a qualified EDM type name for $(OP) in $(ANNO) but found $(TYPE)'
|
|
1170
1174
|
},
|
|
1171
1175
|
'odata-anno-xpr-args': {
|
|
1172
1176
|
'std': 'Unexpected arguments for $(OP) in $(ANNO)',
|
package/lib/base/messages.js
CHANGED
|
@@ -890,7 +890,7 @@ function transformElementRef( arg ) {
|
|
|
890
890
|
return quoted( arg );
|
|
891
891
|
// Can be used by CSN backends or compiler to create a simple path such as E:elem
|
|
892
892
|
return quoted(
|
|
893
|
-
(arg.param ? ':' : '') +
|
|
893
|
+
((arg.scope === 'param' || arg.param) ? ':' : '') +
|
|
894
894
|
ref.map(
|
|
895
895
|
item => (typeof item !== 'string'
|
|
896
896
|
? `${ item.id }${item.args ? '(…)' : ''}${item.where ? '[…]' : ''}`
|
|
@@ -207,7 +207,7 @@ function assertConsistency( model, stage ) {
|
|
|
207
207
|
optional: [
|
|
208
208
|
'elements', '$autoElement', '$uncheckedElements', '_origin', '_extensions',
|
|
209
209
|
'$requireElementAccess', '_effectiveType', '$effectiveSeqNo', '_deps',
|
|
210
|
-
'$calcDepElement', '$filtered', '_parent',
|
|
210
|
+
'$calcDepElement', '$filtered', '$enclosed', '_parent',
|
|
211
211
|
],
|
|
212
212
|
schema: {
|
|
213
213
|
kind: { test: isString, enum: [ 'builtin' ] },
|
|
@@ -261,6 +261,7 @@ function assertConsistency( model, stage ) {
|
|
|
261
261
|
foreignKeys: { kind: true, inherits: 'definitions', instanceOf: 'ignore' },
|
|
262
262
|
$keysNavigation: { kind: true, test: TODO },
|
|
263
263
|
$filtered: { kind: true, inherits: 'value' }, // for assoc+filter
|
|
264
|
+
$enclosed: { kind: true, inherits: 'value' }, // for comp+filter
|
|
264
265
|
params: { kind: true, inherits: 'definitions' },
|
|
265
266
|
_extendType: { kind: true, test: TODO },
|
|
266
267
|
mixin: { inherits: 'definitions' },
|
package/lib/compiler/define.js
CHANGED
|
@@ -1024,15 +1024,11 @@ function define( model ) {
|
|
|
1024
1024
|
const { targetAspect } = obj;
|
|
1025
1025
|
if (targetAspect) {
|
|
1026
1026
|
if (obj.foreignKeys) {
|
|
1027
|
-
error( 'type-unexpected-foreign-keys', [ obj.foreignKeys[$location], construct ]
|
|
1028
|
-
{},
|
|
1029
|
-
'A managed aspect composition can\'t have a foreign keys specification' );
|
|
1027
|
+
error( 'type-unexpected-foreign-keys', [ obj.foreignKeys[$location], construct ] );
|
|
1030
1028
|
delete obj.foreignKeys; // continuation semantics: not specified
|
|
1031
1029
|
}
|
|
1032
1030
|
if (obj.on && !obj.target) {
|
|
1033
|
-
error( 'type-unexpected-on-condition', [ obj.on.location, construct ]
|
|
1034
|
-
{},
|
|
1035
|
-
'A managed aspect composition can\'t have a specified ON-condition' );
|
|
1031
|
+
error( 'type-unexpected-on-condition', [ obj.on.location, construct ] );
|
|
1036
1032
|
delete obj.on; // continuation semantics: not specified
|
|
1037
1033
|
}
|
|
1038
1034
|
if (targetAspect.elements)
|
|
@@ -1182,8 +1178,8 @@ function define( model ) {
|
|
|
1182
1178
|
// TODO: it is not just "syntax" - maybe better test for `$calcDepElement`?
|
|
1183
1179
|
createAndLinkCalcDepElement( elem );
|
|
1184
1180
|
|
|
1185
|
-
// Special case (hack) for calculated elements that use
|
|
1186
|
-
// See "Notes on `$
|
|
1181
|
+
// Special case (hack) for calculated elements that use composition+filter:
|
|
1182
|
+
// See "Notes on `$enclosed`" in `ExposingAssocWithFilter.md` for details.
|
|
1187
1183
|
if (elem.target && elem.value.path?.[elem.value.path.length - 1]?.where) {
|
|
1188
1184
|
delete elem.type;
|
|
1189
1185
|
delete elem.on;
|
package/lib/compiler/populate.js
CHANGED
|
@@ -617,8 +617,6 @@ function populate( model ) {
|
|
|
617
617
|
|
|
618
618
|
function resolveTabRef( alias ) {
|
|
619
619
|
// effectiveType() must not be called on $self, is unnecessary for mixins:
|
|
620
|
-
// TODO: have a test for `select from E { a, $self.a as b, $self.{ b as c } }`
|
|
621
|
-
// TODO: have a negative test for `select from E { $self.*, assoc.* }`
|
|
622
620
|
// (we might have those already)
|
|
623
621
|
if (alias.kind === 'mixin' || alias.kind === '$self')
|
|
624
622
|
return;
|
|
@@ -56,7 +56,8 @@ function propagate( model ) {
|
|
|
56
56
|
enum: expensive,
|
|
57
57
|
params: expensive, // actually only with parent action
|
|
58
58
|
returns,
|
|
59
|
-
$filtered: annotation,
|
|
59
|
+
$filtered: annotation, // TODO(v5): Remove
|
|
60
|
+
$enclosed: annotation,
|
|
60
61
|
};
|
|
61
62
|
const ruleToFunction = {
|
|
62
63
|
__proto__: null,
|
|
@@ -281,7 +282,7 @@ function propagate( model ) {
|
|
|
281
282
|
if (target.targetAspect)
|
|
282
283
|
return;
|
|
283
284
|
if (target.type?._artifact === model.definitions['cds.Association'])
|
|
284
|
-
return; // don't propagate targetAspect to associations (e.g. via $
|
|
285
|
+
return; // don't propagate targetAspect to associations (e.g. via $enclosed)
|
|
285
286
|
const ta = source.targetAspect;
|
|
286
287
|
if (!ta.elements && !ta._origin) { // _origin set for elements in source
|
|
287
288
|
notWithExpand( prop, target, source );
|
package/lib/compiler/resolve.js
CHANGED
|
@@ -182,9 +182,12 @@ function resolve( model ) {
|
|
|
182
182
|
// Path could start with table alias; get start index
|
|
183
183
|
let index = path.indexOf(nav.item);
|
|
184
184
|
if (index === -1)
|
|
185
|
-
return;
|
|
185
|
+
return; // should not happen
|
|
186
186
|
|
|
187
187
|
let navItem = nav.navigation;
|
|
188
|
+
if (!nav.item._navigation) // first non-table-alias
|
|
189
|
+
setLink( nav.item, '_navigation', navItem );
|
|
190
|
+
|
|
188
191
|
if (path[index].where || path[index].args)
|
|
189
192
|
return;
|
|
190
193
|
++index;
|
|
@@ -566,7 +569,7 @@ function resolve( model ) {
|
|
|
566
569
|
const iType = iTypeArt || inferredElement;
|
|
567
570
|
// FIXME: The coding above returns incorrect iType for expand on associations
|
|
568
571
|
|
|
569
|
-
// $
|
|
572
|
+
// $enclosed: maybe composition was changed to association; we allow that change here.
|
|
570
573
|
const compToAssoc = sType === model.definitions['cds.Association'] && inferredElement.target;
|
|
571
574
|
|
|
572
575
|
// xor: could be missing a type;
|
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
const {
|
|
6
6
|
forEachGeneric,
|
|
7
7
|
forEachInOrder,
|
|
8
|
+
isBetaEnabled,
|
|
8
9
|
} = require('../base/model');
|
|
9
10
|
const { dictLocation, weakLocation, weakRefLocation } = require('../base/location');
|
|
10
11
|
|
|
@@ -18,6 +19,7 @@ const {
|
|
|
18
19
|
traverseQueryPost,
|
|
19
20
|
traverseQueryExtra,
|
|
20
21
|
setExpandStatus,
|
|
22
|
+
getUnderlyingBuiltinType,
|
|
21
23
|
} = require('./utils');
|
|
22
24
|
const { Location } = require('../base/location');
|
|
23
25
|
const { CompilerAssertion } = require('../base/error');
|
|
@@ -39,6 +41,12 @@ function tweakAssocs( model ) {
|
|
|
39
41
|
getOrigin,
|
|
40
42
|
} = model.$functions;
|
|
41
43
|
|
|
44
|
+
Object.assign(model.$functions, {
|
|
45
|
+
firstProjectionForPath,
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
const isV5preview = isBetaEnabled( model.options, 'v5preview' );
|
|
49
|
+
|
|
42
50
|
// Phase 5: rewrite associations
|
|
43
51
|
model._entities.forEach( rewriteArtifact );
|
|
44
52
|
// Think hard whether an on condition rewrite can lead to a new cyclic
|
|
@@ -448,14 +456,27 @@ function tweakAssocs( model ) {
|
|
|
448
456
|
};
|
|
449
457
|
setArtifactLink( elem.type, assocType._artifact );
|
|
450
458
|
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
459
|
+
if (!isV5preview) { // TODO(v5): Remove, only use $enclosed
|
|
460
|
+
elem.$filtered = {
|
|
461
|
+
val: true,
|
|
462
|
+
literal: 'boolean',
|
|
463
|
+
location,
|
|
464
|
+
$inferred: '$generated',
|
|
465
|
+
};
|
|
466
|
+
}
|
|
467
|
+
|
|
468
|
+
const isComp = (getUnderlyingBuiltinType( assoc )?.name?.id === 'cds.Composition');
|
|
469
|
+
if (isComp) {
|
|
470
|
+
elem.$enclosed = {
|
|
471
|
+
val: true,
|
|
472
|
+
literal: 'boolean',
|
|
473
|
+
location,
|
|
474
|
+
$inferred: '$generated',
|
|
475
|
+
};
|
|
476
|
+
}
|
|
457
477
|
}
|
|
458
478
|
|
|
479
|
+
|
|
459
480
|
/**
|
|
460
481
|
* Transform a filter on `assocPathStep` into an ON-condition.
|
|
461
482
|
* Paths inside the filter are rewritten relative to `assoc`, so they can be redirected
|
|
@@ -541,7 +562,7 @@ function tweakAssocs( model ) {
|
|
|
541
562
|
setLink( rhs, '_artifact', rhs.path[rhs.path.length - 1]._artifact );
|
|
542
563
|
|
|
543
564
|
if (elem.$syntax !== 'calc') { // different to lhs!
|
|
544
|
-
const projectedFk = firstProjectionForPath( rhs.path, nav.tableAlias, elem );
|
|
565
|
+
const projectedFk = firstProjectionForPath( rhs.path, 0, nav.tableAlias, elem );
|
|
545
566
|
rewritePath( rhs, projectedFk.item, elem, projectedFk.elem, elem.value.location );
|
|
546
567
|
}
|
|
547
568
|
|
|
@@ -596,7 +617,8 @@ function tweakAssocs( model ) {
|
|
|
596
617
|
return; // not $self or source element
|
|
597
618
|
if (expr.scope === 'param' || root.kind === '$parameters')
|
|
598
619
|
return; // are not allowed anyway - there was an error before
|
|
599
|
-
const
|
|
620
|
+
const startIndex = (root.kind === '$self' ? 1 : 0);
|
|
621
|
+
const result = firstProjectionForPath( expr.path, startIndex, tableAlias, assoc );
|
|
600
622
|
// For `assoc[…]`, ensure that we don't rewrite to another projection on `assoc`.
|
|
601
623
|
if (result.item && assoc._origin === result.item._artifact)
|
|
602
624
|
result.elem = assoc;
|
|
@@ -782,51 +804,60 @@ function navProjection( navigation, preferred ) {
|
|
|
782
804
|
|
|
783
805
|
|
|
784
806
|
/**
|
|
785
|
-
* For a path `a.b.c.d`, return a projection for the first path item that is projected
|
|
807
|
+
* For a path `a.b.c.d`, return a projection for the first path item that is projected,
|
|
808
|
+
* starting at `startIndex` in this path using the given navigation (table alias or
|
|
809
|
+
* navigation element).
|
|
786
810
|
* For example, if a query has multiple projections such as `a.b, a, a.b.c`, the
|
|
787
811
|
* _first_ possible projection will be used and the caller can rewrite `a.b.c.d` to `b.c.d`.
|
|
788
|
-
* This avoids
|
|
812
|
+
* This avoids `extend`s affect the ON-condition.
|
|
789
813
|
*
|
|
790
|
-
* The returned object `ret` has `ret.item`, which is the path item
|
|
791
|
-
* `ret.elem` is the element projection.
|
|
814
|
+
* The returned object `ret` has `ret.item`, which is the path item at index `ret.index`
|
|
815
|
+
* that is projected. `ret.elem` is the element projection.
|
|
792
816
|
*
|
|
793
817
|
* @param {any[]} path
|
|
794
|
-
* @param {
|
|
795
|
-
* @param {object}
|
|
818
|
+
* @param {number} startIndex
|
|
819
|
+
* @param {object} nav
|
|
820
|
+
* @param {object} elem Preferred association/element that should be used if projected.
|
|
796
821
|
* @return {{elem: object, item: object}|null}
|
|
797
822
|
*/
|
|
798
|
-
function firstProjectionForPath( path,
|
|
799
|
-
|
|
800
|
-
const root = viaSelf ? 1 : 0;
|
|
801
|
-
if (root >= path.length) // e.g. just `$self` path item
|
|
823
|
+
function firstProjectionForPath( path, startIndex, nav, elem ) {
|
|
824
|
+
if (startIndex >= path.length) // e.g. just `$self` path item
|
|
802
825
|
return { item: undefined, elem: {} };
|
|
803
826
|
|
|
827
|
+
let tableAlias = nav;
|
|
828
|
+
while (tableAlias.kind === '$navElement')
|
|
829
|
+
tableAlias = tableAlias._parent;
|
|
830
|
+
|
|
804
831
|
// We want to use the _first_ valid projection that is written by the user (if the preferred
|
|
805
|
-
// `assoc` is not directly projected). To achieve that, look into the
|
|
832
|
+
// `assoc` is not directly projected). To achieve that, look into the query's elements.
|
|
806
833
|
const selectedElements = Object.values(tableAlias._parent.elements);
|
|
807
|
-
|
|
808
|
-
let
|
|
809
|
-
|
|
834
|
+
|
|
835
|
+
let proj = null;
|
|
836
|
+
let navItem = nav;
|
|
837
|
+
for (let i = startIndex; i < path.length; ++i) {
|
|
838
|
+
const item = path[i];
|
|
810
839
|
navItem = item?.id && navItem.elements?.[item.id];
|
|
811
840
|
if (!navItem) {
|
|
812
841
|
break;
|
|
813
842
|
}
|
|
814
843
|
else if (navItem._projections) {
|
|
815
|
-
const
|
|
816
|
-
if (
|
|
844
|
+
const projElem = navProjection( navItem, elem );
|
|
845
|
+
if (projElem && projElem === elem) {
|
|
817
846
|
// in case the specified association is found, _always_ use it.
|
|
818
|
-
return { item, elem };
|
|
847
|
+
return { index: i, item, elem };
|
|
819
848
|
}
|
|
820
|
-
else if (
|
|
821
|
-
const
|
|
822
|
-
proj
|
|
849
|
+
else if (projElem) {
|
|
850
|
+
const queryIndex = selectedElements.indexOf(projElem);
|
|
851
|
+
if (!proj || queryIndex < proj.queryIndex) {
|
|
852
|
+
proj = {
|
|
853
|
+
index: i, item, elem: projElem, queryIndex,
|
|
854
|
+
};
|
|
855
|
+
}
|
|
823
856
|
}
|
|
824
857
|
}
|
|
825
858
|
}
|
|
826
859
|
|
|
827
|
-
return
|
|
828
|
-
? { item: path[root], elem: null }
|
|
829
|
-
: proj.reduce( (acc, curr) => (acc.index > curr.index ? curr : acc), proj[0] ); // first
|
|
860
|
+
return proj || { index: startIndex, item: path[startIndex], elem: null };
|
|
830
861
|
}
|
|
831
862
|
|
|
832
863
|
/**
|
|
@@ -98,15 +98,21 @@
|
|
|
98
98
|
//
|
|
99
99
|
// Select Item via Origin
|
|
100
100
|
// ----------------------
|
|
101
|
-
// A bare select item that gets an annotation via propagation from
|
|
102
|
-
// similar to an element that gets it via an include.
|
|
101
|
+
// A bare select item of path length one, that gets an annotation via propagation from
|
|
102
|
+
// its origin, behaves similar to an element that gets it via an include.
|
|
103
103
|
// However, elements may have been renamed or may not be available at all.
|
|
104
104
|
// On top of that, they may be inside nested projections (expand).
|
|
105
|
+
// Or even simpler: sub-elements may have been selected.
|
|
105
106
|
//
|
|
106
107
|
// Instead of changing the path prefix, we need to check if the referenced path
|
|
107
108
|
// was projected or if a prefix was projected (e.g. for structures or associations).
|
|
108
109
|
// The same rules as for ON-condition rewriting apply.
|
|
109
110
|
//
|
|
111
|
+
// Furthermore, as the target is a select item, and this select item belongs to a table
|
|
112
|
+
// alias, we should rewrite all annotation paths only to projected elements of that
|
|
113
|
+
// table alias. Cross-rewriting between table aliases should not be done.
|
|
114
|
+
// This is the same we do for association rewriting.
|
|
115
|
+
//
|
|
110
116
|
// TODO:
|
|
111
117
|
// For now, we do not rewrite sub-structure elements. The whole structure needs
|
|
112
118
|
// to be projected or the select item isn't considered. That is, `expand {*}`
|
|
@@ -158,6 +164,7 @@ class AnnoRewriteConfig {
|
|
|
158
164
|
viaExpand;
|
|
159
165
|
viaExpandType;
|
|
160
166
|
isInFilter;
|
|
167
|
+
tokenExpr;
|
|
161
168
|
}
|
|
162
169
|
|
|
163
170
|
function xprRewriteFns( model ) {
|
|
@@ -167,6 +174,7 @@ function xprRewriteFns( model ) {
|
|
|
167
174
|
resolvePath,
|
|
168
175
|
navigationEnv,
|
|
169
176
|
resolvePathRoot,
|
|
177
|
+
firstProjectionForPath,
|
|
170
178
|
} = model.$functions;
|
|
171
179
|
|
|
172
180
|
return {
|
|
@@ -275,6 +283,11 @@ function xprRewriteFns( model ) {
|
|
|
275
283
|
root._parent.kind !== 'function'))
|
|
276
284
|
return reportAnnoRewriteError( expr, config, 'unsupported' );
|
|
277
285
|
|
|
286
|
+
// magic variables / replacement variables are never rewritten; they can't
|
|
287
|
+
// have filters nor can they point to elements.
|
|
288
|
+
if (expr._artifact?.kind === 'builtin')
|
|
289
|
+
return null;
|
|
290
|
+
|
|
278
291
|
let hasError = false;
|
|
279
292
|
if (config.isViaType || config.isViaCalcElement)
|
|
280
293
|
hasError = adaptPathPrefixViaType( expr, config );
|
|
@@ -335,12 +348,41 @@ function xprRewriteFns( model ) {
|
|
|
335
348
|
*/
|
|
336
349
|
function getRootEnv( expr, config ) {
|
|
337
350
|
const { target } = config;
|
|
351
|
+
|
|
352
|
+
if (expr.scope === 'param') // path is absolute
|
|
353
|
+
return navigationEnv( config.targetRoot, null, null, 'nav' );
|
|
354
|
+
|
|
355
|
+
// On select items, use navigation elements or table alias
|
|
356
|
+
// TODO: Expand/inline paths don't have a `_navigation` property on their last
|
|
357
|
+
// path step, yet. We need to implement expand/inline.
|
|
358
|
+
const isSimpleSelectItem = target.value?.path && target._main?.query && !target._pathHead;
|
|
359
|
+
if (isSimpleSelectItem) {
|
|
360
|
+
const isSelfPath = (expr.path[0]?._navigation?.kind === '$self');
|
|
361
|
+
if (isSelfPath) {
|
|
362
|
+
// Path is absolute, use table alias to resolve it.
|
|
363
|
+
let tableAlias = target.value.path[0]._navigation;
|
|
364
|
+
while (tableAlias && tableAlias.kind === '$navElement')
|
|
365
|
+
tableAlias = tableAlias._parent;
|
|
366
|
+
if (tableAlias)
|
|
367
|
+
return tableAlias;
|
|
368
|
+
}
|
|
369
|
+
else {
|
|
370
|
+
// Path is relative
|
|
371
|
+
const nav = target.value.path[target.value.path.length - 1]._navigation?._parent;
|
|
372
|
+
if (nav)
|
|
373
|
+
return nav;
|
|
374
|
+
}
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
if (isSimpleSelectItem && model.options.testMode)
|
|
378
|
+
throw new CompilerAssertion(`select item has no table alias: ${ JSON.stringify(target.value.path) }`);
|
|
379
|
+
|
|
338
380
|
if (isAnnoPathAbsolute( expr ))
|
|
339
381
|
return navigationEnv( config.targetRoot, null, null, 'nav' );
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
return navigationEnv( target._parent, null, null, 'nav' );
|
|
382
|
+
|
|
383
|
+
// anno path is relative / element reference (others were already rejected)
|
|
384
|
+
// if the target is a root artifact, use it. Otherwise, use its parent.
|
|
385
|
+
return navigationEnv( isAnnoRootArt( target ) ? target : target._parent, null, null, 'nav' );
|
|
344
386
|
}
|
|
345
387
|
|
|
346
388
|
/**
|
|
@@ -350,15 +392,20 @@ function xprRewriteFns( model ) {
|
|
|
350
392
|
* @returns {boolean}
|
|
351
393
|
*/
|
|
352
394
|
function rewriteGenericAnnoPath( expr, config, refCtx ) {
|
|
353
|
-
const
|
|
354
|
-
|
|
395
|
+
const isAbsolute = isAnnoPathAbsolute( expr );
|
|
396
|
+
const rootIndex = isAbsolute ? 1 : 0;
|
|
355
397
|
|
|
356
|
-
//
|
|
398
|
+
// We get the root environment now, even though below we resolve the root item
|
|
399
|
+
// again if it was absolute (e.g. $self). We do so, because for queries, we
|
|
400
|
+
// want to respect the select item's corresponding table alias.
|
|
401
|
+
const rootEnv = getRootEnv( expr, config );
|
|
402
|
+
|
|
403
|
+
// reset artifact link; we'll set it again
|
|
357
404
|
setArtifactLink( expr, null );
|
|
358
405
|
|
|
359
406
|
// Adapt root path, as it isn't rewritten in rewriteItem
|
|
360
407
|
const rootItem = expr.path[0];
|
|
361
|
-
if (
|
|
408
|
+
if (isAbsolute) {
|
|
362
409
|
delete rootItem._artifact;
|
|
363
410
|
delete rootItem._navigation;
|
|
364
411
|
// TODO: What about `up_`? Shouldn't we set `_navigation` as well?
|
|
@@ -368,10 +415,10 @@ function xprRewriteFns( model ) {
|
|
|
368
415
|
return reportAnnoRewriteError( expr, config );
|
|
369
416
|
}
|
|
370
417
|
|
|
418
|
+
let env = rootEnv;
|
|
371
419
|
let art = rootItem._artifact;
|
|
372
420
|
for (let i = rootIndex; i < expr.path.length; ++i) {
|
|
373
|
-
|
|
374
|
-
art = rewriteItem( expr, config, env, item );
|
|
421
|
+
art = rewriteItem( expr, config, env, i );
|
|
375
422
|
if (!art)
|
|
376
423
|
return reportAnnoRewriteError( expr, config );
|
|
377
424
|
env = navigationEnv( art, null, null, 'nav' );
|
|
@@ -581,48 +628,74 @@ function xprRewriteFns( model ) {
|
|
|
581
628
|
return self;
|
|
582
629
|
}
|
|
583
630
|
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
631
|
+
/**
|
|
632
|
+
* Rewrite the item in `expr.path` at the given index.
|
|
633
|
+
* This function may splice the array if more than one path segment
|
|
634
|
+
* is replaced by a single item (e.g. in queries).
|
|
635
|
+
*
|
|
636
|
+
* @param {XSN.Expression} expr
|
|
637
|
+
* @param {AnnoRewriteConfig} config
|
|
638
|
+
* @param {object} env
|
|
639
|
+
* @param {number} index
|
|
640
|
+
* @returns {*|null}
|
|
641
|
+
*/
|
|
642
|
+
function rewriteItem( expr, config, env, index ) {
|
|
643
|
+
const item = expr.path[index];
|
|
644
|
+
const rewriteTarget = findRewriteTarget( expr, index, env, config.target );
|
|
645
|
+
const found = setArtifactLink( item, rewriteTarget[0] );
|
|
646
|
+
if (!found)
|
|
647
|
+
return null;
|
|
648
|
+
|
|
649
|
+
if (item.id !== found.name.id) {
|
|
650
|
+
// Path was rewritten; original token text string is no longer accurate
|
|
651
|
+
config.tokenExpr.$tokenTexts = true;
|
|
652
|
+
item.id = found.name.id;
|
|
593
653
|
}
|
|
594
|
-
|
|
654
|
+
|
|
655
|
+
if (rewriteTarget[1] > index)
|
|
656
|
+
expr.path.splice(index + 1, rewriteTarget[1] - index);
|
|
657
|
+
|
|
658
|
+
return rewriteTarget[0];
|
|
595
659
|
}
|
|
596
660
|
|
|
597
|
-
function findRewriteTarget(
|
|
598
|
-
if (
|
|
599
|
-
|
|
661
|
+
function findRewriteTarget( expr, index, env, target ) {
|
|
662
|
+
if (env.kind === '$navElement' || env.kind === '$tableAlias') {
|
|
663
|
+
const r = firstProjectionForPath( expr.path, index, env, target );
|
|
664
|
+
return [ r.elem, r.index ];
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
const item = expr.path[index];
|
|
668
|
+
// Not a query -> no $navElement -> use `elements`
|
|
669
|
+
if (!env.query && env.kind !== 'select') {
|
|
670
|
+
if (env.elements?.[item.id])
|
|
671
|
+
return [ env.elements[item.id], index ];
|
|
672
|
+
return [ null, expr.path.length ];
|
|
673
|
+
}
|
|
600
674
|
const items = (env._leadingQuery || env)._combined?.[item.id];
|
|
601
|
-
const
|
|
675
|
+
const allNavs = !items || Array.isArray(items) ? items : [ items ];
|
|
676
|
+
|
|
677
|
+
// If the annotation target itself has a table alias, require projections of that
|
|
678
|
+
// table alias. Of course, that only works if we're talking about the same query.
|
|
679
|
+
const tableAlias = (target._main?._origin === item._artifact._main &&
|
|
680
|
+
target.value?.path[0]?._navigation?.kind === '$tableAlias')
|
|
681
|
+
? target.value.path[0]._navigation : null;
|
|
602
682
|
|
|
603
683
|
// Look at all table aliase that could project `item` and only select
|
|
604
684
|
// those that have actual projections.
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
//
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
projected = taProjected;
|
|
685
|
+
const navs = allNavs?.filter(p => p._origin === item._artifact &&
|
|
686
|
+
(!tableAlias || tableAlias === p._parent));
|
|
687
|
+
if (!navs || navs.length === 0)
|
|
688
|
+
return [ null, expr.path.length ];
|
|
689
|
+
|
|
690
|
+
// If there are multiple navigations for the element, just use the first that matches.
|
|
691
|
+
// In case of table aliases, it's just one.
|
|
692
|
+
for (const nav of navs) {
|
|
693
|
+
const r = firstProjectionForPath( expr.path, index, nav._parent, target );
|
|
694
|
+
if (r.elem)
|
|
695
|
+
return [ r.elem, r.index ];
|
|
617
696
|
}
|
|
618
697
|
|
|
619
|
-
|
|
620
|
-
projected = projected[0];
|
|
621
|
-
|
|
622
|
-
// If there are multiple projections, check if the annotation target is
|
|
623
|
-
// projected as well, otherwise, simply take the first one.
|
|
624
|
-
return projected._projections.find(proj => proj === target) ||
|
|
625
|
-
projected._projections[0] || null;
|
|
698
|
+
return [ null, expr.path.length ];
|
|
626
699
|
}
|
|
627
700
|
}
|
|
628
701
|
|
|
@@ -248,8 +248,15 @@ function xpr2edmJson( carrier, anno, location, options, messageFunctions ) {
|
|
|
248
248
|
transform['<='] = op('$Le');
|
|
249
249
|
transform.$Le = noOp;
|
|
250
250
|
transform.in = (parent, prop, xpr) => {
|
|
251
|
-
|
|
252
|
-
|
|
251
|
+
let args = xpr[1].list;
|
|
252
|
+
if (!args) {
|
|
253
|
+
if (Array.isArray(xpr[1].xpr))
|
|
254
|
+
args = xpr[1].xpr;
|
|
255
|
+
else
|
|
256
|
+
args = [ xpr[1] ];
|
|
257
|
+
}
|
|
258
|
+
evalArgs({ min: 1 }, args, prop);
|
|
259
|
+
parent.$In = [ xpr[0], args ];
|
|
253
260
|
delete parent[prop];
|
|
254
261
|
transformExpression(parent, undefined, transform);
|
|
255
262
|
};
|
|
@@ -293,9 +300,9 @@ function xpr2edmJson( carrier, anno, location, options, messageFunctions ) {
|
|
|
293
300
|
transform.$Neg = noOp;
|
|
294
301
|
transform['*'] = op('$Mul');
|
|
295
302
|
transform.$Mul = noOp;
|
|
296
|
-
transform['/'] = op('$
|
|
297
|
-
transform.$
|
|
298
|
-
// $
|
|
303
|
+
transform['/'] = op('$DivBy');
|
|
304
|
+
transform.$DivBy = noOp;
|
|
305
|
+
// $Div, $Mod are functions
|
|
299
306
|
//----------------------------------
|
|
300
307
|
// LITERALS
|
|
301
308
|
transform.val = (parent, prop, xpr, csnPath, parentparent, parentprop) => {
|
|
@@ -575,6 +582,15 @@ function xpr2edmJson( carrier, anno, location, options, messageFunctions ) {
|
|
|
575
582
|
}
|
|
576
583
|
}
|
|
577
584
|
else {
|
|
585
|
+
// Error out for arbitrary types until we know better
|
|
586
|
+
// probably todo: Check for reachability of arb type names such as namespace
|
|
587
|
+
// reqDef entry etc...
|
|
588
|
+
if (typeDef) { // eslint-disable-line no-lonely-if
|
|
589
|
+
error('odata-anno-xpr-type', location, {
|
|
590
|
+
anno, op: `${xpr}(…)`, type: `${typeDef}`, '#': 'edm',
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
/*
|
|
578
594
|
typeFacets.forEach((facet) => {
|
|
579
595
|
if (facet.args.length === 1 && facet.args[0].val) {
|
|
580
596
|
const facetName = facet.func.startsWith('$') ? facet.func.slice(1) : facet.func;
|
|
@@ -582,6 +598,7 @@ function xpr2edmJson( carrier, anno, location, options, messageFunctions ) {
|
|
|
582
598
|
parent[`$${facetName}`] = facet.args[0].val;
|
|
583
599
|
}
|
|
584
600
|
});
|
|
601
|
+
*/
|
|
585
602
|
}
|
|
586
603
|
|
|
587
604
|
delete typeProp.args;
|
|
@@ -648,8 +665,8 @@ function xpr2edmJson( carrier, anno, location, options, messageFunctions ) {
|
|
|
648
665
|
const funcDefs = {
|
|
649
666
|
$Has: twoArgs,
|
|
650
667
|
Has: [ twoArgs, dollar ],
|
|
651
|
-
$
|
|
652
|
-
|
|
668
|
+
$Div: twoArgs,
|
|
669
|
+
Div: [ twoArgs, dollar ],
|
|
653
670
|
$Mod: twoArgs,
|
|
654
671
|
Mod: [ twoArgs, dollar ],
|
|
655
672
|
$Apply: () => {
|
|
@@ -1034,7 +1034,7 @@ function csn2annotationEdm( reqDefs, reqDefsUtils, csnVocabularies, serviceName,
|
|
|
1034
1034
|
typeName = dTypeName.split('.')[1];
|
|
1035
1035
|
}
|
|
1036
1036
|
|
|
1037
|
-
if (value) {
|
|
1037
|
+
if (typeof value === 'string') {
|
|
1038
1038
|
// replace all occurrences of '.' by '/' up to first '@'
|
|
1039
1039
|
value = value.split('@').map((o, i) => (i === 0 ? o.replace(/\./g, '/') : o)).join('@');
|
|
1040
1040
|
}
|
package/lib/json/to-csn.js
CHANGED
|
@@ -93,7 +93,8 @@ const transformers = {
|
|
|
93
93
|
cardinality, // also in pathItem: after 'id', before 'where'
|
|
94
94
|
targetAspect,
|
|
95
95
|
target,
|
|
96
|
-
$filtered: value, // assoc+filter
|
|
96
|
+
$filtered: value, // assoc+filter v4
|
|
97
|
+
$enclosed: value, // comp+filter v5
|
|
97
98
|
foreignKeys,
|
|
98
99
|
enum: enumDict,
|
|
99
100
|
items,
|
package/lib/model/csnRefs.js
CHANGED
|
@@ -917,11 +917,11 @@ function csnRefs( csn, universalReady ) {
|
|
|
917
917
|
: { $aliases: Object.create(null), $next: pcache.$next };
|
|
918
918
|
}
|
|
919
919
|
|
|
920
|
-
function initColumnElement( col, colIndex, parentElementOrQueryCache ) {
|
|
920
|
+
function initColumnElement( col, colIndex, parentElementOrQueryCache, externalElements ) {
|
|
921
921
|
if (col === '*')
|
|
922
922
|
return;
|
|
923
923
|
if (col.inline) {
|
|
924
|
-
col.inline.map( c => initColumnElement( c, null, parentElementOrQueryCache ) );
|
|
924
|
+
col.inline.map( c => initColumnElement( c, null, parentElementOrQueryCache, externalElements ) );
|
|
925
925
|
return;
|
|
926
926
|
}
|
|
927
927
|
setCache( col, '_parent', // not set for query (has property _select)
|
|
@@ -936,11 +936,18 @@ function csnRefs( csn, universalReady ) {
|
|
|
936
936
|
|
|
937
937
|
while (type.items)
|
|
938
938
|
type = type.items;
|
|
939
|
+
if (!type.elements) {
|
|
940
|
+
// in OData backend, the sub elements from a column with expand might have
|
|
941
|
+
// been “externalized” into a named type. No backward _column link is
|
|
942
|
+
// possible this way, of course...
|
|
943
|
+
type = artifactRef( type.type );
|
|
944
|
+
externalElements = true;
|
|
945
|
+
}
|
|
939
946
|
const elem = setCache( col, '_element', type.elements[as] );
|
|
940
|
-
if (elem)
|
|
947
|
+
if (elem && !externalElements) // TODO to.sql: something is strange if `elem` is not set
|
|
941
948
|
setCache( elem, '_column', col );
|
|
942
949
|
if (col.expand)
|
|
943
|
-
col.expand.map( c => initColumnElement( c, null, elem ) );
|
|
950
|
+
col.expand.map( c => initColumnElement( c, null, elem, externalElements ) );
|
|
944
951
|
}
|
|
945
952
|
|
|
946
953
|
// property name convention in cache:
|
package/lib/render/toCdl.js
CHANGED
|
@@ -614,7 +614,7 @@ function csnToCdl( csn, options, msg ) {
|
|
|
614
614
|
else if (element['#'] !== undefined) { // enum symbol reference
|
|
615
615
|
result += ` = #${element['#']}`;
|
|
616
616
|
}
|
|
617
|
-
else if (!isCalcElement || !isDirectAssocOrComp(element.type) && !element.$filtered) {
|
|
617
|
+
else if (!isCalcElement || !isDirectAssocOrComp(element.type) && !element.$filtered && !element.$enclosed) {
|
|
618
618
|
// If the element is a calculated element _and_ a direct association or
|
|
619
619
|
// composition, we'd render `Association to F on (cond) = calcValue;` which
|
|
620
620
|
// would alter the ON-condition.
|
|
@@ -187,28 +187,46 @@ function createReferentialConstraints( csn, options ) {
|
|
|
187
187
|
* @param {CSN.PathSegment | object} upLinkFor the name of the composition which used this association in a `$self = <comp>.<up_>` comparison
|
|
188
188
|
* it is used for a comment in the constraint, which is only printed out in test-mode
|
|
189
189
|
*/
|
|
190
|
-
function attachConstraintsToDependentKeys(
|
|
190
|
+
function attachConstraintsToDependentKeys(
|
|
191
|
+
dependentKeys,
|
|
192
|
+
parentKeys,
|
|
193
|
+
parentTable,
|
|
194
|
+
sourceAssociation,
|
|
195
|
+
upLinkFor = null
|
|
196
|
+
) {
|
|
191
197
|
while (dependentKeys.length > 0) {
|
|
192
198
|
const dependentKeyValuePair = dependentKeys.pop();
|
|
193
199
|
const dependentKey = dependentKeyValuePair[1];
|
|
194
200
|
// if it already has a dependent key assigned, do not overwrite it.
|
|
195
201
|
// this is the case for <up_> associations in on-conditions of compositions
|
|
196
|
-
|
|
202
|
+
const { $foreignKeyConstraint } = dependentKey;
|
|
203
|
+
// in contrast to foreign keys which stem from managed associations,
|
|
204
|
+
// a tenant foreign key column may have multiple parent keys as partners
|
|
205
|
+
const tenantForeignKey = isTenant && dependentKeyValuePair[0] === 'tenant';
|
|
206
|
+
if ($foreignKeyConstraint && (!tenantForeignKey || $foreignKeyConstraint.upLinkFor)) {
|
|
197
207
|
return;
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
208
|
+
}
|
|
209
|
+
else if ($foreignKeyConstraint && tenantForeignKey) {
|
|
210
|
+
parentKeys.pop();
|
|
211
|
+
$foreignKeyConstraint.sourceAssociation.push(sourceAssociation);
|
|
212
|
+
}
|
|
213
|
+
else {
|
|
214
|
+
const parentKeyValuePair = parentKeys.pop();
|
|
215
|
+
const parentKeyName = parentKeyValuePair[0];
|
|
216
|
+
|
|
217
|
+
const constraint = {
|
|
218
|
+
parentKey: parentKeyName,
|
|
219
|
+
parentTable,
|
|
220
|
+
upLinkFor,
|
|
221
|
+
sourceAssociation: tenantForeignKey
|
|
222
|
+
? [ sourceAssociation ]
|
|
223
|
+
: sourceAssociation,
|
|
224
|
+
onDelete: upLinkFor ? 'CASCADE' : 'RESTRICT',
|
|
225
|
+
validated,
|
|
226
|
+
enforced,
|
|
227
|
+
};
|
|
228
|
+
dependentKey.$foreignKeyConstraint = constraint;
|
|
229
|
+
}
|
|
212
230
|
}
|
|
213
231
|
}
|
|
214
232
|
|
|
@@ -473,11 +491,11 @@ function createReferentialConstraints( csn, options ) {
|
|
|
473
491
|
}
|
|
474
492
|
|
|
475
493
|
/**
|
|
476
|
-
* Creates the final referential constraints from all dependent key <-> parent key pairs stemming from the same
|
|
494
|
+
* Creates the final referential constraints from all dependent key <-> parent key pairs stemming from the same sourceAssociation
|
|
477
495
|
* and attaches it to the given artifact.
|
|
478
496
|
*
|
|
479
497
|
* Go over all elements with $foreignKeyConstraint property:
|
|
480
|
-
* - Find all other elements in artifact with the same
|
|
498
|
+
* - Find all other elements in artifact with the same sourceAssociation
|
|
481
499
|
* - Create constraints with the information supplied by $parentKey, $parentTable and $onDelete
|
|
482
500
|
*
|
|
483
501
|
* @param {CSN.Artifact} artifact
|
|
@@ -485,6 +503,23 @@ function createReferentialConstraints( csn, options ) {
|
|
|
485
503
|
*/
|
|
486
504
|
function collectAndAttachReferentialConstraints( artifact, artifactName ) {
|
|
487
505
|
const referentialConstraints = Object.create(null);
|
|
506
|
+
|
|
507
|
+
// tenant foreign keys may have multiple parent keys
|
|
508
|
+
// process tenant foreign key first
|
|
509
|
+
if (isTenant && artifact.elements?.tenant) {
|
|
510
|
+
const element = artifact.elements.tenant;
|
|
511
|
+
if (element.$foreignKeyConstraint) {
|
|
512
|
+
const $foreignKeyConstraint = Object.assign({}, element.$foreignKeyConstraint);
|
|
513
|
+
delete element.$foreignKeyConstraint;
|
|
514
|
+
// create a foreign key constraint for the tenant column with each association in the dependent entity
|
|
515
|
+
$foreignKeyConstraint.sourceAssociation.forEach((sourceAssociation) => {
|
|
516
|
+
const copy = Object.assign({}, $foreignKeyConstraint);
|
|
517
|
+
copy.sourceAssociation = sourceAssociation;
|
|
518
|
+
createReferentialConstraints(copy, 'tenant');
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
|
|
488
523
|
for (const elementName in artifact.elements) {
|
|
489
524
|
const element = artifact.elements[elementName];
|
|
490
525
|
if (!element.$foreignKeyConstraint)
|
|
@@ -492,16 +527,34 @@ function createReferentialConstraints( csn, options ) {
|
|
|
492
527
|
// copy constraint property, and delete it from the element
|
|
493
528
|
const $foreignKeyConstraint = Object.assign({}, element.$foreignKeyConstraint);
|
|
494
529
|
delete element.$foreignKeyConstraint;
|
|
530
|
+
createReferentialConstraints($foreignKeyConstraint, elementName);
|
|
531
|
+
}
|
|
532
|
+
if (Object.keys(referentialConstraints).length) {
|
|
533
|
+
if (!('$tableConstraints' in artifact))
|
|
534
|
+
artifact.$tableConstraints = Object.create(null);
|
|
535
|
+
|
|
536
|
+
artifact.$tableConstraints.referential = referentialConstraints;
|
|
537
|
+
}
|
|
538
|
+
|
|
539
|
+
/**
|
|
540
|
+
* Creates referential constraints for database relationships. This function constructs constraints based on foreign key information and element names,
|
|
541
|
+
* and determines deletion rules based on the existing constraints and options. It manages dependencies and names for constraints dynamically during
|
|
542
|
+
* execution.
|
|
543
|
+
*
|
|
544
|
+
* @param {object} $foreignKeyConstraint - An object encapsulating details about the foreign key constraint
|
|
545
|
+
* @param {string} elementName - The name of the dependent element or table that is linked by the foreign key.
|
|
546
|
+
*/
|
|
547
|
+
function createReferentialConstraints($foreignKeyConstraint, elementName) {
|
|
495
548
|
const { parentTable } = $foreignKeyConstraint;
|
|
496
549
|
const parentKey = [ $foreignKeyConstraint.parentKey ];
|
|
497
550
|
const dependentKey = [ elementName ];
|
|
498
551
|
const onDeleteRules = new Set();
|
|
499
552
|
onDeleteRules.add($foreignKeyConstraint.onDelete);
|
|
500
553
|
forEach(artifact.elements, (foreignKeyName, foreignKey) => {
|
|
501
|
-
// find all other `$foreignKeyConstraint`s with same
|
|
554
|
+
// find all other `$foreignKeyConstraint`s with same `sourceAssociation` and same `parentTable`
|
|
502
555
|
const matchingForeignKeyFound = foreignKey.$foreignKeyConstraint &&
|
|
503
|
-
|
|
504
|
-
|
|
556
|
+
foreignKey.$foreignKeyConstraint.sourceAssociation === $foreignKeyConstraint.sourceAssociation &&
|
|
557
|
+
foreignKey.$foreignKeyConstraint.parentTable === $foreignKeyConstraint.parentTable;
|
|
505
558
|
if (!matchingForeignKeyFound)
|
|
506
559
|
return;
|
|
507
560
|
|
|
@@ -536,12 +589,6 @@ function createReferentialConstraints( csn, options ) {
|
|
|
536
589
|
enforced: $foreignKeyConstraint.enforced,
|
|
537
590
|
};
|
|
538
591
|
}
|
|
539
|
-
if (Object.keys(referentialConstraints).length) {
|
|
540
|
-
if (!('$tableConstraints' in artifact))
|
|
541
|
-
artifact.$tableConstraints = Object.create(null);
|
|
542
|
-
|
|
543
|
-
artifact.$tableConstraints.referential = referentialConstraints;
|
|
544
|
-
}
|
|
545
592
|
}
|
|
546
593
|
}
|
|
547
594
|
|
|
@@ -242,7 +242,7 @@ function flattenAllStructStepsInRefs( csn, options, messageFunctions, resolved,
|
|
|
242
242
|
// full path into target, uncomment this line and
|
|
243
243
|
// comment/remove setProp in expansion.js
|
|
244
244
|
// setProp(parent, '$structRef', parent.ref);
|
|
245
|
-
parent.ref = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes);
|
|
245
|
+
[ parent.ref ] = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes);
|
|
246
246
|
resolved.set(parent, { links, art, scope });
|
|
247
247
|
// Explicitly set implicit alias for things that are now flattened - but only in columns
|
|
248
248
|
// TODO: Can this be done elegantly during expand phase already?
|
|
@@ -543,7 +543,7 @@ function handleManagedAssociationsAndCreateForeignKeys( csn, options, messageFun
|
|
|
543
543
|
if (clone.ref) {
|
|
544
544
|
clone.ref[clone.ref.length - 1] = flatElemName;
|
|
545
545
|
// Now we need to properly flatten the whole ref
|
|
546
|
-
clone.ref = flattenStructStepsInRef(clone.ref, pathToKey);
|
|
546
|
+
[ clone.ref ] = flattenStructStepsInRef(clone.ref, pathToKey);
|
|
547
547
|
}
|
|
548
548
|
if (!clone.as) {
|
|
549
549
|
clone.as = flatElemName;
|
|
@@ -951,7 +951,7 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
|
|
|
951
951
|
if (isFulltextIndex)
|
|
952
952
|
error(null, path, { name: artName }, 'A fulltext index can\'t be defined on a structured element $(NAME)');
|
|
953
953
|
// First, compute the name from the path, e.g ['s', 's1', 's2' ] will result in 'S_s1_s2' ...
|
|
954
|
-
const refPath = flattenStructStepsInRef(val.ref, path);
|
|
954
|
+
const [ refPath ] = flattenStructStepsInRef(val.ref, path);
|
|
955
955
|
// ... and take this as the prefix for all elements
|
|
956
956
|
const flattenedElems = flattenStructuredElement(art, refPath, [], ['definitions', artName, 'elements']);
|
|
957
957
|
Object.keys(flattenedElems).forEach((elem, i, elems) => {
|
|
@@ -967,7 +967,7 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
|
|
|
967
967
|
}
|
|
968
968
|
else {
|
|
969
969
|
// The reference is not structured, so just replace it by a ref to the combined prefix path
|
|
970
|
-
const refPath = flattenStructStepsInRef(val.ref, path);
|
|
970
|
+
const [ refPath ] = flattenStructStepsInRef(val.ref, path);
|
|
971
971
|
flattenedIndex.push({ ref: refPath });
|
|
972
972
|
}
|
|
973
973
|
}
|
|
@@ -290,6 +290,7 @@ function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternal
|
|
|
290
290
|
// Later, the query can/must be rewritten as long as a flat OData CSN is published
|
|
291
291
|
// but this then operates on the entity/view which has all struct infos available
|
|
292
292
|
function flattenAndPrefixExprPaths(carrier, propNames, csnPath, rootPrefix, typeIdx, refParentIsItems = false) {
|
|
293
|
+
|
|
293
294
|
const refCheck = {
|
|
294
295
|
ref: (elemref, prop, xpr, path) => {
|
|
295
296
|
const { art, scope } = inspectRef(path);
|
|
@@ -316,53 +317,60 @@ function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternal
|
|
|
316
317
|
return `${head}`;
|
|
317
318
|
})();
|
|
318
319
|
|
|
320
|
+
let refChanged = false;
|
|
319
321
|
const absolutifier = {
|
|
320
322
|
ref : (parent, prop, xpr) => {
|
|
321
323
|
const head = xpr[0].id || xpr[0];
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
if(
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
324
|
+
let isPrefixed = false;
|
|
325
|
+
if(!isMagicVariable(head)) {
|
|
326
|
+
if (head === '$self' && typeIdx < rootPrefix.length) {
|
|
327
|
+
isPrefixed = true;
|
|
328
|
+
const [xprHead, ...xprTail] = xpr.slice(1, xpr.length);
|
|
329
|
+
if(xprHead) {
|
|
330
|
+
if (xprHead.id) {
|
|
331
|
+
xprHead.id = rootPrefix.slice(1, typeIdx).concat(xprHead.id).join('_');
|
|
332
|
+
parent[prop] = [ xprHead, ...xprTail ];
|
|
333
|
+
}
|
|
334
|
+
else
|
|
335
|
+
parent[prop] = [ rootPrefix.slice(1, typeIdx).concat(xprHead).join('_'), ...xprTail];
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
else if (head !== '$self' && !parent.param && rootPrefix.length > 2) {
|
|
339
|
+
isPrefixed = true;
|
|
340
|
+
const [xprHead, ...xprTail] = xpr;
|
|
341
|
+
if (!refParentIsItems) {
|
|
342
|
+
if (xprHead.id) {
|
|
343
|
+
xprHead.id = rootPrefix.slice(1, -1).concat(xprHead.id).join('_');
|
|
344
|
+
parent[prop] = [ xprHead, ...xprTail ];
|
|
345
|
+
}
|
|
346
|
+
else
|
|
347
|
+
parent[prop] = [ rootPrefix.slice(1, -1).concat(xprHead).join('_'), ...xprTail];
|
|
328
348
|
}
|
|
329
349
|
else
|
|
330
|
-
parent[prop] = [ rootPrefix.slice(
|
|
350
|
+
parent[prop] = [ ...rootPrefix.slice(0, rootPrefix.length-1), ...xpr];
|
|
351
|
+
}
|
|
352
|
+
if(isPrefixed) {
|
|
331
353
|
if (carrier.$scope === 'params')
|
|
332
354
|
parent.param = true;
|
|
333
355
|
else
|
|
334
356
|
parent[prop].unshift('$self');
|
|
335
357
|
}
|
|
336
358
|
}
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
if (!refParentIsItems) {
|
|
340
|
-
if (xprHead.id) {
|
|
341
|
-
xprHead.id = rootPrefix.slice(1, -1).concat(xprHead.id).join('_');
|
|
342
|
-
parent[prop] = [ xprHead, ...xprTail ];
|
|
343
|
-
}
|
|
344
|
-
else
|
|
345
|
-
parent[prop] = [ rootPrefix.slice(1, -1).concat(xprHead).join('_'), ...xprTail];
|
|
346
|
-
}
|
|
347
|
-
else
|
|
348
|
-
parent[prop] = [ ...rootPrefix.slice(0, rootPrefix.length-1), ...xpr];
|
|
349
|
-
if (carrier.$scope === 'params')
|
|
350
|
-
parent.param = true;
|
|
351
|
-
else
|
|
352
|
-
parent[prop].unshift('$self');
|
|
353
|
-
}
|
|
359
|
+
if(isPrefixed)
|
|
360
|
+
refChanged = isPrefixed;
|
|
354
361
|
}
|
|
355
362
|
}
|
|
356
|
-
|
|
357
363
|
propNames.forEach(pn => {
|
|
364
|
+
refChanged = false;
|
|
358
365
|
refCheck.anno = pn;
|
|
359
366
|
transformExpression(carrier, pn, [ refCheck, refFlattener ], csnPath);
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
propNames.forEach(pn => {
|
|
367
|
+
adaptRefs.forEach(fn =>
|
|
368
|
+
{ if( fn(refParentIsItems)) refChanged = true });
|
|
369
|
+
adaptRefs.length = 0;
|
|
364
370
|
transformExpression(carrier, pn, absolutifier, csnPath)
|
|
365
|
-
|
|
371
|
+
if(refChanged && carrier[pn]['='])
|
|
372
|
+
carrier[pn]['='] = true;
|
|
373
|
+
});
|
|
366
374
|
}
|
|
367
375
|
|
|
368
376
|
// TODO: This should be part of the generic path rewriting algorithm
|
|
@@ -437,13 +445,17 @@ function flattenAllStructStepsInRefs( csn, refFlattener, adaptRefs, inspectRef,
|
|
|
437
445
|
typeNames.forEach(tn => {
|
|
438
446
|
forEachMemberRecursively(csn.definitions[tn], (member, memberName, prop, csnPath) => {
|
|
439
447
|
Object.keys(member).filter(pn => pn[0] === '@').forEach(pn => {
|
|
448
|
+
let refChanged = false;
|
|
440
449
|
refCheck.anno = pn;
|
|
441
450
|
transformExpression(member, pn, [ refCheck, refFlattener ], csnPath);
|
|
451
|
+
adaptRefs.forEach(fn => {
|
|
452
|
+
if (fn(true, 1)) refChanged = true });
|
|
453
|
+
adaptRefs.length = 0;
|
|
454
|
+
if(refChanged && member[pn]['='])
|
|
455
|
+
member[pn]['='] = true;
|
|
442
456
|
});
|
|
443
457
|
}, [ 'definitions', tn ]);
|
|
444
458
|
})
|
|
445
|
-
adaptRefs.forEach(fn => fn(true, 1));
|
|
446
|
-
adaptRefs.length = 0;
|
|
447
459
|
}
|
|
448
460
|
|
|
449
461
|
function getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, options, resolved, pathDelimiter) {
|
|
@@ -468,6 +480,7 @@ function getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, optio
|
|
|
468
480
|
const adaptRefs = [];
|
|
469
481
|
const transformer = {
|
|
470
482
|
ref: (parent, prop, ref, path) => {
|
|
483
|
+
let refChanged = false;
|
|
471
484
|
const { links, art, scope } = inspectRef(path);
|
|
472
485
|
const resolvedLinkTypes = resolveLinkTypes(links);
|
|
473
486
|
setProp(parent, '$path', [ ...path ]);
|
|
@@ -480,7 +493,7 @@ function getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, optio
|
|
|
480
493
|
// full path into target, uncomment this line and
|
|
481
494
|
// comment/remove setProp in expansion.js
|
|
482
495
|
// setProp(parent, '$structRef', parent.ref);
|
|
483
|
-
parent.ref = flattenStructStepsInRef(ref,
|
|
496
|
+
[ parent.ref, refChanged ] = flattenStructStepsInRef(ref,
|
|
484
497
|
scopedPath, links, scope, resolvedLinkTypes,
|
|
485
498
|
suspend, suspendPos, parent.$bparam);
|
|
486
499
|
resolved.set(parent, { links, art, scope });
|
|
@@ -496,9 +509,10 @@ function getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, optio
|
|
|
496
509
|
(insideColumns(scopedPath) || insideKeys(scopedPath)) &&
|
|
497
510
|
!parent.as) {
|
|
498
511
|
parent.as = lastRef;
|
|
499
|
-
}
|
|
512
|
+
}
|
|
513
|
+
return refChanged;
|
|
500
514
|
}
|
|
501
|
-
|
|
515
|
+
return false;
|
|
502
516
|
/**
|
|
503
517
|
* Return true if the path points inside columns
|
|
504
518
|
*
|
|
@@ -326,12 +326,12 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
|
|
|
326
326
|
* @param {bool} [suspend] suspend flattening by caller until association path step
|
|
327
327
|
* @param {int} [suspendPos] suspend if starting pos is lower or equal to suspendPos and suspend is true
|
|
328
328
|
* @param {bool} [revokeAtSuspendPos] revoke suspension after suspendPos (binding parameter path use case)
|
|
329
|
-
* @returns
|
|
329
|
+
* @returns [string[], bool]
|
|
330
330
|
*/
|
|
331
331
|
function flattenStructStepsInRef(ref, path, links, scope, resolvedLinkTypes=new WeakMap(), suspend=false, suspendPos=0, revokeAtSuspendPos=false) {
|
|
332
332
|
// Refs of length 1 cannot contain steps - no need to check
|
|
333
333
|
if (ref.length < 2 || (scope === '$self' && ref.length === 2)) {
|
|
334
|
-
return ref;
|
|
334
|
+
return [ ref, false ];
|
|
335
335
|
}
|
|
336
336
|
|
|
337
337
|
const result = scope === '$self' ? [ref[0]] : [];
|
|
@@ -342,7 +342,7 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
|
|
|
342
342
|
scope = res.scope;
|
|
343
343
|
}
|
|
344
344
|
if (scope === '$magic')
|
|
345
|
-
return ref;
|
|
345
|
+
return [ ref, false ];
|
|
346
346
|
|
|
347
347
|
// Don't process a leading $self - it will a .art with .elements!
|
|
348
348
|
let i = scope === '$self' ? 1 : 0;
|
|
@@ -353,7 +353,8 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
|
|
|
353
353
|
effectiveType(links[i].art)[propName] ||
|
|
354
354
|
(resolvedLinkTypes.get(links[i])||{})[propName]);
|
|
355
355
|
|
|
356
|
-
|
|
356
|
+
let refChanged = false
|
|
357
|
+
let flattenStep = false;
|
|
357
358
|
suspend = !!art('items') || (suspend && i <= suspendPos);
|
|
358
359
|
for(; i < links.length; i++) {
|
|
359
360
|
|
|
@@ -365,6 +366,7 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
|
|
|
365
366
|
ref[i].id = result[result.length-1];
|
|
366
367
|
result[result.length-1] = ref[i];
|
|
367
368
|
}
|
|
369
|
+
refChanged = true;
|
|
368
370
|
// suspend flattening if the next path step has some 'items'
|
|
369
371
|
suspend = !!art('items');
|
|
370
372
|
}
|
|
@@ -381,7 +383,7 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
|
|
|
381
383
|
!links[i].art?.from &&
|
|
382
384
|
art('elements');
|
|
383
385
|
}
|
|
384
|
-
return result;
|
|
386
|
+
return [ result, refChanged ];
|
|
385
387
|
}
|
|
386
388
|
|
|
387
389
|
/**
|
|
@@ -280,7 +280,8 @@ module.exports = (csn, options) => {
|
|
|
280
280
|
},
|
|
281
281
|
params: (parent, prop, params) => {
|
|
282
282
|
forEachValue(params, (param) => {
|
|
283
|
-
const propagateToParams =
|
|
283
|
+
const propagateToParams = (param.type !== '$self' || csn.definitions.$self) &&
|
|
284
|
+
(typeof param.type !== 'string' || csn.definitions[param.type]?.kind !== 'entity');
|
|
284
285
|
propagateMemberPropsFromOrigin(param, {
|
|
285
286
|
'@': !propagateToParams, items: true, elements: true, enum: true, virtual: true,
|
|
286
287
|
});
|
|
@@ -339,8 +340,8 @@ module.exports = (csn, options) => {
|
|
|
339
340
|
copyProperties(origin, target, getMemberPropagationRuleFor, except, force);
|
|
340
341
|
|
|
341
342
|
// For a `type of` with .items, we want to take stuff from types (which we skip for "normal" propagation, see specialItemsRules).
|
|
342
|
-
// So for a type of we also propagate stuff from the virtual origin (which we don't give a "kind", therefore skipping that part of the check)
|
|
343
|
-
if (target.type
|
|
343
|
+
// So for a `type of` we also propagate stuff from the virtual origin (which we don't give a "kind", therefore skipping that part of the check)
|
|
344
|
+
if (target.type?.ref)
|
|
344
345
|
copyProperties(virtualOrigin, target, getMemberPropagationRuleFor, except);
|
|
345
346
|
|
|
346
347
|
if (!target.kind)
|