@sap/cds-compiler 4.6.0 → 4.7.4

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 (70) hide show
  1. package/CHANGELOG.md +44 -0
  2. package/bin/cds_update_identifiers.js +6 -2
  3. package/bin/cdsc.js +1 -1
  4. package/doc/CHANGELOG_ARCHIVE.md +9 -9
  5. package/doc/CHANGELOG_BETA.md +6 -0
  6. package/lib/api/main.js +56 -9
  7. package/lib/api/options.js +6 -3
  8. package/lib/api/validate.js +20 -29
  9. package/lib/base/message-registry.js +27 -3
  10. package/lib/base/messages.js +8 -3
  11. package/lib/base/model.js +2 -0
  12. package/lib/checks/dbFeatureFlags.js +28 -0
  13. package/lib/checks/elements.js +81 -13
  14. package/lib/checks/enricher.js +3 -2
  15. package/lib/checks/validator.js +38 -4
  16. package/lib/compiler/assert-consistency.js +4 -4
  17. package/lib/compiler/checks.js +5 -4
  18. package/lib/compiler/define.js +2 -2
  19. package/lib/compiler/generate.js +2 -1
  20. package/lib/compiler/populate.js +5 -1
  21. package/lib/compiler/propagator.js +3 -11
  22. package/lib/compiler/shared.js +2 -1
  23. package/lib/compiler/tweak-assocs.js +43 -24
  24. package/lib/edm/annotations/edmJson.js +3 -0
  25. package/lib/edm/annotations/genericTranslation.js +156 -106
  26. package/lib/edm/annotations/preprocessAnnotations.js +11 -14
  27. package/lib/edm/csn2edm.js +27 -24
  28. package/lib/edm/edm.js +8 -8
  29. package/lib/edm/edmPreprocessor.js +135 -37
  30. package/lib/edm/edmUtils.js +20 -7
  31. package/lib/gen/Dictionary.json +2 -1
  32. package/lib/gen/language.checksum +1 -1
  33. package/lib/gen/language.interp +9 -11
  34. package/lib/gen/languageParser.js +5942 -5446
  35. package/lib/json/to-csn.js +7 -114
  36. package/lib/language/genericAntlrParser.js +106 -48
  37. package/lib/model/cloneCsn.js +203 -0
  38. package/lib/model/csnRefs.js +11 -3
  39. package/lib/model/csnUtils.js +42 -85
  40. package/lib/optionProcessor.js +2 -2
  41. package/lib/render/manageConstraints.js +1 -1
  42. package/lib/render/toCdl.js +133 -88
  43. package/lib/render/toHdbcds.js +1 -5
  44. package/lib/render/toSql.js +7 -9
  45. package/lib/render/utils/common.js +9 -16
  46. package/lib/transform/addTenantFields.js +277 -102
  47. package/lib/transform/db/applyTransformations.js +14 -9
  48. package/lib/transform/db/backlinks.js +2 -1
  49. package/lib/transform/db/constraints.js +60 -82
  50. package/lib/transform/db/expansion.js +6 -6
  51. package/lib/transform/db/featureFlags.js +5 -0
  52. package/lib/transform/db/flattening.js +4 -4
  53. package/lib/transform/db/killAnnotations.js +1 -0
  54. package/lib/transform/db/rewriteCalculatedElements.js +2 -2
  55. package/lib/transform/db/transformExists.js +12 -0
  56. package/lib/transform/db/views.js +5 -2
  57. package/lib/transform/draft/odata.js +7 -6
  58. package/lib/transform/effective/associations.js +2 -1
  59. package/lib/transform/effective/main.js +3 -2
  60. package/lib/transform/effective/types.js +6 -3
  61. package/lib/transform/forOdata.js +39 -24
  62. package/lib/transform/forRelationalDB.js +34 -27
  63. package/lib/transform/localized.js +29 -9
  64. package/lib/transform/odata/flattening.js +419 -0
  65. package/lib/transform/odata/toFinalBaseType.js +95 -15
  66. package/lib/transform/odata/typesExposure.js +9 -7
  67. package/lib/transform/transformUtils.js +7 -6
  68. package/lib/transform/translateAssocsToJoins.js +3 -3
  69. package/lib/utils/objectUtils.js +14 -0
  70. package/package.json +1 -1
