@sap/cds-compiler 3.6.2 → 3.8.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (89) hide show
  1. package/CHANGELOG.md +109 -1
  2. package/README.md +3 -0
  3. package/bin/cdsc.js +12 -5
  4. package/doc/CHANGELOG_ARCHIVE.md +6 -6
  5. package/doc/CHANGELOG_BETA.md +35 -2
  6. package/doc/CHANGELOG_DEPRECATED.md +2 -2
  7. package/doc/DeprecatedOptions_v2.md +1 -1
  8. package/doc/NameResolution.md +1 -1
  9. package/lib/api/main.js +63 -23
  10. package/lib/api/options.js +1 -0
  11. package/lib/api/validate.js +5 -0
  12. package/lib/base/dictionaries.js +15 -3
  13. package/lib/base/keywords.js +2 -0
  14. package/lib/base/message-registry.js +120 -34
  15. package/lib/base/messages.js +51 -27
  16. package/lib/base/model.js +4 -2
  17. package/lib/base/shuffle.js +2 -1
  18. package/lib/checks/arrayOfs.js +1 -1
  19. package/lib/checks/defaultValues.js +1 -1
  20. package/lib/checks/elements.js +29 -1
  21. package/lib/checks/{emptyOrOnlyVirtual.js → hasPersistedElements.js} +10 -6
  22. package/lib/checks/invalidTarget.js +1 -1
  23. package/lib/checks/nonexpandableStructured.js +1 -1
  24. package/lib/checks/onConditions.js +15 -9
  25. package/lib/checks/sql-snippets.js +2 -2
  26. package/lib/checks/types.js +5 -1
  27. package/lib/checks/validator.js +7 -3
  28. package/lib/compiler/assert-consistency.js +42 -26
  29. package/lib/compiler/base.js +50 -4
  30. package/lib/compiler/builtins.js +17 -8
  31. package/lib/compiler/checks.js +241 -246
  32. package/lib/compiler/define.js +113 -146
  33. package/lib/compiler/extend.js +889 -383
  34. package/lib/compiler/finalize-parse-cdl.js +5 -58
  35. package/lib/compiler/index.js +1 -1
  36. package/lib/compiler/kick-start.js +7 -8
  37. package/lib/compiler/populate.js +297 -293
  38. package/lib/compiler/propagator.js +27 -18
  39. package/lib/compiler/resolve.js +146 -463
  40. package/lib/compiler/shared.js +36 -79
  41. package/lib/compiler/tweak-assocs.js +30 -28
  42. package/lib/compiler/utils.js +31 -5
  43. package/lib/edm/annotations/genericTranslation.js +131 -59
  44. package/lib/edm/annotations/preprocessAnnotations.js +3 -0
  45. package/lib/edm/csn2edm.js +22 -5
  46. package/lib/edm/edm.js +6 -4
  47. package/lib/edm/edmAnnoPreprocessor.js +1 -0
  48. package/lib/edm/edmPreprocessor.js +42 -26
  49. package/lib/gen/Dictionary.json +38 -2
  50. package/lib/gen/language.checksum +1 -1
  51. package/lib/gen/language.interp +3 -1
  52. package/lib/gen/languageLexer.js +1 -1
  53. package/lib/gen/languageParser.js +4828 -4472
  54. package/lib/inspect/inspectPropagation.js +20 -34
  55. package/lib/json/from-csn.js +140 -44
  56. package/lib/json/to-csn.js +114 -122
  57. package/lib/language/errorStrategy.js +2 -0
  58. package/lib/language/genericAntlrParser.js +156 -36
  59. package/lib/language/language.g4 +100 -58
  60. package/lib/language/textUtils.js +13 -0
  61. package/lib/main.d.ts +43 -3
  62. package/lib/main.js +4 -2
  63. package/lib/model/csnRefs.js +15 -3
  64. package/lib/model/csnUtils.js +12 -74
  65. package/lib/model/revealInternalProperties.js +4 -2
  66. package/lib/modelCompare/compare.js +2 -1
  67. package/lib/optionProcessor.js +3 -0
  68. package/lib/render/manageConstraints.js +5 -2
  69. package/lib/render/toCdl.js +216 -104
  70. package/lib/render/toHdbcds.js +2 -9
  71. package/lib/render/toRename.js +14 -51
  72. package/lib/render/toSql.js +4 -3
  73. package/lib/render/utils/common.js +9 -5
  74. package/lib/transform/braceExpression.js +6 -0
  75. package/lib/transform/db/assertUnique.js +2 -1
  76. package/lib/transform/db/expansion.js +2 -0
  77. package/lib/transform/db/flattening.js +37 -36
  78. package/lib/transform/db/rewriteCalculatedElements.js +600 -0
  79. package/lib/transform/db/transformExists.js +4 -0
  80. package/lib/transform/db/views.js +40 -37
  81. package/lib/transform/forOdataNew.js +20 -15
  82. package/lib/transform/forRelationalDB.js +58 -41
  83. package/lib/transform/odata/typesExposure.js +50 -15
  84. package/lib/transform/parseExpr.js +16 -8
  85. package/lib/transform/transformUtilsNew.js +42 -14
  86. package/lib/transform/translateAssocsToJoins.js +60 -37
  87. package/lib/transform/universalCsn/coreComputed.js +15 -7
  88. package/lib/transform/universalCsn/universalCsnEnricher.js +4 -4
  89. package/package.json +2 -1
