@sap/cds-compiler 2.7.0 → 2.10.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 (63) hide show
  1. package/CHANGELOG.md +103 -0
  2. package/lib/api/main.js +8 -10
  3. package/lib/api/options.js +13 -9
  4. package/lib/api/validate.js +11 -8
  5. package/lib/base/keywords.js +32 -2
  6. package/lib/base/message-registry.js +16 -0
  7. package/lib/base/messages.js +2 -0
  8. package/lib/base/model.js +1 -0
  9. package/lib/checks/onConditions.js +5 -0
  10. package/lib/checks/types.js +26 -2
  11. package/lib/checks/unknownMagic.js +38 -0
  12. package/lib/checks/validator.js +7 -2
  13. package/lib/compiler/assert-consistency.js +11 -5
  14. package/lib/compiler/builtins.js +2 -0
  15. package/lib/compiler/checks.js +3 -1
  16. package/lib/compiler/definer.js +87 -29
  17. package/lib/compiler/resolver.js +75 -16
  18. package/lib/compiler/shared.js +29 -9
  19. package/lib/edm/annotations/genericTranslation.js +182 -186
  20. package/lib/edm/csn2edm.js +93 -98
  21. package/lib/edm/edm.js +16 -20
  22. package/lib/edm/edmPreprocessor.js +274 -83
  23. package/lib/edm/edmUtils.js +29 -10
  24. package/lib/gen/language.checksum +1 -1
  25. package/lib/gen/language.interp +12 -1
  26. package/lib/gen/language.tokens +57 -53
  27. package/lib/gen/languageLexer.interp +10 -1
  28. package/lib/gen/languageLexer.js +770 -744
  29. package/lib/gen/languageLexer.tokens +49 -46
  30. package/lib/gen/languageParser.js +4727 -4323
  31. package/lib/json/from-csn.js +52 -23
  32. package/lib/json/to-csn.js +185 -71
  33. package/lib/language/errorStrategy.js +1 -0
  34. package/lib/language/genericAntlrParser.js +9 -0
  35. package/lib/language/language.g4 +90 -31
  36. package/lib/main.js +4 -0
  37. package/lib/model/api.js +78 -0
  38. package/lib/model/csnRefs.js +7 -1
  39. package/lib/model/csnUtils.js +5 -4
  40. package/lib/optionProcessor.js +7 -1
  41. package/lib/render/.eslintrc.json +3 -1
  42. package/lib/render/toCdl.js +45 -9
  43. package/lib/render/toHdbcds.js +100 -34
  44. package/lib/render/toSql.js +12 -4
  45. package/lib/render/utils/common.js +5 -9
  46. package/lib/sql-identifier.js +6 -1
  47. package/lib/transform/db/draft.js +6 -4
  48. package/lib/transform/db/expansion.js +14 -4
  49. package/lib/transform/db/flattening.js +13 -5
  50. package/lib/transform/db/transformExists.js +252 -58
  51. package/lib/transform/forHanaNew.js +7 -1
  52. package/lib/transform/forOdataNew.js +12 -8
  53. package/lib/transform/odata/attachPath.js +19 -4
  54. package/lib/transform/odata/generateForeignKeyElements.js +11 -10
  55. package/lib/transform/odata/referenceFlattener.js +44 -38
  56. package/lib/transform/odata/sortByAssociationDependency.js +2 -2
  57. package/lib/transform/odata/structuralPath.js +76 -0
  58. package/lib/transform/odata/structureFlattener.js +13 -10
  59. package/lib/transform/odata/typesExposure.js +22 -12
  60. package/lib/transform/transformUtilsNew.js +33 -1
  61. package/lib/transform/translateAssocsToJoins.js +6 -4
  62. package/lib/transform/universalCsnEnricher.js +67 -0
  63. package/package.json +1 -1
@@ -1,9 +1,8 @@
1
1
  'use strict';
2
2
  /* eslint max-statements-per-line:off */
3
3
  const { setProp, isDeprecatedEnabled, isBetaEnabled } = require('../base/model');
