@sap/cds-compiler 4.0.0 → 4.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 (85) hide show
  1. package/CHANGELOG.md +115 -5
  2. package/bin/cdsc.js +12 -12
  3. package/doc/CHANGELOG_BETA.md +11 -0
  4. package/lib/api/main.js +60 -12
  5. package/lib/api/validate.js +1 -1
  6. package/lib/base/location.js +6 -7
  7. package/lib/base/message-registry.js +84 -38
  8. package/lib/base/messages.js +11 -10
  9. package/lib/base/model.js +6 -2
  10. package/lib/checks/defaultValues.js +6 -6
  11. package/lib/checks/foreignKeys.js +0 -5
  12. package/lib/checks/onConditions.js +17 -12
  13. package/lib/checks/queryNoDbArtifacts.js +132 -72
  14. package/lib/checks/sql-snippets.js +15 -4
  15. package/lib/checks/types.js +3 -3
  16. package/lib/checks/utils.js +1 -1
  17. package/lib/compiler/assert-consistency.js +44 -16
  18. package/lib/compiler/base.js +1 -0
  19. package/lib/compiler/builtins.js +7 -8
  20. package/lib/compiler/checks.js +274 -197
  21. package/lib/compiler/classes.js +62 -0
  22. package/lib/compiler/cycle-detector.js +3 -3
  23. package/lib/compiler/define.js +63 -50
  24. package/lib/compiler/extend.js +38 -20
  25. package/lib/compiler/finalize-parse-cdl.js +2 -1
  26. package/lib/compiler/generate.js +0 -8
  27. package/lib/compiler/index.js +9 -7
  28. package/lib/compiler/kick-start.js +2 -0
  29. package/lib/compiler/populate.js +139 -110
  30. package/lib/compiler/propagator.js +4 -3
  31. package/lib/compiler/resolve.js +157 -126
  32. package/lib/compiler/shared.js +706 -404
  33. package/lib/compiler/tweak-assocs.js +21 -10
  34. package/lib/compiler/utils.js +228 -36
  35. package/lib/edm/annotations/genericTranslation.js +30 -2
  36. package/lib/edm/edm.js +4 -1
  37. package/lib/edm/edmPreprocessor.js +12 -5
  38. package/lib/edm/edmUtils.js +2 -4
  39. package/lib/gen/Dictionary.json +34 -10
  40. package/lib/gen/language.checksum +1 -1
  41. package/lib/gen/language.interp +1 -1
  42. package/lib/gen/languageParser.js +3987 -3963
  43. package/lib/json/from-csn.js +43 -47
  44. package/lib/json/to-csn.js +11 -11
  45. package/lib/language/antlrParser.js +2 -1
  46. package/lib/language/genericAntlrParser.js +52 -43
  47. package/lib/language/language.g4 +59 -59
  48. package/lib/language/multiLineStringParser.js +2 -0
  49. package/lib/main.d.ts +5 -0
  50. package/lib/model/csnRefs.js +37 -19
  51. package/lib/model/csnUtils.js +20 -16
  52. package/lib/model/revealInternalProperties.js +29 -21
  53. package/lib/model/sortViews.js +4 -2
  54. package/lib/modelCompare/compare.js +112 -39
  55. package/lib/modelCompare/utils/filter.js +54 -24
  56. package/lib/optionProcessor.js +6 -6
  57. package/lib/render/manageConstraints.js +20 -17
  58. package/lib/render/toCdl.js +34 -20
  59. package/lib/render/toHdbcds.js +2 -2
  60. package/lib/render/toRename.js +4 -9
  61. package/lib/render/toSql.js +77 -26
  62. package/lib/render/utils/common.js +3 -3
  63. package/lib/render/utils/unique.js +52 -0
  64. package/lib/transform/db/applyTransformations.js +61 -20
  65. package/lib/transform/db/assertUnique.js +7 -8
  66. package/lib/transform/db/associations.js +2 -2
  67. package/lib/transform/db/cdsPersistence.js +8 -8
  68. package/lib/transform/db/expansion.js +17 -21
  69. package/lib/transform/db/flattening.js +23 -23
  70. package/lib/transform/db/rewriteCalculatedElements.js +20 -14
  71. package/lib/transform/db/temporal.js +1 -1
  72. package/lib/transform/db/transformExists.js +8 -7
  73. package/lib/transform/db/views.js +73 -33
  74. package/lib/transform/draft/db.js +11 -9
  75. package/lib/transform/draft/odata.js +1 -1
  76. package/lib/transform/{forOdataNew.js → forOdata.js} +56 -42
  77. package/lib/transform/forRelationalDB.js +69 -75
  78. package/lib/transform/localized.js +6 -5
  79. package/lib/transform/odata/toFinalBaseType.js +3 -3
  80. package/lib/transform/{transformUtilsNew.js → transformUtils.js} +4 -101
  81. package/lib/transform/translateAssocsToJoins.js +14 -28
  82. package/package.json +1 -1
  83. package/share/messages/check-proper-type-of.md +1 -1
  84. package/share/messages/{check-proper-type.md → def-missing-type.md} +3 -5
  85. package/share/messages/message-explanations.json +1 -1
