@sap/cds-compiler 2.11.2 → 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 (140) hide show
  1. package/CHANGELOG.md +175 -2
  2. package/bin/.eslintrc.json +1 -2
  3. package/bin/cds_update_identifiers.js +10 -8
  4. package/bin/cdsc.js +23 -17
  5. package/bin/cdsse.js +2 -2
  6. package/bin/cdsv2m.js +3 -2
  7. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  8. package/doc/CHANGELOG_BETA.md +25 -6
  9. package/doc/CHANGELOG_DEPRECATED.md +22 -6
  10. package/doc/NameResolution.md +21 -16
  11. package/lib/api/main.js +32 -79
  12. package/lib/api/options.js +3 -2
  13. package/lib/api/validate.js +2 -1
  14. package/lib/backends.js +16 -26
  15. package/lib/base/dictionaries.js +0 -8
  16. package/lib/base/error.js +26 -0
  17. package/lib/base/keywords.js +10 -19
  18. package/lib/base/location.js +9 -4
  19. package/lib/base/message-registry.js +75 -9
  20. package/lib/base/messages.js +31 -35
  21. package/lib/base/model.js +2 -62
  22. package/lib/base/optionProcessorHelper.js +246 -183
  23. package/lib/checks/.eslintrc.json +2 -0
  24. package/lib/checks/actionsFunctions.js +2 -1
  25. package/lib/checks/annotationsOData.js +1 -1
  26. package/lib/checks/cdsPersistence.js +2 -1
  27. package/lib/checks/emptyOrOnlyVirtual.js +2 -2
  28. package/lib/checks/enricher.js +17 -1
  29. package/lib/checks/foreignKeys.js +4 -4
  30. package/lib/checks/invalidTarget.js +3 -1
  31. package/lib/checks/managedInType.js +4 -4
  32. package/lib/checks/managedWithoutKeys.js +3 -1
  33. package/lib/checks/queryNoDbArtifacts.js +1 -3
  34. package/lib/checks/selectItems.js +4 -4
  35. package/lib/checks/sql-snippets.js +94 -0
  36. package/lib/checks/types.js +1 -1
  37. package/lib/checks/unknownMagic.js +1 -1
  38. package/lib/checks/validator.js +12 -7
  39. package/lib/compiler/assert-consistency.js +12 -8
  40. package/lib/compiler/base.js +0 -1
  41. package/lib/compiler/builtins.js +42 -21
  42. package/lib/compiler/checks.js +46 -12
  43. package/lib/compiler/cycle-detector.js +1 -1
  44. package/lib/compiler/define.js +1103 -0
  45. package/lib/compiler/extend.js +983 -0
  46. package/lib/compiler/finalize-parse-cdl.js +231 -0
  47. package/lib/compiler/index.js +46 -39
  48. package/lib/compiler/kick-start.js +190 -0
  49. package/lib/compiler/moduleLayers.js +4 -4
  50. package/lib/compiler/populate.js +1226 -0
  51. package/lib/compiler/propagator.js +113 -47
  52. package/lib/compiler/resolve.js +1433 -0
  53. package/lib/compiler/shared.js +100 -65
  54. package/lib/compiler/tweak-assocs.js +529 -0
  55. package/lib/compiler/utils.js +215 -33
  56. package/lib/edm/.eslintrc.json +5 -0
  57. package/lib/edm/annotations/genericTranslation.js +38 -25
  58. package/lib/edm/annotations/preprocessAnnotations.js +3 -3
  59. package/lib/edm/csn2edm.js +10 -9
  60. package/lib/edm/edm.js +19 -20
  61. package/lib/edm/edmPreprocessor.js +166 -95
  62. package/lib/edm/edmUtils.js +127 -34
  63. package/lib/gen/Dictionary.json +92 -43
  64. package/lib/gen/language.checksum +1 -1
  65. package/lib/gen/language.interp +11 -1
  66. package/lib/gen/language.tokens +86 -82
  67. package/lib/gen/languageLexer.interp +18 -1
  68. package/lib/gen/languageLexer.js +925 -847
  69. package/lib/gen/languageLexer.tokens +78 -74
  70. package/lib/gen/languageParser.js +5434 -4298
  71. package/lib/json/from-csn.js +59 -17
  72. package/lib/json/to-csn.js +189 -71
  73. package/lib/language/antlrParser.js +3 -3
  74. package/lib/language/docCommentParser.js +3 -3
  75. package/lib/language/errorStrategy.js +26 -8
  76. package/lib/language/genericAntlrParser.js +144 -53
  77. package/lib/language/language.g4 +424 -200
  78. package/lib/language/multiLineStringParser.js +536 -0
  79. package/lib/main.d.ts +550 -61
  80. package/lib/main.js +38 -11
  81. package/lib/model/api.js +3 -1
  82. package/lib/model/csnRefs.js +322 -198
  83. package/lib/model/csnUtils.js +226 -370
  84. package/lib/model/enrichCsn.js +124 -69
  85. package/lib/model/revealInternalProperties.js +29 -7
  86. package/lib/model/sortViews.js +10 -2
  87. package/lib/modelCompare/compare.js +17 -12
  88. package/lib/optionProcessor.js +8 -3
  89. package/lib/render/.eslintrc.json +1 -2
  90. package/lib/render/DuplicateChecker.js +1 -1
  91. package/lib/render/manageConstraints.js +36 -33
  92. package/lib/render/toCdl.js +174 -275
  93. package/lib/render/toHdbcds.js +203 -122
  94. package/lib/render/toRename.js +7 -10
  95. package/lib/render/toSql.js +161 -82
  96. package/lib/render/utils/common.js +22 -8
  97. package/lib/render/utils/sql.js +10 -7
  98. package/lib/render/utils/stringEscapes.js +111 -0
  99. package/lib/sql-identifier.js +1 -1
  100. package/lib/transform/.eslintrc.json +5 -0
  101. package/lib/transform/braceExpression.js +4 -2
  102. package/lib/transform/db/.eslintrc.json +2 -0
  103. package/lib/transform/db/applyTransformations.js +212 -0
  104. package/lib/transform/db/assertUnique.js +1 -1
  105. package/lib/transform/db/associations.js +187 -0
  106. package/lib/transform/db/cdsPersistence.js +150 -0
  107. package/lib/transform/db/constraints.js +61 -56
  108. package/lib/transform/db/expansion.js +50 -29
  109. package/lib/transform/db/flattening.js +556 -106
  110. package/lib/transform/db/groupByOrderBy.js +3 -1
  111. package/lib/transform/db/temporal.js +236 -0
  112. package/lib/transform/db/transformExists.js +103 -28
  113. package/lib/transform/db/views.js +92 -44
  114. package/lib/transform/draft/.eslintrc.json +38 -0
  115. package/lib/transform/{db/draft.js → draft/db.js} +9 -7
  116. package/lib/transform/draft/odata.js +227 -0
  117. package/lib/transform/forHanaNew.js +98 -783
  118. package/lib/transform/forOdataNew.js +22 -175
  119. package/lib/transform/localized.js +36 -32
  120. package/lib/transform/odata/generateForeignKeyElements.js +3 -3
  121. package/lib/transform/odata/referenceFlattener.js +95 -89
  122. package/lib/transform/odata/structureFlattener.js +1 -1
  123. package/lib/transform/odata/toFinalBaseType.js +86 -12
  124. package/lib/transform/odata/typesExposure.js +5 -5
  125. package/lib/transform/odata/utils.js +2 -2
  126. package/lib/transform/transformUtilsNew.js +47 -33
  127. package/lib/transform/translateAssocsToJoins.js +13 -30
  128. package/lib/transform/universalCsn/.eslintrc.json +36 -0
  129. package/lib/transform/universalCsn/coreComputed.js +170 -0
  130. package/lib/transform/universalCsn/universalCsnEnricher.js +715 -0
  131. package/lib/transform/universalCsn/utils.js +63 -0
  132. package/lib/utils/file.js +8 -3
  133. package/lib/utils/objectUtils.js +30 -0
  134. package/lib/utils/timetrace.js +8 -2
  135. package/package.json +1 -1
  136. package/share/messages/README.md +26 -0
  137. package/lib/compiler/definer.js +0 -2349
  138. package/lib/compiler/resolver.js +0 -2922
  139. package/lib/transform/db/helpers.js +0 -58
  140. package/lib/transform/universalCsnEnricher.js +0 -67
