@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
@@ -109,29 +109,97 @@ function checkVirtualElement( member ) {
109
109
  }
110
110
 
111
111
  /**
112
- * Checks whether managed associations
113
- * with cardinality 'to many' have an on-condition.
112
+ * Checks whether managed associations with cardinality 'to many' have no ON-condition.
113
+ * If there isn't, and if _all_ key elements on the target side are covered by the foreign key,
114
+ * then the association is effectively to-one -> warning.
114
115
  *
115
116
  * @param {CSN.Artifact} member The member (e.g. element artifact)
116
117
  */
117
118
  function checkManagedAssoc( member ) {
118
119
  if (!member.target || isManagedComposition.bind(this)(member))
119
120
  return;
120
-
121
121
  // Implementation note: Imported services (i.e. external ones) may contain to-many associations
122
122
  // with an empty foreign key list. If the user (in this case importer) explicitly sets an empty
123
123
  // foreign key array, we won't emit a warning to avoid spamming the user.
124
- const max = member.cardinality?.max ? member.cardinality.max : 1;
125
- if (max !== 1 && !member.on && (!member.keys || member.keys.length > 0)) {
126
- const isNoDb = this.artifact['@cds.persistence.skip'] || this.artifact['@cds.persistence.exists'];
127
- this.warning(isNoDb ? 'to-many-no-on-noDB' : 'to-many-no-on', member.cardinality ? member.cardinality.$path : member.$path, {
128
- value: cardinality2str(member, false),
129
- '#': this.csnUtils.isComposition(member) ? 'comp' : 'std',
130
- }, {
131
- std: 'Expected association with target cardinality $(VALUE) to have an ON-condition',
132
- comp: 'Expected composition with target cardinality $(VALUE) to have an ON-condition',
133
- });
124
+ const max = member.cardinality?.max ?? 1;
125
+ if (max === 1 || member.on || !member.keys || member.keys.length === 0)
126
+ return;
127
+ // We use the fact that `key` is only supported top-level (warning otherwise).
128
+ // And if an element of a structured _type_ is "key", we get a warning for the key.
129
+ // However, we may get false negatives for our warning, which is acceptable, e.g. for
130
+ // `type T : { key i: String; }; entity A { id : T; };` with `… to many A { id };`
131
+ const target = typeof member.target === 'object' ? member.target : this.csnUtils.getCsnDef(member.target);
132
+ const targetKeys = Object.entries(target.elements || {}).filter(elem => !!elem[1].key);
133
+ const foreignKeys = structurizeForeignKeys(member.keys);
134
+ if (!coversAllTargetKeys.call(this, foreignKeys, targetKeys))
135
+ return; // foreign key does not cover at least one target key -> can be to-many
136
+
137
+ const isNoDb = this.artifact['@cds.persistence.skip'] || this.artifact['@cds.persistence.exists'];
138
+ this.warning(isNoDb ? 'to-many-no-on-noDB' : 'to-many-no-on', member.cardinality?.$path || member.$path, {
139
+ value: cardinality2str(member, false),
140
+ '#': this.csnUtils.isComposition(member) ? 'comp' : 'std',
141
+ }, {
142
+ std: 'Expected association with target cardinality $(VALUE) to have an ON-condition',
143
+ comp: 'Expected composition with target cardinality $(VALUE) to have an ON-condition',
144
+ });
145
+ }
146
+
147
+ /**
148
+ * Returns true if the foreign keys cover _all_ of the target keys.
149
+ * Returns false otherwise.
150
+ *
151
+ * @see checkManagedAssoc()
152
+ *
153
+ * @param {object} foreignKeys Structure returned by `structurizeForeignKeys()`.
154
+ * @param {Array} targetKeys Object.entries() value of target keys.
155
+ * @returns {boolean} Whether all target keys are covered
156
+ */
157
+ function coversAllTargetKeys( foreignKeys, targetKeys ) {
158
+ if (foreignKeys.length < targetKeys.length || targetKeys.length === 0) {
159
+ // there are fewer foreign keys than keys on the target side
160
+ // or there are no keys on the target side, in which case there is no
161
+ // possibility to cover all keys.
162
+ return false;
163
+ }
164
+
165
+ for (const [ targetKeyName, targetKey ] of targetKeys) {
166
+ const foreignKey = foreignKeys.entries[targetKeyName];
167
+ if (!foreignKey)
168
+ return false; // foreign key does not cover this target key
169
+ if (foreignKey.length > 0) { // foreign key only selects sub-structures, not whole structured key
170
+ const elements = targetKey.elements || this.csnUtils.getFinalTypeInfo(targetKey.type)?.elements;
171
+ if (!elements)
172
+ return false; // model error (e.g. 'many type')
173
+ if (!coversAllTargetKeys( foreignKey, Object.entries(elements) ))
174
+ return false;
175
+ }
176
+ }
177
+ return true;
178
+ }
179
+
180
+ /**
181
+ * Structurizes a foreign key into an object that can be used to compare foreign
182
+ * keys against their corresponding keys on target side.
183
+ *
184
+ * For `Association to T { a.b, a.c, b }` this structure will be returned:
185
+ * `{ length: 2, entries: { a: { length: 2, entries: { b: { length: 0, … }, …} } }}`
186
+ *
187
+ * @param {object[]} keys Foreign key array.
188
+ * @returns {object} Structured foreign key. Custom format, i.e. not via `elements`.
189
+ */
190
+ function structurizeForeignKeys( keys ) {
191
+ const map = { entries: Object.create(null), length: 0 };
192
+ for (const key of keys) {
193
+ let entry = map;
194
+ for (const step of key.ref) {
195
+ if (!entry.entries[step]) {
196
+ entry.entries[step] = { entries: Object.create(null), length: 0 };
197
+ ++entry.length;
198
+ }
199
+ entry = entry.entries[step];
200
+ }
134
201
  }
202
+ return map;
135
203
  }
136
204
 
137
205
  /**
@@ -85,6 +85,7 @@ function enrichCsn( csn, options ) {
85
85
  csnPath.pop();
86
86
  }
87
87
 
88
+
88
89
  /**
89
90
  * Transformer for things that are annotations. When we have a "=" plus an expression of some sorts,
90
91
  * we treat it like a "standard" thing.
@@ -94,7 +95,7 @@ function enrichCsn( csn, options ) {
94
95
  * @param {object} node The value of node[_prop]
95
96
  */
96
97
  function annotation( _parent, _prop, node ) {
97
- if (options.processAnnotations) {
98
+ if (options.enrichAnnotations) {
98
99
  if (node?.['='] !== undefined && xprInAnnoProperties.some(xProp => node[xProp] !== undefined)) {
99
100
  standard(_parent, _prop, node);
100
101
  }
@@ -212,5 +213,5 @@ module.exports = enrichCsn;
212
213
 
213
214
  /**
214
215
  * @typedef {object} enrichCsnOptions
215
- * @property {boolean} [processAnnotations=false] Wether to process annotations and call custom transformers on them
216
+ * @property {boolean} [enrichAnnotations=false] Wether to process annotations and call custom transformers on them
216
217
  */
@@ -5,7 +5,7 @@ const {
5
5
  forEachMember, getNormalizedQuery, hasAnnotationValue,
6
6
  applyTransformations, functionList,
7
7
  } = require('../model/csnUtils');
8
- const enrich = require('./enricher');
8
+ const enrichCsn = require('./enricher');
9
9
 
10
10
  // forRelationalDB
11
11
  const { validateSelectItems } = require('./selectItems');
@@ -48,6 +48,7 @@ const {
48
48
  checkSqlAnnotationOnArtifact,
49
49
  checkSqlAnnotationOnElement,
50
50
  } = require('./sql-snippets');
51
+ const dbFeatureFlags = require('./dbFeatureFlags');
51
52
 
52
53
  const forRelationalDBMemberValidators
53
54
  = [
@@ -65,8 +66,7 @@ const forRelationalDBMemberValidators
65
66
  checkElementTypeDefinitionHasType,
66
67
  ];
67
68
 
68
- const forRelationalDBArtifactValidators
69
- = [
69
+ const forRelationalDBArtifactValidators = [
70
70
  checkPrimaryKey,
71
71
  // @cds.persistence has no impact on odata
72
72
  validateCdsPersistenceAnnotation,
@@ -74,6 +74,8 @@ const forRelationalDBArtifactValidators
74
74
  validateHasPersistedElements,
75
75
  // sql.prepend/append
76
76
  checkSqlAnnotationOnArtifact,
77
+ // strip down CSN to reduce it's size by removing non-sql relevant parts
78
+ stripNonDbRelevant,
77
79
  ];
78
80
 
79
81
  const forRelationalDBCsnValidators = [
@@ -82,6 +84,7 @@ const forRelationalDBCsnValidators = [
82
84
  nonexpandableStructuredInExpression,
83
85
  navigationIntoMany,
84
86
  checkPathsInStoredCalcElement,
87
+ dbFeatureFlags,
85
88
  ];
86
89
  /**
87
90
  * @type {Array<(query: CSN.Query, path: CSN.Path) => void>}
@@ -149,7 +152,9 @@ function _validate( csn, that,
149
152
  artifactValidators = [],
150
153
  queryValidators = [],
151
154
  iterateOptions = {} ) {
152
- const { cleanup } = enrich(csn, { processAnnotations: that.options.tranformation === 'odata' });
155
+ const { cleanup } = enrichCsn(csn, that.options);
156
+ // TODO: Don't know if that's feasible? Do we really need to enrich annotations always?
157
+ // const { cleanup } = enrich(csn, { processAnnotations: that.options.tranformation === 'odata' });
153
158
 
154
159
  applyTransformations(csn, mergeCsnValidators(csnValidators, that), [], { drillRef: true });
155
160
 
@@ -277,4 +282,33 @@ function forOdata( csn, that ) {
277
282
  });
278
283
  }
279
284
 
285
+ const dbRelevantKinds = {
286
+ entity: true,
287
+ type: true,
288
+ aspect: true,
289
+ service: true,
290
+ context: true,
291
+ };
292
+
293
+ /**
294
+ * Shrink the CSN by
295
+ * - deleting bound actions
296
+ * - turning all non-entity/type/aspect/service/context entities into dummies
297
+ *
298
+ * @param {CSN.Artifact} artifact
299
+ * @param {string} artifactName
300
+ */
301
+ function stripNonDbRelevant( artifact, artifactName ) {
302
+ if (this.options.transformation !== 'effective') {
303
+ if ( !dbRelevantKinds[artifact.kind] ) {
304
+ this.csn.definitions[artifactName] = {
305
+ kind: 'action',
306
+ };
307
+ }
308
+ else if (artifact.actions) {
309
+ delete artifact.actions;
310
+ }
311
+ }
312
+ }
313
+
280
314
  module.exports = { forRelationalDB, forOdata };
@@ -364,7 +364,7 @@ function assertConsistency( model, stage ) {
364
364
  optional: [
365
365
  'path', 'elements', '_outer', '_parent', '_main', '_block', 'kind',
366
366
  'scope', '_artifact', '$inferred', '$expand', '$inCycle', '$tableAliases', '_$next',
367
- '_origin', '_effectiveType', '$effectiveSeqNo', '_extensions',
367
+ '_origin', '_effectiveType', '$effectiveSeqNo', '_extensions', '$contains',
368
368
  ],
369
369
  },
370
370
  target: {
@@ -463,7 +463,7 @@ function assertConsistency( model, stage ) {
463
463
  ...typeProperties, // for CAST
464
464
  ],
465
465
  },
466
- query: { requires: [ 'query', 'location' ], optional: [ 'stored' ] },
466
+ query: { requires: [ 'query', 'location' ], optional: [ 'stored', '$parens' ] },
467
467
  },
468
468
  literal: { // TODO: check value against literal
469
469
  test: isString,
@@ -481,7 +481,7 @@ function assertConsistency( model, stage ) {
481
481
  'literal', 'val', 'sym', 'struct', 'variant', 'path', 'name', '$duplicates', 'upTo',
482
482
  // expressions as annotation values
483
483
  '$tokenTexts', 'op', 'args', 'func', '_artifact', 'type', '$typeArgs',
484
- 'scale', 'srid', 'length', 'precision',
484
+ 'scale', 'srid', 'length', 'precision', 'scope',
485
485
  ],
486
486
  // TODO: restrict path to #simplePath
487
487
  },
@@ -490,7 +490,7 @@ function assertConsistency( model, stage ) {
490
490
  args: {
491
491
  inherits: 'value',
492
492
  optional: [
493
- 'name', '$duplicate', '$expected', 'args', 'suffix',
493
+ 'name', '$duplicate', '$expected', 'args', 'suffix', '$parens',
494
494
  'param', 'scope', // for dynamic parameter '?'
495
495
  ],
496
496
  test: args,
@@ -591,10 +591,11 @@ function check( model ) {
591
591
  *
592
592
  * @param {any} xpr The expression to check
593
593
  * @param {XSN.Artifact} user User for semantic location
594
- * @returns {void}
594
+ * @param {string} [context] where the expression is used, e.g. 'anno'
595
595
  */
596
- function checkGenericExpression( xpr, user ) {
597
- checkExpressionNotVirtual( xpr, user );
596
+ function checkGenericExpression( xpr, user, context ) {
597
+ if (context !== 'anno')
598
+ checkExpressionNotVirtual( xpr, user );
598
599
  checkExpressionAssociationUsage( xpr, user, false );
599
600
  if (xpr.op?.val === 'cast') {
600
601
  requireExplicitTypeInSqlCast( xpr, user );
@@ -903,7 +904,7 @@ function check( model ) {
903
904
  */
904
905
  function checkAnnotationExpressions( anno, art ) {
905
906
  if (anno.$tokenTexts) {
906
- checkGenericExpression( anno, art );
907
+ checkGenericExpression( anno, art, 'anno' );
907
908
  }
908
909
  else if (anno.literal === 'array') {
909
910
  anno.val.forEach( val => checkAnnotationExpressions( val, art ) );
@@ -946,8 +946,8 @@ function define( model ) {
946
946
 
947
947
  if (hasItems && !wildcard && parent.excludingDict && !options.$recompile) {
948
948
  // TODO: the SQL backend should probably delete `excluding` when expanding `*`
949
- // TODO: use `parent` for semantic location, use `-excluding`
950
- warning( 'query-ignoring-exclude', [ parent.excludingDict[$location], user ],
949
+ // TODO: use `parent` for semantic location; requires `_parent`/... links.
950
+ warning( 'query-ignoring-excluding', [ parent.excludingDict[$location], user ],
951
951
  { prop: '*' },
952
952
  'Excluding elements without wildcard $(PROP) has no effect' );
953
953
  }
@@ -782,7 +782,7 @@ function generate( model ) {
782
782
  }
783
783
 
784
784
  /**
785
- * Copy the annotations `@cds.persistence.skip`/`@cds.persistence.exists` from
785
+ * Copy relevant annotations from
786
786
  * source to target if present on source but not target.
787
787
  *
788
788
  * @param {object} target
@@ -796,6 +796,7 @@ function generate( model ) {
796
796
  if (copyExists)
797
797
  copy( '@cds.persistence.exists' );
798
798
  copy( '@cds.persistence.skip' );
799
+ copy( '@cds.tenant.independent' );
799
800
 
800
801
  function copy( anno ) {
801
802
  if ( source[anno] && !target[anno] )
@@ -100,6 +100,8 @@ function populate( model ) {
100
100
  ? 'Composition'
101
101
  : true;
102
102
  const redirectInSubQueries = isDeprecatedEnabled( options, '_redirectInSubQueries' );
103
+ const ignoreSpecifiedElements
104
+ = isDeprecatedEnabled( model.options, 'ignoreSpecifiedQueryElements' );
103
105
 
104
106
  forEachDefinition( model, traverseElementEnvironments );
105
107
  while (newAutoExposed.length) {
@@ -542,7 +544,9 @@ function populate( model ) {
542
544
  ielem[prop].$priority = 'annotate';
543
545
  wasAnnotated = true;
544
546
  }
545
- else if (typePropertiesFromSpecifiedElements[prop]) {
547
+ else if (typePropertiesFromSpecifiedElements[prop] && !ignoreSpecifiedElements) {
548
+ // If ignoreSpecifiedElements is set, we ignore type properties of specified elements,
549
+ // similar to how it was done in cds-compiler v3. Only annotations are copied.
546
550
  if (!ielem.typeProps$)
547
551
  setLink( ielem, 'typeProps$', Object.create( null ) );
548
552
  // Note: At this point in time, effectiveType() was likely not called on the
@@ -27,11 +27,12 @@ const $inferred = Symbol.for( 'cds.$inferred' );
27
27
  function propagate( model ) {
28
28
  const props = {
29
29
  '@com.sap.gtt.core.CoreModel.Indexable': never,
30
- '@cds.persistence.exists': never,
30
+ '@cds.persistence.exists': never, // also copied in generate.js
31
31
  '@cds.persistence.table': never,
32
32
  '@cds.persistence.calcview': never,
33
33
  '@cds.persistence.udf': never,
34
- '@cds.persistence.skip': notWithPersistenceTable,
34
+ '@cds.persistence.skip': notWithPersistenceTable, // also copied in generate.js
35
+ // '@cds.tenant.independent' is propagated as normal, but also copied in generate.js
35
36
  '@sql.append': never,
36
37
  '@sql.prepend': never,
37
38
  '@sql.replace': never,
@@ -106,15 +107,6 @@ function propagate( model ) {
106
107
  chain.push({ target, source: target.value._artifact });
107
108
  if (checkAndSetStatus( target.value._artifact ))
108
109
  news.push( target.value._artifact );
109
-
110
- if (target.value?._artifact.$inferred !== 'include') {
111
- // If the referred to element is not inferred, it is a new one and not the original.
112
- // The new one was not originally referred to => error;
113
- // TODO: no messages in propagator.js
114
- warning( 'ref-unexpected-override', [ target.name.location, target ],
115
- { id: target.name.id, target: target.value?._artifact },
116
- 'Calculated element $(ID) does not originally refer to $(TARGET)' );
117
- }
118
110
  }
119
111
  chain.push( { target, source: origin } );
120
112
  if (checkAndSetStatus( origin ))
@@ -1245,7 +1245,8 @@ function fns( model ) {
1245
1245
  else {
1246
1246
  if (!art.query && !art.type && !art.params && (art.elements || art.kind === 'aspect'))
1247
1247
  return art;
1248
- signalNotFound( 'ref-invalid-include', [ ref.location, user ], null );
1248
+ const variant = art.params && 'param' || 'std';
1249
+ signalNotFound( 'ref-invalid-include', [ ref.location, user ], null, { '#': variant } );
1249
1250
  }
1250
1251
  return false;
1251
1252
  }
@@ -160,12 +160,23 @@ function tweakAssocs( model ) {
160
160
  function checkAnnotationForRefs( expr, user ) {
161
161
  if (expr.$tokenTexts)
162
162
  return traverseExpr( expr, 'annoRewrite', user, checkAnnotationRef );
163
- if (expr.literal === 'array')
164
- return expr.val.find( val => checkAnnotationForRefs( val, user ) );
163
+ if (expr.literal === 'array') {
164
+ for (const val of expr.val) {
165
+ const found = checkAnnotationForRefs( val, user );
166
+ if (found)
167
+ return found;
168
+ }
169
+ return null;
170
+ }
165
171
  if (expr.literal !== 'struct')
166
172
  return null;
167
173
  const struct = Object.values( expr.struct );
168
- return struct.find( val => checkAnnotationForRefs( val, user ) );
174
+ for (const val of struct) {
175
+ const found = checkAnnotationForRefs( val, user );
176
+ if (found)
177
+ return found;
178
+ }
179
+ return null;
169
180
  }
170
181
 
171
182
  function checkAnnotationRef( ref, refCtx, user ) {
@@ -308,6 +319,8 @@ function tweakAssocs( model ) {
308
319
  let elem = element.items || element; // TODO v5: nested items
309
320
  if (elem.elements)
310
321
  forEachGeneric( elem, 'elements', rewriteAssociation );
322
+ if (elem.targetAspect?.elements)
323
+ forEachGeneric( elem.targetAspect, 'elements', rewriteAssociation );
311
324
  if (!originTarget( elem ))
312
325
  return;
313
326
  // console.log(message( null, elem.location, elem,
@@ -451,11 +464,7 @@ function tweakAssocs( model ) {
451
464
  'Selecting unmanaged associations from a sub query is not supported' );
452
465
  }
453
466
 
454
- // filter was copied in original element already
455
- // TODO: not correct for e.g. composition-of-inline-aspect (#12223)
456
- if (elem.$inferred !== 'include')
457
- addConditionFromAssocPublishing( elem, assoc, nav );
458
-
467
+ addConditionFromAssocPublishing( elem, assoc, nav );
459
468
  elem.on.$inferred = 'rewrite';
460
469
  }
461
470
 
@@ -468,6 +477,10 @@ function tweakAssocs( model ) {
468
477
  * The added condition (filter) is already rewritten relative to `elem`.
469
478
  */
470
479
  function addConditionFromAssocPublishing( elem, assoc, nav ) {
480
+ if (elem.$inferred || elem._main?.$inferred === 'composition-entity') {
481
+ // filter was copied in original element already
482
+ return;
483
+ }
471
484
  const publishAssoc = (elem._main?.query || elem.$syntax === 'calc') &&
472
485
  elem.value?.path?.length > 0;
473
486
  if (!publishAssoc)
@@ -501,10 +514,10 @@ function tweakAssocs( model ) {
501
514
  }
502
515
 
503
516
  elem.on = {
504
- op: { val: 'and', location },
517
+ op: { val: 'ixpr', location },
505
518
  args: [
506
- // TODO: Get rid of $parens
507
519
  { ...elem.on, $parens: [ assoc.location ] },
520
+ { val: 'and', literal: 'token', location },
508
521
  filterToCondition( lastStep, elem, nav ),
509
522
  ],
510
523
  location,
@@ -526,7 +539,6 @@ function tweakAssocs( model ) {
526
539
  */
527
540
  function filterToCondition( assocPathStep, elem, nav ) {
528
541
  const cond = copyExpr( assocPathStep.where );
529
- // TODO: Get rid of $parens
530
542
  cond.$parens = [ assocPathStep.location ];
531
543
  traverseExpr( cond, 'rewrite-filter', elem, (expr) => {
532
544
  if (!expr.path || expr.path.length === 0)
@@ -584,7 +596,7 @@ function tweakAssocs( model ) {
584
596
  const lhs = {
585
597
  path: [
586
598
  { id: assoc.name.id, location: elem.name.location },
587
- ...copyExpr( fKey.targetElement.path ),
599
+ ...copyExpr( fKey.targetElement.path, weakLocation( elem.name.location ) ),
588
600
  ],
589
601
  location: elem.name.location,
590
602
  };
@@ -597,7 +609,7 @@ function tweakAssocs( model ) {
597
609
  path: [
598
610
  // use origin's name; elem could have alias
599
611
  { id: assoc.name.id, location: elem.name.location },
600
- ...copyExpr( fKey.targetElement.path ),
612
+ ...copyExpr( fKey.targetElement.path, weakLocation( elem.name.location ) ),
601
613
  ],
602
614
  location: elem.name.location,
603
615
  };
@@ -762,7 +774,7 @@ function tweakAssocs( model ) {
762
774
  state = setArtifactLink( i, elem );
763
775
  }
764
776
  else if (i) {
765
- state = rewriteItem( state, i, i.id, assoc );
777
+ state = rewriteItem( state, i, i.id, assoc, false );
766
778
  if (!state || state === true)
767
779
  break;
768
780
  }
@@ -801,9 +813,10 @@ function tweakAssocs( model ) {
801
813
  elem;
802
814
  // TODO: probably better to collect the non-projected foreign keys
803
815
  // and have one message for all
804
- error( 'rewrite-undefined-key', [ weakLocation( culprit.location ), assoc ],
805
- { id: item.id, art: alias._main },
806
- 'Foreign key $(ID) has not been found in target $(ART)' );
816
+ error( 'rewrite-undefined-key', [ weakLocation( culprit.location ), assoc ], {
817
+ '#': 'std', id: item.id, target: alias._main, name: assoc.name.id,
818
+ });
819
+ // ''
807
820
  return null;
808
821
  }
809
822
  item.id = name;
@@ -813,13 +826,19 @@ function tweakAssocs( model ) {
813
826
  // refs in ON cannot navigate along `items`, no need to consider `items` here
814
827
  if (env?.target)
815
828
  env = env.target._artifact?._effectiveType;
816
- elem = setArtifactLink( item, env?.elements?.[name] );
817
-
818
- if (elem)
819
- return elem;
820
- // TODO: better (extra message), TODO: do it
821
- error( 'query-undefined-element', [ item.location, assoc ],
822
- { id: name || item.id, '#': 'redirected' } );
829
+ const found = setArtifactLink( item, env?.elements?.[name] );
830
+ if (found)
831
+ return found;
832
+
833
+ const isExplicit = elem.target && !elem.target.$inferred;
834
+ const loc = isExplicit ? elem.target.location : item.location;
835
+ error( 'query-undefined-element', [ loc, assoc ], {
836
+ '#': isExplicit ? 'redirected' : 'std',
837
+ id: name || item.id,
838
+ name: elem.name.id,
839
+ target: elem.target._artifact,
840
+ keyword: 'redirected to',
841
+ } );
823
842
  return null;
824
843
  }
825
844
  }
@@ -309,6 +309,9 @@ function xpr2edmJson( carrier, anno, location, options, messageFunctions ) {
309
309
  anno, elemref: parent, '#': 'wrongref',
310
310
  });
311
311
  }
312
+ const [ head, ...tail ] = xpr;
313
+ if ((head.id || head) === '$self')
314
+ xpr = tail;
312
315
  parentparent[parentprop] = { $Path: xpr.map(ps => ps.id || ps).join('/') };
313
316
  };
314
317
  //----------------------------------