4
- const { forEachDefinition, forEachGeneric, forEachMember, forEachMemberRecursively,
4
+ const { forEachDefinition, forEachGeneric, forEachMemberRecursively,
5
5
  isEdmPropertyRendered, getUtils, cloneCsn, isBuiltinType } = require('../model/csnUtils');
6
- const { makeMessageFunction } = require('../base/messages');
7
6
  const edmUtils = require('./edmUtils.js');
8
7
  const typesExposure = require('../transform/odata/typesExposure');
9
8
  const expandCSNToFinalBaseType = require('../transform/odata/toFinalBaseType');
@@ -35,12 +34,11 @@ const {
35
34
  * @param {CSN.Model} csn
36
35
  * @param {object} _options
37
36
  */
38
- function initializeModel(csn, _options)
37
+ function initializeModel(csn, _options, messageFunctions)
39
38
  {
40
39
  if (!_options)
41
40
  throw Error('Please debug me: initializeModel must be invoked with options');
42
41
 
43
- const messageFunctions = makeMessageFunction(csn, _options);
44
42
  const { info, warning, error, throwWithError } = messageFunctions;
45
43
 
46
44
  const csnUtils = getUtils(csn);
@@ -142,14 +140,18 @@ function initializeModel(csn, _options)
142
140
  // must be run before proxy exposure to avoid potential reference collisions
143
141
  convertExposedTypesOfOtherServicesIntoCrossReferences();
144
142
  // create association target proxies
145
- forEachDefinition(csn, exposeTargetsAsProxiesOrSchemaRefs);
143
+ // Decide if an entity set needs to be constructed or not
144
+ forEachDefinition(csn, [ exposeTargetsAsProxiesOrSchemaRefs, determineEntitySet ]);
145
+ if(options.isV4())
146
+ forEachDefinition(csn, initializeEdmNavPropBindingTargets);
146
147
 
147
148
  // Things that can be done in one pass
148
149
  // Create edmKeyRefPaths
149
- // Decide if an entity set needs to be constructed or not
150
+ // Create NavigationPropertyBindings, requires determineEntitySet
150
151
  // Map /** doc comments */ to @CoreDescription
151
152
  // Artifact identifier spec compliance check (should be run last)
152
- forEachDefinition(csn, [ initializeEdmKeyRefPaths, determineEntitySet, initializeEdmTypesAndDescription, checkArtifactIdentifierAndBoundActions ]);
153
+ forEachDefinition(csn, [ initializeEdmKeyRefPaths, initializeEdmNavPropBindingPaths,
154
+ initializeEdmTypesAndDescription, checkArtifactIdentifierAndBoundActions ]);
153
155
  }
154
156
  return [serviceRoots, schemas, whatsMyServiceRootName, options];
155
157
 
@@ -337,33 +339,57 @@ function initializeModel(csn, _options)
337
339
  // entity set. Instead try to rewrite the annotation in such a way that it is effective
338
340
  // on the containment navigation property.
339
341
  function initializeContainments(container) {
340
- forEachMemberRecursively(container, (element, elementName) => {
341
- if(isAssociationOrComposition(element) && !element._ignore) {
342
- if(element['@odata.contained']) {
342
+ if(['entity', 'view'].includes(container.kind)) {
343
+ forEachMemberRecursively(container, initContainments,
344
+ [], true, { elementsOnly: true });
345
+ }
346
+
347
+ function initContainments(elt, eltName) {
348
+ if(isAssociationOrComposition(elt) && elt['@odata.contained'] && !elt._ignore) {
343
349
  // Let the containee know its container
344
350
  // (array because the contanee may contained more then once)
345
- let containee = element._target;
346
- if (!containee._containerEntity) {
347
- setProp(containee, '_containerEntity', []);
348
- }
351
+ let containee = elt._target;
352
+ if (!containee._containerEntity)
353
+ setProp(containee, '_containerEntity', []);
349
354
  // add container only once per containee
350
- if (!containee._containerEntity.includes(container.name)) {
351
- containee._containerEntity.push(container.name);
352
- // Mark associations in the containee pointing to the container (i.e. to this entity)
353
- containee.elements && Object.values(containee.elements).forEach( containeeElement => {
354
- if (containeeElement._target && containeeElement._target.name) {
355
- // If this is an association that points to a container (but is not by itself contained,
356
- // which would indicate the top role in a hierarchy) mark it with '_isToContainer'
357
- if (containeeElement._target.name == container.name && !containeeElement['@odata.contained']) {
358
- setProp(containeeElement, '_isToContainer', true);
359
- }
360
- }
361
- });
362
- }
363
- rewriteContainmentAnnotations(container, containee, elementName);
355
+ if (!containee._containerEntity.includes(container.name))
356
+ containee._containerEntity.push(container.name);
357
+ // Mark associations in the containee pointing to the container (i.e. to this entity)
358
+ forEachMemberRecursively(containee, markToContainer,
359
+ [], true, { elementsOnly: true });
360
+ rewriteContainmentAnnotations(container, containee, eltName);
361
+ }
362
+ else {
363
+ // try to find elements to drill down further
364
+ while(elt && !(isBuiltinType(elt.type) || elt.elements)) {
365
+ elt = csn.definitions[elt.type];
366
+ }
367
+ if(elt && elt.elements) {
368
+ forEachMemberRecursively(elt, initContainments,
369
+ [], true, { elementsOnly: true });
364
370
  }
365
371
  }
366
- });
372
+ }
373
+
374
+ function markToContainer(elt) {
375
+ if(elt._target && elt._target.name) {
376
+ // If this is an association that points to the container (but is not by itself contained,
377
+ // which would indicate the top role in a hierarchy) mark it with '_isToContainer'
378
+ if(elt._target.name === container.name && !elt['odata.contained']) {
379
+ setProp(elt, '_isToContainer', true);
380
+ }
381
+ }
382
+ else {
383
+ // try to find elements to drill down further
384
+ while(elt && !(isBuiltinType(elt.type) || elt.elements)) {
385
+ elt = csn.definitions[elt.type];
386
+ }
387
+ if(elt && elt.elements) {
388
+ forEachMemberRecursively(elt, markToContainer,
389
+ [], true, { elementsOnly: true });
390
+ }
391
+ }
392
+ }
367
393
  }
368
394
 
369
395
  // Split an entity with parameters into two entity types with their entity sets,
@@ -420,7 +446,6 @@ function initializeModel(csn, _options)
420
446
  assignProp(parameterCsn, '_SetAttributes',
421
447
  {'@sap.creatable': false, '@sap.updatable': false, '@sap.deletable': false, '@sap.pageable': false });
422
448
 
423
- assignProp(parameterCsn, '$keys', Object.create(null));
424
449
  setProp(parameterCsn, '$isParamEntity', true);
425
450
  setProp(parameterCsn, '$mySchemaName', entityCsn.$mySchemaName);
426
451
 
@@ -438,9 +463,10 @@ function initializeModel(csn, _options)
438
463
  elt.name = n;
439
464
  delete elt.kind;
440
465
  elt.key = true; // params become primary key in parameter entity
441
- parameterCsn.$keys[n] = parameterCsn.elements[n] = elt;
466
+ parameterCsn.elements[n] = elt;
442
467
  });
443
-
468
+ linkAssociationTarget(parameterCsn);
469
+ initializeContainments(parameterCsn);
444
470
  // add assoc to result set, FIXME: is the cardinality correct?
445
471
  parameterCsn.elements[parameterToOriginalAssocName] = {
446
472
  '@odata.contained': true,
@@ -543,16 +569,17 @@ function initializeModel(csn, _options)
543
569
  let keys = Object.create(null);
544
570
  let validFrom = [], validKey = [];
545
571
 
546
- let structParent = def.items || def;
547
-
548
572
  // Iterate all struct elements
549
- forEachGeneric(structParent, 'elements', (element, elementName) => {
550
- initElement(element, elementName, def);
573
+ forEachMemberRecursively(def.items || def, (element, elementName, prop, path = [], construct) => {
574
+ if(!['elements'].includes(prop))
575
+ return;
576
+
577
+ initElement(element, elementName, construct);
551
578
 
552
579
  if(!['event', 'aspect'].includes(def.kind)) {
553
580
  if(element._parent && element._parent.$mySchemaName) {
554
581
  if(!isODataSimpleIdentifier(elementName)) {
555
- signalIllegalIdentifier(elementName, ['definitions', def.name, 'elements', elementName]);
582
+ signalIllegalIdentifier(elementName, ['definitions', def.name].concat(path));
556
583
  } else if (options.isV2() && /^(_|[0-9])/.test(elementName) && ['view', 'entity'].includes(element._parent.kind)) {
557
584
  // FIXME: Rewrite signalIllegalIdentifier function to be more flexible
558
585
  error(null, ['definitions', def.name, 'elements', elementName], { prop: elementName[0] },
@@ -578,10 +605,12 @@ function initializeModel(csn, _options)
578
605
 
579
606
  //forward annotations from managed association element to its foreign keys
580
607
  if(element.keys && options.isFlatFormat) {
608
+ const elements = construct.items && construct.items.elements || construct.elements;
581
609
  for(let fk of element.keys) {
582
610
  forAll(element, (attr, attrName) => {
583
- if(attrName[0] === '@' && fk.$generatedFieldName)
584
- def.elements[fk.$generatedFieldName][attrName] = attr;
611
+ if(attrName[0] === '@' && fk.$generatedFieldName && elements && elements[fk.$generatedFieldName]) {
612
+ elements[fk.$generatedFieldName][attrName] = attr;
613
+ }
585
614
  });
586
615
  }
587
616
  }
@@ -594,7 +623,7 @@ function initializeModel(csn, _options)
594
623
  keys[elementName] = element;
595
624
  }
596
625
  applyAppSpecificLateCsnTransformationOnElement(options, element, def, error);
597
- });
626
+ }, [], true, { elementsOnly: true });
598
627
 
599
628
  if(!isDeprecatedEnabled(options, 'v1KeysForTemporal')) {
600
629
  // if artifact has a cds.valid.key mention it as @Core.AlternateKey
@@ -646,14 +675,14 @@ function initializeModel(csn, _options)
646
675
  if(!isStructuredArtifact(struct))
647
676
  return;
648
677
 
649
- forEachMember(struct, element => {
678
+ forEachMemberRecursively(struct.items || struct, (element) => {
650
679
  if (isAssociationOrComposition(element) && !element._ignore) {
651
680
  // setup the constraints object
652
681
  setProp(element, '_constraints', { constraints: Object.create(null), selfs: [], _origins: [], termCount: 0 });
653
682
  // and crack the ON condition
654
683
  resolveOnConditionAndPrepareConstraints(csn, element, messageFunctions);
655
684
  }
656
- });
685
+ }, [], true, { elementsOnly: true });
657
686
  }
658
687
 
659
688
  /*
@@ -669,13 +698,16 @@ function initializeModel(csn, _options)
669
698
  4) All of this can be revoked with options.renderForeignKeys.
670
699
  */
671
700
  function ignoreProperties(struct) {
672
- forEachGeneric(struct, 'elements', (element) => {
701
+ if(!isStructuredArtifact(struct))
702
+ return;
703
+
704
+ forEachMemberRecursively(struct.items || struct, (element) => {
673
705
  if(!element.target) {
674
706
  if(element['@odata.foreignKey4']) {
675
707
  let isContainerAssoc = false;
676
- let elements = struct.elements;
708
+ let elements = (struct.items || struct).elements;
677
709
  let assoc = undefined;
678
- let paths = element['@odata.foreignKey4'].split('.')
710
+ const paths = element['@odata.foreignKey4'].split('.')
679
711
  for(let p of paths) {
680
712
  assoc = elements[p];
681
713
  if(assoc) // could be that the @odata.foreignKey4 was propagated...
@@ -714,7 +746,7 @@ function initializeModel(csn, _options)
714
746
  // ignore it if option odataContainment is true and no foreign keys should be rendered
715
747
  assignAnnotation(element, '@odata.navigable', false);
716
748
  }
717
- });
749
+ }, [], true, { elementsOnly: true });
718
750
  }
719
751
 
720
752
  /*
@@ -726,7 +758,7 @@ function initializeModel(csn, _options)
726
758
  if(!isStructuredArtifact(struct))
727
759
  return;
728
760
 
729
- forEachMember(struct, element => {
761
+ forEachMemberRecursively(struct.items || struct, (element) => {
730
762
  if (isAssociationOrComposition(element) && !element._ignore) {
731
763
  finalizeReferentialConstraints(csn, element, options, info);
732
764
 
@@ -753,7 +785,7 @@ function initializeModel(csn, _options)
753
785
  }
754
786
  }
755
787
  }
756
- });
788
+ }, [], true, { elementsOnly: true });
757
789
  }
758
790
 
759
791
  /*
@@ -816,7 +848,7 @@ function initializeModel(csn, _options)
816
848
  const globalSchemaPrefix = whatsMyServiceRootName(struct.$mySchemaName);
817
849
  // if this artifact is a service member check its associations
818
850
  if(globalSchemaPrefix) {
819
- forEachGeneric(struct, 'elements', element => {
851
+ forEachGeneric(struct.items || struct, 'elements', element => {
820
852
  if(!isAssociationOrComposition(element) || element._ignore || element['@odata.navigable'] === false)
821
853
  return;
822
854
  /*
@@ -874,7 +906,7 @@ function initializeModel(csn, _options)
874
906
  }
875
907
  else {
876
908
  // fake the target to be proxy
877
- element._target.$externalRef = true;
909
+ setProp(element._target, '$externalRef', true);
878
910
  }
879
911
  }
880
912
  else {
@@ -923,7 +955,6 @@ function initializeModel(csn, _options)
923
955
  setProp(proxy, '$keys', Object.create(null));
924
956
  setProp(proxy, '$hasEntitySet', false);
925
957
  setProp(proxy, '$exposedTypes', Object.create(null));
926
-
927
958
  // copy all annotations of the target to the proxy
928
959
  Object.entries(assoc._target).forEach(([k, v]) => {
929
960
  if(k[0] === '@')
@@ -1070,10 +1101,12 @@ function initializeModel(csn, _options)
1070
1101
  if(!elem.target) {
1071
1102
  type.elements[elemName] = Object.create(null);
1072
1103
  Object.keys(elem).forEach(prop => type.elements[elemName][prop] = elem[prop])
1104
+ type.elements[elemName].notNull = true;
1073
1105
  }
1074
1106
  else {
1075
1107
  type.elements[elemName] = createProxyOrSchemaRefForManagedAssoc(elem);
1076
1108
  }
1109
+ setProp(type.elements[elemName], 'name', elem.name);
1077
1110
  });
1078
1111
  return type;
1079
1112
  }
@@ -1100,6 +1133,7 @@ function initializeModel(csn, _options)
1100
1133
  // art is in the target side, clone it and remove key property
1101
1134
  let cloneArt = cloneCsn(art, options);
1102
1135
  setProp(cloneArt, 'name', art.name);
1136
+ cloneArt.notNull = true;
1103
1137
  delete cloneArt.key;
1104
1138
  newElt.elements[art.name] = cloneArt;
1105
1139
  });
@@ -1189,14 +1223,14 @@ function initializeModel(csn, _options)
1189
1223
  function registerProxy(proxy, element) {
1190
1224
  if(proxy === undefined)
1191
1225
  return undefined;
1192
- const proxyName = globalSchemaPrefix + '.' + proxy.name;
1193
- const schemaName = globalSchemaPrefix + '.' + proxy.$mySchemaName;
1226
+ const fqProxyName = globalSchemaPrefix + '.' + proxy.name;
1227
+ const fqSchemaName = globalSchemaPrefix + '.' + proxy.$mySchemaName;
1194
1228
 
1195
1229
  if(!element._target.$cachedProxy)
1196
1230
  assignProp(element._target, '$cachedProxy', Object.create(null));
1197
1231
  if(getProxyForTargetOf(element)) {
1198
1232
  info(null, ['definitions', struct.name, 'elements', element.name],
1199
- { name: proxyName }, 'Proxy EDM entity type $(NAME) has already been registered');
1233
+ { name: fqProxyName }, 'Proxy EDM entity type $(NAME) has already been registered');
1200
1234
  }
1201
1235
  else
1202
1236
  element._target.$cachedProxy[globalSchemaPrefix] = proxy;
@@ -1206,7 +1240,7 @@ function initializeModel(csn, _options)
1206
1240
  // (that may reside in another subcontext schema), but only once
1207
1241
  const schemaSet = new Set();
1208
1242
  // start with the schema name for the proxy
1209
- schemaSet.add(schemaName);
1243
+ schemaSet.add(fqSchemaName);
1210
1244
  // followed by all namespaces that are potentially exposed by the exposed types
1211
1245
  // don't forget to prepend the global namespace prefix
1212
1246
  // schemas are ordered in csn2edm.js for each service
@@ -1219,12 +1253,16 @@ function initializeModel(csn, _options)
1219
1253
  }
1220
1254
  });
1221
1255
  /** @type {object} */
1222
- const alreadyRegistered = csn.definitions[proxyName]
1256
+ const alreadyRegistered = csn.definitions[fqProxyName]
1223
1257
  if(!alreadyRegistered) {
1224
- csn.definitions[proxyName] = proxy;
1258
+ csn.definitions[fqProxyName] = proxy;
1259
+ setProp(proxy, '$path', ['definitions', fqProxyName]);
1225
1260
  Object.entries(proxy.$exposedTypes).forEach(([tn, v]) => {
1226
- if(csn.definitions[globalSchemaPrefix + '.' + tn] === undefined)
1227
- csn.definitions[globalSchemaPrefix + '.' + tn] = v;
1261
+ const fqtn = globalSchemaPrefix + '.' + tn;
1262
+ if(csn.definitions[fqtn] === undefined) {
1263
+ csn.definitions[fqtn] = v;
1264
+ setProp(v, '$path', ['definitions', fqtn]);
1265
+ }
1228
1266
  });
1229
1267
  info(null, ['definitions', element._parent.name, 'elements', element.name],
1230
1268
  { name: proxy.name }, 'Created proxy EDM entity type $(NAME)');
@@ -1232,16 +1270,16 @@ function initializeModel(csn, _options)
1232
1270
  else if(alreadyRegistered && !alreadyRegistered.$proxy &&
1233
1271
  !['entity', 'view'].includes(alreadyRegistered.kind)) {
1234
1272
  warning(null, ['definitions', element._parent.name, 'elements', element.name],
1235
- { name: proxyName, kind: alreadyRegistered.kind },
1273
+ { name: fqProxyName, kind: alreadyRegistered.kind },
1236
1274
  'No proxy EDM entity type created due to name collision with $(NAME) of kind $(KIND)');
1237
1275
  return undefined;
1238
1276
  }
1239
1277
  }
1240
1278
  else {
1241
1279
  // it's a service reference, just add that reference proxy
1242
- if(!schemas[schemaName]) {
1243
- schemas[schemaName] = proxy;
1244
- schemaNames.push(schemaName);
1280
+ if(!schemas[fqSchemaName]) {
1281
+ schemas[fqSchemaName] = proxy;
1282
+ schemaNames.push(fqSchemaName);
1245
1283
  info(null, ['definitions', struct.name, 'elements', element.name],
1246
1284
  { name: proxy.name }, 'Created EDM namespace reference $(NAME)');
1247
1285
  }
@@ -1286,11 +1324,8 @@ function initializeModel(csn, _options)
1286
1324
  else if(!k.target) {
1287
1325
  struct.$edmKeyPaths.push([kn]);
1288
1326
  }
1289
- // complain about nullable keys in toplevel
1290
- if(k.notNull === false) {
1291
- const pathToElement = ['definitions', struct.name, 'elements', k.name];
1292
- signalErrorForNullableKey(pathToElement);
1293
- }
1327
+ // check toplevel key for spec violations
1328
+ checkKeySpecViolations(k, ['definitions', struct.name, 'elements', k.name]);
1294
1329
  }
1295
1330
  });
1296
1331
  }
@@ -1306,7 +1341,7 @@ function initializeModel(csn, _options)
1306
1341
  If element is of scalar type, return it as an array.
1307
1342
  */
1308
1343
  function produceKeyRefPaths(eltCsn, prefix) {
1309
- let keyPaths = [];
1344
+ const keyPaths = [];
1310
1345
  if(!isEdmPropertyRendered(eltCsn, options)) {
1311
1346
  // let annos = Object.keys(eltCsn).filter(a=>a[0]==='@').join(', ');
1312
1347
  // warning(null, ['definitions', struct.name, 'elements', eltCsn.name ],
@@ -1314,15 +1349,19 @@ function initializeModel(csn, _options)
1314
1349
  return keyPaths;
1315
1350
  }
1316
1351
  // OData requires all elements along the path to be nullable: false (that is either key or notNull)
1317
- let elements = eltCsn.elements || getFinalTypeDef(eltCsn.items && eltCsn.items.type || eltCsn.type).elements;
1352
+
1353
+ const finalType = getFinalTypeDef(eltCsn.items && eltCsn.items.type || eltCsn.type);
1354
+ const elements = eltCsn.elements || eltCsn.items && eltCsn.items.elements|| finalType.elements || finalType.items && finalType.items.elements;
1318
1355
  if(elements) {
1319
1356
  Object.entries(elements).forEach(([eltName, elt]) => {
1320
- keyPaths.push(...produceKeyRefPaths(elt, prefix + options.pathDelimiter + eltName));
1321
- if(elt.notNull === false && !isBuiltinType(elt)) {
1322
- const odataKeyPath = `${prefix}/${eltName}`;
1357
+ const newRefs = produceKeyRefPaths(elt, prefix + options.pathDelimiter + eltName);
1358
+ if(newRefs.length) {
1359
+ keyPaths.push(...newRefs);
1360
+ // check path step key for spec violations
1361
+ const pathSegment = `${prefix}/${eltName}`;
1323
1362
  // we want to point to the element in the entity which is the first path step
1324
- const pathToMaliciousKey = struct.$path.concat(['elements']).concat(odataKeyPath.split('/')[0]);
1325
- signalErrorForNullableKey(pathToMaliciousKey, odataKeyPath);
1363
+ const location = struct.$path.concat(['elements']).concat(pathSegment.split('/')[0]);
1364
+ checkKeySpecViolations(elt, location, pathSegment);
1326
1365
  }
1327
1366
  });
1328
1367
  }
@@ -1353,17 +1392,169 @@ function initializeModel(csn, _options)
1353
1392
  return keyPaths;
1354
1393
  }
1355
1394
 
1356
- function signalErrorForNullableKey(pathToElement, keyElement) {
1357
- error(null, pathToElement,
1358
- {name: keyElement, '#': keyElement ? 'std' : 'scalar'},
1359
- {
1360
- std: 'Key element $(NAME) must not be nullable',
1361
- scalar: 'Key element must not be nullable'
1395
+ function checkKeySpecViolations(elt, location, pathSegment) {
1396
+ // Nullability
1397
+ if((!elt.key && (elt.notNull === undefined || elt.notNull === false)) ||
1398
+ elt.key && (elt.notNull !== undefined && elt.notNull === false)) {
1399
+ error('odata-spec-violation-key-null', location,
1400
+ {name: pathSegment, '#': pathSegment ? 'std' : 'scalar'});
1401
+ }
1402
+ // many
1403
+ let type = elt.items || getFinalTypeDef(elt.type).items;
1404
+ if(type) {
1405
+ error('odata-spec-violation-key-array', location,
1406
+ {name: pathSegment, '#': pathSegment ? 'std' : 'scalar'});
1407
+ }
1408
+ // type
1409
+ if(!elt.elements) {
1410
+ if(!type)
1411
+ type = isBuiltinType(elt.type) ? elt : csn.definitions[elt.type];
1412
+
1413
+ // check for legal scalar types, proxy exposed structured types are not resolvable in CSN
1414
+ // V2 allows any Edm.PrimitiveType (even Double and Binary), V4 is more specific:
1415
+ if(options.isV4() && type && !isAssociationOrComposition(type) && isBuiltinType(type.type)) {
1416
+ const edmType = edmUtils.mapCdsToEdmType(type);
1417
+ const legalEdmTypes = [
1418
+ 'Edm.Boolean', 'Edm.Byte', 'Edm.Date', 'Edm.DateTimeOffset', 'Edm.Decimal', 'Edm.Duration',
1419
+ 'Edm.Guid', 'Edm.Int16', 'Edm.Int32', 'Edm.Int64', 'Edm.SByte', 'Edm.String', 'Edm.TimeOfDay' ];
1420
+ if(!legalEdmTypes.includes(edmType)) {
1421
+ warning('odata-spec-violation-key-type', location,
1422
+ {name: pathSegment, type: type.type, id: edmType, '#': pathSegment ? 'std' : 'scalar'});
1423
+ }
1424
+ }
1425
+ }
1426
+ }
1427
+ }
1428
+
1429
+ /*
1430
+ Calculate all reachable entity set paths for a given navigation start point
1431
+
1432
+ Rule: First non-containment association terminates Path, if association is
1433
+ containment enabling assoc, Target is own Struct/ plus the path down to the
1434
+ n-2nd path segment (which is the path to the n-1st implicit entity set).
1435
+
1436
+ Example:
1437
+ entity Header {
1438
+ items: composition of many {
1439
+ toF: association to F;
1440
+ subitems: composition of many {
1441
+ toG: association to G;
1442
+ subitems: composition of many {
1443
+ toG: association to G;
1444
+ };
1445
+ }
1446
+ }
1447
+ }
1448
+ Must produce:
1449
+ Path="items/up_" Target="Header"/>
1450
+ Path="items/toF" Target="F"/>
1451
+ Path="items/subitems/up_" Target="Header/items"/>
1452
+ Path="items/subitems/toG" Target="G"/>
1453
+ Path="items/subitems/subitems/up_" Target="Header/items/subitems"/>
1454
+ Path="items/subitems/subitems/toG" Target="G"/>
1455
+ */
1456
+ function initializeEdmNavPropBindingTargets(struct) {
1457
+ if(options.isV4() && struct.$mySchemaName && struct.$hasEntitySet) {
1458
+ forEachGeneric(struct.items || struct, 'elements', (element) => {
1459
+ produceTargetPath([edmUtils.getBaseName(struct.name)], element, struct);
1460
+ });
1461
+ }
1462
+
1463
+ function produceTargetPath(prefix, elt, curDef) {
1464
+ const newPrefix = [...prefix, elt.name];
1465
+ if(isEdmPropertyRendered(elt, options)) {
1466
+ // Assoc can never be a derived TypeDefinition, no need to
1467
+ // unroll derived type chains for assocs
1468
+ if(isAssociationOrComposition(elt) && !elt.$touched) {
1469
+ if(!elt._target.$edmTgtPaths)
1470
+ setProp(elt._target, '$edmTgtPaths', []);
1471
+ if(!elt._target.$hasEntitySet && !elt._isToContainer && curDef !== elt._target) {
1472
+ // follow elements in the target but avoid cycles
1473
+ setProp(elt, '$touched', true);
1474
+ elt._target.$edmTgtPaths.push(newPrefix);
1475
+ Object.values(elt._target.elements).forEach(e => produceTargetPath(newPrefix, e, elt._target));
1476
+ delete elt.$touched;
1477
+ }
1478
+ }
1479
+ else {
1480
+ // try to find elements to drill down further
1481
+ while(elt && !(isBuiltinType(elt.type) || elt.elements)) {
1482
+ elt = csn.definitions[elt.type];
1483
+ }
1484
+ elt && elt.elements && Object.values(elt.elements).forEach(e => produceTargetPath(newPrefix, e, curDef));
1362
1485
  }
1363
- );
1486
+ }
1364
1487
  }
1365
1488
  }
1366
1489
 
1490
+ function initializeEdmNavPropBindingPaths(struct) {
1491
+ if(options.isV4() && struct.$mySchemaName && struct.$hasEntitySet) {
1492
+ let npbs = [];
1493
+ forEachGeneric(struct.items || struct, 'elements', (element) => {
1494
+ npbs = npbs.concat(produceNavigationPath(element, struct));
1495
+ });
1496
+ setProp(struct, '$edmNPBs', npbs);
1497
+ }
1498
+
1499
+ // collect all paths originating from this element that end up in an entity set
1500
+ function produceNavigationPath(elt, curDef) {
1501
+ let npbs = [];
1502
+ const prefix = elt.name;
1503
+ if(isEdmPropertyRendered(elt, options)) {
1504
+ // Assoc can never be a derived TypeDefinition, no need to
1505
+ // unroll derived type chains for assocs
1506
+ if(isAssociationOrComposition(elt) && !elt.$touched) {
1507
+ // drill into target only if
1508
+ // 1) target has no entity set and this assoc is not going to the container
1509
+ // 2) current definition and target are the same (cycle)
1510
+ if(!elt._target.$hasEntitySet && !elt._isToContainer && curDef !== elt._target) {
1511
+ // follow elements in the target but avoid cycles
1512
+ setProp(elt, '$touched', true);
1513
+ Object.values(elt._target.elements).forEach(e => npbs = npbs.concat(produceNavigationPath(e, elt._target)));
1514
+ delete elt.$touched;
1515
+ }
1516
+ else if(!(options.odataContainment && options.isV4() && elt['@odata.contained'])) {
1517
+ // end point reached but must not be an external reference nor a proxy nor a composition itself
1518
+ // last assoc step must not be to-n and target a singleton
1519
+ let p = undefined;
1520
+ if (!elt._target.$externalRef &&
1521
+ !(edmUtils.isToMany(elt) && edmUtils.isSingleton(elt._target, options.isV4()))) {
1522
+ if(elt._target.$edmTgtPaths && elt._target.$edmTgtPaths.length) {
1523
+ p = elt._target.$edmTgtPaths.find(p => p[0] === edmUtils.getBaseName(struct.name)) || elt._target.$edmTgtPaths[0];
1524
+ }
1525
+ else if(elt._target.$hasEntitySet) {
1526
+ const baseName = edmUtils.getBaseName(elt._target.$entitySetName || elt._target.name);
1527
+ // if own struct and target have a set they either are in the same $mySchemaName or not
1528
+ // if target is in another schema, target the full qualified entity set
1529
+ p = (elt._target.$mySchemaName === struct.$mySchemaName) ?
1530
+ [ baseName ] : [elt._target.$mySchemaName + '.EntityContainer', baseName];
1531
+ }
1532
+ if(p) {
1533
+ // if own struct and target have a set they either are in the same $mySchemaName or not
1534
+ // if target is in another schema, target the full qualified entity set
1535
+ const npb = {
1536
+ Path: elt.name,
1537
+ Target: p.join('/')
1538
+ };
1539
+ npbs.push( npb );
1540
+ }
1541
+ }
1542
+ // Do not prepend prefix here!
1543
+ return npbs;
1544
+ }
1545
+ }
1546
+ else {
1547
+ // try to find elements to drill down further
1548
+ while(elt && !(isBuiltinType(elt.type) || elt.elements)) {
1549
+ elt = csn.definitions[elt.type];
1550
+ }
1551
+ elt && elt.elements && Object.values(elt.elements).forEach(e => npbs = npbs.concat(produceNavigationPath(e, curDef)));
1552
+ }
1553
+ }
1554
+ npbs.forEach(p => p.Path = prefix + '/' + p.Path );
1555
+ return npbs;
1556
+ }
1557
+ }
1367
1558
 
1368
1559
  function determineEntitySet(struct) {
1369
1560
  // if this is an entity or a view, determine if an entity set is required or not
@@ -1827,7 +2018,7 @@ function applyAppSpecificLateCsnTransformationOnStructure(options, struct, error
1827
2018
  let key = { name: keyName, key : true, type : 'cds.String', '@sap.sortable':false, '@sap.filterable':false, '@UI.Hidden': true };
1828
2019
  elements[keyName] = key;
1829
2020
  setProp(struct, '$keys',{ [keyName] : key } );
1830
- forEachGeneric(struct, 'elements', (e,n) =>
2021
+ forEachGeneric(struct.items || struct, 'elements', (e,n) =>
1831
2022
  {
1832
2023
  if(e.key) delete e.key;
1833
2024
  elements[n] = e;