@@ -1,10 +1,63 @@
1
1
  'use strict';
2
2
 
3
- const { usesMixinAssociation, getMixinAssocOfQueryIfPublished } = require('./helpers');
4
- const { getUtils, cloneCsn } = require('../../model/csnUtils');
3
+ const {
4
+ getUtils, cloneCsn, applyTransformationsOnNonDictionary,
5
+ } = require('../../model/csnUtils');
5
6
  const { implicitAs, csnRefs } = require('../../model/csnRefs');
6
7
  const { isBetaEnabled } = require('../../base/model');
8
+ const { ModelError } = require('../../base/error');
7
9
 
10
+ /**
11
+ * If a mixin association is published, return the mixin association.
12
+ *
13
+ * @param {CSN.Query} query Query of the artifact to check
14
+ * @param {object} association Association (Element) published by the view
15
+ * @param {string} associationName
16
+ * @returns {object} The mixin association
17
+ */
18
+ function getMixinAssocOfQueryIfPublished(query, association, associationName) {
19
+ if (query && query.SELECT && query.SELECT.mixin) {
20
+ const aliasedColumnsMap = Object.create(null);
21
+ if (query.SELECT.columns) {
22
+ for (const column of query.SELECT.columns) {
23
+ if (column.as && column.ref && column.ref.length === 1)
24
+ aliasedColumnsMap[column.as] = column;
25
+ }
26
+ }
27
+
28
+ for (const elem of Object.keys(query.SELECT.mixin)) {
29
+ const mixinElement = query.SELECT.mixin[elem];
30
+ let originalName = associationName;
31
+ if (aliasedColumnsMap[associationName])
32
+ originalName = aliasedColumnsMap[associationName].ref[0];
33
+
34
+ if (elem === originalName)
35
+ return { mixinElement, mixinName: originalName };
36
+ }
37
+ }
38
+ return {};
39
+ }
40
+
41
+ /**
42
+ * Check whether the given artifact uses the given mixin association.
43
+ *
44
+ * We can rely on the fact that there can be no usage starting with $self/$projection,
45
+ * since lib/checks/selectItems.js forbids that.
46
+ *
47
+ * @param {CSN.Query} query Query of the artifact to check
48
+ * @param {object} association Mixin association (Element) to check for
49
+ * @param {string} associationName
50
+ * @returns {boolean} True if used
51
+ */
52
+ function usesMixinAssociation(query, association, associationName) {
53
+ if (query && query.SELECT && query.SELECT.columns) {
54
+ for (const column of query.SELECT.columns) {
55
+ if (typeof column === 'object' && column.ref && column.ref.length > 1 && (column.ref[0] === associationName || column.ref[0].id === associationName))
56
+ return true;
57
+ }
58
+ }
59
+ return false;
60
+ }
8
61
 
