@sap/cds-compiler 2.12.0 → 2.15.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (128) hide show
  1. package/CHANGELOG.md +221 -15
  2. package/bin/cdsc.js +125 -50
  3. package/bin/cdsse.js +2 -2
  4. package/doc/CHANGELOG_BETA.md +13 -6
  5. package/doc/CHANGELOG_DEPRECATED.md +22 -6
  6. package/doc/NameResolution.md +21 -16
  7. package/lib/api/main.js +47 -84
  8. package/lib/api/options.js +5 -6
  9. package/lib/api/validate.js +6 -11
  10. package/lib/backends.js +15 -23
  11. package/lib/base/dictionaries.js +0 -8
  12. package/lib/base/error.js +26 -0
  13. package/lib/base/keywords.js +7 -17
  14. package/lib/base/location.js +9 -4
  15. package/lib/base/message-registry.js +114 -18
  16. package/lib/base/messages.js +101 -90
  17. package/lib/base/model.js +2 -63
  18. package/lib/base/optionProcessorHelper.js +177 -123
  19. package/lib/checks/annotationsOData.js +12 -33
  20. package/lib/checks/arrayOfs.js +1 -34
  21. package/lib/checks/cdsPersistence.js +2 -1
  22. package/lib/checks/enricher.js +17 -1
  23. package/lib/checks/invalidTarget.js +3 -1
  24. package/lib/checks/managedWithoutKeys.js +3 -1
  25. package/lib/checks/selectItems.js +4 -4
  26. package/lib/checks/sql-snippets.js +27 -26
  27. package/lib/checks/types.js +1 -1
  28. package/lib/checks/validator.js +6 -11
  29. package/lib/compiler/assert-consistency.js +6 -3
  30. package/lib/compiler/base.js +1 -0
  31. package/lib/compiler/builtins.js +19 -6
  32. package/lib/compiler/checks.js +23 -60
  33. package/lib/compiler/cycle-detector.js +1 -1
  34. package/lib/compiler/define.js +1151 -0
  35. package/lib/compiler/extend.js +1000 -0
  36. package/lib/compiler/finalize-parse-cdl.js +237 -0
  37. package/lib/compiler/index.js +107 -39
  38. package/lib/compiler/kick-start.js +190 -0
  39. package/lib/compiler/moduleLayers.js +4 -4
  40. package/lib/compiler/populate.js +1227 -0
  41. package/lib/compiler/propagator.js +114 -46
  42. package/lib/compiler/resolve.js +1521 -0
  43. package/lib/compiler/shared.js +126 -65
  44. package/lib/compiler/tweak-assocs.js +535 -0
  45. package/lib/compiler/utils.js +197 -33
  46. package/lib/edm/.eslintrc.json +5 -0
  47. package/lib/edm/annotations/genericTranslation.js +38 -24
  48. package/lib/edm/annotations/preprocessAnnotations.js +2 -2
  49. package/lib/edm/csn2edm.js +219 -100
  50. package/lib/edm/edm.js +302 -230
  51. package/lib/edm/edmPreprocessor.js +554 -419
  52. package/lib/edm/edmUtils.js +138 -44
  53. package/lib/gen/Dictionary.json +100 -19
  54. package/lib/gen/language.checksum +1 -1
  55. package/lib/gen/language.interp +11 -1
  56. package/lib/gen/language.tokens +86 -83
  57. package/lib/gen/languageLexer.interp +10 -1
  58. package/lib/gen/languageLexer.js +860 -833
  59. package/lib/gen/languageLexer.tokens +78 -75
  60. package/lib/gen/languageParser.js +5765 -4480
  61. package/lib/json/csnVersion.js +10 -11
  62. package/lib/json/from-csn.js +15 -3
  63. package/lib/json/to-csn.js +126 -68
  64. package/lib/language/docCommentParser.js +4 -4
  65. package/lib/language/genericAntlrParser.js +123 -5
  66. package/lib/language/language.g4 +355 -156
  67. package/lib/language/multiLineStringParser.js +5 -5
  68. package/lib/main.d.ts +486 -59
  69. package/lib/main.js +41 -9
  70. package/lib/model/api.js +3 -1
  71. package/lib/model/csnRefs.js +252 -156
  72. package/lib/model/csnUtils.js +384 -297
  73. package/lib/model/enrichCsn.js +71 -29
  74. package/lib/model/revealInternalProperties.js +29 -8
  75. package/lib/model/sortViews.js +2 -1
  76. package/lib/modelCompare/compare.js +23 -18
  77. package/lib/optionProcessor.js +63 -26
  78. package/lib/render/manageConstraints.js +35 -32
  79. package/lib/render/toCdl.js +897 -947
  80. package/lib/render/toHdbcds.js +205 -257
  81. package/lib/render/toSql.js +264 -225
  82. package/lib/render/utils/common.js +136 -25
  83. package/lib/render/utils/sql.js +4 -3
  84. package/lib/render/utils/stringEscapes.js +111 -0
  85. package/lib/sql-identifier.js +1 -1
  86. package/lib/transform/.eslintrc.json +5 -0
  87. package/lib/transform/db/.eslintrc.json +3 -1
  88. package/lib/transform/db/applyTransformations.js +35 -12
  89. package/lib/transform/db/assertUnique.js +1 -1
  90. package/lib/transform/db/associations.js +104 -306
  91. package/lib/transform/db/cdsPersistence.js +2 -2
  92. package/lib/transform/db/constraints.js +58 -53
  93. package/lib/transform/db/expansion.js +60 -33
  94. package/lib/transform/db/flattening.js +582 -104
  95. package/lib/transform/db/groupByOrderBy.js +3 -1
  96. package/lib/transform/db/transformExists.js +66 -13
  97. package/lib/transform/db/views.js +11 -7
  98. package/lib/transform/draft/.eslintrc.json +38 -0
  99. package/lib/transform/{db/draft.js → draft/db.js} +6 -5
  100. package/lib/transform/draft/odata.js +227 -0
  101. package/lib/transform/forHanaNew.js +109 -208
  102. package/lib/transform/forOdataNew.js +59 -212
  103. package/lib/transform/localized.js +46 -26
  104. package/lib/transform/odata/toFinalBaseType.js +85 -11
  105. package/lib/transform/odata/typesExposure.js +147 -199
  106. package/lib/transform/odata/utils.js +2 -2
  107. package/lib/transform/transformUtilsNew.js +44 -33
  108. package/lib/transform/translateAssocsToJoins.js +3 -20
  109. package/lib/transform/universalCsn/.eslintrc.json +36 -0
  110. package/lib/transform/universalCsn/coreComputed.js +172 -0
  111. package/lib/transform/universalCsn/universalCsnEnricher.js +737 -0
  112. package/lib/transform/universalCsn/utils.js +63 -0
  113. package/lib/utils/moduleResolve.js +13 -6
  114. package/lib/utils/objectUtils.js +30 -0
  115. package/package.json +1 -1
  116. package/share/messages/README.md +26 -0
  117. package/share/messages/message-explanations.json +2 -1
  118. package/share/messages/syntax-expected-integer.md +37 -0
  119. package/lib/compiler/definer.js +0 -2361
  120. package/lib/compiler/resolver.js +0 -3079
  121. package/lib/transform/odata/attachPath.js +0 -96
  122. package/lib/transform/odata/expandStructKeysInAssociations.js +0 -59
  123. package/lib/transform/odata/generateForeignKeyElements.js +0 -261
  124. package/lib/transform/odata/referenceFlattener.js +0 -290
  125. package/lib/transform/odata/sortByAssociationDependency.js +0 -105
  126. package/lib/transform/odata/structuralPath.js +0 -72
  127. package/lib/transform/odata/structureFlattener.js +0 -171
  128. package/lib/transform/universalCsnEnricher.js +0 -237
