@dxos/echo-protocol 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8

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 (35) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +1 -1
  3. package/dist/lib/neutral/index.mjs +721 -0
  4. package/dist/lib/neutral/index.mjs.map +7 -0
  5. package/dist/lib/neutral/meta.json +1 -0
  6. package/dist/types/src/document-structure.d.ts +85 -30
  7. package/dist/types/src/document-structure.d.ts.map +1 -1
  8. package/dist/types/src/echo-feed-codec.d.ts +17 -0
  9. package/dist/types/src/echo-feed-codec.d.ts.map +1 -0
  10. package/dist/types/src/edge-peer.d.ts +11 -0
  11. package/dist/types/src/edge-peer.d.ts.map +1 -0
  12. package/dist/types/src/foreign-key.d.ts +1 -1
  13. package/dist/types/src/foreign-key.d.ts.map +1 -1
  14. package/dist/types/src/index.d.ts +5 -3
  15. package/dist/types/src/index.d.ts.map +1 -1
  16. package/dist/types/src/query/ast.d.ts +290 -28
  17. package/dist/types/src/query/ast.d.ts.map +1 -1
  18. package/dist/types/src/reference.d.ts +11 -64
  19. package/dist/types/src/reference.d.ts.map +1 -1
  20. package/dist/types/src/space-id.d.ts.map +1 -1
  21. package/dist/types/tsconfig.tsbuildinfo +1 -1
  22. package/package.json +14 -15
  23. package/src/document-structure.ts +106 -33
  24. package/src/echo-feed-codec.ts +67 -0
  25. package/src/edge-peer.ts +27 -0
  26. package/src/foreign-key.ts +4 -3
  27. package/src/index.ts +5 -3
  28. package/src/query/ast.ts +353 -38
  29. package/src/reference.ts +13 -158
  30. package/dist/lib/browser/index.mjs +0 -526
  31. package/dist/lib/browser/index.mjs.map +0 -7
  32. package/dist/lib/browser/meta.json +0 -1
  33. package/dist/lib/node-esm/index.mjs +0 -527
  34. package/dist/lib/node-esm/index.mjs.map +0 -7
  35. package/dist/lib/node-esm/meta.json +0 -1
package/src/query/ast.ts CHANGED
@@ -2,15 +2,17 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Schema } from 'effect';
5
+ import * as Match from 'effect/Match';
6
+ import * as Schema from 'effect/Schema';
6
7
 
7
- import { DXN, ObjectId } from '@dxos/keys';
8
+ import { EID, EntityId, URI } from '@dxos/keys';
8
9
 
9
10
  import { ForeignKey } from '../foreign-key';
10
11
 
11
- const TypenameSpecifier = Schema.Union(DXN.Schema, Schema.Null).annotations({
12
- description: 'DXN or null. Null means any type will match',
13
- });
12
+ // Type identifier URI — either a DXN (typename) or an EID (stored-schema-as-object).
13
+ // Matches the URI written into an object's `system.type` (see `getSchemaURI`). Null
14
+ // matches any type.
15
+ const TypenameSpecifier = Schema.Union(URI.Schema, Schema.Null);
14
16
 
15
17
  // NOTE: This pattern with 3 definitions per schema is need to make the types opaque, and circular references in AST to not cause compiler errors.
16
18
 
@@ -25,7 +27,7 @@ const FilterObject_ = Schema.Struct({
25
27
 
26
28
  typename: TypenameSpecifier,
27
29
 
28
- id: Schema.optional(Schema.Array(ObjectId)),
30
+ id: Schema.optional(Schema.Array(EntityId)),
29
31
 
30
32
  /**
31
33
  * Filter by property.
@@ -41,11 +43,25 @@ const FilterObject_ = Schema.Struct({
41
43
  */
42
44
  foreignKeys: Schema.optional(Schema.Array(ForeignKey)),
43
45
 
46
+ /**
47
+ * Match objects whose meta `key` equals this fully-qualified registry key (FQN format).
48
+ */
49
+ metaKey: Schema.optional(Schema.String),
50
+
51
+ /**
52
+ * Semver range matched against the object's meta `version`.
53
+ * Only consulted when {@link metaKey} is set. Objects with no `version` do not satisfy a version-constrained filter.
54
+ */
55
+ metaVersion: Schema.optional(Schema.String),
56
+
44
57
  // NOTE: Make sure to update `FilterStep.isNoop` if you change this.
45
58
  });
