@sap/cds-compiler 4.3.2 → 4.4.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 (81) hide show
  1. package/CHANGELOG.md +29 -0
  2. package/lib/api/main.js +14 -24
  3. package/lib/api/options.js +1 -0
  4. package/lib/api/trace.js +38 -0
  5. package/lib/base/location.js +46 -1
  6. package/lib/base/message-registry.js +68 -16
  7. package/lib/base/messages.js +8 -3
  8. package/lib/checks/.eslintrc.json +1 -0
  9. package/lib/checks/actionsFunctions.js +1 -1
  10. package/lib/checks/annotationsOData.js +2 -2
  11. package/lib/checks/selectItems.js +4 -1
  12. package/lib/compiler/assert-consistency.js +3 -2
  13. package/lib/compiler/base.js +1 -1
  14. package/lib/compiler/builtins.js +25 -1
  15. package/lib/compiler/checks.js +6 -5
  16. package/lib/compiler/define.js +12 -10
  17. package/lib/compiler/extend.js +22 -22
  18. package/lib/compiler/finalize-parse-cdl.js +1 -1
  19. package/lib/compiler/generate.js +70 -53
  20. package/lib/compiler/kick-start.js +7 -5
  21. package/lib/compiler/populate.js +31 -22
  22. package/lib/compiler/propagator.js +6 -2
  23. package/lib/compiler/resolve.js +52 -17
  24. package/lib/compiler/shared.js +74 -38
  25. package/lib/compiler/tweak-assocs.js +64 -23
  26. package/lib/compiler/utils.js +40 -23
  27. package/lib/edm/.eslintrc.json +2 -0
  28. package/lib/edm/EdmPrimitiveTypeDefinitions.js +252 -0
  29. package/lib/edm/annotations/edmJson.js +994 -0
  30. package/lib/edm/annotations/genericTranslation.js +75 -421
  31. package/lib/edm/annotations/vocabularyDefinitions.js +160 -0
  32. package/lib/edm/csn2edm.js +12 -5
  33. package/lib/edm/edm.js +14 -73
  34. package/lib/edm/edmPreprocessor.js +6 -0
  35. package/lib/gen/Dictionary.json +187 -16
  36. package/lib/gen/language.checksum +1 -1
  37. package/lib/gen/language.interp +1 -1
  38. package/lib/gen/languageLexer.interp +1 -1
  39. package/lib/gen/languageLexer.js +1129 -671
  40. package/lib/gen/languageParser.js +4285 -4283
  41. package/lib/json/from-csn.js +13 -18
  42. package/lib/json/to-csn.js +11 -6
  43. package/lib/language/antlrParser.js +0 -1
  44. package/lib/language/docCommentParser.js +1 -1
  45. package/lib/language/errorStrategy.js +95 -30
  46. package/lib/language/genericAntlrParser.js +21 -1
  47. package/lib/main.js +13 -3
  48. package/lib/model/csnRefs.js +42 -8
  49. package/lib/model/csnUtils.js +14 -2
  50. package/lib/model/enrichCsn.js +33 -5
  51. package/lib/model/revealInternalProperties.js +5 -0
  52. package/lib/modelCompare/compare.js +76 -14
  53. package/lib/modelCompare/utils/filter.js +19 -12
  54. package/lib/optionProcessor.js +2 -0
  55. package/lib/render/.eslintrc.json +1 -1
  56. package/lib/render/manageConstraints.js +1 -0
  57. package/lib/render/toHdbcds.js +3 -0
  58. package/lib/render/toRename.js +3 -1
  59. package/lib/render/toSql.js +46 -92
  60. package/lib/render/utils/common.js +76 -0
  61. package/lib/render/utils/delta.js +17 -3
  62. package/lib/sql-identifier.js +1 -1
  63. package/lib/transform/db/.eslintrc.json +1 -0
  64. package/lib/transform/db/applyTransformations.js +30 -4
  65. package/lib/transform/db/associations.js +22 -10
  66. package/lib/transform/db/backlinks.js +6 -2
  67. package/lib/transform/db/expansion.js +2 -2
  68. package/lib/transform/db/transformExists.js +13 -39
  69. package/lib/transform/draft/db.js +14 -3
  70. package/lib/transform/draft/odata.js +5 -18
  71. package/lib/transform/effective/associations.js +46 -15
  72. package/lib/transform/effective/main.js +7 -2
  73. package/lib/transform/effective/misc.js +43 -24
  74. package/lib/transform/effective/queries.js +20 -22
  75. package/lib/transform/effective/types.js +6 -2
  76. package/lib/transform/forOdata.js +5 -2
  77. package/lib/transform/localized.js +1 -1
  78. package/lib/transform/parseExpr.js +73 -21
  79. package/lib/transform/translateAssocsToJoins.js +22 -15
  80. package/lib/utils/term.js +2 -2
  81. package/package.json +2 -1