@@ -14,7 +14,7 @@
14
14
  const { locationString } = require('../base/messages');
15
15
  const { isBetaEnabled, isDeprecatedEnabled } = require('../base/model');
16
16
  const { pathName } = require('../compiler/utils');
17
- const { CompilerAssertion, ModelError } = require('../base/error');
17
+ const { CompilerAssertion } = require('../base/error');
18
18
 
19
19
  const compilerVersion = require('../../package.json').version;
20
20
  const creator = `CDS Compiler v${ compilerVersion }`;
@@ -212,116 +212,10 @@ const castProperties = [
212
212
  ];
213
213
 
214
214
  const csnDictionaries = [
215
- 'args', 'params', 'enum', 'mixin', 'elements', 'actions', 'definitions',
215
+ 'args', 'params', 'enum', 'mixin', 'elements', 'actions', 'definitions', 'vocabularies',
216
216
  ];
217
217
  const csnDirectValues = [ 'val' ]; // + all starting with '@' - TODO: still relevant
218
218
 
219
- /**
220
- * Sort property names of CSN according to sequence which is also used by the compactModel function
221
- * Only returns enumerable properties, except for certain hidden properties
222
- * if requested (cloneOptions != false): $location, elements.
223
- *
224
- * If cloneOptions is false or if either cloneOptions.testMode or cloneOptions.testSortCsn
225
- * are set, definitions are also sorted.
226
- *
227
- * @param {object} csn
228
- * @param {CSN.Options|false} cloneOptions
229
- */
230
- function sortCsn( csn, cloneOptions = false ) {
231
- if (cloneOptions && typeof cloneOptions === 'object')
232
- initModuleVars( cloneOptions );
233
-
234
- if (Array.isArray(csn))
235
- return csn.map( v => (!v || typeof v !== 'object' ? v : sortCsn(v, cloneOptions) ) );
236
- const r = {};
237
- for (const n of Object.keys(csn).sort( compareProperties ) ) {
238
- const sortDict = n === 'definitions' &&
239
- (!cloneOptions || cloneOptions.testMode || cloneOptions.testSortCsn);
240
- const val = csn[n];
241
- if (!val || typeof val !== 'object' || csnDirectValues.includes(n))
242
- r[n] = val;
243
- else if (n.charAt(0) === '@')
244
- r[n] = cloneAnnotationValue( val );
245
- else if (csnDictionaries.includes(n) && !Array.isArray(val))
246
- // Array check for property `args` which may either be a dictionary or an array.
247
- r[n] = csnDictionary( val, sortDict, cloneOptions );
248
-
249
- else
250
- r[n] = sortCsn(val, cloneOptions);
251
- }
252
- if (cloneOptions && typeof csn === 'object') {
253
- if ({}.hasOwnProperty.call( csn, '$sources' ) && !r.$sources)
254
- setHidden( r, '$sources', csn.$sources );
255
- if ({}.hasOwnProperty.call( csn, '$location' ) && !r.$location)
256
- setHidden( r, '$location', csn.$location );
257
- if ({}.hasOwnProperty.call( csn, '$path' )) // used in generic reference flattener
258
- setHidden( r, '$path', csn.$path );
259
- if ({}.hasOwnProperty.call( csn, '$paths' )) // used in generic reference flattener
260
- setHidden( r, '$paths', csn.$paths );
261
- if (hasNonEnumerable( csn, 'elements' ) && !r.elements) // non-enumerable 'elements'
262
- setHidden( r, 'elements', csnDictionary( csn.elements, false, cloneOptions ) );
263
- if (hasNonEnumerable( csn, '$tableConstraints' ) && !r.$tableConstraints)
264
- setHidden( r, '$tableConstraints', csn.$tableConstraints );
265
- if (cloneOptions.hiddenPropertiesToClone) {
266
- cloneOptions.hiddenPropertiesToClone.forEach((property) => {
267
- if ({}.hasOwnProperty.call( csn, property )) // used in generic reference flattener
268
- setHidden( r, property, csn[property] );
269
- });
270
- }
271
- }
272
- return r;
273
- }
274
-
275
- function cloneAnnotationValue( val ) {
276
- if (typeof val !== 'object') // scalar
277
- return val;
278
- return JSON.parse( JSON.stringify( val ) );
279
- }
280
-
281
- /**
282
- * Check whether the given object has non enumerable property.
283
- * Ensure that we don't take it from the prototype, only "directly" - we accidentally
284
- * cloned elements with a cds.linked input otherwise.
285
- *
286
- * @param {object} object
287
- * @param {string} property
288
- * @returns
289
- */
290
- function hasNonEnumerable( object, property ) {
291
- return {}.hasOwnProperty.call( object, property ) &&
292
- !{}.propertyIsEnumerable.call( object, property );
293
- }
294
-
295
- /**
296
- * @param {object} csn
297
- * @param {boolean} sort
298
- * @param {CSN.Options | false} cloneOptions If != false,
299
- * cloneOptions.dictionaryPrototype is used and cloneOptions are
300
- * passed to sort().
301
- * @returns {object}
302
- */
303
- function csnDictionary( csn, sort, cloneOptions = false ) {
304
- if (!csn || Array.isArray(csn)) // null or strange CSN
305
- return csn;
306
- const proto = cloneOptions && (typeof cloneOptions === 'object') &&
307
- cloneOptions.dictionaryPrototype;
308
- // eslint-disable-next-line no-nested-ternary
309
- const dictProto = (typeof proto === 'object') // including null
310
- ? proto
311
- : (proto) ? Object.prototype : null;
312
- const r = Object.create( dictProto );
313
- for (const n of (sort) ? Object.keys(csn).sort() : Object.keys(csn)) {
314
- // CSN does not allow any dictionary that are not objects.
315
- // The compiler handles it, but a pre-transformed OData CSN won't trigger recompilation.
316
- if (csn[n] && typeof csn[n] === 'object')
317
- r[n] = sortCsn(csn[n], cloneOptions);
318
- else
319
- throw new ModelError(`Found non-object dictionary entry: "${ n }" of type "${ typeof csn[n] }"`);
320
- }
321
-
322
- return r;
323
- }
324
-
325
219
  /**
326
220
  * Compact the given XSN model and transform it into CSN.
327
221
  *
@@ -1320,7 +1214,7 @@ function exprInternal( node, xprParens ) {
1320
1214
  return extra( call, node );
1321
1215
  }
1322
1216
  if (node.query)
1323
- return query( node.query, null, null, null, 1 );
1217
+ return query( node.query, null, null, null, (node.$parens ? 1 - node.$parens.length : 1) );
1324
1218
  if (!node.op) // parse error
1325
1219
  return { xpr: [] };
1326
1220
 
@@ -1336,7 +1230,7 @@ function exprInternal( node, xprParens ) {
1336
1230
  return cast( expression( node.args[0] ), node );
1337
1231
  case 'list':
1338
1232
  return extra( { list: node.args.map( expression ) }, node, 0 );
1339
- default: { // '=', 'and', CSN v0 input: binary (n-ary) and unary prefix
1233
+ default: { // CSN v0 input (A2J: '='/'and'): binary (n-ary) and unary prefix
1340
1234
  if (!node.args.length)
1341
1235
  return { xpr: [] };
1342
1236
  const nary = [];
@@ -1345,7 +1239,7 @@ function exprInternal( node, xprParens ) {
1345
1239
  val = 'nary';
1346
1240
  node = {
1347
1241
  op: { val },
1348
- args: (nary.length > 2 ? nary.slice(1) : nary), // length 1,2 only with CSN v0
1242
+ args: (nary.length > 2 ? nary.slice(1) : nary),
1349
1243
  $parens: node.$parens,
1350
1244
  };
1351
1245
  }
@@ -1361,6 +1255,7 @@ function flattenInternalXpr( array, op ) {
1361
1255
  return array.flat( Infinity );
1362
1256
  if (array.length < 5 || op !== 'nary')
1363
1257
  return array;
1258
+ // nary: [ ‹a›, '+', ‹b›, '+', ‹c› ] → [ [ ‹a›, '+', ‹b› ], '+', ‹c› ]
1364
1259
  let left = array.slice( 0, 3 );
1365
1260
  let index = 3;
1366
1261
  while (index < array.length)
@@ -1612,12 +1507,10 @@ function initModuleVars( options = { csnFlavor: 'gensrc' } ) {
1612
1507
  }
1613
1508
 
1614
1509
  module.exports = {
1615
- cloneCsnDictionary: (csn, options) => csnDictionary(csn, false, options),
1616
- cloneAnnotationValue,
1617
1510
  compactModel,
1618
1511
  compactQuery,
1619
1512
  compactExpr,
1620
- sortCsn,
1621
1513
  csnDictionaries,
1622
1514
  csnDirectValues,
1515
+ csnPropertyOrder: propertyOrder,
1623
1516
  };
@@ -103,6 +103,7 @@ Object.assign(GenericAntlrParser.prototype, {
103
103
  fragileAlias,
104
104
  identAst,
105
105
  pushXprToken,
106
+ pushOpToken,
106
107
  argsExpression,
107
108
  valuePathAst,
108
109
  fixNewKeywordPlacement,
@@ -360,6 +361,8 @@ function attachLocation( art ) {
360
361
  art.location = this.tokenLocation(this._ctx.start, this._ctx.stop);
361
362
  return art;
362
363
  }
364
+ if (!this._ctx.stop)
365
+ return art;
363
366
 
364
367
  // The last token (this._ctx.stop) may be a multi-line string literal, in which
365
368
  // case we can't rely on `this._ctx.stop.line`.
@@ -639,14 +642,16 @@ function surroundByParens( expr, open, close, asQuery = false ) {
639
642
  expr.$parens.push( location );
640
643
  else
641
644
  expr.$parens = [ location ];
645
+ if (expr.$opPrecedence)
646
+ expr.$opPrecedence = null;
642
647
  return (asQuery) ? { query: expr, location } : expr;
643
648
  }
644
649
 
645
650
 
646
- function tokensToStringRepresentation( matchedRule ) {
651
+ function tokensToStringRepresentation( start, stop ) {
647
652
  const tokens = this._input.getTokens(
648
- matchedRule.start.tokenIndex,
649
- matchedRule.stop.tokenIndex + 1, null
653
+ start.tokenIndex,
654
+ stop.tokenIndex + 1, null
650
655
  ).filter(tok => tok.channel === antlr4.Token.DEFAULT_CHANNEL);
651
656
  if (tokens.length === 0)
652
657
  return '';
@@ -772,33 +777,6 @@ function identAst( token, category, noTokenTypeCheck = false ) {
772
777
  return { id, $delimited: true, location: this.tokenLocation( token ) };
773
778
  }
774
779
 
775
- // only to be used in @after
776
- // TODO: remove compatible stuff (A2J/checks use op: 'and'/'=')
777
- function argsExpression( args, nary, location ) {
778
- // console.log('AE:',args);
779
- if (args.length === 1)
780
- return args[0];
781
- if (nary && args.length === 3 && args[1]?.val === nary) {
782
- return this.attachLocation( {
783
- op: { val: nary, location: args[1].location },
784
- args: [ args[0], args[2] ],
785
- location: undefined,
786
- } );
787
- }
788
- // eslint-disable-next-line no-nested-ternary
789
- const val = nary === '?:' ? nary
790
- : (nary && nary !== '=' ? 'nary' : 'ixpr');
791
- const op = {
792
- val, // there is no n-ary in rule conditionTerm
793
- location: this.startLocation(),
794
- };
795
- return this.attachLocation( {
796
- op,
797
- args,
798
- location: location && { __proto__: CsnLocation.prototype, ...location },
799
- } );
800
- }
801
-
802
780
  function pushXprToken( args ) {
803
781
  const token = this._input.LT(-1);
804
782
  args.push( {
@@ -910,11 +888,11 @@ function fixNewKeywordPlacement( args ) {
910
888
  args.push(ixpr);
911
889
  }
912
890
 
913
- function expressionAsAnnotationValue( assignment, cond ) {
914
- if (!cond.cond) // parse error
891
+ function expressionAsAnnotationValue( assignment, cond, start, stop ) {
892
+ if (!cond) // parse error
915
893
  return;
916
- Object.assign(assignment, cond.cond);
917
- assignment.$tokenTexts = this.tokensToStringRepresentation(cond);
894
+ Object.assign(assignment, cond);
895
+ assignment.$tokenTexts = this.tokensToStringRepresentation( start, stop );
918
896
  }
919
897
 
920
898
  // If a '-' is directly before an unsigned number, consider it part of the number;
@@ -1030,7 +1008,22 @@ function assignAnnotationValue( anno, value ) {
1030
1008
  }
1031
1009
 
1032
1010
  function relevantDigits( val ) {
1033
- return val.replace( /0*(e.+)?$/i, '' ).replace( /\./, '' ).replace( /^[-+0]+/, '' );
1011
+ val = val.replace( /e.+$/i, '' );
1012
+
1013
+ // To avoid the super-linear RegEx `0+$`, use the non-backtracking version and
1014
+ // simply check if we're at the end.
1015
+ const trailingZeroes = /0+/g;
1016
+ let re;
1017
+ while ((re = trailingZeroes.exec(val)) !== null) {
1018
+ if (trailingZeroes.lastIndex === val.length) {
1019
+ val = val.slice(0, re.index);
1020
+ break;
1021
+ }
1022
+ }
1023
+
1024
+ return val
1025
+ .replace( /\./, '' )
1026
+ .replace( /^[-+0]+/, '' );
1034
1027
  }
1035
1028
 
1036
1029
  // Create AST node for quoted literals like string and e.g. date'2017-02-22'.
@@ -1277,25 +1270,90 @@ function createSource() {
1277
1270
  return new XsnSource();
1278
1271
  }
1279
1272
 
1273
+ const operatorPrecedences = {
1274
+ // query:
1275
+ union: 1,
1276
+ except: 1,
1277
+ minus: 1,
1278
+ intersect: 2,
1279
+ };
1280
+
1280
1281
  // Create AST node for binary operator `op` and arguments `args`
1281
- function leftAssocBinaryOp( left, opToken, eToken, right, extraProp = 'quantifier' ) {
1282
- const op = this.valueWithTokenLocation( opToken.text.toLowerCase(), opToken);
1282
+ function leftAssocBinaryOp( expr, right, opToken, eToken, extraProp ) {
1283
+ if (!right)
1284
+ return expr;
1285
+ const op = this.valueWithTokenLocation( opToken.text.toLowerCase(), opToken );
1283
1286
  const extra = eToken
1284
1287
  ? this.valueWithTokenLocation( eToken.text.toLowerCase(), eToken )
1285
1288
  : undefined;
1286
- if (!left.$parens &&
1287
- (left.op && left.op.val) === (op && op.val) &&
1288
- (left[extraProp] && left[extraProp].val) === (extra && extra.val)) {
1289
- left.args.push( right );
1290
- return left;
1291
- }
1292
- else if (extra) {
1293
- return {
1294
- op, [extraProp]: extra, args: [ left, right ], location: left.location,
1295
- };
1289
+ if (!expr.$parens && expr.op?.val === op.val && expr[extraProp]?.val === extra?.val) {
1290
+ expr.args.push( right );
1291
+ return expr;
1292
+ }
1293
+ const opPrec = operatorPrecedences[op.val] || 0;
1294
+ let left = expr;
1295
+ let args;
1296
+ while (opPrec > nodePrecedence( left )) {
1297
+ args = left.args;
1298
+ left = args[args.length - 1];
1299
+ }
1300
+ // TODO: location correct?
1301
+ const node = (extra) // eslint-disable-next-line
1302
+ ? { op, [extraProp]: extra, args: [ left, right ], location: left.location }
1303
+ : { op, args: [ left, right ], location: left.location };
1304
+ if (!args)
1305
+ return node;
1306
+ args[args.length - 1] = node;
1307
+ return expr;
1308
+ }
1309
+
1310
+ function nodePrecedence( node ) {
1311
+ const { op } = node;
1312
+ return op && !node.$parens && operatorPrecedences[op.val] || Infinity;
1313
+ }
1314
+
1315
+ function pushOpToken( args, precedence ) { // for nary only; uses LT(-1) as operator token
1316
+ let node = null;
1317
+ let left = args;
1318
+ while (left?.$opPrecedence && left.$opPrecedence < precedence) {
1319
+ args = left;
1320
+ node = args[args.length - 1]; // last sub node of left side
1321
+ left = node.args;
1322
+ }
1323
+
1324
+ if (left?.$opPrecedence === precedence ) { // nary
1325
+ args = left;
1326
+ }
1327
+ else if (node) {
1328
+ const sub = this.argsExpression( [ node, null ], true );
1329
+ args[args.length - 1] = sub;
1330
+ args = sub.args;
1331
+ args.length = 1;
1332
+ }
1333
+ else if (args.length > 1) { // new top-level op & op on left
1334
+ args[0] = this.argsExpression( [ ...args ], args.$opPrecedence != null ); // finish expresion
1335
+ args.length = 1;
1296
1336
  }
1337
+ args.$opPrecedence = precedence;
1338
+ // TODO (if necessary): `location` for sub expessions, top-level is be properly set
1339
+ this.pushXprToken( args );
1340
+ return args;
1341
+ }
1297
1342
 
1298
- return { op, args: [ left, right ], location: left.location };
1343
+ // only to be used in @after or via pushOpToken
1344
+ function argsExpression( args, nary ) {
1345
+ if (args.length === 1) // args.length === 0 is ok (for OVER…)
1346
+ return args[0];
1347
+ const $parens = args[0]?.$parens;
1348
+ const loc = ($parens) ? $parens[$parens.length - 1] : args[0]?.location;
1349
+ const location = loc ? { __proto__: CsnLocation.prototype, ...loc } : this.startLocation();
1350
+ // console.log('AE:',args);
1351
+ const op = {
1352
+ // eslint-disable-next-line no-nested-ternary
1353
+ val: nary === '?:' ? nary : nary ? 'nary' : 'ixpr',
1354
+ location,
1355
+ };
1356
+ return this.attachLocation( { op, args, location } );
1299
1357
  }
1300
1358
 
1301
1359
  const maxCardinalityKeywords = { 1: 'one', '*': 'many' };
@@ -0,0 +1,203 @@
1
+ 'use strict';
2
+
3
+ const { csnPropertyOrder } = require('../json/to-csn');
4
+ const { ModelError } = require('../base/error');
5
+ const { setHidden } = require('../utils/objectUtils');
6
+ const { isAnnotationExpression } = require('../compiler/builtins');
7
+
8
+ const csnDictionaries = [
9
+ 'args',
10
+ 'params',
11
+ 'enum',
12
+ 'mixin',
13
+ 'elements',
14
+ 'actions',
15
+ 'definitions',
16
+ 'vocabularies',
17
+ ];
18
+
19
+ function shallowCopy( val, _options, _sort ) {
20
+ return val;
21
+ }
22
+
23
+ const internalCsnProps = {
24
+ $sources: shallowCopy,
25
+ $location: shallowCopy,
26
+ $path: shallowCopy,
27
+ $paths: shallowCopy,
28
+ elements: cloneCsnDict,
29
+ $tableConstraints: shallowCopy,
30
+ };
31
+ const internalCsnPropertyNames = Object.keys(internalCsnProps);
32
+
33
+
34
+ /**
35
+ * Deeply clone the given CSN model and return it.
36
+ * In testMode (or with testSortCsn), definitions are sorted.
37
+ *
38
+ * This function is CSN aware! Don't put annotation values into it, or
39
+ * keys such as "elements" will be interpreted according to CSN rules!
40
+ *
41
+ * @see cloneAnnotationValue()
42
+ * @see cloneCsnDict()
43
+ *
44
+ * @param {object} csn Top-level CSN. You can pass non-dictionary values.
45
+ * @param {CSN.Options} options CSN Options, only used for `dictionaryPrototype`, `testMode`, and `testSortCsn`.
46
+ * @param {boolean} sort Whether to sort CSN properties.
47
+ */
48
+ function cloneCsn( csn, options, sort ) {
49
+ if (!csn || typeof csn !== 'object')
50
+ return csn;
51
+ if (Array.isArray(csn))
52
+ return csn.map( v => cloneCsn(v, options, sort) );
53
+
54
+ const keys = Object.keys(csn);
55
+ if (sort)
56
+ keys.sort( compareProperties );
57
+
58
+ const r = {};
59
+ for (const n of keys) {
60
+ const val = csn[n];
61
+ if (!val || typeof val !== 'object') {
62
+ r[n] = val;
63
+ }
64
+ else if (n.charAt(0) === '@') {
65
+ r[n] = cloneAnnotationValue(val, options, false); // TODO: pass 'sort'
66
+ }
67
+ else if (csnDictionaries.includes(n) && !Array.isArray(val)) {
68
+ const sortDict = n === 'definitions' &&
69
+ (!options || options.testMode || options.testSortCsn);
70
+ // Array check for property `args` which may either be a dictionary or an array.
71
+ r[n] = cloneCsnDict(val, options, sort, sortDict);
72
+ }
73
+ else {
74
+ r[n] = cloneCsn(val, options, sort);
75
+ }
76
+ }
77
+
78
+ // Note: internal properties with value `undefined` are _not_ cloned!
79
+ // The `hasNonEnumerable()` is required to work with cds.linked() CSN!
80
+ // It _must_ appear before csn[prop] or it may invoke getters!
81
+ internalCsnPropertyNames.forEach((prop) => {
82
+ if (r[prop] === undefined && hasNonEnumerable( csn, prop ) && csn[prop] !== undefined)
83
+ setHidden( r, prop, internalCsnProps[prop](csn[prop], options, sort) );
84
+ });
85
+ options?.hiddenPropertiesToClone?.forEach((prop) => {
86
+ if (r[prop] === undefined && hasNonEnumerable( csn, prop ) && csn[prop] !== undefined)
87
+ setHidden( r, prop, csn[prop] );
88
+ });
89
+
90
+ return r;
91
+ }
92
+
93
+ function hasNonEnumerable( object, property ) {
94
+ return Object.prototype.hasOwnProperty.call( object, property ) &&
95
+ !Object.prototype.propertyIsEnumerable.call( object, property );
96
+ }
97
+
98
+ /**
99
+ * Deeply clone the given CSN dictionary and return it.
100
+ * This function does _not_ sort the given dictionary.
101
+ * See cloneCsnNonDict() if you want sorted definitions.
102
+ *
103
+ * This function is CSN aware! Don't put annotation values into it, or
104
+ * keys such as "elements" will be interpreted according to CSN rules!
105
+ *
106
+ * @see cloneAnnotationValue
107
+ * @see cloneCsnNonDict
108
+ *
109
+ * @param {object} csn
110
+ * @param {CSN.Options} options Only cloneOptions.dictionaryPrototype is
111
+ * used and cloneOptions are passed to sortCsn().
112
+ */
113
+ function cloneCsnDict( csn, options, sortProps, sortDict ) {
114
+ const proto = options?.dictionaryPrototype;
115
+ const dictProto = (typeof proto === 'object') // including null
116
+ ? proto
117
+ : null;
118
+ const r = Object.create( dictProto );
119
+ const keys = Object.keys(csn);
120
+ if (sortDict)
121
+ keys.sort();
122
+ for (const n of keys) {
123
+ // CSN does not allow any dictionary that are not objects.
124
+ // The compiler handles it, but a pre-transformed OData CSN won't trigger recompilation.
125
+ if (csn[n] && typeof csn[n] === 'object')
126
+ r[n] = cloneCsn(csn[n], options, sortProps);
127
+ else
128
+ throw new ModelError(`Found non-object dictionary entry: "${ n }" of type "${ typeof csn[n] }"`);
129
+ }
130
+ return r;
131
+ }
132
+
133
+ /**
134
+ * Clones the given annotation _value_. `value` must not be an object with annotations,
135
+ * but the annotation value itself, e.g. `[ { a: 1 } ]`, not `@anno: [...]`.
136
+ *
137
+ * @param {any} value
138
+ * @param {object} options
139
+ * @param {boolean} sort Whether to sort properties inside expressions.
140
+ * @returns {any}
141
+ */
142
+ function cloneAnnotationValue( value, options, sort ) {
143
+ if (!value || typeof value !== 'object') // scalar
144
+ return value;
145
+ if (!Array.isArray(value) && isAnnotationExpression( value ))
146
+ return cloneCsn( value, options, sort );
147
+ return JSON.parse( JSON.stringify( value ) );
148
+ }
149
+
150
+ /**
151
+ * Sorts the definition dictionary in tests mode.
152
+ *
153
+ * @param {CSN.Model} csn
154
+ * @param {CSN.Options} options
155
+ * @returns The input csn model.
156
+ */
157
+ function sortCsnDefinitionsForTests( csn, options ) {
158
+ if (!options.testMode && !options.testSortCsn)
159
+ return csn;
160
+ const sorted = Object.create(null);
161
+ Object.keys(csn.definitions || {}).sort().forEach((name) => {
162
+ sorted[name] = csn.definitions[name];
163
+ });
164
+ csn.definitions = sorted;
165
+ return csn;
166
+ }
167
+
168
+ function sortCsnForTests( csn, options ) {
169
+ if (options.testMode)
170
+ return cloneCsn(csn, options, true);
171
+ if (options.testSortCsn)
172
+ return sortCsnDefinitionsForTests( csn, options );
173
+ return csn;
174
+ }
175
+
176
+ // Difference to to-csn.js: Annotations are always sorted
177
+ function compareProperties( a, b ) {
178
+ if (a === b)
179
+ return 0;
180
+ const oa = csnPropertyOrder[a] || csnPropertyOrder[a.charAt(0)] || 9999;
181
+ const ob = csnPropertyOrder[b] || csnPropertyOrder[b.charAt(0)] || 9999;
182
+ return oa - ob || (a < b ? -1 : 1);
183
+ }
184
+
185
+ module.exports = {
186
+ cloneCsnDict(csn, options) {
187
+ return cloneCsnDict(csn, options, false, false);
188
+ },
189
+ cloneCsnNonDict(csn, options) {
190
+ return cloneCsn(csn, options, false);
191
+ },
192
+ cloneFullCsn(csn, options) {
193
+ return cloneCsn(csn, options, false);
194
+ },
195
+ cloneAnnotationValue(csn) {
196
+ return cloneAnnotationValue(csn, {}, false);
197
+ },
198
+ sortCsn(csn, options) {
199
+ return cloneCsn(csn, options, true);
200
+ },
201
+ sortCsnDefinitionsForTests,
202
+ sortCsnForTests,
203
+ };
@@ -171,6 +171,8 @@
171
171
  // hierarchy, query number, table aliases and links from a column to its
