@sap/cds-compiler 2.11.4 → 2.13.8

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 (133) hide show
  1. package/CHANGELOG.md +159 -1
  2. package/bin/cds_update_identifiers.js +7 -7
  3. package/bin/cdsc.js +22 -23
  4. package/bin/cdsse.js +2 -2
  5. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  6. package/doc/CHANGELOG_BETA.md +25 -6
  7. package/doc/CHANGELOG_DEPRECATED.md +22 -6
  8. package/doc/NameResolution.md +21 -16
  9. package/lib/api/main.js +30 -63
  10. package/lib/api/options.js +5 -5
  11. package/lib/api/validate.js +0 -5
  12. package/lib/backends.js +15 -23
  13. package/lib/base/dictionaries.js +0 -8
  14. package/lib/base/error.js +26 -0
  15. package/lib/base/keywords.js +7 -17
  16. package/lib/base/location.js +9 -4
  17. package/lib/base/message-registry.js +52 -2
  18. package/lib/base/messages.js +16 -26
  19. package/lib/base/model.js +2 -62
  20. package/lib/base/optionProcessorHelper.js +246 -183
  21. package/lib/checks/.eslintrc.json +2 -0
  22. package/lib/checks/actionsFunctions.js +2 -1
  23. package/lib/checks/annotationsOData.js +1 -1
  24. package/lib/checks/cdsPersistence.js +2 -1
  25. package/lib/checks/enricher.js +17 -1
  26. package/lib/checks/foreignKeys.js +4 -4
  27. package/lib/checks/invalidTarget.js +3 -1
  28. package/lib/checks/managedInType.js +4 -4
  29. package/lib/checks/managedWithoutKeys.js +3 -1
  30. package/lib/checks/queryNoDbArtifacts.js +1 -3
  31. package/lib/checks/selectItems.js +4 -4
  32. package/lib/checks/sql-snippets.js +94 -0
  33. package/lib/checks/types.js +1 -1
  34. package/lib/checks/validator.js +12 -7
  35. package/lib/compiler/assert-consistency.js +10 -6
  36. package/lib/compiler/base.js +0 -1
  37. package/lib/compiler/builtins.js +8 -6
  38. package/lib/compiler/checks.js +46 -12
  39. package/lib/compiler/cycle-detector.js +1 -1
  40. package/lib/compiler/define.js +1103 -0
  41. package/lib/compiler/extend.js +983 -0
  42. package/lib/compiler/finalize-parse-cdl.js +231 -0
  43. package/lib/compiler/index.js +33 -14
  44. package/lib/compiler/kick-start.js +190 -0
  45. package/lib/compiler/moduleLayers.js +4 -4
  46. package/lib/compiler/populate.js +1226 -0
  47. package/lib/compiler/propagator.js +113 -47
  48. package/lib/compiler/resolve.js +1433 -0
  49. package/lib/compiler/shared.js +76 -38
  50. package/lib/compiler/tweak-assocs.js +529 -0
  51. package/lib/compiler/utils.js +204 -33
  52. package/lib/edm/.eslintrc.json +5 -0
  53. package/lib/edm/annotations/genericTranslation.js +38 -25
  54. package/lib/edm/annotations/preprocessAnnotations.js +3 -3
  55. package/lib/edm/csn2edm.js +10 -9
  56. package/lib/edm/edm.js +19 -20
  57. package/lib/edm/edmPreprocessor.js +166 -95
  58. package/lib/edm/edmUtils.js +127 -34
  59. package/lib/gen/Dictionary.json +92 -43
  60. package/lib/gen/language.checksum +1 -1
  61. package/lib/gen/language.interp +11 -1
  62. package/lib/gen/language.tokens +86 -82
  63. package/lib/gen/languageLexer.interp +18 -1
  64. package/lib/gen/languageLexer.js +925 -847
  65. package/lib/gen/languageLexer.tokens +78 -74
  66. package/lib/gen/languageParser.js +5434 -4298
  67. package/lib/json/from-csn.js +59 -17
  68. package/lib/json/to-csn.js +143 -71
  69. package/lib/language/antlrParser.js +3 -3
  70. package/lib/language/docCommentParser.js +3 -3
  71. package/lib/language/genericAntlrParser.js +144 -54
  72. package/lib/language/language.g4 +424 -203
  73. package/lib/language/multiLineStringParser.js +536 -0
  74. package/lib/main.d.ts +472 -61
  75. package/lib/main.js +38 -11
  76. package/lib/model/api.js +3 -1
  77. package/lib/model/csnRefs.js +321 -204
  78. package/lib/model/csnUtils.js +224 -263
  79. package/lib/model/enrichCsn.js +97 -40
  80. package/lib/model/revealInternalProperties.js +27 -6
  81. package/lib/model/sortViews.js +2 -1
  82. package/lib/modelCompare/compare.js +17 -12
  83. package/lib/optionProcessor.js +7 -6
  84. package/lib/render/DuplicateChecker.js +1 -1
  85. package/lib/render/manageConstraints.js +36 -33
  86. package/lib/render/toCdl.js +174 -275
  87. package/lib/render/toHdbcds.js +201 -115
  88. package/lib/render/toRename.js +7 -10
  89. package/lib/render/toSql.js +149 -75
  90. package/lib/render/utils/common.js +22 -8
  91. package/lib/render/utils/sql.js +10 -7
  92. package/lib/render/utils/stringEscapes.js +111 -0
  93. package/lib/sql-identifier.js +1 -1
  94. package/lib/transform/.eslintrc.json +5 -0
  95. package/lib/transform/braceExpression.js +4 -2
  96. package/lib/transform/db/.eslintrc.json +2 -0
  97. package/lib/transform/db/applyTransformations.js +35 -12
  98. package/lib/transform/db/assertUnique.js +1 -1
  99. package/lib/transform/db/associations.js +187 -0
  100. package/lib/transform/db/cdsPersistence.js +150 -0
  101. package/lib/transform/db/constraints.js +61 -56
  102. package/lib/transform/db/expansion.js +50 -29
  103. package/lib/transform/db/flattening.js +552 -105
  104. package/lib/transform/db/groupByOrderBy.js +3 -1
  105. package/lib/transform/db/temporal.js +236 -0
  106. package/lib/transform/db/transformExists.js +94 -28
  107. package/lib/transform/db/views.js +5 -4
  108. package/lib/transform/draft/.eslintrc.json +38 -0
  109. package/lib/transform/{db/draft.js → draft/db.js} +9 -7
  110. package/lib/transform/draft/odata.js +227 -0
  111. package/lib/transform/forHanaNew.js +94 -801
  112. package/lib/transform/forOdataNew.js +22 -175
  113. package/lib/transform/localized.js +36 -32
  114. package/lib/transform/odata/generateForeignKeyElements.js +3 -3
  115. package/lib/transform/odata/referenceFlattener.js +95 -89
  116. package/lib/transform/odata/structureFlattener.js +1 -1
  117. package/lib/transform/odata/toFinalBaseType.js +86 -12
  118. package/lib/transform/odata/typesExposure.js +5 -5
  119. package/lib/transform/odata/utils.js +2 -2
  120. package/lib/transform/transformUtilsNew.js +47 -33
  121. package/lib/transform/translateAssocsToJoins.js +10 -27
  122. package/lib/transform/universalCsn/.eslintrc.json +36 -0
  123. package/lib/transform/universalCsn/coreComputed.js +170 -0
  124. package/lib/transform/universalCsn/universalCsnEnricher.js +715 -0
  125. package/lib/transform/universalCsn/utils.js +63 -0
  126. package/lib/utils/file.js +2 -1
  127. package/lib/utils/objectUtils.js +30 -0
  128. package/lib/utils/timetrace.js +8 -2
  129. package/package.json +1 -1
  130. package/share/messages/README.md +26 -0
  131. package/lib/compiler/definer.js +0 -2340
  132. package/lib/compiler/resolver.js +0 -2988
  133. package/lib/transform/universalCsnEnricher.js +0 -67