9
62
  /**
10
63
  * @param {CSN.Model} csn
@@ -15,7 +68,7 @@ const { isBetaEnabled } = require('../../base/model');
15
68
  */
16
69
  function getViewTransformer(csn, options, messageFunctions, transformCommon) {
17
70
  const {
18
- get$combined, cloneWithTransformations, isAssocOrComposition,
71
+ get$combined, isAssocOrComposition,
19
72
  } = getUtils(csn);
20
73
  const { inspectRef, queryOrMain } = csnRefs(csn);
21
74
  const pathDelimiter = (options.forHana.names === 'hdbcds') ? '.' : '_';
@@ -130,10 +183,10 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
130
183
  const matchingCombined = $combined[elemName];
131
184
  // Internal errors - this should never happen!
132
185
  if (matchingCombined.length > 1) { // should already be caught by compiler
133
- 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)}`);
134
187
  }
135
188
  else if (matchingCombined.length === 0) { // no clue how this could happen? Invalid CSN?
136
- 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}`);
137
190
  }
138
191
  alias = matchingCombined[0].parent;
139
192
  }
@@ -181,41 +234,38 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
181
234
  * create the __clone for publishing stuff.
182
235
  *
183
236
  * @todo Factor out the checks
184
- * @todo Union, Join, Subqueries? Assoc usage in there? __clone?
185
237
  * @param {CSN.Query} query
186
238
  * @param {object} elements
187
239
  * @param {object} columnMap
188
240
  * @param {WeakMap} publishedMixins Map to collect the published mixins
189
241
  * @param {CSN.Element} elem
190
242
  * @param {string} elemName