46
59
  export interface FilterObject extends Schema.Schema.Type<typeof FilterObject_> {}
47
60
  export const FilterObject: Schema.Schema<FilterObject> = FilterObject_;
48
61
 
62
+ /**
63
+ * Compare.
64
+ */
49
65
  const FilterCompare_ = Schema.Struct({
50
66
  type: Schema.Literal('compare'),
51
67
  operator: Schema.Literal('eq', 'neq', 'gt', 'gte', 'lt', 'lte'),
@@ -54,6 +70,9 @@ const FilterCompare_ = Schema.Struct({
54
70
  export interface FilterCompare extends Schema.Schema.Type<typeof FilterCompare_> {}
55
71
  export const FilterCompare: Schema.Schema<FilterCompare> = FilterCompare_;
56
72
 
73
+ /**
74
+ * In.
75
+ */
57
76
  const FilterIn_ = Schema.Struct({
58
77
  type: Schema.Literal('in'),
59
78
  values: Schema.Array(Schema.Any),
@@ -61,53 +80,137 @@ const FilterIn_ = Schema.Struct({
61
80
  export interface FilterIn extends Schema.Schema.Type<typeof FilterIn_> {}
62
81
  export const FilterIn: Schema.Schema<FilterIn> = FilterIn_;
63
82
 
83
+ /**
84
+ * Contains.
85
+ */
86
+ const FilterContains_ = Schema.Struct({
87
+ type: Schema.Literal('contains'),
88
+ value: Schema.Any,
89
+ });
90
+
91
+ export interface FilterContains extends Schema.Schema.Type<typeof FilterContains_> {}
92
+
93
+ /**
94
+ * Predicate for an array property to contain the provided value.
95
+ * Nested objects are matched using strict structural matching.
96
+ */
97
+ export const FilterContains: Schema.Schema<FilterContains> = FilterContains_;
98
+
99
+ /**
100
+ * Filters objects that have certain tag.
101
+ */
102
+ const FilterTag_ = Schema.Struct({
103
+ type: Schema.Literal('tag'),
104
+ tag: Schema.String, // TODO(burdon): Make OR-collection?
105
+ });
106
+
107
+ export interface FilterTag extends Schema.Schema.Type<typeof FilterTag_> {}
108
+ export const FilterTag: Schema.Schema<FilterTag> = FilterTag_;
109
+
110
+ /**
111
+ * Range.
112
+ */
64
113
  const FilterRange_ = Schema.Struct({
65
114
  type: Schema.Literal('range'),
66
115
  from: Schema.Any,
67
116
  to: Schema.Any,
68
117
  });
118
+
69
119
  export interface FilterRange extends Schema.Schema.Type<typeof FilterRange_> {}
70
120
  export const FilterRange: Schema.Schema<FilterRange> = FilterRange_;
71
121
 
122
+ /**
123
+ * Filter by system timestamp (createdAt / updatedAt).
124
+ * Timestamps are unix milliseconds stored in the object meta index.
125
+ */
126
+ const FilterTimestamp_ = Schema.Struct({
127
+ type: Schema.Literal('timestamp'),
128
+ field: Schema.Literal('createdAt', 'updatedAt'),
129
+ operator: Schema.Literal('gt', 'gte', 'lt', 'lte'),
130
+ value: Schema.Number,
131
+ });
132
+
133
+ export interface FilterTimestamp extends Schema.Schema.Type<typeof FilterTimestamp_> {}
134
+ export const FilterTimestamp: Schema.Schema<FilterTimestamp> = FilterTimestamp_;
135
+
136
+ /**
137
+ * Text search.
138
+ */
72
139
  const FilterTextSearch_ = Schema.Struct({
73
140
  type: Schema.Literal('text-search'),
74
141
  text: Schema.String,
75
142
  searchKind: Schema.optional(Schema.Literal('full-text', 'vector')),
76
143
  });
144
+
77
145
  export interface FilterTextSearch extends Schema.Schema.Type<typeof FilterTextSearch_> {}
78
146
  export const FilterTextSearch: Schema.Schema<FilterTextSearch> = FilterTextSearch_;
79
147
 
148
+ /**
149
+ * Not.
150
+ */
80
151
  const FilterNot_ = Schema.Struct({
81
152
  type: Schema.Literal('not'),
82
153
  filter: Schema.suspend(() => Filter),
83
154
  });
155
+
84
156
  export interface FilterNot extends Schema.Schema.Type<typeof FilterNot_> {}
85
157
  export const FilterNot: Schema.Schema<FilterNot> = FilterNot_;
86
158
 
159
+ /**
160
+ * And.
161
+ */
87
162
  const FilterAnd_ = Schema.Struct({
88
163
  type: Schema.Literal('and'),
89
164
  filters: Schema.Array(Schema.suspend(() => Filter)),
90
165
  });
166
+
91
167
  export interface FilterAnd extends Schema.Schema.Type<typeof FilterAnd_> {}
92
168
  export const FilterAnd: Schema.Schema<FilterAnd> = FilterAnd_;
93
169
 
170
+ /**
171
+ * Or.
172
+ */
94
173
  const FilterOr_ = Schema.Struct({
95
174
  type: Schema.Literal('or'),
96
175
  filters: Schema.Array(Schema.suspend(() => Filter)),
97
176
  });
177
+
98
178
  export interface FilterOr extends Schema.Schema.Type<typeof FilterOr_> {}
99
179
  export const FilterOr: Schema.Schema<FilterOr> = FilterOr_;
100
180
 
181
+ /**
182
+ * Filter objects that are children of the specified parents.
183
+ * With transitive=true (default), matches grandchildren and beyond.
184
+ */
185
+ const FilterChildOf_ = Schema.Struct({
186
+ type: Schema.Literal('child-of'),
187
+ /** Parent DXNs to match children of. */
188
+ parents: Schema.Array(EID.Schema),
189
+ /** Whether to match transitively (grandchildren, etc.). Defaults to true. */
190
+ transitive: Schema.Boolean,
191
+ });
192
+
193
+ export interface FilterChildOf extends Schema.Schema.Type<typeof FilterChildOf_> {}
194
+ export const FilterChildOf: Schema.Schema<FilterChildOf> = FilterChildOf_;
195
+
196
+ /**
197
+ * Union of filters.
198
+ */
101
199
  export const Filter = Schema.Union(
102
200
  FilterObject,
103
- FilterTextSearch,
104
201
  FilterCompare,
105
202
  FilterIn,
203
+ FilterContains,
204
+ FilterTag,
106
205
  FilterRange,
206
+ FilterTimestamp,
207
+ FilterTextSearch,
208
+ FilterChildOf,
107
209
  FilterNot,
108
210
  FilterAnd,
109
211
  FilterOr,
110
- );
212
+ ).annotations({ identifier: 'org.dxos.schema.filter' });
213
+
111
214
  export type Filter = Schema.Schema.Type<typeof Filter>;
112
215
 
113
216
  /**
@@ -117,6 +220,7 @@ const QuerySelectClause_ = Schema.Struct({
117
220
  type: Schema.Literal('select'),
118
221
  filter: Schema.suspend(() => Filter),
119
222
  });
223
+
120
224
  export interface QuerySelectClause extends Schema.Schema.Type<typeof QuerySelectClause_> {}
121
225
  export const QuerySelectClause: Schema.Schema<QuerySelectClause> = QuerySelectClause_;
122
226
 
@@ -128,6 +232,7 @@ const QueryFilterClause_ = Schema.Struct({
128
232
  selection: Schema.suspend(() => Query),
129
233
  filter: Schema.suspend(() => Filter),
130
234
  });
235
+
131
236
  export interface QueryFilterClause extends Schema.Schema.Type<typeof QueryFilterClause_> {}
132
237
  export const QueryFilterClause: Schema.Schema<QueryFilterClause> = QueryFilterClause_;
133
238
 
@@ -139,6 +244,7 @@ const QueryReferenceTraversalClause_ = Schema.Struct({
139
244
  anchor: Schema.suspend(() => Query),
140
245
  property: Schema.String, // TODO(dmaretskyi): Change to EscapedPropPath.
141
246
  });
247
+
142
248
  export interface QueryReferenceTraversalClause extends Schema.Schema.Type<typeof QueryReferenceTraversalClause_> {}
143
249
  export const QueryReferenceTraversalClause: Schema.Schema<QueryReferenceTraversalClause> =
144
250
  QueryReferenceTraversalClause_;
@@ -149,9 +255,14 @@ export const QueryReferenceTraversalClause: Schema.Schema<QueryReferenceTraversa
149
255
  const QueryIncomingReferencesClause_ = Schema.Struct({
150
256
  type: Schema.Literal('incoming-references'),
151
257
  anchor: Schema.suspend(() => Query),
152
- property: Schema.String,
258
+ /**
259
+ * Property path where the reference is located.
260
+ * If null, matches references from any property.
261
+ */
262
+ property: Schema.NullOr(Schema.String),
153
263
  typename: TypenameSpecifier,
154
264
  });
265
+
155
266
  export interface QueryIncomingReferencesClause extends Schema.Schema.Type<typeof QueryIncomingReferencesClause_> {}
156
267
  export const QueryIncomingReferencesClause: Schema.Schema<QueryIncomingReferencesClause> =
157
268
  QueryIncomingReferencesClause_;
@@ -170,6 +281,7 @@ const QueryRelationClause_ = Schema.Struct({
170
281
  direction: Schema.Literal('outgoing', 'incoming', 'both'),
171
282
  filter: Schema.optional(Schema.suspend(() => Filter)),
172
283
  });
284
+
173
285
  export interface QueryRelationClause extends Schema.Schema.Type<typeof QueryRelationClause_> {}
174
286
  export const QueryRelationClause: Schema.Schema<QueryRelationClause> = QueryRelationClause_;
175
287
 
@@ -181,9 +293,27 @@ const QueryRelationTraversalClause_ = Schema.Struct({
181
293
  anchor: Schema.suspend(() => Query),
182
294
  direction: Schema.Literal('source', 'target', 'both'),
183
295
  });
296
+
184
297
  export interface QueryRelationTraversalClause extends Schema.Schema.Type<typeof QueryRelationTraversalClause_> {}
185
298
  export const QueryRelationTraversalClause: Schema.Schema<QueryRelationTraversalClause> = QueryRelationTraversalClause_;
186
299
 
300
+ /**
301
+ * Traverse parent-child hierarchy.
302
+ */
303
+ const QueryHierarchyTraversalClause_ = Schema.Struct({
304
+ type: Schema.Literal('hierarchy-traversal'),
305
+ anchor: Schema.suspend(() => Query),
306
+ /**
307
+ * to-parent: traverse from child to parent.
308
+ * to-children: traverse from parent to children.
309
+ */
310
+ direction: Schema.Literal('to-parent', 'to-children'),
311
+ });
312
+
313
+ export interface QueryHierarchyTraversalClause extends Schema.Schema.Type<typeof QueryHierarchyTraversalClause_> {}
314
+ export const QueryHierarchyTraversalClause: Schema.Schema<QueryHierarchyTraversalClause> =
315
+ QueryHierarchyTraversalClause_;
316
+
187
317
  /**
188
318
  * Union of multiple queries.
189
319
  */
@@ -191,6 +321,7 @@ const QueryUnionClause_ = Schema.Struct({
191
321
  type: Schema.Literal('union'),
192
322
  queries: Schema.Array(Schema.suspend(() => Query)),
193
323
  });
324
+
194
325
  export interface QueryUnionClause extends Schema.Schema.Type<typeof QueryUnionClause_> {}
195
326
  export const QueryUnionClause: Schema.Schema<QueryUnionClause> = QueryUnionClause_;
196
327
 
@@ -202,9 +333,53 @@ const QuerySetDifferenceClause_ = Schema.Struct({
202
333
  source: Schema.suspend(() => Query),
203
334
  exclude: Schema.suspend(() => Query),
204
335
  });
336
+
205
337
  export interface QuerySetDifferenceClause extends Schema.Schema.Type<typeof QuerySetDifferenceClause_> {}
206
338
  export const QuerySetDifferenceClause: Schema.Schema<QuerySetDifferenceClause> = QuerySetDifferenceClause_;
207
339
 
340
+ export const OrderDirection = Schema.Literal('asc', 'desc');
341
+ export type OrderDirection = Schema.Schema.Type<typeof OrderDirection>;
342
+
343
+ const Order_ = Schema.Union(
344
+ Schema.Struct({
345
+ // How database wants to order them (in practice - by id).
346
+ kind: Schema.Literal('natural'),
347
+ }),
348
+ Schema.Struct({
349
+ kind: Schema.Literal('property'),
350
+ property: Schema.String,
351
+ direction: OrderDirection,
352
+ }),
353
+ Schema.Struct({
354
+ // Order by relevance rank (for FTS/vector search results).
355
+ // Default direction is 'desc' (higher rank = better match first).
356
+ kind: Schema.Literal('rank'),
357
+ direction: OrderDirection,
358
+ }),
359
+ Schema.Struct({
360
+ // Order by system timestamp (createdAt / updatedAt) from the object meta index.
361
+ kind: Schema.Literal('timestamp'),
362
+ field: Schema.Literal('createdAt', 'updatedAt'),
363
+ direction: OrderDirection,
364
+ }),
365
+ );
366
+
367
+ export type Order = Schema.Schema.Type<typeof Order_>;
368
+ export const Order: Schema.Schema<Order> = Order_;
369
+
370
+ /**
371
+ * Order the query results.
372
+ * Left-to-right the orders dominate.
373
+ */
374
+ const QueryOrderClause_ = Schema.Struct({
375
+ type: Schema.Literal('order'),
376
+ query: Schema.suspend(() => Query),
377
+ order: Schema.Array(Order),
378
+ });
379
+
380
+ export interface QueryOrderClause extends Schema.Schema.Type<typeof QueryOrderClause_> {}
381
+ export const QueryOrderClause: Schema.Schema<QueryOrderClause> = QueryOrderClause_;
382
+
208
383
  /**
209
384
  * Add options to a query.
210
385
  */
@@ -213,9 +388,37 @@ const QueryOptionsClause_ = Schema.Struct({
213
388
  query: Schema.suspend(() => Query),
214
389
  options: Schema.suspend(() => QueryOptions),
215
390
  });
391
+
216
392
  export interface QueryOptionsClause extends Schema.Schema.Type<typeof QueryOptionsClause_> {}
217
393
  export const QueryOptionsClause: Schema.Schema<QueryOptionsClause> = QueryOptionsClause_;
218
394
 
395
+ /**
396
+ * Limit the number of results.
397
+ */
398
+ const QueryLimitClause_ = Schema.Struct({
399
+ type: Schema.Literal('limit'),
400
+ query: Schema.suspend(() => Query),
401
+ limit: Schema.Number,
402
+ });
403
+
404
+ export interface QueryLimitClause extends Schema.Schema.Type<typeof QueryLimitClause_> {}
405
+ export const QueryLimitClause: Schema.Schema<QueryLimitClause> = QueryLimitClause_;
406
+
407
+ export const QueryFromClause_ = Schema.Struct({
408
+ type: Schema.Literal('from'),
409
+ query: Schema.suspend(() => Query),
410
+ from: Schema.Union(
411
+ Schema.TaggedStruct('scope', {
412
+ scopes: Schema.Array(Schema.suspend(() => Scope)),
413
+ }),
414
+ Schema.TaggedStruct('query', {
415
+ query: Schema.suspend(() => Query),
416
+ }),
417
+ ),
418
+ });
419
+ export interface QueryFromClause extends Schema.Schema.Type<typeof QueryFromClause_> {}
420
+ export const QueryFromClause: Schema.Schema<QueryFromClause> = QueryFromClause_;
421
+
219
422
  const Query_ = Schema.Union(
220
423
  QuerySelectClause,
221
424
  QueryFilterClause,
@@ -223,46 +426,158 @@ const Query_ = Schema.Union(
223
426
  QueryIncomingReferencesClause,
224
427
  QueryRelationClause,
225
428
  QueryRelationTraversalClause,
429
+ QueryHierarchyTraversalClause,
226
430
  QueryUnionClause,
227
431
  QuerySetDifferenceClause,
432
+ QueryOrderClause,
228
433
  QueryOptionsClause,
229
- );
434
+ QueryLimitClause,
435
+ QueryFromClause,
436
+ ).annotations({ identifier: 'org.dxos.schema.query' });
230
437
 
231
438
  export type Query = Schema.Schema.Type<typeof Query_>;
232
439
  export const Query: Schema.Schema<Query> = Query_;
233
440
 
234
441
  export const QueryOptions = Schema.Struct({
235
- spaceIds: Schema.optional(Schema.Array(Schema.String)),
442
+ /**
443
+ * Nested select statements will use this option to filter deleted objects.
444
+ */
236
445
  deleted: Schema.optional(Schema.Literal('include', 'exclude', 'only')),
446
+
447
+ /**
448
+ * Diagnostics-only label for logs / tooling (not used by execution semantics).
449
+ */
450
+ debugLabel: Schema.optional(Schema.String),
237
451
  });
452
+
238
453
  export interface QueryOptions extends Schema.Schema.Type<typeof QueryOptions> {}
239
454
 
455
+ /**
456
+ * Selects from a space (automerge documents).
457
+ * When `spaceId` is omitted, targets the owning space — i.e. the space of whichever
458
+ * database executes the query. This lets callers reference "this space" without
459
+ * having to look up its id.
460
+ * When `includeAllFeeds` is true, also selects from all feeds belonging to that space.
461
+ */
462
+ export const SpaceScope = Schema.TaggedStruct('space', {
463
+ spaceId: Schema.optional(Schema.String),
464
+ includeAllFeeds: Schema.optional(Schema.Boolean),
465
+ });
466
+ export interface SpaceScope extends Schema.Schema.Type<typeof SpaceScope> {}
467
+
468
+ /**
469
+ * Selects from a specific feed (by its underlying queue DXN).
470
+ */
471
+ export const FeedScope = Schema.TaggedStruct('feed', {
472
+ feedUri: Schema.String,
473
+ });
474
+ export interface FeedScope extends Schema.Schema.Type<typeof FeedScope> {}
475
+
476
+ /**
477
+ * Selects from a code-shipped object registry.
478
+ *
479
+ * - `'local'` — the in-process registry attached to the hypergraph.
480
+ * - `'remote'` — a future remote registry service (not yet implemented).
481
+ *
482
+ * To include both, add two separate `RegistryScope` entries to the `scopes` array.
483
+ */
484
+ export const RegistryScope = Schema.TaggedStruct('registry', {
485
+ location: Schema.Literal('local', 'remote'),
486
+ });
487
+ export interface RegistryScope extends Schema.Schema.Type<typeof RegistryScope> {}
488
+
489
+ /**
490
+ * Specifies the scope of the data to query from.
491
+ * A `from` clause may carry multiple scopes; results are unioned across them.
492
+ */
493
+ export const Scope = Schema.Union(SpaceScope, FeedScope, RegistryScope);
494
+ export type Scope = Schema.Schema.Type<typeof Scope>;
495
+
240
496
  export const visit = (query: Query, visitor: (node: Query) => void) => {
241
- switch (query.type) {
242
- case 'filter':
243
- visit(query.selection, visitor);
244
- break;
245
- case 'reference-traversal':
246
- visit(query.anchor, visitor);
247
- break;
248
- case 'incoming-references':
249
- visit(query.anchor, visitor);
250
- break;
251
- case 'relation':
252
- visit(query.anchor, visitor);
253
- break;
254
- case 'options':
255
- visit(query.query, visitor);
256
- break;
257
- case 'relation-traversal':
258
- visit(query.anchor, visitor);
259
- break;
260
- case 'union':
261
- query.queries.forEach((q) => visit(q, visitor));
262
- break;
263
- case 'set-difference':
264
- visit(query.source, visitor);
265
- visit(query.exclude, visitor);
266
- break;
267
- }
497
+ visitor(query);
498
+
499
+ Match.value(query).pipe(
500
+ Match.when({ type: 'filter' }, ({ selection }) => visit(selection, visitor)),
501
+ Match.when({ type: 'reference-traversal' }, ({ anchor }) => visit(anchor, visitor)),
502
+ Match.when({ type: 'incoming-references' }, ({ anchor }) => visit(anchor, visitor)),
503
+ Match.when({ type: 'relation' }, ({ anchor }) => visit(anchor, visitor)),
504
+ Match.when({ type: 'options' }, ({ query }) => visit(query, visitor)),
505
+ Match.when({ type: 'relation-traversal' }, ({ anchor }) => visit(anchor, visitor)),
506
+ Match.when({ type: 'hierarchy-traversal' }, ({ anchor }) => visit(anchor, visitor)),
507
+ Match.when({ type: 'union' }, ({ queries }) => queries.forEach((q) => visit(q, visitor))),
508
+ Match.when({ type: 'set-difference' }, ({ source, exclude }) => {
509
+ visit(source, visitor);
510
+ visit(exclude, visitor);
511
+ }),
512
+ Match.when({ type: 'order' }, ({ query }) => visit(query, visitor)),
513
+ Match.when({ type: 'limit' }, ({ query }) => visit(query, visitor)),
514
+ Match.when({ type: 'from' }, (node) => {
515
+ visit(node.query, visitor);
516
+ if (node.from._tag === 'query') {
517
+ visit(node.from.query, visitor);
518
+ }
519
+ }),
520
+ Match.when({ type: 'select' }, () => {}),
521
+ Match.exhaustive,
522
+ );
523
+ };
524
+
525
+ /**
526
+ * Recursively transforms a query tree bottom-up.
527
+ * The mapper receives each node with its children already transformed.
528
+ */
529
+ export const map = (query: Query, mapper: (node: Query) => Query): Query => {
530
+ const mapped: Query = Match.value(query).pipe(
531
+ Match.when({ type: 'filter' }, (node) => ({ ...node, selection: map(node.selection, mapper) })),
532
+ Match.when({ type: 'reference-traversal' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
533
+ Match.when({ type: 'incoming-references' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
534
+ Match.when({ type: 'relation' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
535
+ Match.when({ type: 'relation-traversal' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
536
+ Match.when({ type: 'hierarchy-traversal' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
537
+ Match.when({ type: 'options' }, (node) => ({ ...node, query: map(node.query, mapper) })),
538
+ Match.when({ type: 'order' }, (node) => ({ ...node, query: map(node.query, mapper) })),
539
+ Match.when({ type: 'limit' }, (node) => ({ ...node, query: map(node.query, mapper) })),
540
+ Match.when({ type: 'from' }, (node) => ({
541
+ ...node,
542
+ query: map(node.query, mapper),
543
+ ...(node.from._tag === 'query' ? { from: { _tag: 'query' as const, query: map(node.from.query, mapper) } } : {}),
544
+ })),
545
+ Match.when({ type: 'union' }, (node) => ({ ...node, queries: node.queries.map((q) => map(q, mapper)) })),
546
+ Match.when({ type: 'set-difference' }, (node) => ({
547
+ ...node,
548
+ source: map(node.source, mapper),
549
+ exclude: map(node.exclude, mapper),
550
+ })),
551
+ Match.when({ type: 'select' }, (node) => node),
552
+ Match.exhaustive,
553
+ );
554
+ return mapper(mapped);
555
+ };
556
+
557
+ export const fold = <T>(query: Query, reducer: (node: Query) => T): T[] => {
558
+ return Match.value(query).pipe(
559
+ Match.withReturnType<T[]>(),
560
+ Match.when({ type: 'filter' }, ({ selection }) => fold(selection, reducer)),
561
+ Match.when({ type: 'reference-traversal' }, ({ anchor }) => fold(anchor, reducer)),
562
+ Match.when({ type: 'incoming-references' }, ({ anchor }) => fold(anchor, reducer)),
563
+ Match.when({ type: 'relation' }, ({ anchor }) => fold(anchor, reducer)),
564
+ Match.when({ type: 'options' }, ({ query }) => fold(query, reducer)),
565
+ Match.when({ type: 'relation-traversal' }, ({ anchor }) => fold(anchor, reducer)),
566
+ Match.when({ type: 'hierarchy-traversal' }, ({ anchor }) => fold(anchor, reducer)),
567
+ Match.when({ type: 'union' }, ({ queries }) => queries.flatMap((q) => fold(q, reducer))),
568
+ Match.when({ type: 'set-difference' }, ({ source, exclude }) =>
569
+ fold(source, reducer).concat(fold(exclude, reducer)),
570
+ ),
571
+ Match.when({ type: 'order' }, ({ query }) => fold(query, reducer)),
572
+ Match.when({ type: 'limit' }, ({ query }) => fold(query, reducer)),
573
+ Match.when({ type: 'from' }, (node) => {
574
+ const results = fold(node.query, reducer);
575
+ if (node.from._tag === 'query') {
576
+ return results.concat(fold(node.from.query, reducer));
577
+ }
578
+ return results;
579
+ }),
580
+ Match.when({ type: 'select' }, () => []),
581
+ Match.exhaustive,
582
+ );
268
583
  };