@dxos/echo-protocol 0.8.3 → 0.8.4-main.1c7ec43d41

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.
@@ -8,7 +8,7 @@ import { visitValues } from '@dxos/util';
8
8
 
9
9
  import { type RawString } from './automerge';
10
10
  import type { ForeignKey } from './foreign-key';
11
- import { isEncodedReference, type EncodedReference } from './reference';
11
+ import { type EncodedReference, isEncodedReference } from './reference';
12
12
  import { type SpaceDocVersion } from './space-doc-version';
13
13
 
14
14
  export type SpaceState = {
@@ -137,6 +137,10 @@ export const ObjectStructure = Object.freeze({
137
137
  return object.system?.target;
138
138
  },
139
139
 
140
+ getParent: (object: ObjectStructure): EncodedReference | undefined => {
141
+ return object.system?.parent;
142
+ },
143
+
140
144
  /**
141
145
  * @returns All references in the data section of the object.
142
146
  */
@@ -153,6 +157,10 @@ export const ObjectStructure = Object.freeze({
153
157
  return references;
154
158
  },
155
159
 
160
+ getTags: (object: ObjectStructure): string[] => {
161
+ return object.meta.tags ?? [];
162
+ },
163
+
156
164
  makeObject: ({
157
165
  type,
158
166
  data,
@@ -214,6 +222,14 @@ export type ObjectMeta = {
214
222
  * Foreign keys.
215
223
  */
216
224
  keys: ForeignKey[];
225
+
226
+ /**
227
+ * Tags.
228
+ * An array of DXNs of Tag objects within the space.
229
+ *
230
+ * NOTE: Optional for backwards compatibilty.
231
+ */
232
+ tags?: string[];
217
233
  };
218
234
 
219
235
  /**
@@ -236,13 +252,19 @@ export type ObjectSystem = {
236
252
  */
237
253
  deleted?: boolean;
238
254
 
255
+ /**
256
+ * Object parent.
257
+ * Objects with no parent are at the top level of the object hierarchy in the space.
258
+ */
259
+ parent?: EncodedReference;
260
+
239
261
  /**
240
262
  * Only for relations.
241
263
  */
242
264
  source?: EncodedReference;
243
265
 
244
266
  /**
245
- * Only for relations.w
267
+ * Only for relations.
246
268
  */
247
269
  target?: EncodedReference;
248
270
  };
@@ -0,0 +1,67 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { FeedProtocol } from '@dxos/protocols';
6
+
7
+ import type { ForeignKey } from './foreign-key';
8
+
9
+ /** Property name for meta when object is serialized to JSON. Matches @dxos/echo/internal ATTR_META. */
10
+ const ATTR_META = '@meta';
11
+
12
+ /**
13
+ * Codec for ECHO objects in feed block payload: JSON object ↔ UTF-8 bytes.
14
+ * Encodes with queue position stripped; decodes with optional position injection.
15
+ */
16
+ export class EchoFeedCodec {
17
+ static readonly #encoder = new TextEncoder();
18
+ static readonly #decoder = new TextDecoder();
19
+
20
+ /**
21
+ * Prepares a value for feed storage (strips queue position from metadata) and encodes to bytes.
22
+ */
23
+ static encode(value: Record<string, unknown>): Uint8Array {
24
+ const prepared = EchoFeedCodec.#stripQueuePosition(value);
25
+ return EchoFeedCodec.#encoder.encode(JSON.stringify(prepared));
26
+ }
27
+
28
+ /**
29
+ * Decodes feed block bytes to a JSON value.
30
+ * If position is provided, injects queue position into the decoded object's metadata.
31
+ */
32
+ static decode(data: Uint8Array, position?: number): Record<string, unknown> {
33
+ const decoded = JSON.parse(EchoFeedCodec.#decoder.decode(data));
34
+ if (position !== undefined && typeof decoded === 'object' && decoded !== null) {
35
+ EchoFeedCodec.#setQueuePosition(decoded, position);
36
+ }
37
+ return decoded;
38
+ }
39
+
40
+ static #stripQueuePosition(value: Record<string, unknown>): Record<string, unknown> {
41
+ if (typeof value !== 'object' || value === null) {
42
+ return value;
43
+ }
44
+ const obj = structuredClone(value);
45
+ const meta = obj[ATTR_META] as { keys?: ForeignKey[] } | undefined;
46
+ if (meta?.keys?.some((key: ForeignKey) => key.source === FeedProtocol.KEY_QUEUE_POSITION)) {
47
+ meta.keys = meta.keys.filter((key: ForeignKey) => key.source !== FeedProtocol.KEY_QUEUE_POSITION);
48
+ }
49
+ return obj;
50
+ }
51
+
52
+ static #setQueuePosition(obj: Record<string, any>, position: number): void {
53
+ obj[ATTR_META] ??= { keys: [] };
54
+ obj[ATTR_META]!.keys ??= [];
55
+ const keys = obj[ATTR_META]!.keys!;
56
+ for (let i = 0; i < keys.length; i++) {
57
+ if (keys[i].source === FeedProtocol.KEY_QUEUE_POSITION) {
58
+ keys.splice(i, 1);
59
+ i--;
60
+ }
61
+ }
62
+ keys.push({
63
+ source: FeedProtocol.KEY_QUEUE_POSITION,
64
+ id: position.toString(),
65
+ });
66
+ }
67
+ }
@@ -2,7 +2,8 @@
2
2
  // Copyright 2025 DXOS.org
3
3
  //
4
4
 
5
- import { Schema, SchemaAST } from 'effect';
5
+ import * as Schema from 'effect/Schema';
6
+ import * as SchemaAST from 'effect/SchemaAST';
6
7
 
7
8
  const ForeignKey_ = Schema.Struct({
8
9
  /**
@@ -15,8 +16,8 @@ const ForeignKey_ = Schema.Struct({
15
16
  * Id within the foreign database.
16
17
  */
17
18
  // TODO(wittjosiah): This annotation is currently used to ensure id field shows up in forms.
18
- // TODO(dmaretskyi): `false` is not a valid value for the annotation.
19
- id: Schema.String.annotations({ [SchemaAST.IdentifierAnnotationId]: false }),
19
+ // TODO(dmaretskyi): `false` is not a valid value for the annotation. Use a different annotation.
20
+ id: Schema.String.annotations({ [SchemaAST.IdentifierAnnotationId]: 'false' }),
20
21
  });
21
22
 
22
23
  export type ForeignKey = Schema.Schema.Type<typeof ForeignKey_>;
package/src/index.ts CHANGED
@@ -2,10 +2,11 @@
2
2
  // Copyright 2023 DXOS.org
3
3
  //
4
4
 
5
+ export type * from './collection-sync';
5
6
  export * from './document-structure';
7
+ export * from './echo-feed-codec';
8
+ export * from './foreign-key';
9
+ export * from './query';
6
10
  export * from './reference';
7
11
  export * from './space-doc-version';
8
- export * from './collection-sync';
9
12
  export * from './space-id';
10
- export * from './foreign-key';
11
- export * from './query';
package/src/query/ast.ts CHANGED
@@ -2,14 +2,15 @@
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
8
  import { DXN, ObjectId } from '@dxos/keys';
8
9
 
9
10
  import { ForeignKey } from '../foreign-key';
10
11
 
11
12
  const TypenameSpecifier = Schema.Union(DXN.Schema, Schema.Null).annotations({
12
- description: 'DXN or null. Null means any type will match',
13
+ description: 'DXN or null; null matches any type',
13
14
  });
14
15
 
15
16
  // 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.
@@ -46,6 +47,9 @@ const FilterObject_ = Schema.Struct({
46
47
  export interface FilterObject extends Schema.Schema.Type<typeof FilterObject_> {}
47
48
  export const FilterObject: Schema.Schema<FilterObject> = FilterObject_;
48
49
 
50
+ /**
51
+ * Compare.
52
+ */
49
53
  const FilterCompare_ = Schema.Struct({
50
54
  type: Schema.Literal('compare'),
51
55
  operator: Schema.Literal('eq', 'neq', 'gt', 'gte', 'lt', 'lte'),
@@ -54,6 +58,9 @@ const FilterCompare_ = Schema.Struct({
54
58
  export interface FilterCompare extends Schema.Schema.Type<typeof FilterCompare_> {}
55
59
  export const FilterCompare: Schema.Schema<FilterCompare> = FilterCompare_;
56
60
 
61
+ /**
62
+ * In.
63
+ */
57
64
  const FilterIn_ = Schema.Struct({
58
65
  type: Schema.Literal('in'),
59
66
  values: Schema.Array(Schema.Any),
@@ -61,53 +68,137 @@ const FilterIn_ = Schema.Struct({
61
68
  export interface FilterIn extends Schema.Schema.Type<typeof FilterIn_> {}
62
69
  export const FilterIn: Schema.Schema<FilterIn> = FilterIn_;
63
70
 
71
+ /**
72
+ * Contains.
73
+ */
74
+ const FilterContains_ = Schema.Struct({
75
+ type: Schema.Literal('contains'),
76
+ value: Schema.Any,
77
+ });
78
+
79
+ export interface FilterContains extends Schema.Schema.Type<typeof FilterContains_> {}
80
+
81
+ /**
82
+ * Predicate for an array property to contain the provided value.
83
+ * Nested objects are matched using strict structural matching.
84
+ */
85
+ export const FilterContains: Schema.Schema<FilterContains> = FilterContains_;
86
+
87
+ /**
88
+ * Filters objects that have certain tag.
89
+ */
90
+ const FilterTag_ = Schema.Struct({
91
+ type: Schema.Literal('tag'),
92
+ tag: Schema.String, // TODO(burdon): Make OR-collection?
93
+ });
94
+
95
+ export interface FilterTag extends Schema.Schema.Type<typeof FilterTag_> {}
96
+ export const FilterTag: Schema.Schema<FilterTag> = FilterTag_;
97
+
98
+ /**
99
+ * Range.
100
+ */
64
101
  const FilterRange_ = Schema.Struct({
65
102
  type: Schema.Literal('range'),
66
103
  from: Schema.Any,
67
104
  to: Schema.Any,
68
105
  });
106
+
69
107
  export interface FilterRange extends Schema.Schema.Type<typeof FilterRange_> {}
70
108
  export const FilterRange: Schema.Schema<FilterRange> = FilterRange_;
71
109
 
110
+ /**
111
+ * Filter by system timestamp (createdAt / updatedAt).
112
+ * Timestamps are unix milliseconds stored in the object meta index.
113
+ */
114
+ const FilterTimestamp_ = Schema.Struct({
115
+ type: Schema.Literal('timestamp'),
116
+ field: Schema.Literal('createdAt', 'updatedAt'),
117
+ operator: Schema.Literal('gt', 'gte', 'lt', 'lte'),
118
+ value: Schema.Number,
119
+ });
120
+
121
+ export interface FilterTimestamp extends Schema.Schema.Type<typeof FilterTimestamp_> {}
122
+ export const FilterTimestamp: Schema.Schema<FilterTimestamp> = FilterTimestamp_;
123
+
124
+ /**
125
+ * Text search.
126
+ */
72
127
  const FilterTextSearch_ = Schema.Struct({
73
128
  type: Schema.Literal('text-search'),
74
129
  text: Schema.String,
75
130
  searchKind: Schema.optional(Schema.Literal('full-text', 'vector')),
76
131
  });
132
+
77
133
  export interface FilterTextSearch extends Schema.Schema.Type<typeof FilterTextSearch_> {}
78
134
  export const FilterTextSearch: Schema.Schema<FilterTextSearch> = FilterTextSearch_;
79
135
 
136
+ /**
137
+ * Not.
138
+ */
80
139
  const FilterNot_ = Schema.Struct({
81
140
  type: Schema.Literal('not'),
82
141
  filter: Schema.suspend(() => Filter),
83
142
  });
143
+
84
144
  export interface FilterNot extends Schema.Schema.Type<typeof FilterNot_> {}
85
145
  export const FilterNot: Schema.Schema<FilterNot> = FilterNot_;
86
146
 
147
+ /**
148
+ * And.
149
+ */
87
150
  const FilterAnd_ = Schema.Struct({
88
151
  type: Schema.Literal('and'),
89
152
  filters: Schema.Array(Schema.suspend(() => Filter)),
90
153
  });
154
+
91
155
  export interface FilterAnd extends Schema.Schema.Type<typeof FilterAnd_> {}
92
156
  export const FilterAnd: Schema.Schema<FilterAnd> = FilterAnd_;
93
157
 
158
+ /**
159
+ * Or.
160
+ */
94
161
  const FilterOr_ = Schema.Struct({
95
162
  type: Schema.Literal('or'),
96
163
  filters: Schema.Array(Schema.suspend(() => Filter)),
97
164
  });
165
+
98
166
  export interface FilterOr extends Schema.Schema.Type<typeof FilterOr_> {}
99
167
  export const FilterOr: Schema.Schema<FilterOr> = FilterOr_;
100
168
 
169
+ /**
170
+ * Filter objects that are children of the specified parents.
171
+ * With transitive=true (default), matches grandchildren and beyond.
172
+ */
173
+ const FilterChildOf_ = Schema.Struct({
174
+ type: Schema.Literal('child-of'),
175
+ /** Parent DXNs to match children of. */
176
+ parents: Schema.Array(DXN.Schema),
177
+ /** Whether to match transitively (grandchildren, etc.). Defaults to true. */
178
+ transitive: Schema.Boolean,
179
+ });
180
+
181
+ export interface FilterChildOf extends Schema.Schema.Type<typeof FilterChildOf_> {}
182
+ export const FilterChildOf: Schema.Schema<FilterChildOf> = FilterChildOf_;
183
+
184
+ /**
185
+ * Union of filters.
186
+ */
101
187
  export const Filter = Schema.Union(
102
188
  FilterObject,
103
- FilterTextSearch,
104
189
  FilterCompare,
105
190
  FilterIn,
191
+ FilterContains,
192
+ FilterTag,
106
193
  FilterRange,
194
+ FilterTimestamp,
195
+ FilterTextSearch,
196
+ FilterChildOf,
107
197
  FilterNot,
108
198
  FilterAnd,
109
199
  FilterOr,
110
- );
200
+ ).annotations({ identifier: 'org.dxos.schema.filter' });
201
+
111
202
  export type Filter = Schema.Schema.Type<typeof Filter>;
112
203
 
113
204
  /**
@@ -117,6 +208,7 @@ const QuerySelectClause_ = Schema.Struct({
117
208
  type: Schema.Literal('select'),
118
209
  filter: Schema.suspend(() => Filter),
119
210
  });
211
+
120
212
  export interface QuerySelectClause extends Schema.Schema.Type<typeof QuerySelectClause_> {}
121
213
  export const QuerySelectClause: Schema.Schema<QuerySelectClause> = QuerySelectClause_;
122
214
 
@@ -128,6 +220,7 @@ const QueryFilterClause_ = Schema.Struct({
128
220
  selection: Schema.suspend(() => Query),
129
221
  filter: Schema.suspend(() => Filter),
130
222
  });
223
+
131
224
  export interface QueryFilterClause extends Schema.Schema.Type<typeof QueryFilterClause_> {}
132
225
  export const QueryFilterClause: Schema.Schema<QueryFilterClause> = QueryFilterClause_;
133
226
 
@@ -139,6 +232,7 @@ const QueryReferenceTraversalClause_ = Schema.Struct({
139
232
  anchor: Schema.suspend(() => Query),
140
233
  property: Schema.String, // TODO(dmaretskyi): Change to EscapedPropPath.
141
234
  });
235
+
142
236
  export interface QueryReferenceTraversalClause extends Schema.Schema.Type<typeof QueryReferenceTraversalClause_> {}
143
237
  export const QueryReferenceTraversalClause: Schema.Schema<QueryReferenceTraversalClause> =
144
238
  QueryReferenceTraversalClause_;
@@ -149,9 +243,14 @@ export const QueryReferenceTraversalClause: Schema.Schema<QueryReferenceTraversa
149
243
  const QueryIncomingReferencesClause_ = Schema.Struct({
150
244
  type: Schema.Literal('incoming-references'),
151
245
  anchor: Schema.suspend(() => Query),
152
- property: Schema.String,
246
+ /**
247
+ * Property path where the reference is located.
248
+ * If null, matches references from any property.
249
+ */
250
+ property: Schema.NullOr(Schema.String),
153
251
  typename: TypenameSpecifier,
154
252
  });
253
+
155
254
  export interface QueryIncomingReferencesClause extends Schema.Schema.Type<typeof QueryIncomingReferencesClause_> {}
156
255
  export const QueryIncomingReferencesClause: Schema.Schema<QueryIncomingReferencesClause> =
157
256
  QueryIncomingReferencesClause_;
@@ -170,6 +269,7 @@ const QueryRelationClause_ = Schema.Struct({
170
269
  direction: Schema.Literal('outgoing', 'incoming', 'both'),
171
270
  filter: Schema.optional(Schema.suspend(() => Filter)),
172
271
  });
272
+
173
273
  export interface QueryRelationClause extends Schema.Schema.Type<typeof QueryRelationClause_> {}
174
274
  export const QueryRelationClause: Schema.Schema<QueryRelationClause> = QueryRelationClause_;
175
275
 
@@ -181,9 +281,27 @@ const QueryRelationTraversalClause_ = Schema.Struct({
181
281
  anchor: Schema.suspend(() => Query),
182
282
  direction: Schema.Literal('source', 'target', 'both'),
183
283
  });
284
+
184
285
  export interface QueryRelationTraversalClause extends Schema.Schema.Type<typeof QueryRelationTraversalClause_> {}
185
286
  export const QueryRelationTraversalClause: Schema.Schema<QueryRelationTraversalClause> = QueryRelationTraversalClause_;
186
287
 
288
+ /**
289
+ * Traverse parent-child hierarchy.
290
+ */
291
+ const QueryHierarchyTraversalClause_ = Schema.Struct({
292
+ type: Schema.Literal('hierarchy-traversal'),
293
+ anchor: Schema.suspend(() => Query),
294
+ /**
295
+ * to-parent: traverse from child to parent.
296
+ * to-children: traverse from parent to children.
297
+ */
298
+ direction: Schema.Literal('to-parent', 'to-children'),
299
+ });
300
+
301
+ export interface QueryHierarchyTraversalClause extends Schema.Schema.Type<typeof QueryHierarchyTraversalClause_> {}
302
+ export const QueryHierarchyTraversalClause: Schema.Schema<QueryHierarchyTraversalClause> =
303
+ QueryHierarchyTraversalClause_;
304
+
187
305
  /**
188
306
  * Union of multiple queries.
189
307
  */
@@ -191,6 +309,7 @@ const QueryUnionClause_ = Schema.Struct({
191
309
  type: Schema.Literal('union'),
192
310
  queries: Schema.Array(Schema.suspend(() => Query)),
193
311
  });
312
+
194
313
  export interface QueryUnionClause extends Schema.Schema.Type<typeof QueryUnionClause_> {}
195
314
  export const QueryUnionClause: Schema.Schema<QueryUnionClause> = QueryUnionClause_;
196
315
 
@@ -202,9 +321,47 @@ const QuerySetDifferenceClause_ = Schema.Struct({
202
321
  source: Schema.suspend(() => Query),
203
322
  exclude: Schema.suspend(() => Query),
204
323
  });
324
+
205
325
  export interface QuerySetDifferenceClause extends Schema.Schema.Type<typeof QuerySetDifferenceClause_> {}
206
326
  export const QuerySetDifferenceClause: Schema.Schema<QuerySetDifferenceClause> = QuerySetDifferenceClause_;
207
327
 
328
+ export const OrderDirection = Schema.Literal('asc', 'desc');
329
+ export type OrderDirection = Schema.Schema.Type<typeof OrderDirection>;
330
+
331
+ const Order_ = Schema.Union(
332
+ Schema.Struct({
333
+ // How database wants to order them (in practice - by id).
334
+ kind: Schema.Literal('natural'),
335
+ }),
336
+ Schema.Struct({
337
+ kind: Schema.Literal('property'),
338
+ property: Schema.String,
339
+ direction: OrderDirection,
340
+ }),
341
+ Schema.Struct({
342
+ // Order by relevance rank (for FTS/vector search results).
343
+ // Default direction is 'desc' (higher rank = better match first).
344
+ kind: Schema.Literal('rank'),
345
+ direction: OrderDirection,
346
+ }),
347
+ );
348
+
349
+ export type Order = Schema.Schema.Type<typeof Order_>;
350
+ export const Order: Schema.Schema<Order> = Order_;
351
+
352
+ /**
353
+ * Order the query results.
354
+ * Left-to-right the orders dominate.
355
+ */
356
+ const QueryOrderClause_ = Schema.Struct({
357
+ type: Schema.Literal('order'),
358
+ query: Schema.suspend(() => Query),
359
+ order: Schema.Array(Order),
360
+ });
361
+
362
+ export interface QueryOrderClause extends Schema.Schema.Type<typeof QueryOrderClause_> {}
363
+ export const QueryOrderClause: Schema.Schema<QueryOrderClause> = QueryOrderClause_;
364
+
208
365
  /**
209
366
  * Add options to a query.
210
367
  */
@@ -213,9 +370,37 @@ const QueryOptionsClause_ = Schema.Struct({
213
370
  query: Schema.suspend(() => Query),
214
371
  options: Schema.suspend(() => QueryOptions),
215
372
  });
373
+
216
374
  export interface QueryOptionsClause extends Schema.Schema.Type<typeof QueryOptionsClause_> {}
217
375
  export const QueryOptionsClause: Schema.Schema<QueryOptionsClause> = QueryOptionsClause_;
218
376
 
377
+ /**
378
+ * Limit the number of results.
379
+ */
380
+ const QueryLimitClause_ = Schema.Struct({
381
+ type: Schema.Literal('limit'),
382
+ query: Schema.suspend(() => Query),
383
+ limit: Schema.Number,
384
+ });
385
+
386
+ export interface QueryLimitClause extends Schema.Schema.Type<typeof QueryLimitClause_> {}
387
+ export const QueryLimitClause: Schema.Schema<QueryLimitClause> = QueryLimitClause_;
388
+
389
+ export const QueryFromClause_ = Schema.Struct({
390
+ type: Schema.Literal('from'),
391
+ query: Schema.suspend(() => Query),
392
+ from: Schema.Union(
393
+ Schema.TaggedStruct('scope', {
394
+ scope: Schema.suspend(() => Scope),
395
+ }),
396
+ Schema.TaggedStruct('query', {
397
+ query: Schema.suspend(() => Query),
398
+ }),
399
+ ),
400
+ });
401
+ export interface QueryFromClause extends Schema.Schema.Type<typeof QueryFromClause_> {}
402
+ export const QueryFromClause: Schema.Schema<QueryFromClause> = QueryFromClause_;
403
+
219
404
  const Query_ = Schema.Union(
220
405
  QuerySelectClause,
221
406
  QueryFilterClause,
@@ -223,46 +408,142 @@ const Query_ = Schema.Union(
223
408
  QueryIncomingReferencesClause,
224
409
  QueryRelationClause,
225
410
  QueryRelationTraversalClause,
411
+ QueryHierarchyTraversalClause,
226
412
  QueryUnionClause,
227
413
  QuerySetDifferenceClause,
414
+ QueryOrderClause,
228
415
  QueryOptionsClause,
229
- );
416
+ QueryLimitClause,
417
+ QueryFromClause,
418
+ ).annotations({ identifier: 'org.dxos.schema.query' });
230
419
 
231
420
  export type Query = Schema.Schema.Type<typeof Query_>;
232
421
  export const Query: Schema.Schema<Query> = Query_;
233
422
 
234
423
  export const QueryOptions = Schema.Struct({
235
- spaceIds: Schema.optional(Schema.Array(Schema.String)),
424
+ /**
425
+ * Nested select statements will use this option to filter deleted objects.
426
+ */
236
427
  deleted: Schema.optional(Schema.Literal('include', 'exclude', 'only')),
428
+
429
+ /**
430
+ * Diagnostics-only label for logs / tooling (not used by execution semantics).
431
+ */
432
+ debugLabel: Schema.optional(Schema.String),
237
433
  });
434
+
238
435
  export interface QueryOptions extends Schema.Schema.Type<typeof QueryOptions> {}
239
436
 
437
+ /**
438
+ * Specifies the scope of the data to query from.
439
+ */
440
+ export const Scope = Schema.Struct({
441
+ /**
442
+ * The nested select statemets will select from the given spaces.
443
+ *
444
+ * NOTE: Spaces and queues are unioned together if both are specified.
445
+ */
446
+ spaceIds: Schema.optional(Schema.Array(Schema.String)),
447
+
448
+ /**
449
+ * If true, the nested select statements will select from all queues in the spaces specified by `spaceIds`.
450
+ */
451
+ allQueuesFromSpaces: Schema.optional(Schema.Boolean),
452
+
453
+ /**
454
+ * The nested select statemets will select from the given queues.
455
+ *
456
+ * NOTE: Spaces and queues are unioned together if both are specified.
457
+ */
458
+ queues: Schema.optional(Schema.Array(DXN.Schema)),
459
+ });
460
+ export interface Scope extends Schema.Schema.Type<typeof Scope> {}
461
+
240
462
  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
- }
463
+ visitor(query);
464
+
465
+ Match.value(query).pipe(
466
+ Match.when({ type: 'filter' }, ({ selection }) => visit(selection, visitor)),
467
+ Match.when({ type: 'reference-traversal' }, ({ anchor }) => visit(anchor, visitor)),
468
+ Match.when({ type: 'incoming-references' }, ({ anchor }) => visit(anchor, visitor)),
469
+ Match.when({ type: 'relation' }, ({ anchor }) => visit(anchor, visitor)),
470
+ Match.when({ type: 'options' }, ({ query }) => visit(query, visitor)),
471
+ Match.when({ type: 'relation-traversal' }, ({ anchor }) => visit(anchor, visitor)),
472
+ Match.when({ type: 'hierarchy-traversal' }, ({ anchor }) => visit(anchor, visitor)),
473
+ Match.when({ type: 'union' }, ({ queries }) => queries.forEach((q) => visit(q, visitor))),
474
+ Match.when({ type: 'set-difference' }, ({ source, exclude }) => {
475
+ visit(source, visitor);
476
+ visit(exclude, visitor);
477
+ }),
478
+ Match.when({ type: 'order' }, ({ query }) => visit(query, visitor)),
479
+ Match.when({ type: 'limit' }, ({ query }) => visit(query, visitor)),
480
+ Match.when({ type: 'from' }, (node) => {
481
+ visit(node.query, visitor);
482
+ if (node.from._tag === 'query') {
483
+ visit(node.from.query, visitor);
484
+ }
485
+ }),
486
+ Match.when({ type: 'select' }, () => {}),
487
+ Match.exhaustive,
488
+ );
489
+ };
490
+
491
+ /**
492
+ * Recursively transforms a query tree bottom-up.
493
+ * The mapper receives each node with its children already transformed.
494
+ */
495
+ export const map = (query: Query, mapper: (node: Query) => Query): Query => {
496
+ const mapped: Query = Match.value(query).pipe(
497
+ Match.when({ type: 'filter' }, (node) => ({ ...node, selection: map(node.selection, mapper) })),
498
+ Match.when({ type: 'reference-traversal' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
499
+ Match.when({ type: 'incoming-references' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
500
+ Match.when({ type: 'relation' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
501
+ Match.when({ type: 'relation-traversal' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
502
+ Match.when({ type: 'hierarchy-traversal' }, (node) => ({ ...node, anchor: map(node.anchor, mapper) })),
503
+ Match.when({ type: 'options' }, (node) => ({ ...node, query: map(node.query, mapper) })),
504
+ Match.when({ type: 'order' }, (node) => ({ ...node, query: map(node.query, mapper) })),
505
+ Match.when({ type: 'limit' }, (node) => ({ ...node, query: map(node.query, mapper) })),
506
+ Match.when({ type: 'from' }, (node) => ({
507
+ ...node,
508
+ query: map(node.query, mapper),
509
+ ...(node.from._tag === 'query' ? { from: { _tag: 'query' as const, query: map(node.from.query, mapper) } } : {}),
510
+ })),
511
+ Match.when({ type: 'union' }, (node) => ({ ...node, queries: node.queries.map((q) => map(q, mapper)) })),
512
+ Match.when({ type: 'set-difference' }, (node) => ({
513
+ ...node,
514
+ source: map(node.source, mapper),
515
+ exclude: map(node.exclude, mapper),
516
+ })),
517
+ Match.when({ type: 'select' }, (node) => node),
518
+ Match.exhaustive,
519
+ );
520
+ return mapper(mapped);
521
+ };
522
+
523
+ export const fold = <T>(query: Query, reducer: (node: Query) => T): T[] => {
524
+ return Match.value(query).pipe(
525
+ Match.withReturnType<T[]>(),
526
+ Match.when({ type: 'filter' }, ({ selection }) => fold(selection, reducer)),
527
+ Match.when({ type: 'reference-traversal' }, ({ anchor }) => fold(anchor, reducer)),
528
+ Match.when({ type: 'incoming-references' }, ({ anchor }) => fold(anchor, reducer)),
529
+ Match.when({ type: 'relation' }, ({ anchor }) => fold(anchor, reducer)),
530
+ Match.when({ type: 'options' }, ({ query }) => fold(query, reducer)),
531
+ Match.when({ type: 'relation-traversal' }, ({ anchor }) => fold(anchor, reducer)),
532
+ Match.when({ type: 'hierarchy-traversal' }, ({ anchor }) => fold(anchor, reducer)),
533
+ Match.when({ type: 'union' }, ({ queries }) => queries.flatMap((q) => fold(q, reducer))),
534
+ Match.when({ type: 'set-difference' }, ({ source, exclude }) =>
535
+ fold(source, reducer).concat(fold(exclude, reducer)),
536
+ ),
537
+ Match.when({ type: 'order' }, ({ query }) => fold(query, reducer)),
538
+ Match.when({ type: 'limit' }, ({ query }) => fold(query, reducer)),
539
+ Match.when({ type: 'from' }, (node) => {
540
+ const results = fold(node.query, reducer);
541
+ if (node.from._tag === 'query') {
542
+ return results.concat(fold(node.from.query, reducer));
543
+ }
544
+ return results;
545
+ }),
546
+ Match.when({ type: 'select' }, () => []),
547
+ Match.exhaustive,
548
+ );
268
549
  };