191
- * @param {CSN.Path} path
243
+ * @param {CSN.Path} elementsPath Path pointing to elements
244
+ * @param {CSN.Path} queryPath Path pointing to the query
192
245
  */
193
- function handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName, path) {
194
- if (isUnion(path) && options.transformation === 'hdbcds') {
246
+ function handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName, elementsPath, queryPath) {
247
+ if (isUnion(queryPath) && options.transformation === 'hdbcds') {
195
248
  if (isBetaEnabled(options, 'ignoreAssocPublishingInUnion') && doA2J) {
196
249
  if (elem.keys)
197
- info(null, path, `Managed association "${elemName}", published in a UNION, will be ignored`);
250
+ info(null, queryPath, `Managed association "${elemName}", published in a UNION, will be ignored`);
198
251
  else
199
- info(null, path, `Association "${elemName}", published in a UNION, will be ignored`);
252
+ info(null, queryPath, `Association "${elemName}", published in a UNION, will be ignored`);
200
253
 
201
254
  elem._ignore = true;
202
255
  }
203
256
  else {
204
- error(null, path, `Association "${elemName}" can't be published in a SAP HANA CDS UNION`);
257
+ error(null, queryPath, `Association "${elemName}" can't be published in a SAP HANA CDS UNION`);
205
258
  }
206
259
  }
207
- else if (path.length > 4 && options.transformation === 'hdbcds') { // path.length > 4 -> is a subquery
208
- error(null, path, { name: elemName },
260
+ else if (queryPath.length > 4 && options.transformation === 'hdbcds') { // path.length > 4 -> is a subquery
261
+ error(null, queryPath, { name: elemName },
209
262
  'Association $(NAME) can\'t be published in a subquery');
210
263
  }
211
264
  else {
212
- /* Old implementation:
213
- const isNotMixinByItself = !(elem.value && elem.value.path && elem.value.path.length == 1 && art.query && art.query.mixin && art.query.mixin[elem.value.path[0].id]);
214
- */
215
265
  const isNotMixinByItself = checkIsNotMixinByItself(query, columnMap, elemName);
216
266
  const { mixinElement, mixinName } = getMixinAssocOfQueryIfPublished(query, elem, elemName);
217
267
  if (isNotMixinByItself || mixinElement !== undefined) {
218
- // If the mixin is only published and not used, only display the __ clone. Kill the "original".
268
+ // If the mixin is only published and not used, only display the __ clone. Kill the "original".
219
269
  if (mixinElement !== undefined && !usesMixinAssociation(query, elem, elemName))
220
270
  delete query.SELECT.mixin[mixinName];
221
271
 
@@ -226,8 +276,8 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
226
276
  mixinElemName = `_${mixinElemName}`;
227
277
 
228
278
  // Copy the association element to the MIXIN clause under its alias name
229
- // (shallow copy is sufficient, just fix name and value)
230
- const mixinElem = Object.assign({}, elem);
279
+ // Needs to be a deep copy, as we transform the on-condition
280
+ const mixinElem = cloneCsn(elem, options);
231
281
  // Perform common transformations on the newly generated MIXIN element (won't be reached otherwise)
232
282
  transformCommon(mixinElem, mixinElemName);
233
283
 
@@ -238,30 +288,24 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
238
288
  // and fixing the association alias just created
239
289
 
240
290
  if (mixinElem.on) {
241
- mixinElem.on = cloneWithTransformations(mixinElem.on, {
242
- ref: (ref) => {
243
- // Clone the path, without any transformations
244
- const clonedPath = cloneWithTransformations(ref, {});
245
- // Prepend '$projection' to the path, unless the first path step is the (mixin) element itself or starts with '$')
246
- if (clonedPath[0] === elemName) {
247
- clonedPath[0] = mixinElemName;
291
+ mixinElem.on = applyTransformationsOnNonDictionary(mixinElem, 'on', {
292
+ ref: (parent, prop, ref, refpath) => {
293
+ if (ref[0] === elemName) {
294
+ ref[0] = mixinElemName;
248
295
  }
249
- else if (!(clonedPath[0] && clonedPath[0].startsWith('$'))) {
250
- const projectionId = '$projection';
251
- clonedPath.unshift(projectionId);
296
+ else if (!(ref[0] && ref[0].startsWith('$'))) {
297
+ ref.unshift('$projection');
252
298
  }
253
- return clonedPath;
254
- },
255
- func: (func) => {
256
- // Unfortunately, function names are disguised as paths, so we would prepend a '$projection'
257
- // above (no way to distinguish that in the callback for 'path' above). We can only pluck it
258
- // off again here ... sigh
259
- if (func.ref && func.ref[0] && func.ref[0] === '$projection')
260
- func.ref = func.ref.slice(1);
261
-
262
- return func;
299
+ else if (ref[0] && ref[0].startsWith('$')) {
300
+ // TODO: I think this is non-sense. Stuff with $ is either magic or must start with $self, right?
301
+ const { scope } = inspectRef(refpath);
302
+ if (scope !== '$magic' && scope !== '$self')
303
+ ref.unshift('$projection');
304
+ }
305
+ parent.ref = ref;
306
+ return ref;
263
307
  },
264
- });
308
+ }, {}, elementsPath.concat(elemName));
265
309
  }
266
310
 
267
311
  if (!mixinElem._ignore)
@@ -304,6 +348,10 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
304
348
  // eslint-disable-next-line complexity
305
349
  function transformViewOrEntity(query, artifact, artName, path) {
306
350
  const { elements } = queryOrMain(query, artifact);
351
+ // We use the elements from the leading query/main artifact - adapt the path
352
+ const elementsPath = elements === artifact.elements ? path.slice(0, 2).concat('elements') : path.concat('elements');
353
+ const queryPath = path;
354
+
307
355
  let hasNonAssocElements = false;
308
356
  const isSelect = query && query.SELECT;
309
357
  const isProjection = !!artifact.projection || query && query.SELECT && !query.SELECT.columns;
@@ -338,12 +386,12 @@ function getViewTransformer(csn, options, messageFunctions, transformCommon) {
338
386
  // (180 b) Create MIXINs for association elements in projections or views (those that are not mixins by themselves)
339
387
  // CDXCORE-585: Allow mixin associations to be used and published in parallel
340
388
  if (query !== undefined && elem.target)
341
- handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName, path);
389
+ handleAssociationElement(query, elements, columnMap, publishedMixins, elem, elemName, elementsPath, queryPath);
342
390
  }
343
391
 
344
392
  if (query && !hasNonAssocElements) {
345
- // Complain if there are no elements other than unmanaged associations
346
- // Allow with plain
393
+ // Complain if there are no elements other than unmanaged associations
394
+ // Allow with plain
347
395
  error(null, [ 'definitions', artName ], { $reviewed: true },
348
396
  'Expecting view or projection to have at least one element that is not an unmanaged association');
349
397
  }
@@ -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
@@ -134,7 +135,8 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
134
135
  warning(null, [ 'definitions', artifactName ], 'Entity annotated with “@odata.draft.enabled” should have one key element of type “cds.UUID”');
135
136
 
136
137
 
137
- const matchingService = getMatchingService(artifactName);
138
+ // Ignore boolean return value. We know that we're inside a service or else we wouldn't have reached this code.
139
+ const matchingService = getMatchingService(artifactName) || '';
138
140
  // Generate the DraftAdministrativeData projection into the service, unless there is already one
139
141
  const draftAdminDataProjectionName = `${matchingService}.DraftAdministrativeData`;
140
142
  let draftAdminDataProjection = csn.definitions[draftAdminDataProjectionName];
@@ -311,7 +313,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
311
313
  function getDraftShadowEntityFor(draftNode, draftNodeName) {
312
314
  // Sanity check
313
315
  if (!draftNodes[draftNodeName])
314
- throw new Error(`Not a draft node: ${draftNodeName}`);
316
+ throw new ModelError(`Not a draft node: ${draftNodeName}`);
315
317
 
316
318
  return { shadowTarget: csn.definitions[`${draftNodeName}${draftSuffix}`], shadowTargetName: `${draftNodeName}${draftSuffix}` };
317
319
  }
@@ -336,7 +338,7 @@ function generateDrafts(csn, options, pathDelimiter, messageFunctions) {
336
338
  * Get the service name containing the artifact.
337
339
  *
338
340
  * @param {string} artifactName Absolute name of the artifact
339
- * @returns {boolean|string} Name of the service or false if no match is found.
341
+ * @returns {false|string} Name of the service or false if no match is found.
340
342
  */
341
343
  function getMatchingService(artifactName) {
342
344
  const matches = [];
@@ -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;