@sap/cds-compiler 5.3.0 → 5.4.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 +30 -2
- package/bin/cdsc.js +1 -1
- package/doc/CHANGELOG_BETA.md +2 -2
- package/lib/api/options.js +4 -2
- package/lib/base/builtins.js +0 -10
- package/lib/base/keywords.js +3 -31
- package/lib/base/message-registry.js +23 -5
- package/lib/base/messages.js +1 -1
- package/lib/checks/existsMustEndInAssoc.js +7 -2
- package/lib/checks/foreignKeys.js +12 -7
- package/lib/compiler/assert-consistency.js +11 -3
- package/lib/compiler/builtins.js +2 -0
- package/lib/compiler/checks.js +88 -38
- package/lib/compiler/define.js +2 -2
- package/lib/compiler/shared.js +9 -10
- package/lib/compiler/xpr-rewrite.js +11 -0
- package/lib/compiler/xsn-model.js +1 -1
- package/lib/edm/csn2edm.js +2 -0
- package/lib/edm/edm.js +2 -1
- package/lib/edm/edmPreprocessor.js +14 -1
- package/lib/edm/edmUtils.js +17 -2
- package/lib/gen/BaseParser.js +291 -197
- package/lib/gen/CdlParser.js +1631 -1605
- package/lib/gen/Dictionary.json +74 -6
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +1 -1
- package/lib/gen/languageParser.js +1808 -1804
- package/lib/language/antlrParser.js +8 -4
- package/lib/language/genericAntlrParser.js +3 -3
- package/lib/model/csnUtils.js +6 -1
- package/lib/optionProcessor.js +4 -0
- package/lib/parsers/AstBuildingParser.js +172 -108
- package/lib/parsers/CdlGrammar.g4 +154 -134
- package/lib/parsers/Lexer.js +3 -3
- package/lib/parsers/identifiers.js +59 -0
- package/lib/render/toCdl.js +5 -5
- package/lib/render/utils/common.js +5 -0
- package/lib/render/utils/delta.js +23 -5
- package/lib/transform/db/expansion.js +2 -1
- package/lib/transform/db/rewriteCalculatedElements.js +11 -5
- package/lib/transform/db/transformExists.js +52 -26
- package/lib/transform/effective/annotations.js +147 -0
- package/lib/transform/effective/main.js +17 -3
- package/lib/transform/forOdata.js +53 -10
- package/lib/transform/forRelationalDB.js +8 -1
- package/lib/transform/odata/createForeignKeys.js +180 -0
- package/lib/transform/odata/flattening.js +135 -19
- package/lib/transform/odata/typesExposure.js +4 -3
- package/lib/transform/transformUtils.js +6 -6
- package/package.json +1 -1
|
@@ -10,6 +10,8 @@ const { forEachDefinition,
|
|
|
10
10
|
isAspect,
|
|
11
11
|
getServiceNames,
|
|
12
12
|
forEachGeneric,
|
|
13
|
+
cardinality2str,
|
|
14
|
+
getUtils
|
|
13
15
|
} = require('../model/csnUtils');
|
|
14
16
|
const { checkCSNVersion } = require('../json/csnVersion');
|
|
15
17
|
const validate = require('../checks/validator');
|
|
@@ -18,6 +20,7 @@ const expandToFinalBaseType = require('./odata/toFinalBaseType');
|
|
|
18
20
|
const { timetrace } = require('../utils/timetrace');
|
|
19
21
|
const enrichUniversalCsn = require('./universalCsn/universalCsnEnricher');
|
|
20
22
|
const flattening = require('./odata/flattening');
|
|
23
|
+
const createForeignKeyElements = require('./odata/createForeignKeys');
|
|
21
24
|
const associations = require('./db/associations')
|
|
22
25
|
const expansion = require('./db/expansion');
|
|
23
26
|
const generateDrafts = require('./draft/odata');
|
|
@@ -181,24 +184,31 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
181
184
|
// If errors are detected, throwWithAnyError() will return from further processing
|
|
182
185
|
expandStructsInExpression(csn, { skipArtifact: isExternalServiceMember, drillRef: true });
|
|
183
186
|
|
|
187
|
+
// do expansion before Fk creation because of messages reporting
|
|
184
188
|
if (!structuredOData) {
|
|
185
189
|
expansion.expandStructureReferences(csn, options, '_',
|
|
186
190
|
{ error, info, throwWithAnyError }, csnUtils,
|
|
187
191
|
{ skipArtifact: isExternalServiceMember });
|
|
188
|
-
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
createForeignKeyElements(csn, options, messageFunctions, csnUtils, { skipArtifact: isExternalServiceMember });
|
|
189
195
|
|
|
196
|
+
if (!structuredOData) {
|
|
197
|
+
const resolved = new WeakMap();
|
|
190
198
|
const { inspectRef, effectiveType } = csnRefs(csn);
|
|
199
|
+
const { getFinalTypeInfo } = getUtils(csn);
|
|
191
200
|
const { adaptRefs, transformer: refFlattener } =
|
|
192
201
|
flattening.getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, options, resolved, '_');
|
|
193
202
|
|
|
194
|
-
flattening.allInOneFlattening(csn, refFlattener, adaptRefs,
|
|
195
|
-
|
|
203
|
+
const allMgdAssocDefs = flattening.allInOneFlattening(csn, refFlattener, adaptRefs,
|
|
204
|
+
inspectRef, getFinalTypeInfo, isExternalServiceMember, error, csnUtils, options);
|
|
196
205
|
flattening.flattenAllStructStepsInRefs(csn, refFlattener, adaptRefs,
|
|
197
206
|
inspectRef, effectiveType, csnUtils, error, options,
|
|
198
207
|
{ //skip: ['action', 'aspect', 'event', 'function', 'type'],
|
|
199
208
|
skipArtifact: isExternalServiceMember,
|
|
200
209
|
});
|
|
201
|
-
|
|
210
|
+
flattening.replaceManagedAssocsAsKeys(allMgdAssocDefs, csnUtils);
|
|
211
|
+
|
|
202
212
|
// replace structured with flat dictionaries that contain
|
|
203
213
|
// rewritten path expressions
|
|
204
214
|
forEachDefinition(csn, (def) => {
|
|
@@ -223,16 +233,14 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
223
233
|
});
|
|
224
234
|
}
|
|
225
235
|
|
|
226
|
-
|
|
227
|
-
// see db/views.js::addForeignKeysToColumns
|
|
228
|
-
flattening.handleManagedAssociationsAndCreateForeignKeys(csn, options, messageFunctions, '_',
|
|
229
|
-
!structuredOData, csnUtils,{ skipArtifact: isExternalServiceMember });
|
|
236
|
+
bindCsnReferenceOnly();
|
|
230
237
|
|
|
231
238
|
// Allow using managed associations as steps in on-conditions to access their fks
|
|
232
239
|
// To be done after handleManagedAssociationsAndCreateForeignKeys,
|
|
233
240
|
// since then the foreign keys of the managed assocs are part of the elements
|
|
234
|
-
if(!structuredOData)
|
|
241
|
+
if(!structuredOData) {
|
|
235
242
|
forEachDefinition(csn, associations.getFKAccessFinalizer(csn, csnUtils, '_'));
|
|
243
|
+
}
|
|
236
244
|
|
|
237
245
|
// structure flattener reports errors, further processing is not safe -> throw exception in case of errors
|
|
238
246
|
throwWithAnyError();
|
|
@@ -277,7 +285,8 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
277
285
|
!(propertyName === 'enum' || propertyName === 'returns') &&
|
|
278
286
|
(!member.virtual || def.query)) {
|
|
279
287
|
// If we have a 'preserved dotted name' (i.e. we are a result of flattening), use that for the @cds.persistence.name annotation
|
|
280
|
-
member['@cds.persistence.name'] = getElementDatabaseNameOf(member.$defPath?.slice(1).join('.')
|
|
288
|
+
member['@cds.persistence.name'] = getElementDatabaseNameOf((!member['@odata.foreignKey4'] && member.$defPath?.slice(1).join('.'))
|
|
289
|
+
|| memberName, options.sqlMapping, 'hana'); // hana to allow "hdbcds"
|
|
281
290
|
}
|
|
282
291
|
|
|
283
292
|
// Mark fields with @odata.on.insert/update as @Core.Computed
|
|
@@ -286,6 +295,12 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
286
295
|
// Resolve annotation shorthands for elements, actions, action parameters
|
|
287
296
|
renameShorthandAnnotations(member);
|
|
288
297
|
|
|
298
|
+
// If an association was modelled as not null, like so:
|
|
299
|
+
// <associationName>: Association to <target> not null;
|
|
300
|
+
// a cardinality property is set to the association member
|
|
301
|
+
// with the value { "min": 1 };
|
|
302
|
+
setCardinalityToNotNullAssociations(member);
|
|
303
|
+
|
|
289
304
|
// - If the association target is annotated with @cds.odata.valuelist, annotate the
|
|
290
305
|
// association with @Common.ValueList.viaAssociation
|
|
291
306
|
// - Check for @Analytics.Measure and @Aggregation.default
|
|
@@ -419,6 +434,27 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
419
434
|
}
|
|
420
435
|
}
|
|
421
436
|
|
|
437
|
+
// If an association was modelled as not null, like so:
|
|
438
|
+
// <associationName>: Association to <target> not null;
|
|
439
|
+
// a cardinality property is set to the association member
|
|
440
|
+
// with the value { "min": 1 };
|
|
441
|
+
function setCardinalityToNotNullAssociations(member) {
|
|
442
|
+
if (member.target && member.keys && !member.on) {
|
|
443
|
+
if (member.notNull) {
|
|
444
|
+
if (member.cardinality === undefined)
|
|
445
|
+
member.cardinality = {};
|
|
446
|
+
// min=0 is falsy => check for undefined
|
|
447
|
+
if (member.cardinality.min === undefined) {
|
|
448
|
+
member.cardinality.min = 1;
|
|
449
|
+
}
|
|
450
|
+
else if (member.cardinality.min === 0) {
|
|
451
|
+
warning(null, member.$path, { value: cardinality2str(member, false), code: 'not null' },
|
|
452
|
+
'Expected target cardinality $(VALUE) and $(CODE) to match');
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
422
458
|
// Apply default type facets to each type definition and every member
|
|
423
459
|
// But do not apply default string length (as in DB)
|
|
424
460
|
function setDefaultTypeFacets(def) {
|
|
@@ -466,4 +502,11 @@ function transform4odataWithCsn(inputModel, options, messageFunctions) {
|
|
|
466
502
|
}
|
|
467
503
|
}
|
|
468
504
|
|
|
505
|
+
function bindCsnReferenceOnly() {
|
|
506
|
+
// invalidate caches for CSN ref API
|
|
507
|
+
const csnRefApi = csnRefs(csn);
|
|
508
|
+
Object.assign(csnUtils, csnRefApi);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
|
|
469
512
|
} // transform4odataWithCsn
|
|
@@ -248,7 +248,7 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
|
|
|
248
248
|
}
|
|
249
249
|
});
|
|
250
250
|
|
|
251
|
-
processCalculatedElementsInEntities(csn);
|
|
251
|
+
processCalculatedElementsInEntities(csn, options);
|
|
252
252
|
|
|
253
253
|
timetrace.start('Transform CSN')
|
|
254
254
|
|
|
@@ -856,6 +856,13 @@ function transformForRelationalDBWithCsn(csn, options, messageFunctions) {
|
|
|
856
856
|
checkTypeParamValue(node, 'srid', { max: Number.MAX_SAFE_INTEGER }, path);
|
|
857
857
|
break;
|
|
858
858
|
}
|
|
859
|
+
case 'cds.Map': {
|
|
860
|
+
if (options.sqlDialect === 'plain')
|
|
861
|
+
error('ref-unsupported-type', path, { '#': 'dialect', type: node.type, value: 'plain' });
|
|
862
|
+
else if (options.transformation === 'hdbcds')
|
|
863
|
+
error('ref-unsupported-type', path, {'#': 'hdbcds', type: node.type, value: options.sqlDialect });
|
|
864
|
+
break;
|
|
865
|
+
}
|
|
859
866
|
case 'cds.Vector': {
|
|
860
867
|
if (options.sqlDialect !== 'hana') {
|
|
861
868
|
error('ref-unsupported-type', path, {
|
|
@@ -0,0 +1,180 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const { isBuiltinType } = require('../../base/builtins');
|
|
4
|
+
const { setProp } = require('../../base/model');
|
|
5
|
+
const { applyTransformations, implicitAs, findAnnotationExpression, copyAnnotations, isDeepEqual } = require('../../model/csnUtils');
|
|
6
|
+
const { EdmTypeFacetNames } = require('../../edm/EdmPrimitiveTypeDefinitions');
|
|
7
|
+
|
|
8
|
+
function createForeignKeyElements(csn, options, messageFunctions, csnUtils, iterateOptions = {}) {
|
|
9
|
+
|
|
10
|
+
const { error } = messageFunctions;
|
|
11
|
+
|
|
12
|
+
applyTransformations(csn, { elements: createForeignKeysInCsn, params: createForeignKeysInCsn},
|
|
13
|
+
[], Object.assign(iterateOptions, { skip: ['aspect', 'event'], skipStandard: { targetAspect: true } }));
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Process a given elements or params dictionary and create foreign key elements.
|
|
17
|
+
*
|
|
18
|
+
* @param {object} parent The thing HAVING params or elements
|
|
19
|
+
* @param {string} prop
|
|
20
|
+
* @param {object} dict The params or elements thing
|
|
21
|
+
* @param {CSN.Path} path
|
|
22
|
+
*/
|
|
23
|
+
function createForeignKeysInCsn( parent, prop, dict, path ) {
|
|
24
|
+
const orderedElements = [];
|
|
25
|
+
// First, generate the FK elements for given element
|
|
26
|
+
Object.entries(dict).forEach(([elementName, element]) => {
|
|
27
|
+
orderedElements.push([ elementName, element ]);
|
|
28
|
+
if (!csnUtils.isManagedAssociation(element)) return;
|
|
29
|
+
const elementPath = path.concat(prop, elementName);
|
|
30
|
+
const generatedForeignKeys = createForeignKeysForElement(csnUtils, elementPath, element, elementName, csn, options, '_');
|
|
31
|
+
|
|
32
|
+
// Second, finalize the generated FK elements
|
|
33
|
+
const refCount = generatedForeignKeys.reduce((acc, fk) => {
|
|
34
|
+
// count duplicates
|
|
35
|
+
if (acc[fk[0]])
|
|
36
|
+
acc[fk[0]]++;
|
|
37
|
+
else
|
|
38
|
+
acc[fk[0]] = 1;
|
|
39
|
+
|
|
40
|
+
// check for name clash with existing elements
|
|
41
|
+
if (parent[prop][fk[0]] && isDeepEqual(element, parent[prop][fk[0]], true)) {
|
|
42
|
+
// error location is the colliding element
|
|
43
|
+
error('name-duplicate-element', elementPath, { '#': 'flatten-fkey-exists', name: fk[0], art: elementName });
|
|
44
|
+
}
|
|
45
|
+
// attach a proper $path
|
|
46
|
+
setProp(element, '$path', elementPath);
|
|
47
|
+
return acc;
|
|
48
|
+
}, Object.create(null));
|
|
49
|
+
|
|
50
|
+
// set default for single foreign key from association (if available)
|
|
51
|
+
if (element.default?.val !== undefined && generatedForeignKeys.length === 1)
|
|
52
|
+
generatedForeignKeys[0][1].default = element.default;
|
|
53
|
+
|
|
54
|
+
// check for duplicate foreign keys
|
|
55
|
+
Object.entries(refCount).forEach(([ name, occ ]) => {
|
|
56
|
+
if (occ > 1)
|
|
57
|
+
error('name-duplicate-element', elementPath, { '#': 'flatten-fkey-gen', name, art: elementName });
|
|
58
|
+
});
|
|
59
|
+
if (element.keys) {
|
|
60
|
+
if (options.transformation === 'effective')
|
|
61
|
+
delete element.default;
|
|
62
|
+
}
|
|
63
|
+
setProp(element, '$generatedForeignKeys', generatedForeignKeys.map(gfk => gfk[0]));
|
|
64
|
+
orderedElements.push(...generatedForeignKeys);
|
|
65
|
+
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
parent[prop] = orderedElements.reduce((elementsAccumulator, [ name, element ]) => {
|
|
69
|
+
elementsAccumulator[name] = element;
|
|
70
|
+
return elementsAccumulator;
|
|
71
|
+
}, Object.create(null));
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function createForeignKeysForElement(csnUtils, path, element, prefix, csn, options, pathDelimiter, lvl = 0 ) {
|
|
75
|
+
const special$self = !csn?.definitions?.$self && '$self';
|
|
76
|
+
const isInspectRefResult = !Array.isArray(path);
|
|
77
|
+
|
|
78
|
+
let fks = [];
|
|
79
|
+
if (!element)
|
|
80
|
+
return fks;
|
|
81
|
+
|
|
82
|
+
let finalElement = element;
|
|
83
|
+
let finalTypeName;
|
|
84
|
+
|
|
85
|
+
// resolve derived type
|
|
86
|
+
if (element.type && !isBuiltinType(element.type) && element.type !== special$self) {
|
|
87
|
+
const tmpElt = csnUtils.effectiveType(element);
|
|
88
|
+
// effective type resolves to structs and enums only but not scalars
|
|
89
|
+
if (Object.keys(tmpElt).length) {
|
|
90
|
+
finalElement = tmpElt;
|
|
91
|
+
finalTypeName = finalElement.$path[1];
|
|
92
|
+
}
|
|
93
|
+
else {
|
|
94
|
+
// unwind a derived type chain to a scalar type
|
|
95
|
+
while (finalElement?.type && !isBuiltinType(finalElement?.type)) {
|
|
96
|
+
finalTypeName = finalElement.type;
|
|
97
|
+
finalElement = csn.definitions[finalElement.type];
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
if (!finalElement)
|
|
103
|
+
return [];
|
|
104
|
+
|
|
105
|
+
// main if for this function
|
|
106
|
+
// the element is a managed association
|
|
107
|
+
if (csnUtils.isManagedAssociation(finalElement)) {
|
|
108
|
+
finalElement.keys.forEach((key, keyIndex) => {
|
|
109
|
+
const continuePath = getContinuePath([ 'keys', keyIndex ]);
|
|
110
|
+
const alias = key.as || implicitAs(key.ref);
|
|
111
|
+
const result = csnUtils.inspectRef(continuePath);
|
|
112
|
+
fks = fks.concat(createForeignKeysForElement(csnUtils, result, result.art, alias, csn, options, pathDelimiter, lvl + 1));
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
// the element is a structure
|
|
116
|
+
else if (finalElement.elements) {
|
|
117
|
+
Object.entries(finalElement.elements).forEach(([ elemName, elem ]) => {
|
|
118
|
+
// Skip already produced foreign keys
|
|
119
|
+
if (!elem['@odata.foreignKey4']) {
|
|
120
|
+
const continuePath = getContinuePath([ 'elements', elemName ]);
|
|
121
|
+
fks = fks.concat(createForeignKeysForElement(csnUtils, continuePath, elem, elemName, csn, options, pathDelimiter, lvl + 1));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
// we have reached a leaf element, create a foreign key
|
|
126
|
+
else if ((finalElement.type == null || isBuiltinType(finalElement.type)) && !finalElement.on) {
|
|
127
|
+
const newFk = Object.create(null);
|
|
128
|
+
[ 'type', 'length', 'scale', 'precision', 'srid', 'default', '@odata.Type', ...EdmTypeFacetNames.map(f => `@odata.${f}`) ].forEach((prop) => {
|
|
129
|
+
// copy props from original element to preserve derived types!
|
|
130
|
+
if (element[prop] !== undefined)
|
|
131
|
+
newFk[prop] = element[prop];
|
|
132
|
+
});
|
|
133
|
+
return [ [ prefix, newFk ] ];
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
fks.forEach((fk) => {
|
|
137
|
+
// prepend current prefix
|
|
138
|
+
fk[0] = `${prefix}${pathDelimiter}${fk[0]}`;
|
|
139
|
+
// if this is the entry association, decorate the final foreign keys with the association props/annos
|
|
140
|
+
if (lvl === 0) {
|
|
141
|
+
if (options.transformation !== 'effective')
|
|
142
|
+
fk[1]['@odata.foreignKey4'] = prefix;
|
|
143
|
+
|
|
144
|
+
const validAnnoNames = Object.keys(element).filter(pn => pn[0] === '@' && !findAnnotationExpression(element, pn));
|
|
145
|
+
copyAnnotations(element, fk[1], true, {}, validAnnoNames);
|
|
146
|
+
// propagate not null to final foreign key
|
|
147
|
+
for (const prop of [ 'notNull', 'key' ]) {
|
|
148
|
+
if (element[prop] !== undefined)
|
|
149
|
+
fk[1][prop] = element[prop];
|
|
150
|
+
}
|
|
151
|
+
if (element.$location)
|
|
152
|
+
setProp(fk[1], '$location', element.$location);
|
|
153
|
+
}
|
|
154
|
+
});
|
|
155
|
+
return fks;
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Get the path to continue resolving references
|
|
159
|
+
*
|
|
160
|
+
* If we are currently inside of a type, we need to start our path fresh from that given type.
|
|
161
|
+
* Otherwise, we would try to resolve .elements on a thing that does not exist.
|
|
162
|
+
*
|
|
163
|
+
* We also respect if we have a previous inspectRef result as our base.
|
|
164
|
+
*
|
|
165
|
+
* @param {Array} additions
|
|
166
|
+
* @returns {CSN.Path}
|
|
167
|
+
*/
|
|
168
|
+
function getContinuePath( additions ) {
|
|
169
|
+
if (csn.definitions[finalElement.type])
|
|
170
|
+
return [ 'definitions', finalElement.type, ...additions ];
|
|
171
|
+
else if (finalTypeName)
|
|
172
|
+
return [ 'definitions', finalTypeName, ...additions ];
|
|
173
|
+
else if (isInspectRefResult)
|
|
174
|
+
return [ path, ...additions ];
|
|
175
|
+
return [ ...path, ...additions ];
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
module.exports = createForeignKeyElements;
|
|
@@ -5,14 +5,16 @@ const { forEachDefinition,
|
|
|
5
5
|
transformExpression, transformAnnotationExpression } = require('../../model/csnUtils');
|
|
6
6
|
const { isBuiltinType, isMagicVariable } = require('../../base/builtins');
|
|
7
7
|
const transformUtils = require('../transformUtils');
|
|
8
|
-
const { setProp } = require('../../base/model');
|
|
8
|
+
const { setProp, forEachGeneric } = require('../../base/model');
|
|
9
9
|
const { applyTransformationsOnDictionary,
|
|
10
10
|
applyTransformationsOnNonDictionary } = require('../db/applyTransformations.js');
|
|
11
11
|
const { handleManagedAssociationsAndCreateForeignKeys } = require('../db/flattening');
|
|
12
12
|
const { cloneCsnNonDict } = require('../../model/cloneCsn');
|
|
13
13
|
const { forEach } = require('../../utils/objectUtils');
|
|
14
|
+
const { assignAnnotation } = require('../../edm/edmUtils');
|
|
14
15
|
|
|
15
|
-
function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternalServiceMember, error, csnUtils, options) {
|
|
16
|
+
function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, getFinalTypeInfo, isExternalServiceMember, error, csnUtils, options) {
|
|
17
|
+
const allMgdAssocDefs = [];
|
|
16
18
|
forEachDefinition(csn, (def, defName) => {
|
|
17
19
|
if (def.kind === 'entity' && !isExternalServiceMember(def, defName)) {
|
|
18
20
|
['elements', 'params'].forEach(dictName => {
|
|
@@ -27,7 +29,7 @@ function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternal
|
|
|
27
29
|
let resolvedElt = child;
|
|
28
30
|
let typeIdx = 0;
|
|
29
31
|
if (child.type && !child.elements) {
|
|
30
|
-
resolvedElt =
|
|
32
|
+
resolvedElt = getFinalTypeInfo(child.type);
|
|
31
33
|
if (resolvedElt.elements)
|
|
32
34
|
typeIdx = rootPrefix.length + 1;
|
|
33
35
|
}
|
|
@@ -41,10 +43,12 @@ function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternal
|
|
|
41
43
|
{ '#': 'flatten-element-exist', name: flatEltName });
|
|
42
44
|
propagateToFlatElem(child, flatElt);
|
|
43
45
|
rewriteOnCondition(flatElt, flattenedSubTree);
|
|
46
|
+
adaptManagedAssociationSpecialFields(flatElt, flatEltName, flattenedSubTree);
|
|
44
47
|
orderedElementList.push([flatEltName, flatElt]);
|
|
45
48
|
});
|
|
46
49
|
}
|
|
47
50
|
else {
|
|
51
|
+
// TODO: run adaptManagedAssociationSpecialFields here as well
|
|
48
52
|
const flatElt = cloneElt(dictName, child, location, [ defName, childName ]);
|
|
49
53
|
orderedElementList.push([childName, flatElt]);
|
|
50
54
|
}
|
|
@@ -53,16 +57,26 @@ function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternal
|
|
|
53
57
|
const flatDict = orderedElementList.reduce((elements, [ flatEltName, flatElt ]) => {
|
|
54
58
|
if (flatElt.items) {
|
|
55
59
|
// rewrite annotation paths inside items.elements
|
|
56
|
-
forEachMemberRecursively(flatElt.items, (elt,
|
|
60
|
+
forEachMemberRecursively(flatElt.items, (elt, eltName, _prop, path) => {
|
|
57
61
|
const exprAnnos = Object.keys(elt).filter(pn => pn[0] === '@');
|
|
58
62
|
flattenAndPrefixExprPaths(elt, exprAnnos, elt.$path, path, 0, true);
|
|
63
|
+
if (csnUtils.isManagedAssociation(elt)) {
|
|
64
|
+
allMgdAssocDefs.push(elt);
|
|
65
|
+
}
|
|
59
66
|
}, [ flatEltName ], true, { pathWithoutProp: true } );
|
|
60
67
|
}
|
|
68
|
+
setProp(flatElt, '$pathInStructuredModel', flatElt.$path);
|
|
61
69
|
setProp(flatElt, '$path', [ ...csnPath, flatEltName ]);
|
|
62
70
|
elements[flatEltName] = flatElt;
|
|
63
71
|
return elements;
|
|
64
72
|
}, Object.create(null));
|
|
65
73
|
setProp(def, `$flat${dictName}`, flatDict);
|
|
74
|
+
orderedElementList.reduce(
|
|
75
|
+
(mgdAssocs, [ _flatEltName, flatElt ]) => {
|
|
76
|
+
if(csnUtils.isManagedAssociation(flatElt))
|
|
77
|
+
mgdAssocs.push(flatElt);
|
|
78
|
+
return mgdAssocs;
|
|
79
|
+
}, allMgdAssocDefs);
|
|
66
80
|
}
|
|
67
81
|
});
|
|
68
82
|
// entity annotations
|
|
@@ -126,7 +140,30 @@ function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternal
|
|
|
126
140
|
});
|
|
127
141
|
}
|
|
128
142
|
}
|
|
143
|
+
// loop through types as well in order to collect the managaed associations
|
|
144
|
+
// that reside in types definitions
|
|
145
|
+
if ((def.kind === 'action' || def.kind === 'function' || def.kind === 'type') && !isExternalServiceMember(def, defName)) {
|
|
146
|
+
if (def.kind === 'type' && csnUtils.isManagedAssociation(def))
|
|
147
|
+
allMgdAssocDefs.push(def);
|
|
148
|
+
else
|
|
149
|
+
forEachMemberRecursively(def, (elt, _eltName, _prop, _path) => {
|
|
150
|
+
if (csnUtils.isManagedAssociation(elt)) {
|
|
151
|
+
allMgdAssocDefs.push(elt);
|
|
152
|
+
}
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
// loop through actions/functions and action/function parameters as well
|
|
156
|
+
if (def.kind === 'entity' && def.actions && !isExternalServiceMember(def, defName)) {
|
|
157
|
+
forEachGeneric(def, 'actions', (act) => {
|
|
158
|
+
forEachMemberRecursively(act, (elt, _eltName, _prop, _path) => {
|
|
159
|
+
if (csnUtils.isManagedAssociation(elt)) {
|
|
160
|
+
allMgdAssocDefs.push(elt);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
});
|
|
164
|
+
}
|
|
129
165
|
});
|
|
166
|
+
return allMgdAssocDefs;
|
|
130
167
|
|
|
131
168
|
function recurseIntoElement(scope, elt, resolvedElt, rootPathIsNotNull, location, rootPrefix = [], typeIdx = 0) {
|
|
132
169
|
const eltName = rootPrefix[rootPrefix.length-1];
|
|
@@ -143,7 +180,7 @@ function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternal
|
|
|
143
180
|
forEach(resolvedElt.elements, (childName, child) => {
|
|
144
181
|
resolvedElt = child;
|
|
145
182
|
if (child.type && !child.elements) {
|
|
146
|
-
resolvedElt =
|
|
183
|
+
resolvedElt = getFinalTypeInfo(child.type);
|
|
147
184
|
if (resolvedElt.elements)
|
|
148
185
|
typeIdx = rootPrefix.length + 1;
|
|
149
186
|
}
|
|
@@ -206,14 +243,16 @@ function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternal
|
|
|
206
243
|
function cloneElt(scope, elt, location, rootPrefix = [], typeIdx = 0) {
|
|
207
244
|
const flatElt = cloneCsnNonDict(elt,
|
|
208
245
|
{ ...options,
|
|
209
|
-
hiddenPropertiesToClone: [ '$structRef', '$fkExtensions' ]
|
|
246
|
+
hiddenPropertiesToClone: [ '$structRef', '$fkExtensions', '$generatedForeignKeys' ]
|
|
210
247
|
} );
|
|
211
248
|
|
|
212
249
|
// needed for @cds.persistence.name
|
|
213
250
|
setProp(flatElt, '$defPath', rootPrefix);
|
|
214
251
|
setProp(flatElt, '$scope', scope);
|
|
215
252
|
retypeCloneWithFinalBaseType(flatElt, location);
|
|
216
|
-
|
|
253
|
+
if((elt._type || elt).type === 'cds.Map') {
|
|
254
|
+
assignAnnotation(flatElt, '@open', elt['@open'] || true);
|
|
255
|
+
}
|
|
217
256
|
const [ nonAnnoProps, exprAnnoProps ] = Object.keys(flatElt).reduce((acc, pn) => {
|
|
218
257
|
if (pn[0] !== '@' && pn !== 'value')
|
|
219
258
|
acc[0].push(pn);
|
|
@@ -405,6 +444,25 @@ function allInOneFlattening(csn, refFlattener, adaptRefs, inspectRef, isExternal
|
|
|
405
444
|
});
|
|
406
445
|
}
|
|
407
446
|
}
|
|
447
|
+
|
|
448
|
+
// update the @odata.foreignKey4 and the $generatedForeignKeys array of
|
|
449
|
+
// an association that have been flattened
|
|
450
|
+
function adaptManagedAssociationSpecialFields(flatElt, flatEltName, flattenedSubTree) {
|
|
451
|
+
if (!(flatElt.target || flatElt.keys) && !flatElt.on )
|
|
452
|
+
return;
|
|
453
|
+
const structuredAssocName = flatElt.$defPath[flatElt.$defPath.length - 1];
|
|
454
|
+
const generatedForeignKeysForAssoc = flattenedSubTree
|
|
455
|
+
.filter(se => {
|
|
456
|
+
// compare $defPath without the last element
|
|
457
|
+
let comeFromSameDef = flatElt.$defPath.slice(0, flatElt.$defPath.length - 1).join('.') === se[1].$defPath.slice(0, se[1].$defPath.length - 1).join('.');
|
|
458
|
+
return (comeFromSameDef && (se[1]['@odata.foreignKey4'] && se[1]['@odata.foreignKey4'] === structuredAssocName) && se[0].startsWith(flatEltName))
|
|
459
|
+
});
|
|
460
|
+
|
|
461
|
+
generatedForeignKeysForAssoc.forEach(gfk => gfk[1]['@odata.foreignKey4'] = flatEltName);
|
|
462
|
+
// reassign the generated foreign keys for current assoc in order to assign
|
|
463
|
+
// correct values for $generatedFieldName later on during flattenManagedAssocsAsKeys();
|
|
464
|
+
setProp(flatElt, '$generatedForeignKeys', generatedForeignKeysForAssoc.map(gfk => gfk[0]));
|
|
465
|
+
}
|
|
408
466
|
}
|
|
409
467
|
|
|
410
468
|
function flattenAllStructStepsInRefs( csn, refFlattener, adaptRefs, inspectRef, effectiveType,
|
|
@@ -494,6 +552,8 @@ function getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, optio
|
|
|
494
552
|
const { links, art, scope } = inspectRef(path);
|
|
495
553
|
const resolvedLinkTypes = resolveLinkTypes(links);
|
|
496
554
|
setProp(parent, '$path', [ ...path ]);
|
|
555
|
+
if (insideKeys(path))
|
|
556
|
+
setProp(parent, '_art', art);
|
|
497
557
|
const lastRef = ref[ref.length - 1];
|
|
498
558
|
const fn = (suspend = false, suspendPos = 0,
|
|
499
559
|
refFilter = (_parent) => true) => {
|
|
@@ -536,18 +596,19 @@ function getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, optio
|
|
|
536
596
|
|| path[path.length - 3] === 'projection')
|
|
537
597
|
&& path[path.length - 2] === 'columns';
|
|
538
598
|
}
|
|
539
|
-
/**
|
|
540
|
-
* Return true if the path points inside keys
|
|
541
|
-
*
|
|
542
|
-
* @param {CSN.Path} path
|
|
543
|
-
* @returns {boolean}
|
|
544
|
-
*/
|
|
545
|
-
function insideKeys( path ) {
|
|
546
|
-
return path.length >= 3
|
|
547
|
-
&& path[path.length - 2] === 'keys'
|
|
548
|
-
&& typeof path[path.length - 1] === 'number';
|
|
549
|
-
}
|
|
550
599
|
};
|
|
600
|
+
/**
|
|
601
|
+
* Return true if the path points inside keys
|
|
602
|
+
*
|
|
603
|
+
* @param {CSN.Path} path
|
|
604
|
+
* @returns {boolean}
|
|
605
|
+
*/
|
|
606
|
+
function insideKeys( path ) {
|
|
607
|
+
return path.length >= 3
|
|
608
|
+
&& path[path.length - 2] === 'keys'
|
|
609
|
+
&& typeof path[path.length - 1] === 'number';
|
|
610
|
+
}
|
|
611
|
+
|
|
551
612
|
|
|
552
613
|
// adapt queries later
|
|
553
614
|
if(ctx?.annoExpr?.['=']) {
|
|
@@ -565,9 +626,64 @@ function getStructRefFlatteningTransformer(csn, inspectRef, effectiveType, optio
|
|
|
565
626
|
return { adaptRefs, transformer, inspectRef, effectiveType };
|
|
566
627
|
}
|
|
567
628
|
|
|
629
|
+
// replace managed associations in key refs with their respective foreing keys
|
|
630
|
+
function replaceManagedAssocsAsKeys(allFlatManagedAssocDefinitions, csnUtils) {
|
|
631
|
+
allFlatManagedAssocDefinitions.forEach( assoc => {
|
|
632
|
+
let finished = false;
|
|
633
|
+
|
|
634
|
+
while (!finished) {
|
|
635
|
+
const newKeys = [];
|
|
636
|
+
finished = processKeys(newKeys);
|
|
637
|
+
assoc.keys = newKeys;
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function processKeys(collector) {
|
|
641
|
+
let done = true;
|
|
642
|
+
assoc.keys.forEach( key => {
|
|
643
|
+
const art = key._art;
|
|
644
|
+
if (art && csnUtils.isManagedAssociation(art)) {
|
|
645
|
+
done = false;
|
|
646
|
+
// key._art is the artifact from the structured model, because of that we need to look
|
|
647
|
+
// for the flat representation in the allFlatManagedAssocDefinitions
|
|
648
|
+
allFlatManagedAssocDefinitions.find(fa => (fa.$pathInStructuredModel || fa.$path).join() === art.$path.join())
|
|
649
|
+
.keys.forEach( keyAssocKey => {
|
|
650
|
+
collector.push(cloneAndExtendRef(keyAssocKey, key));
|
|
651
|
+
});
|
|
652
|
+
}
|
|
653
|
+
else if (art && !art.on){
|
|
654
|
+
if (!key.$generatedFieldName) {
|
|
655
|
+
const flatAssocName = assoc.$path[assoc.$path.length - 1];
|
|
656
|
+
// When we have a definition like type "<type_name>: Association to <target_name>",
|
|
657
|
+
// we do not generate foreign keys in the definition, therefore no $generatedForeignKeys,
|
|
658
|
+
// respectively we do not assign $generatedFieldName
|
|
659
|
+
if (assoc.$generatedForeignKeys) {
|
|
660
|
+
const generatedForeignKey = assoc.$generatedForeignKeys.find(gfk => gfk === `${flatAssocName}_${key.as || key.ref.join('_')}`);
|
|
661
|
+
key.$generatedFieldName = generatedForeignKey;
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
if (key.as && key.as === key.ref[0]) delete key.as;
|
|
665
|
+
collector.push(key);
|
|
666
|
+
}
|
|
667
|
+
})
|
|
668
|
+
|
|
669
|
+
return done;
|
|
670
|
+
}
|
|
671
|
+
});
|
|
672
|
+
|
|
673
|
+
function cloneAndExtendRef(keyAssocKey, key) {
|
|
674
|
+
let newKey = { ref: [`${key.ref.join('_')}_${keyAssocKey.as || keyAssocKey.ref.join('_')}`] };
|
|
675
|
+
setProp(newKey, '_art', keyAssocKey._art);
|
|
676
|
+
if (key.as) {
|
|
677
|
+
newKey.as = `${key.as}_${keyAssocKey.as || keyAssocKey.ref.join('_')}`;
|
|
678
|
+
}
|
|
679
|
+
return newKey;
|
|
680
|
+
}
|
|
681
|
+
}
|
|
682
|
+
|
|
568
683
|
module.exports = {
|
|
569
684
|
allInOneFlattening,
|
|
570
685
|
flattenAllStructStepsInRefs,
|
|
571
686
|
handleManagedAssociationsAndCreateForeignKeys,
|
|
572
|
-
getStructRefFlatteningTransformer
|
|
687
|
+
getStructRefFlatteningTransformer,
|
|
688
|
+
replaceManagedAssocsAsKeys
|
|
573
689
|
};
|
|
@@ -64,6 +64,7 @@ const { cloneCsnNonDict } = require('../../model/cloneCsn');
|
|
|
64
64
|
function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackSchemaName, options, csnUtils, message) {
|
|
65
65
|
const { error } = message;
|
|
66
66
|
const special$self = !csn?.definitions?.$self && '$self';
|
|
67
|
+
|
|
67
68
|
// are we working with OData proxies or cross-service refs
|
|
68
69
|
const isMultiSchema = options.odataVersion === 'v4' && (options.odataProxies || options.odataXServiceRefs);
|
|
69
70
|
// service sub schemas as return value
|
|
@@ -166,7 +167,7 @@ function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackS
|
|
|
166
167
|
isKey = false;
|
|
167
168
|
// in case this was a named type and if the openness does not match the type definition
|
|
168
169
|
// expose the type as a new one not changing the original definition.
|
|
169
|
-
if(elements && !!node['@open'] !== !!typeDef['@open'])
|
|
170
|
+
if(elements && node['@open'] != null && !!node['@open'] !== !!typeDef['@open'])
|
|
170
171
|
fullQualifiedNewTypeName += node['@open'] ? '_open' : '_closed';
|
|
171
172
|
lastNonAnonymousFQDefName = fullQualifiedNewTypeName;
|
|
172
173
|
}
|
|
@@ -293,10 +294,10 @@ function typesExposure(csn, whatsMyServiceName, requestedServiceNames, fallBackS
|
|
|
293
294
|
if(type) {
|
|
294
295
|
typeName = (type.ref && csnUtils.artifactRef(type)) || type;
|
|
295
296
|
const rc = { isExposable: true, typeDef, typeName, isAnonymous: false };
|
|
296
|
-
if(!isBuiltinType(typeName) && typeName !== special$self) {
|
|
297
|
+
if((!isBuiltinType(typeName) || typeName === 'cds.Map') && typeName !== special$self) {
|
|
297
298
|
rc.typeDef = typeDef = csnUtils.artifactRef(typeName, typeName);
|
|
298
299
|
if(!isArtifactInService(typeName, serviceName)) {
|
|
299
|
-
while(!isBuiltinType(typeName)) {
|
|
300
|
+
while(!isBuiltinType(typeName) || typeDef.$emtpyMapType) {
|
|
300
301
|
typeDef = csnUtils.artifactRef(typeName, typeName);
|
|
301
302
|
if(typeDef !== typeName) {
|
|
302
303
|
// Implementation note: For `type S: T:struct;`, elements from `T:struct` were already propagated to `S`.
|
|
@@ -1332,14 +1332,14 @@ function getTransformers(model, options, msgFunctions, pathDelimiter = '_') {
|
|
|
1332
1332
|
|
|
1333
1333
|
function isScalarOrNoType(art) {
|
|
1334
1334
|
art = effectiveType(art);
|
|
1335
|
-
if(art) {
|
|
1336
|
-
const type = art.type ||
|
|
1337
|
-
// items in ON
|
|
1335
|
+
if (art) {
|
|
1336
|
+
const type = art.type || art.items?.type;
|
|
1337
|
+
// items in ON-conditions are illegal but this should be checked elsewhere
|
|
1338
1338
|
const elements = art.elements || (art.items && art.items.elements);
|
|
1339
1339
|
// @Core.Computed has no type
|
|
1340
|
-
return(!elements && !type ||
|
|
1341
|
-
|
|
1342
|
-
|
|
1340
|
+
return (!elements && !type ||
|
|
1341
|
+
(type && isBuiltinType(type) &&
|
|
1342
|
+
type !== 'cds.Association' && type !== 'cds.Composition' && type !== 'cds.Map'))
|
|
1343
1343
|
}
|
|
1344
1344
|
return false;
|
|
1345
1345
|
}
|