@@ -5,7 +5,7 @@ const oDataDictionary = require('../../gen/Dictionary.json');
5
5
  const preprocessAnnotations = require('./preprocessAnnotations.js');
6
6
  const { forEachDefinition } = require('../../model/csnUtils');
7
7
  const { forEach } = require('../../utils/objectUtils');
8
- const { isBetaEnabled } = require('../../base/model.js');
8
+ const { isBetaEnabled, setProp } = require('../../base/model.js');
9
9
 
10
10
  /*
11
11
  OASIS: https://github.com/oasis-tcs/odata-vocabularies/tree/master/vocabularies
@@ -147,10 +147,23 @@ const vocabularyDefinitions = {
147
147
  'inc': { Alias: 'Validation', Namespace: 'Org.OData.Validation.V1' },
148
148
  'int': { filename: 'Validation.xml' }
149
149
  },
150
+ /* unvalidated vocabularies below here:
151
+ A vocabulary is unvalidated if it doesn't have an int.filename property as this indicates that
152
+ the vocabulary is added to the validation dictionary
153
+ Example:
154
+ 'Org.Snafu.V1': {
155
+ 'ref': { Uri: 'https://snafu.org/snafu.xml' },
156
+ 'inc': { Alias: 'Snafu', Namespace: 'Org.Snafu.V1' },
157
+ },
158
+ */
150
159
  };
151
160
 
152
- const knownVocabularies = Object.keys(vocabularyDefinitions);
153
-
161
+ /* create inverted voc definitions list to allow addressing full qualified vocabularies
162
+ Object.entries(vocabularyDefinitions).forEach(([n, v]) => {
163
+ if(!vocabularyDefinitions[v.inc.Namespace])
164
+ vocabularyDefinitions[v.inc.Namespace] = vocabularyDefinitions[n];
165
+ });
166
+ */
154
167
  /**************************************************************************************************
155
168
  * csn2annotationEdm
156
169
  *
@@ -158,24 +171,16 @@ const knownVocabularies = Object.keys(vocabularyDefinitions);
158
171
  * v - array with two boolean entries, first is for v2, second is for v4
159
172
  * dictReplacement: for test purposes, replaces the standard oDataDictionary
160
173
  */