@@ -1,136 +1,196 @@
1
1
  'use strict';
2
2
 
3
3
  const { hasAnnotationValue, isPersistedOnDatabase, isBuiltinType } = require('../model/csnUtils');
4
+ const { requireForeignKeyAccess } = require('./onConditions');
5
+ const { pathId } = require('../model/csnRefs');
6
+
7
+ const generalQueryProperties = [ 'from', 'columns', 'where', 'groupBy', 'orderBy', 'having', 'limit' ];
8
+
4
9
  /**
5
- * Make sure that all source artifacts and association targets reach the database
6
- * (otherwise the view can't be activated), but only if the source artifact is NOT activated against the database // <- what does this mean?
10
+ * Ensure that all source artifacts and association targets are persisted on the database.
11
+ * Otherwise, we would end up with a JOIN against a non-existent table.
7
12
  *
8
13
  * Check the given query for:
9
- * - Associations-traversal over skipped/abstract things
14
+ * - Association-traversal over skipped/abstract things
10
15
  * - Associations (indirectly) using managed associations without foreign keys
11
16
  *
12
17
  * Currently checked:
13
- * - "columns" for something like toF.id, where F is skipped. But publishing toF is fine, will be ignored later on
18
+ * - "columns" for something like toF.field, where F is skipped. But publishing toF is fine, will be ignored later on
14
19
  * - "from" for something like "select from E.toF" where E, F or E AND F are no-db.
15
20
  *
16
- *
17
21
  * @param {CSN.Query} query Query to check
18
22
  */
19
23
  function checkQueryForNoDBArtifacts( query ) {
20
24
  if (isPersistedOnDatabase(this.artifact) && !hasAnnotationValue(this.artifact, '@cds.persistence.table')) {
21
- const generalQueryProperties = [ 'from', 'columns', 'where', 'groupBy', 'orderBy', 'having', 'limit' ];
22
25
  for (const prop of generalQueryProperties) {
23
26
  const queryPart = (query.SELECT || query.SET)[prop];
24
27
  if (Array.isArray(queryPart)) {
25
28
  for (const part of queryPart)
26
- checkRef.call(this, part, prop === 'columns');
29
+ checkQueryRef.call(this, part, prop === 'columns');
27
30
  }
28
31
  else if (typeof queryPart === 'object') {
29
- checkRef.call(this, queryPart, prop === 'columns');
32
+ checkQueryRef.call(this, queryPart, prop === 'columns');
30
33
  }
31
34
  }
32
35
  }
33
36
  }
34
37
 
35
38
  /**
36
- * Count the leaf-elements resulting from a given element.
39
+ * @param {CSN.Element} assoc Definition to check
40
+ * @returns {boolean} True, if there are any foreign keys.
41
+ */
42
+ function hasForeignKeys( assoc ) {
43
+ if (!assoc || !assoc.keys)
44
+ return false;
45
+ return _hasForeignKeyOrElements.call(this, assoc);
46
+ }
47
+
48
+ /**
49
+ * Returns true if the given definition has at least one foreign key or element leaf node.
37
50
  *
38
- * @param {CSN.Element} def Definition to check
39
- * @returns {number} Number of leaf elements
51
+ * @param {CSN.Artifact} def
52
+ * @returns {boolean} True if there are FKs/element leaves.
40
53
  */
