@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
|
@@ -56,6 +56,25 @@ function createReferentialConstraints( csn, options ) {
|
|
|
56
56
|
},
|
|
57
57
|
});
|
|
58
58
|
}
|
|
59
|
+
// for `texts` compositions, we may generate foreign key constraints even w/o `up_`
|
|
60
|
+
else if (elementName === 'texts' && element.target === `${path[path.length - 1]}.texts`) {
|
|
61
|
+
const { on } = element;
|
|
62
|
+
const target = csn.definitions[element.target];
|
|
63
|
+
// `texts` entities have a key named "locale"
|
|
64
|
+
const targetSideHasLocaleKey = target.elements.locale?.key;
|
|
65
|
+
if (targetSideHasLocaleKey && !skipConstraintGeneration(parent, target, { /* there is no assoc */ })) {
|
|
66
|
+
const sourceElements = Array.from(elementsOfSourceSide(on, elements));
|
|
67
|
+
const targetElements = Array.from(elementsOfTargetSide(on, target.elements));
|
|
68
|
+
// `texts` entities have all the keys the original entity has
|
|
69
|
+
const allElementsAreKeysAndHaveTheSameName = targetElements.length &&
|
|
70
|
+
targetElements.every(
|
|
71
|
+
([ targetKey, e ]) => e.key &&
|
|
72
|
+
sourceElements.some(([ sourceKey, sourceElement ]) => sourceElement.key && targetKey === sourceKey )
|
|
73
|
+
);
|
|
74
|
+
if (allElementsAreKeysAndHaveTheSameName)
|
|
75
|
+
attachConstraintsToDependentKeys(targetElements, sourceElements, path[path.length - 1], 'texts', { texts: true });
|
|
76
|
+
}
|
|
77
|
+
}
|
|
59
78
|
}
|
|
60
79
|
},
|
|
61
80
|
}, [], { skipIgnore: false, skipArtifact: a => a.query || a.kind !== 'entity' });
|
|
@@ -134,7 +153,7 @@ function createReferentialConstraints( csn, options ) {
|
|
|
134
153
|
* @param {Array} parentKeys array holding parent keys in the format [['key1', 'value1'], [...], ...]
|
|
135
154
|
* @param {CSN.PathSegment} parentTable the sql-table where the foreign key constraints will be pointing to
|
|
136
155
|
* @param {CSN.PathSegment} sourceAssociation the name of the association from which the constraint originates
|
|
137
|
-
* @param {CSN.PathSegment} upLinkFor the name of the composition which used this association in a `$self = <comp>.<up_>` comparison
|
|
156
|
+
* @param {CSN.PathSegment | object} upLinkFor the name of the composition which used this association in a `$self = <comp>.<up_>` comparison
|
|
138
157
|
* it is used for a comment in the constraint, which is only printed out in test-mode
|
|
139
158
|
*/
|
|
140
159
|
function attachConstraintsToDependentKeys( dependentKeys, parentKeys, parentTable, sourceAssociation, upLinkFor = null ) {
|
|
@@ -440,13 +459,16 @@ function createReferentialConstraints( csn, options ) {
|
|
|
440
459
|
*/
|
|
441
460
|
function elementsOfTargetSide( on, targetElements ) {
|
|
442
461
|
const elements = new Map();
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
462
|
+
const findElements = (tokenStream) => {
|
|
463
|
+
tokenStream
|
|
464
|
+
.forEach((element) => {
|
|
465
|
+
if (typeof element === 'object' && element.ref?.length > 1 && targetElements[element.ref[element.ref.length - 1]])
|
|
466
|
+
elements.set(element.ref[element.ref.length - 1], targetElements[element.ref[element.ref.length - 1]]);
|
|
467
|
+
else if (element.xpr)
|
|
468
|
+
findElements(element.xpr);
|
|
469
|
+
});
|
|
470
|
+
};
|
|
471
|
+
findElements(on);
|
|
450
472
|
return elements;
|
|
451
473
|
}
|
|
452
474
|
|
|
@@ -459,12 +481,16 @@ function createReferentialConstraints( csn, options ) {
|
|
|
459
481
|
*/
|
|
460
482
|
function elementsOfSourceSide( on, sourceElements ) {
|
|
461
483
|
const elements = new Map();
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
484
|
+
const findElements = (tokenStream) => {
|
|
485
|
+
tokenStream
|
|
486
|
+
.forEach((element) => {
|
|
487
|
+
if (typeof element === 'object' && element.ref?.length === 1 && sourceElements[element.ref[0]])
|
|
488
|
+
elements.set(element.ref[0], sourceElements[element.ref[0]]);
|
|
489
|
+
else if (element.xpr)
|
|
490
|
+
findElements(element.xpr);
|
|
491
|
+
});
|
|
492
|
+
};
|
|
493
|
+
findElements(on);
|
|
468
494
|
return elements;
|
|
469
495
|
}
|
|
470
496
|
|
|
@@ -507,12 +533,16 @@ function createReferentialConstraints( csn, options ) {
|
|
|
507
533
|
dependentKey.push(foreignKeyName);
|
|
508
534
|
onDeleteRules.add($foreignKeyConstraintCopy.onDelete);
|
|
509
535
|
});
|
|
510
|
-
// onDelete Rule is the "weakest" rule applicable. Precedence: RESTRICT >
|
|
536
|
+
// onDelete Rule is the "weakest" rule applicable. Precedence: RESTRICT > CASCADE
|
|
511
537
|
const onDelete = onDeleteRules.has('RESTRICT') ? 'RESTRICT' : 'CASCADE';
|
|
512
538
|
let onDeleteRemark = null;
|
|
513
539
|
// comments in sqlite files are causing the JDBC driver to throw an error on deployment
|
|
514
|
-
if (options.testMode && onDelete === 'CASCADE')
|
|
515
|
-
|
|
540
|
+
if (options.testMode && onDelete === 'CASCADE') {
|
|
541
|
+
if ($foreignKeyConstraint.upLinkFor?.texts)
|
|
542
|
+
onDeleteRemark = `Constraint originates from localized composition ”${$foreignKeyConstraint.parentTable}:texts“`;
|
|
543
|
+
else
|
|
544
|
+
onDeleteRemark = `Up_ link for Composition "${$foreignKeyConstraint.upLinkFor}" implies existential dependency`;
|
|
545
|
+
}
|
|
516
546
|
// constraint identifier usually start with `c__` to avoid name clashes
|
|
517
547
|
let identifier = options.pre2134ReferentialConstraintNames ? '' : 'c__';
|
|
518
548
|
identifier += `${getResultingName(csn, options.sqlMapping, artifactName)}_${$foreignKeyConstraint.sourceAssociation}`;
|
|
@@ -5,8 +5,9 @@ const {
|
|
|
5
5
|
applyTransformations,
|
|
6
6
|
setDependencies,
|
|
7
7
|
walkCsnPath,
|
|
8
|
+
getUtils,
|
|
8
9
|
} = require('../../model/csnUtils');
|
|
9
|
-
const {
|
|
10
|
+
const { implicitAs, columnAlias, pathId } = require('../../model/csnRefs');
|
|
10
11
|
const { setProp } = require('../../base/model');
|
|
11
12
|
const { forEach } = require('../../utils/objectUtils');
|
|
12
13
|
|
|
@@ -18,35 +19,30 @@ const { forEach } = require('../../utils/objectUtils');
|
|
|
18
19
|
* @param {CSN.Options} options
|
|
19
20
|
* @param {string} pathDelimiter
|
|
20
21
|
* @param {object} messageFunctions
|
|
21
|
-
* @param {Function} messageFunctions.error
|
|
22
|
-
* @param {Function} messageFunctions.info
|
|
23
|
-
* @param {Function} messageFunctions.throwWithAnyError
|
|
24
22
|
* @param {object} csnUtils
|
|
25
23
|
* @param {object} [iterateOptions]
|
|
26
24
|
*/
|
|
27
|
-
function expandStructureReferences( csn, options, pathDelimiter,
|
|
28
|
-
const {
|
|
29
|
-
isStructured, get$combined, getFinalTypeInfo,
|
|
30
|
-
} = csnUtils;
|
|
31
|
-
let { effectiveType, inspectRef } = csnUtils;
|
|
25
|
+
function expandStructureReferences( csn, options, pathDelimiter, messageFunctions, csnUtils, iterateOptions = {} ) {
|
|
26
|
+
const { error, info, throwWithAnyError } = messageFunctions;
|
|
32
27
|
|
|
33
28
|
rewriteExpandInline();
|
|
34
29
|
|
|
35
|
-
|
|
36
30
|
applyTransformations(csn, {
|
|
37
31
|
keys: (parent, name, keys, path) => {
|
|
38
32
|
parent.keys = expand(keys, path.concat('keys'), true);
|
|
39
33
|
},
|
|
40
34
|
columns: (parent, name, columns, path) => {
|
|
41
35
|
const artifact = csn.definitions[path[1]];
|
|
36
|
+
csnUtils.initDefinition(artifact); // potentially no initialized, yet
|
|
42
37
|
if (!hasAnnotationValue(artifact, '@cds.persistence.table')) {
|
|
43
|
-
const root = get$combined({ SELECT: parent });
|
|
38
|
+
const root = csnUtils.get$combined({ SELECT: parent });
|
|
44
39
|
// TODO: replace with the correct options.transformation?
|
|
45
40
|
// Do not expand the * in OData for a moment, not to introduce changes
|
|
46
41
|
// while the OData CSN is still official
|
|
42
|
+
const isComplexQuery = parent.from.join !== undefined;
|
|
47
43
|
if (!options.toOdata)
|
|
48
|
-
parent.columns = replaceStar(root, columns, parent.excluding);
|
|
49
|
-
parent.columns = expand(parent.columns, path.concat('columns'), true);
|
|
44
|
+
parent.columns = replaceStar(root, columns, parent.excluding, isComplexQuery);
|
|
45
|
+
parent.columns = expand(parent.columns, path.concat('columns'), true, isComplexQuery);
|
|
50
46
|
}
|
|
51
47
|
},
|
|
52
48
|
groupBy: (parent, name, groupBy, path) => {
|
|
@@ -74,7 +70,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
|
|
|
74
70
|
// get$combined expects a SET/SELECT - so we wrap the parent
|
|
75
71
|
// (which is the thing inside SET/SELECT)
|
|
76
72
|
// We can directly use SELECT here, as only projections and SELECT can have .columns
|
|
77
|
-
const root = get$combined({ SELECT: parent });
|
|
73
|
+
const root = csnUtils.get$combined({ SELECT: parent });
|
|
78
74
|
if (!hasAnnotationValue(artifact, '@cds.persistence.table')) {
|
|
79
75
|
// Make root look like normal .elements - we never cared about conflict afaik anyway
|
|
80
76
|
Object.keys(root).forEach((key) => {
|
|
@@ -102,7 +98,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
|
|
|
102
98
|
|
|
103
99
|
cleanup.forEach(fn => fn());
|
|
104
100
|
|
|
105
|
-
|
|
101
|
+
csnUtils = getUtils(csn);
|
|
106
102
|
|
|
107
103
|
const publishing = [];
|
|
108
104
|
// OData must allow navigations to @cds.persistence.skip targets
|
|
@@ -144,7 +140,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
|
|
|
144
140
|
if (!(obj && obj.ref) || obj.$scope === 'alias')
|
|
145
141
|
continue;
|
|
146
142
|
|
|
147
|
-
const links = obj._links || inspectRef(path.concat([ name, i ])).links;
|
|
143
|
+
const links = obj._links || csnUtils.inspectRef(path.concat([ name, i ])).links;
|
|
148
144
|
|
|
149
145
|
if (!links)
|
|
150
146
|
continue;
|
|
@@ -244,7 +240,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
|
|
|
244
240
|
*/
|
|
245
241
|
function nextBase( parent, base ) {
|
|
246
242
|
if (parent.ref) {
|
|
247
|
-
const finalBaseType = getFinalTypeInfo(parent._art.type);
|
|
243
|
+
const finalBaseType = csnUtils.getFinalTypeInfo(parent._art.type);
|
|
248
244
|
const art = parent._art;
|
|
249
245
|
|
|
250
246
|
if (finalBaseType && (finalBaseType.type === 'cds.Association' || finalBaseType.type === 'cds.Composition'))
|
|
@@ -297,7 +293,7 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
|
|
|
297
293
|
function isToMany( obj ) {
|
|
298
294
|
if (!obj._art)
|
|
299
295
|
return false;
|
|
300
|
-
const eType = effectiveType(obj._art);
|
|
296
|
+
const eType = csnUtils.effectiveType(obj._art);
|
|
301
297
|
return (eType.type === 'cds.Association' || eType.type === 'cds.Composition') && eType.cardinality && eType.cardinality.max !== 1;
|
|
302
298
|
}
|
|
303
299
|
|
|
@@ -516,18 +512,18 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
|
|
|
516
512
|
*
|
|
517
513
|
* @param {Array} thing
|
|
518
514
|
* @param {CSN.Path} path
|
|
519
|
-
* @param {boolean} [withAlias=false] Whether to "expand" the (implicit) alias
|
|
515
|
+
* @param {boolean} [withAlias=false] Whether to "expand" the (implicit) alias as well.
|
|
516
|
+
* @param {boolean} [isComplexQuery]
|
|
520
517
|
* @returns {Array} New array - with all structured things expanded
|
|
521
518
|
*/
|
|
522
|
-
function expand( thing, path, withAlias = false ) {
|
|
519
|
+
function expand( thing, path, withAlias = false, isComplexQuery = false ) {
|
|
523
520
|
const newThing = [];
|
|
524
521
|
for (let i = 0; i < thing.length; i++) {
|
|
525
522
|
const col = thing[i];
|
|
526
523
|
if (col.ref && col.$scope !== '$magic') {
|
|
527
|
-
const _art = col._art || inspectRef(path.concat(i)).art;
|
|
528
|
-
if (_art && isStructured(_art))
|
|
529
|
-
newThing.push(...expandRef(_art, col, withAlias));
|
|
530
|
-
|
|
524
|
+
const _art = col._art || csnUtils.inspectRef(path.concat(i)).art;
|
|
525
|
+
if (_art && csnUtils.isStructured(_art))
|
|
526
|
+
newThing.push(...expandRef(_art, col, withAlias, isComplexQuery));
|
|
531
527
|
else
|
|
532
528
|
newThing.push(col);
|
|
533
529
|
}
|
|
@@ -535,6 +531,23 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
|
|
|
535
531
|
col.as = implicitAs(col.ref);
|
|
536
532
|
newThing.push(col);
|
|
537
533
|
}
|
|
534
|
+
else if (col.cast?.type) {
|
|
535
|
+
const _art = col.cast._type || csnUtils.inspectRef(path.concat(i, 'cast', 'type')).art;
|
|
536
|
+
if (_art && csnUtils.isStructured(_art)) {
|
|
537
|
+
// special case for `null as name : Struct`
|
|
538
|
+
if (col.val === null) {
|
|
539
|
+
newThing.push(...expandValAsStructure(_art, col, withAlias));
|
|
540
|
+
}
|
|
541
|
+
else {
|
|
542
|
+
error('type-invalid-cast', path.concat(i, 'cast', 'type'), {
|
|
543
|
+
'#': col.val !== undefined ? 'val-to-structure' : 'expr-to-structure', value: col.val,
|
|
544
|
+
});
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
newThing.push(col);
|
|
549
|
+
}
|
|
550
|
+
}
|
|
538
551
|
else {
|
|
539
552
|
newThing.push(col);
|
|
540
553
|
}
|
|
@@ -544,44 +557,109 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
|
|
|
544
557
|
}
|
|
545
558
|
|
|
546
559
|
/**
|
|
547
|
-
*
|
|
548
|
-
*
|
|
549
|
-
* Iterative, to not run into stack overflow.
|
|
560
|
+
* Expands a column, and calls leafCallback() when a leaf node is reached.
|
|
550
561
|
*
|
|
551
562
|
* @param {CSN.Element} art
|
|
552
|
-
*
|
|
553
|
-
*
|
|
554
|
-
* @
|
|
563
|
+
* Structured Artifact which is used for expansion (and names, etc.). For a ref, it's the
|
|
564
|
+
* underlying type or a cast-type, for a value, it's always the cast-type.
|
|
565
|
+
* @param {string} colName
|
|
566
|
+
* Name of the column, that is used as the first name segment, e.g. a column `a` may end up in
|
|
567
|
+
* leafs `a_b` and `a_c`, if `art` has elements `b` and `c`.
|
|
568
|
+
* @param {string[]} colTypeRef
|
|
569
|
+
* Expanded type for the column. Basically the path to the to-be-expanded `art`.
|
|
570
|
+
* @param {(currentRef: any[], currentAlias: string[]) => object} leafCallback
|
|
571
|
+
* Callback when leaf nodes are reached. currentRef is the type reference for the expanded
|
|
572
|
+
* column. currentAlias is the columns calculated alias.
|
|
573
|
+
* @returns {object[]}
|
|
555
574
|
*/
|
|
556
|
-
function
|
|
575
|
+
function _expandStructCol( art, colName, colTypeRef, leafCallback ) {
|
|
557
576
|
const expanded = [];
|
|
558
|
-
/** @type {Array<[CSN.Element, any[],
|
|
559
|
-
const stack = [ [ art,
|
|
577
|
+
/** @type {Array<[CSN.Element, any[], string[]]>} */
|
|
578
|
+
const stack = [ [ art, colTypeRef, [ colName ] ] ];
|
|
560
579
|
while (stack.length > 0) {
|
|
561
580
|
const [ current, currentRef, currentAlias ] = stack.pop();
|
|
562
|
-
if (isStructured(current)) {
|
|
563
|
-
|
|
564
|
-
|
|
581
|
+
if (csnUtils.isStructured(current)) {
|
|
582
|
+
const elements = Object.entries(current.elements || csnUtils.effectiveType(current).elements).reverse();
|
|
583
|
+
for (const [ name, elem ] of elements)
|
|
584
|
+
stack.push([ elem, currentRef.concat(name), currentAlias.concat(name) ]);
|
|
565
585
|
}
|
|
566
586
|
else {
|
|
567
|
-
const
|
|
568
|
-
|
|
569
|
-
const newAlias = currentAlias.join(pathDelimiter);
|
|
570
|
-
// if (alias !== undefined) // explicit alias
|
|
571
|
-
obj.as = newAlias;
|
|
572
|
-
// alias was implicit - to later distinguish expanded s -> s.a from explicitly written s.a
|
|
573
|
-
if (root.as === undefined)
|
|
574
|
-
setProp(obj, '$implicitAlias', true);
|
|
575
|
-
}
|
|
576
|
-
if (root.key)
|
|
577
|
-
obj.key = true;
|
|
578
|
-
expanded.push(obj);
|
|
587
|
+
const newCol = leafCallback(currentRef, currentAlias);
|
|
588
|
+
expanded.push(newCol);
|
|
579
589
|
}
|
|
580
590
|
}
|
|
581
591
|
|
|
582
592
|
return expanded;
|
|
583
593
|
}
|
|
584
594
|
|
|
595
|
+
/**
|
|
596
|
+
* Expand the ref and - if requested - expand/set the alias with it.
|
|
597
|
+
*
|
|
598
|
+
* @param {CSN.Element} art
|
|
599
|
+
* @param {object} root Column, ref in order by, etc.
|
|
600
|
+
* @param {boolean} withAlias Whether to add an explicit flattened alias to the expanded columns/references.
|
|
601
|
+
* @param {boolean} [isComplexQuery]
|
|
602
|
+
* @returns {Array}
|
|
603
|
+
*/
|
|
604
|
+
function expandRef( art, root, withAlias, isComplexQuery ) {
|
|
605
|
+
return _expandStructCol(art, columnAlias(root), root.ref, ( currentRef, currentAlias) => {
|
|
606
|
+
const obj = { ...root, ref: currentRef };
|
|
607
|
+
if (withAlias) {
|
|
608
|
+
// TODO: Remove this line in case foreign key annotations should
|
|
609
|
+
// be adressed via full path into target instead of using alias
|
|
610
|
+
// names. See flattening.js::flattenAllStructStepsInRefs()
|
|
611
|
+
// apply transformations on `ref` counterpart comment.
|
|
612
|
+
setProp(obj, '$structRef', currentAlias);
|
|
613
|
+
obj.as = currentAlias.join(pathDelimiter);
|
|
614
|
+
// alias was implicit - to later distinguish expanded s -> s.a from explicitly written s.a
|
|
615
|
+
if (root.as === undefined)
|
|
616
|
+
setProp(obj, '$implicitAlias', true);
|
|
617
|
+
}
|
|
618
|
+
|
|
619
|
+
// The Java runtime, as of 2023-09-13, assumes that for _simple projections_, all references
|
|
620
|
+
// are relative to the query source. To avoid breaking that assumption unless necessary,
|
|
621
|
+
// we only add the table alias if:
|
|
622
|
+
// - it is a complex query with possibly multiple available table aliases, or
|
|
623
|
+
// - the transformation is not for OData (which is used by Java), or
|
|
624
|
+
// - the first path step has the same name as the table alias (only one, as otherwise the query would be complex)
|
|
625
|
+
if (typeof root.$env === 'string' && (isComplexQuery || options.transformation !== 'odata' || root.$env === pathId(obj.ref[0])))
|
|
626
|
+
obj.ref = [ root.$env, ...obj.ref ];
|
|
627
|
+
|
|
628
|
+
return obj;
|
|
629
|
+
});
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Expand `null` columns which were cast to a structure, that is: `null as name : Struct`.
|
|
634
|
+
* Requires that `col` has an alias.
|
|
635
|
+
*
|
|
636
|
+
* @param {CSN.Element} art
|
|
637
|
+
* @param {object} col
|
|
638
|
+
* @param {boolean} withAlias Whether to add an explicit flattened alias to the expanded columns/references.
|
|
639
|
+
* @returns {Array}
|
|
640
|
+
*/
|
|
641
|
+
function expandValAsStructure( art, col, withAlias ) {
|
|
642
|
+
const colName = col.as || '';
|
|
643
|
+
// Expression-columns may have an internal name such as `$_column_N`. If the name is internal,
|
|
644
|
+
// we should not publish names based upon the internal name.
|
|
645
|
+
const isInternal = !col.as || !Object.prototype.propertyIsEnumerable.call(col, 'as');
|
|
646
|
+
|
|
647
|
+
return _expandStructCol(art, colName, col.cast.type?.ref || [ col.cast.type ], ( currentRef, currentAlias) => {
|
|
648
|
+
const newCol = {
|
|
649
|
+
...col,
|
|
650
|
+
val: col.val,
|
|
651
|
+
cast: { type: { ref: currentRef } },
|
|
652
|
+
};
|
|
653
|
+
if (withAlias) {
|
|
654
|
+
if (!isInternal)
|
|
655
|
+
newCol.as = currentAlias.join(pathDelimiter);
|
|
656
|
+
else
|
|
657
|
+
setProp(newCol, 'as', currentAlias.join(pathDelimiter));
|
|
658
|
+
}
|
|
659
|
+
return newCol;
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
|
|
585
663
|
/**
|
|
586
664
|
* Get the effective name produced by the object
|
|
587
665
|
*
|
|
@@ -604,9 +682,10 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
|
|
|
604
682
|
* @param {object} base The raw set of things a * can expand to
|
|
605
683
|
* @param {Array} subs Things - the .expand/.inline or .columns
|
|
606
684
|
* @param {string[]} [excluding=[]]
|
|
685
|
+
* @param {boolean} [isComplexQuery=false] Wether the query is a single source select or something more complex
|
|
607
686
|
* @returns {Array} If there was a star, expand it and handle shadowing/excluding, else just return subs
|
|
608
687
|
*/
|
|
609
|
-
function replaceStar( base, subs, excluding = [] ) {
|
|
688
|
+
function replaceStar( base, subs, excluding = [], isComplexQuery = false ) {
|
|
610
689
|
const stars = [];
|
|
611
690
|
const names = Object.create(null);
|
|
612
691
|
for (let i = 0; i < subs.length; i++) {
|
|
@@ -638,7 +717,11 @@ function expandStructureReferences( csn, options, pathDelimiter, { error, info,
|
|
|
638
717
|
}
|
|
639
718
|
}
|
|
640
719
|
else { // the thing is not shadowed - use the name from the base
|
|
641
|
-
|
|
720
|
+
const col = { ref: [ part ] };
|
|
721
|
+
if (isComplexQuery) // $env: tableAlias
|
|
722
|
+
setProp(col, '$env', base[part][0].parent);
|
|
723
|
+
|
|
724
|
+
star.push(col);
|
|
642
725
|
}
|
|
643
726
|
}
|
|
644
727
|
}
|
|
@@ -7,7 +7,7 @@ const {
|
|
|
7
7
|
} = require('../../model/csnUtils');
|
|
8
8
|
const transformUtils = require('../transformUtils');
|
|
9
9
|
const { csnRefs } = require('../../model/csnRefs');
|
|
10
|
-
const { setProp } = require('../../base/model');
|
|
10
|
+
const { setProp, isBetaEnabled } = require('../../base/model');
|
|
11
11
|
const { forEach } = require('../../utils/objectUtils');
|
|
12
12
|
const { cardinality2str } = require('../../model/csnUtils');
|
|
13
13
|
|
|
@@ -208,7 +208,10 @@ function flattenAllStructStepsInRefs( csn, options, resolved, pathDelimiter, ite
|
|
|
208
208
|
const lastRef = ref[ref.length - 1];
|
|
209
209
|
const fn = () => {
|
|
210
210
|
const scopedPath = [ ...parent.$path ];
|
|
211
|
-
|
|
211
|
+
// TODO: If foreign key annotations should be assigned via
|
|
212
|
+
// full path into target, uncomment this line and
|
|
213
|
+
// comment/remove setProp in expansion.js
|
|
214
|
+
// setProp(parent, '$structRef', parent.ref);
|
|
212
215
|
parent.ref = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes);
|
|
213
216
|
resolved.set(parent, { links, art, scope });
|
|
214
217
|
// Explicitly set implicit alias for things that are now flattened - but only in columns
|
|
@@ -219,7 +222,9 @@ function flattenAllStructStepsInRefs( csn, options, resolved, pathDelimiter, ite
|
|
|
219
222
|
delete parent.$implicitAlias;
|
|
220
223
|
}
|
|
221
224
|
// To handle explicitly written s.a - add implicit alias a, since after flattening it would otherwise be s_a
|
|
222
|
-
else if (parent.ref[parent.ref.length - 1] !== lastRef &&
|
|
225
|
+
else if (parent.ref[parent.ref.length - 1] !== lastRef &&
|
|
226
|
+
(insideColumns(scopedPath) || insideKeys(scopedPath)) &&
|
|
227
|
+
!parent.as) {
|
|
223
228
|
parent.as = lastRef;
|
|
224
229
|
}
|
|
225
230
|
};
|
|
@@ -396,6 +401,36 @@ function getBranches( element, elementName, effectiveType, pathDelimiter ) {
|
|
|
396
401
|
return branches;
|
|
397
402
|
}
|
|
398
403
|
|
|
404
|
+
/**
|
|
405
|
+
* Link annotate extensions to managed associations as a preparational step
|
|
406
|
+
* for later annotation assignment on the final foreignkeys
|
|
407
|
+
* This function must be applied on an unmodified, structured CSN in order to
|
|
408
|
+
* traverse both the extensions and dictionary trees in corresponding order.
|
|
409
|
+
*
|
|
410
|
+
* @param {CSN.Model} csn
|
|
411
|
+
* @param {object} options
|
|
412
|
+
*/
|
|
413
|
+
function linkForeignKeyAnnotationExtensionsToAssociation( csn, options ) {
|
|
414
|
+
if (isBetaEnabled(options, 'annotateForeignKeys')) {
|
|
415
|
+
csn.extensions?.forEach(( ext ) => {
|
|
416
|
+
const defName = ext.annotate;
|
|
417
|
+
|
|
418
|
+
const traverseExtensions = (env, enode) => {
|
|
419
|
+
if (env?.target && env?.keys) {
|
|
420
|
+
setProp(env, '$fkExtensions', enode);
|
|
421
|
+
}
|
|
422
|
+
else {
|
|
423
|
+
const elements = env?.items?.elements || env?.elements;
|
|
424
|
+
if (enode?.elements && elements)
|
|
425
|
+
Object.keys(enode.elements).forEach(en => traverseExtensions(elements[en], enode.elements[en]));
|
|
426
|
+
}
|
|
427
|
+
};
|
|
428
|
+
if (ext.annotate)
|
|
429
|
+
traverseExtensions(csn.definitions[defName], ext);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
399
434
|
/**
|
|
400
435
|
* @param {CSN.Model} csn
|
|
401
436
|
* @param {CSN.Options} options
|
|
@@ -530,7 +565,7 @@ function handleManagedAssociationsAndCreateForeignKeys( csn, options, error, war
|
|
|
530
565
|
* @returns {object} The clone of base
|
|
531
566
|
*/
|
|
532
567
|
function cloneAndExtendRef( key, base, ref ) {
|
|
533
|
-
const clone = cloneCsnNonDict(base, options);
|
|
568
|
+
const clone = cloneCsnNonDict(base, { ...options, hiddenPropertiesToClone: [ '$structRef', '$fkExtensions' ] } );
|
|
534
569
|
if (key.ref) {
|
|
535
570
|
// We build a ref that contains the aliased fk - that element will be created later on, so this ref is not resolvable yet
|
|
536
571
|
// Therefore we keep it as $ref - ref is the non-aliased, resolvable "clone"
|
|
@@ -546,6 +581,8 @@ function handleManagedAssociationsAndCreateForeignKeys( csn, options, error, war
|
|
|
546
581
|
}
|
|
547
582
|
setProp(clone, '$ref', $ref);
|
|
548
583
|
clone.ref = clone.ref.concat(key.ref);
|
|
584
|
+
if (clone.$structRef && key.$structRef)
|
|
585
|
+
clone.$structRef = clone.$structRef.concat(key.$structRef);
|
|
549
586
|
}
|
|
550
587
|
|
|
551
588
|
if (!clone.as && clone.ref && clone.ref.length > 0) {
|
|
@@ -646,6 +683,34 @@ function handleManagedAssociationsAndCreateForeignKeys( csn, options, error, war
|
|
|
646
683
|
}
|
|
647
684
|
}
|
|
648
685
|
}
|
|
686
|
+
// assign annotations from fkExtension tree to foreign keys
|
|
687
|
+
if (isBetaEnabled(options, 'annotateForeignKeys')) {
|
|
688
|
+
const extCollector = {};
|
|
689
|
+
fks.forEach(([ _fkn, fk ]) => {
|
|
690
|
+
let ext = element.$fkExtensions;
|
|
691
|
+
let extKey = elementName;
|
|
692
|
+
for (const step of fk.$extensionPath) {
|
|
693
|
+
extKey += `.${step}`;
|
|
694
|
+
ext = ext?.elements?.[step];
|
|
695
|
+
if (!ext)
|
|
696
|
+
break;
|
|
697
|
+
// collect annotations, lowest wins
|
|
698
|
+
// eslint-disable-next-line no-loop-func
|
|
699
|
+
Object.entries(ext).forEach(([ k, v ]) => {
|
|
700
|
+
if (k[0] === '@') {
|
|
701
|
+
fk[k] = v;
|
|
702
|
+
extCollector[extKey] = ext;
|
|
703
|
+
}
|
|
704
|
+
});
|
|
705
|
+
}
|
|
706
|
+
});
|
|
707
|
+
|
|
708
|
+
// remove consumed annotations after applying the annotation hierarchy to each fk!
|
|
709
|
+
Object.values(extCollector).forEach(ext => Object.keys(ext).forEach((k) => {
|
|
710
|
+
if (k[0] === '@')
|
|
711
|
+
delete ext[k];
|
|
712
|
+
}));
|
|
713
|
+
}
|
|
649
714
|
orderedElements.push(...fks);
|
|
650
715
|
});
|
|
651
716
|
|
|
@@ -669,10 +734,11 @@ function handleManagedAssociationsAndCreateForeignKeys( csn, options, error, war
|
|
|
669
734
|
* @param {CSN.Model} csn
|
|
670
735
|
* @param {object} options
|
|
671
736
|
* @param {string} pathDelimiter
|
|
737
|
+
* @param {object} extensionPath
|
|
672
738
|
* @param {number} lvl
|
|
673
739
|
* @returns {Array[]} First element of every sub-array is the foreign key name, second is the foreign key definition
|
|
674
740
|
*/
|
|
675
|
-
function createForeignKeys( csnUtils, path, element, prefix, csn, options, pathDelimiter, lvl = 0 ) {
|
|
741
|
+
function createForeignKeys( csnUtils, path, element, prefix, csn, options, pathDelimiter, extensionPath = [], lvl = 0 ) {
|
|
676
742
|
const special$self = !csn?.definitions?.$self && '$self';
|
|
677
743
|
const isInspectRefResult = !Array.isArray(path);
|
|
678
744
|
|
|
@@ -718,7 +784,7 @@ function createForeignKeys( csnUtils, path, element, prefix, csn, options, pathD
|
|
|
718
784
|
const continuePath = getContinuePath([ 'keys', keyIndex ]);
|
|
719
785
|
const alias = key.as || implicitAs(key.ref);
|
|
720
786
|
const result = csnUtils.inspectRef(continuePath);
|
|
721
|
-
fks = fks.concat(createForeignKeys(csnUtils, result, result.art, alias, csn, options, pathDelimiter, lvl + 1));
|
|
787
|
+
fks = fks.concat(createForeignKeys(csnUtils, result, result.art, alias, csn, options, pathDelimiter, extensionPath.concat(key.$structRef), lvl + 1));
|
|
722
788
|
});
|
|
723
789
|
if (!hasKeys)
|
|
724
790
|
delete finalElement.keys;
|
|
@@ -732,13 +798,14 @@ function createForeignKeys( csnUtils, path, element, prefix, csn, options, pathD
|
|
|
732
798
|
// Skip already produced foreign keys
|
|
733
799
|
if (!elem['@odata.foreignKey4']) {
|
|
734
800
|
const continuePath = getContinuePath([ 'elements', elemName ]);
|
|
735
|
-
fks = fks.concat(createForeignKeys(csnUtils, continuePath, elem, elemName, csn, options, pathDelimiter, lvl + 1));
|
|
801
|
+
fks = fks.concat(createForeignKeys(csnUtils, continuePath, elem, elemName, csn, options, pathDelimiter, extensionPath.concat(elemName), lvl + 1));
|
|
736
802
|
}
|
|
737
803
|
});
|
|
738
804
|
}
|
|
739
805
|
// we have reached a leaf element, create a foreign key
|
|
740
806
|
else if (finalElement.type == null || isBuiltinType(finalElement.type)) {
|
|
741
807
|
const newFk = Object.create(null);
|
|
808
|
+
setProp(newFk, '$extensionPath', extensionPath);
|
|
742
809
|
for (const prop of [ 'type', 'length', 'scale', 'precision', 'srid', 'default', '@odata.Type' ]) {
|
|
743
810
|
// copy props from original element to preserve derived types!
|
|
744
811
|
if (element[prop] !== undefined)
|
|
@@ -794,6 +861,7 @@ module.exports = {
|
|
|
794
861
|
flattenAllStructStepsInRefs,
|
|
795
862
|
flattenElements,
|
|
796
863
|
removeLeadingSelf,
|
|
864
|
+
linkForeignKeyAnnotationExtensionsToAssociation,
|
|
797
865
|
handleManagedAssociationsAndCreateForeignKeys,
|
|
798
866
|
getBranches,
|
|
799
867
|
};
|
|
@@ -152,6 +152,8 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
152
152
|
{ skipArtifact: isExternalServiceMember }
|
|
153
153
|
);
|
|
154
154
|
|
|
155
|
+
flattening.linkForeignKeyAnnotationExtensionsToAssociation(csn, options);
|
|
156
|
+
|
|
155
157
|
// All type refs must be resolved, including external APIs.
|
|
156
158
|
// OData has no 'type of' so 'real' imported OData APIs marked @cds.external are safe.
|
|
157
159
|
// If in the future 'other' APIs that might support type refs are imported, these refs must be
|
|
@@ -169,6 +171,7 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
169
171
|
expansion.expandStructureReferences(csn, options, '_', { error, info, throwWithAnyError }, csnUtils, { skipArtifact: isExternalServiceMember });
|
|
170
172
|
const resolved = new WeakMap();
|
|
171
173
|
// No refs with struct-steps exist anymore
|
|
174
|
+
|
|
172
175
|
flattening.flattenAllStructStepsInRefs(csn, options, resolved, '_', { skipArtifact: isExternalServiceMember });
|
|
173
176
|
// No type references exist anymore
|
|
174
177
|
// Needs to happen exactly between flattenAllStructStepsInRefs and flattenElements to keep model resolvable.
|
|
@@ -389,7 +392,7 @@ function transform4odataWithCsn(inputModel, options) {
|
|
|
389
392
|
|
|
390
393
|
// Only on element level
|
|
391
394
|
if(node.kind == null) {
|
|
392
|
-
if (node['@mandatory']&& node['@Common.FieldControl'
|
|
395
|
+
if (node['@mandatory'] && !Object.entries(node).some(([k,v]) => k === '@Common.FieldControl' || k.startsWith('@Common.FieldControl.') && v != null)) {
|
|
393
396
|
setAnnotation(node, '@Common.FieldControl', { '#': 'Mandatory' });
|
|
394
397
|
}
|
|
395
398
|
if (node['@assert.range'] != null &&
|