161
- function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefined, options=undefined, messageFunctions=undefined) {
174
+ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName,
175
+ Edm = undefined, options=undefined, messageFunctions=undefined, mergedVocDefs=vocabularyDefinitions) {
162
176
  // global variable where we store all the generated annotations
163
177
  const g_annosArray = [];
164
178
 
165
179
  const { message } = messageFunctions;
166
180
 
167
- const [ adhocDictionary, allKnownVocabularies ] = createAdhocDictionary();
168
-
169
- /*
170
- allKnownVocabularies is the union of knownVocabularies & adhoc $mySchemaName annotation definition
171
- this allows to identify namespace prefixes of arbitrary length.
181
+ const [ userDefinedTermDict, allKnownVocabularies ] = createUserDefinedTermDictionary();
172
182
 
173
- If in the future, the full Oasis vocabulary names shall be made usable for assignments
174
- eg. @com.sap.vocabularies.Common.v1.Label, all that needs to be done is to x-link the
175
- vocabulary dictionary whith the full ns as key:
176
- vocabularyDefinitions['@com.sap.vocabularies.Common.v1'] = vocabularyDefinitions['Common']
177
- */
178
- allKnownVocabularies.push(...knownVocabularies);
183
+ allKnownVocabularies.push(...Object.keys(mergedVocDefs));
179
184
  allKnownVocabularies.sort((a,b) => b.length-a.length);
180
185
  const whatsMyTermNamespace = function(anno) {
181
186
  return allKnownVocabularies.reduce((rc, ns) => !rc && anno && anno.startsWith('@' + ns + '.') ? ns : rc, undefined);
@@ -189,13 +194,21 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
189
194
 
190
195
  // we take note of which vocabularies are actually used in a service in order to avoid
191
196
  // producing useless references; reset everything to "unused"
192
- knownVocabularies.forEach(n => {
193
- vocabularyDefinitions[n].used = false;
194
- });
197
+ for(const n in mergedVocDefs) {
198
+ mergedVocDefs[n].used = false;
199
+ delete mergedVocDefs[n].$ignore;
200
+ }
195
201
 
196
202
  // These vocabularies are always added for the runtimes
197
- vocabularyDefinitions['Common'].used = true;
198
- vocabularyDefinitions['Core'].used = true;
203
+ mergedVocDefs['Common'].used = true;
204
+ mergedVocDefs['Core'].used = true;
205
+
206
+ const vocDef = mergedVocDefs[serviceName];
207
+ if(vocDef && vocDef.$optVocRef) {
208
+ setProp(vocDef, '$ignore', true);
209
+ message('odata-anno-vocref', null,
210
+ { name: serviceName, '#': 'service' } );
211
+ }
199
212
 
200
213
  // provide functions for dictionary lookup
201
214
  // use closure to avoid making "dict" and "experimental" global variables
@@ -210,15 +223,16 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
210
223
  // - issue a warning if the term is flagged as "experimental"
211
224
  getDictTerm: function(termName, msg) {
212
225
  const dictTerm = (dict.terms[termName] ||
213
- adhocDictionary.terms[serviceName + '.' + termName] ||
214
- adhocDictionary.terms[termName]);
226
+ userDefinedTermDict.terms[serviceName + '.' + termName] ||
227
+ userDefinedTermDict.terms[termName]);
215
228
  // register vocabulary usage if possible
216
229
  const vocName = termName.slice(0, termName.indexOf('.'));
217
- if(vocabularyDefinitions[vocName])
218
- vocabularyDefinitions[vocName].used = true;
230
+ const vocDef = mergedVocDefs[vocName];
231
+ if(vocDef && !vocDef.$ignore)
232
+ vocDef.used = true;
219
233
  else if(dictTerm?.$myServiceRoot &&
220
- adhocDictionary.xrefs[dictTerm?.$myServiceRoot])
221
- adhocDictionary.xrefs[dictTerm.$myServiceRoot].used = true;
234
+ userDefinedTermDict.xrefs[dictTerm?.$myServiceRoot])
235
+ userDefinedTermDict.xrefs[dictTerm.$myServiceRoot].used = true;
222
236
  if (dictTerm) {
223
237
  // issue message for usage of experimental Terms, but only once per Term
224
238
  if (dictTerm['$experimental'] && !experimental[termName]) {
@@ -237,13 +251,14 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
237
251
  // in addition, note usage of the respective vocabulary
238
252
  getDictType: function (typeName) {
239
253
  let dictType = (dict.types[typeName] ||
240
- adhocDictionary.types[serviceName + '.' + typeName] ||
241
- adhocDictionary.types[typeName]);
254
+ userDefinedTermDict.types[serviceName + '.' + typeName] ||
255
+ userDefinedTermDict.types[typeName]);
242
256
  if (dictType) {
243
257
  // register usage of vocabulary
244
- const voc = vocabularyDefinitions[typeName.slice(0, typeName.indexOf('.'))];
245
- if(voc)
246
- voc.used = true;
258
+ const vocName = typeName.slice(0, typeName.indexOf('.'));
259
+ const vocDef = mergedVocDefs[vocName];
260
+ if(vocDef && !vocDef.$ignore)
261
+ vocDef.used = true;
247
262
  }
248
263
  return dictType;
249
264
  }
@@ -275,12 +290,13 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
275
290
  }
276
291
  });
277
292
 
293
+
278
294
  // filter out empty <Annotations...> elements
279
295
  // add references for the used vocabularies
280
296
  return {
281
297
  annos: g_annosArray,
282
- usedVocabularies: Object.values(vocabularyDefinitions).filter(v => v.used),
283
- xrefs: Object.values(adhocDictionary.xrefs).filter(v => v.used).map(v => v.$myServiceRoot)
298
+ usedVocabularies: Object.values(mergedVocDefs).filter(v => v.used),
299
+ xrefs: Object.values(userDefinedTermDict.xrefs).filter(v => v.used).map(v => v.$myServiceRoot)
284
300
  };
285
301
 
286
302
  //-------------------------------------------------------------------------------------------------
@@ -503,10 +519,14 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
503
519
  function filterKnownAnnotations() {
504
520
  const annoNames = Object.keys(carrier).filter( x => x[0] === '@' );
505
521
  const nullWhitelist = [ '@Core.OperationAvailable' ];
506
- const knownAnnos = annoNames.filter(whatsMyTermNamespace).filter(x => carrier[x] !== null || nullWhitelist.includes(x));
522
+ const knownAnnos = annoNames.filter(n => {
523
+ const tns = whatsMyTermNamespace(n);
524
+ return tns &&
525
+ (mergedVocDefs[tns] && !mergedVocDefs[tns].$ignore ||
526
+ !mergedVocDefs[tns]);
527
+ }).filter(x => carrier[x] !== null || nullWhitelist.includes(x));
507
528
  if(isBetaEnabled(options, 'odataTerms')) {
508
529
  // Extend knownAnnos with the in-service term definitions
509
- const adhoc = [];
510
530
  annoNames.forEach(an => {
511
531
  const paths = an.slice(1).split('.');
512
532
  const hasNSPrefix = paths[0] === serviceName;
@@ -530,10 +550,9 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
530
550
  carrier[fqName] = carrier[an];
531
551
  delete carrier[an];
532
552
  }
533
- adhoc.push(fqName);
553
+ knownAnnos.push(fqName);
534
554
  }
535
555
  });