172
172
  // respective inferred element.
173
173
 
174
+ // TODO: some `name` property would be useful (also set with `initDefinition`)
175
+
174
176
  // Properties in cache:
175
177
  //
176
178
  // - _effectiveType on def/member/items: cached result of effectiveType()
@@ -896,9 +898,14 @@ function queryOrMain( query, main ) {
896
898
  /**
897
899
  * Traverse query in pre-order
898
900
  *
901
+ * The callback is called on the following XSN nodes inside the query `query`:
902
+ * - a query node, which has property `SET` or `SELECT` (or `projection`),
903
+ * - a query source node inside `from` if it has property `ref`,
904
+ * - NOT on a `join` node inside `from`.
905
+ *
899
906
  * @param {CSN.Query} query
900
- * @param {CSN.QuerySelect} fromSelect
901
- * @param {CSN.Query} parentQuery
907
+ * @param {CSN.QuerySelect} fromSelect: for query in `from`
908
+ * @param {CSN.Query} parentQuery: for a sub query (ex those in `from`)
902
909
  * @param {(query: CSN.Query&CSN.QueryFrom, select: CSN.QuerySelectEnriched, parentQuery: CSN.Query) => void} callback
903
910
  */
904
911
  function traverseQuery( query, fromSelect, parentQuery, callback ) {
@@ -1006,7 +1013,8 @@ function pathId( item ) {
1006
1013
  }
1007
1014
 
1008
1015
  function implicitAs( ref ) {
1009
- const id = pathId( ref[ref.length - 1] );
1016
+ const item = ref[ref.length - 1];
1017
+ const id = (typeof item === 'string') ? item : item.id; // inlined `pathId`
1010
1018
  return id.substring( id.lastIndexOf('.') + 1 );
1011
1019
  }
1012
1020