@sap/cds-compiler 2.12.0 → 2.13.6
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 +110 -15
- package/bin/cdsc.js +13 -13
- package/bin/cdsse.js +2 -2
- package/doc/CHANGELOG_BETA.md +13 -6
- package/doc/CHANGELOG_DEPRECATED.md +22 -6
- package/doc/NameResolution.md +21 -16
- package/lib/api/main.js +28 -63
- package/lib/api/options.js +3 -3
- package/lib/api/validate.js +0 -5
- package/lib/backends.js +15 -23
- package/lib/base/dictionaries.js +0 -8
- package/lib/base/error.js +26 -0
- package/lib/base/keywords.js +7 -17
- package/lib/base/location.js +9 -4
- package/lib/base/message-registry.js +25 -4
- package/lib/base/messages.js +16 -26
- package/lib/base/model.js +2 -63
- package/lib/base/optionProcessorHelper.js +158 -123
- package/lib/checks/annotationsOData.js +1 -1
- package/lib/checks/cdsPersistence.js +2 -1
- package/lib/checks/enricher.js +17 -1
- package/lib/checks/invalidTarget.js +3 -1
- package/lib/checks/managedWithoutKeys.js +3 -1
- package/lib/checks/selectItems.js +4 -4
- package/lib/checks/sql-snippets.js +27 -26
- package/lib/checks/types.js +1 -1
- package/lib/checks/validator.js +4 -7
- package/lib/compiler/assert-consistency.js +5 -3
- package/lib/compiler/builtins.js +8 -6
- package/lib/compiler/checks.js +14 -3
- package/lib/compiler/cycle-detector.js +1 -1
- package/lib/compiler/define.js +1103 -0
- package/lib/compiler/extend.js +983 -0
- package/lib/compiler/finalize-parse-cdl.js +231 -0
- package/lib/compiler/index.js +32 -13
- package/lib/compiler/kick-start.js +190 -0
- package/lib/compiler/moduleLayers.js +4 -4
- package/lib/compiler/populate.js +1226 -0
- package/lib/compiler/propagator.js +111 -46
- package/lib/compiler/resolve.js +1433 -0
- package/lib/compiler/shared.js +64 -37
- package/lib/compiler/tweak-assocs.js +529 -0
- package/lib/compiler/utils.js +197 -33
- package/lib/edm/.eslintrc.json +5 -0
- package/lib/edm/annotations/genericTranslation.js +5 -9
- package/lib/edm/annotations/preprocessAnnotations.js +2 -2
- package/lib/edm/csn2edm.js +9 -8
- package/lib/edm/edm.js +11 -12
- package/lib/edm/edmPreprocessor.js +137 -73
- package/lib/edm/edmUtils.js +116 -22
- package/lib/gen/Dictionary.json +10 -3
- package/lib/gen/language.checksum +1 -1
- package/lib/gen/language.interp +9 -1
- package/lib/gen/language.tokens +86 -83
- package/lib/gen/languageLexer.interp +10 -1
- package/lib/gen/languageLexer.js +860 -833
- package/lib/gen/languageLexer.tokens +78 -75
- package/lib/gen/languageParser.js +5282 -4265
- package/lib/json/from-csn.js +12 -1
- package/lib/json/to-csn.js +126 -66
- package/lib/language/docCommentParser.js +2 -2
- package/lib/language/genericAntlrParser.js +76 -3
- package/lib/language/language.g4 +297 -130
- package/lib/language/multiLineStringParser.js +5 -5
- package/lib/main.d.ts +468 -59
- package/lib/main.js +35 -9
- package/lib/model/api.js +3 -1
- package/lib/model/csnRefs.js +225 -156
- package/lib/model/csnUtils.js +192 -223
- package/lib/model/enrichCsn.js +70 -29
- package/lib/model/revealInternalProperties.js +27 -6
- package/lib/model/sortViews.js +2 -1
- package/lib/modelCompare/compare.js +17 -12
- package/lib/optionProcessor.js +5 -4
- package/lib/render/manageConstraints.js +35 -32
- package/lib/render/toCdl.js +73 -288
- package/lib/render/toHdbcds.js +25 -23
- package/lib/render/toSql.js +98 -41
- package/lib/render/utils/common.js +5 -10
- package/lib/render/utils/sql.js +4 -3
- package/lib/render/utils/stringEscapes.js +111 -0
- package/lib/sql-identifier.js +1 -1
- package/lib/transform/.eslintrc.json +5 -0
- package/lib/transform/db/.eslintrc.json +2 -0
- package/lib/transform/db/applyTransformations.js +35 -12
- package/lib/transform/db/assertUnique.js +1 -1
- package/lib/transform/db/associations.js +103 -305
- package/lib/transform/db/cdsPersistence.js +2 -2
- package/lib/transform/db/constraints.js +55 -52
- package/lib/transform/db/expansion.js +46 -24
- package/lib/transform/db/flattening.js +553 -102
- package/lib/transform/db/groupByOrderBy.js +3 -1
- package/lib/transform/db/transformExists.js +59 -6
- package/lib/transform/db/views.js +5 -4
- package/lib/transform/draft/.eslintrc.json +38 -0
- package/lib/transform/{db/draft.js → draft/db.js} +6 -5
- package/lib/transform/draft/odata.js +227 -0
- package/lib/transform/forHanaNew.js +67 -183
- package/lib/transform/forOdataNew.js +17 -171
- package/lib/transform/localized.js +34 -19
- package/lib/transform/odata/generateForeignKeyElements.js +1 -1
- package/lib/transform/odata/referenceFlattener.js +95 -89
- package/lib/transform/odata/structureFlattener.js +1 -1
- package/lib/transform/odata/toFinalBaseType.js +86 -12
- package/lib/transform/odata/typesExposure.js +5 -5
- package/lib/transform/odata/utils.js +2 -2
- package/lib/transform/transformUtilsNew.js +36 -22
- package/lib/transform/translateAssocsToJoins.js +2 -19
- package/lib/transform/universalCsn/.eslintrc.json +36 -0
- package/lib/transform/universalCsn/coreComputed.js +170 -0
- package/lib/transform/universalCsn/universalCsnEnricher.js +715 -0
- package/lib/transform/universalCsn/utils.js +63 -0
- package/lib/utils/objectUtils.js +30 -0
- package/package.json +1 -1
- package/share/messages/README.md +26 -0
- package/lib/compiler/definer.js +0 -2361
- package/lib/compiler/resolver.js +0 -3079
- package/lib/transform/universalCsnEnricher.js +0 -237
|
@@ -1,8 +1,10 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
|
|
3
3
|
const {
|
|
4
|
-
|
|
5
|
-
applyTransformations,
|
|
4
|
+
getUtils, walkCsnPath,
|
|
5
|
+
applyTransformations, applyTransformationsOnNonDictionary,
|
|
6
|
+
isBuiltinType, cloneCsn,
|
|
7
|
+
copyAnnotations, implicitAs, isDeepEqual,
|
|
6
8
|
} = require('../../model/csnUtils');
|
|
7
9
|
const transformUtils = require('../transformUtilsNew');
|
|
8
10
|
const { csnRefs } = require('../../model/csnRefs');
|
|
@@ -15,26 +17,21 @@ const { setProp } = require('../../base/model');
|
|
|
15
17
|
*/
|
|
16
18
|
function removeLeadingSelf(csn) {
|
|
17
19
|
const magicVars = [ '$now', '$self', '$projection', '$user', '$session', '$at' ];
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
if (ref[0] === '$self' && ref.length > 1 && !magicVars.includes(ref[1]))
|
|
30
|
-
root.ref = ref.slice(1);
|
|
31
|
-
},
|
|
32
|
-
});
|
|
33
|
-
}
|
|
20
|
+
applyTransformations(csn, {
|
|
21
|
+
elements: (parent, prop, elements) => {
|
|
22
|
+
for (const [ elementName, element ] of Object.entries(elements)) {
|
|
23
|
+
if (element.on) {
|
|
24
|
+
applyTransformationsOnNonDictionary(elements, elementName, {
|
|
25
|
+
ref: (root, name, ref) => {
|
|
26
|
+
// Renderers seem to expect it to not be there...
|
|
27
|
+
if (ref[0] === '$self' && ref.length > 1 && !magicVars.includes(ref[1]))
|
|
28
|
+
root.ref = ref.slice(1);
|
|
29
|
+
},
|
|
30
|
+
});
|
|
34
31
|
}
|
|
35
|
-
}
|
|
36
|
-
}
|
|
37
|
-
});
|
|
32
|
+
}
|
|
33
|
+
}, /* only for kind entity and view */ /* do not go into .actions */
|
|
34
|
+
}, [], { skipIgnore: false, allowArtifact: artifact => (artifact.kind === 'entity'), skipDict: { actions: true } });
|
|
38
35
|
}
|
|
39
36
|
|
|
40
37
|
/**
|
|
@@ -46,8 +43,9 @@ function removeLeadingSelf(csn) {
|
|
|
46
43
|
* @param {CSN.Options} options
|
|
47
44
|
* @param {WeakMap} resolved Cache for resolved refs
|
|
48
45
|
* @param {string} pathDelimiter
|
|
46
|
+
* @param {object} iterateOptions
|
|
49
47
|
*/
|
|
50
|
-
function resolveTypeReferences(csn, options, resolved, pathDelimiter) {
|
|
48
|
+
function resolveTypeReferences(csn, options, resolved, pathDelimiter, iterateOptions = {}) {
|
|
51
49
|
/**
|
|
52
50
|
* Remove .localized from the element and any sub-elements
|
|
53
51
|
*
|
|
@@ -69,15 +67,24 @@ function resolveTypeReferences(csn, options, resolved, pathDelimiter) {
|
|
|
69
67
|
}
|
|
70
68
|
}
|
|
71
69
|
const { toFinalBaseType } = transformUtils.getTransformers(csn, options, pathDelimiter);
|
|
70
|
+
const { getServiceName, getFinalBaseType } = getUtils(csn);
|
|
71
|
+
|
|
72
|
+
// We don't want to iterate over actions
|
|
73
|
+
if (iterateOptions.skipDict && !iterateOptions.skipDict.actions)
|
|
74
|
+
iterateOptions.skipDict.actions = true;
|
|
75
|
+
else
|
|
76
|
+
iterateOptions.skipDict = { actions: true };
|
|
72
77
|
applyTransformations(csn, {
|
|
73
78
|
cast: (parent) => {
|
|
74
79
|
// Resolve cast already - we otherwise lose .localized
|
|
75
80
|
if (parent.cast.type && !isBuiltinType(parent.cast.type))
|
|
76
81
|
toFinalBaseType(parent.cast, resolved, true);
|
|
77
82
|
},
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
83
|
+
// @ts-ignore
|
|
84
|
+
type: (parent, prop, type, csnPath) => {
|
|
85
|
+
if (options.toOdata && parent.kind && [ 'aspect', 'event', 'type' ].includes(parent.kind))
|
|
86
|
+
return;
|
|
87
|
+
if (!isBuiltinType(type) && (!options.toOdata || options.toOdata && !isODataV4BuiltinFromService(type) && !isODataItems(type))) {
|
|
81
88
|
toFinalBaseType(parent, resolved);
|
|
82
89
|
// structured types might not have the child-types replaced.
|
|
83
90
|
// Drill down to ensure this.
|
|
@@ -94,32 +101,74 @@ function resolveTypeReferences(csn, options, resolved, pathDelimiter) {
|
|
|
94
101
|
}
|
|
95
102
|
}
|
|
96
103
|
}
|
|
97
|
-
|
|
98
|
-
if (!directLocalized)
|
|
104
|
+
const directLocalized = parent.localized || false;
|
|
105
|
+
if (!directLocalized && !options.toOdata)
|
|
99
106
|
removeLocalized(parent);
|
|
100
107
|
}
|
|
101
108
|
// HANA/SQLite do not support array-of - turn into CLOB/Text
|
|
102
|
-
if (parent.items) {
|
|
109
|
+
if (parent.items && !options.toOdata) {
|
|
103
110
|
parent.type = 'cds.LargeString';
|
|
104
111
|
delete parent.items;
|
|
105
112
|
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* OData V4 only:
|
|
116
|
+
* Do not replace a type ref if:
|
|
117
|
+
* The type definition is terminating on a scalar type (that can also be a derived type chain)
|
|
118
|
+
* AND the typeName (that is the start of that (derived) type chain is defined within the same
|
|
119
|
+
* service as the artifact from which the type reference has to be resolved.
|
|
120
|
+
*
|
|
121
|
+
* @param {string} typeName
|
|
122
|
+
* @returns {boolean}
|
|
123
|
+
*/
|
|
124
|
+
function isODataV4BuiltinFromService(typeName) {
|
|
125
|
+
if (!options.toOdata || (options.toOdata && options.toOdata.version === 'v2'))
|
|
126
|
+
return false;
|
|
127
|
+
|
|
128
|
+
const typeServiceName = getServiceName(typeName);
|
|
129
|
+
const finalBaseType = getFinalBaseType(typeName);
|
|
130
|
+
// we need the service of the current definition
|
|
131
|
+
const currDefServiceName = getServiceName(csnPath[1]);
|
|
132
|
+
|
|
133
|
+
return typeServiceName === currDefServiceName && isBuiltinType(finalBaseType);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* OData stops replacing types @ 'items', if the type ref is a user defined type
|
|
138
|
+
* AND that type has items, don't do toFinalBaseType
|
|
139
|
+
*
|
|
140
|
+
* @param {string} typeName
|
|
141
|
+
* @returns {boolean}
|
|
142
|
+
*/
|
|
143
|
+
function isODataItems(typeName) {
|
|
144
|
+
const typeDef = csn.definitions[typeName];
|
|
145
|
+
return !!(options.toOdata && typeDef && typeDef.items);
|
|
146
|
+
}
|
|
106
147
|
},
|
|
107
148
|
// HANA/SQLite do not support array-of - turn into CLOB/Text
|
|
108
149
|
items: (parent) => {
|
|
109
|
-
|
|
110
|
-
|
|
150
|
+
// OData has no LargeString substitution and doesn't expand types under items
|
|
151
|
+
if (!options.toOdata) {
|
|
152
|
+
parent.type = 'cds.LargeString';
|
|
153
|
+
delete parent.items;
|
|
154
|
+
}
|
|
111
155
|
},
|
|
112
156
|
}, [ (definitions, artifactName, artifact) => {
|
|
113
157
|
// Replace events, actions and functions with simple dummies - they don't have effect on forHanaNew stuff
|
|
114
158
|
// and that way they contain no references and don't hurt.
|
|
115
|
-
|
|
159
|
+
|
|
160
|
+
// Do not do for OData
|
|
161
|
+
// TODO:factor out somewhere else
|
|
162
|
+
if (!options.toOdata &&
|
|
163
|
+
([ 'action', 'function', 'event' ].includes(artifact.kind))) {
|
|
116
164
|
const dummy = { kind: artifact.kind };
|
|
117
165
|
if (artifact.$location)
|
|
118
166
|
setProp(dummy, '$location', artifact.$location);
|
|
119
167
|
|
|
120
168
|
definitions[artifactName] = dummy;
|
|
121
169
|
}
|
|
122
|
-
|
|
170
|
+
// TODO: skipDict options as default function arguments not via Object.assign
|
|
171
|
+
} ], iterateOptions);
|
|
123
172
|
}
|
|
124
173
|
|
|
125
174
|
/**
|
|
@@ -127,8 +176,9 @@ function resolveTypeReferences(csn, options, resolved, pathDelimiter) {
|
|
|
127
176
|
* @param {CSN.Options} options
|
|
128
177
|
* @param {WeakMap} resolved Cache for resolved refs
|
|
129
178
|
* @param {string} pathDelimiter
|
|
179
|
+
* @param {object} iterateOptions
|
|
130
180
|
*/
|
|
131
|
-
function flattenAllStructStepsInRefs(csn, options, resolved, pathDelimiter) {
|
|
181
|
+
function flattenAllStructStepsInRefs(csn, options, resolved, pathDelimiter, iterateOptions = {}) {
|
|
132
182
|
const { inspectRef, effectiveType } = csnRefs(csn);
|
|
133
183
|
const { flattenStructStepsInRef } = transformUtils.getTransformers(csn, options, pathDelimiter);
|
|
134
184
|
const adaptRefs = [];
|
|
@@ -153,6 +203,7 @@ function flattenAllStructStepsInRefs(csn, options, resolved, pathDelimiter) {
|
|
|
153
203
|
}
|
|
154
204
|
|
|
155
205
|
applyTransformations(csn, {
|
|
206
|
+
// @ts-ignore
|
|
156
207
|
ref: (parent, prop, ref, path) => {
|
|
157
208
|
const { links, art, scope } = inspectRef(path);
|
|
158
209
|
const resolvedLinkTypes = resolveLinkTypes(links);
|
|
@@ -161,7 +212,7 @@ function flattenAllStructStepsInRefs(csn, options, resolved, pathDelimiter) {
|
|
|
161
212
|
const fn = () => {
|
|
162
213
|
const scopedPath = [ ...parent.$path ];
|
|
163
214
|
|
|
164
|
-
parent.ref = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes
|
|
215
|
+
parent.ref = flattenStructStepsInRef(ref, scopedPath, links, scope, resolvedLinkTypes);
|
|
165
216
|
resolved.set(parent, { links, art, scope });
|
|
166
217
|
// Explicitly set implicit alias for things that are now flattened - but only in columns
|
|
167
218
|
// TODO: Can this be done elegantly during expand phase already?
|
|
@@ -178,7 +229,7 @@ function flattenAllStructStepsInRefs(csn, options, resolved, pathDelimiter) {
|
|
|
178
229
|
// adapt queries later
|
|
179
230
|
adaptRefs.push(fn);
|
|
180
231
|
},
|
|
181
|
-
});
|
|
232
|
+
}, [], iterateOptions);
|
|
182
233
|
|
|
183
234
|
adaptRefs.forEach(fn => fn());
|
|
184
235
|
|
|
@@ -207,88 +258,102 @@ function flattenAllStructStepsInRefs(csn, options, resolved, pathDelimiter) {
|
|
|
207
258
|
* @param {CSN.Options} options
|
|
208
259
|
* @param {string} pathDelimiter
|
|
209
260
|
* @param {Function} error
|
|
261
|
+
* @param {Object} iterateOptions
|
|
210
262
|
*/
|
|
211
|
-
function flattenElements(csn, options, pathDelimiter, error) {
|
|
263
|
+
function flattenElements(csn, options, pathDelimiter, error, iterateOptions = {}) {
|
|
212
264
|
const { isAssocOrComposition } = getUtils(csn);
|
|
213
265
|
const { flattenStructuredElement } = transformUtils.getTransformers(csn, options, pathDelimiter);
|
|
214
266
|
const { effectiveType } = csnRefs(csn);
|
|
215
|
-
|
|
267
|
+
const transformers = {
|
|
268
|
+
elements: flatten,
|
|
269
|
+
};
|
|
270
|
+
|
|
271
|
+
if (options.toOdata) // Odata needs to flatten the .params as if it was a .elements
|
|
272
|
+
transformers.params = flatten;
|
|
273
|
+
|
|
274
|
+
applyTransformations(csn, transformers, [], iterateOptions);
|
|
275
|
+
|
|
216
276
|
/**
|
|
217
|
-
* Flatten
|
|
277
|
+
* Flatten a given .elements or .params dictionary - keeping the order consistent.
|
|
218
278
|
*
|
|
219
|
-
* @param {
|
|
220
|
-
* @param {string}
|
|
279
|
+
* @param {object} parent The parent object having dict at prop - parent[prop] === dict
|
|
280
|
+
* @param {string} prop
|
|
281
|
+
* @param {object} dict
|
|
282
|
+
* @param {CSN.Path} path
|
|
221
283
|
*/
|
|
222
|
-
function
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
onPart.ref[0] = possibleFlatName;
|
|
272
|
-
}
|
|
284
|
+
function flatten(parent, prop, dict, path) {
|
|
285
|
+
if (!parent[prop].$orderedElements)
|
|
286
|
+
setProp(parent[prop], '$orderedElements', []);
|
|
287
|
+
Object.entries(dict).forEach(([ elementName, element ]) => {
|
|
288
|
+
if (element.elements) {
|
|
289
|
+
// Ignore the structured element, replace it by its flattened form
|
|
290
|
+
// TODO: use $ignore - _ is for links
|
|
291
|
+
element._ignore = true;
|
|
292
|
+
|
|
293
|
+
const branches = getBranches(element, elementName);
|
|
294
|
+
const flatElems = flattenStructuredElement(element, elementName, [], path.concat([ 'elements', elementName ]));
|
|
295
|
+
|
|
296
|
+
for (const flatElemName in flatElems) {
|
|
297
|
+
if (parent[prop][flatElemName])
|
|
298
|
+
// TODO: combine message ID with generated FK duplicate
|
|
299
|
+
// do the duplicate check in the consruct callback, requires to mark generated flat elements,
|
|
300
|
+
// check: Error location should be the existing element like @odata.foreignKey4
|
|
301
|
+
error(null, path.concat([ 'elements', elementName ]), `"${path[1]}.${elementName}": Flattened struct element name conflicts with existing element: "${flatElemName}"`);
|
|
302
|
+
|
|
303
|
+
const flatElement = flatElems[flatElemName];
|
|
304
|
+
|
|
305
|
+
// Check if we have a valid notNull chain
|
|
306
|
+
const branch = branches[flatElemName];
|
|
307
|
+
if (flatElement.notNull !== false && !branch.some(s => !s.notNull))
|
|
308
|
+
flatElement.notNull = true;
|
|
309
|
+
|
|
310
|
+
|
|
311
|
+
if (flatElement.type && isAssocOrComposition(flatElement.type) && flatElement.on) {
|
|
312
|
+
// Make refs resolvable by fixing the first ref step
|
|
313
|
+
for (const onPart of flatElement.on) {
|
|
314
|
+
if (onPart.ref) {
|
|
315
|
+
const firstRef = onPart.ref[0];
|
|
316
|
+
|
|
317
|
+
/*
|
|
318
|
+
when element is defined in the current name resolution scope, like
|
|
319
|
+
entity E {
|
|
320
|
+
key x: Integer;
|
|
321
|
+
s : {
|
|
322
|
+
y : Integer;
|
|
323
|
+
a3 : association to E on a3.x = y;
|
|
324
|
+
}
|
|
325
|
+
}
|
|
326
|
+
We need to replace y with s_y and a3 with s_a3 - we must take care to not escape our local scope
|
|
327
|
+
*/
|
|
328
|
+
const prefix = flatElement._flatElementNameWithDots.split('.').slice(0, -1).join(pathDelimiter);
|
|
329
|
+
const possibleFlatName = prefix + pathDelimiter + firstRef;
|
|
330
|
+
|
|
331
|
+
if (flatElems[possibleFlatName])
|
|
332
|
+
onPart.ref[0] = possibleFlatName;
|
|
273
333
|
}
|
|
274
334
|
}
|
|
275
|
-
elementsArray.push([ flatElemName, flatElement ]);
|
|
276
|
-
// Still add them - otherwise we might not detect collisions between generated elements.
|
|
277
|
-
parent.elements[flatElemName] = flatElement;
|
|
278
335
|
}
|
|
336
|
+
parent[prop].$orderedElements.push([ flatElemName, flatElement ]);
|
|
337
|
+
// Still add them - otherwise we might not detect collisions between generated elements.
|
|
338
|
+
parent[prop][flatElemName] = flatElement;
|
|
279
339
|
}
|
|
280
340
|
}
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
341
|
+
else {
|
|
342
|
+
parent[prop].$orderedElements.push([ elementName, element ]);
|
|
343
|
+
}
|
|
344
|
+
});
|
|
284
345
|
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
346
|
+
// $orderedElements is removed by reducing and assigning a new dictionary
|
|
347
|
+
parent[prop] = parent[prop].$orderedElements.reduce((elements, [ name, element ]) => {
|
|
348
|
+
// rewrite $path to match the flattened dictionary entry
|
|
349
|
+
// ([ 'definitions', artName ] remain constant
|
|
350
|
+
setProp(element, '$path', [ ...path, prop, name ]);
|
|
351
|
+
elements[name] = element;
|
|
352
|
+
return elements;
|
|
353
|
+
}, Object.create(null));
|
|
290
354
|
}
|
|
291
355
|
|
|
356
|
+
|
|
292
357
|
/**
|
|
293
358
|
* Get not just the leafs, but all the branches of a structured element
|
|
294
359
|
*
|
|
@@ -332,9 +397,395 @@ function flattenElements(csn, options, pathDelimiter, error) {
|
|
|
332
397
|
}
|
|
333
398
|
}
|
|
334
399
|
|
|
400
|
+
/**
|
|
401
|
+
* @param {CSN.Model} csn
|
|
402
|
+
* @param {CSN.Options} options
|
|
403
|
+
* @param {Function} error
|
|
404
|
+
* @param {string} pathDelimiter
|
|
405
|
+
* @param {boolean} flattenKeyRefs
|
|
406
|
+
* @param {object} iterateOptions
|
|
407
|
+
*/
|
|
408
|
+
function handleManagedAssociationsAndCreateForeignKeys(csn, options, error, pathDelimiter, flattenKeyRefs, iterateOptions = {}) {
|
|
409
|
+
const { isManagedAssociation, inspectRef, isStructured } = getUtils(csn);
|
|
410
|
+
const { flattenStructStepsInRef, flattenStructuredElement } = transformUtils.getTransformers(csn, options, pathDelimiter);
|
|
411
|
+
if (flattenKeyRefs) {
|
|
412
|
+
applyTransformations(csn, {
|
|
413
|
+
elements: (parent, prop, elements, path) => {
|
|
414
|
+
Object.entries(elements).forEach(([ elementName, element ]) => {
|
|
415
|
+
if (isManagedAssociation(element)) {
|
|
416
|
+
// replace foreign keys that are managed associations by their respective foreign keys
|
|
417
|
+
flattenFKs(element, elementName, [ ...path, 'elements', elementName ]);
|
|
418
|
+
}
|
|
419
|
+
});
|
|
420
|
+
},
|
|
421
|
+
}, [], {
|
|
422
|
+
skipIgnore: false,
|
|
423
|
+
allowArtifact: artifact => (artifact.kind === 'entity' || artifact.kind === 'type'),
|
|
424
|
+
skipDict: { actions: true },
|
|
425
|
+
});
|
|
426
|
+
}
|
|
427
|
+
createForeignKeyElements();
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* Flattens all foreign keys
|
|
431
|
+
*
|
|
432
|
+
* Structures will be resolved to individual elements with scalar types
|
|
433
|
+
*
|
|
434
|
+
* Associations will be replaced by their respective foreign keys
|
|
435
|
+
*
|
|
436
|
+
* If a structure contains an assoc, this will also be resolved and vice versa
|
|
437
|
+
*
|
|
438
|
+
* @param {*} assoc
|
|
439
|
+
* @param {*} assocName
|
|
440
|
+
* @param {*} path
|
|
441
|
+
*/
|
|
442
|
+
function flattenFKs(assoc, assocName, path) {
|
|
443
|
+
let finished = false;
|
|
444
|
+
while (!finished) {
|
|
445
|
+
const newKeys = [];
|
|
446
|
+
finished = processKeys(newKeys);
|
|
447
|
+
assoc.keys = newKeys;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// @ts-ignore
|
|
451
|
+
/**
|
|
452
|
+
* Walk over the keys and replace structures by their leafs, managed associations by their foreign keys and keep scalar values as-is.
|
|
453
|
+
*
|
|
454
|
+
* @param {object[]} collector New keys array to collect the flattened stuff in
|
|
455
|
+
* @returns {boolean} True if all keys are scalar - false if there are things that still need to be processed.
|
|
456
|
+
*/
|
|
457
|
+
function processKeys(collector) {
|
|
458
|
+
const inferredAlias = '$inferredAlias';
|
|
459
|
+
|
|
460
|
+
let done = true;
|
|
461
|
+
for (let i = 0; i < assoc.keys.length; i++) {
|
|
462
|
+
const pathToKey = path.concat([ 'keys', i ]);
|
|
463
|
+
const { art } = inspectRef(pathToKey);
|
|
464
|
+
const { ref } = assoc.keys[i];
|
|
465
|
+
if (isStructured(art)) {
|
|
466
|
+
done = false;
|
|
467
|
+
// Mark this element to filter it later - not needed after expansion
|
|
468
|
+
setProp(assoc.keys[i], '$toDelete', true);
|
|
469
|
+
const flat = flattenStructuredElement(art, ref[ref.length - 1], [], pathToKey);
|
|
470
|
+
Object.keys(flat).forEach((flatElemName) => {
|
|
471
|
+
const key = assoc.keys[i];
|
|
472
|
+
const clone = cloneCsn(assoc.keys[i], options);
|
|
473
|
+
if (clone.as) {
|
|
474
|
+
const lastRef = clone.ref[clone.ref.length - 1];
|
|
475
|
+
// Cut off the last ref part from the beginning of the flat name
|
|
476
|
+
const flatBaseName = flatElemName.slice(lastRef.length);
|
|
477
|
+
// Join it to the existing table alias
|
|
478
|
+
clone.as += flatBaseName;
|
|
479
|
+
// do not loose the $ref for nested keys
|
|
480
|
+
if (key.$ref) {
|
|
481
|
+
let aliasedLeaf = key.$ref[key.$ref.length - 1];
|
|
482
|
+
aliasedLeaf += flatBaseName;
|
|
483
|
+
setProp(clone, '$ref', key.$ref.slice(0, key.$ref.length - 1).concat(aliasedLeaf));
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
if (clone.ref) {
|
|
487
|
+
clone.ref[clone.ref.length - 1] = flatElemName;
|
|
488
|
+
// Now we need to properly flatten the whole ref
|
|
489
|
+
clone.ref = flattenStructStepsInRef(clone.ref, pathToKey);
|
|
490
|
+
}
|
|
491
|
+
if (!clone.as) {
|
|
492
|
+
clone.as = flatElemName;
|
|
493
|
+
// TODO: can we use $inferred? Does it have other weird side-effects?
|
|
494
|
+
setProp(clone, inferredAlias, true);
|
|
495
|
+
}
|
|
496
|
+
// Directly work on csn.definitions - this way the changes take effect in csnRefs/inspectRef immediately
|
|
497
|
+
// Add the newly generated foreign keys to the end - they will be picked up later on
|
|
498
|
+
// Recursive solutions run into call stack issues
|
|
499
|
+
collector.push(clone);
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
else if (art.target) {
|
|
503
|
+
done = false;
|
|
504
|
+
// Mark this element to filter it later - not needed after expansion
|
|
505
|
+
setProp(assoc.keys[i], '$toDelete', true);
|
|
506
|
+
// Directly work on csn.definitions - this way the changes take effect in csnRefs/inspectRef immediately
|
|
507
|
+
// Add the newly generated foreign keys to the end - they will be picked up later on
|
|
508
|
+
// Recursive solutions run into call stack issues
|
|
509
|
+
art.keys.forEach(key => collector.push(cloneAndExtendRef(key, assoc.keys[i], ref)));
|
|
510
|
+
}
|
|
511
|
+
else if (assoc.keys[i].ref && !assoc.keys[i].as) {
|
|
512
|
+
setProp(assoc.keys[i], inferredAlias, true);
|
|
513
|
+
if (!(options.toOdata && assoc.keys[i].ref.length === 1))
|
|
514
|
+
// In OData backend there are no aliases assigned when the same as the ref
|
|
515
|
+
// TODO: remove the if after the new flattening in OData has been compleated
|
|
516
|
+
assoc.keys[i].as = assoc.keys[i].ref[assoc.keys[i].ref.length - 1];
|
|
517
|
+
collector.push(assoc.keys[i]);
|
|
518
|
+
}
|
|
519
|
+
else {
|
|
520
|
+
collector.push(assoc.keys[i]);
|
|
521
|
+
}
|
|
522
|
+
}
|
|
523
|
+
return done;
|
|
524
|
+
}
|
|
525
|
+
assoc.keys = assoc.keys.filter(o => !o.$toDelete);
|
|
526
|
+
|
|
527
|
+
/**
|
|
528
|
+
* Clone base and extend the .ref and .as of the clone with the .ref and .as of ref.
|
|
529
|
+
*
|
|
530
|
+
* @param {object} key A foreign key entry (of a managed assoc as a fk of another assoc)
|
|
531
|
+
* @param {object} base The fk-ref that has key as a fk
|
|
532
|
+
* @param {Array} ref
|
|
533
|
+
* @returns {object} The clone of base
|
|
534
|
+
*/
|
|
535
|
+
function cloneAndExtendRef(key, base, ref) {
|
|
536
|
+
const clone = cloneCsn(base, options);
|
|
537
|
+
if (key.ref) {
|
|
538
|
+
// We build a ref that contains the aliased fk - that element will be created later on, so this ref is not resolvable yet
|
|
539
|
+
// Therefore we keep it as $ref - ref is the non-aliased, resolvable "clone"
|
|
540
|
+
// Later on, after we know that these foreign key elements are created, we replace ref with this $ref
|
|
541
|
+
let $ref;
|
|
542
|
+
if (base.$ref) {
|
|
543
|
+
// if a base $ref is provided, use it to correctly resolve association chains
|
|
544
|
+
const refChain = [ base.$ref[base.$ref.length - 1] ].concat(key.as || key.ref);
|
|
545
|
+
$ref = base.$ref.slice(0, base.$ref.length - 1).concat(refChain);
|
|
546
|
+
}
|
|
547
|
+
else {
|
|
548
|
+
$ref = base.ref.concat(key.as || key.ref); // Keep along the aliases
|
|
549
|
+
}
|
|
550
|
+
setProp(clone, '$ref', $ref);
|
|
551
|
+
clone.ref = clone.ref.concat(key.ref);
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (!clone.as && clone.ref && clone.ref.length > 0) {
|
|
555
|
+
clone.as = ref[ref.length - 1] + pathDelimiter + (key.as || key.ref.join(pathDelimiter));
|
|
556
|
+
// TODO: can we use $inferred? Does it have other weird side-effects?
|
|
557
|
+
setProp(clone, '$inferredAlias', true);
|
|
558
|
+
}
|
|
559
|
+
else {
|
|
560
|
+
clone.as += pathDelimiter + (key.as || key.ref.join(pathDelimiter));
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return clone;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
/**
|
|
568
|
+
* Create the foreign key elements in all .elements things
|
|
569
|
+
*/
|
|
570
|
+
function createForeignKeyElements() {
|
|
571
|
+
const transformers = {
|
|
572
|
+
elements: createFks,
|
|
573
|
+
};
|
|
574
|
+
if (options.toOdata)
|
|
575
|
+
transformers.params = createFks;
|
|
576
|
+
|
|
577
|
+
// Do not respect _ignore flag for toOdata and toRename (we may have facades for existing HANA CDS
|
|
578
|
+
// artifacts that shall still be maintained from outside, filter them away in the rename script)
|
|
579
|
+
iterateOptions.skipIgnore = !(options.toOdata || options.toRename);
|
|
580
|
+
applyTransformations(csn, transformers, [], iterateOptions);
|
|
581
|
+
|
|
582
|
+
/**
|
|
583
|
+
* Process a given .elements or .params dictionary and create foreign key elements
|
|
584
|
+
*
|
|
585
|
+
* @param {object} parent The thing HAVING params or elements
|
|
586
|
+
* @param {string} prop
|
|
587
|
+
* @param {object} dict The params or elements thing
|
|
588
|
+
* @param {CSN.Path} path
|
|
589
|
+
*/
|
|
590
|
+
function createFks(parent, prop, dict, path) {
|
|
591
|
+
const orderedElements = [];
|
|
592
|
+
Object.entries(dict).forEach(([ elementName, element ]) => {
|
|
593
|
+
orderedElements.push([ elementName, element ]);
|
|
594
|
+
const eltPath = path.concat(prop, elementName);
|
|
595
|
+
const fks = createForeignKeysInternal(eltPath, element, elementName, csn, options, pathDelimiter);
|
|
596
|
+
|
|
597
|
+
// finalize the generated foreign keys
|
|
598
|
+
const refCount = fks.reduce((acc, fk) => {
|
|
599
|
+
// count duplicates
|
|
600
|
+
if (acc[fk[0]])
|
|
601
|
+
acc[fk[0]]++;
|
|
602
|
+
else
|
|
603
|
+
acc[fk[0]] = 1;
|
|
604
|
+
|
|
605
|
+
// check for name clash with existing elements
|
|
606
|
+
if ((parent[prop][fk[0]]) &&
|
|
607
|
+
((options.toOdata && isDeepEqual(element, parent[prop][fk[0]], true)) ||
|
|
608
|
+
!options.toOdata)) {
|
|
609
|
+
// error location is the colliding element
|
|
610
|
+
error(null, eltPath, { name: fk[0], art: elementName },
|
|
611
|
+
'Generated foreign key element $(NAME) for association $(ART) conflicts with existing element');
|
|
612
|
+
}
|
|
613
|
+
// attach a proper $path
|
|
614
|
+
setProp(element, '$path', eltPath);
|
|
615
|
+
return acc;
|
|
616
|
+
}, Object.create(null));
|
|
617
|
+
|
|
618
|
+
// check for duplicate foreign keys
|
|
619
|
+
Object.entries(refCount).forEach(([ name, occ ]) => {
|
|
620
|
+
if (occ > 1) {
|
|
621
|
+
error(null, eltPath, { name },
|
|
622
|
+
'Duplicate definition of foreign key element $(NAME)');
|
|
623
|
+
}
|
|
624
|
+
});
|
|
625
|
+
if (element.keys) {
|
|
626
|
+
element.keys.forEach((key, i) => {
|
|
627
|
+
// Assumption: If all key refs have been flattened, there is a
|
|
628
|
+
// 1:1 match to the corresponding foreign key element. Order is the
|
|
629
|
+
// same, so an index access should work
|
|
630
|
+
if (flattenKeyRefs) {
|
|
631
|
+
key.$generatedFieldName = fks[i][0];
|
|
632
|
+
key.ref = [ (key.$ref || key.ref).join(pathDelimiter) ];
|
|
633
|
+
delete key.$ref;
|
|
634
|
+
// TODO: remove the if after the new flattening in OData has been completed
|
|
635
|
+
if (options.toOdata && key.as && key.as === key.ref[0])
|
|
636
|
+
delete key.as;
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
// OData specific:
|
|
640
|
+
// Not Null sets min cardinality to 1
|
|
641
|
+
if (options.toOdata && element.notNull) {
|
|
642
|
+
if (element.cardinality === undefined)
|
|
643
|
+
element.cardinality = {};
|
|
644
|
+
// min=0 is falsy => check for undefined
|
|
645
|
+
if (element.cardinality.min === undefined)
|
|
646
|
+
element.cardinality.min = 1;
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
orderedElements.push(...fks);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
parent[prop] = orderedElements.reduce((elementsAccumulator, [ name, element ]) => {
|
|
653
|
+
elementsAccumulator[name] = element;
|
|
654
|
+
return elementsAccumulator;
|
|
655
|
+
}, Object.create(null));
|
|
656
|
+
}
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
/**
|
|
661
|
+
* This is the internal version of the foreign key procedure.
|
|
662
|
+
*
|
|
663
|
+
* If element is not a managed association, an empty array is returned
|
|
664
|
+
*
|
|
665
|
+
* @param {Array|object} path CSN path pointing to element or the result of a previous call to inspectRef
|
|
666
|
+
* @param {CSN.Element} element
|
|
667
|
+
* @param {string} prefix Element name
|
|
668
|
+
* @param {CSN.Model} csn
|
|
669
|
+
* @param {object} options
|
|
670
|
+
* @param {string} pathDelimiter
|
|
671
|
+
* @param {number} lvl
|
|
672
|
+
* @returns {Array[]} First element of every sub-array is the foreign key name, second is the foreign key definition
|
|
673
|
+
*/
|
|
674
|
+
function createForeignKeysInternal(path, element, prefix, csn, options, pathDelimiter, lvl = 0) {
|
|
675
|
+
const {
|
|
676
|
+
effectiveType,
|
|
677
|
+
inspectRef,
|
|
678
|
+
} = getUtils(csn);
|
|
679
|
+
|
|
680
|
+
|
|
681
|
+
const isInspectRefResult = !Array.isArray(path);
|
|
682
|
+
|
|
683
|
+
let fks = [];
|
|
684
|
+
if (!element)
|
|
685
|
+
return fks;
|
|
686
|
+
|
|
687
|
+
let finalElement = element;
|
|
688
|
+
// TODO: effectiveType's return value is 'path' for the next inspectRef
|
|
689
|
+
if (element.type && !isBuiltinType(element.type)) {
|
|
690
|
+
const tmpElt = effectiveType(element);
|
|
691
|
+
// effective type resolves to structs and enums only but not scalars
|
|
692
|
+
if (Object.keys(tmpElt).length) {
|
|
693
|
+
finalElement = tmpElt;
|
|
694
|
+
}
|
|
695
|
+
else {
|
|
696
|
+
// unwind a derived type chain to a scalar type
|
|
697
|
+
while (finalElement.type && !isBuiltinType(finalElement.type))
|
|
698
|
+
finalElement = csn.definitions[finalElement.type];
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
if (finalElement.target && !finalElement.on) {
|
|
703
|
+
const hasKeys = !!finalElement.keys;
|
|
704
|
+
if (!hasKeys) {
|
|
705
|
+
const target = csn.definitions[finalElement.target];
|
|
706
|
+
|
|
707
|
+
setProp(finalElement, 'keys', [ ] );
|
|
708
|
+
if (target && target.elements) {
|
|
709
|
+
finalElement.keys = Object.entries(target.elements).filter(([ _n, e ]) => e.key)
|
|
710
|
+
. map(([ n, _e ]) => ({ ref: [ n ], as: n }));
|
|
711
|
+
}
|
|
712
|
+
}
|
|
713
|
+
// TODO: has managed assoc keys?
|
|
714
|
+
finalElement.keys.forEach((key, keyIndex) => {
|
|
715
|
+
const continuePath = isInspectRefResult ? [ path, 'keys', keyIndex ] : [ ...path, 'keys', keyIndex ];
|
|
716
|
+
const alias = key.as || implicitAs(key.ref);
|
|
717
|
+
const result = inspectRef(continuePath);
|
|
718
|
+
fks = fks.concat(createForeignKeysInternal(result, result.art, alias, csn, options, pathDelimiter, lvl + 1));
|
|
719
|
+
});
|
|
720
|
+
if (!hasKeys)
|
|
721
|
+
delete finalElement.keys;
|
|
722
|
+
}
|
|
723
|
+
// return if the toplevel element is not a managed association
|
|
724
|
+
else if (lvl === 0) {
|
|
725
|
+
return fks;
|
|
726
|
+
}
|
|
727
|
+
// we have reached a leaf element, create a foreign key
|
|
728
|
+
else if (finalElement && isBuiltinType(finalElement.type)) {
|
|
729
|
+
const newFk = Object.create(null);
|
|
730
|
+
for (const prop of [ 'type', 'length', 'scale', 'precision', 'srid', 'default', '@odata.Type' ]) {
|
|
731
|
+
// copy props from original element to preserve derived types!
|
|
732
|
+
if (element[prop] !== undefined)
|
|
733
|
+
newFk[prop] = element[prop];
|
|
734
|
+
}
|
|
735
|
+
return [ [ prefix, newFk ] ];
|
|
736
|
+
}
|
|
737
|
+
else if (finalElement.elements) {
|
|
738
|
+
Object.entries(finalElement.elements).forEach(([ elemName, elem ]) => {
|
|
739
|
+
// Skip already produced foreign keys
|
|
740
|
+
if (!elem['@odata.foreignKey4']) {
|
|
741
|
+
const continuePath = isInspectRefResult ? [ path, 'elements', elemName ] : [ ...path, 'elements', elemName ];
|
|
742
|
+
fks = fks.concat(createForeignKeysInternal(continuePath, elem, elemName, csn, options, pathDelimiter, lvl + 1));
|
|
743
|
+
}
|
|
744
|
+
});
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
fks.forEach((fk) => {
|
|
748
|
+
// prepend current prefix
|
|
749
|
+
fk[0] = `${prefix}${pathDelimiter}${fk[0]}`;
|
|
750
|
+
// if this is the entry association, decorate the final foreign keys with the association props
|
|
751
|
+
if (lvl === 0) {
|
|
752
|
+
fk[1]['@odata.foreignKey4'] = prefix;
|
|
753
|
+
if (!options.forHana)
|
|
754
|
+
copyAnnotations(element, fk[1], true);
|
|
755
|
+
|
|
756
|
+
// propagate not null to final foreign key
|
|
757
|
+
for (const prop of [ 'notNull', 'key' ]) {
|
|
758
|
+
if (element[prop] !== undefined)
|
|
759
|
+
fk[1][prop] = element[prop];
|
|
760
|
+
}
|
|
761
|
+
if (element.$location)
|
|
762
|
+
setProp(fk[1], '$location', element.$location);
|
|
763
|
+
}
|
|
764
|
+
});
|
|
765
|
+
return fks;
|
|
766
|
+
}
|
|
767
|
+
|
|
768
|
+
/**
|
|
769
|
+
* This is the public createForeignKeys function that has no side effects
|
|
770
|
+
*
|
|
771
|
+
* @param {CSN.Path} path Path to the managed association
|
|
772
|
+
* @param {CSN.Model} csn
|
|
773
|
+
* @param {string} pathDelimiter
|
|
774
|
+
* @returns {Array}
|
|
775
|
+
*/
|
|
776
|
+
function createForeignKeys(path, csn, pathDelimiter = '_') {
|
|
777
|
+
if (!path || !Array.isArray(path))
|
|
778
|
+
throw Error('path must be a CSN path array');
|
|
779
|
+
if (!csn || typeof csn !== 'object')
|
|
780
|
+
throw Error('csn not provided');
|
|
781
|
+
return createForeignKeysInternal(path, walkCsnPath(csn, path), path[path.length - 1], csn, {}, pathDelimiter);
|
|
782
|
+
}
|
|
783
|
+
|
|
335
784
|
module.exports = {
|
|
336
785
|
resolveTypeReferences,
|
|
337
786
|
flattenAllStructStepsInRefs,
|
|
338
787
|
flattenElements,
|
|
339
788
|
removeLeadingSelf,
|
|
789
|
+
handleManagedAssociationsAndCreateForeignKeys,
|
|
790
|
+
createForeignKeys,
|
|
340
791
|
};
|