@@ -333,6 +333,81 @@ function getDefaultTypeLengths( sqlDialect ) {
333
333
  return { ...sqlDefaultLengths.default, ...sqlDefaultLengths[sqlDialect] };
334
334
  }
335
335
 
336
+ /**
337
+ * Maps $-variables per SQL dialect to a renderable expression.
338
+ * Callers can use `.fallback` in case the wanted dialect is not found.
339
+ *
340
+ * IMPORTANT: There is no sqlDialect better-sqlite. This "fake" dialect is
341
+ * set in variableForDialect() below.
342
+ *
343
+ * @type {object}
344
+ */
345
+ const variablesToSql = {
346
+ fallback: {
347
+ // no fallback for $user.id and $user.tenant -> warning in call-site
348
+ '$user.locale': '\'en\'',
349
+ // $at.* are handled in all dialects -> there is no need for a fallback
350
+ },
351
+ hana: {
352
+ '$user.id': "SESSION_CONTEXT('APPLICATIONUSER')",
353
+ '$user.locale': "SESSION_CONTEXT('LOCALE')",
354
+ '$user.tenant': "SESSION_CONTEXT('TENANT')",
355
+ '$at.from': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-FROM'))",
356
+ '$at.to': "TO_TIMESTAMP(SESSION_CONTEXT('VALID-TO'))",
357
+ },
358
+ postgres: {
359
+ '$user.id': "current_setting('cap.applicationuser')",
360
+ '$user.locale': "current_setting('cap.locale')",
361
+ '$user.tenant': "current_setting('cap.tenant')",
362
+ '$at.from': "current_setting('cap.valid_from')::timestamp",
363
+ '$at.to': "current_setting('cap.valid_to')::timestamp",
364
+ },
365
+ 'better-sqlite': {
366
+ '$user.id': "session_context( '$user.id' )",
367
+ '$user.locale': "session_context( '$user.locale' )",
368
+ '$user.tenant': "session_context( '$user.tenant' )",
369
+ '$at.from': "session_context( '$valid.from' )",
370
+ '$at.to': "session_context( '$valid.to' )",
371
+ },
372
+ sqlite: {
373
+ // For sqlite, we render the string-format-time (strftime) function.
374
+ // Because the format of `current_timestamp` is like that: '2021-05-14 09:17:19' whereas
375
+ // the format for timestamps (at least in Node.js) is like that: '2021-01-01T00:00:00.000Z'
376
+ // --> Therefore the comparison in the temporal where clause doesn't work properly.
377
+ '$at.from': "strftime('%Y-%m-%dT%H:%M:%S.000Z', 'now')",
378
+ // + 1ms compared to $at.from
379
+ '$at.to': "strftime('%Y-%m-%dT%H:%M:%S.001Z', 'now')",
380
+ },
381
+ plain: {
382
+ '$at.from': 'current_timestamp',
383
+ '$at.to': 'current_timestamp',
384
+ },
385
+ h2: {
386
+ '$user.id': '@applicationuser',
387
+ '$user.locale': '@locale',
388
+ '$user.tenant': '@tenant',
389
+ '$at.from': '@valid_from',
390
+ '$at.to': '@valid_to',
391
+ },
392
+ };
393
+
394
+ /**
395
+ * Get a renderable string for given variable for the given options.sqlDialect.
396
+ * Note that this function does not handle `variableReplacements`. Callers should
397
+ * first check if the user has specified them and use them instead.
398
+ *
399
+ * @param {SqlOptions} options Used for `sqlDialect` and better-sqlite option.
400
+ * @param {string} variable Variable to render, e.g. `$user.id`.
401
+ * @return {string|null} `null` if the variable could not be found for the given dialect and in the fallback values.
402
+ */
403
+ function variableForDialect( options, variable ) {
404
+ const dialect = options.sqlDialect === 'sqlite' && options.betterSqliteSessionVariables
405
+ ? 'better-sqlite'
406
+ : options.sqlDialect;
407
+ return variablesToSql[dialect]?.[variable] || variablesToSql.fallback[variable] || null;
408
+ }
409
+
410
+
336
411
  /**
337
412
  * Get the element matching the column
338
413
  *
@@ -614,6 +689,7 @@ module.exports = {
614
689
  addContextMarkers,
615
690
  cdsToSqlTypes,
616
691
  cdsToHdbcdsTypes,
692
+ variableForDialect,
617
693
  hasHanaComment,
618
694
  getHanaComment,
619
695
  findElement,
@@ -71,7 +71,7 @@ class DeltaRenderer {
71
71
  /**
72
72
  * Render column modifications as SQL.
73
73
  */