@@ -1,5 +1,7 @@
1
1
  'use strict';
2
2
 
3
+ const { ModelError } = require('../../base/error');
4
+
3
5
  /**
4
6
  * Replace (formerly) managed association in a GROUP BY/ORDER BY with its foreign keys.
5
7
  *
@@ -89,7 +91,7 @@ function replaceAssociationsInGroupByOrderBy(inputQuery, options, inspectRef, er
89
91
  function getForeignKeyRefs(assoc) {
90
92
  return assoc.keys.map((fk) => {
91
93
  if (!fk.$generatedFieldName)
92
- throw new Error(`Expecting generated field name for foreign key: ${JSON.stringify(fk)}`);
94
+ throw new ModelError(`Expecting generated field name for foreign key: ${JSON.stringify(fk)}`);
93
95
 
94
96
  return { ref: [ fk.$generatedFieldName ] };
95
97
  });
@@ -4,6 +4,7 @@ const { forAllQueries, forEachDefinition, walkCsnPath } = require('../../model/c
4
4
  const { setProp } = require('../../base/model');
5
5
  const { getRealName } = require('../../render/utils/common');
6
6
  const { csnRefs } = require('../../model/csnRefs');
7
+ const { ModelError } = require('../../base/error');
7
8
 
8
9
  /**
9
10
  * Turn a `exists assoc[filter = 100]` into a `exists (select 1 as dummy from assoc.target where <assoc on condition> and assoc.target.filter = 100)`.
@@ -48,7 +49,7 @@ const { csnRefs } = require('../../model/csnRefs');
48
49
  * @param {Function} error
49
50
  */
