@sap/cds-compiler 3.0.0 → 3.1.2

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 (79) hide show
  1. package/CHANGELOG.md +104 -9
  2. package/bin/.eslintrc.json +2 -1
  3. package/bin/cdsc.js +28 -16
  4. package/doc/API.md +11 -0
  5. package/doc/CHANGELOG_ARCHIVE.md +1 -1
  6. package/doc/CHANGELOG_BETA.md +24 -2
  7. package/doc/CHANGELOG_DEPRECATED.md +21 -1
  8. package/lib/api/main.js +92 -40
  9. package/lib/api/options.js +2 -3
  10. package/lib/base/keywords.js +64 -1
  11. package/lib/base/message-registry.js +33 -5
  12. package/lib/base/messages.js +54 -65
  13. package/lib/base/model.js +2 -0
  14. package/lib/base/optionProcessorHelper.js +53 -21
  15. package/lib/checks/actionsFunctions.js +8 -7
  16. package/lib/checks/selectItems.js +96 -14
  17. package/lib/checks/types.js +5 -8
  18. package/lib/checks/validator.js +1 -2
  19. package/lib/compiler/assert-consistency.js +65 -13
  20. package/lib/compiler/base.js +6 -4
  21. package/lib/compiler/builtins.js +93 -4
  22. package/lib/compiler/checks.js +1 -1
  23. package/lib/compiler/define.js +28 -23
  24. package/lib/compiler/extend.js +20 -11
  25. package/lib/compiler/finalize-parse-cdl.js +5 -9
  26. package/lib/compiler/index.js +2 -0
  27. package/lib/compiler/populate.js +37 -32
  28. package/lib/compiler/propagator.js +11 -6
  29. package/lib/compiler/resolve.js +15 -19
  30. package/lib/compiler/shared.js +54 -18
  31. package/lib/compiler/tweak-assocs.js +5 -11
  32. package/lib/compiler/utils.js +15 -6
  33. package/lib/edm/annotations/genericTranslation.js +12 -2
  34. package/lib/edm/annotations/preprocessAnnotations.js +18 -15
  35. package/lib/edm/csn2edm.js +18 -17
  36. package/lib/edm/edm.js +22 -13
  37. package/lib/edm/edmAnnoPreprocessor.js +349 -0
  38. package/lib/edm/edmInboundChecks.js +85 -0
  39. package/lib/edm/edmPreprocessor.js +336 -665
  40. package/lib/edm/edmUtils.js +86 -45
  41. package/lib/gen/Dictionary.json +29 -9
  42. package/lib/gen/language.checksum +1 -1
  43. package/lib/gen/language.interp +1 -2
  44. package/lib/gen/languageLexer.js +3 -0
  45. package/lib/gen/languageParser.js +4332 -4496
  46. package/lib/inspect/.eslintrc.json +4 -0
  47. package/lib/inspect/index.js +14 -0
  48. package/lib/inspect/inspectModelStatistics.js +81 -0
  49. package/lib/inspect/inspectPropagation.js +189 -0
  50. package/lib/inspect/inspectUtils.js +44 -0
  51. package/lib/json/from-csn.js +19 -20
  52. package/lib/json/to-csn.js +11 -8
  53. package/lib/language/genericAntlrParser.js +150 -92
  54. package/lib/language/language.g4 +47 -74
  55. package/lib/main.d.ts +1 -0
  56. package/lib/model/api.js +1 -1
  57. package/lib/model/csnRefs.js +56 -29
  58. package/lib/model/csnUtils.js +29 -14
  59. package/lib/model/revealInternalProperties.js +6 -4
  60. package/lib/modelCompare/compare.js +3 -0
  61. package/lib/optionProcessor.js +81 -38
  62. package/lib/render/toCdl.js +57 -32
  63. package/lib/render/toHdbcds.js +1 -1
  64. package/lib/render/toSql.js +31 -11
  65. package/lib/render/utils/common.js +3 -4
  66. package/lib/transform/db/associations.js +43 -35
  67. package/lib/transform/db/cdsPersistence.js +0 -1
  68. package/lib/transform/db/flattening.js +3 -4
  69. package/lib/transform/db/transformExists.js +7 -5
  70. package/lib/transform/draft/db.js +1 -1
  71. package/lib/transform/forHanaNew.js +11 -2
  72. package/lib/transform/forOdataNew.js +4 -4
  73. package/lib/transform/localized.js +15 -11
  74. package/lib/transform/odata/typesExposure.js +14 -5
  75. package/lib/utils/file.js +28 -18
  76. package/lib/utils/moduleResolve.js +0 -1
  77. package/package.json +3 -4
  78. package/share/messages/syntax-expected-integer.md +9 -8
  79. package/lib/checks/unknownMagic.js +0 -41
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { isBuiltinType } = require('../model/csnUtils');
4
+ const { isBetaEnabled } = require('../base/model');
4
5
 