536
- knownAnnos.push(...adhoc);
537
556
  }
538
557
  return knownAnnos;
539
558
  }
@@ -868,17 +887,20 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
868
887
  newAnno.setEdmAttribute('Term', termNameWithoutQualifiers);
869
888
  newAnno.setEdmAttribute('Qualifier', qualifier);
870
889
  }
871
- // get the type of the term from the dictionary
890
+ // get the type of the term from the dictionary
872
891
  let termTypeName = null;
873
892
  const dictTerm = getDictTerm(termNameWithoutQualifiers, msg);
874
893
  if (dictTerm) {
875
894
  termTypeName = dictTerm.Type;
876
895
  }
877
896
  else {
878
- message('odata-anno-def', msg.location,{ anno: termNameWithoutQualifiers });
897
+ // message if term is completely unknown or if vocabulary is unchecked
898
+ const vocDef = mergedVocDefs[whatsMyTermNamespace('@'+termNameWithoutQualifiers)];
899
+ if((vocDef?.int && vocDef?.int?.filename) || !vocDef)
900
+ message('odata-anno-def', msg.location,{ anno: termNameWithoutQualifiers });
879
901
  }
880
902
 
881
- // handle the annotation value and put the result into the <Annotation ...> tag just created above
903
+ // handle the annotation value and put the result into the <Annotation ...> tag just created above
882
904
  handleValue(annoValue, newAnno, termNameWithoutQualifiers, termTypeName, msg);
883
905
  }
884
906
  return newAnno;
@@ -1179,21 +1201,15 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
1179
1201
  }
1180
1202
  }
1181
1203
  else if (typeof value === 'number') {
1182
- if (isComplexType(resolvedType)) {
1204
+ if (isComplexType(resolvedType) ||
1205
+ resolvedType === 'Edm.PropertyPath' ||
1206
+ resolvedType === 'Edm.Boolean') {
1183
1207
  message('odata-anno-value', msg.location,
1184
- { anno: msg.anno(), value, type: resolvedType });
1208
+ { anno: msg.anno(), value, type: resolvedType });
1185
1209
  }
1186
1210
  else if (resolvedType === 'Edm.String') {
1187
1211
  typeName = 'String';
1188
1212
  }
1189
- else if (resolvedType === 'Edm.PropertyPath') {
1190
- message('odata-anno-value', msg.location,
1191
- { anno: msg.anno(), value, type: resolvedType });
1192
- }
1193
- else if (resolvedType === 'Edm.Boolean') {
1194
- message('odata-anno-value', msg.location,
1195
- { anno: msg.anno(), value, type: resolvedType });
1196
- }
1197
1213
  else if (resolvedType === 'Edm.Decimal') {
1198
1214
  typeName = 'Decimal';
1199
1215
  }
@@ -1204,18 +1220,18 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
1204
1220
  //typeName = Number.isInteger(val) ? 'Int' : 'Float';
1205
1221
  if(Number.isInteger(value)) {
1206
1222
  typeName = 'Int';
1207
- if(resolvedType == undefined || resolvedType === 'Edm.PrimitiveType' || !resolvedType.startsWith('Edm.'))
1223
+ if(resolvedType == null || resolvedType === 'Edm.PrimitiveType' || !resolvedType.startsWith('Edm.'))
1208
1224
  resolvedType = 'Edm.Int64';
1209
1225
  }
1210
1226
  else {
1211
1227
  typeName = 'Float';
1212
- if(resolvedType == undefined || resolvedType === 'Edm.PrimitiveType'|| !resolvedType.startsWith('Edm.'))
1228
+ if(resolvedType == null || resolvedType === 'Edm.PrimitiveType'|| !resolvedType.startsWith('Edm.'))
1213
1229
  resolvedType = 'Edm.Double';
1214
1230
  }
1215
1231
  }
1216
1232
  }
1217
1233
  else if (value === null)