50
51
  function handleExists(csn, options, error) {
51
- const { inspectRef } = csnRefs(csn);
52
+ let { inspectRef } = csnRefs(csn);
52
53
  const generatedExists = new WeakMap();
53
54
  forEachDefinition(csn, (artifact, artifactName) => {
54
55
  if (artifact.projection) // do the same hack we do for the other stuff...
@@ -81,6 +82,8 @@ function handleExists(csn, options, error) {
81
82
  // to check for further exists
82
83
  const { result, leftovers } = processExists(queryPath, exprPath);
83
84
  walkCsnPath(csn, exprPath.slice(0, -1))[exprPath[exprPath.length - 1]] = result;
85
+ if (leftovers.length > 0)
86
+ inspectRef = csnRefs(csn).inspectRef; // Refresh caches - we need to resolve stuff in the newly created subquery
84
87
  toProcess.push(...leftovers.reverse()); // any leftovers - schedule for further processing
85
88
  }
86
89
  }
@@ -107,8 +110,8 @@ function handleExists(csn, options, error) {
107
110
  sources[join.as] = join.as;
108
111
  }
109
112
  else if (join.args) {
110
- const subsources = getJoinSources(join.args);
111
- sources = Object.assign(sources, subsources);
113
+ const subSources = getJoinSources(join.args);
114
+ sources = Object.assign(sources, subSources);
112
115
  }
113
116
  else if (join.ref) {
114
117
  sources[join.ref[join.ref.length - 1]] = join.ref[join.ref.length - 1];
@@ -193,7 +196,7 @@ function handleExists(csn, options, error) {
193
196
  const stack = [ [ null, startAssoc, startRest, startIndex ] ];
194
197
  const { links } = inspectRef(path);
195
198
  while (stack.length > 0) {
196
- // previous: to nest "up" if the previous assoc did not originaly have a filter
199
+ // previous: to nest "up" if the previous assoc did not originally have a filter
197
200
  // assoc: the assoc path step
198
201
  // rest: path steps after assoc
199
202
  // index: index of after-assoc in the overall ref-array - so we know where to start looking for the next assoc
@@ -408,14 +411,14 @@ function handleExists(csn, options, error) {
408
411
  * Translate an `EXISTS <managed assoc>` into a part of a WHERE condition.
409
412
  *
410
413
  * For each of the foreign keys, do:
411
- * + build the target side by prefixing `target` infront of the ref
414
+ * + build the target side by prefixing `target` in front of the ref
412
415
  * + build the source side by prefixing `base` (if not already part of `current`)
413
- * and the assoc name itself (current) infront of the ref
416
+ * and the assoc name itself (current) in front of the ref
414
417
  * + Compare source and target with `=`
415
418
  *
416
419
  * If there is more than one foreign key, join with `and`.
417
420
  *
418
- * The new tokens are immediatly added to the WHERE of the subselect
421
+ * The new tokens are immediately added to the WHERE of the subselect
419
422
  *
420
423
  * @param {CSN.Element} root
421
424
  * @param {string} target
@@ -496,11 +499,16 @@ function handleExists(csn, options, error) {
496
499
  whereExtension.push({ ref: [ target, ...part.ref.slice(1) ] });
497
500
  }
498
501
  else if (part.$scope === '$self') { // source side - "absolute" scope
499
- // cut off the $self, as we prefix the entity name now
500
- whereExtension.push({ ref: [ base, ...part.ref.slice(1) ] });
502
+ const column = part._art._column;
503
+ if (column && column.as) { // Replace with the "original" expression (the .ref, .xpr etc.)
504
+ whereExtension.push(translateToSourceSide(column));
505
+ }
506
+ else {
507
+ whereExtension.push(assignAndDeleteAs({}, part, { ref: [ base, ...part.ref.slice(1) ] }));
508
+ }
501
509
  }
502
510
  else if (art) { // source side - with local scope
503
- if (isPrefixedWithTableAlias)
511
+ if (isPrefixedWithTableAlias || part.$scope === 'alias')
504
512
  whereExtension.push({ ref: [ ...current.ref.slice(0, -1), ...part.ref ] });
505
513
  else
506
514
  whereExtension.push({ ref: [ base, ...current.ref.slice(0, -1), ...part.ref ] });
@@ -512,6 +520,51 @@ function handleExists(csn, options, error) {
512
520
 
513
521
  return whereExtension;
514
522
 
523
+
524
+ /**
525
+ * Run Object.assign on all of the passed in parameters and delete a .as at the end
526
+ *
527
+ * @param {...any} args
528
+ * @returns {object} The merged args without an .as property
529
+ */
530
+ function assignAndDeleteAs(...args) {
531
+ const obj = Object.assign.apply(null, args);
532
+ delete obj.as;
533
+ return obj;
534
+ }
535
+ /**
536
+ * Translate the given obj (a column-like thing) into an expression that we can use in the WHERE.
537
+ * - Strip off $self/$projection and correctly replace with source expression
538
+ * - Drill further down into .xpr
539
+ * - Correctly set table alias in front of ref
540
+ *
541
+ * @param {object} obj
542
+ * @returns {object}
543
+ */
544
+ function translateToSourceSide(obj) {
545
+ if (obj.ref) {
546
+ if (obj.$scope === '$self') { // TODO: Check with this way down, do we keep the links?
547
+ const column = obj._art._column;
548
+ if (column && column.as)
549
+ return translateToSourceSide(column);
550
+ return assignAndDeleteAs({}, obj, { ref: [ base, ...obj.ref.slice(1) ] });
551
+ }
552
+ else if (typeof obj.$env === 'string') {
553
+ return assignAndDeleteAs({}, obj, { ref: [ obj.$env, ...obj.ref ] });
554
+ }
555
+
556
+ return assignAndDeleteAs({}, obj, { ref: [ ...obj.ref ] });
557
+ }
558
+ else if (obj.xpr) { // we need to drill further down into .xpr
559
+ return assignAndDeleteAs({}, obj, { xpr: obj.xpr.map(translateToSourceSide) });
560
+ }
561
+ else if (obj.args) {
562
+ return assignAndDeleteAs({}, obj, { args: obj.args.map(translateToSourceSide) });
563
+ }
564
+
565
+ return obj;
566
+ }
567
+
515
568
  /**
516
569
  * Check that an expression triple is a valid $self
517
570
  *
@@ -584,7 +637,7 @@ function handleExists(csn, options, error) {
584
637
  }
585
638
 
586
639
  /**
587
- * Check (using inspectRef -> links), wether the first path step is an entity or query source
640
+ * Check (using inspectRef -> links), whether the first path step is an entity or query source
588
641
  *
589
642
  * @param {CSN.Path} path
590
643
  * @returns {boolean}
@@ -634,7 +687,7 @@ function handleExists(csn, options, error) {
634
687
  return error(null, xpr.$path, 'Boolean $env is not handled yet - please report this error!');
635
688
  }
636
689
  else if (xpr.ref) {
637
- throw new Error('Missing $env and missing leading artifact ref - throwing to trigger recompilation!');
690
+ throw new ModelError('Missing $env and missing leading artifact ref - throwing to trigger recompilation!');
638
691
  }
639
692
  }
640
693
 
@@ -699,7 +752,7 @@ function handleExists(csn, options, error) {
699
752
  *
700
753
  * @param {string} target
701
754
  * @param {TokenStream} where
702
- * @returns {TokenStream} The input-where with the refs "absolutified"
755
+ * @returns {TokenStream} The input-where with the refs transformed to absolute ones
703
756
  */
704
757
  function remapExistingWhere(target, where) {
705
758
  return where.map((part) => {
@@ -1,10 +1,11 @@
1
1
  'use strict';
2
2
 
3
3
  const {
4
- getUtils, cloneCsn, applyTransformationsOnNonDictionary,
4
+ getUtils, cloneCsnNonDict, applyTransformationsOnNonDictionary,
5
5
  } = require('../../model/csnUtils');
6
6
  const { implicitAs, csnRefs } = require('../../model/csnRefs');
7
7
  const { isBetaEnabled } = require('../../base/model');
8
+ const { ModelError } = require('../../base/error');
8
9
 
9
10
  /**
10
11
  * If a mixin association is published, return the mixin association.
@@ -38,7 +39,7 @@ function getMixinAssocOfQueryIfPublished(query, association, associationName) {
38
39
  }
39
40
 
40
41
  /**
41
- * Check wether the given artifact uses the given mixin association.
42
+ * Check whether the given artifact uses the given mixin association.
42
43
  *
43
44
  * We can rely on the fact that there can be no usage starting with $self/$projection,
44
45
  * since lib/checks/selectItems.js forbids that.
@@ -182,10 +183,10 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
182
183
  const matchingCombined = $combined[elemName];
183
184
  // Internal errors - this should never happen!
184
185
  if (matchingCombined.length > 1) { // should already be caught by compiler
185
- throw new Error(`Ambiguous name - can't be resolved: ${elemName}. Found in: ${matchingCombined.map(o => o.parent)}`);
186
+ throw new ModelError(`Ambiguous name - can't be resolved: ${elemName}. Found in: ${matchingCombined.map(o => o.parent)}`);
186
187
  }
187
188
  else if (matchingCombined.length === 0) { // no clue how this could happen? Invalid CSN?
188
- throw new Error(`No matching entry found in UNION of all elements for: ${elemName}`);
189
+ throw new ModelError(`No matching entry found in UNION of all elements for: ${elemName}`);
189
190
  }
190
191
  alias = matchingCombined[0].parent;
191
192
  }
@@ -208,7 +209,7 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
208
209
  const assocCol = columnMap[elemName];
209
210
  if (assocCol && assocCol.ref) {
210
211
  elem.keys.forEach((foreignKey) => {
211
- const ref = cloneCsn(assocCol.ref, options);
212
+ const ref = cloneCsnNonDict(assocCol.ref, options);
212
213
  ref[ref.length - 1] = [ getLastRefStepString(ref) ].concat(foreignKey.as).join(pathDelimiter);
213
214
  const result = {
214
215
  ref,
@@ -276,7 +277,7 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
276
277
 
277
278
  // Copy the association element to the MIXIN clause under its alias name
278
279
  // Needs to be a deep copy, as we transform the on-condition
279
- const mixinElem = cloneCsn(elem, options);
280
+ const mixinElem = cloneCsnNonDict(elem, options);
280
281
  // Perform common transformations on the newly generated MIXIN element (won't be reached otherwise)
281
282
  transformCommon(mixinElem, mixinElemName);
282
283
 
@@ -304,7 +305,7 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
304
305
  parent.ref = ref;
305
306
  return ref;
306
307
  },
307
- }, elementsPath.concat(elemName));
308
+ }, {}, elementsPath.concat(elemName));
308
309
  }
309
310
 
310
311
  if (!mixinElem._ignore)
@@ -456,6 +457,9 @@ function checkIsNotMixinByItself(query, columnMap, elementName) {
456
457
  if (query && query.SELECT && query.SELECT.mixin) {
457
458
  const col = columnMap[elementName];
458
459
 
460
+ if (!col.ref) // No ref -> new association, but not a mixin.
461
+ return true;
462
+
459
463
  // Use getLastRefStepString - with hdbcds.hdbcds and malicious CSN input we might have .id
460
464
  const realName = getLastRefStepString(col.ref);
461
465
  // If the element is not part of the mixin => True
@@ -0,0 +1,38 @@
1
+ {
2
+ "root": true,
3
+ "plugins": ["sonarjs", "jsdoc"],
4
+ "extends": ["../../../.eslintrc-ydkjsi.json", "plugin:sonarjs/recommended", "plugin:jsdoc/recommended"],
5
+ "rules": {
6
+ "prefer-const": "error",
7
+ "quotes": ["error", "single", "avoid-escape"],
8
+ "prefer-template": "error",
9
+ "no-trailing-spaces": "error",
10
+ "template-curly-spacing":["error", "never"],
11
+ "complexity": ["warn", 30],
12
+ "max-len": "off",
13
+ // Don't enforce stupid descriptions
14
+ "jsdoc/require-param-description": "off",
15
+ "jsdoc/require-returns-description": "off",
16
+ // Sometimes if-else's are more specific
17
+ "sonarjs/prefer-single-boolean-return": "off",
18
+ // Very whiny and nitpicky
19
+ "sonarjs/cognitive-complexity": "off",
20
+ // Does not recognize TS types
21
+ "jsdoc/no-undefined-types": "off",
22
+ // Whiny and annoying
23
+ "sonarjs/no-duplicate-string": "off"
24
+ },
25
+ "parserOptions": {
26
+ "ecmaVersion": 2018,
27
+ "sourceType": "script"
28
+ },
29
+ "env": {
30
+ "es6": true,
31
+ "node": true
32
+ },
33
+ "settings": {
34
+ "jsdoc": {
35
+ "mode": "typescript"
36
+ }
37
+ }
38
+ }
@@ -6,6 +6,7 @@ const {
6
6
  } = require('../../model/csnUtils');
7
7
  const { setProp, isDeprecatedEnabled } = require('../../base/model');
8
8
  const { getTransformers } = require('../transformUtilsNew');
9
+ const { ModelError } = require('../../base/error');
9
10
  const draftAnnotation = '@odata.draft.enabled';
10
11
  const booleanBuiltin = 'cds.Boolean';
11
12
 
@@ -38,7 +39,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
38
39
  * @param {string} artifactName
39
40
  */
40
41
  function generateDraft(artifact, artifactName) {
41
- if ((artifact.kind === 'entity' || artifact.kind === 'view') &&
42
+ if ((artifact.kind === 'entity') &&
42
43
  hasAnnotationValue(artifact, draftAnnotation) &&
43
44
  isPartOfService(artifactName)) {
44
45
  // Determine the set of target draft nodes belonging to this draft root (the draft root
@@ -79,7 +80,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
79
80
  const draftNodeName = elem.target;
80
81
  // Sanity check
81
82
  if (!draftNode)
82
- throw new Error(`Expecting target to be resolved: ${JSON.stringify(elem, null, 2)}`);
83
+ throw new ModelError(`Expecting target to be resolved: ${JSON.stringify(elem, null, 2)}`);
83
84
 
84
85
  // Ignore composition if not part of a service
85
86
  if (!isPartOfService(draftNodeName)) {
@@ -89,7 +90,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
89
90
  }
90
91
  // Barf if a draft node other than the root has @odata.draft.enabled itself
91
92
  if (draftNode !== rootArtifact && hasAnnotationValue(draftNode, draftAnnotation)) {
92
- error(null, [ 'definitions', artifactName, 'elements', elemName ], 'Composition in draft-enabled entity can\'t lead to another entity with “@odata.draft.enabled');
93
+ error('ref-unexpected-draft-enabled', [ 'definitions', artifactName, 'elements', elemName ], { anno: '@odata.draft.enabled' });
93
94
  delete draftNodes[draftNodeName];
94
95
  continue;
95
96
  }
@@ -110,7 +111,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
110
111
  function generateDraftForHana(artifact, artifactName, draftRootName) {
111
112
  // Sanity check
112
113
  if (!isPartOfService(artifactName))
113
- throw new Error(`Expecting artifact to be part of a service: ${JSON.stringify(artifact)}`);
114
+ throw new ModelError(`Expecting artifact to be part of a service: ${JSON.stringify(artifact)}`);
114
115
 
115
116
 
116
117
  // The name of the draft shadow entity we should generate
@@ -312,7 +313,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
312
313
  function getDraftShadowEntityFor(draftNode, draftNodeName) {
313
314
  // Sanity check
314
315
  if (!draftNodes[draftNodeName])
315
- throw new Error(`Not a draft node: ${draftNodeName}`);
316
+ throw new ModelError(`Not a draft node: ${draftNodeName}`);
316
317
 
317
318
  return { shadowTarget: csn.definitions[`${draftNodeName}${draftSuffix}`], shadowTargetName: `${draftNodeName}${draftSuffix}` };
318
319
  }
@@ -0,0 +1,227 @@
1
+ 'use strict';
2
+
3
+ const { forEachDefinition, getUtils, getServiceNames } = require('../../model/csnUtils');
4
+ const { forEach } = require('../../utils/objectUtils');
5
+ const { isArtifactInSomeService, getServiceOfArtifact } = require('../odata/utils');
6
+ const { getTransformers } = require('../transformUtilsNew');
7
+ const { ModelError } = require('../../base/error');
8
+ const { makeMessageFunction } = require('../../base/messages');
9
+
10
+ /**
11
+ * - Generate artificial draft fields if requested
12
+ *
13
+ * - Check associations for:
14
+ * - exposed associations do not point to non-exposed targets
15
+ * - structured types must not contain associations for OData V2
16
+ * - Element must not be an 'array of' for OData V2 TODO: move to the validator
17
+ * - Perform checks for exposed non-abstract entities and views - check media type and key-ness
18
+ *
19
+ * @param {CSN.Model} csn
20
+ * @param {CSN.Options} options
21
+ * @param {Array} [services] Will be calculated JIT if not provided
22
+ * @returns {CSN.Model} Returns the transformed input model
23
+ * @todo should be done by the compiler - Check associations for valid foreign keys
24
+ * @todo check if needed at all: Remove '$projection' from paths in the element's ON-condition
25
+ */
26
+ function generateDrafts(csn, options, services) {
27
+ const {
28
+ createForeignKeyElement,
29
+ createAndAddDraftAdminDataProjection, createScalarElement,
30
+ createAssociationElement, createAssociationPathComparison,
31
+ addElement, createAction, assignAction,
32
+ resetAnnotation,
33
+ } = getTransformers(csn, options);
34
+
35
+ const {
36
+ getFinalType,
37
+ getServiceName,
38
+ hasAnnotationValue,
39
+ getFinalBaseType,
40
+ getFinalTypeDef,
41
+ } = getUtils(csn);
42
+
43
+ const { error, info } = makeMessageFunction(csn, options, 'for.odata');
44
+
45
+ if (!services)
46
+ services = getServiceNames(csn);
47
+
48
+ const visitedArtifacts = Object.create(null);
49
+ // @ts-ignore
50
+ const externalServices = services.filter(serviceName => csn.definitions[serviceName]['@cds.external']);
51
+ // @ts-ignore
52
+ const isExternalServiceMember = (_art, name) => externalServices.includes(getServiceName(name));
53
+
54
+ forEachDefinition(csn, (def, defName) => {
55
+ // Generate artificial draft fields for entities/views if requested, ignore if not part of a service
56
+ if (def.kind === 'entity' && def['@odata.draft.enabled'] && isArtifactInSomeService(defName, services))
57
+ generateDraftForOdata(def, defName, def);
58
+
59
+ visitedArtifacts[defName] = true;
60
+ }, { skipArtifact: isExternalServiceMember });
61
+
62
+ return csn;
63
+ /**
64
+ * Generate all that is required in ODATA for draft enablement of 'artifact' into the artifact,
65
+ * into its transitively reachable composition targets, and into the model.
66
+ * 'rootArtifact' is the root artifact where composition traversal started.
67
+ *
68
+ * Constraints
69
+ * Draft Root: Exactly one PK of type UUID
70
+ * Draft Node: One PK of type UUID + 0..1 PK of another type
71
+ * Draft Node: Must not be reachable from multiple draft roots
72
+ *
73
+ * @param {CSN.Artifact} artifact
74
+ * @param {string} artifactName
75
+ * @param {CSN.Artifact} rootArtifact artifact where composition traversal started
76
+ */
77
+ function generateDraftForOdata(artifact, artifactName, rootArtifact) {
78
+ // Sanity check
79
+ // @ts-ignore
80
+ if (!isArtifactInSomeService(artifactName, services))
81
+ throw new ModelError(`Expecting artifact to be part of a service: ${JSON.stringify(artifact)}`);
82
+
83
+ // Nothing to do if already draft-enabled (composition traversal may have circles)
84
+ if ((artifact['@Common.DraftRoot.PreparationAction'] || artifact['@Common.DraftNode.PreparationAction']) &&
85
+ artifact.actions && artifact.actions.draftPrepare)
86
+ return;
87
+
88
+
89
+ // Generate the DraftAdministrativeData projection into the service, unless there is already one
90
+ // @ts-ignore
91
+ const draftAdminDataProjectionName = `${getServiceOfArtifact(artifactName, services)}.DraftAdministrativeData`;
92
+ let draftAdminDataProjection = csn.definitions[draftAdminDataProjectionName];
93
+ if (!draftAdminDataProjection) {
94
+ // @ts-ignore
95
+ draftAdminDataProjection = createAndAddDraftAdminDataProjection(getServiceOfArtifact(artifactName, services));
96
+ }
97
+ // Report an error if it is not an entity or not what we expect
98
+ if (draftAdminDataProjection.kind !== 'entity' || !draftAdminDataProjection.elements.DraftUUID) {
99
+ error(null, [ 'definitions', draftAdminDataProjectionName ], { name: draftAdminDataProjectionName },
100
+ 'Generated entity $(NAME) conflicts with existing artifact');
101
+ }
102
+ // Generate the annotations describing the draft actions (only draft roots can be activated/edited)
103
+ if (artifact === rootArtifact) {
104
+ resetAnnotation(artifact, '@Common.DraftRoot.ActivationAction', 'draftActivate', info, [ 'definitions', draftAdminDataProjectionName ]);
105
+ resetAnnotation(artifact, '@Common.DraftRoot.EditAction', 'draftEdit', info, [ 'definitions', draftAdminDataProjectionName ]);
106
+ resetAnnotation(artifact, '@Common.DraftRoot.PreparationAction', 'draftPrepare', info, [ 'definitions', draftAdminDataProjectionName ]);
107
+ }
108
+ else {
109
+ resetAnnotation(artifact, '@Common.DraftNode.PreparationAction', 'draftPrepare', info, [ 'definitions', draftAdminDataProjectionName ]);
110
+ }
111
+
112
+ Object.values(artifact.elements || {}).forEach( (elem) => {
113
+ // Make all non-key elements nullable
114
+ if (elem.notNull && elem.key !== true)
115
+ delete elem.notNull;
116
+ });
117
+ // Generate the additional elements into the draft-enabled artifact
118
+
119
+ // key IsActiveEntity : Boolean default true
120
+ const isActiveEntity = createScalarElement('IsActiveEntity', 'cds.Boolean', true, true, false);
121
+ isActiveEntity.IsActiveEntity['@UI.Hidden'] = true;
122
+ addElement(isActiveEntity, artifact, artifactName);
123
+
124
+ // HasActiveEntity : Boolean default false
125
+ const hasActiveEntity = createScalarElement('HasActiveEntity', 'cds.Boolean', false, false, true);
126
+ hasActiveEntity.HasActiveEntity['@UI.Hidden'] = true;
127
+ addElement(hasActiveEntity, artifact, artifactName);
128
+
129
+ // HasDraftEntity : Boolean default false;
130
+ const hasDraftEntity = createScalarElement('HasDraftEntity', 'cds.Boolean', false, false, true);
131
+ hasDraftEntity.HasDraftEntity['@UI.Hidden'] = true;
132
+ addElement(hasDraftEntity, artifact, artifactName);
133
+
134
+ // @odata.contained: true
135
+ // DraftAdministrativeData : Association to one DraftAdministrativeData;
136
+ const draftAdministrativeData = createAssociationElement('DraftAdministrativeData', draftAdminDataProjectionName, true);
137
+ draftAdministrativeData.DraftAdministrativeData.cardinality = { max: 1 };
138
+ draftAdministrativeData.DraftAdministrativeData['@odata.contained'] = true;
139
+ draftAdministrativeData.DraftAdministrativeData['@UI.Hidden'] = true;
140
+ addElement(draftAdministrativeData, artifact, artifactName);
141
+
142
+ // Note that we need to do the ODATA transformation steps for managed associations
143
+ // (foreign key field generation, generatedFieldName) by hand, because the corresponding
144
+ // transformation steps have already been done on all artifacts when we come here)
145
+ let uuidDraftKey = draftAdministrativeData.DraftAdministrativeData.keys.filter(key => key.ref && key.ref.length === 1 && key.ref[0] === 'DraftUUID');
146
+ if (uuidDraftKey && uuidDraftKey[0]) {
147
+ uuidDraftKey = uuidDraftKey[0]; // filter returns an array, but it has only one element
148
+ const path = [ 'definitions', artifactName, 'elements', 'DraftAdministrativeData', 'keys', 0 ];
149
+ createForeignKeyElement(draftAdministrativeData.DraftAdministrativeData, 'DraftAdministrativeData', uuidDraftKey, artifact, artifactName, path);
150
+ }
151
+ // SiblingEntity : Association to one <artifact> on (... IsActiveEntity unequal, all other key fields equal ...)
152
+ const siblingEntity = createAssociationElement('SiblingEntity', artifactName, false);
153
+ siblingEntity.SiblingEntity.cardinality = { max: 1 };
154
+ addElement(siblingEntity, artifact, artifactName);
155
+ // ... on SiblingEntity.IsActiveEntity != IsActiveEntity ...
156
+ siblingEntity.SiblingEntity.on = createAssociationPathComparison('SiblingEntity', 'IsActiveEntity', '!=', 'IsActiveEntity');
157
+
158
+ // Iterate elements
159
+ // TODO: Iterative vs recursive? What is more likely: Super deep nesting or cycles via malicious CSN?
160
+ if (artifact.elements) {
161
+ // No need to reverse the stack, not order dependent
162
+ const stack = [ artifact ];
163
+ while (stack.length > 0) {
164
+ const { elements } = stack.pop();
165
+ forEach(elements, (elemName, elem) => {
166
+ if (elemName !== 'IsActiveEntity' && elem.key) {
167
+ // Amend the ON-condition above:
168
+ // ... and SiblingEntity.<keyfield> = <keyfield> ... (for all key fields except 'IsActiveEntity')
169
+ const cond = createAssociationPathComparison('SiblingEntity', elemName, '=', elemName);
170
+ cond.push('and');
171
+ cond.push(...siblingEntity.SiblingEntity.on);
172
+ siblingEntity.SiblingEntity.on = cond;
173
+ }
174
+
175
+ // Draft-enable the targets of composition elements (draft nodes), too
176
+ // TODO rewrite
177
+ if (elem.target && elem.type && getFinalType(elem.type) === 'cds.Composition') {
178
+ const draftNode = csn.definitions[elem.target];
179
+
180
+ // Ignore if that is our own draft root
181
+ if (draftNode !== rootArtifact) {
182
+ // Report error when the draft node has @odata.draft.enabled itself
183
+ if (hasAnnotationValue(draftNode, '@odata.draft.enabled', true)) {
184
+ error('ref-unexpected-draft-enabled', [ 'definitions', artifactName, 'elements', elemName ], { anno: '@odata.draft.enabled' });
185
+ }
186
+ // Ignore composition if it's target is not part of a service or explicitly draft disabled
187
+ else if (!getServiceName(elem.target) || hasAnnotationValue(draftNode, '@odata.draft.enabled', false)) {
188
+ return;
189
+ }
190
+ else {
191
+ // Generate draft stuff into the target
192
+ generateDraftForOdata(draftNode, elem.target, rootArtifact);
193
+ }
194
+ }
195
+ }
196
+ else if (elem.elements) { // anonymous structure
197
+ stack.push(elem);
198
+ }
199
+ else if (elem.type) { // types - possibly structured
200
+ const typeDef = elem.type.ref ? getFinalBaseType(elem.type) : getFinalTypeDef(elem.type);
201
+ if (typeDef.elements)
202
+ stack.push(typeDef);
203
+ }
204
+ });
205
+ }
206
+ }
207
+
208
+
209
+ // Generate the actions into the draft-enabled artifact (only draft roots can be activated/edited)
210
+
211
+ // action draftPrepare (SideEffectsQualifier: String) return <artifact>;
212
+ const draftPrepare = createAction('draftPrepare', artifactName, 'SideEffectsQualifier', 'cds.String');
213
+ assignAction(draftPrepare, artifact);
214
+
215
+ if (artifact === rootArtifact) {
216
+ // action draftActivate() return <artifact>;
217
+ const draftActivate = createAction('draftActivate', artifactName);
218
+ assignAction(draftActivate, artifact);
219
+
220
+ // action draftEdit (PreserveChanges: Boolean) return <artifact>;
221
+ const draftEdit = createAction('draftEdit', artifactName, 'PreserveChanges', 'cds.Boolean');
222
+ assignAction(draftEdit, artifact);
223
+ }
224
+ }
225
+ }
226
+
227
+ module.exports = generateDrafts;