74
- alterColumns(artifactName, columnName, delta, definitionsStr) {
74
+ alterColumns(artifactName, columnName, delta, definitionsStr, _eltName, _env) {
75
75
  return [ `ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER (${definitionsStr});` ];
76
76
  }
77
77
 
@@ -177,14 +177,28 @@ class DeltaRendererPostgres extends DeltaRenderer {
177
177
  /**
178
178
  * Render column modifications as Postgres SQL - no ( ), special NOT NULL.
179
179
  */
180
- alterColumns(artifactName, columnName, delta, definitionsStr) {
180
+ alterColumns(artifactName, columnName, delta, definitionsStr, eltName, env) {
181
181
  const sqls = [];
182
182
  if (delta.new.notNull === true || delta.new.key === true)
183
183
  definitionsStr = definitionsStr.replace(' NOT NULL', ''); // TODO: Is this robust enough?
184
184
  else if (delta.new.notNull === false || delta.new.$notNull === false)
185
185
  definitionsStr = definitionsStr.replace(' NULL', ''); // TODO: Is this robust enough?
186
186
 
187
- sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${definitionsStr};`);
187
+ if (delta.old.default && !delta.old.value) // Drop old default if any exists
188
+ sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER COLUMN ${columnName} DROP DEFAULT;`);
189
+
190
+ if (delta.new.default && !delta.new.value ) { // Alter column with default
191
+ const df = delta.new.default;
192
+ delete delta.new.default;
193
+ const eltStrNoDefault = this.scopedFunctions.renderElement(eltName, delta.new, null, null, env);
194
+ delta.new.default = df;
195
+ sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${eltStrNoDefault};`);
196
+ sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER COLUMN ${columnName} SET DEFAULT ${this.scopedFunctions.renderExpr(delta.new.default, env.withSubPath('default'))};`);
197
+ }
198
+ else { // Alter column without default
199
+ sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${definitionsStr};`);
200
+ }
201
+
188
202
  if (delta.new.notNull && !delta.old.notNull)
189
203
  sqls.push(`ALTER TABLE ${this.scopedFunctions.renderArtifactName(artifactName)} ALTER ${columnName} SET NOT NULL;`);
190
204
  else if (delta.old.notNull && !delta.new.notNull)
@@ -18,7 +18,7 @@
18
18
  //
19
19
  // Using the provided function smartId() instead of an identity function avoids
20
20
  // this situation: it constructs delimited identifiers for the reserved names.
21
- // Other names are returned directly to to avoid that people think that they
21
+ // Other names are returned directly to avoid that people think that they
22
22
  // had to use all-upper names in CDS.
23
23
 
24
24
  // Please note that `.` to `_` replacements (and similar replacements for the
@@ -3,6 +3,7 @@
3
3
  "plugins": ["sonarjs", "jsdoc"],
4
4
  "extends": ["plugin:jsdoc/recommended", "../../../.eslintrc-ydkjsi.json", "plugin:sonarjs/recommended"],
5
5
  "rules": {
6
+ "cds-compiler/message-no-quotes": "off",
6
7
  "prefer-const": "error",
7
8
  "quotes": ["error", "single", "avoid-escape"],
8
9
  "prefer-template": "error",
@@ -13,6 +13,7 @@
13
13
 
14
14
 
15
15
  const { setProp } = require('../../base/model');
16
+ const { xprInAnnoProperties } = require('../../compiler/builtins');
16
17
 
17
18
 
18
19
  /**
@@ -40,6 +41,7 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
40
41
  mixin: dictionary,
41
42
  ref: pathRef,
42
43
  $origin: () => {}, // no-op
44
+ '@': annotation,
43
45
  };
44
46
 
45
47
  const csnPath = [ ...path ];
@@ -48,7 +50,7 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
48
50
  }
49
51
  else if (options.directDict) {
50
52
  for (const name of Object.getOwnPropertyNames( parent ))
51
- standard( parent, name, parent[name] );
53
+ dictEntry( parent, name, parent[name] );
52
54
  }
53
55
  else {
54
56
  standard( parent, prop, parent[prop] );
@@ -67,7 +69,6 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
67
69
  function standard( _parent, _prop, node ) {
68
70
  if (!node || typeof node !== 'object' ||
69
71
  !{}.propertyIsEnumerable.call( _parent, _prop ) ||
70
- (typeof _prop === 'string' && _prop.startsWith('@')) ||
71
72
  (options.skipIgnore && node.$ignore) ||
72
73
  options.skipStandard?.[_prop]
73
74
  )
@@ -81,7 +82,7 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
81
82
 
82
83
  else {
83
84
  for (const name of Object.getOwnPropertyNames( node )) {
84
- const trans = transformers[name] || standard;
85
+ const trans = transformers[name] || transformers[name.charAt(0)] || standard;
85
86
  if (customTransformers[name])
86
87
  customTransformers[name](node, name, node[name], csnPath, _parent, _prop);
87
88
  trans( node, name, node[name], csnPath );
@@ -105,7 +106,7 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
105
106
 
106
107
  csnPath.push( entryName );
107
108
  for (const name of Object.getOwnPropertyNames( node )) {
108
- const trans = transformers[name] || standard;
109
+ const trans = transformers[name] || transformers[name.charAt(0)] || standard;
109
110
  if (customTransformers[name])
110
111
  customTransformers[name](node, name, node[name], csnPath, dict);
111
112
  trans( node, name, node[name], csnPath );
@@ -133,6 +134,30 @@ function applyTransformationsInternal( parent, prop, customTransformers, artifac
133
134
  csnPath.pop();
134
135
  }
135
136
 
137
+ /**
138
+ * Transformer for things that are annotations. When we have a "=" plus an expression of some sorts,
139
+ * we treat it like a "standard" thing.
140
+ *
141
+ * @param {object | Array} _parent the thing that has _prop
142
+ * @param {string|number} _prop the name of the current property or index
143
+ * @param {object} node The value of node[_prop]
144
+ */
145
+ function annotation( _parent, _prop, node ) {
146
+ if (options.processAnnotations) {
147
+ if (node?.['='] !== undefined && xprInAnnoProperties.some(xProp => node[xProp] !== undefined)) {
148
+ standard(_parent, _prop, node);
149
+ }
150
+ else if (node && typeof node === 'object') {
151
+ csnPath.push(_prop);
152
+
153
+ for (const name of Object.getOwnPropertyNames( node ))
154
+ annotation( node, name, node[name] );
155
+
156
+ csnPath.pop();
157
+ }
158
+ }
159
+ }
160
+
136
161
  /**
137
162
  * Special version of "dictionary" to apply artifactTransformers.
138
163
  *
@@ -279,4 +304,5 @@ module.exports = {
279
304
  * @property {object} [skipDict] stop drill-down on certain "dictionary" props
280
305
  * @property {boolean} [skipIgnore=true] Whether to skip $ignore elements or not
281
306
  * @property {boolean} [directDict=false] Implicitly set via applyTransformationsOnDictionary
307
+ * @property {boolean} [processAnnotations=false] Wether to process annotations and call custom transformers on them
282
308
  */
@@ -13,9 +13,11 @@ const {
13
13
  * @param {CSN.Model} csn
14
14
  * @param {object} csnUtils
15
15
  * @param {string} pathDelimiter
16
+ * @param {object} [iterateOptions={}]
17
+ * @param {CSN.Options} [options={}]
16
18
  * @returns {CSN.Model} Return the input csn, with the transformations applied
17
19
  */
18
- function attachOnConditions( csn, csnUtils, pathDelimiter ) {
20
+ function attachOnConditions( csn, csnUtils, pathDelimiter, iterateOptions = {}, options = {} ) {
19
21
  const { isManagedAssociation } = csnUtils;
20
22
 
21
23
  const alreadyHandled = new WeakMap();
@@ -27,8 +29,8 @@ function attachOnConditions( csn, csnUtils, pathDelimiter ) {
27
29
  if (isManagedAssociation(elem))
28
30
  transformManagedAssociation(elem, elemName);
29
31
  }
30
- }, /* only for views and entities */
31
- }, [], { skipIgnore: false, allowArtifact: artifact => (artifact.kind === 'entity') });
32
+ }, /* only for views and entities */
33
+ }, [], Object.assign({ skipIgnore: false, allowArtifact: artifact => (artifact.kind === 'entity') }, iterateOptions));
32
34
 
33
35
  return csn;
34
36
 
@@ -47,7 +49,7 @@ function attachOnConditions( csn, csnUtils, pathDelimiter ) {
47
49
  // Assemble an ON-condition with the foreign keys created in earlier steps
48
50
  const onCondParts = [];
49
51
  let joinWithAnd = false;
50
- if (elem.keys.length === 0) { // TODO: really kill instead of $ignore?
52
+ if (elem.keys.length === 0 && options.transformation !== 'effective') { // TODO: really kill instead of $ignore?
51
53
  elem.$ignore = true;
52
54
  }
53
55
  else {
@@ -55,12 +57,15 @@ function attachOnConditions( csn, csnUtils, pathDelimiter ) {
55
57
  // Assemble left hand side of 'assoc.key = fkey'
56
58
  const assocKeyArg = {
57
59
  ref: [
60
+ ...elemName.startsWith('$') ? [ '$self' ] : [],
58
61
  elemName,
59
- ].concat(foreignKey.ref),
62
+ ...foreignKey.ref,
63
+ ],
60
64
  };
61
65
  const fkName = `${elemName}${pathDelimiter}${foreignKey.as || implicitAs(foreignKey.ref)}`;
62
66
  const fKeyArg = {
63
67
  ref: [
68
+ ...fkName.startsWith('$') ? [ '$self' ] : [],
64
69
  fkName,
65
70
  ],
66
71
  };
@@ -104,9 +109,10 @@ function attachOnConditions( csn, csnUtils, pathDelimiter ) {
104
109
  * @param {CSN.Model} csn
105
110
  * @param {object} csnUtils
106
111
  * @param {string} pathDelimiter
112
+ * @param {boolean} [processOnInQueries=false] Wether to process on-conditions in queries (joins and mixins)
107
113
  * @returns {(artifact: CSN.Artifact, artifactName: string) => void} Callback for forEachDefinition
108
114
  */
109
- function getFKAccessFinalizer( csn, csnUtils, pathDelimiter ) {
115
+ function getFKAccessFinalizer( csn, csnUtils, pathDelimiter, processOnInQueries = false ) {
110
116
  const {
111
117
  inspectRef,
112
118
  } = csnUtils;
@@ -174,10 +180,16 @@ function getFKAccessFinalizer( csn, csnUtils, pathDelimiter ) {
174
180
  }
175
181
 
176
182
  if (artifact.query || artifact.projection) {
177
- applyTransformationsOnNonDictionary(artifact, artifact.query ? 'query' : 'projection', {
178
- orderBy: (parent, prop, thing, path) => applyTransformationsOnNonDictionary(parent, prop, transformer, {}, path),
179
- groupBy: (parent, prop, thing, path) => applyTransformationsOnNonDictionary(parent, prop, transformer, {}, path),
180
- }, {}, [ 'definitions', artifactName ]);
183
+ const transform = (parent, prop, thing, path) => applyTransformationsOnNonDictionary(parent, prop, transformer, {}, path);
184
+ const queryTransformers = {
185
+ orderBy: transform,
186
+ groupBy: transform,
187
+ where: transform,
188
+ having: transform,
189
+ };
190
+ if (processOnInQueries)
191
+ queryTransformers.on = transform;
192
+ applyTransformationsOnNonDictionary(artifact, artifact.query ? 'query' : 'projection', queryTransformers, {}, [ 'definitions', artifactName ]);
181
193
  }
182
194
 
183
195
 
@@ -28,7 +28,7 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
28
28
  function transformSelfInBacklinks( artifact, artifactName, dummy, path ) {
29
29
  // Fixme: For toHana mixins must be transformed, for toSql -d hana
30
30
  // mixin elements must be transformed, why can't toSql also use mixins?
31
- if (artifact.kind === 'entity' || artifact.query || (options.forHana && options.sqlMapping === 'hdbcds' && artifact.kind === 'type'))
31
+ if (options.transformation === 'effective' && artifact.elements || artifact.kind === 'entity' || artifact.query || (options.forHana && options.sqlMapping === 'hdbcds' && artifact.kind === 'type'))
32
32
  processDict(artifact.elements, path.concat([ 'elements' ]));
33
33
  if (artifact.query?.SELECT?.mixin)
34
34
  processDict(artifact.query.SELECT.mixin, path.concat([ 'query', 'SELECT', 'mixin' ]));
@@ -66,6 +66,9 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
66
66
  // Don't add braces if it is a single expression (ignoring superfluous braces)
67
67
  const multipleExprs = elem.on.filter(x => x !== '(' && x !== ')' ).length > 3;
68
68
  elem.on = processExpressionArgs(elem.on, pathToOn);
69
+ const column = csnUtils.getColumn(elem);
70
+ if (column?.cast?.on) // avoid difference between column and element
71
+ column.cast.on = elem.on;
69
72
 
70
73
  /**
71
74
  * Process the args
@@ -187,7 +190,8 @@ function getBacklinkTransformer( csnUtils, messageFunctions, options, pathDelimi
187
190
  if (assoc.keys.length)
188
191
  return transformDollarSelfComparisonWithManagedAssoc(assocOp, assoc, assocName, elemName);
189
192
 
190
- elem.$ignore = true;
193
+ if (options.transformation !== 'effective')
194
+ elem.$ignore = true;
191
195
  return [];
192
196
  }
193
197
 
@@ -33,7 +33,7 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
33
33
  },
34
34
  columns: (parent, name, columns, path) => {
35
35
  const artifact = csn.definitions[path[1]];
36
- csnUtils.initDefinition(artifact); // potentially no initialized, yet
36
+ csnUtils.initDefinition(artifact); // potentially not initialized, yet
37
37
  if (!hasAnnotationValue(artifact, '@cds.persistence.table')) {
38
38
  const root = csnUtils.get$combined({ SELECT: parent });
39
39
  // TODO: replace with the correct options.transformation?
@@ -719,7 +719,7 @@ function expandStructureReferences( csn, options, pathDelimiter, messageFunction
719
719
  }
720
720
  }
721
721
  else { // the thing is not shadowed - use the name from the base
722
- const col = { ref: [ part ] };
722
+ const col = part.startsWith('$') ? { ref: [ base[part][0].parent, part ] } : { ref: [ part ] };
723
723
  if (isComplexQuery) // $env: tableAlias
724
724
  setProp(col, '$env', base[part][0].parent);
725
725
 
@@ -273,14 +273,6 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
273
273
  ref, head, tail,
274
274
  } = getFirstAssoc(current, exprPath.concat(i));
275
275
 
276
- const lastAssoc = getLastAssoc(current, exprPath.concat(i));
277
- // toE.toF.id -> we must not end on a non-assoc - this will also be caught downstream by
278
- // '“EXISTS” can only be used with associations/compositions, found $(TYPE)'
279
- // But the error might not be clear, since it could be because of our rewritten stuff. The later check
280
- // checks for exists id -> our rewrite turns toE.toF.id into toE[exists toF[exists id]], leading to the same error
281
- if (lastAssoc.tail.length > 0)
282
- error(null, current.$path, { id: lastAssoc.tail[0].id ? lastAssoc.tail[0].id : lastAssoc.tail[0], name: lastAssoc.ref.id ? lastAssoc.ref.id : lastAssoc.ref }, 'Unexpected path step $(ID) after association $(NAME) in "EXISTS"');
283
-
284
276
  const newThing = [ ...head, nestFilters(head.length + 1, ref, tail, exprPath.concat([ i ])) ];
285
277
  expr[i].ref = newThing;
286
278
  }
@@ -312,11 +304,6 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
312
304
  const base = getBase(queryBase, isPrefixedWithTableAlias, current, exprPath.concat(i));
313
305
  const { root, ref } = getFirstAssoc(current, exprPath.concat(i));
314
306
 
315
- if (!root.target) {
316
- error(null, exprPath.concat(i), { type: root.type }, '“EXISTS” can only be used with associations/compositions, found $(TYPE)');
317
- return { result: [], leftovers: [] };
318
- }
319
-
320
307
  const subselect = getSubselect(root.target, ref, sources);
321
308
 
322
309
  const target = subselect.SELECT.from.as; // use subquery alias as target - prevent shadowing
@@ -331,7 +318,7 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
331
318
 
332
319
  newExpr.push('exists');
333
320
  if (ref && ref.where) {
334
- const remappedWhere = remapExistingWhere(target, ref.where);
321
+ const remappedWhere = remapExistingWhere(target, ref.where, exprPath, current);
335
322
  if (remappedWhere.length > 3)
336
323
  subselect.SELECT.where.push(...[ 'and', '(', ...remappedWhere, ')' ]);
337
324
  else
@@ -381,7 +368,7 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
381
368
  */
382
369
  function translateManagedAssocToWhere( root, target, isPrefixedWithTableAlias, base, current ) {
383
370
  if (current.$scope === '$self') {
384
- error('ref-unexpected-exists-self', current.$path, { id: current.ref[0], name: 'exists' }, 'With $(NAME), path steps must not start with $(ID)');
371
+ error('ref-unexpected-self', current.$path, { '#': 'exists', id: current.ref[0], name: 'exists' });
385
372
  return [];
386
373
  }
387
374
 
@@ -572,27 +559,6 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
572
559
  };
573
560
  }
574
561
 
575
- /**
576
- * Get the last association from the expression part - similar to getFirstAssoc
577
- *
578
- * @param {object} xprPart
579
- * @param {CSN.Path} path
580
- * @returns {{head: Array, root: CSN.Element, ref: string|object, tail: Array}} The last assoc (root), the corresponding ref (ref), anything before the ref (head) and the rest of the ref (tail).
581
- */
582
- function getLastAssoc( xprPart, path ) {
583
- const { links, art } = inspectRef(path);
584
- for (let i = xprPart.ref.length - 1; i > -1; i--) {
585
- if (links[i].art && links[i].art.target) {
586
- return {
587
- head: (i === 0 ? [] : xprPart.ref.slice(0, i)), root: links[i].art, ref: xprPart.ref[i], tail: xprPart.ref.slice(i + 1),
588
- };
589
- }
590
- }
591
- return {
592
- head: (xprPart.ref.length === 1 ? [] : xprPart.ref.slice(0, xprPart.ref.length - 1)), root: art, ref: xprPart.ref[xprPart.ref.length - 1], tail: [],
593
- };
594
- }
595
-
596
562
  /**
597
563
  * Check (using inspectRef -> links), whether the first path step is an entity or query source
598
564
  *
@@ -713,13 +679,21 @@ function handleExists( csn, options, error, inspectRef, initDefinition, dropDefi
713
679
  *
714
680
  * This function does this by adding the assoc target before all the refs so that the refs are resolvable in the WHERE.
715
681
  *
682
+ * This function also rejects $self paths in filter conditions.
683
+ *
716
684
  * @param {string} target
717
685
  * @param {TokenStream} where
718
- * @returns {TokenStream} The input-where with the refs transformed to absolute ones
686
+ * @param {CSN.Path} path path to the part, used if error needs to be thrown
687
+ * @param {CSN.Artifact} parent the host of the `where`, used if error needs to be thrown
688
+ *
689
+ * @returns {TokenStream} where The input-where with the refs transformed to absolute ones
719
690
  */
720
- function remapExistingWhere( target, where ) {
691
+ function remapExistingWhere( target, where, path, parent ) {
721
692
  return where.map((part) => {
722
- if (part.ref && part.$scope !== '$magic') {
693
+ if (part.$scope === '$self') {
694
+ error('ref-unexpected-self', path, { '#': 'exists-filter', elemref: parent, id: part.ref[0] });
695
+ }
696
+ else if (part.ref && part.$scope !== '$magic') {
723
697
  part.ref = [ target, ...part.ref ];
724
698
  return part;
725
699
  }
@@ -2,7 +2,7 @@
2
2
 
3
3
  const {
4
4
  hasAnnotationValue, getServiceNames, forEachDefinition,
5
- getResultingName, forEachMemberRecursively,
5
+ getResultingName, forEachMemberRecursively, applyAnnotationsFromExtensions,
6
6
  } = require('../../model/csnUtils');
7
7
  const { setProp, isDeprecatedEnabled } = require('../../base/model');
8
8
  const { getTransformers } = require('../transformUtils');
@@ -29,9 +29,12 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
29
29
  } = getTransformers(csn, options, pathDelimiter);
30
30
  const { getCsnDef, isComposition } = csnUtils;
31
31
  const { error, warning } = messageFunctions;
32
+ const generatedArtifacts = Object.create(null);
32
33
 
33
34
  forEachDefinition(csn, generateDraft);
34
35
 
36
+ applyAnnotationsFromExtensions(csn, { filter: name => generatedArtifacts[name], applyToElements: false });
37
+
35
38
  /**
36
39
  * Generate the draft stuff for a given artifact
37
40
  *
@@ -117,6 +120,8 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
117
120
  // The name of the draft shadow entity we should generate
118
121
  const draftsArtifactName = `${artifactName}${draftSuffix}`;
119
122
 
123
+ generatedArtifacts[draftsArtifactName] = true;
124
+
120
125
  // extract keys for UUID inspection
121
126
  const keys = [];
122
127
  forEachMemberRecursively(artifact, (elt, name, prop, path) => {
@@ -132,7 +137,7 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
132
137
  'Entity annotated with “@odata.draft.enabled” should have exactly one key element, but found $(COUNT)');
133
138
  }
134
139
  else {
135
- const uuidCount = keys.reduce((acc, k) => ((k.type === 'cds.String' && k.$renamed === 'cds.UUID' && k.length === 36) ? acc + 1 : acc), 0);
140
+ const uuidCount = keys.reduce((acc, k) => ((k.type === 'cds.UUID' || k.type === 'cds.String' && k.$renamed === 'cds.UUID' && k.length === 36) ? acc + 1 : acc), 0);
136
141
  if (uuidCount === 0)
137
142
  warning(null, [ 'definitions', artifactName ], 'Entity annotated with “@odata.draft.enabled” should have one key element of type “cds.UUID”');
138
143
  }
@@ -143,10 +148,16 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
143
148
  const draftAdminDataProjectionName = `${matchingService}.DraftAdministrativeData`;
144
149
  let draftAdminDataProjection = csn.definitions[draftAdminDataProjectionName];
145
150
  if (!draftAdminDataProjection) {
151
+ generatedArtifacts[draftAdminDataProjectionName] = true;
146
152
  draftAdminDataProjection = createAndAddDraftAdminDataProjection(matchingService, true);
147
153
 
148
154
  if (!draftAdminDataProjection.projection.columns && draftAdminDataProjection.elements.DraftUUID)
149
155
  draftAdminDataProjection.projection.columns = Object.keys(draftAdminDataProjection.elements).map(e => (e === 'DraftUUID' ? { key: true, ref: [ 'DraftAdministrativeData', e ] } : { ref: [ 'DraftAdministrativeData', e ] }));
156
+
157
+ if (options.transformation === 'effective' && draftAdminDataProjection.projection) {
158
+ draftAdminDataProjection.query = { SELECT: draftAdminDataProjection.projection };
159
+ delete draftAdminDataProjection.projection;
160
+ }
150
161
  }
151
162
 
152
163
  // Barf if it is not an entity or not what we expect
@@ -159,7 +170,7 @@ function generateDrafts( csn, options, pathDelimiter, messageFunctions ) {
159
170
 
160
171
  const persistenceName = getResultingName(csn, options.sqlMapping, draftsArtifactName);
161
172
  // Duplicate the artifact as a draft shadow entity
162
- if (csn.definitions[persistenceName]) {
173
+ if (csn.definitions[persistenceName] && !(options.transformation === 'effective' && csn.definitions[persistenceName].kind === 'entity' && csn.definitions[persistenceName].elements.DraftAdministrativeData_DraftUUID)) {
163
174
  const definingDraftRoot = draftRoots.get(csn.definitions[persistenceName]);
164
175
  if (!definingDraftRoot) {
165
176
  error(null, [ 'definitions', artifactName ], { name: persistenceName },
@@ -1,10 +1,9 @@
1
1
  'use strict';
2
2
 
3
- const { forEachDefinition, getServiceNames } = require('../../model/csnUtils');
3
+ const { forEachDefinition, getServiceNames, applyAnnotationsFromExtensions } = require('../../model/csnUtils');
4
4
  const { forEach } = require('../../utils/objectUtils');
5
5
  const { isArtifactInSomeService, getServiceOfArtifact } = require('../odata/utils');
6
6
  const { getTransformers } = require('../transformUtils');
7
- const { ModelError } = require('../../base/error');
8
7
  const { makeMessageFunction } = require('../../base/messages');
9
8
 
10
9
  /**
@@ -29,7 +28,6 @@ const { makeMessageFunction } = require('../../base/messages');
29
28
  */
30
29
  function generateDrafts( csn, options, services ) {
31
30
  const {
32
- createForeignKeyElement,
33
31
  createAndAddDraftAdminDataProjection, createScalarElement,
34
32
  createAssociationElement, createAssociationPathComparison,
35
33
  addElement, createAction, assignAction,
@@ -52,7 +50,7 @@ function generateDrafts( csn, options, services ) {
52
50
  const externalServices = services.filter(serviceName => csn.definitions[serviceName]['@cds.external']);
53
51
  // @ts-ignore
54
52
  const isExternalServiceMember = (_art, name) => externalServices.includes(getServiceName(name));
55
-
53
+ const filterDict = Object.create(null);
56
54
  forEachDefinition(csn, (def, defName) => {
57
55
  // Generate artificial draft fields for entities/views if requested, ignore if not part of a service
58
56
  if (def.kind === 'entity' && def['@odata.draft.enabled'] && isArtifactInSomeService(defName, services))
@@ -61,6 +59,7 @@ function generateDrafts( csn, options, services ) {
61
59
  visitedArtifacts[defName] = true;
62
60
  }, { skipArtifact: isExternalServiceMember });
63
61
 
62
+ applyAnnotationsFromExtensions(csn, { override: true, filter: name => filterDict[name] });
64
63
  return csn;
65
64
  /**
66
65
  * Generate all that is required in ODATA for draft enablement of 'artifact' into the artifact,
@@ -77,11 +76,6 @@ function generateDrafts( csn, options, services ) {
77
76
  * @param {CSN.Artifact} rootArtifact artifact where composition traversal started
78
77
  */
79
78
  function generateDraftForOdata( artifact, artifactName, rootArtifact ) {
80
- // Sanity check
81
- // @ts-ignore
82
- if (!isArtifactInSomeService(artifactName, services))
83
- throw new ModelError(`Expecting artifact to be part of a service: ${JSON.stringify(artifact)}`);
84
-
85
79
  // Nothing to do if already draft-enabled (composition traversal may have circles)
86
80
  if ((artifact['@Common.DraftRoot.PreparationAction'] || artifact['@Common.DraftNode.PreparationAction']) &&
87
81
  artifact.actions && artifact.actions.draftPrepare)
@@ -106,9 +100,11 @@ function generateDrafts( csn, options, services ) {
106
100
  resetAnnotation(artifact, '@Common.DraftRoot.ActivationAction', 'draftActivate', info, [ 'definitions', draftAdminDataProjectionName ]);
107
101
  resetAnnotation(artifact, '@Common.DraftRoot.EditAction', 'draftEdit', info, [ 'definitions', draftAdminDataProjectionName ]);
108
102
  resetAnnotation(artifact, '@Common.DraftRoot.PreparationAction', 'draftPrepare', info, [ 'definitions', draftAdminDataProjectionName ]);
103
+ filterDict[artifactName] = true;
109
104
  }
110
105
  else {
111
106
  resetAnnotation(artifact, '@Common.DraftNode.PreparationAction', 'draftPrepare', info, [ 'definitions', draftAdminDataProjectionName ]);
107
+ filterDict[artifactName] = true;
112
108
  }
113
109
 
114
110
  Object.values(artifact.elements || {}).forEach( (elem) => {
@@ -141,15 +137,6 @@ function generateDrafts( csn, options, services ) {
141
137
  draftAdministrativeData.DraftAdministrativeData['@UI.Hidden'] = true;
142
138
  addElement(draftAdministrativeData, artifact, artifactName);
143
139
 
144
- // Note that we need to do the ODATA transformation steps for managed associations
145
- // (foreign key field generation, generatedFieldName) by hand, because the corresponding
146
- // transformation steps have already been done on all artifacts when we come here)
147
- let uuidDraftKey = draftAdministrativeData.DraftAdministrativeData.keys.filter(key => key.ref && key.ref.length === 1 && key.ref[0] === 'DraftUUID');
148
- if (uuidDraftKey && uuidDraftKey[0]) {
149
- uuidDraftKey = uuidDraftKey[0]; // filter returns an array, but it has only one element
150
- const path = [ 'definitions', artifactName, 'elements', 'DraftAdministrativeData', 'keys', 0 ];
151
- createForeignKeyElement(draftAdministrativeData.DraftAdministrativeData, 'DraftAdministrativeData', uuidDraftKey, artifact, artifactName, path);
152
- }
153
140
  // SiblingEntity : Association to one <artifact> on (... IsActiveEntity unequal, all other key fields equal ...)
154
141
  const siblingEntity = createAssociationElement('SiblingEntity', artifactName, false);
155
142
  siblingEntity.SiblingEntity.cardinality = { max: 1 };