@sap/cds-compiler 6.1.0 → 6.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/CHANGELOG.md +78 -0
  2. package/bin/cdsc.js +17 -6
  3. package/bin/cdsse.js +1 -1
  4. package/bin/cdsv2m.js +1 -1
  5. package/lib/api/main.js +29 -7
  6. package/lib/api/options.js +1 -1
  7. package/lib/base/builtins.js +9 -0
  8. package/lib/base/keywords.js +1 -1
  9. package/lib/base/message-registry.js +41 -10
  10. package/lib/base/messages.js +13 -6
  11. package/lib/base/model.js +1 -1
  12. package/lib/base/optionProcessorHelper.js +7 -2
  13. package/lib/checks/assocOutsideService.js +17 -30
  14. package/lib/checks/checkForTypes.js +0 -18
  15. package/lib/checks/checkPathsInStoredCalcElement.js +2 -1
  16. package/lib/checks/featureFlags.js +4 -1
  17. package/lib/checks/onConditions.js +2 -2
  18. package/lib/checks/queryNoDbArtifacts.js +16 -15
  19. package/lib/checks/types.js +1 -1
  20. package/lib/checks/utils.js +30 -6
  21. package/lib/checks/validator.js +4 -5
  22. package/lib/compiler/assert-consistency.js +3 -1
  23. package/lib/compiler/base.js +1 -1
  24. package/lib/compiler/builtins.js +1 -1
  25. package/lib/compiler/checks.js +85 -39
  26. package/lib/compiler/define.js +24 -5
  27. package/lib/compiler/extend.js +1 -1
  28. package/lib/compiler/finalize-parse-cdl.js +9 -1
  29. package/lib/compiler/generate.js +4 -4
  30. package/lib/compiler/index.js +88 -6
  31. package/lib/compiler/lsp-api.js +2 -0
  32. package/lib/compiler/populate.js +8 -8
  33. package/lib/compiler/propagator.js +1 -1
  34. package/lib/compiler/resolve.js +22 -21
  35. package/lib/compiler/shared.js +6 -6
  36. package/lib/compiler/tweak-assocs.js +53 -31
  37. package/lib/compiler/utils.js +9 -16
  38. package/lib/compiler/xpr-rewrite.js +2 -2
  39. package/lib/gen/BaseParser.js +35 -29
  40. package/lib/gen/CdlGrammar.checksum +1 -1
  41. package/lib/gen/CdlParser.js +1424 -1430
  42. package/lib/gen/Dictionary.json +1 -2
  43. package/lib/gen/cdlKeywords.json +26 -0
  44. package/lib/inspect/inspectPropagation.js +1 -1
  45. package/lib/json/from-csn.js +2 -2
  46. package/lib/json/to-csn.js +1 -1
  47. package/lib/language/multiLineStringParser.js +1 -1
  48. package/lib/model/cloneCsn.js +1 -0
  49. package/lib/model/csnRefs.js +9 -4
  50. package/lib/model/csnUtils.js +67 -2
  51. package/lib/optionProcessor.js +9 -9
  52. package/lib/parsers/AstBuildingParser.js +28 -26
  53. package/lib/parsers/identifiers.js +2 -30
  54. package/lib/render/toCdl.js +73 -13
  55. package/lib/render/toSql.js +127 -108
  56. package/lib/render/utils/common.js +4 -2
  57. package/lib/render/utils/sql.js +67 -0
  58. package/lib/transform/addTenantFields.js +4 -4
  59. package/lib/transform/db/assertUnique.js +2 -1
  60. package/lib/transform/db/associations.js +37 -1
  61. package/lib/transform/db/assocsToQueries/transformExists.js +21 -32
  62. package/lib/transform/db/assocsToQueries/utils.js +1 -1
  63. package/lib/transform/db/cdsPersistence.js +1 -1
  64. package/lib/transform/db/expansion.js +37 -36
  65. package/lib/transform/db/killAnnotations.js +1 -0
  66. package/lib/transform/db/processSqlServices.js +20 -2
  67. package/lib/transform/draft/db.js +20 -20
  68. package/lib/transform/draft/odata.js +38 -40
  69. package/lib/transform/effective/associations.js +1 -1
  70. package/lib/transform/effective/flattening.js +40 -47
  71. package/lib/transform/effective/main.js +6 -4
  72. package/lib/transform/forOdata.js +201 -92
  73. package/lib/transform/forRelationalDB.js +151 -142
  74. package/lib/transform/localized.js +116 -109
  75. package/lib/transform/odata/adaptAnnotationRefs.js +21 -16
  76. package/lib/transform/odata/createForeignKeys.js +73 -70
  77. package/lib/transform/odata/flattening.js +216 -200
  78. package/lib/transform/odata/foreignKeyRefsInXprAnnos.js +47 -45
  79. package/lib/transform/odata/toFinalBaseType.js +40 -39
  80. package/lib/transform/odata/typesExposure.js +151 -133
  81. package/lib/transform/odata/utils.js +7 -6
  82. package/lib/transform/parseExpr.js +165 -162
  83. package/lib/transform/transformUtils.js +184 -551
  84. package/lib/transform/translateAssocsToJoins.js +511 -596
  85. package/lib/transform/tupleExpansion.js +495 -0
  86. package/lib/transform/universalCsn/universalCsnEnricher.js +1 -0
  87. package/lib/utils/moduleResolve.js +1 -1
  88. package/package.json +2 -2
  89. package/lib/base/cleanSymbols.js +0 -17
  90. package/lib/checks/nonexpandableStructured.js +0 -39
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const {
4
+ transformAnnotationExpression,
4
5
  applyTransformationsOnNonDictionary,
5
6
  applyTransformations,
6
7
  implicitAs,
@@ -106,18 +107,40 @@ function attachOnConditions( csn, csnUtils, pathDelimiter, iterateOptions = {},
106
107
 
107
108
  /**
108
109
  * @param {CSN.Model} csn
110
+ * @param {CSN.Options} options
109
111
  * @param {object} csnUtils
110
112
  * @param {string} pathDelimiter
111
113
  * @param {boolean} [processOnInQueries=false] Wether to process on-conditions in queries (joins and mixins)
112
114
  * @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition
113
115
  */
114
- function getFKAccessFinalizer( csn, csnUtils, pathDelimiter, processOnInQueries = false ) {
116
+ function getFKAccessFinalizer( csn, options, csnUtils, pathDelimiter, processOnInQueries = false ) {
115
117
  const {
116
118
  inspectRef,
117
119
  } = csnUtils;
118
120
 
119
121
  return handleManagedAssocSteps;
120
122
 
123
+ /**
124
+ *
125
+ * @param {object} obj Object with annotations.
126
+ * @param {object} transformer Annotation expression transformers.
127
+ * @param {CSN.Path} path CSN path for locations.
128
+ */
129
+ function processRefsInAnnotations(obj, transformer, path) {
130
+ Object.keys(obj)
131
+ .filter(pn => pn.startsWith('@') && obj[pn])
132
+ .forEach((anno) => {
133
+ // TODO: Ensure we only do processing here for annotations that have refs, to save time
134
+ const annoBefore = JSON.stringify(obj[anno]);
135
+ transformAnnotationExpression(obj, anno, transformer, path);
136
+ if (obj[anno].ref)
137
+ transformer.ref(obj[anno], 'ref', obj[anno].ref, path.concat(anno));
138
+ const annoAfter = JSON.stringify(obj[anno]);
139
+ if (annoBefore !== annoAfter)
140
+ obj[anno]['='] = true;
141
+ });
142
+ }
143
+
121
144
  /**
122
145
  * Loop over all elements and for all unmanaged associations translate
123
146
  * <assoc base>.<managed assoc>.<fk> to <assoc base>.<managed assoc>_<fk>
@@ -133,6 +156,11 @@ function getFKAccessFinalizer( csn, csnUtils, pathDelimiter, processOnInQueries
133
156
  function handleManagedAssocSteps( artifact, artifactName ) {
134
157
  const transformer = getTransformer();
135
158
  const inColumnsTransformer = getTransformer(true);
159
+
160
+ if (options.transformation === 'effective')
161
+ processRefsInAnnotations(artifact, transformer, [ 'definitions', artifactName ]);
162
+
163
+
136
164
  for (const elemName in artifact.elements) {
137
165
  const elem = artifact.elements[elemName];
138
166
  // The association is an unmanaged one
@@ -140,6 +168,10 @@ function getFKAccessFinalizer( csn, csnUtils, pathDelimiter, processOnInQueries
140
168
  applyTransformationsOnNonDictionary(elem, 'on', transformer, {}, [ 'definitions', artifactName, 'elements', elemName ]);
141
169
  else if (elem.value?.stored)
142
170
  applyTransformationsOnNonDictionary(elem, 'value', transformer, {}, [ 'definitions', artifactName, 'elements', elemName ]);
171
+
172
+ // TODO: Is this enough? I suppose that these annotations can be in places that are not an element.
173
+ if (options.transformation === 'effective')
174
+ processRefsInAnnotations(elem, transformer, [ 'definitions', artifactName, 'elements', elemName ]);
143
175
  }
144
176
 
145
177
  if (artifact.query || artifact.projection) {
@@ -165,6 +197,10 @@ function getFKAccessFinalizer( csn, csnUtils, pathDelimiter, processOnInQueries
165
197
  };
166
198
  queryTransformers.on = transform;
167
199
  }
200
+
201
+ if (options.transformation === 'effective' || options.transformation === 'odata')
202
+ queryTransformers.xpr = transform;
203
+
168
204
  applyTransformationsOnNonDictionary(artifact, artifact.query ? 'query' : 'projection', queryTransformers, { drillRef: true }, [ 'definitions', artifactName ]);
169
205
  }
170
206
 
@@ -26,30 +26,29 @@ const { getHelpers } = require('./utils');
26
26
  * + Is it part of the source side: <path> is turned into <query source>.<path> - a leading $self is stripped-off
27
27
  * + Is it something else: Don't touch it, leave as is
28
28
  *
29
- * Given that `assoc` from above has the on-condition assoc.id = id, we would generate the following:
29
+ * Given that `assoc` from above has the on-condition `assoc.id = id`, we would generate the following:
30
30
  * - F.id = E.id
31
31
  *
32
- * The final subselect looks like (select 1 as dummy from E where F.id = E.id and filter = 100).
32
+ * The final subselect looks like `(select 1 as dummy from E where F.id = E.id and filter = 100)`.
33
33
  *
34
34
  * For a $self backlink:
35
35
  * - For $self = <assoc>.<another-assoc>, we do the following for each foreign key of <another-assoc>
36
36
  * + <assoc>.<another-assoc>.<fk> -> <assoc.target>.<another-assoc>.<fk>
37
- * + Afterwards, we get the corresponding key from the source side: <query-source>.<fk>
37
+ * + Afterward, we get the corresponding key from the source side: <query-source>.<fk>
38
38
  * + And turn this into a comparison: <assoc.target>.<another-assoc>.<fk> = <query-source>.<fk>
39
39
  *
40
40
  * So for the sample above, given an on-condition like $self = assoc.backToE, we would generate:
41
41
  * - F.backToE.id = E.id
42
42
  *
43
- * The final subselect looks like (select 1 as dummy from E where F.backToE.id = E.id and filter = 100).
43
+ * The final subselect looks like `(select 1 as dummy from E where F.backToE.id = E.id and filter = 100)`.
44
44
  *
45
45
  * @param {CSN.Model} csn
46
46
  * @param {CSN.Options} options
47
- * @param {Function} error
48
- * @param {Function} inspectRef
49
- * @param {Function} initDefinition
50
- * @param {Function} dropDefinitionCache
47
+ * @param {object} messageFunctions
48
+ * @param {object} csnUtils
51
49
  */
52
- function handleExists( csn, options, error, inspectRef, initDefinition, dropDefinitionCache ) {
50
+ function handleExists( csn, options, messageFunctions, csnUtils ) {
51
+ const { error } = messageFunctions;
53
52
  const {
54
53
  getBase,
55
54
  firstLinkIsEntityOrQuerySource,
@@ -57,17 +56,12 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
57
56
  translateManagedAssocToWhere,
58
57
  getQuerySources,
59
58
  translateUnmanagedAssocToWhere,
60
- } = getHelpers(csn, inspectRef, error);
59
+ } = getHelpers(csn, csnUtils.inspectRef, error);
60
+
61
61
  const generatedExists = new WeakMap();
62
62
  forEachDefinition(csn, (artifact, artifactName) => {
63
- // drop cache: Otherwise, the projection/query hack below won't work, because csnRefs
64
- // thinks that the artifact was already initialized (including all queries).
65
- dropDefinitionCache(artifact);
66
- if (artifact.projection) // do the same hack we do for the other stuff...
67
- artifact.query = { SELECT: artifact.projection };
68
-
69
- if (artifact.query) {
70
- forAllQueries(artifact.query, function handleExistsQuery(query, path) {
63
+ if (artifact.query || artifact.projection) {
64
+ forAllQueries(artifact.query || { SELECT: artifact.projection }, function handleExistsQuery(query, path) {
71
65
  if (!generatedExists.has(query)) {
72
66
  const toProcess = []; // Collect all expressions we need to process here
73
67
  if (query.SELECT?.where?.length > 1)
@@ -90,8 +84,8 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
90
84
  while (toProcess.length > 0) {
91
85
  const [ queryPath, exprPath ] = toProcess.pop();
92
86
  // Re-init caches for this artifact
93
- dropDefinitionCache(artifact);
94
- initDefinition(artifact);
87
+ csnUtils.dropDefinitionCache(artifact);
88
+ csnUtils.initDefinition(artifact);
95
89
  // leftovers can happen with nested exists - we then need to drill down into the created SELECT
96
90
  // to check for further exists
97
91
  const { result, leftovers } = processExists(queryPath, exprPath);
@@ -100,16 +94,10 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
100
94
  toProcess.push(...leftovers); // any leftovers - schedule for further processing
101
95
  }
102
96
  // Make sure we leave csnRefs usable
103
- dropDefinitionCache(artifact);
104
- initDefinition(artifact);
97
+ csnUtils.dropDefinitionCache(artifact);
98
+ csnUtils.initDefinition(artifact);
105
99
  }
106
- }, [ 'definitions', artifactName, 'query' ]);
107
- }
108
-
109
- if (artifact.projection) { // undo our hack
110
- artifact.projection = artifact.query.SELECT;
111
-
112
- delete artifact.query;
100
+ }, [ 'definitions', artifactName, artifact.projection ? 'projection' : 'query' ]);
113
101
  }
114
102
  });
115
103
 
@@ -168,7 +156,7 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
168
156
  }
169
157
  }
170
158
  const stack = [ [ null, startAssoc, startRest, startIndex ] ];
171
- const { links } = inspectRef(path);
159
+ const { links } = csnUtils.inspectRef(path);
172
160
  while (stack.length > 0) {
173
161
  // previous: to nest "up" if the previous assoc did not originally have a filter
174
162
  // assoc: the assoc path step
@@ -258,8 +246,9 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
258
246
  const newExpr = [];
259
247
  const query = walkCsnPath(csn, queryPath);
260
248
  const expr = walkCsnPath(csn, exprPath);
261
- const queryBase = query.SELECT.from.ref ? (query.SELECT.from.as || query.SELECT.from.ref) : null;
262
- const sources = getQuerySources(query.SELECT);
249
+ const select = query.projection ?? query.SELECT;
250
+ const queryBase = select?.from?.ref ? (select.from.as || select.from.ref) : null;
251
+ const sources = getQuerySources(select);
263
252
 
264
253
  for (let i = 0; i < expr.length; i++) {
265
254
  if (i < expr.length - 1 && expr[i] === 'exists' && expr[i + 1].ref) {
@@ -430,7 +430,7 @@ function getHelpers( csn, inspectRef, error ) {
430
430
  }
431
431
 
432
432
  /**
433
- * Use cacjed _links and _art or calculate via inspectRef
433
+ * Use cached _links and _art or calculate via inspectRef
434
434
  * @param {object} obj
435
435
  * @param {CSN.Path} objPath
436
436
  * @returns {object}
@@ -125,7 +125,7 @@ function getPersistenceTableProcessor( csn, options, messageFunctions ) {
125
125
 
126
126
  recurseElements(artifact, [ 'definitions', artifactName ], (member, path) => {
127
127
  // All elements must have a type for this to work
128
- if (!member.$ignore && !member.kind && !member.type) {
128
+ if (!member.$ignore && !member.kind && !member.type && !member.elements) { // .items? Probably resolved at this point
129
129
  error(null, path, { anno: '@cds.persistence.table' },
130
130
  'Expecting element to have a type if view is annotated with $(ANNO)');
131
131
  }
@@ -38,18 +38,16 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
38
38
  columns: (parent, name, columns, path) => {
39
39
  const artifact = csn.definitions[path[1]];
40
40
  csnUtils.initDefinition(artifact); // potentially not initialized, yet
41
- if (!artifact['@cds.persistence.table']) {
42
- const root = csnUtils.get$combined({ SELECT: parent });
43
- // TODO: replace with the correct options.transformation?
44
- // Do not expand the * in OData for a moment, not to introduce changes
45
- // while the OData CSN is still official
46
- const isComplexQuery = parent.from.join !== undefined;
47
- if (!options.toOdata)
48
- parent.columns = replaceStar(root, columns, parent.excluding, isComplexQuery);
49
- // FIXME(v6): Remove argument "isComplexOrNestedQuery"; we use path.length > 4 to check
50
- // if we're inside the outermost "columns". If so, always prepend a table alias. See #11662
51
- parent.columns = expand(parent.columns, path.concat('columns'), true, isComplexQuery || path.length > 4);
52
- }
41
+ const root = csnUtils.get$combined({ SELECT: parent });
42
+ // TODO: replace with the correct options.transformation?
43
+ // Do not expand the * in OData for a moment, not to introduce changes
44
+ // while the OData CSN is still official
45
+ const isComplexQuery = parent.from.join !== undefined;
46
+ if (!options.toOdata)
47
+ parent.columns = replaceStar(root, columns, parent.excluding, isComplexQuery);
48
+ // FIXME(v6): Remove argument "isComplexOrNestedQuery"; we use path.length > 4 to check
49
+ // if we're inside the outermost "columns". If so, always prepend a table alias. See #11662
50
+ parent.columns = expand(parent.columns, path.concat('columns'), true, isComplexQuery || path.length > 4);
53
51
  },
54
52
  groupBy: (parent, name, groupBy, path) => {
55
53
  parent.groupBy = expand(groupBy, path.concat('groupBy'));
@@ -86,24 +84,22 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
86
84
  // (which is the thing inside SET/SELECT)
87
85
  // We can directly use SELECT here, as only projections and SELECT can have .columns
88
86
  const root = csnUtils.get$combined({ SELECT: parent });
89
- if (!artifact['@cds.persistence.table']) {
90
- // Make root look like normal .elements - we never cared about conflict afaik anyway
91
- Object.keys(root).forEach((key) => {
92
- root[key] = root[key][0].element;
87
+ // Make root look like normal .elements - we never cared about conflict afaik anyway
88
+ Object.keys(root).forEach((key) => {
89
+ root[key] = root[key][0].element;
90
+ });
91
+ const rewritten = rewrite(root, parent.columns, parent.excluding);
92
+ /*
93
+ * Do not remove unexpandable many columns in OData
94
+ */
95
+ if (rewritten.toMany.length > 0 && !options.toOdata) {
96
+ markAsToDummify(artifact, path[1]);
97
+ rewritten.toMany.forEach(({ art }) => {
98
+ error( null, art.$path || [ 'definitions', path[1] ], { name: `${ art.$env || path[1] }:${ art.ref.map(r => r.id || r) }` }, 'Unexpected .expand with to-many association $(NAME)');
93
99
  });
94
- const rewritten = rewrite(root, parent.columns, parent.excluding);
95
- /*
96
- * Do not remove unexpandable many columns in OData
97
- */
98
- if (rewritten.toMany.length > 0 && !options.toOdata) {
99
- markAsToDummify(artifact, path[1]);
100
- rewritten.toMany.forEach(({ art }) => {
101
- error( null, art.$path || [ 'definitions', path[1] ], { name: `${ art.$env || path[1] }:${ art.ref.map(r => r.id || r) }` }, 'Unexpected .expand with to-many association $(NAME)');
102
- });
103
- }
104
- else {
105
- parent.columns = rewritten.columns;
106
- }
100
+ }
101
+ else {
102
+ parent.columns = rewritten.columns;
107
103
  }
108
104
  },
109
105
  });
@@ -280,11 +276,13 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
280
276
  const allToMany = [];
281
277
  const newThing = [];
282
278
  const containsExpandInline = columns.some(col => col.expand || col.inline);
283
- if (containsExpandInline) // Replace stars - needs to happen before resolving .expand/.inline since the .expand/.inline first path step affects the root *
284
- columns = replaceStar(root, columns, excluding);
285
- else
279
+ if (!containsExpandInline)
286
280
  return { columns, toMany: [] };
287
281
 
282
+ // Replace stars - needs to happen before resolving .expand/.inline since the
283
+ // .expand/.inline first path step affects the root *
284
+ columns = replaceStar(root, columns, excluding);
285
+
288
286
  for (const col of columns) {
289
287
  if (col.expand || col.inline) {
290
288
  const { expanded, toManys } = expandInline(root, col, col.ref || [], col.expand ? [ dbName(col) ] : []);
@@ -364,7 +362,10 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
364
362
  }
365
363
  else if (current.on || current.cast?.on) {
366
364
  rewriteOn(current, [ currentAlias.slice(0, -1).join(pathDelimiter) ]);
367
- expanded.push(Object.assign({}, current, { ref: currentRef, as: currentAlias.join(pathDelimiter) } ));
365
+ const expandedCol = Object.assign({}, current, { as: currentAlias.join(pathDelimiter) } );
366
+ if (currentRef.length)
367
+ expandedCol.ref = currentRef;
368
+ expanded.push(expandedCol);
368
369
  }
369
370
  else if (current.val !== undefined || current.func !== undefined) {
370
371
  expanded.push(Object.assign(current, { as: currentAlias.join(pathDelimiter) }));
@@ -440,7 +441,7 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
440
441
  function rewriteOnCondition( on, currentRef, stack ) {
441
442
  for (let i = 0; i < on.length; i++) {
442
443
  const part = on[i];
443
- if (part.ref && part.ref[0] !== '$self' && part.ref[0] !== '$projection') {
444
+ if (part.ref && part.$scope !== '$magic' && part.$scope !== '$self' && part.$scope !== '$projection') {
444
445
  part.ref = currentRef[0] ? [ currentRef[0], ...part.ref ] : part.ref;
445
446
  on[i] = part;
446
447
  stack.push([ part, part.ref ]);
@@ -461,7 +462,7 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
461
462
  function rewriteSingleExpressionArray( expressionArray, currentRef, stack ) {
462
463
  for (let i = 0; i < expressionArray.length; i++) {
463
464
  const part = expressionArray[i];
464
- if (part.ref) {
465
+ if (part.ref && part.$scope !== '$magic' && part.$scope !== '$self') {
465
466
  part.ref = currentRef.concat(part.ref);
466
467
  expressionArray[i] = part;
467
468
  stack.push([ part, part.ref ]);
@@ -657,7 +658,7 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
657
658
  obj.ref = [ root.$env, ...obj.ref ];
658
659
 
659
660
  if (iterateOptions.keepKeysOrigin) {
660
- setProp(obj, '$originalKeyRef', { ref: root.ref, as: root.as });
661
+ setProp(obj, '$originalKeyRef', root);
661
662
  setProp(obj, '$path', root.$path);
662
663
  }
663
664
 
@@ -26,6 +26,7 @@ const requiredAnnos = {
26
26
  '@Core.Computed': true,
27
27
  [sqlServiceAnnotation]: true,
28
28
  '@cds.external': true, // for external ABAP SQL services and data products for now
29
+ '@DataIntegration.dataproduct.type': true, // for data product production
29
30
  };
30
31
 
31
32
  /**
@@ -16,12 +16,15 @@ const sqlServiceAnnotation = '@protocol';
16
16
  function processSqlServices(csn, options) {
17
17
  setProp(csn, '$sqlServiceEntities', Object.create(null));
18
18
  setProp(csn, '$dummyServiceEntities', Object.create(null));
19
+ setProp(csn, '$dataProductEntities', Object.create(null));
19
20
  return function findAndMarkSqlServiceArtifacts(artifact, artifactName) {
20
- const { sqlServiceName, dummyServiceName } = isEntityInSqlService(artifact, artifactName, csn, options);
21
+ const { sqlServiceName, dummyServiceName, dataProductServiceName } = isEntityInSqlService(artifact, artifactName, csn, options);
21
22
  if (sqlServiceName?.length > 0)
22
23
  setProp(artifact, '$sqlService', sqlServiceName);
23
24
  if (dummyServiceName?.length > 0)
24
25
  setProp(artifact, '$dummyService', dummyServiceName);
26
+ if (dataProductServiceName?.length > 0)
27
+ setProp(artifact, '$dataProductService', dataProductServiceName);
25
28
  };
26
29
  }
27
30
 
@@ -55,7 +58,7 @@ function isDummyService(artifact, options) {
55
58
  * @returns {object} An object containing the names of the SQL service and external ABAP SQL service, if found.
56
59
  */
57
60
  function isEntityInSqlService(artifact, artifactName, csn, options) {
58
- const result = { sqlServiceName: undefined, dummyServiceName: undefined };
61
+ const result = { sqlServiceName: undefined, dummyServiceName: undefined, dataProductServiceName: undefined };
59
62
  if (artifact.kind !== 'entity' || !artifactName.includes('.') || hasPersistenceSkipAnnotation(artifact))
60
63
  return result;
61
64
 
@@ -72,6 +75,9 @@ function isEntityInSqlService(artifact, artifactName, csn, options) {
72
75
  if (isDummyService(definition, options))
73
76
  result.dummyServiceName = possibleServiceName;
74
77
 
78
+ if (isDataProductService(definition, options))
79
+ result.dataProductServiceName = possibleServiceName;
80
+
75
81
  // We don't allow nested services/contexts - if we find one, we don't need to keep searching
76
82
  if (definition.kind === 'service' || definition.kind === 'context')
77
83
  return result;
@@ -108,10 +114,22 @@ function createServiceDummy(artifact, artifactName, csn, { error }) {
108
114
  csn.definitions[`dummy.${ artifactName }`] = dummy;
109
115
  }
110
116
 
117
+ /**
118
+ * Determines if the given artifact is a data product service.
119
+ *
120
+ * @param {object} artifact - The artifact to evaluate.
121
+ * @param {object} options - The options object containing feature flags.
122
+ * @returns {boolean} - Returns `true` if the artifact is a data product service, otherwise `false`.
123
+ */
124
+ function isDataProductService(artifact, options) {
125
+ return isBetaEnabled(options, 'projectionViews') && artifact.kind === 'service' && artifact['@DataIntegration.dataproduct.type'] === 'primary';
126
+ }
127
+
111
128
  module.exports = {
112
129
  processSqlServices,
113
130
  isSqlService,
114
131
  isDummyService,
132
+ isDataProductService,
115
133
  sqlServiceAnnotation,
116
134
  createServiceDummy,
117
135
  };
@@ -4,7 +4,7 @@ const {
4
4
  getServiceNames, forEachDefinition,
5
5
  getResultingName, forEachMemberRecursively, applyAnnotationsFromExtensions,
6
6
  } = require('../../model/csnUtils');
7
- const { setProp, isBetaEnabled } = require('../../base/model');
7
+ const { setProp } = require('../../base/model');
8
8
  const { getTransformers } = require('../transformUtils');
9
9
  const { ModelError } = require('../../base/error');
10
10
  const { forEach } = require('../../utils/objectUtils');
@@ -56,10 +56,10 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
56
56
 
57
57
  // Redirect associations/compositions between draft shadow nodes
58
58
  for (const name in draftNodes) {
59
- const shadowNode = csn.definitions[`${name}${draftSuffix}`];
59
+ const shadowNode = csn.definitions[`${ name }${ draftSuffix }`];
60
60
  // Might not exist because of previous errors
61
61
  if (shadowNode)
62
- redirectDraftTargets(csn.definitions[`${name}${draftSuffix}`], draftNodes);
62
+ redirectDraftTargets(csn.definitions[`${ name }${ draftSuffix }`], draftNodes);
63
63
  }
64
64
  }
65
65
  }
@@ -84,7 +84,7 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
84
84
  const draftNodeName = elem.target;
85
85
  // Sanity check
86
86
  if (!draftNode)
87
- throw new ModelError(`Expecting target to be resolved: ${JSON.stringify(elem, null, 2)}`);
87
+ throw new ModelError(`Expecting target to be resolved: ${ JSON.stringify(elem, null, 2) }`);
88
88
 
89
89
  // Ignore composition if not part of a service
90
90
  if (!isPartOfService(draftNodeName)) {
@@ -115,11 +115,11 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
115
115
  function generateDraftForHana( artifact, artifactName, draftRootName ) {
116
116
  // Sanity check
117
117
  if (!isPartOfService(artifactName))
118
- throw new ModelError(`Expecting artifact to be part of a service: ${JSON.stringify(artifact)}`);
118
+ throw new ModelError(`Expecting artifact to be part of a service: ${ JSON.stringify(artifact) }`);
119
119
 
120
120
 
121
121
  // The name of the draft shadow entity we should generate
122
- const draftsArtifactName = `${artifactName}${draftSuffix}`;
122
+ const draftsArtifactName = `${ artifactName }${ draftSuffix }`;
123
123
 
124
124
  generatedArtifacts[draftsArtifactName] = true;
125
125
 
@@ -132,7 +132,7 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
132
132
  // Ignore boolean return value. We know that we're inside a service or else we wouldn't have reached this code.
133
133
  const matchingService = getMatchingService(artifactName) || '';
134
134
  // Generate the DraftAdministrativeData projection into the service, unless there is already one
135
- const draftAdminDataProjectionName = `${matchingService}.DraftAdministrativeData`;
135
+ const draftAdminDataProjectionName = `${ matchingService }.DraftAdministrativeData`;
136
136
  let draftAdminDataProjection = csn.definitions[draftAdminDataProjectionName];
137
137
  if (!draftAdminDataProjection) {
138
138
  generatedArtifacts[draftAdminDataProjectionName] = true;
@@ -232,10 +232,9 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
232
232
  draftAdministrativeData.DraftAdministrativeData.notNull = true;
233
233
  addElement(draftAdministrativeData, draftsArtifact, artifactName);
234
234
 
235
- if (isBetaEnabled(options, 'draftMessages') || options.draftMessages) {
236
- const draftMessages = { DraftMessages: { '@Core.Computed': true, virtual: true, items: { type: 'DRAFT.DraftAdministrativeData_DraftMessage' } } };
237
- addElement(draftMessages, draftsArtifact, artifactName);
238
- }
235
+ const draftMessages = { DraftMessages: { '@Core.Computed': true, virtual: true, items: { type: 'DRAFT.DraftAdministrativeData_DraftMessage' } } };
236
+ addElement(draftMessages, draftsArtifact, artifactName);
237
+
239
238
  // Note that we may need to do the HANA transformation steps for managed associations
240
239
  // (foreign key field generation, generatedFieldName, creating ON-condition) by hand,
241
240
  // because the corresponding transformation steps have already been done on all artifacts
@@ -278,18 +277,19 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
278
277
  const sourceElement = source.elements[draftUUIDKey.ref[0]];
279
278
  const targetElement = {};
280
279
  forEach(sourceElement, (key, value) => {
281
- if(!key.startsWith('@') && key !== 'key')
280
+ if (!key.startsWith('@') && key !== 'key')
282
281
  targetElement[key] = value;
283
- })
282
+ });
284
283
 
285
- if(sourceElement.key) targetElement.notNull = true;
284
+ if (sourceElement.key)
285
+ targetElement.notNull = true;
286
286
 
287
- draftsArtifact.elements['DraftAdministrativeData' + (options.sqlMapping === 'hdbcds' ? '.' : '_') + draftUUIDKey.ref[0]] = targetElement;
287
+ draftsArtifact.elements[`DraftAdministrativeData${ options.sqlMapping === 'hdbcds' ? '.' : '_' }${ draftUUIDKey.ref[0] }`] = targetElement;
288
288
 
289
289
  draftAdministrativeData.DraftAdministrativeData.on = createAssociationPathComparison('DraftAdministrativeData',
290
290
  getNameForRef(draftUUIDKey),
291
291
  '=',
292
- `DraftAdministrativeData${pathDelimiter}DraftUUID`);
292
+ `DraftAdministrativeData${ pathDelimiter }DraftUUID`);
293
293
  // The notNull has been transferred to the foreign key field and must be removed on the association
294
294
  delete draftAdministrativeData.DraftAdministrativeData.notNull;
295
295
 
@@ -337,9 +337,9 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
337
337
  function getDraftShadowEntityFor( draftNode, draftNodeName ) {
338
338
  // Sanity check
339
339
  if (!draftNodes[draftNodeName])
340
- throw new ModelError(`Not a draft node: ${draftNodeName}`);
340
+ throw new ModelError(`Not a draft node: ${ draftNodeName }`);
341
341
 
342
- return { shadowTarget: csn.definitions[`${draftNodeName}${draftSuffix}`], shadowTargetName: `${draftNodeName}${draftSuffix}` };
342
+ return { shadowTarget: csn.definitions[`${ draftNodeName }${ draftSuffix }`], shadowTargetName: `${ draftNodeName }${ draftSuffix }` };
343
343
  }
344
344
  }
345
345
 
@@ -351,7 +351,7 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
351
351
  */
352
352
  function isPartOfService( artifactName ) {
353
353
  for (const serviceName of allServices) {
354
- if (artifactName.startsWith(`${serviceName}.`))
354
+ if (artifactName.startsWith(`${ serviceName }.`))
355
355
  return true;
356
356
  }
357
357
 
@@ -369,7 +369,7 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
369
369
  /** @type {false|string} */
370
370
  let match = false;
371
371
  for (const serviceName of allServices) {
372
- if (artifactName.startsWith(`${serviceName}.`) && (!match || serviceName.length < match.length))
372
+ if (artifactName.startsWith(`${ serviceName }.`) && (!match || serviceName.length < match.length))
373
373
  match = serviceName;
374
374
  }
375
375
  return match;