@@ -1,8 +1,10 @@
1
1
  'use strict';
2
2
 
3
3
  const {
4
- forEachDefinition, getUtils,
5
- applyTransformations, forAllElements, isBuiltinType,
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
- 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
- }
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
- type: (parent, prop, type) => {
79
- if (!isBuiltinType(type)) {
80
- const directLocalized = parent.localized || false;
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
- parent.type = 'cds.LargeString';
110
- delete parent.items;
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
- if (artifact.kind === 'action' || artifact.kind === 'function' || artifact.kind === 'event') {
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
- } ], true, { skipDict: { actions: true } });
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,89 +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
- forEachDefinition(csn, flattenStructuredElements);
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 structures
277
+ * Flatten a given .elements or .params dictionary - keeping the order consistent.
218
278
  *
219
- * @param {CSN.Artifact} art Artifact
220
- * @param {string} artName Artifact Name
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 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 (let i = 0; i < flatElement.on.length; i++) {
253
- const onPart = flatElement.on[i];
254
- if (onPart.ref) {
255
- const firstRef = flatElement.on[i].ref[0];
256
-
257
- /*
258
- when element is defined in the current name resolution scope, like
259
- entity E {
260
- key x: Integer;
261
- s : {
262
- y : Integer;
263
- a3 : association to E on a3.x = y;
264
- }
265
- }
266
- We need to replace y with s_y and a3 with s_a3 - we must take care to not escape our local scope
267
- */
268
- const prefix = flatElement._flatElementNameWithDots.split('.').slice(0, -1).join(pathDelimiter);
269
- const possibleFlatName = prefix + pathDelimiter + firstRef;
270
-
271
- if (flatElems[possibleFlatName])
272
- flatElement.on[i].ref[0] = possibleFlatName;
273
- }
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;
274
333
  }
