@sap/cds-compiler 2.12.0 → 2.13.6

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 (118) hide show
  1. package/CHANGELOG.md +110 -15
  2. package/bin/cdsc.js +13 -13
  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 +28 -63
  8. package/lib/api/options.js +3 -3
  9. package/lib/api/validate.js +0 -5
  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 +25 -4
  16. package/lib/base/messages.js +16 -26
  17. package/lib/base/model.js +2 -63
  18. package/lib/base/optionProcessorHelper.js +158 -123
  19. package/lib/checks/annotationsOData.js +1 -1
  20. package/lib/checks/cdsPersistence.js +2 -1
  21. package/lib/checks/enricher.js +17 -1
  22. package/lib/checks/invalidTarget.js +3 -1
  23. package/lib/checks/managedWithoutKeys.js +3 -1
  24. package/lib/checks/selectItems.js +4 -4
  25. package/lib/checks/sql-snippets.js +27 -26
  26. package/lib/checks/types.js +1 -1
  27. package/lib/checks/validator.js +4 -7
  28. package/lib/compiler/assert-consistency.js +5 -3
  29. package/lib/compiler/builtins.js +8 -6
  30. package/lib/compiler/checks.js +14 -3
  31. package/lib/compiler/cycle-detector.js +1 -1
  32. package/lib/compiler/define.js +1103 -0
  33. package/lib/compiler/extend.js +983 -0
  34. package/lib/compiler/finalize-parse-cdl.js +231 -0
  35. package/lib/compiler/index.js +32 -13
  36. package/lib/compiler/kick-start.js +190 -0
  37. package/lib/compiler/moduleLayers.js +4 -4
  38. package/lib/compiler/populate.js +1226 -0
  39. package/lib/compiler/propagator.js +111 -46
  40. package/lib/compiler/resolve.js +1433 -0
  41. package/lib/compiler/shared.js +64 -37
  42. package/lib/compiler/tweak-assocs.js +529 -0
  43. package/lib/compiler/utils.js +197 -33
  44. package/lib/edm/.eslintrc.json +5 -0
  45. package/lib/edm/annotations/genericTranslation.js +5 -9
  46. package/lib/edm/annotations/preprocessAnnotations.js +2 -2
  47. package/lib/edm/csn2edm.js +9 -8
  48. package/lib/edm/edm.js +11 -12
  49. package/lib/edm/edmPreprocessor.js +137 -73
  50. package/lib/edm/edmUtils.js +116 -22
  51. package/lib/gen/Dictionary.json +10 -3
  52. package/lib/gen/language.checksum +1 -1
  53. package/lib/gen/language.interp +9 -1
  54. package/lib/gen/language.tokens +86 -83
  55. package/lib/gen/languageLexer.interp +10 -1
  56. package/lib/gen/languageLexer.js +860 -833
  57. package/lib/gen/languageLexer.tokens +78 -75
  58. package/lib/gen/languageParser.js +5282 -4265
  59. package/lib/json/from-csn.js +12 -1
  60. package/lib/json/to-csn.js +126 -66
  61. package/lib/language/docCommentParser.js +2 -2
  62. package/lib/language/genericAntlrParser.js +76 -3
  63. package/lib/language/language.g4 +297 -130
  64. package/lib/language/multiLineStringParser.js +5 -5
  65. package/lib/main.d.ts +468 -59
  66. package/lib/main.js +35 -9
  67. package/lib/model/api.js +3 -1
  68. package/lib/model/csnRefs.js +225 -156
  69. package/lib/model/csnUtils.js +192 -223
  70. package/lib/model/enrichCsn.js +70 -29
  71. package/lib/model/revealInternalProperties.js +27 -6
  72. package/lib/model/sortViews.js +2 -1
  73. package/lib/modelCompare/compare.js +17 -12
  74. package/lib/optionProcessor.js +5 -4
  75. package/lib/render/manageConstraints.js +35 -32
  76. package/lib/render/toCdl.js +73 -288
  77. package/lib/render/toHdbcds.js +25 -23
  78. package/lib/render/toSql.js +98 -41
  79. package/lib/render/utils/common.js +5 -10
  80. package/lib/render/utils/sql.js +4 -3
  81. package/lib/render/utils/stringEscapes.js +111 -0
  82. package/lib/sql-identifier.js +1 -1
  83. package/lib/transform/.eslintrc.json +5 -0
  84. package/lib/transform/db/.eslintrc.json +2 -0
  85. package/lib/transform/db/applyTransformations.js +35 -12
  86. package/lib/transform/db/assertUnique.js +1 -1
  87. package/lib/transform/db/associations.js +103 -305
  88. package/lib/transform/db/cdsPersistence.js +2 -2
  89. package/lib/transform/db/constraints.js +55 -52
  90. package/lib/transform/db/expansion.js +46 -24
  91. package/lib/transform/db/flattening.js +553 -102
  92. package/lib/transform/db/groupByOrderBy.js +3 -1
  93. package/lib/transform/db/transformExists.js +59 -6
  94. package/lib/transform/db/views.js +5 -4
  95. package/lib/transform/draft/.eslintrc.json +38 -0
  96. package/lib/transform/{db/draft.js → draft/db.js} +6 -5
  97. package/lib/transform/draft/odata.js +227 -0
  98. package/lib/transform/forHanaNew.js +67 -183
  99. package/lib/transform/forOdataNew.js +17 -171
  100. package/lib/transform/localized.js +34 -19
  101. package/lib/transform/odata/generateForeignKeyElements.js +1 -1
  102. package/lib/transform/odata/referenceFlattener.js +95 -89
  103. package/lib/transform/odata/structureFlattener.js +1 -1
  104. package/lib/transform/odata/toFinalBaseType.js +86 -12
  105. package/lib/transform/odata/typesExposure.js +5 -5
  106. package/lib/transform/odata/utils.js +2 -2
  107. package/lib/transform/transformUtilsNew.js +36 -22
  108. package/lib/transform/translateAssocsToJoins.js +2 -19
  109. package/lib/transform/universalCsn/.eslintrc.json +36 -0
  110. package/lib/transform/universalCsn/coreComputed.js +170 -0
  111. package/lib/transform/universalCsn/universalCsnEnricher.js +715 -0
  112. package/lib/transform/universalCsn/utils.js +63 -0
  113. package/lib/utils/objectUtils.js +30 -0
  114. package/package.json +1 -1
  115. package/share/messages/README.md +26 -0
  116. package/lib/compiler/definer.js +0 -2361
  117. package/lib/compiler/resolver.js +0 -3079
  118. 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
  }
@@ -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
 
@@ -5,6 +5,7 @@ const {
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
  }
@@ -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)
@@ -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;