5
6
  // Only to be used with validator.js - a correct this value needs to be provided!
6
7
 
@@ -52,13 +53,13 @@ function checkActionOrFunction(art, artName, prop, path) {
52
53
  function checkActionOrFunctionParameter(param, currPath, actKind) {
53
54
  const paramType = param.type ? this.csnUtils.getFinalTypeDef(param.type) : param;
54
55
 
55
- if (param.default || paramType.default) {
56
- this.error('param-default', currPath, { '#': actKind },
57
- {
58
- std: 'Artifact parameters can\'t have a default value', // Not used
59
- action: 'Action parameters can\'t have a default value',
60
- function: 'Function parameters can\'t have a default value',
61
- });
56
+ if (!isBetaEnabled(this.options, 'optionalActionFunctionParameters') && (param.default || paramType.default)) {
57
+ this.message('param-default', currPath, { '#': actKind },
58
+ {
59
+ std: 'Artifact parameters can\'t have a default value', // Not used
60
+ action: 'Action parameters can\'t have a default value',
61
+ function: 'Function parameters can\'t have a default value',
62
+ });
62
63
  }
63
64
 
64
65
  if (paramType.type && this.csnUtils.isAssocOrComposition(param.type)) {
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { forEachGeneric } = require('../model/csnUtils');
3
+ const { forEachGeneric, applyTransformationsOnNonDictionary } = require('../model/csnUtils');
4
4
 
5
5
  // Only to be used with validator.js - a correct this value needs to be provided!
6
6
 
@@ -17,21 +17,103 @@ function validateSelectItems(query) {
17
17
  const { SELECT } = query;
18
18
  if (!SELECT)
19
19
  return;
20
+ /**
21
+ * Check for a $self.<assoc> in columns etc. - since the $self.<assoc> references the "outside" view
22
+ * of the association, this is not allowed.
23
+ *
24
+ * @param {string} queryPart Part of the query that is being checked
25
+ * @returns {Function} Function as callback for applyTransformations
26
+ */
27
+ function checkRefForInvalid$Self(queryPart) {
28
+ const signalError = (error, parent, type) => {
29
+ if (queryPart === 'columns') {
30
+ error(null, parent.$path,
31
+ { name: parent.ref[0], type },
32
+ 'Select items starting with $(NAME) must not contain path steps of type $(TYPE)');
33
+ }
34
+ else if (queryPart === 'orderBy') {
35
+ error(null, parent.$path,
36
+ { id: queryPart, type },
37
+ 'Items of the $(ID)-clause must not contain path steps of type $(TYPE)');
38
+ }
39
+ else {
40
+ error(null, parent.$path,
41
+ { id: queryPart, name: parent.ref[0], type },
42
+ 'Items of the $(ID)-clause starting with $(NAME) must not contain path steps of type $(TYPE)');
43
+ }
44
+ };
45
+ return function checkForInvalid$SelfInRef(parent) {
46
+ if (parent.ref && (parent.$scope === '$self' || parent.$scope === '$query')) {
47
+ const { _links } = parent;
48
+ for (let j = parent.$scope === '$self' ? 1 : 0; j < _links.length - 1; j++) {
49
+ if (_links[j].art.target) {
50
+ if (_links[j].art.on) {
51
+ // It's an unmanaged association - traversal is always forbidden
52
+ signalError(this.error, parent, _links[j].art.type);
53
+ }
54
+ else {
55
+ // It's a managed association - access of the foreign keys is allowed
56
+ const nextRef = parent.ref[j + 1].id || parent.ref[j + 1];
57
+ if (!_links[j].art.keys.some(r => r.ref[0] === nextRef))
58
+ signalError(this.error, parent, _links[j].art.type);
59
+ }
60
+ }
61
+ }
62
+
63
+ const last = _links[_links.length - 1];
20
64
 
21
- forEachGeneric(SELECT, 'columns', (selectItem) => {
22
- if (selectItem.ref && (selectItem.ref[0] === '$self' || selectItem.ref[0] === '$projection')) {
23
- const pathStepWithTarget = selectItem._links.slice(1).find(link => link.art.target);
24
- if (pathStepWithTarget) {
25
- this.error(null, selectItem.$path,
26
- { name: selectItem.ref[0], type: pathStepWithTarget.art.type },
27
- 'Select items starting with $(NAME) must not contain path steps of type $(TYPE)');
65
+ if (last.art.target && last.art.on) {
66
+ // It's an unmanaged association - traversal is always forbidden
67
+ signalError(this.error, parent, last.art.type);
68
+ } // managed is okay, can be handled via tuple expansion
28
69
  }
29
- }
30
- else if (this.options.transformation === 'hdbcds' && selectItem.xpr && selectItem.func) {
31
- this.error(null, selectItem.$path,
32
- 'Window functions are not supported by SAP HANA CDS');
33
- }
34
- });
70
+ };
71
+ }
72
+
73
+ /**
74
+ * Check the given assoc filter for usage of $self - in an assoc-filter, you must only
75
+ * address things on the target side of the association, not from global scope.
76
+ *
77
+ * @param {object} parent
78
+ * @param {string} prop
79
+ * @param {Array} where
80
+ */
81
+ function checkFilterForInvalid$Self(parent, prop, where) {
82
+ where.forEach((whereStep) => {
83
+ if (whereStep.ref && ( whereStep.ref[0] === '$projection' || whereStep.ref[0] === '$self')) {
84
+ this.error('expr-where-unexpected-self', whereStep.$path,
85
+ { name: whereStep.ref[0] },
86
+ 'Path steps inside of filters must not start with $(NAME)');
87
+ }
88
+ });
89
+ }
90
+
91
+ const aTCB = (parent, prop) => {
92
+ applyTransformationsOnNonDictionary(parent, prop, {
93
+ ref: checkRefForInvalid$Self(prop).bind(this),
94
+ where: checkFilterForInvalid$Self.bind(this),
95
+ }, { skipStandard: { on: true }, drillRef: true });
96
+ };
97
+
98
+ const transformers = {
99
+ columns: aTCB,
100
+ groupBy: aTCB,
101
+ orderBy: aTCB,
102
+ having: aTCB,
103
+ where: aTCB,
104
+ };
105
+
106
+ if (this.options.transformation === 'hdbcds') {
107
+ transformers.xpr = (parent) => {
108
+ if (parent.func) {
109
+ this.error(null, parent.$path,
110
+ 'Window functions are not supported by SAP HANA CDS');
111
+ }
112
+ };
113
+ }
114
+
115
+ applyTransformationsOnNonDictionary(query, 'SELECT', transformers );
116
+
35
117
  // .call() with 'this' to ensure we have access to the options
36
118
  rejectManagedAssociationsAndStructuresForHdbcdsNames.call(this, SELECT, SELECT.$path);
37
119
  }
@@ -1,6 +1,6 @@
1
1
  'use strict';
2
2
 
3
- const { getUtils, isBuiltinType, hasAnnotationValue } = require('../model/csnUtils');
3
+ const { getUtils, hasAnnotationValue } = require('../model/csnUtils');
4
4
 
5
5
  // Only to be used with validator.js - a correct this value needs to be provided!
6
6
 
@@ -55,7 +55,7 @@ function checkElementTypeDefinitionHasType(member, memberName, prop, path) {
55
55
 
56
56
  // should only happen with csn input, not in cdl
57
57
  if (!hasArtifactTypeInformation(member)) {
58
- warnAboutMissingType(this.error, path, memberName, true);
58
+ errorAboutMissingType(this.error, path, memberName, true);
59
59
  return;
60
60
  }
61
61
 
@@ -96,7 +96,7 @@ function checkTypeDefinitionHasType(artifact, artifactName, prop, path) {
96
96
 
97
97
  // should only happen with csn input, not in cdl
98
98
  if (!hasArtifactTypeInformation(artifact)) {
99
- warnAboutMissingType(this.error, path, artifactName);
99
+ errorAboutMissingType(this.error, path, artifactName);
100
100
  return;
101
101
  }
102
102
 
@@ -157,9 +157,8 @@ function checkTypeOfHasProperType(artOrElement, name, model, error, path, derive
157
157
  * @param {CSN.Path} path the path to the element or the artifact
158
158
  * @param {string} name of the element or the artifact which is dubious
159
159
  * @param {boolean} isElement indicates whether we are dealing with an element or an artifact
160
- * @todo Rename, is an error not a warning
161
160
  */
162
- function warnAboutMissingType(error, path, name, isElement = false) {
161
+ function errorAboutMissingType(error, path, name, isElement = false) {
163
162
  error('check-proper-type', path, { art: name, '#': isElement ? 'elm' : 'std' }, {
164
163
  std: 'Dubious type $(ART) without type information',
165
164
  elm: 'Dubious element $(ART) without type information',
@@ -174,12 +173,10 @@ function warnAboutMissingType(error, path, name, isElement = false) {
174
173
  *
175
174
  * @param {CSN.Artifact} artifact the artifact to check
176
175
  * @returns {boolean} indicates whether the artifact has type information
177
- * @todo What is the point of isBuiltinType here if we check for artifact.type at the end?
178
176
  */
179
177
  function hasArtifactTypeInformation(artifact) {
180
178
  // When is what property set?
181
- return isBuiltinType(artifact.type) || // => `Integer`
182
- artifact.elements || // => `type A {}`
179
+ return artifact.elements || // => `type A {}`
183
180
  artifact.items || // => `type A : array of Integer`
184
181
  artifact.enum || // => `type A : Integer enum {}`, `type` also set
185
182
  artifact.target || // => `type A : Association to B;`
@@ -32,7 +32,6 @@ const { validateAssociationsInItems } = require('./arrayOfs');
32
32
  const checkQueryForNoDBArtifacts = require('./queryNoDbArtifacts');
33
33
  const checkExplicitlyNullableKeys = require('./nullableKeys');
34
34
  const nonexpandableStructuredInExpression = require('./nonexpandableStructured');
35
- const unknownMagic = require('./unknownMagic');
36
35
  const managedWithoutKeys = require('./managedWithoutKeys');
37
36
  const {
38
37
  checkSqlAnnotationOnArtifact,
@@ -62,7 +61,7 @@ const forHanaArtifactValidators
62
61
  checkSqlAnnotationOnArtifact,
63
62
  ];
64
63
 
65
- const forHanaCsnValidators = [ nonexpandableStructuredInExpression, unknownMagic ];
64
+ const forHanaCsnValidators = [ nonexpandableStructuredInExpression ];
66
65
  /**
67
66
  * @type {Array<(query: CSN.Query, path: CSN.Path) => void>}
68
67
  */
@@ -97,7 +97,6 @@ function assertConsistency( model, stage ) {
97
97
  '$lateExtensions',
98
98
  '_entities', '$entity',
99
99
  '$blocks',
100
- '$newfeatures',
101
100
  '$messageFunctions',
102
101
  '$functions',
103
102
  '$volatileFunctions',
@@ -143,7 +142,6 @@ function assertConsistency( model, stage ) {
143
142
  },
144
143
  fileDep: { test: TODO }, // in usings
145
144
  $frontend: { parser: true, test: isString, enum: [ 'cdl', 'json', 'xml' ] },
146
- $newfeatures: { test: TODO }, // if new features have been used which break the old backends
147
145
  messages: {
148
146
  enumerable: () => true, // does not matter (non-enum std), enum in CSN/XML parser
149
147
  test: isArray( TODO ),
@@ -175,7 +173,6 @@ function assertConsistency( model, stage ) {
175
173
  return innerDict( val, parent, lang, spec );
176
174
  } ),
177
175
  },
178
- _assocSources: { kind: true, test: TODO }, // just null: isArray( inDefinitions ) during resolve
179
176
  $magicVariables: {
180
177
  // $magicVariables contains "builtin" artifacts that differ from
181
178
  // "normal artifacts" and therefore have a custom schema
@@ -256,6 +253,7 @@ function assertConsistency( model, stage ) {
256
253
  'where', 'groupBy', 'having', 'orderBy', '$orderBy', 'limit',
257
254
  '_projections', '_block', '_parent', '_main', '_effectiveType', '$expand',
258
255
  '$tableAliases', 'kind', '_$next', '_combined', '$inlines', '_status',
256
+ '_extension', // for unapplied extensions
259
257
  ],
260
258
  },
261
259
  none: { optional: () => true }, // parse error
@@ -375,13 +373,21 @@ function assertConsistency( model, stage ) {
375
373
  },
376
374
  // locations of parentheses pairs around expression:
377
375
  $parens: { parser: true, test: TODO },
376
+ $prefix: { test: isString }, // compiler-corrected path prefix
378
377
  $syntax: {
379
378
  parser: true,
380
379
  kind: [ 'entity', 'view', 'type', 'aspect' ],
381
380
  test: isString, // CSN parser should check for 'entity', 'view', 'projection'
382
381
  },
383
382
  value: {
384
- optional: [ 'location', '$inferred', 'sort', 'nulls' ],
383
+ optional: [
384
+ 'location', '$inferred', 'sort', 'nulls',
385
+ 'param', 'scope', // for dynamic parameter '?'
386
+ // through cast() with enum through CSN->XSN
387
+ // TODO: re-check #9225, this should be directly in the query element,
388
+ // not inside value, no `enum` inside `cast`!
389
+ 'elements', 'items', 'enum', '$expand', 'target',
390
+ ],
385
391
 
386
392
  kind: true,
387
393
  test: expression, // properties below are "sub specifications"
@@ -439,7 +445,10 @@ function assertConsistency( model, stage ) {
439
445
  struct: { inherits: 'val', test: isDictionary( definition ) }, // def because double @
440
446
  args: {
441
447
  inherits: 'value',
442
- optional: [ 'name', '$duplicate', '$expected', 'args', 'suffix' ],
448
+ optional: [
449
+ 'name', '$duplicate', '$expected', 'args', 'suffix',
450
+ 'param', 'scope', // for dynamic parameter '?'
451
+ ],
443
452
  test: args,
444
453
  },
445
454
  on: { kind: true, inherits: 'value', test: expression },
@@ -456,7 +465,7 @@ function assertConsistency( model, stage ) {
456
465
  optional: [ 'name', '_block', '$priority', '$inferred', '$duplicates', '$errorReported' ],
457
466
  // TODO: name requires if not in parser?
458
467
  },
459
- $priority: { test: TODO },
468
+ $priority: { test: isOneOf([ undefined, false, 'extend', 'annotate' ]) },
460
469
  $annotations: { parser: true, kind: true, test: TODO }, // deprecated, still there for cds-lsp
461
470
  name: {
462
471
  isRequired: stageParser && (() => false), // not required in parser
@@ -475,7 +484,7 @@ function assertConsistency( model, stage ) {
475
484
  variant: { test: TODO }, // TODO: not set in CDL parser
476
485
  element: { test: TODO }, // TODO: { test: isString },
477
486
  action: { test: isString },
478
- param: { test: isString },
487
+ param: { test: TODO },
479
488
  alias: { test: isString },
480
489
  expectedKind: { kind: [ 'extend' ], inherits: 'kind' },
481
490
  virtual: { kind: true, test: locationVal() },
@@ -528,7 +537,6 @@ function assertConsistency( model, stage ) {
528
537
  _service: { kind: true, test: TODO },
529
538
  _main: { kind: true, test: TODO },
530
539
  _artifact: { test: TODO },
531
- _base: { test: TODO, kind: true },
532
540
  _navigation: { test: TODO },
533
541
  _effectiveType: { kind: true, test: TODO },
534
542
  _joinParent: { test: TODO },
@@ -552,6 +560,7 @@ function assertConsistency( model, stage ) {
552
560
  'where', 'columns', 'mixin', 'quantifier', 'offset',
553
561
  'orderBy', '$orderBy', 'groupBy', 'excludingDict', 'having',
554
562
  'limit', '_status',
563
+ '_extension', // for unapplied extensions
555
564
  ],
556
565
  },
557
566
  _leadingQuery: { kind: true, test: TODO },
@@ -585,21 +594,57 @@ function assertConsistency( model, stage ) {
585
594
  // (it can contain the artifact itself with no/failed autoexposure):
586
595
  _descendants: { kind: [ 'entity' ], test: isDictionary( isArray( TODO ) ) },
587
596
 
588
- $errorReported: { parser: true, test: isBoolean }, // to avoid duplicate messages
597
+ $errorReported: { parser: true, kind: true, test: isString }, // to avoid duplicate messages
589
598
  $duplicates: { parser: true, kind: true, test: TODO }, // array of arts or true
590
599
  $extension: { kind: true, test: TODO }, // TODO: introduce $applied instead or $status
591
- $inferred: { parser: true, kind: true, test: isString },
600
+ $inferred: {
601
+ parser: true,
602
+ kind: true,
603
+ test: isOneOf([
604
+ // Uppercase values are used in logic, lowercase value are "just for us", i.e.
605
+ // debugging or to add non-enumerable properties such as $generated in Universal CSN.
606
+ // However, that is no longer true. For example, `autoexposed` is used in populate.js
607
+ // as well.
608
+ 'IMPLICIT',
609
+ 'REDIRECTED',
610
+
611
+ '$autoElement', // for magicVars: $user is automatically changed to $user.id
612
+ '$generated', // compiler generated annotations, e.g. @Core.Computed
613
+ '*', // inferred from query wildcard
614
+ 'as', // query alias name
615
+ 'aspect-composition',
616
+ 'autoexposed', // for auto-exposed entities (they can't be referred to)
617
+ 'cast', // type from cast() function
618
+ 'composition-entity',
619
+ 'copy', // only used in rewriteCondition(): On-condition is copied
620
+ 'duplicate-autoexposed', // just like `autoexposed`, but with `duplicate` error.
621
+ 'expand-element', // expanded elements
622
+ 'expand-param', // expanded params (difference to expand-element only for debugging)
623
+ 'include', // through includes, e.g. `entity E : F {}`
624
+ 'keys',
625
+ 'localized', // e.g. compiler-generated elements for localized: `text` assoc, etc.
626
+ 'localized-entity', // `.texts` entity
627
+ 'nav', // only used for MASKED, TODO(v4): Remove
628
+ 'none', // only used in ensureColumnName(): Used in object representing empty alias
629
+ 'query', // inferred query properties, e.g. `key`
630
+ 'rewrite', // on-conditions or FKeys are rewritten
631
+ ]),
632
+ },
592
633
 
593
634
  // Helper property for the XSN-to-CSN transformation, see function setExpandStatus():
594
635
  // client, universal: render expanded elements? gensrc: produce annotate statements?
595
- $expand: { kind: true, test: isString }, // TODO: rename it to $elementsExpand ?
636
+ // TODO: rename it to $elementsExpand ?
637
+ $expand: {
638
+ kind: true,
639
+ // See description of `setExpandStatus()` of in `lib/compiler/utils.js`.
640
+ test: isOneOf([ 'origin', 'annotate', 'target' ]),
641
+ },
596
642
 
597
643
  $autoexpose: { kind: [ 'entity' ], test: isBoolean, also: [ null, 'Composition' ] },
598
- $a2j: { kind: true, enumerable: true, test: TODO },
599
644
  $extra: { parser: true, test: TODO }, // for unexpected properties in CSN
600
645
  $withLocalized: { test: isBoolean },
601
646
  $sources: { parser: true, test: isArray( isString ) },
602
- $expected: { parser: true, test: isString },
647
+ $expected: { parser: true, test: isOneOf([ 'approved-exists', 'exists' ]) },
603
648
  $messageFunctions: { test: TODO },
604
649
  $functions: { test: TODO },
605
650
  $volatileFunctions: { test: TODO },
@@ -859,6 +904,13 @@ function assertConsistency( model, stage ) {
859
904
  isString(node, parent, prop, spec);
860
905
  }
861
906
 
907
+ function isOneOf(values) {
908
+ return function isOneOfInner( node, parent, prop ) {
909
+ if (!values.includes(node))
910
+ throw new Error( `Unexpected value '${ node }', expected ${ JSON.stringify(values) }${ at( [ node, parent ], prop ) }` );
911
+ };
912
+ }
913
+
862
914
  function isString( node, parent, prop, spec ) {
863
915
  if (typeof node !== 'string')
864
916
  throw new Error( `Expected string${ at( [ node, parent ], prop ) }` );
@@ -16,16 +16,18 @@ const kindProperties = {
16
16
  namespace: { artifacts: true }, // on-the-fly context
17
17
  context: { artifacts: true, normalized: 'namespace' },
18
18
  service: { artifacts: true, normalized: 'namespace' },
19
- entity: { elements: true, actions: true, params: () => false },
19
+ entity: {
20
+ elements: true, actions: true, params: () => false, include: true,
21
+ },
20
22
  select: { normalized: 'select', elements: true },
21
23
  $join: { normalized: 'select' },
22
24
  $tableAlias: { normalized: 'alias' }, // table alias in select
23
25
  $self: { normalized: 'alias' }, // table alias in select
24
26
  $navElement: { normalized: 'element' },
25
27
  $inline: { normalized: 'element' }, // column with inline property
26
- event: { elements: true },
27
- type: { elements: propExists, enum: propExists },
28
- aspect: { elements: propExists },
28
+ event: { elements: true, include: true },
29
+ type: { elements: propExists, enum: propExists, include: true },
30
+ aspect: { elements: propExists, actions: true, include: true },
29
31
  annotation: { elements: propExists, enum: propExists },
30
32
  enum: { normalized: 'element' },
31
33
  element: { elements: propExists, enum: propExists, dict: 'elements' },
@@ -1,7 +1,7 @@
1
1
  // The builtin artifacts of CDS
2
2
 
3
3
  // TODO: split this file
4
- // - in base/: common definitions
4
+ // - in base/: common definitions, datetime formats
5
5
  // - in compiler/: XSN-specific
6
6
  // - in ?: CSN-specific
7
7
 
@@ -154,12 +154,14 @@ function compileArg( src ) {
154
154
  expr: src.expr || [],
155
155
  separator: src.separator || [],
156
156
  };
157
- for (const generic of [ 'intro', 'expr', 'separator' ]) { // intro before expr!
157
+ for (const generic of [ 'intro', 'expr', 'separator' ]) {
158
+ // intro before expr: if both intro and expr, tag as 'expr'
158
159
  for (const token of src[generic] || [])
159
160
  tgt[token] = generic;
160
161
  }
161
- if (tgt.intro) // same token could be in both 'expr' and 'intro':
162
- tgt.introMsg = src.introMsg || tgt.intro.filter( token => tgt[token] === 'intro' );
162
+ // As GenericIntro is always together with GenericExpr, only mention those
163
+ // which are not already proposed for GenericExpr:
164
+ tgt.introMsg = src.introMsg || tgt.intro.filter( token => tgt[token] === 'intro' );
163
165
  return tgt;
164
166
  }
165
167
 
@@ -193,6 +195,92 @@ const magicVariables = {
193
195
 
194
196
  // see lib/render/renderUtil.js for DB-specific magic vars, specified in CAP Cds via function
195
197
 
198
+ /**
199
+ * Patterns for literal token tests and creation. The value is a map from the
200
+ * `prefix` argument of function `quotedliteral` to the following properties:
201
+ * - `test_msg`: error message which is issued if `test_fn` fails.
202
+ * - `test_fn`: function called with argument `value`, fails falsy return value
203
+ * - `unexpected_msg`: error message which is issued if `unexpected_char` matches
204
+ * - `unexpected_char`: regular expression matching an illegal character in `value`,
205
+ * the error location is only correct for a literal <prefix>'<value>'
206
+ * - `literal`: the value which is used instead of `prefix` in the AST
207
+ * TODO: we might do a range check (consider leap seconds, i.e. max value 60),
208
+ * but always allow Feb 29 (no leap year computation)
209
+ * Notes:
210
+ * - Dates/Times as defined in ISO 8601, see <https://en.wikipedia.org/wiki/ISO_8601>
211
+ */
212
+ const quotedLiteralPatterns = {
213
+ x: {
214
+ test_variant: 'uneven-hex',
215
+ test_fn: (str => Number.isInteger(str.length / 2)),
216
+ unexpected_variant: 'invalid-hex',
217
+ unexpected_char: /[^0-9a-f]/i,
218
+ json_type: 'string',
219
+ },
220
+ time: {
221
+ test_variant: 'time',
222
+ test_fn: (x) => {
223
+ // Leading `T` allowed in ISO 8601.
224
+ const match = x.match( /^T?(\d{1,2}):(\d{1,2})(?::(\d{1,2}))?$/ );
225
+ return match !== null && checkTime( match[1], match[2], match[3] );
226
+ },
227
+ json_type: 'string',
228
+ },
229
+ date: {
230
+ test_variant: 'date',
231
+ test_fn: (x) => {
232
+ const match = x.match( /^(-?\d{4})-(\d{1,2})-(\d{1,2})$/ );
233
+ return match !== null && checkDate( match[1], match[2], match[3] );
234
+ },
235
+ json_type: 'string',
236
+ },
237
+ timestamp: {
238
+ test_variant: 'timestamp',
239
+ test_fn: (x) => {
240
+ // eslint-disable-next-line max-len
241
+ const match = x.match( /^(-?\d{4})-(\d{2})-(\d{2})[T ](\d{2}):(\d{2})(?::(\d{2})(\.\d{1,7})?)?$/ );
242
+ return match !== null && checkDate( match[1], match[2], match[3] ) &&
243
+ checkTime( match[4], match[5], match[6] );
244
+ },
245
+ json_type: 'string',
246
+ },
247
+ };
248
+
249
+ /**
250
+ * Check that the given date is within boundaries.
251
+ * We can't use Date.parse() since that also allows non-standard values (2022-02-31 for example).
252
+ * Checks according to ISO 8601.
253
+ *
254
+ * @returns {boolean} True if the date is valid.
255
+ */
256
+ function checkDate(year, month, day) {
257
+ // Negative years are allowed
258
+ year = Math.abs(Number.parseInt(year, 10));
259
+ month = Number.parseInt(month, 10);
260
+ day = Number.parseInt(day, 10);
261
+ // If any is NaN, the condition will be false.
262
+ // Year 0 does not exist, but ISO 8601 allows it and defines it as 1 BC.
263
+ return !Number.isNaN(year) && month > 0 && month < 13 && day > 0 && day < 32;
264
+ }
265
+
266
+ /**
267
+ * Check that the given time is within boundaries.
268
+ * Checks according to ISO 8601.
269
+ *
270
+ * @returns {boolean} True if the date is valid.
271
+ */
272
+ function checkTime(hour, minutes, seconds) {
273
+ hour = Number.parseInt(hour, 10);
274
+ minutes = Number.parseInt(minutes, 10);
275
+ seconds = seconds ? Number.parseInt(seconds, 10) : 0;
276
+ if (hour === 24) // allow 24:00:00 (ISO 8601 version earlier than 2019)
277
+ return minutes === 0 && seconds === 0;
278
+ // If any is NaN, the condition will be false.
279
+ return hour >= 0 && hour < 24 &&
280
+ minutes >= 0 && minutes < 60 &&
281
+ seconds >= 0 && seconds < 61; // we allow 60 for lead seconds
282
+ }
283
+
196
284
  /** All types belong to one category. */
197
285
  const typeCategories = {
198
286
  string: [],
@@ -403,6 +491,7 @@ module.exports = {
403
491
  typeParameters,
404
492
  functionsWithoutParens,
405
493
  specialFunctions,
494
+ quotedLiteralPatterns,
406
495
  initBuiltins,
407
496
  isInReservedNamespace,
408
497
  isBuiltinType,
@@ -414,7 +414,7 @@ function check( model ) { // = XSN
414
414
  }
415
415
  }
416
416
 
417
- // TODO: make this part of the the name resolution in the compiler
417
+ // TODO: make this part of the name resolution in the compiler
418
418
  // Check that queries in 'art' do not contain unmanaged associations in GROUP BY or ORDER BY
419
419
  function checkNoUnmanagedAssocsInGroupByOrderBy( query ) {
420
420
  const art = query._main; // TODO - remove, use query for semantic location