275
334
  }
276
- elementsArray.push([ flatElemName, flatElement ]);
277
- // Still add them - otherwise we might not detect collisions between generated elements.
278
- parent.elements[flatElemName] = flatElement;
279
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;
280
339
  }
281
340
  }
282
- // Don't fake consistency of the model by adding empty elements {}
283
- if (elementsArray.length === 0)
284
- return;
341
+ else {
342
+ parent[prop].$orderedElements.push([ elementName, element ]);
343
+ }
344
+ });
285
345
 
286
- parent.elements = elementsArray.reduce((previous, [ name, element ]) => {
287
- previous[name] = element;
288
- return previous;
289
- }, Object.create(null));
290
- }, true);
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));
291
354
  }
292
355
 
356
+
293
357
  /**
294
358
  * Get not just the leafs, but all the branches of a structured element
295
359
  *
@@ -305,11 +369,11 @@ function flattenElements(csn, options, pathDelimiter, error) {
305
369
  /**
306
370
  * Walk the element chain
307
371
  *
308
- * @param {CSN.Element} e
372
+ * @param {object} e
309
373
  * @param {string} name
310
374
  */
311
375
  function walkElements(e, name) {
312
- if (isBuiltinType(e)) {
376
+ if (isBuiltinType(e.type)) {
313
377
  branches[subbranchNames.concat(name).join(pathDelimiter)] = subbranchElements.concat(e);
314
378
  }
315
379
  else {
@@ -333,9 +397,392 @@ function flattenElements(csn, options, pathDelimiter, error) {
333
397
  }
334
398
  }
335
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
+ applyTransformations(csn, transformers, [], Object.assign({ skipIgnore: false }, iterateOptions));
578
+
579
+ /**
580
+ * Process a given .elements or .params dictionary and create foreign key elements
581
+ *
582
+ * @param {object} parent The thing HAVING params or elements
583
+ * @param {string} prop
584
+ * @param {object} dict The params or elements thing
585
+ * @param {CSN.Path} path
586
+ */
587
+ function createFks(parent, prop, dict, path) {
588
+ const orderedElements = [];
589
+ Object.entries(dict).forEach(([ elementName, element ]) => {
590
+ orderedElements.push([ elementName, element ]);
591
+ const eltPath = path.concat(prop, elementName);
592
+ const fks = createForeignKeysInternal(eltPath, element, elementName, csn, options, pathDelimiter);
593
+
594
+ // finalize the generated foreign keys
595
+ const refCount = fks.reduce((acc, fk) => {
596
+ // count duplicates
597
+ if (acc[fk[0]])
598
+ acc[fk[0]]++;
599
+ else
600
+ acc[fk[0]] = 1;
601
+
602
+ // check for name clash with existing elements
603
+ if ((parent[prop][fk[0]]) &&
604
+ ((options.toOdata && isDeepEqual(element, parent[prop][fk[0]], true)) ||
605
+ !options.toOdata)) {
606
+ // error location is the colliding element
607
+ error(null, eltPath, { name: fk[0], art: elementName },
608
+ 'Generated foreign key element $(NAME) for association $(ART) conflicts with existing element');
609
+ }
610
+ // attach a proper $path
611
+ setProp(element, '$path', eltPath);
612
+ return acc;
613
+ }, Object.create(null));
614
+
615
+ // check for duplicate foreign keys
616
+ Object.entries(refCount).forEach(([ name, occ ]) => {
617
+ if (occ > 1) {
618
+ error(null, eltPath, { name },
619
+ 'Duplicate definition of foreign key element $(NAME)');
620
+ }
621
+ });
622
+ if (element.keys) {
623
+ element.keys.forEach((key, i) => {
624
+ // Assumption: If all key refs have been flattened, there is a
625
+ // 1:1 match to the corresponding foreign key element. Order is the
626
+ // same, so an index access should work
627
+ if (flattenKeyRefs) {
628
+ key.$generatedFieldName = fks[i][0];
629
+ key.ref = [ (key.$ref || key.ref).join(pathDelimiter) ];
630
+ delete key.$ref;
631
+ // TODO: remove the if after the new flattening in OData has been completed
632
+ if (options.toOdata && key.as && key.as === key.ref[0])
633
+ delete key.as;
634
+ }
635
+ });
636
+ // OData specific:
637
+ // Not Null sets min cardinality to 1
638
+ if (options.toOdata && element.notNull) {
639
+ if (element.cardinality === undefined)
640
+ element.cardinality = {};
641
+ // min=0 is falsy => check for undefined
642
+ if (element.cardinality.min === undefined)
643
+ element.cardinality.min = 1;
644
+ }
645
+ }
646
+ orderedElements.push(...fks);
647
+ });
648
+
649
+ parent[prop] = orderedElements.reduce((elementsAccumulator, [ name, element ]) => {
650
+ elementsAccumulator[name] = element;
651
+ return elementsAccumulator;
652
+ }, Object.create(null));
653
+ }
654
+ }
655
+ }
656
+
657
+ /**
658
+ * This is the internal version of the foreign key procedure.
659
+ *
660
+ * If element is not a managed association, an empty array is returned
661
+ *
662
+ * @param {Array|object} path CSN path pointing to element or the result of a previous call to inspectRef
663
+ * @param {CSN.Element} element
664
+ * @param {string} prefix Element name
665
+ * @param {CSN.Model} csn
666
+ * @param {object} options
667
+ * @param {string} pathDelimiter
668
+ * @param {number} lvl
669
+ * @returns {Array[]} First element of every sub-array is the foreign key name, second is the foreign key definition
670
+ */
671
+ function createForeignKeysInternal(path, element, prefix, csn, options, pathDelimiter, lvl = 0) {
672
+ const {
673
+ effectiveType,
674
+ inspectRef,
675
+ } = getUtils(csn);
676
+
677
+
678
+ const isInspectRefResult = !Array.isArray(path);
679
+
680
+ let fks = [];
681
+ if (!element)
682
+ return fks;
683
+
684
+ let finalElement = element;
685
+ // TODO: effectiveType's return value is 'path' for the next inspectRef
686
+ if (element.type && !isBuiltinType(element.type)) {
687
+ const tmpElt = effectiveType(element);
688
+ // effective type resolves to structs and enums only but not scalars
689
+ if (Object.keys(tmpElt).length) {
690
+ finalElement = tmpElt;
691
+ }
692
+ else {
693
+ // unwind a derived type chain to a scalar type
694
+ while (finalElement.type && !isBuiltinType(finalElement.type))
695
+ finalElement = csn.definitions[finalElement.type];
696
+ }
697
+ }
698
+
699
+ if (finalElement.target && !finalElement.on) {
700
+ const hasKeys = !!finalElement.keys;
701
+ if (!hasKeys) {
702
+ const target = csn.definitions[finalElement.target];
703
+
704
+ setProp(finalElement, 'keys', [ ] );
705
+ if (target && target.elements) {
706
+ finalElement.keys = Object.entries(target.elements).filter(([ _n, e ]) => e.key)
707
+ . map(([ n, _e ]) => ({ ref: [ n ], as: n }));
708
+ }
709
+ }
710
+ // TODO: has managed assoc keys?
711
+ finalElement.keys.forEach((key, keyIndex) => {
712
+ const continuePath = isInspectRefResult ? [ path, 'keys', keyIndex ] : [ ...path, 'keys', keyIndex ];
713
+ const alias = key.as || implicitAs(key.ref);
714
+ const result = inspectRef(continuePath);
715
+ fks = fks.concat(createForeignKeysInternal(result, result.art, alias, csn, options, pathDelimiter, lvl + 1));
716
+ });
717
+ if (!hasKeys)
718
+ delete finalElement.keys;
719
+ }
720
+ // return if the toplevel element is not a managed association
721
+ else if (lvl === 0) {
722
+ return fks;
723
+ }
724
+ // we have reached a leaf element, create a foreign key
725
+ else if (finalElement && isBuiltinType(finalElement.type)) {
726
+ const newFk = Object.create(null);
727
+ for (const prop of [ 'type', 'length', 'scale', 'precision', 'srid', 'default', '@odata.Type' ]) {
728
+ // copy props from original element to preserve derived types!
729
+ if (element[prop] !== undefined)
730
+ newFk[prop] = element[prop];
731
+ }
732
+ return [ [ prefix, newFk ] ];
733
+ }
734
+ else if (finalElement.elements) {
735
+ Object.entries(finalElement.elements).forEach(([ elemName, elem ]) => {
736
+ // Skip already produced foreign keys
737
+ if (!elem['@odata.foreignKey4']) {
738
+ const continuePath = isInspectRefResult ? [ path, 'elements', elemName ] : [ ...path, 'elements', elemName ];
739
+ fks = fks.concat(createForeignKeysInternal(continuePath, elem, elemName, csn, options, pathDelimiter, lvl + 1));
740
+ }
741
+ });
742
+ }
743
+
744
+ fks.forEach((fk) => {
745
+ // prepend current prefix
746
+ fk[0] = `${prefix}${pathDelimiter}${fk[0]}`;
747
+ // if this is the entry association, decorate the final foreign keys with the association props
748
+ if (lvl === 0) {
749
+ fk[1]['@odata.foreignKey4'] = prefix;
750
+ if (!options.forHana)
751
+ copyAnnotations(element, fk[1], true);
752
+
753
+ // propagate not null to final foreign key
754
+ for (const prop of [ 'notNull', 'key' ]) {
755
+ if (element[prop] !== undefined)
756
+ fk[1][prop] = element[prop];
757
+ }
758
+ if (element.$location)
759
+ setProp(fk[1], '$location', element.$location);
760
+ }
761
+ });
762
+ return fks;
763
+ }
764
+
765
+ /**
766
+ * This is the public createForeignKeys function that has no side effects
767
+ *
768
+ * @param {CSN.Path} path Path to the managed association
769
+ * @param {CSN.Model} csn
770
+ * @param {string} pathDelimiter
771
+ * @returns {Array}
772
+ */
773
+ function createForeignKeys(path, csn, pathDelimiter = '_') {
774
+ if (!path || !Array.isArray(path))
775
+ throw Error('path must be a CSN path array');
776
+ if (!csn || typeof csn !== 'object')
777
+ throw Error('csn not provided');
778
+ return createForeignKeysInternal(path, walkCsnPath(csn, path), path[path.length - 1], csn, {}, pathDelimiter);
779
+ }
780
+
336
781
  module.exports = {
337
782
  resolveTypeReferences,
338
783
  flattenAllStructStepsInRefs,
339
784
  flattenElements,
340
785
  removeLeadingSelf,
786
+ handleManagedAssociationsAndCreateForeignKeys,
787
+ createForeignKeys,
341
788
  };