@sap/cds-compiler 2.12.0 → 2.15.2

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