@sap/cds-compiler 3.5.2 → 3.6.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 (85) hide show
  1. package/CHANGELOG.md +63 -1
  2. package/bin/cdsc.js +14 -6
  3. package/doc/CHANGELOG_ARCHIVE.md +10 -10
  4. package/doc/CHANGELOG_DEPRECATED.md +2 -2
  5. package/lib/api/main.js +32 -55
  6. package/lib/api/options.js +1 -0
  7. package/lib/api/validate.js +5 -0
  8. package/lib/base/message-registry.js +104 -32
  9. package/lib/base/messages.js +277 -212
  10. package/lib/base/model.js +33 -22
  11. package/lib/base/optionProcessorHelper.js +9 -2
  12. package/lib/base/shuffle.js +50 -0
  13. package/lib/checks/actionsFunctions.js +37 -20
  14. package/lib/checks/foreignKeys.js +13 -6
  15. package/lib/checks/nonexpandableStructured.js +1 -2
  16. package/lib/checks/onConditions.js +21 -19
  17. package/lib/checks/parameters.js +1 -1
  18. package/lib/checks/queryNoDbArtifacts.js +2 -0
  19. package/lib/checks/types.js +16 -22
  20. package/lib/compiler/assert-consistency.js +31 -28
  21. package/lib/compiler/builtins.js +20 -4
  22. package/lib/compiler/checks.js +72 -63
  23. package/lib/compiler/define.js +396 -314
  24. package/lib/compiler/extend.js +55 -49
  25. package/lib/compiler/index.js +5 -0
  26. package/lib/compiler/populate.js +28 -11
  27. package/lib/compiler/propagator.js +2 -1
  28. package/lib/compiler/resolve.js +29 -20
  29. package/lib/compiler/shared.js +15 -10
  30. package/lib/compiler/utils.js +7 -7
  31. package/lib/edm/annotations/genericTranslation.js +51 -46
  32. package/lib/edm/annotations/preprocessAnnotations.js +39 -42
  33. package/lib/edm/csn2edm.js +69 -21
  34. package/lib/edm/edm.js +2 -2
  35. package/lib/edm/edmInboundChecks.js +6 -8
  36. package/lib/edm/edmPreprocessor.js +88 -80
  37. package/lib/edm/edmUtils.js +6 -15
  38. package/lib/gen/Dictionary.json +81 -13
  39. package/lib/gen/language.checksum +1 -1
  40. package/lib/gen/language.interp +2 -1
  41. package/lib/gen/languageParser.js +4680 -4484
  42. package/lib/inspect/inspectModelStatistics.js +2 -1
  43. package/lib/inspect/inspectPropagation.js +2 -1
  44. package/lib/json/from-csn.js +131 -78
  45. package/lib/json/to-csn.js +39 -23
  46. package/lib/language/antlrParser.js +0 -3
  47. package/lib/language/docCommentParser.js +7 -3
  48. package/lib/language/errorStrategy.js +3 -2
  49. package/lib/language/genericAntlrParser.js +96 -41
  50. package/lib/language/language.g4 +112 -128
  51. package/lib/language/multiLineStringParser.js +2 -1
  52. package/lib/main.d.ts +115 -2
  53. package/lib/main.js +16 -3
  54. package/lib/model/csnRefs.js +3 -3
  55. package/lib/model/csnUtils.js +109 -179
  56. package/lib/model/enrichCsn.js +13 -8
  57. package/lib/model/revealInternalProperties.js +4 -3
  58. package/lib/optionProcessor.js +23 -3
  59. package/lib/render/manageConstraints.js +11 -15
  60. package/lib/render/toCdl.js +144 -47
  61. package/lib/render/toHdbcds.js +22 -22
  62. package/lib/render/toRename.js +3 -4
  63. package/lib/render/toSql.js +29 -20
  64. package/lib/render/utils/delta.js +3 -1
  65. package/lib/render/utils/sql.js +3 -16
  66. package/lib/transform/db/associations.js +6 -6
  67. package/lib/transform/db/cdsPersistence.js +3 -3
  68. package/lib/transform/db/constraints.js +8 -8
  69. package/lib/transform/db/expansion.js +4 -4
  70. package/lib/transform/db/flattening.js +12 -15
  71. package/lib/transform/db/temporal.js +4 -3
  72. package/lib/transform/db/transformExists.js +2 -1
  73. package/lib/transform/draft/db.js +7 -7
  74. package/lib/transform/forOdataNew.js +15 -4
  75. package/lib/transform/forRelationalDB.js +53 -39
  76. package/lib/transform/odata/toFinalBaseType.js +106 -82
  77. package/lib/transform/odata/typesExposure.js +26 -17
  78. package/lib/transform/odata/utils.js +1 -1
  79. package/lib/transform/parseExpr.js +1 -1
  80. package/lib/transform/transformUtilsNew.js +33 -10
  81. package/lib/transform/translateAssocsToJoins.js +8 -7
  82. package/lib/transform/universalCsn/coreComputed.js +7 -5
  83. package/lib/transform/universalCsn/universalCsnEnricher.js +12 -4
  84. package/lib/utils/timetrace.js +2 -2
  85. package/package.json +1 -2