41
- function leafCount( def ) {
42
- let c = 0;
43
- if (!def)
44
- return c;
45
- if (def.elements) {
46
- c += Object.values(def.elements).reduce((acc, e) => {
47
- acc += leafCount.call(this, e);
48
- return acc;
49
- }, 0);
54
+ function _hasForeignKeyOrElements( def ) {
55
+ if (!def) {
56
+ return false;
50
57
  }
51
58
  else if (def.keys) {
52
- c += def.keys.reduce((acc, e) => {
53
- acc += leafCount.call(this, e._art);
54
- return acc;
55
- }, 0);
59
+ return def.keys.some(e => _hasForeignKeyOrElements.call(this, e._art));
60
+ }
61
+ else if (def.elements) {
62
+ return Object.values(def.elements).some( e => _hasForeignKeyOrElements.call(this, e));
56
63
  }
57
64
  else if (def.type) {
58
65
  if (isBuiltinType(def.type) && !(def.target))
59
- return 1;
60
- c += leafCount.call(this, this.csn.definitions[def.type]);
66
+ return true;
67
+ return _hasForeignKeyOrElements.call(this, this.artifactRef(def.type, null));
61
68
  }
62
- return c;
69
+ return false;
63
70
  }
64
71
 
65
72
  /**
66
- * Check the given ref for usage of skipped/abstract assoc targets
73
+ * Check the given `obj.ref` for usage of skipped/abstract assoc targets
67
74
  *
68
- * @param {object} obj CSN "thing" to check
75
+ * @param {CSN.Column} obj CSN "thing" to check
69
76
  * @param {boolean} inColumns True if the ref is part of a from
70
77
  */
71
- function checkRef( obj, inColumns ) {
72
- if (!(obj && obj.ref) || !obj._links || obj.$scope === 'alias')
78
+ function checkQueryRef( obj, inColumns ) {
79
+ if (!obj || obj.$scope === 'alias')
73
80
  return;
74
81
 
75
- const links = obj._links;
82
+ if (obj.expand || obj.inline)
83
+ _checkExpandInline.call(this, obj);
84
+
85
+ else if (obj.ref && obj._links)
86
+ _checkRef.call(this, obj.ref, obj._links, obj.$path, inColumns);
87
+ }
88
+
89
+ /**
90
+ * Run _checkRef on all expand/inline structure leaf nodes.
91
+ * We do so by creating artificial paths that follow expand/inline nodes to their leaves.
92
+ *
93
+ * @param {CSN.Column} obj
94
+ * @param {CSN.Path} previousRefs
95
+ * @param {object[]} previousLinks
96
+ */
97
+ function _checkExpandInline( obj, previousRefs = [], previousLinks = [] ) {
98
+ if (obj.ref && obj._links) { // There could be anonymous nested "expand".
99
+ previousRefs = previousRefs.concat(obj.ref);
100
+ previousLinks = previousLinks.concat(obj._links);
101
+ }
102
+
103
+ if (!obj.expand && !obj.inline) {
104
+ if (obj.ref && obj._links) {
105
+ // `inColumns: true` for expand/inline
106
+ _checkRef.call(this, previousRefs, previousLinks, obj.$path, true);
107
+ }
108
+ return;
109
+ }
110
+
111
+ for (const col of obj.expand || obj.inline)
112
+ _checkExpandInline.call(this, col, previousRefs, previousLinks);
113
+ }
114
+
115
+ /**
116
+ * Implementation of checkQueryRef() that works on ref/links arrays instead of a column.
117
+ *
118
+ * @param {CSN.Path} ref
119
+ * @param {object[]} _links
120
+ * @param {CSN.Path} $path
121
+ * @param {boolean} inColumns
122
+ */
123
+ function _checkRef( ref, _links, $path, inColumns ) {
124
+ if (!ref || !_links )
125
+ return;
126
+
127
+ let nonPersistedTarget = null;
128
+ const isPublishedAssoc = this.csnUtils.isAssocOrComposition(_links[_links.length - 1].art);
76
129
 
77
130
  // Don't check the last element - to allow association publishing in columns
78
- for (let i = 0; i < (inColumns ? links.length - 1 : links.length); i++) {
79
- const link = links[i];
131
+ for (let i = 0; i < (inColumns ? _links.length - 1 : _links.length); i++) {
132
+ const link = _links[i];
80
133
  if (!link)
81
134
  continue;
82
-
83
135
  const { art } = link;
84
136
  if (!art)
85
137
  continue;
86
138
 
87
- const endArtifact = art.target ? this.csn.definitions[art.target] : art;
88
- const pathStep = obj.ref[i].id ? obj.ref[i].id : obj.ref[i];
89
- const name = art.target ? art.target : pathStep;
90
- if (!isPersistedOnDatabase(endArtifact)) {
91
- const nextElement = obj.ref[i + 1];
92
- /**
93
- * if we only navigate to foreign keys of the managed association in a view, we do not need to join,
94
- * thus we can produce the view even if the target of the association is not persisted
95
- *
96
- * @param {CSN.Element} assoc association in ref
97
- * @param {string} nextStep the ref step following the association
98
- * @returns {boolean} true if no join will be generated
99
- */
100
- const isJoinRelevant = (assoc, nextStep) => {
101
- if (!assoc.keys)
102
- return true;
103
- const isExposedColumnAssocOrComposition = this.csnUtils.isAssocOrComposition(obj._art.type);
104
- return !assoc.keys
105
- .some(fk => fk.ref[0] === nextStep && !isExposedColumnAssocOrComposition);
106
- };
107
- if (isJoinRelevant(art, nextElement)) {
108
- const cdsPersistenceSkipped = hasAnnotationValue(endArtifact, '@cds.persistence.skip');
109
- this.error( null, obj.$path, {
110
- id: pathStep, elemref: obj, name, '#': cdsPersistenceSkipped ? 'std' : 'abstract',
139
+ const isLast = i >= _links.length - 1;
140
+ const isUnmanagedOrNoKeys = !art.keys;
141
+ const targetArt = art.target ? this.artifactRef(art.target) : art;
142
+ const pathStep = pathId(ref[i]);
143
+ const name = art.target || pathStep;
144
+
145
+ // If any path-step is not persisted, then all following path steps must only access foreign keys.
146
+ // For example, it could be toF.toG.field, where toG is FK of toF; the FK-only-check would succeed,
147
+ // but we only check "field" in the next iteration, where it is seen as access on a non-skipped
148
+ // entity, hence the need to store if any target is skipped.
149
+ if (!isPersistedOnDatabase(targetArt))
150
+ nonPersistedTarget = { name, pathStep };
151
+
152
+ if (nonPersistedTarget) {
153
+ let isJoinRelevant = isPublishedAssoc || // publishing associations is always join relevant
154
+ isLast || // e.g. FROM targets are always join relevant.
155
+ isUnmanagedOrNoKeys; // unmanaged associations are always join relevant -> no FKs
156
+
157
+ if (!isJoinRelevant) {
158
+ // for managed, published associations with more than one $path-step, only FK
159
+ // access is allowed.
160
+ requireForeignKeyAccess({ ref, _links }, i, () => {
161
+ isJoinRelevant = true;
162
+ });
163
+ }
164
+
165
+ if (isJoinRelevant) {
166
+ const cdsPersistenceSkipped = hasAnnotationValue(targetArt, '@cds.persistence.skip');
167
+ this.error( null, $path, {
168
+ '#': cdsPersistenceSkipped ? 'std' : 'abstract',
169
+ id: nonPersistedTarget.pathStep,
170
+ elemref: { ref },
171
+ name: nonPersistedTarget.name,
111
172
  }, {
112
173
  std: 'Unexpected “@cds.persistence.skip” annotation on association target $(NAME) of $(ID) in path $(ELEMREF)',
113
174
  abstract: 'Unexpected “abstract” association target $(NAME) of $(ID) in path $(ELEMREF)',
114
175
  } );
176
+ break; // only one error per path
115
177
  }
116
178
  }
179
+
117
180
  // check managed association to have foreign keys array filled
118
- if (art.keys && leafCount.call(this, art) === 0) {
119
- this.error(null,
120
- obj.$path,
121
- { id: pathStep, elemref: obj },
181
+ if (art.keys && !hasForeignKeys.call(this, art)) {
182
+ this.error(null, $path, { id: pathStep, elemref: { ref } },
122
183
  'Path step $(ID) of $(ELEMREF) has no foreign keys');
184
+ break; // only one error per path
123
185
  }
124
-
125
- if (art.on) {
126
- for (let j = 0; j < art.on.length; j++) {
127
- if (j < art.on.length - 2 && art.on[j].ref && art.on[j + 1] === '=' && art.on[j + 2].ref) {
186
+ else if (art.on) {
187
+ for (let j = 0; j < art.on.length - 2; j++) {
188
+ if (art.on[j].ref && art.on[j + 1] === '=' && art.on[j + 2].ref) {
128
189
  const [ fwdAssoc, fwdPath ] = getForwardAssociation(pathStep, art.on[j], art.on[j + 2]);
129
- if (fwdAssoc && fwdAssoc.keys && leafCount.call(this, fwdAssoc) === 0) {
130
- this.error(null, obj.$path,
131
- { name: pathStep, elemref: obj, id: fwdPath },
190
+ if (fwdAssoc?.keys && !hasForeignKeys.call(this, fwdAssoc)) {
191
+ this.error(null, $path, { name: pathStep, elemref: { ref }, id: fwdPath },
132
192
  'Path step $(NAME) of $(ELEMREF) is a $self comparison with $(ID) that has no foreign keys');
133
- j += 2;
193
+ break; // only one error per path
134
194
  }
135
195
  }
136
196
  }
@@ -139,7 +199,7 @@ function checkRef( obj, inColumns ) {
139
199
  }
140
200
 
141
201
  /**
142
- * Get the forward association from a backling $self association.
202
+ * Get the forward association from a backlink $self association.
143
203
  *
144
204
  * @param {string} prefix Name of the association
145
205
  * @param {object} lhs Left hand side of the on-condition part
@@ -18,14 +18,25 @@ function checkSqlAnnotationOnElement( member, memberName, prop, path ) {
18
18
  this.message('anno-invalid-sql-element', path, { anno: 'sql.prepend' }, 'Annotation $(ANNO) can\'t be used on elements' );
19
19
 
20
20
  if (member['@sql.append']) {
21
- if (this.artifact.query || this.artifact.projection)
21
+ if (this.artifact.query || this.artifact.projection) {
22
22
  this.message('anno-invalid-sql-view-element', path, { anno: 'sql.append' }, 'Annotation $(ANNO) can\'t be used on elements in views' );
23
- else if (this.csnUtils.isStructured(member))
23
+ }
24
+ else if (this.csnUtils.isStructured(member)) {
24
25
  this.message('anno-invalid-sql-struct', path, { anno: 'sql.append' }, 'Annotation $(ANNO) can\'t be used on structured elements' );
25
- else if (member.value && !member.value.stored)
26
+ }
27
+ else if (this.csnUtils.isManagedAssociation(member)) {
28
+ this.message('anno-invalid-sql-assoc', path, { anno: 'sql.append', '#': member.type }, {
29
+ std: 'Annotation $(ANNO) can\'t be used here',
30
+ 'cds.Association': 'Annotation $(ANNO) can\'t be used on association elements',
31
+ 'cds.Composition': 'Annotation $(ANNO) can\'t be used on composition elements',
32
+ } );
33
+ }
34
+ else if (member.value && !member.value.stored) {
26
35
  this.message('anno-invalid-sql-calc', path, { anno: 'sql.append' }, 'Annotation $(ANNO) can\'t be used on calculated elements on read' );
27
- else
36
+ }
37
+ else {
28
38
  checkValidAnnoValue(member, '@sql.append', path, this.error, this.options);
39
+ }
29
40
  }
30
41
  }
31
42
 
@@ -162,9 +162,9 @@ function errorAboutMissingType( error, path, artifact, name,
162
162
  let variant = isElement ? 'elm' : 'std';
163
163
  if (artifact.value?.stored)
164
164
  variant = 'calc';
165
- error('check-proper-type', path, { art: name, '#': variant }, {
166
- std: 'Dubious type $(ART) without type information',
167
- elm: 'Dubious element $(ART) without type information',
165
+ error('def-missing-type', path, { art: name, '#': variant }, {
166
+ std: 'Missing type for $(ART)',
167
+ elm: 'Missing type for element $(ART)',
168
168
  calc: 'A stored calculated element must have a type',
169
169
  });
170
170
  }
@@ -1,7 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { isBuiltinType } = require('../model/csnUtils');
4
- const { RelationalOperators } = require('../transform/transformUtilsNew');
4
+ const { RelationalOperators } = require('../transform/transformUtils');
5
5
  /**
6
6
  * Prepare the ref steps so that they are loggable
7
7
  *
@@ -67,6 +67,9 @@
67
67
  'use strict';
68
68
 
69
69
  const { locationString, hasErrors } = require('../base/messages');
70
+ const {
71
+ XsnSource, XsnName, XsnArtifact, CsnLocation,
72
+ } = require('./classes');
70
73
 
71
74
 
72
75
  // Properties that can appear where a type can have type arguments.
@@ -84,7 +87,8 @@ class InternalConsistencyError extends Error {
84
87
  function assertConsistency( model, stage ) {
85
88
  const stageParser = typeof stage === 'object';
86
89
  const options = stageParser && stage || model.options || { testMode: true };
87
- if (!options.testMode || options.parseOnly && !stageParser)
90
+ if (!options.testMode || options.testMode === '$noAssertConsistency' ||
91
+ options.parseOnly && !stageParser)
88
92
  return;
89
93
 
90
94
  const schema = {
@@ -105,6 +109,7 @@ function assertConsistency( model, stage ) {
105
109
  '$blocks',
106
110
  '$messageFunctions',
107
111
  '$functions',
112
+ '$assert',
108
113
  '_sortedSources',
109
114
  ],
110
115
  },
@@ -122,12 +127,14 @@ function assertConsistency( model, stage ) {
122
127
  '$withLocalized',
123
128
  '$sources',
124
129
  ],
130
+ instanceOf: XsnSource,
125
131
  },
126
132
  location: {
127
133
  // every thing with a $location in CSN must have a XSN location even
128
134
  // with syntax errors (currently even internal artifacts like $using):
129
135
  isRequired: parent => noSyntaxErrors() || parent && parent.kind,
130
136
  kind: true,
137
+ instanceOf: CsnLocation,
131
138
  requires: [ 'file' ], // line is optional in top-level location
132
139
  optional: [ 'line', 'col', 'endLine', 'endCol', '$notFound' ],
133
140
  schema: {
@@ -138,8 +145,8 @@ function assertConsistency( model, stage ) {
138
145
  $notFound: { test: isBoolean },
139
146
  },
140
147
  },
141
- sources: { test: isDictionary( isObject ) },
142
- _sortedSources: { test: isArray( isObject ) },
148
+ sources: { test: isDictionary( isObject ), instanceOf: XsnSource },
149
+ _sortedSources: { test: isArray( isObject ), instanceOf: XsnSource },
143
150
  file: { test: isString },
144
151
  dirname: { test: isString }, // TODO: really necessary?
145
152
  realname: { test: isString }, // TODO: really necessary?
@@ -158,11 +165,13 @@ function assertConsistency( model, stage ) {
158
165
  test: isDictionary( definition ),
159
166
  requires: [ 'kind', 'location', 'name' ],
160
167
  optional: thoseWithKind,
168
+ instanceOf: XsnArtifact,
161
169
  },
162
170
  vocabularies: {
163
171
  test: isDictionary( definition ),
164
172
  requires: [ 'kind', 'name' ],
165
173
  optional: thoseWithKind,
174
+ instanceOf: XsnArtifact,
166
175
  },
167
176
  extensions: {
168
177
  kind: [ 'context' ], // syntax error (as opposed to HANA CDS), but still there
@@ -174,7 +183,7 @@ function assertConsistency( model, stage ) {
174
183
  i18n: {
175
184
  test: isDictionary( ( val, parent, prop, spec, lang ) => {
176
185
  const textValueIsString = (v, p, textProp, s, textKey) => {
177
- isString(v.val, p, textKey, s);
186
+ isString( v.val, p, textKey, s );
178
187
  };
179
188
  const innerDict = isDictionary( textValueIsString );
180
189
  return innerDict( val, parent, lang, spec );
@@ -197,7 +206,7 @@ function assertConsistency( model, stage ) {
197
206
  ],
198
207
  schema: {
199
208
  kind: { test: isString, enum: [ 'builtin' ] },
200
- name: { test: isObject, requires: [ 'id', 'element' ] },
209
+ name: { test: isObject, instanceOf: XsnName, requires: [ 'id', 'element' ] },
201
210
  $autoElement: { test: isString },
202
211
  $uncheckedElements: { test: isBoolean },
203
212
  $requireElementAccess: { test: isBoolean },
@@ -240,7 +249,7 @@ function assertConsistency( model, stage ) {
240
249
  typeProps$: { kind: true, enumerable: false, test: TODO },
241
250
  actions: { kind: true, inherits: 'definitions' },
242
251
  enum: { kind: true, inherits: 'definitions' },
243
- foreignKeys: { kind: true, inherits: 'definitions' },
252
+ foreignKeys: { kind: true, inherits: 'definitions', instanceOf: 'ignore' },
244
253
  $keysNavigation: { kind: true, test: TODO },
245
254
  params: { kind: true, inherits: 'definitions' },
246
255
  _extendType: { kind: true, test: TODO },
@@ -306,6 +315,7 @@ function assertConsistency( model, stage ) {
306
315
  columns: {
307
316
  kind: [ 'extend', '$column' ],
308
317
  test: isArray( column ),
318
+ instanceOf: XsnArtifact,
309
319
  optional: thoseWithKind,
310
320
  enum: [ '*' ],
311
321
  requires: [ 'location' ],
@@ -383,6 +393,7 @@ function assertConsistency( model, stage ) {
383
393
  'select', '$join', 'mixin',
384
394
  'source', 'namespace', 'using',
385
395
  '$tableAlias', '$navElement',
396
+ 'builtin', // magic variables
386
397
  ],
387
398
  },
388
399
  // locations of parentheses pairs around expression:
@@ -501,6 +512,7 @@ function assertConsistency( model, stage ) {
501
512
  name: {
502
513
  isRequired: stageParser && (() => false), // not required in parser
503
514
  kind: true,
515
+ instanceOf: 'ignore', // TODO: XsnName,
504
516
  schema: {
505
517
  select: { test: TODO },
506
518
  }, // TODO: rename query prop in name
@@ -527,6 +539,7 @@ function assertConsistency( model, stage ) {
527
539
  kind: [ 'action', 'function' ],
528
540
  requires: [ 'kind', 'location' ],
529
541
  optional: thoseWithKind,
542
+ instanceOf: XsnArtifact,
530
543
  },
531
544
  items: {
532
545
  kind: true,
@@ -686,6 +699,7 @@ function assertConsistency( model, stage ) {
686
699
  $expected: { parser: true, test: isOneOf([ 'approved-exists', 'exists' ]) },
687
700
  $messageFunctions: { test: TODO },
688
701
  $functions: { test: TODO },
702
+ $assert: { test: TODO }, // currently just for missing Error[ref-cycle]
689
703
  };
690
704
  let _noSyntaxErrors = null;
691
705
  assertProp( model, null, stageParser ? ':parser' : ':model', null, true );
@@ -714,8 +728,10 @@ function assertConsistency( model, stage ) {
714
728
  : {}.propertyIsEnumerable.call( parent, prop ) !== enumerable)
715
729
  throw new InternalConsistencyError( `Unexpected enumerability ${ !enumerable }${ at( [ node, parent ], prop ) }` );
716
730
  }
717
- (spec.test || standard)( node, parent, prop, spec,
718
- typeof noPropertyTest === 'string' && noPropertyTest );
731
+ if (node !== undefined) { // ignore if undefined
732
+ (spec.test || standard)( node, parent, prop, spec,
733
+ typeof noPropertyTest === 'string' && noPropertyTest );
734
+ }
719
735
  }
720
736
 
721
737
  function definition( node, parent, prop, spec, name ) {
@@ -779,10 +795,11 @@ function assertConsistency( model, stage ) {
779
795
  const opt = Array.isArray(optional)
780
796
  ? optional.includes( n ) || optional.includes( n.charAt(0) )
781
797
  : optional( n, spec );
782
- if (!(opt || requires.includes( n ) || n === '$extra'))
783
- throw new InternalConsistencyError( `Property '${ n }' is not expected${ at( [ node[n], node, parent ], prop, name ) }` );
784
-
785
- assertProp( node[n], node, n, spec.schema && spec.schema[n] );
798
+ if (node[n] !== undefined) {
799
+ if (!(opt || requires.includes( n ) || n === '$extra'))
800
+ throw new InternalConsistencyError( `Property '${ n }' is not expected${ at( [ node[n], node, parent ], prop, name ) }` );
801
+ assertProp( node[n], node, n, spec.schema && spec.schema[n] );
802
+ }
786
803
  }
787
804
  }
788
805
 
@@ -921,7 +938,9 @@ function assertConsistency( model, stage ) {
921
938
  const valSchema = { val: Object.assign( {}, spec, { test: func } ) };
922
939
  const requires = [ 'val', 'location' ];
923
940
  const optional = [ 'literal', '$inferred', '$priority', '_pathHead' ];
924
- standard( node, parent, prop, { schema: valSchema, requires, optional }, name );
941
+ standard( node, parent, prop, {
942
+ schema: valSchema, requires, optional, instanceOf: spec.instanceOf,
943
+ }, name );
925
944
  };
926
945
  }
927
946
 
@@ -948,7 +967,7 @@ function assertConsistency( model, stage ) {
948
967
 
949
968
  function isString( node, parent, prop, spec ) {
950
969
  if (typeof node !== 'string')
951
- throw new InternalConsistencyError( `Expected string${ at( [ node, parent ], prop ) }` );
970
+ throw new InternalConsistencyError( `Expected string but found ${ typeof node }${ at( [ node, parent ], prop ) }` );
952
971
  // TODO: also check getOwnPropertyNames(node)
953
972
  if (spec.enum && !spec.enum.includes( node ))
954
973
  throw new InternalConsistencyError( `Unexpected value '${ node }'${ at( [ node, parent ], prop ) }` );
@@ -962,10 +981,19 @@ function assertConsistency( model, stage ) {
962
981
  }
963
982
 
964
983
  function isObject( node, parent, prop, spec, name ) {
965
- if (!node || typeof node !== 'object' || Object.getPrototypeOf( node ) !== Object.prototype)
966
- throw new InternalConsistencyError( `Expected standard object${ at( [ null, parent ], prop, name ) }` );
984
+ if (!node || typeof node !== 'object' )
985
+ throw new InternalConsistencyError( `Expected object${ at( [ null, parent ], prop, name ) }` );
986
+ const found = Object.getPrototypeOf( node )?.constructor?.name || 'null';
987
+ if (!spec.instanceOf && Object.getPrototypeOf( node ) !== Object.prototype)
988
+ throw new InternalConsistencyError( `Expected standard object but found ${ found }${ at( [ null, parent ], prop, name ) }` );
989
+ // TODO
990
+ // else if (spec.instanceOf && spec.instanceOf !== 'ignore' &&
991
+ // Object.getPrototypeOf( node ) !== spec.instanceOf.prototype)
992
+ // eslint-disable-next-line max-len
993
+ // throw new InternalConsistencyError( `Expected object of class ${ spec.instanceOf.name } but found ${ found }${ at( [ null, parent ], prop, name ) }` );
967
994
  }
968
995
 
996
+
969
997
  function inDefinitions( art, parent, prop, spec, name ) {
970
998
  if (Array.isArray(art)) // do not check with redefinitions
971
999
  return;
@@ -72,6 +72,7 @@ function propExists( prop, parent ) {
72
72
  * have a "sparse" name structure.
73
73
  *
74
74
  * @param {XSN.Artifact} art
75
+ * @returns {XSN.Name}
75
76
  */
76
77
  function getArtifactName( art ) {
77
78
  if (!art.name || art.name.absolute) // no name or “old style”
@@ -390,7 +390,7 @@ function isRelationTypeName( typeName ) {
390
390
  * @returns {boolean}
391
391
  */
392
392
  function isInReservedNamespace( absolute ) {
393
- return absolute.startsWith( 'cds.') &&
393
+ return absolute === 'cds' || absolute.startsWith( 'cds.') &&
394
394
  !absolute.match(/^cds\.foundation(\.|$)/) &&
395
395
  !absolute.match(/^cds\.outbox(\.|$)/) && // Requested by Node runtime
396
396
  !absolute.match(/^cds\.xt(\.|$)/); // Requested by Mtx
@@ -485,7 +485,10 @@ function initBuiltins( model ) {
485
485
  for (const name in builtins) {
486
486
  const magic = builtins[name];
487
487
  // TODO: rename to $builtinFunction
488
- const art = { kind: 'builtin', name: { id: name, absolute: name } };
488
+ const art = {
489
+ kind: 'builtin',
490
+ name: { id: name, absolute: '', element: name },
491
+ };
489
492
  artifacts[name] = art;
490
493
 
491
494
  if (magic.$autoElement)
@@ -513,12 +516,8 @@ function initBuiltins( model ) {
513
516
 
514
517
  for (const n of names) {
515
518
  const magic = {
516
- kind: 'builtin',
517
- name: {
518
- id: n,
519
- absolute: art.name.absolute,
520
- element: art.name.element ? `${ art.name.element }.${ n }` : n,
521
- },
519
+ kind: 'builtin', // TODO: '$variable'
520
+ name: { id: n, absolute: '', element: `${ art.name.element }.${ n }` },
522
521
  };
523
522
  // Propagate this property so that it is available for sub-elements.
524
523
  if (art.$uncheckedElements)