1218
- if((resolvedType == null || resolvedType == 'Edm.PrimitiveType') && typeName === 'String') {
1234
+ if((resolvedType == null || resolvedType === 'Edm.PrimitiveType') && typeName === 'String') {
1219
1235
  resolvedType = 'Edm.String';
1220
1236
  }
1221
1237
  else {
@@ -1246,7 +1262,7 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
1246
1262
  if (dTypeName && !isComplexType(dTypeName)) {
1247
1263
  if (!getDictType(dTypeName) && !isPrimitiveType(dTypeName) && !isCollection(dTypeName))
1248
1264
  message('odata-anno-dict', msg.location,
1249
- { anno: msg.anno(), type: dTypeName, '#': 'std' });
1265
+ { anno: msg.anno(), type: dTypeName });
1250
1266
  else
1251
1267
  message('odata-anno-value', msg.location,
1252
1268
  { anno: msg.anno(), str: 'structured', type: dTypeName, '#': 'struct' });
@@ -1286,7 +1302,7 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
1286
1302
  // Dictionary Type, render in XML only for backward compatibility
1287
1303
  newRecord.setXml( { Type: actualTypeName });
1288
1304
  const vocName = actualTypeName.slice(0, actualTypeName.indexOf('.'));
1289
- const vocDef = vocabularyDefinitions[vocName];
1305
+ const vocDef = mergedVocDefs[vocName];
1290
1306
  // Set full qualified type in JSON
1291
1307
  // TODO: Adhoc type x-ref URIs (only if abstract types are allowed in CDS)
1292
1308
  if(vocDef)
@@ -1558,7 +1574,7 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
1558
1574
  }
1559
1575
 
1560
1576
  /**
1561
- * translate vocabulary definitions into an adhoc dictionary
1577
+ * translate vocabulary definitions into a userDefinedTermDict
1562
1578
  * with the same structure as the global jsonDictionary that
1563
1579
  * contains all official term and type definitions.
1564
1580
  *
@@ -1567,7 +1583,7 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
1567
1583
  *
1568
1584
  * @returns [object, Array<object>]
1569
1585
  */
1570
- function createAdhocDictionary() {
1586
+ function createUserDefinedTermDictionary() {
1571
1587
  const allKnownVocabularies = [];
1572
1588
  const dict = { terms: {}, types: {}, xrefs: {} };
1573
1589
 
@@ -1578,7 +1594,7 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
1578
1594
  let dictDef = oDataDictionary.terms[termName];
1579
1595
  if(dictDef) {
1580
1596
  message('odata-anno-dict', ['vocabularies', termName],
1581
- { anno: termName, '#': 'redefinition' } );
1597
+ { anno: termName, string: 'annotation', '#': 'redefinition' } );
1582
1598
  }
1583
1599
  else if(!dictDef) {
1584
1600
  const annoDef = csnVocabularies[termName];
@@ -1805,8 +1821,64 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
1805
1821
 
1806
1822
  }
1807
1823
 
1824
+ function mergeVocRefs(options, message) {
1825
+ /* Merge options.odataVocRefs into vocabularyDefinitions and
1826
+ create a csn2edm stack local dictionary.
1827
+ odataVocRefs is an object, each property is the
1828
+ annotation prefix (as in mergedVocDefs), the value
1829
+ is an object { Alias, Namespace, Uri }, this way
1830
+ the definitions are unique and duplicate entries to address
1831
+ the annotation via alias and namespace is possible (see
1832
+ inverted index of mergedVocDefs above)
1833
+ */
1834
+ const mergedVocDefs = Object.assign({}, vocabularyDefinitions);
1835
+ const reqProps = ['Alias', 'Namespace', 'Uri'];
1836
+ if(options.odataVocRefs) {
1837
+ const vocRefs = options.odataVocRefs;
1838
+ if (typeof vocRefs === 'object' && !Array.isArray(vocRefs)) {
1839
+ Object.entries(vocRefs).forEach(([id, def]) => {
1840
+ let defOk = true;
1841
+ reqProps.forEach(name => {
1842
+ if(!def[name] || typeof def[name] !== 'string') {
1843
+ message('odata-anno-vocref', null,
1844
+ { id, name, '#': 'malformed' } );
1845
+ defOk = false;
1846
+ }
1847
+ else if(name === 'Alias' && !edmUtils.isODataSimpleIdentifier(def[name])) {
1848
+ message('odata-spec-violation-id', null, { id:name, value: def[name], '#': 'vocrefalias' });
1849
+ defOk = false;
1850
+ }
1851
+ });
1852
+ if(defOk) {
1853
+ const vocDef = mergedVocDefs[id];
1854
+ if(vocDef && !vocDef.$optVocRef) {
1855
+ message('odata-anno-vocref', null,
1856
+ { id, type: mergedVocDefs[id].inc.Namespace, '#': 'redef' } );
1857
+ }
1858
+ else {
1859
+ if(id !== def.Alias) {
1860
+ message('odata-anno-vocref', null,
1861
+ { id, name: def.Alias } );
1862
+ }
1863
+ else {
1864
+ // no int.filename => no validation
1865
+ const vocDef = {
1866
+ ref: { Uri: def.Uri },
1867
+ inc: { Alias: def.Alias, Namespace: def.Namespace }
1868
+ };
1869
+ setProp(vocDef, '$optVocRef', true);
1870
+ mergedVocDefs[id] = vocDef;
1871
+ }
1872
+ }
1873
+ }
1874
+ });
1875
+ }
1876
+ }
1877
+ return mergedVocDefs;
1878
+ }
1879
+
1808
1880
  //-------------------------------------------------------------------------------------------------
1809
1881
  //-------------------------------------------------------------------------------------------------
1810
1882
  //-------------------------------------------------------------------------------------------------
1811
1883
 
1812
- module.exports = { knownVocabularies, vocabularyDefinitions, csn2annotationEdm };
1884
+ module.exports = { vocabularyDefinitions, csn2annotationEdm, mergeVocRefs };
@@ -63,6 +63,9 @@ function preprocessAnnotations(csn, serviceName, options) {
63
63
  artifact.elements && Object.entries(artifact.elements).forEach(([elementName, element]) => {
64
64
  handleAnnotations(artifactName, elementName, element, [ ...location, 'elements', elementName ]);
65
65
  });
66
+ artifact.params && Object.entries(artifact.params).forEach(([paramName, param]) => {
67
+ handleAnnotations(artifactName, paramName, param, [ ...location, 'actions', artifactName, 'params', paramName ]);
68
+ });
66
69
  forEachGeneric(artifact, 'actions', (action, actionName) => {
67
70
  action.params && Object.entries(action.params).forEach(([paramName, param]) => {
68
71
  handleAnnotations(actionName, paramName, param, [ ...location, 'actions', actionName, 'params', paramName ]);
@@ -51,6 +51,8 @@ function csn2edmAll(_csn, _options, serviceNames=undefined) {
51
51
  whatsMyServiceRootName,
52
52
  fallBackSchemaName,
53
53
  options ] = initializeModel(csn, _options, messageFunctions, serviceNames);
54
+
55
+ const mergedVocabularies = translate.mergeVocRefs(options, message);
54
56
 
55
57
  const Edm = getEdm(options, messageFunctions);
56
58
 
@@ -78,6 +80,7 @@ function csn2edmAll(_csn, _options, serviceNames=undefined) {
78
80
  services[serviceCsn.name] = createEdm(serviceCsn);
79
81
  return services; }, rc);
80
82
  }
83
+
81
84
  throwWithError();
82
85
  return rc;
83
86
 
@@ -423,6 +426,24 @@ function csn2edmAll(_csn, _options, serviceNames=undefined) {
423
426
  }
424
427
  });
425
428
 
429
+ /*
430
+ Remove EntityContainer if empty
431
+ V4 spec says:
432
+ Chapter 5 Element edm:Schema
433
+ It MAY contain elements [...], edm:EntityContainer, [...].
434
+ Chapter 13 Element edm:EntityContainer
435
+ The edm:EntityContainer MUST contain one or more edm:EntitySet, edm:Singleton, edm:ActionImport, or edm:FunctionImport elements.
436
+
437
+ The first sentence in chapter 13 is:
438
+ Each metadata document used to describe an OData service MUST define exactly one entity container.
439
+
440
+ This sentence expresses that an OData SERVICE must contain an entity container, but an EDMX is not required to have a container.
441
+ Therefore it is absolutely legal and necessary to remove an empty container from the IR!
442
+ */
443
+ if(Schema._ec && Schema._ec._children.length === 0) {
444
+ Schema._children.splice(Schema._children.indexOf(Schema._ec), 1);
445
+ }
446
+
426
447
  Object.entries(NamesInSchemaXRef).forEach(([name, refs]) => {
427
448
  if(refs.length > 1) {
428
449
  error(null, ['definitions', `${Schema._edmAttributes.Namespace}.${name}`], { name: Schema._edmAttributes.Namespace },
@@ -518,10 +539,6 @@ function csn2edmAll(_csn, _options, serviceNames=undefined) {
518
539
  const properties = createProperties(elementsCsn, structuredTypeCsn)[0];
519
540
  const loc = ['definitions', structuredTypeCsn.name];
520
541
 
521
- if(properties.length === 0) {
522
- warning(null, loc, { name: structuredTypeCsn.name },
523
- 'EDM ComplexType $(NAME) has no properties');
524
- }
525
542
  if(!edmUtils.isODataSimpleIdentifier(attributes.Name))
526
543
  message('odata-spec-violation-id', loc, { id: attributes.Name });
527
544
 
@@ -1030,7 +1047,7 @@ function csn2edmAll(_csn, _options, serviceNames=undefined) {
1030
1047
 
1031
1048
  // generate the Edm.Annotations tree and append it to the corresponding schema
1032
1049
  function addAnnotations(xServiceRefs) {
1033
- let { annos, usedVocabularies, xrefs } = translate.csn2annotationEdm(reqDefs, csn.vocabularies, serviceCsn.name, Edm, options, messageFunctions);
1050
+ let { annos, usedVocabularies, xrefs } = translate.csn2annotationEdm(reqDefs, csn.vocabularies, serviceCsn.name, Edm, options, messageFunctions, mergedVocabularies);
1034
1051
  // distribute edm:Annotations into the schemas
1035
1052
  // Distribute each anno into Schema
1036
1053
  annos.forEach(anno => {
package/lib/edm/edm.js CHANGED
@@ -749,7 +749,7 @@ function getEdm(options, messageFunctions) {
749
749
  this._edmAttributes && Object.entries(this._edmAttributes).forEach(([p, v]) => {
750
750
  if (p !== 'Name' && p !== this._typeName
751
751
  // remove this line if Nullable=true becomes default
752
- && !(p === 'Nullable' && v == false))
752
+ && !(p === 'Nullable' && !v))
753
753
  {
754
754
  json[p[0] === '@' ? p : '$' + p] = v;
755
755
  }
@@ -763,7 +763,7 @@ function getEdm(options, messageFunctions) {
763
763
  }
764
764
  }
765
765
 
766
- class ComplexType extends TypeBase {
766
+ class ComplexType extends TypeBase {
767
767
  constructor(v, details, csn) {
768
768
  super(v, details, csn);
769
769
  if(this.v4 && !!csn['@open'] && isBetaEnabled(options, 'odataOpenType')) {
@@ -979,6 +979,7 @@ function getEdm(options, messageFunctions) {
979
979
  // TIPHANACDS-4180
980
980
  if(this.v2)
981
981
  {
982
+ // eslint-disable-next-line sonarjs/no-redundant-boolean
982
983
  if(csn['@odata.etag'] == true || csn['@cds.etag'] == true)
983
984
  this._edmAttributes.ConcurrencyMode='Fixed'
984
985
 
@@ -1101,8 +1102,8 @@ function getEdm(options, messageFunctions) {
1101
1102
  delete this._edmAttributes.Nullable;
1102
1103
  }
1103
1104
  // we have exactly one selfReference or the default partner
1104
- let partner =
1105
- !csn.$noPartner ?
1105
+ let partner =
1106
+ !csn.$noPartner ?
1106
1107
  csn._selfReferences.length === 1
1107
1108
  ? csn._selfReferences[0]
1108
1109
  : csn._constraints._partnerCsn
@@ -1121,6 +1122,7 @@ function getEdm(options, messageFunctions) {
1121
1122
  See csn2edm.createParmeterizedEntityTypeAndSet() for details
1122
1123
  2) ContainsTarget stems from the @odata.contained annotation
1123
1124
  */
1125
+ // eslint-disable-next-line sonarjs/no-redundant-boolean
1124
1126
  if(csn['@odata.contained'] == true || csn.containsTarget) {
1125
1127
  this._edmAttributes.ContainsTarget = true;
1126
1128
  }
@@ -70,6 +70,7 @@ function applyAppSpecificLateCsnTransformationOnElement(options, element, struct
70
70
  // for @[odata|cds].etag annotations...
71
71
  if(options.isV4())
72
72
  {
73
+ // eslint-disable-next-line sonarjs/no-redundant-boolean
73
74
  if(element['@odata.etag'] == true || element['@cds.etag'] == true) {
74
75
  // don't put element name into collection as per advice from Ralf Handl, as
75
76
  // no runtime is interested in the property itself, it is sufficient to mark
@@ -100,10 +100,6 @@ function initializeModel(csn, _options, messageFunctions, requestedServiceNames=
100
100
 
101
101
  /*
102
102
  Enrich the CSN by de-anonymizing and exposing types that are required to make the service self contained.
103
- Type exposure will add additional schema contexts and group the exposed types in these contexts.
104
- Contexts either represent another service (if the type to be exposed resides in that
105
- service), the namespace (including (sub-)contexts) or as last resort (if the type name
106
- has no prefix path) a 'root' namespace.
107
103
  */
108
104
  const schemas = typesExposure(csn, whatsMyServiceRootName, requestedServiceNames,
109
105
  fallBackSchemaName, options, csnUtils, { error });
@@ -824,8 +820,11 @@ function initializeModel(csn, _options, messageFunctions, requestedServiceNames=
824
820
  (isContainerAssoc && !options.renderForeignKeys))
825
821
  edmUtils.assignAnnotation(element, '@cds.api.ignore', true);
826
822
  // Only in containment:
827
- // If this element is a foreign key and if it is rendered, remove it from the key ref vector
828
- else if(options.odataContainment && isContainerAssoc && options.renderForeignKeys) {
823
+ // If this element is a foreign key and if it is rendered, remove it from the key ref vector (if available)
824
+ else if(options.odataContainment &&
825
+ isContainerAssoc &&
826
+ options.renderForeignKeys &&
827
+ struct.$keys) {
829
828
  delete struct.$keys[element.name];
830
829
  }
831
830
  }
@@ -834,8 +833,8 @@ function initializeModel(csn, _options, messageFunctions, requestedServiceNames=
834
833
  if(options.odataContainment && element['@odata.containment.ignore']) {
835
834
  if(!options.renderForeignKeys)
836
835
  edmUtils.assignAnnotation(element, '@cds.api.ignore', true);
837
- else
838
- // If foreign keys shall be rendered, remove it from key ref vector
836
+ else if(struct.$keys)
837
+ // If foreign keys shall be rendered, remove it from key ref vector (if available)
839
838
  delete struct.$keys[element.name];
840
839
  }
841
840
  }
@@ -1564,7 +1563,10 @@ function initializeModel(csn, _options, messageFunctions, requestedServiceNames=
1564
1563
  Object.entries(elements).forEach(([eltName, elt]) => {
1565
1564
  if(!elt.$visited) {
1566
1565
  setProp(elt, '$visited', true);
1567
- const newRefs = produceKeyRefPaths(elt, prefix + options.pathDelimiter + eltName, path);
1566
+ let newRefs = [];
1567
+ // if the foreign keys are explicitly requested, ignore associations and use the flat foreign key instead
1568
+ if(!options.renderForeignKeys || (options.renderForeignKeys && !elt.target))
1569
+ newRefs = produceKeyRefPaths(elt, prefix + options.pathDelimiter + eltName, path);
1568
1570
  if(newRefs.length) {
1569
1571
  keyPaths.push(...newRefs);
1570
1572
  // check path step key for spec violations
@@ -1859,33 +1861,47 @@ function initializeModel(csn, _options, messageFunctions, requestedServiceNames=
1859
1861
  if(typeDef?.enum) {
1860
1862
  const enumValue = [];
1861
1863
  for(const enumSymbol in typeDef.enum) {
1862
- let enumSymbolDef = typeDef.enum[enumSymbol];
1863
1864
  const result = { '@Core.SymbolicName': enumSymbol };
1864
- if(enumSymbolDef['#'])
1865
+ let enumSymbolDef = typeDef.enum[enumSymbol];
1866
+ while(enumSymbolDef && !enumSymbolDef.$visited && enumSymbolDef['#']) {
1867
+ setProp(enumSymbolDef, '$visited', true);
1865
1868
  enumSymbolDef = typeDef.enum[enumSymbolDef['#']];
1866
- if(enumSymbolDef.val === undefined) {
1867
- if(typeDef.type === 'cds.String') {
1868
- // the symbol is used as value for type 'cds.String'
1869
- result.Value = enumSymbol;
1869
+ }
1870
+ // reset visited
1871
+ for(const es in typeDef.enum)
1872
+ delete typeDef.enum[es].$visited;
1873
+
1874
+ if(enumSymbolDef) {
1875
+ if(enumSymbolDef.val !== undefined) {
1876
+ // 'null' value is represented spec conform as empty record in AllowedValues collection
1877
+ result.Value = enumSymbolDef.val;
1870
1878
  enumValue.push(result);
1871
1879
  }
1872
- else if(node.kind !== 'annotation')
1873
- // omit the entry and warn
1874
- warning('odata-enum-missing-value', path,
1880
+ else {
1881
+ if(typeDef.type === 'cds.String') {
1882
+ // the symbol is used as value for type 'cds.String'
1883
+ result.Value = enumSymbol;
1884
+ enumValue.push(result);
1885
+ }
1886
+ else if(node.kind !== 'annotation') {
1887
+ // omit the entry and warn
1888
+ warning('odata-enum-missing-value', path,
1889
+ { name: enumSymbol, anno: '@Valiation.AllowedValues', type: typeDef.type },
1890
+ 'Expected enum element $(NAME) of type $(TYPE) to have a value, not added to $(ANNO)');
1891
+ }
1892
+ }
1893
+ }
1894
+ else { // enumSymbolDef not found
1895
+ // omit the entry and warn
1896
+ warning('odata-enum-missing-value', path,
1875
1897
  { name: enumSymbol, anno: '@Valiation.AllowedValues', type: typeDef.type },
1876
1898
  'Expected enum element $(NAME) of type $(TYPE) to have a value, not added to $(ANNO)');
1877
1899
  }
1878
- else {
1879
- // 'null' value is represented spec conform as empty record in AllowedValues collection
1880
- //if(enumSymbolDef.val !== null)
1881
- result.Value = enumSymbolDef.val;
1882
- enumValue.push(result);
1883
- }
1884
1900
 
1885
1901
  // Can't rely that @description has already been renamed to @Core.Description
1886
1902
  // Eval description according to precedence (doc comment must be considered already in Odata transformer
1887
1903
  // as in contrast to the other doc commments as it is used to annotate the @Validation.AllowedValues)
1888
- const desc = enumSymbolDef['@Core.Description'] || enumSymbolDef['@description'] || enumSymbolDef.doc;
1904
+ const desc = enumSymbolDef ? enumSymbolDef['@Core.Description'] || enumSymbolDef['@description'] || enumSymbolDef.doc : undefined;
1889
1905
  if (desc)
1890
1906
  result['@Core.Description'] = desc;
1891
1907
  }
@@ -1933,7 +1949,7 @@ function initializeModel(csn, _options, messageFunctions, requestedServiceNames=
1933
1949
  const localRestrictions = container[NavResAnno] ?
1934
1950
  cloneAnnotationValue(container[NavResAnno]) : []
1935
1951
 
1936
- // prefix the existing navigation property restritictions on the container
1952
+ // prefix the existing navigation property restrictions on the container
1937
1953
  if(prefix.length) {
1938
1954
  localRestrictions.forEach(npe => {
1939
1955
  if(npe.NavigationProperty &&