@@ -159,13 +159,6 @@ const knownVocabularies = Object.keys(vocabularyDefinitions);
159
159
  * dictReplacement: for test purposes, replaces the standard oDataDictionary
160
160
  */
161
161
  function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefined, options=undefined, messageFunctions=undefined) {
162
-
163
- if(!Edm)
164
- throw new Error('Please debug me: csn2annotationsEdm must be invoked with Edm');
165
- if(!options)
166
- throw new Error('Please debug me: csn2annotationsEdm must be invoked with options');
167
- if(!messageFunctions)
168
- throw new Error('Please debug me: csn2annotationsEdm must be invoked with messageFunctions');
169
162
  // global variable where we store all the generated annotations
170
163
  const g_annosArray = [];
171
164
 
@@ -223,7 +216,7 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
223
216
  const vocName = termName.slice(0, termName.indexOf('.'));
224
217
  if(vocabularyDefinitions[vocName])
225
218
  vocabularyDefinitions[vocName].used = true;
226
- else if(dictTerm?.$myServiceRoot &&
219
+ else if(dictTerm?.$myServiceRoot &&
227
220
  adhocDictionary.xrefs[dictTerm?.$myServiceRoot])
228
221
  adhocDictionary.xrefs[dictTerm.$myServiceRoot].used = true;
229
222
  if (dictTerm) {
@@ -407,7 +400,7 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
407
400
  function relParList() {
408
401
  // we rely on the order of params in the csn being the correct one
409
402
  const params = [];
410
- if (entityNameIfBound) {
403
+ if (entityNameIfBound && !(cAction.kind === 'function' && cAction.$hasBindingParam)) {
411
404
  params.push(cAction['@cds.odata.bindingparameter.collection'] ? 'Collection(' + entityNameIfBound + ')' : entityNameIfBound);
412
405
  }
413
406
  if (cAction.kind === 'function') {
@@ -457,7 +450,7 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
457
450
 
458
451
  // Filter unknown toplevel annotations
459
452
  // Final filtering of all annotations is done in handleTerm
460
-
453
+
461
454
  let knownAnnos = filterKnownAnnotations();
462
455
  if (knownAnnos.length === 0) return;
463
456
 
@@ -815,8 +808,8 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
815
808
  const fullTermName = voc + '.' + term;
816
809
 
817
810
  // msg is "semantic" location message used for messages
818
- const msg = {
819
- fullTermName,
811
+ const msg = {
812
+ fullTermName,
820
813
  stack: [],
821
814
  location: [ ...location, '@' + fullTermName ],
822
815
  };
@@ -832,7 +825,7 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
832
825
  const dictTerm = getDictTerm(termName, msg); // message for unknown term was already issued in handleTerm
833
826
  if(!addAnnotationFunc(anno, dictTerm && dictTerm.AppliesTo)) {
834
827
  if(dictTerm && dictTerm.AppliesTo) {
835
- message('odata-anno-def', location,
828
+ message('odata-anno-def', location,
836
829
  { anno: termName, rawvalues: dictTerm.AppliesTo, '#': 'notapplied' });
837
830
  }
838
831
  }
@@ -1268,33 +1261,39 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
1268
1261
  message('odata-anno-type', msg.location,
1269
1262
  { anno: msg.anno(), type: actualTypeName, '#': 'unknown' });
1270
1263
  // explicitly mentioned type, render in XML and JSON
1271
- newRecord.setEdmAttribute('Type', actualTypeName);
1272
- }
1273
- else if (isAbstractType(actualTypeName)) {
1274
- // this type is abstract
1275
- message('odata-anno-type', msg.location,
1276
- { anno: msg.anno(), type: actualTypeName, '#': 'abstract' });
1277
- if(dTypeName)
1278
- actualTypeName = dTypeName;
1279
- // set to definition name and render in XML and JSON
1280
- newRecord.setEdmAttribute('Type', actualTypeName);
1281
- }
1282
- else if (dTypeName && !isDerivedFrom(actualTypeName, dTypeName)) {
1283
- // this type doesn't fit the expected one
1284
- message('odata-anno-type', msg.location,
1285
- { anno: msg.anno(),
1286
- type: actualTypeName,
1287
- name: dTypeName,
1288
- code: '$Type',
1289
- '#': 'derived' });
1290
- actualTypeName = dTypeName;
1291
- // explicitly mentioned type, render in XML and JSON
1292
- newRecord.setEdmAttribute('Type', actualTypeName);
1264
+ newRecord.setXml({ 'Type': actualTypeName});
1265
+ // unknown dictionary type: can't fully qualify it
1266
+ newRecord.setJSON({ 'Type': actualTypeName});
1293
1267
  }
1294
1268
  else {
1295
- // ok
1269
+ if (isAbstractType(actualTypeName)) {
1270
+ // this type is abstract
1271
+ message('odata-anno-type', msg.location,
1272
+ { anno: msg.anno(), type: actualTypeName, code: '$Type', '#': 'abstract' });
1273
+ if(dTypeName)
1274
+ actualTypeName = dTypeName;
1275
+ }
1276
+ else if (dTypeName && !isDerivedFrom(actualTypeName, dTypeName)) {
1277
+ // this type doesn't fit the expected one
1278
+ message('odata-anno-type', msg.location,
1279
+ { anno: msg.anno(),
1280
+ type: actualTypeName,
1281
+ name: dTypeName,
1282
+ code: '$Type',
1283
+ '#': 'derived' });
1284
+ actualTypeName = dTypeName;
1285
+ }
1296
1286
  // Dictionary Type, render in XML only for backward compatibility
1297
1287
  newRecord.setXml( { Type: actualTypeName });
1288
+ const vocName = actualTypeName.slice(0, actualTypeName.indexOf('.'));
1289
+ const vocDef = vocabularyDefinitions[vocName];
1290
+ // Set full qualified type in JSON
1291
+ // TODO: Adhoc type x-ref URIs (only if abstract types are allowed in CDS)
1292
+ if(vocDef)
1293
+ newRecord.setJSON( { 'Type': `${vocDef.ref.Uri}#${actualTypeName}` });
1294
+ // don't add short actualTypeName into JSON as this would be wrong for a resolved! type.
1295
+ // A $Type w/o vocDef can only occur for adhoc type defs and these can't be abstract but
1296
+ // are fully resolvable due to their term usage via schema x-ref.
1298
1297
  }
1299
1298
  }
1300
1299
  else if (dTypeName) { // there is an expected type name according to dictionary
@@ -1308,7 +1307,7 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
1308
1307
  }
1309
1308
  if (isAbstractType(actualTypeName))
1310
1309
  message('odata-anno-type', msg.location,
1311
- { anno: msg.anno(), type: dTypeName, '#': 'abstract' });
1310
+ { anno: msg.anno(), type: dTypeName, code: '$Type', '#': 'abstract' });
1312
1311
 
1313
1312
  // Dictionary Type, render in XML only for backward compatibility
1314
1313
  newRecord.setXml( { Type: actualTypeName });
@@ -1458,7 +1457,11 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
1458
1457
  const props = Object.create(null);
1459
1458
  Object.entries(obj).forEach(([k, val]) => {
1460
1459
  if(k === '@type') {
1461
- edmNode.setEdmAttribute('Type', val);
1460
+ edmNode.setJSON({ 'Type': val});
1461
+ // try to shorten full qualified type URI to short type name
1462
+ const parts = val.split('#');
1463
+ const shortTypeName = parts[parts.length-1];
1464
+ edmNode.setXml({ Type: shortTypeName });
1462
1465
  }
1463
1466
  else {
1464
1467
  let child = undefined;
@@ -1554,14 +1557,16 @@ function csn2annotationEdm(reqDefs, csnVocabularies, serviceName, Edm = undefine
1554
1557
  }
1555
1558
  }
1556
1559
 
1557
- /*
1558
- translate vocabulary definitions into an adhoc dictionary
1559
- with the same structure as the gobal jsonDictionary that
1560
- contains all official term and type definitions.
1561
-
1562
- Return the dictionary and an array of schemas to which
1563
- the vocabulary definitions belong
1564
- */
1560
+ /**
1561
+ * translate vocabulary definitions into an adhoc dictionary
1562
+ * with the same structure as the global jsonDictionary that
1563
+ * contains all official term and type definitions.
1564
+ *
1565
+ * Return the dictionary and an array of schemas to which
1566
+ * the vocabulary definitions belong
1567
+ *
1568
+ * @returns [object, Array<object>]
1569
+ */
1565
1570
  function createAdhocDictionary() {
1566
1571
  const allKnownVocabularies = [];
1567
1572
  const dict = { terms: {}, types: {}, xrefs: {} };
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { makeMessageFunction } = require('../../base/messages.js');
4
- const { forEachDefinition } = require('../../model/csnUtils.js');
4
+ const { forEachDefinition, forEachGeneric } = require('../../model/csnUtils.js');
5
5
 
6
6
 
7
7
  /**************************************************************************************************
@@ -34,10 +34,10 @@ function preprocessAnnotations(csn, serviceName, options) {
34
34
  // return value can be null is target has no key
35
35
  function getKeyOfTargetOfManagedAssoc(anno, assoc) {
36
36
  // assoc.target can be the name of the target or the object itself
37
- let targetName = (typeof assoc.target === 'object') ? assoc.target.name : assoc.target;
38
- let target = (typeof assoc.target === 'object') ? assoc.target : csn.definitions[assoc.target];
37
+ const targetName = (typeof assoc.target === 'object') ? assoc.target.name : assoc.target;
38
+ const target = (typeof assoc.target === 'object') ? assoc.target : csn.definitions[assoc.target];
39
39
 
40
- let keyNames = Object.keys(target.elements).filter(x => target.elements[x].key && !target.elements[x].target);
40
+ const keyNames = Object.keys(target.elements).filter(x => target.elements[x].key && !target.elements[x].target);
41
41
  if (keyNames.length === 0) {
42
42
  keyNames.push('MISSING');
43
43
  message('odata-anno-preproc', null, { anno, name: targetName, '#': 'nokey' },
@@ -45,7 +45,7 @@ function preprocessAnnotations(csn, serviceName, options) {
45
45
  }
46
46
  else if (keyNames.length > 1)
47
47
  message('odata-anno-preproc', null, { anno, name: targetName, '#': 'multkeys' });
48
-
48
+
49
49
  return keyNames[0];
50
50
  }
51
51
 
@@ -56,48 +56,45 @@ function preprocessAnnotations(csn, serviceName, options) {
56
56
 
57
57
  // resolve shortcuts
58
58
  function resolveShortcuts() {
59
- let art = null;
60
-
61
59
  forEachDefinition(csn, (artifact, artifactName) => {
62
60
  const location = [ 'definitions', artifactName ];
63
61
  if(artifactName === serviceName || artifactName.startsWith(serviceName + '.')) {
64
- art = artifactName;
65
- handleAnnotations(artifactName, artifact, location);
62
+ handleAnnotations(artifactName, artifactName, artifact, location);
66
63
  artifact.elements && Object.entries(artifact.elements).forEach(([elementName, element]) => {
67
- handleAnnotations(elementName, element, [ ...location, 'elements', elementName ]);
64
+ handleAnnotations(artifactName, elementName, element, [ ...location, 'elements', elementName ]);
68
65
  });
69
- artifact.actions && Object.values(artifact.actions).forEach(action => {
66
+ forEachGeneric(artifact, 'actions', (action, actionName) => {
70
67
  action.params && Object.entries(action.params).forEach(([paramName, param]) => {
71
- handleAnnotations(paramName, param, [ ...location, 'actions', action, 'params', paramName ]);
68
+ handleAnnotations(actionName, paramName, param, [ ...location, 'actions', actionName, 'params', paramName ]);
72
69
  });
73
70
  });
74
71
  }
75
72
  });
76
73
 
77
- function handleAnnotations(carrierName, carrier, location) {
74
+ function handleAnnotations(defName, carrierName, carrier, location) {
78
75
 
79
76
  // collect the names of the carrier's annotation properties
80
- let annoNames = Object.keys(carrier).filter( x => x[0] === '@')
77
+ const annoNames = Object.keys(carrier).filter( x => x[0] === '@')
81
78
 
82
- for (let aName of annoNames) {
83
- let aNameWithoutQualifier = aName.split('#')[0];
79
+ annoNames.forEach(aName => {
80
+ const aNameWithoutQualifier = aName.split('#')[0];
84
81
 
85
82
  // Always - draft annotations, value is action name
86
83
  // - v2: prefix with entity name
87
84
  // - prefix with service name
88
- draftAnnotations(carrier, aName, aNameWithoutQualifier);
85
+ draftAnnotations(aName, aNameWithoutQualifier);
89
86
 
90
87
  // Always - FixedValueListShortcut
91
88
  // expand shortcut form of ValueList annotation
92
- fixedValueListShortCut(carrier, aNameWithoutQualifier);
89
+ fixedValueListShortCut(aNameWithoutQualifier);
93
90
 
94
91
  // Always - TextArrangementReordering
95
92
  // convert @Common.TextArrangement annotation that is on same level as Text annotation into a nested annotation
96
- textArrangementReordering(carrier, aName, aNameWithoutQualifier);
97
- }
93
+ textArrangementReordering(aName, aNameWithoutQualifier);
94
+ });
98
95
 
99
96
  // inner functions
100
- function draftAnnotations(carrier, aName, aNameWithoutQualifier) {
97
+ function draftAnnotations(aName, aNameWithoutQualifier) {
101
98
  if ((carrier.kind === 'entity') &&
102
99
  (aNameWithoutQualifier === '@Common.DraftRoot.PreparationAction' ||
103
100
  aNameWithoutQualifier === '@Common.DraftRoot.ActivationAction' ||
@@ -107,8 +104,9 @@ function preprocessAnnotations(csn, serviceName, options) {
107
104
  let value = carrier[aName];
108
105
  // prefix with service name, if not already done
109
106
  if (value === 'draftPrepare' || value === 'draftActivate' || value === 'draftEdit') {
110
- let serviceName = carrierName.replace(/.[^.]+$/, '');
111
- value = carrier[aName] = serviceName + '.' + value;
107
+ // mocha test has no whatsMySchemaName
108
+ const schemaName = options.whatsMySchemaName && options.whatsMySchemaName(carrierName) || serviceName;
109
+ value = carrier[aName] = schemaName + '.' + value;
112
110
  }
113
111
  // for v2: function imports live inside EntityContainer -> path needs to contain "EntityContainer/"
114
112
  // we decided to prefix names of bound action/functions with entity name -> needs to be reflected in path, too
@@ -119,7 +117,7 @@ function preprocessAnnotations(csn, serviceName, options) {
119
117
  }
120
118
  }
121
119
 
122
- function fixedValueListShortCut(carrier, anno) {
120
+ function fixedValueListShortCut(anno) {
123
121
  if (anno === '@Common.ValueList.entity' ||
124
122
  anno === '@Common.ValueList.viaAssociation') {
125
123
 
@@ -146,12 +144,12 @@ function preprocessAnnotations(csn, serviceName, options) {
146
144
 
147
145
  if (anno === '@Common.ValueList.viaAssociation') {
148
146
  // value is expected to be an expression, namely the path to an association of the carrier entity
149
- let assocName = carrier['@Common.ValueList.viaAssociation']['='];
147
+ const assocName = carrier['@Common.ValueList.viaAssociation']['='];
150
148
  if (!assocName) {
151
149
  message('odata-anno-preproc', [...location, anno], { anno, '#': 'viaassoc' });
152
150
  return false;
153
151
  }
154
- let assoc = csn.definitions[art].elements[assocName];
152
+ const assoc = csn.definitions[defName].elements[assocName];
155
153
  if (!assoc || !assoc.target) {
156
154
  message('odata-anno-preproc', [...location, anno], { anno, id: assocName, '#': 'noassoc' });
157
155
  return false;
@@ -164,26 +162,25 @@ function preprocessAnnotations(csn, serviceName, options) {
164
162
  // if both annotations are present, ignore 'entity' and raise a message
165
163
  if (annoNames.map(x=>x.split('#')[0]).find(x=>(x==='@Common.ValueList.viaAssociation'))) {
166
164
  message('odata-anno-preproc', [...location, anno],
167
- {
168
- name: '@Common.ValueList.entity', anno: '@Common.ValueList',
165
+ {
166
+ name: '@Common.ValueList.entity', anno: '@Common.ValueList',
169
167
  value: 'entity', code: 'viaAssociation', '#': 'vallistignored'
170
168
  });
171
169
  return false;
172
170
  }
173
171
 
174
- let annoVal = carrier['@Common.ValueList.entity']; // name of value list entity
172
+ const annoVal = carrier['@Common.ValueList.entity']; // name of value list entity
175
173
  if (annoVal['=']) {
176
174
  message('odata-anno-preproc', [...location, anno], { anno, '#': 'notastring' },
177
175
  );
178
176
  }
179
-
180
- let nameprefix = art.replace(/.[^.]+$/, ''); // better way of getting the service name?
181
-
177
+ // mocha test has no whatsMySchemaName
178
+ const schemaName = options.whatsMySchemaName && options.whatsMySchemaName(defName) || serviceName
182
179
  enameShort = annoVal['='] || annoVal;
183
- enameFull = nameprefix + '.' + enameShort;
180
+ enameFull = schemaName + '.' + enameShort;
184
181
  }
185
182
 
186
- let vlEntity = csn.definitions[enameFull]; // (object) value list entity
183
+ const vlEntity = csn.definitions[enameFull]; // (object) value list entity
187
184
  if (!vlEntity) {
188
185
  message('odata-anno-preproc', [...location, anno ], { anno, id: enameFull, '#': 'notexist' });
189
186
  return false;
@@ -191,7 +188,7 @@ function preprocessAnnotations(csn, serviceName, options) {
191
188
 
192
189
  // label
193
190
  // explicitly provided label wins
194
- let label = carrier['@Common.ValueList.Label'] ||
191
+ const label = carrier['@Common.ValueList.Label'] ||
195
192
  carrier['@Common.Label'] || vlEntity['@Common.Label'] || enameShort;
196
193
 
197
194
  // localDataProp
@@ -199,14 +196,14 @@ function preprocessAnnotations(csn, serviceName, options) {
199
196
  // if this is a managed assoc, use fk field instead (if there is a single one)
200
197
  let localDataProp = carrierName.split('/').pop();
201
198
  if (carrier.target && carrier.on === undefined) {
202
- localDataProp = localDataProp + fkSeparator +
199
+ localDataProp = localDataProp + fkSeparator +
203
200
  getKeyOfTargetOfManagedAssoc(anno, carrier);
204
201
  }
205
202
 
206
203
  // if this carrier is a generated foreign key field and the association is marked @cds.api.ignore
207
204
  // rename the localDataProp to be 'assocName/key'
208
205
  if(carrier['@cds.api.ignore']) {
209
- let assocName = carrier['@odata.foreignKey4'];
206
+ const assocName = carrier['@odata.foreignKey4'];
210
207
  if(assocName && options.isV4()) {
211
208
  localDataProp = localDataProp.replace(assocName+fkSeparator, assocName+'/');
212
209
  }
@@ -215,7 +212,7 @@ function preprocessAnnotations(csn, serviceName, options) {
215
212
  // valueListProp: the (single) key field of the value list entity
216
213
  // if no key or multiple keys -> message
217
214
  let valueListProp = null;
218
- let keys = Object.keys(vlEntity.elements).filter( x => vlEntity.elements[x].key && !vlEntity.elements[x].target );
215
+ const keys = Object.keys(vlEntity.elements).filter( x => vlEntity.elements[x].key && !vlEntity.elements[x].target );
219
216
  if (keys.length === 0) {
220
217
  message('odata-anno-preproc', [...location, anno], { anno, name: enameFull, '#': 'vhlnokey' });
221
218
  return false;
@@ -231,13 +228,13 @@ function preprocessAnnotations(csn, serviceName, options) {
231
228
  // OR
232
229
  // the (single) non-key string field, if there is one
233
230
  let textField = null;
234
- let Identification = vlEntity['@UI.Identification'];
231
+ const Identification = vlEntity['@UI.Identification'];
235
232
  if (Identification && Identification[0] && Identification[0]['=']) {
236
233
  textField = Identification[0]['='];
237
234
  } else if (Identification && Identification[0] && Identification[0]['Value'] && Identification[0]['Value']['=']) {
238
235
  textField = Identification[0]['Value']['='];
239
236
  } else {
240
- let stringFields = Object.keys(vlEntity.elements).filter(
237
+ const stringFields = Object.keys(vlEntity.elements).filter(
241
238
  x => !vlEntity.elements[x].key && vlEntity.elements[x].type === 'cds.String')
242
239
  if (stringFields.length === 1)
243
240
  textField = stringFields[0];
@@ -259,7 +256,7 @@ function preprocessAnnotations(csn, serviceName, options) {
259
256
  }
260
257
  }
261
258
 
262
- let newObj = Object.create( Object.getPrototypeOf(carrier) );
259
+ const newObj = Object.create( Object.getPrototypeOf(carrier) );
263
260
  Object.keys(carrier).forEach( e => {
264
261
  if (e === '@Common.ValueList.entity' || e === '@Common.ValueList.viaAssociation') {
265
262
  newObj['@Common.ValueList.Label'] = label;
@@ -289,7 +286,7 @@ function preprocessAnnotations(csn, serviceName, options) {
289
286
  }
290
287
  }
291
288
 
292
- function textArrangementReordering(carrier, aName, aNameWithoutQualifier) {
289
+ function textArrangementReordering(aName, aNameWithoutQualifier) {
293
290
  if (aNameWithoutQualifier === '@Common.TextArrangement') {
294
291
  let value = carrier[aName];
295
292
  let textAnno = carrier['@Common.Text'];
@@ -27,6 +27,7 @@ function csn2edm(_csn, serviceName, _options) {
27
27
  function csn2edmAll(_csn, _options, serviceNames=undefined) {
28
28
  // get us a fresh model copy that we can work with
29
29
  const csn = cloneCsnNonDict(_csn, _options);
30
+ const special$self = !csn?.definitions?.$self && '$self';
30
31
 
31
32
  // use original options for messages; cloned CSN for semantic location
32
33
  const messageFunctions = makeMessageFunction(csn, _options, 'to.edmx');
@@ -459,7 +460,7 @@ function csn2edmAll(_csn, _options, serviceNames=undefined) {
459
460
 
460
461
  if(!edmUtils.isODataSimpleIdentifier(p._edmAttributes.Name))
461
462
  message('odata-spec-violation-id', pLoc, { id: p._edmAttributes.Name });
462
- else if (options.isV2() && /^(_|[0-9])/.test(p._edmAttributes.Name)) {
463
+ else if (options.isV2() && /^(_|\d)/.test(p._edmAttributes.Name)) {
463
464
  // FIXME: Rewrite signalIllegalIdentifier function to be more flexible
464
465
  message('odata-spec-violation-id', pLoc,
465
466
  { prop: p._edmAttributes.Name[0], id: p._edmAttributes.Name, version: '2.0', '#': 'v2firstchar' });
@@ -662,19 +663,59 @@ function csn2edmAll(_csn, _options, serviceNames=undefined) {
662
663
  const actionNode = (iAmAnAction) ? new Edm.Action(v, attributes)
663
664
  : new Edm.FunctionDefinition(v, attributes);
664
665
 
666
+ const bpType = entityCsn ? fullQualified(entityCsn.name) : undefined;
667
+ /*
668
+ Check for binding $self parameter. If available, use this parameter
669
+ instead of artifically created binding parameter (hasBindingParameter).
670
+ The binding parameter remains in the CSN and is rendered as any other
671
+ parameter (including default value/not null/ etc) and acts as annotation carrier.
672
+ */
673
+ setProp(actionCsn, '$hasBindingParam', false);
674
+ if(actionCsn.params) {
675
+ const entries = Object.entries(actionCsn.params);
676
+ const firstParam = entries[0][1];
677
+ const type = firstParam?.items?.type || firstParam?.type;
678
+ if(type === special$self) {
679
+ actionCsn.$hasBindingParam = true;
680
+ const bpName = entries[0][0];
681
+ if(bpType) {
682
+ if(firstParam.items?.type)
683
+ firstParam.items.type = bpType;
684
+ if(firstParam.type)
685
+ firstParam.type = bpType;
686
+ }
687
+ if(!edmUtils.isODataSimpleIdentifier(bpName))
688
+ message('odata-spec-violation-id', [ ...loc, 'params', bpName ], { id: bpName });
689
+ }
690
+ }
691
+
665
692
  // bpName is eventually used later for EntitySetPath
666
693
  const bpNameAnno = actionCsn['@cds.odata.bindingparameter.name'];
667
- const bpName = bpNameAnno !== undefined ? (bpNameAnno['='] || bpNameAnno) : 'in';
668
-
694
+ let bpName = 'in';
695
+ if(bpNameAnno != null) {
696
+ if(typeof bpNameAnno === 'string')
697
+ bpName = bpNameAnno;
698
+ if(typeof bpNameAnno === 'object' && bpNameAnno['='])
699
+ bpName = bpNameAnno['='];
700
+ }
701
+ // No explicit binding parameter, check (user defined) annotation value)
702
+ if(!actionCsn.$hasBindingParam) {
703
+ if(!edmUtils.isODataSimpleIdentifier(bpName))
704
+ message('odata-spec-violation-id', [...loc, '@cds.odata.bindingparameter.name'], { id: bpName });
705
+ if(actionCsn.params && actionCsn.params[bpName]) {
706
+ error('duplicate-definition', [...loc, '@cds.odata.bindingparameter.name'], { '#': 'param', name: bpName });
707
+ }
708
+ }
669
709
  if(entityCsn != undefined)
670
710
  {
671
711
  actionNode.setEdmAttribute('IsBound', true);
672
- const bpType = fullQualified(entityCsn.name);
673
- // Binding Parameter: 'in' at first position in sequence, this is decisive!
674
- if(actionCsn['@cds.odata.bindingparameter.collection'])
675
- actionNode.append(new Edm.Parameter(v, { Name: bpName, Type: bpType, Collection:true } ));
676
- else
712
+ if(!actionCsn.$hasBindingParam) {
713
+ // Binding Parameter: 'in' at first position in sequence, this is decisive!
714
+ if(actionCsn['@cds.odata.bindingparameter.collection'])
715
+ actionNode.append(new Edm.Parameter(v, { Name: bpName, Type: bpType, Collection:true/*, Nullable: false*/ } ));
716
+ else
677
717
  actionNode.append(new Edm.Parameter(v, { Name: bpName, Type: bpType } ));
718
+ }
678
719
  }
679
720
  else if(EntityContainer)// unbound => produce Action/FunctionImport
680
721
  {
@@ -788,25 +829,33 @@ function csn2edmAll(_csn, _options, serviceNames=undefined) {
788
829
  // V2 XML: Parameters that are not explicitly marked as Nullable or NotNullable in the CSN must become Nullable=true
789
830
  // V2 XML spec does only mention default Nullable=true for Properties not for Parameters so omitting Nullable=true let
790
831
  // the client assume that Nullable is false.... Correct Nullable Handling is done inside Parameter constructor
791
- actionCsn.params && Object.entries(actionCsn.params).forEach(([parameterName, parameterCsn]) => {
792
- const pLoc = [...loc, 'params', parameterName];
793
- const param = new Edm.Parameter(v, { Name: parameterName }, parameterCsn, 'In' );
794
- collectUsedType(parameterCsn);
795
- edmTypeCompatibilityCheck(param, pLoc);
796
- if(!edmUtils.isODataSimpleIdentifier(parameterName))
797
- message('odata-spec-violation-id', pLoc, { id: parameterName });
832
+ actionCsn.params && Object.entries(actionCsn.params).forEach(([parameterName, parameterCsn], i) => {
833
+ const type = parameterCsn?.items?.type || parameterCsn?.type;
834
+ if(i === 0 && type === special$self) {
835
+ // skip and remove the first parameter if it is a $self binding parameter to
836
+ // omit annotation rendering later on
837
+ delete actionCsn.params[parameterName];
838
+ }
839
+ else {
840
+ const pLoc = [...loc, 'params', parameterName];
841
+ const param = new Edm.Parameter(v, { Name: parameterName }, parameterCsn, 'In' );
842
+ collectUsedType(parameterCsn);
843
+ edmTypeCompatibilityCheck(param, pLoc);
844
+ if(!edmUtils.isODataSimpleIdentifier(parameterName))
845
+ message('odata-spec-violation-id', pLoc, { id: parameterName });
798
846
 
799
847
  // only scalar or structured type in V2 (not entity)
800
- if(param._type &&
848
+ if(param._type &&
801
849
  !param._type.startsWith('Edm.') &&
802
850
  csn.definitions[param._type] &&
803
851
  !edmUtils.isStructuredType(csn.definitions[param._type]))
804
- message('odata-spec-violation-param', pLoc, { version: '2.0' });
852
+ message('odata-spec-violation-param', pLoc, { version: '2.0' });
805
853
 
806
- if(param._isCollection)
807
- message('odata-spec-violation-array', pLoc, { version: '2.0' });
854
+ if(param._isCollection)
855
+ message('odata-spec-violation-array', pLoc, { version: '2.0' });
808
856
 
809
- functionImport.append(param);
857
+ functionImport.append(param);
858
+ }
810
859
  });
811
860
 
812
861
  if(EntityContainer)
@@ -995,7 +1044,6 @@ function csn2edmAll(_csn, _options, serviceNames=undefined) {
995
1044
  }
996
1045
  edm._service._schemas[targetSchema]._annotations.push(anno);
997
1046
  });
998
- annos = [];
999
1047
 
1000
1048
  // create service cross reference and merge it into xServiceRefs
1001
1049
  xrefs.forEach(xr => {
package/lib/edm/edm.js CHANGED
@@ -1380,8 +1380,8 @@ function getEdm(options, messageFunctions) {
1380
1380
  {
1381
1381
  toJSONattributes(json)
1382
1382
  {
1383
- if(this._edmAttributes.Type)
1384
- json['@type'] = this._edmAttributes.Type;
1383
+ if(this._jsonOnlyAttributes.Type)
1384
+ json['@type'] = this._jsonOnlyAttributes.Type;
1385
1385
  let keys = Object.keys(this._edmAttributes).filter(k => k !== 'Type');
1386
1386
  for(const key of keys)
1387
1387
  json['$'+key] = this._edmAttributes[key];
@@ -1,13 +1,12 @@
1
1
  'use strict';
2
2
 
3
3
  const { setProp, isBetaEnabled } = require('../base/model');
4
- const {
5
- forEachDefinition, forEachMemberRecursively, getUtils,
4
+ const {
5
+ forEachDefinition, forEachMemberRecursively,
6
6
  } = require('../model/csnUtils');
7
7
 
8
8
  // eslint-disable-next-line no-unused-vars
9
- function resolveForeignKeyRefs(csn) {
10
- const csnUtils = getUtils(csn);
9
+ function resolveForeignKeyRefs(csn, csnUtils) {
11
10
  forEachDefinition(csn, (def, defName) => {
12
11
  let currPath = ['definitions', defName ];
13
12
  forEachMemberRecursively(def, (construct, _constructName, _prop, path) => {
@@ -21,9 +20,8 @@ function resolveForeignKeyRefs(csn) {
21
20
  }
22
21
 
23
22
 
24
- function inboundQualificationChecks(csn, options, messageFunctions,
25
- serviceRootNames, requestedServiceNames, isMyServiceRequested, whatsMyServiceRootName) {
26
- const csnUtils = getUtils(csn);
23
+ function inboundQualificationChecks(csn, options, messageFunctions,
24
+ serviceRootNames, requestedServiceNames, isMyServiceRequested, whatsMyServiceRootName, csnUtils) {
27
25
  const { message, throwWithError } = messageFunctions;
28
26
 
29
27
  forEachDefinition(csn, [ attach$path, checkChainedArray ]);
@@ -82,4 +80,4 @@ function inboundQualificationChecks(csn, options, messageFunctions,
82
80
  }
83
81
  }
84
82
 
85
- module.exports = { inboundQualificationChecks }
83
+ module.exports = { inboundQualificationChecks }