@sap/cds-compiler 6.5.2 → 6.6.0
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 +26 -0
- package/lib/api/main.js +0 -3
- package/lib/base/builtins.js +2 -1
- package/lib/base/message-registry.js +0 -1
- package/lib/base/model.js +0 -1
- package/lib/checks/sql-snippets.js +8 -0
- package/lib/checks/validator.js +4 -2
- package/lib/compiler/define.js +1 -1
- package/lib/compiler/extend.js +54 -22
- package/lib/compiler/generate.js +31 -8
- package/lib/compiler/index.js +2 -2
- package/lib/compiler/kick-start.js +18 -28
- package/lib/compiler/propagator.js +20 -1
- package/lib/compiler/resolve.js +24 -0
- package/lib/compiler/shared.js +49 -9
- package/lib/compiler/utils.js +1 -0
- package/lib/edm/annotations/edmJson.js +111 -37
- package/lib/edm/annotations/genericTranslation.js +3 -1
- package/lib/main.d.ts +0 -3
- package/lib/main.js +2 -0
- package/lib/transform/db/applyTransformations.js +1 -1
- package/lib/transform/db/associations.js +24 -15
- package/lib/transform/db/flattening.js +1 -1
- package/lib/transform/db/views.js +0 -39
- package/lib/transform/effective/associations.js +5 -55
- package/lib/transform/effective/main.js +4 -2
- package/lib/transform/effective/misc.js +1 -1
- package/lib/transform/effective/types.js +36 -12
- package/lib/transform/forOdata.js +126 -3
- package/lib/transform/forRelationalDB.js +13 -4
- package/lib/transform/transformUtils.js +51 -1
- package/lib/transform/translateAssocsToJoins.js +43 -19
- package/package.json +1 -1
|
@@ -7,6 +7,7 @@ const {
|
|
|
7
7
|
} = require('../../model/csnUtils');
|
|
8
8
|
const { forEachKey } = require('../../utils/objectUtils');
|
|
9
9
|
const { cloneCsnDict, cloneCsnNonDict } = require('../../model/cloneCsn');
|
|
10
|
+
const propertiesToSkipForCasts = { enum: true };
|
|
10
11
|
|
|
11
12
|
/**
|
|
12
13
|
* Resolve all references to structured types in entities to the underlying elements.
|
|
@@ -26,8 +27,14 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
|
|
|
26
27
|
const { flattenStructStepsInRef } = transformers;
|
|
27
28
|
const later = [];
|
|
28
29
|
applyTransformations(csn, {
|
|
30
|
+
cast: (parent, prop, cast, path, grandParent, grandParentProp) => {
|
|
31
|
+
if (cast.type && path.at(-2) !== 'columns' && typeof grandParentProp === 'number') // SQL cast - not happy with this :(
|
|
32
|
+
resolveType(csnUtils, cast, true);
|
|
33
|
+
else if (cast.type) // cdl style cast
|
|
34
|
+
resolveType(csnUtils, cast, false);
|
|
35
|
+
},
|
|
29
36
|
type: (parent) => {
|
|
30
|
-
resolveType(csnUtils, parent);
|
|
37
|
+
resolveType(csnUtils, parent, false);
|
|
31
38
|
},
|
|
32
39
|
}, [ (definitions, artifactName, artifact) => {
|
|
33
40
|
// In a non-flat model, replacing types with some $self inside causes issues for actions (bound or unbound)
|
|
@@ -36,7 +43,7 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
|
|
|
36
43
|
later.push([ { [artifactName]: artifact }, [ 'definitions' ] ]);
|
|
37
44
|
else if (artifact.actions)
|
|
38
45
|
later.push([ artifact.actions, [ 'definitions', artifactName, 'actions' ] ]);
|
|
39
|
-
} ], { skipStandard: { returns: true }, processAnnotations: true, skipDict: { actions: true } });
|
|
46
|
+
} ], { skipStandard: { returns: true, cast: true }, processAnnotations: true, skipDict: { actions: true } });
|
|
40
47
|
|
|
41
48
|
// Type refs like E:struct.sub can not be resolved later, as struct will be flattened. So we rewrite it to E:struct_sub here.
|
|
42
49
|
later.forEach(([ action, actionPath ]) => {
|
|
@@ -52,8 +59,8 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
|
|
|
52
59
|
return function resolveTypesInActions(refreshedCsnUtils) {
|
|
53
60
|
later.forEach(([ action ]) => {
|
|
54
61
|
applyTransformationsOnDictionary(action, {
|
|
55
|
-
type: (parent) => {
|
|
56
|
-
resolveType(refreshedCsnUtils, parent);
|
|
62
|
+
type: (parent, prop, type, path) => {
|
|
63
|
+
resolveType(refreshedCsnUtils, parent, path);
|
|
57
64
|
},
|
|
58
65
|
});
|
|
59
66
|
});
|
|
@@ -70,7 +77,7 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
|
|
|
70
77
|
*
|
|
71
78
|
* @param {object} parent Object with a .type property
|
|
72
79
|
*/
|
|
73
|
-
function resolveType( csnUtils, parent ) {
|
|
80
|
+
function resolveType( csnUtils, parent, isCast ) {
|
|
74
81
|
// TODO: I assume there can be cases with a type ref but still having .elements already? Subelement anno?
|
|
75
82
|
const final = csnUtils.getFinalTypeInfo(parent.type);
|
|
76
83
|
if (final?.elements) {
|
|
@@ -86,7 +93,7 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
|
|
|
86
93
|
}
|
|
87
94
|
else if (final?.type && (options.resolveSimpleTypes || parent.type.ref?.length > 1)) {
|
|
88
95
|
forEachKey(final, (key) => { // copy `type` + properties (default, etc.)
|
|
89
|
-
if (parent[key] === undefined || key === 'type')
|
|
96
|
+
if ((parent[key] === undefined || key === 'type') && (isCast && !propertiesToSkipForCasts[key] || !isCast))
|
|
90
97
|
parent[key] = final[key];
|
|
91
98
|
});
|
|
92
99
|
}
|
|
@@ -95,17 +102,31 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
|
|
|
95
102
|
const stack = [ ];
|
|
96
103
|
if (parent.elements || parent.items)
|
|
97
104
|
stack.push(parent);
|
|
105
|
+
const typeTransformer = getTypeTransformer(csnUtils, stack);
|
|
106
|
+
const typeInCastTransformer = getTypeTransformer(csnUtils, stack, true);
|
|
98
107
|
while (stack.length > 0) {
|
|
99
108
|
const obj = stack.pop();
|
|
100
109
|
if (obj.elements) {
|
|
101
110
|
applyTransformationsOnDictionary(obj.elements, {
|
|
102
|
-
|
|
111
|
+
cast: (parent, prop, cast, path, grandParent, grandParentProp) => {
|
|
112
|
+
if (cast.type && path.at(-2) !== 'columns' && typeof grandParentProp === 'number')
|
|
113
|
+
typeInCastTransformer(cast, 'type', cast.type);
|
|
114
|
+
else if (cast.type)
|
|
115
|
+
typeTransformer(cast, 'type', cast.type);
|
|
116
|
+
},
|
|
117
|
+
type: typeTransformer,
|
|
103
118
|
}, { skipDict: { actions: true } });
|
|
104
119
|
}
|
|
105
120
|
else if (obj.items) {
|
|
106
121
|
applyTransformationsOnNonDictionary(obj, 'items', {
|
|
107
|
-
|
|
108
|
-
|
|
122
|
+
cast: (parent, prop, cast, path, grandParent, grandParentProp) => {
|
|
123
|
+
if (cast.type && path.at(-2) !== 'columns' && typeof grandParentProp === 'number')
|
|
124
|
+
typeInCastTransformer(cast, 'type', cast.type);
|
|
125
|
+
else if (cast.type)
|
|
126
|
+
typeTransformer(cast, 'type', cast.type);
|
|
127
|
+
},
|
|
128
|
+
type: typeTransformer,
|
|
129
|
+
}, { skipDict: { actions: true }, skipStandard: { cast: true } });
|
|
109
130
|
}
|
|
110
131
|
}
|
|
111
132
|
}
|
|
@@ -115,7 +136,7 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
|
|
|
115
136
|
* @param {object[]} stack
|
|
116
137
|
* @returns {Function}
|
|
117
138
|
*/
|
|
118
|
-
function getTypeTransformer( csnUtils, stack ) {
|
|
139
|
+
function getTypeTransformer( csnUtils, stack, isCast ) {
|
|
119
140
|
return function typeTransformer( _parent, _prop, _type ) {
|
|
120
141
|
const finalSub = csnUtils.getFinalTypeInfo(_type);
|
|
121
142
|
|
|
@@ -132,8 +153,11 @@ function resolveTypes( csn, csnUtils, transformers, options ) {
|
|
|
132
153
|
delete _parent.type;
|
|
133
154
|
stack.push( _parent );
|
|
134
155
|
}
|
|
135
|
-
else if (finalSub?.type) {
|
|
136
|
-
|
|
156
|
+
else if (finalSub?.type && (options.resolveSimpleTypes || _parent.type.ref?.length > 1)) {
|
|
157
|
+
forEachKey(finalSub, (key) => { // copy `type` + properties (default, etc.)
|
|
158
|
+
if ((_parent[key] === undefined || key === 'type') && (isCast && !propertiesToSkipForCasts[key] || !isCast))
|
|
159
|
+
_parent[key] = finalSub[key];
|
|
160
|
+
});
|
|
137
161
|
}
|
|
138
162
|
};
|
|
139
163
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const { isBetaEnabled } = require('../base/model');
|
|
4
|
+
const { isBuiltinType } = require('../base/builtins');
|
|
4
5
|
const transformUtils = require('./transformUtils');
|
|
5
6
|
const {
|
|
6
7
|
forEachDefinition,
|
|
@@ -91,9 +92,10 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
91
92
|
|
|
92
93
|
const transformers = transformUtils.getTransformers(csn, options, messageFunctions, '_');
|
|
93
94
|
const {
|
|
94
|
-
addDefaultTypeFacets,
|
|
95
|
-
|
|
96
|
-
|
|
95
|
+
addDefaultTypeFacets, addElement,
|
|
96
|
+
checkMultipleAssignments, createScalarElement,
|
|
97
|
+
recurseElements, setAnnotation,
|
|
98
|
+
renameAnnotation, expandStructsInExpression,
|
|
97
99
|
csnUtils,
|
|
98
100
|
} = transformers;
|
|
99
101
|
|
|
@@ -293,6 +295,9 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
293
295
|
// Resolve annotation shorthands for entities, types, annotations, ...
|
|
294
296
|
renameShorthandAnnotations(def);
|
|
295
297
|
|
|
298
|
+
// Generate annotations and fields needed for the Fiori Tree Views out of the @hierarchy annotation
|
|
299
|
+
generateFioriTreeViewAnnotationsAndFields(def, defName);
|
|
300
|
+
|
|
296
301
|
// Annotate artifacts with their DB names if requested.
|
|
297
302
|
// Skip artifacts that have no DB equivalent anyway
|
|
298
303
|
if (options.sqlMapping && !(def.kind in skipPersNameKinds))
|
|
@@ -360,6 +365,124 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
360
365
|
//--------------------------------------------------------------------
|
|
361
366
|
// HELPER SECTION STARTS HERE
|
|
362
367
|
|
|
368
|
+
// =====================================================================
|
|
369
|
+
// Generate annotations and fields needed for the Fiori Tree Views out of the @hierarchy annotation
|
|
370
|
+
function generateFioriTreeViewAnnotationsAndFields(def, defName) {
|
|
371
|
+
const re = new RegExp('^@hierarchy#.+$');
|
|
372
|
+
if (Object.keys(def).some(key => re.test(key)))
|
|
373
|
+
error(null, [ 'definitions', defName ], {}, 'Assigning qualifier with the @hierarchy annotation is not supported');
|
|
374
|
+
|
|
375
|
+
if (!def['@hierarchy'])
|
|
376
|
+
return;
|
|
377
|
+
if (!(def.projection || def.query))
|
|
378
|
+
return;
|
|
379
|
+
|
|
380
|
+
const defKeys = [];
|
|
381
|
+
const assocs = [];
|
|
382
|
+
Object.entries(def.elements).forEach(([ name, elem ]) => {
|
|
383
|
+
if (elem.key)
|
|
384
|
+
defKeys.push([ name, elem ]);
|
|
385
|
+
if (isAssociation(elem) && elem.target === defName)
|
|
386
|
+
assocs.push([ name, elem ]);
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
// the definition must have exactly one scalar key field
|
|
390
|
+
if (defKeys.length !== 1 && !isBuiltinType(defKeys[0].type))
|
|
391
|
+
return;
|
|
392
|
+
const defKeyName = defKeys[0][0];
|
|
393
|
+
|
|
394
|
+
// if the @hierarchy annotation has a true value, we need to make sure the definition has exacly one
|
|
395
|
+
// association pointing to self and has the single key field as a foreign key
|
|
396
|
+
if (typeof def['@hierarchy'] === 'boolean' && def['@hierarchy'] === true) {
|
|
397
|
+
if (assocs.length !== 1)
|
|
398
|
+
return;
|
|
399
|
+
const assocName = assocs[0][0];
|
|
400
|
+
const assoc = assocs[0][1];
|
|
401
|
+
if (!isValidHierarchyAssociation(assoc, defKeyName))
|
|
402
|
+
return;
|
|
403
|
+
addHierarchyAnnotations(def, defName, defKeyName, assocName);
|
|
404
|
+
addHierarchyFields(def, defName);
|
|
405
|
+
}
|
|
406
|
+
else if (typeof def['@hierarchy'] === 'object' && def['@hierarchy']['=']) {
|
|
407
|
+
const assocName = def['@hierarchy']['='];
|
|
408
|
+
if (!isValidHierarchyAssociation(assocs.find(a => a[0] === assocName)[1], defKeyName))
|
|
409
|
+
return;
|
|
410
|
+
addHierarchyAnnotations(def, defName, defKeyName, assocName);
|
|
411
|
+
addHierarchyFields(def, defName);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
// returns true if assoc has one key and that one key is the corresponding 'keyName'
|
|
416
|
+
function isValidHierarchyAssociation(assoc, keyName) {
|
|
417
|
+
return assoc.keys.length === 1 && assoc.keys[0].ref.join() === keyName;
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
function addHierarchyAnnotations(def, defName, defKeyName, assocName) {
|
|
421
|
+
const qualifier = `${ defName.split('.').pop() }Hierarchy`;
|
|
422
|
+
|
|
423
|
+
[ `@Aggregation.RecursiveHierarchy#${ qualifier }`,
|
|
424
|
+
`@Aggregation.RecursiveHierarchy#${ qualifier }.NodeProperty`,
|
|
425
|
+
`@Aggregation.RecursiveHierarchy#${ qualifier }.ParentNavigationProperty`,
|
|
426
|
+
`@Hierarchy.RecursiveHierarchy#${ qualifier }`,
|
|
427
|
+
`@Hierarchy.RecursiveHierarchy#${ qualifier }.LimitedDescendantCount`,
|
|
428
|
+
`@Hierarchy.RecursiveHierarchy#${ qualifier }.DistanceFromRoot`,
|
|
429
|
+
`@Hierarchy.RecursiveHierarchy#${ qualifier }.DrillState`,
|
|
430
|
+
`@Hierarchy.RecursiveHierarchy#${ qualifier }.LimitedRank` ].forEach(anno => checkAndReportErrorWhenAnnotationExists(def, defName, anno));
|
|
431
|
+
|
|
432
|
+
setAnnotation(def, `@Aggregation.RecursiveHierarchy#${ qualifier }.NodeProperty`, { '=': defKeyName });
|
|
433
|
+
setAnnotation(def, `@Aggregation.RecursiveHierarchy#${ qualifier }.ParentNavigationProperty`, { '=': assocName });
|
|
434
|
+
setAnnotation(def, `@Hierarchy.RecursiveHierarchy#${ qualifier }.LimitedDescendantCount`, { '=': 'LimitedDescendantCount' });
|
|
435
|
+
setAnnotation(def, `@Hierarchy.RecursiveHierarchy#${ qualifier }.DistanceFromRoot`, { '=': 'DistanceFromRoot' });
|
|
436
|
+
setAnnotation(def, `@Hierarchy.RecursiveHierarchy#${ qualifier }.DrillState`, { '=': 'DrillState' });
|
|
437
|
+
setAnnotation(def, `@Hierarchy.RecursiveHierarchy#${ qualifier }.LimitedRank`, { '=': 'LimitedRank' });
|
|
438
|
+
|
|
439
|
+
// if @Capabilities.FilterRestrictions.NonFilterableProperties or @Capabilities.SortRestrictions.NonSortableProperties
|
|
440
|
+
// are already defined, we append to the existing value the created elements
|
|
441
|
+
if (def['@Capabilities.FilterRestrictions.NonFilterableProperties'] && Array.isArray(def['@Capabilities.FilterRestrictions.NonFilterableProperties'])) {
|
|
442
|
+
def['@Capabilities.FilterRestrictions.NonFilterableProperties'].push('LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank');
|
|
443
|
+
}
|
|
444
|
+
else {
|
|
445
|
+
setAnnotation(def, '@Capabilities.FilterRestrictions.NonFilterableProperties',
|
|
446
|
+
[ 'LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank' ]);
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (def['@Capabilities.SortRestrictions.NonSortableProperties'] && Array.isArray(def['@Capabilities.SortRestrictions.NonSortableProperties'])) {
|
|
450
|
+
def['@Capabilities.FilterRestrictions.NonFilterableProperties'].push('LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank');
|
|
451
|
+
}
|
|
452
|
+
else {
|
|
453
|
+
setAnnotation(def, '@Capabilities.SortRestrictions.NonSortableProperties',
|
|
454
|
+
[ 'LimitedDescendantCount', 'DistanceFromRoot', 'DrillState', 'LimitedRank' ]);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
function addHierarchyFields(def, defName) {
|
|
459
|
+
const limitedDescendantCount = createScalarElement('LimitedDescendantCount', 'cds.Integer');
|
|
460
|
+
limitedDescendantCount.LimitedDescendantCount['@Core.Computed'] = true;
|
|
461
|
+
limitedDescendantCount.LimitedDescendantCount.$calc = { val: null };
|
|
462
|
+
addElement(limitedDescendantCount, def, defName);
|
|
463
|
+
|
|
464
|
+
const distanceFromRoot = createScalarElement('DistanceFromRoot', 'cds.Integer');
|
|
465
|
+
distanceFromRoot.DistanceFromRoot['@Core.Computed'] = true;
|
|
466
|
+
distanceFromRoot.DistanceFromRoot.$calc = { val: null };
|
|
467
|
+
addElement(distanceFromRoot, def, defName);
|
|
468
|
+
|
|
469
|
+
const drillState = createScalarElement('DrillState', 'cds.String');
|
|
470
|
+
drillState.DrillState['@Core.Computed'] = true;
|
|
471
|
+
drillState.DrillState.$calc = { val: null };
|
|
472
|
+
addElement(drillState, def, defName);
|
|
473
|
+
|
|
474
|
+
const limitedRank = createScalarElement('LimitedRank', 'cds.Integer');
|
|
475
|
+
limitedRank.LimitedRank['@Core.Computed'] = true;
|
|
476
|
+
limitedRank.LimitedRank.$calc = { val: null };
|
|
477
|
+
addElement(limitedRank, def, defName);
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
function checkAndReportErrorWhenAnnotationExists(def, defName, anno) {
|
|
481
|
+
if (def[anno])
|
|
482
|
+
error(null, [ 'definitions', defName ], { anno, name: defName }, 'Annotation $(ANNO) is already defined on the hierarchy entity $(NAME)');
|
|
483
|
+
}
|
|
484
|
+
// =====================================================================
|
|
485
|
+
|
|
363
486
|
// Transform @readonly/@mandatory/@disabled into @Common.FieldControl annotation
|
|
364
487
|
// with a when/then/else expression consisting of the input from the annotations.
|
|
365
488
|
function processDynamicFieldControlAnnotations(node) {
|
|
@@ -81,6 +81,7 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
|
|
|
81
81
|
let expandStructsInExpression;
|
|
82
82
|
let flattenStructuredElement;
|
|
83
83
|
let flattenStructStepsInRef;
|
|
84
|
+
let expandManagedToFksInArray;
|
|
84
85
|
|
|
85
86
|
bindCsnReference();
|
|
86
87
|
|
|
@@ -219,8 +220,6 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
|
|
|
219
220
|
}
|
|
220
221
|
});
|
|
221
222
|
|
|
222
|
-
processCalculatedElementsInEntities(csn, options);
|
|
223
|
-
|
|
224
223
|
timetrace.start('Transform CSN');
|
|
225
224
|
|
|
226
225
|
// Rename primitive types, make UUID a String; replace `items` by cds.LargeString
|
|
@@ -255,6 +254,15 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
|
|
|
255
254
|
forEachDefinition(csn, flattenIndexes);
|
|
256
255
|
// Managed associations get an on-condition - in views and entities
|
|
257
256
|
associations.attachOnConditions(csn, csnUtils, pathDelimiter);
|
|
257
|
+
|
|
258
|
+
// Add the foreign keys also to the columns if the association itself was explicitly selected
|
|
259
|
+
// TODO: Extend the expansion to also expand managed to their foreign
|
|
260
|
+
// TODO: Remember where in .elements we had associations and do it then?
|
|
261
|
+
applyTransformations(csn, {
|
|
262
|
+
columns: expandManagedToFksInArray(),
|
|
263
|
+
groupBy: expandManagedToFksInArray(true),
|
|
264
|
+
orderBy: expandManagedToFksInArray(true),
|
|
265
|
+
}, [], { allowArtifact: artifact => artifact.query !== undefined || artifact.projection !== undefined });
|
|
258
266
|
}
|
|
259
267
|
|
|
260
268
|
{
|
|
@@ -265,11 +273,11 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
|
|
|
265
273
|
// To be done after handleAssociations, since then the foreign keys of the managed assocs
|
|
266
274
|
// are part of the elements
|
|
267
275
|
if (doA2J)
|
|
268
|
-
fns.push(associations.getFKAccessFinalizer(csn, options, csnUtils, pathDelimiter));
|
|
276
|
+
fns.push(associations.getFKAccessFinalizer(csn, options, csnUtils, pathDelimiter, true));
|
|
269
277
|
|
|
270
278
|
forEachDefinition(csn, fns);
|
|
271
279
|
}
|
|
272
|
-
|
|
280
|
+
processCalculatedElementsInEntities(csn, options);
|
|
273
281
|
// Create convenience views for localized entities/views.
|
|
274
282
|
// To be done after getFKAccessFinalizer because associations are
|
|
275
283
|
// handled and before handleDBChecks which removes the localized attribute.
|
|
@@ -474,6 +482,7 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
|
|
|
474
482
|
addDefaultTypeFacets,
|
|
475
483
|
expandStructsInExpression,
|
|
476
484
|
csnUtils,
|
|
485
|
+
expandManagedToFksInArray,
|
|
477
486
|
} = transformUtils.getTransformers(csn, options, messageFunctions, pathDelimiter));
|
|
478
487
|
}
|
|
479
488
|
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
const { setProp } = require('../base/model');
|
|
8
8
|
|
|
9
9
|
const { copyAnnotations, applyTransformations, isDeepEqual } = require('../model/csnUtils');
|
|
10
|
-
const { getUtils } = require('../model/csnUtils');
|
|
10
|
+
const { getUtils, implicitAs } = require('../model/csnUtils');
|
|
11
11
|
const { typeParameters } = require('../compiler/builtins');
|
|
12
12
|
const { isBuiltinType } = require('../base/builtins');
|
|
13
13
|
const { ModelError, CompilerAssertion } = require('../base/error');
|
|
@@ -60,6 +60,7 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
|
|
|
60
60
|
resetAnnotation,
|
|
61
61
|
expandStructsInExpression: tuples.expandStructsInExpression,
|
|
62
62
|
flattenPath: tuples.flattenPath,
|
|
63
|
+
expandManagedToFksInArray,
|
|
63
64
|
};
|
|
64
65
|
|
|
65
66
|
/**
|
|
@@ -876,6 +877,55 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
|
|
|
876
877
|
}
|
|
877
878
|
return wasOverwritten;
|
|
878
879
|
}
|
|
880
|
+
|
|
881
|
+
/**
|
|
882
|
+
* Expand managed associations in an array and insert them in-place
|
|
883
|
+
*
|
|
884
|
+
* If requested, leave out the assocs themselves
|
|
885
|
+
*
|
|
886
|
+
* @param {boolean} [killAssoc=false]
|
|
887
|
+
* @returns {Function} applyTransformationsCallback
|
|
888
|
+
*/
|
|
889
|
+
function expandManagedToFksInArray( killAssoc = false ) {
|
|
890
|
+
return function expand(parent, prop, array, path) {
|
|
891
|
+
const newColumns = [];
|
|
892
|
+
for (let i = 0; i < array.length; i++) {
|
|
893
|
+
const col = array[i];
|
|
894
|
+
const element = csnUtils.getElement(col) || col.ref && csnUtils.inspectRef(path.concat(prop, i)).art;
|
|
895
|
+
if (!killAssoc || !element?.keys)
|
|
896
|
+
newColumns.push(col);
|
|
897
|
+
if (element?.keys)
|
|
898
|
+
element.keys.forEach(fk => addForeignKeyToColumns(fk, newColumns, col, options));
|
|
899
|
+
}
|
|
900
|
+
parent[prop] = newColumns;
|
|
901
|
+
};
|
|
902
|
+
}
|
|
903
|
+
|
|
904
|
+
/**
|
|
905
|
+
* FKs need to be added to the .columns
|
|
906
|
+
* @todo stolen from lib/transform/db/views.js
|
|
907
|
+
* @todo Can we maybe do this during expansion already?
|
|
908
|
+
* @param {object} foreignKey
|
|
909
|
+
* @param {object[]} columns
|
|
910
|
+
* @param {CSN.Column} associationColumn
|
|
911
|
+
* @param {CSN.Options} options
|
|
912
|
+
*/
|
|
913
|
+
function addForeignKeyToColumns( foreignKey, columns, associationColumn, options ) {
|
|
914
|
+
const ref = cloneCsnNonDict(associationColumn.ref, options);
|
|
915
|
+
ref[ref.length - 1] = [ ref.at(-1).id || ref.at(-1) ].concat(foreignKey.as || foreignKey.ref).join(pathDelimiter);
|
|
916
|
+
const result = {
|
|
917
|
+
ref,
|
|
918
|
+
};
|
|
919
|
+
if (associationColumn.as) {
|
|
920
|
+
const columnName = `${ associationColumn.as }${ pathDelimiter }${ foreignKey.as || implicitAs(foreignKey.ref) }`;
|
|
921
|
+
result.as = columnName;
|
|
922
|
+
}
|
|
923
|
+
|
|
924
|
+
if (associationColumn.key)
|
|
925
|
+
result.key = true;
|
|
926
|
+
|
|
927
|
+
columns.push(result);
|
|
928
|
+
}
|
|
879
929
|
}
|
|
880
930
|
|
|
881
931
|
/**
|
|
@@ -152,7 +152,7 @@ function translateAssocsToJoins(model, inputOptions = {}) {
|
|
|
152
152
|
// (same rewrite as (injected) assoc ON cond paths but with different table alias).
|
|
153
153
|
// 5) Prepend table alias to all remaining paths
|
|
154
154
|
env.walkover = {
|
|
155
|
-
from: false, onCondFrom: true, select: true, filter: false,
|
|
155
|
+
from: false, onCondFrom: true, select: true, filter: false, virtual: true,
|
|
156
156
|
};
|
|
157
157
|
env.callback = substituteDollarSelf;
|
|
158
158
|
forEachQuery(walkQuery, env);
|
|
@@ -340,19 +340,38 @@ function translateAssocsToJoins(model, inputOptions = {}) {
|
|
|
340
340
|
// if the assoc is not join relevant, we should have found the foreign key
|
|
341
341
|
if (tail[i]._navigation.$njr && !fk)
|
|
342
342
|
throw new CompilerAssertion('Debug me: No FK found for FK rewriting');
|
|
343
|
-
|
|
344
|
-
if (fk && fk.name.id !== tail[i + 1].id)
|
|
345
|
-
tail[i + 1].id = fk.name.id; // fk renamed
|
|
346
343
|
}
|
|
347
344
|
}
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
345
|
+
// leave virtual paths as is
|
|
346
|
+
if ( tail.at(-1)._artifact.virtual?.val === true ) {
|
|
347
|
+
replaceNodeContent(pathNode, constructPathNode([ constructTableAliasPathStep(QA), ...tail ]));
|
|
348
|
+
}
|
|
349
|
+
else {
|
|
350
|
+
const newTail = [];
|
|
351
|
+
// construct the tail (segment after last join relevant navigation) and preserve association steps
|
|
352
|
+
// each association may have a structured prefix
|
|
353
|
+
// `struct.foo.bar.assoc. otherStruct.baz.otherAssoc.fk`
|
|
354
|
+
// --> `[otherStruct_baz_otherAssoc, fk]
|
|
355
|
+
for (let i = 0; i < tail.length; i++) {
|
|
356
|
+
if (tail[i]._navigation) {
|
|
357
|
+
const nextNavigation = tail.slice(i + 1).findIndex(ps => ps._navigation);
|
|
358
|
+
if (nextNavigation >= 0) {
|
|
359
|
+
newTail.push({
|
|
360
|
+
id: tail.slice(i, i + 1 + nextNavigation).map(ps => ps.id).join(pathDelimiter),
|
|
361
|
+
_artifact: tail[i + nextNavigation]._artifact,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
else {
|
|
365
|
+
newTail.push({
|
|
366
|
+
id: tail.slice(i).map(ps => ps.id).join(pathDelimiter),
|
|
367
|
+
_artifact: tail.at(-1)._artifact,
|
|
368
|
+
});
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
replaceNodeContent(pathNode,
|
|
373
|
+
constructPathNode([ constructTableAliasPathStep(QA), ...newTail ]));
|
|
374
|
+
}
|
|
356
375
|
}
|
|
357
376
|
|
|
358
377
|
function findForeignKey(assoc, fk) {
|
|
@@ -777,13 +796,18 @@ function translateAssocsToJoins(model, inputOptions = {}) {
|
|
|
777
796
|
// points to the signature of the current view and ensures that the ON cond is
|
|
778
797
|
// resolvable) make sure that the original assoc target is contained in the local
|
|
779
798
|
// query source
|
|
780
|
-
|
|
799
|
+
const fwdTarget = fwdOrigin.target._artifact;
|
|
800
|
+
const assocSource = assoc._main;
|
|
801
|
+
if (assoc.kind === 'mixin' && (i > 0 || fwdTarget !== assocSource) && fwdOrigin.kind !== 'mixin') {
|
|
781
802
|
const tas = Object.values(env.lead.$tableAliases);
|
|
782
|
-
const
|
|
783
|
-
if (
|
|
784
|
-
newTgtAlias.id = tas[
|
|
785
|
-
newTgtAlias._artifact = tas[
|
|
786
|
-
newTgtAlias._navigation = tas[
|
|
803
|
+
const j = tas.findIndex(ta => ta._artifact === fwdOrigin.target._artifact);
|
|
804
|
+
if (j >= 0 && tas[j].$QA) {
|
|
805
|
+
newTgtAlias.id = tas[j].$QA.name.id;
|
|
806
|
+
newTgtAlias._artifact = tas[j]._effectiveType;
|
|
807
|
+
newTgtAlias._navigation = tas[j].$QA.path[0]._navigation;
|
|
808
|
+
}
|
|
809
|
+
else if (i === 0 && fwdTarget !== assocSource) {
|
|
810
|
+
newTgtAlias = Object.assign(newTgtAlias, srcAlias);
|
|
787
811
|
}
|
|
788
812
|
else {
|
|
789
813
|
error(null, [ assocQAT._origin.location, assocQAT._origin ], { name: fwdOrigin.target._artifact.name.id, art: assoc.name.id },
|
|
@@ -1823,7 +1847,7 @@ function walkPath(node, env) {
|
|
|
1823
1847
|
const art = path?.length && path[path.length - 1]._artifact;
|
|
1824
1848
|
|
|
1825
1849
|
// regardless of the position in the query, ignore paths that have virtual path steps
|
|
1826
|
-
if (art && path.some(ps => ps._artifact?.virtual?.val))
|
|
1850
|
+
if (art && path.some(ps => ps._artifact?.virtual?.val) && !env.walkover.virtual)
|
|
1827
1851
|
return path;
|
|
1828
1852
|
if (art && !internalArtifactKinds.includes(art.kind) && node.scope !== 'param') {
|
|
1829
1853
|
if (env.callback) {
|