@dxos/echo-query 0.8.4-main.40e3dcdf1b → 0.8.4-main.43cb759274

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.
@@ -4,10 +4,12 @@
4
4
 
5
5
  import type * as Schema from 'effect/Schema';
6
6
 
7
- import type { Filter as Filter$, Order as Order$, Query as Query$, Ref } from '@dxos/echo';
7
+ import type { Filter as Filter$, Obj as Obj$, Order as Order$, Query as Query$, Ref, Type as Type$ } from '@dxos/echo';
8
8
  import type { ForeignKey, QueryAST } from '@dxos/echo-protocol';
9
9
  import { assertArgument } from '@dxos/invariant';
10
- import type { DXN, ObjectId } from '@dxos/keys';
10
+ // `DXN`/`EID` are type-only imports to keep the `query-lite` bundle free of
11
+ // `effect/Schema` (which pulls runtime helpers QuickJS can't parse — e.g. private class fields).
12
+ import type { DXN, EID, EntityId, URI } from '@dxos/keys';
11
13
 
12
14
  //
13
15
  // Light-weight implementation of query execution.
@@ -83,9 +85,9 @@ class FilterClass implements Filter$.Any {
83
85
  });
84
86
  }
85
87
 
86
- static id(...ids: ObjectId[]): Filter$.Any {
88
+ static id(...ids: EntityId[]): Filter$.Any {
87
89
  // assertArgument(
88
- // ids.every((id) => ObjectId.isValid(id)),
90
+ // ids.every((id) => EntityId.isValid(id)),
89
91
  // 'ids',
90
92
  // 'ids must be valid',
91
93
  // );
@@ -102,10 +104,12 @@ class FilterClass implements Filter$.Any {
102
104
  });
103
105
  }
104
106
 
105
- static type<S extends Schema.Schema.All>(
106
- schema: S | string,
107
- props?: Filter$.Props<Schema.Schema.Type<S>>,
108
- ): Filter$.Filter<Schema.Schema.Type<S>> {
107
+ static type<T extends Type$.AnyEntity>(
108
+ type: T,
109
+ props?: Filter$.Props<Type$.InstanceType<T>>,
110
+ ): Filter$.Filter<Type$.InstanceType<T>>;
111
+ static type(schema: string, props?: Filter$.Props<unknown>): Filter$.Filter<any>;
112
+ static type(schema: Type$.AnyEntity | string, props?: Filter$.Props<unknown>): Filter$.Filter<unknown> {
109
113
  if (typeof schema !== 'string') {
110
114
  throw new TypeError('expected typename as the first paramter');
111
115
  }
@@ -124,10 +128,10 @@ class FilterClass implements Filter$.Any {
124
128
  });
125
129
  }
126
130
 
127
- static typeDXN(dxn: DXN): Filter$.Any {
131
+ static typeURI(uri: URI.URI): Filter$.Any {
128
132
  return new FilterClass({
129
133
  type: 'object',
130
- typename: dxn.toString(),
134
+ typename: uri,
131
135
  props: {},
132
136
  });
133
137
  }
@@ -139,6 +143,16 @@ class FilterClass implements Filter$.Any {
139
143
  });
140
144
  }
141
145
 
146
+ static key(key: string, options?: Filter$.KeyFilterOptions): Filter$.Any {
147
+ return new FilterClass({
148
+ type: 'object',
149
+ typename: null,
150
+ props: {},
151
+ metaKey: key,
152
+ metaVersion: options?.version,
153
+ });
154
+ }
155
+
142
156
  static props<T>(props: Filter$.Props<T>): Filter$.Filter<T> {
143
157
  return new FilterClass({
144
158
  type: 'object',
@@ -155,15 +169,15 @@ class FilterClass implements Filter$.Any {
155
169
  });
156
170
  }
157
171
 
158
- static foreignKeys<S extends Schema.Schema.All>(
159
- schema: S | string,
172
+ static foreignKeys<S extends Type$.AnyEntity | string>(
173
+ schema: S,
160
174
  keys: ForeignKey[],
161
- ): Filter$.Filter<Schema.Schema.Type<S>> {
175
+ ): Filter$.Filter<S extends Type$.AnyEntity ? Type$.InstanceType<S> : unknown> {
162
176
  assertArgument(typeof schema === 'string', 'schema');
163
177
  assertArgument(!schema.startsWith('dxn:'), 'schema');
164
178
  return new FilterClass({
165
179
  type: 'object',
166
- typename: `dxn:type:${schema}`,
180
+ typename: makeTypeDxn(schema),
167
181
  props: {},
168
182
  foreignKeys: keys,
169
183
  });
@@ -221,7 +235,7 @@ class FilterClass implements Filter$.Any {
221
235
  });
222
236
  }
223
237
 
224
- static in<T>(...values: T[]): Filter$.Filter<T | undefined> {
238
+ static in<T>(...values: T[]): Filter$.Filter<T> {
225
239
  return new FilterClass({
226
240
  type: 'in',
227
241
  values,
@@ -235,7 +249,7 @@ class FilterClass implements Filter$.Any {
235
249
  });
236
250
  }
237
251
 
238
- static between<T>(from: T, to: T): Filter$.Filter<unknown> {
252
+ static between<T>(from: T, to: T): Filter$.Filter<T> {
239
253
  return new FilterClass({
240
254
  type: 'range',
241
255
  from,
@@ -243,6 +257,47 @@ class FilterClass implements Filter$.Any {
243
257
  });
244
258
  }
245
259
 
260
+ static updated(range: { after?: Date | number; before?: Date | number }): Filter$.Any {
261
+ return FilterClass._timeRangeFilter('updatedAt', range);
262
+ }
263
+
264
+ static created(range: { after?: Date | number; before?: Date | number }): Filter$.Any {
265
+ return FilterClass._timeRangeFilter('createdAt', range);
266
+ }
267
+
268
+ static childOf(parents: unknown | unknown[], options?: { transitive?: boolean }): Filter$.Any {
269
+ const items = Array.isArray(parents) ? parents : [parents];
270
+ const dxns = items.map((item) => {
271
+ if (isEchoUriLike(item)) {
272
+ return item.toString() as EID.EID;
273
+ }
274
+ throw new TypeError('childOf requires EID values in query-lite');
275
+ });
276
+ return new FilterClass({
277
+ type: 'child-of',
278
+ parents: dxns,
279
+ transitive: options?.transitive ?? true,
280
+ });
281
+ }
282
+
283
+ private static _timeRangeFilter(
284
+ field: 'updatedAt' | 'createdAt',
285
+ range: { after?: Date | number; before?: Date | number },
286
+ ): Filter$.Any {
287
+ const toMs = (d: Date | number) => (typeof d === 'number' ? d : d.getTime());
288
+ const filters: Filter$.Any[] = [];
289
+ if (range.after != null) {
290
+ filters.push(new FilterClass({ type: 'timestamp', field, operator: 'gte', value: toMs(range.after) }));
291
+ }
292
+ if (range.before != null) {
293
+ filters.push(new FilterClass({ type: 'timestamp', field, operator: 'lte', value: toMs(range.before) }));
294
+ }
295
+ if (filters.length === 0) {
296
+ return FilterClass.everything();
297
+ }
298
+ return filters.length === 1 ? filters[0] : FilterClass.and(...filters);
299
+ }
300
+
246
301
  static not<F extends Filter$.Any>(filter: F): Filter$.Filter<Filter$.Type<F>> {
247
302
  return new FilterClass({
248
303
  type: 'not',
@@ -268,6 +323,11 @@ class FilterClass implements Filter$.Any {
268
323
  });
269
324
  }
270
325
 
326
+ /** Returns a human-readable string representation of a Filter AST. */
327
+ static pretty(filter: Filter$.Any): string {
328
+ return prettyFilter(filter.ast);
329
+ }
330
+
271
331
  private constructor(public readonly ast: QueryAST.Filter) {}
272
332
 
273
333
  '~Filter' = FilterClass.variance;
@@ -283,7 +343,7 @@ export { Filter1 as Filter };
283
343
  type RefPropKey<T> = keyof T & string;
284
344
 
285
345
  const propsFilterToAst = (predicates: Filter$.Props<any>): Pick<QueryAST.FilterObject, 'id' | 'props'> => {
286
- let idFilter: readonly ObjectId[] | undefined;
346
+ let idFilter: readonly EntityId[] | undefined;
287
347
  if ('id' in predicates) {
288
348
  assertArgument(
289
349
  typeof predicates.id === 'string' || Array.isArray(predicates.id),
@@ -345,7 +405,32 @@ class QueryClass implements Query$.Any {
345
405
  });
346
406
  }
347
407
 
348
- static type(schema: Schema.Schema.All | string, predicates?: Filter$.Props<unknown>): Query$.Any {
408
+ select(filter: Filter$.Any | Filter$.Props<any>): Query$.Any {
409
+ if (FilterClass.is(filter)) {
410
+ return new QueryClass({
411
+ type: 'filter',
412
+ selection: this.ast,
413
+ filter: filter.ast,
414
+ });
415
+ } else {
416
+ return new QueryClass({
417
+ type: 'filter',
418
+ selection: this.ast,
419
+ filter: FilterClass.props(filter).ast,
420
+ });
421
+ }
422
+ }
423
+
424
+ static type<S extends Schema.Schema.All>(
425
+ schema: S,
426
+ predicates?: Filter$.Props<Schema.Schema.Type<S>>,
427
+ ): Query$.Query<Schema.Schema.Type<S>>;
428
+ static type(type: Type$.Type, predicates?: Filter$.Props<Obj$.Unknown>): Query$.Query<Obj$.Unknown>;
429
+ static type(schema: string, predicates?: Filter$.Props<unknown>): Query$.Query<any>;
430
+ static type(schema: Schema.Schema.All | Type$.Type | string, predicates?: Filter$.Props<unknown>): Query$.Any {
431
+ if (typeof schema !== 'string') {
432
+ throw new TypeError('expected typename as the first paramter');
433
+ }
349
434
  return new QueryClass({
350
435
  type: 'select',
351
436
  filter: FilterClass.type(schema, predicates).ast,
@@ -372,35 +457,54 @@ class QueryClass implements Query$.Any {
372
457
  });
373
458
  }
374
459
 
375
- static from(source: any, options?: { includeFeeds?: boolean }): Query$.Any {
460
+ static from(...args: any[]): Query$.Any {
376
461
  const baseQuery: QueryAST.Query = {
377
462
  type: 'select',
378
463
  filter: FilterClass.everything().ast,
379
464
  };
380
465
  const wrapper = new QueryClass(baseQuery);
381
- return wrapper.from(source, options);
466
+ return (wrapper.from as (...args: any[]) => Query$.Any)(...args);
382
467
  }
383
468
 
384
- constructor(public readonly ast: QueryAST.Query) {}
385
-
386
- '~Query' = QueryClass.variance;
469
+ from(...args: any[]): Query$.Any {
470
+ // Variadic raw scopes: `.from(Scope.space(), Scope.registry())`.
471
+ if (args.length > 1 && args.every((arg) => _isScopeLike(arg))) {
472
+ return new QueryClass({
473
+ type: 'from',
474
+ query: this.ast,
475
+ from: { _tag: 'scope', scopes: args as QueryAST.Scope[] },
476
+ });
477
+ }
387
478
 
388
- select(filter: Filter$.Any | Filter$.Props<any>): Query$.Any {
389
- if (FilterClass.is(filter)) {
479
+ const [arg] = args;
480
+ if (arg === 'all-accessible-spaces') {
390
481
  return new QueryClass({
391
- type: 'filter',
392
- selection: this.ast,
393
- filter: filter.ast,
482
+ type: 'from',
483
+ query: this.ast,
484
+ from: { _tag: 'scope', scopes: [] },
394
485
  });
395
- } else {
486
+ }
487
+
488
+ if (_isScopeLike(arg)) {
396
489
  return new QueryClass({
397
- type: 'filter',
398
- selection: this.ast,
399
- filter: FilterClass.props(filter).ast,
490
+ type: 'from',
491
+ query: this.ast,
492
+ from: { _tag: 'scope', scopes: Array.isArray(arg) ? arg : [arg] },
400
493
  });
401
494
  }
495
+
496
+ throw new TypeError('Database and Feed objects are not supported in query-lite sandbox');
402
497
  }
403
498
 
499
+ /** Returns a human-readable string representation of a Query AST. */
500
+ static pretty(query: Query$.Any): string {
501
+ return prettyQuery(query.ast);
502
+ }
503
+
504
+ constructor(public readonly ast: QueryAST.Query) {}
505
+
506
+ '~Query' = QueryClass.variance;
507
+
404
508
  reference(key: string): Query$.Any {
405
509
  return new QueryClass({
406
510
  type: 'reference-traversal',
@@ -409,13 +513,11 @@ class QueryClass implements Query$.Any {
409
513
  });
410
514
  }
411
515
 
412
- referencedBy(target?: Schema.Schema.All | string, key?: string): Query$.Any {
413
- const typename =
414
- target !== undefined
415
- ? (assertArgument(typeof target === 'string', 'target'),
416
- assertArgument(!target.startsWith('dxn:'), 'target'),
417
- target)
418
- : null;
516
+ referencedBy(target?: Type$.AnyEntity | string, key?: string): Query$.Any {
517
+ if (target !== undefined && typeof target !== 'string') {
518
+ throw new TypeError('referencedBy requires a typename string in query-lite');
519
+ }
520
+ const typename = target !== undefined ? makeTypeDxn(target) : null;
419
521
  return new QueryClass({
420
522
  type: 'incoming-references',
421
523
  anchor: this.ast,
@@ -424,21 +526,37 @@ class QueryClass implements Query$.Any {
424
526
  });
425
527
  }
426
528
 
427
- sourceOf(relation?: Schema.Schema.All | string, predicates?: Filter$.Props<unknown> | undefined): Query$.Any {
529
+ sourceOf(
530
+ relation?: Type$.Relation<any, any, any, any> | string,
531
+ predicates?: Filter$.Props<unknown> | undefined,
532
+ ): Query$.Any {
428
533
  return new QueryClass({
429
534
  type: 'relation',
430
535
  anchor: this.ast,
431
536
  direction: 'outgoing',
432
- filter: relation !== undefined ? FilterClass.type(relation, predicates).ast : undefined,
537
+ filter:
538
+ relation === undefined
539
+ ? undefined
540
+ : typeof relation === 'string'
541
+ ? FilterClass.type(relation, predicates).ast
542
+ : FilterClass.type(relation, predicates).ast,
433
543
  });
434
544
  }
435
545
 
436
- targetOf(relation?: Schema.Schema.All | string, predicates?: Filter$.Props<unknown> | undefined): Query$.Any {
546
+ targetOf(
547
+ relation?: Type$.Relation<any, any, any, any> | string,
548
+ predicates?: Filter$.Props<unknown> | undefined,
549
+ ): Query$.Any {
437
550
  return new QueryClass({
438
551
  type: 'relation',
439
552
  anchor: this.ast,
440
553
  direction: 'incoming',
441
- filter: relation !== undefined ? FilterClass.type(relation, predicates).ast : undefined,
554
+ filter:
555
+ relation === undefined
556
+ ? undefined
557
+ : typeof relation === 'string'
558
+ ? FilterClass.type(relation, predicates).ast
559
+ : FilterClass.type(relation, predicates).ast,
442
560
  });
443
561
  }
444
562
 
@@ -490,36 +608,26 @@ class QueryClass implements Query$.Any {
490
608
  });
491
609
  }
492
610
 
493
- from(arg: any, options?: { includeFeeds?: boolean }): Query$.Any {
494
- if (arg === 'all-accessible-spaces') {
495
- return new QueryClass({
496
- type: 'from',
497
- query: this.ast,
498
- from: {
499
- _tag: 'scope',
500
- scope: {
501
- ...(options?.includeFeeds ? { allQueuesFromSpaces: true } : {}),
502
- },
503
- },
504
- });
505
- }
611
+ options(options: QueryAST.QueryOptions): Query$.Any {
612
+ return new QueryClass({
613
+ type: 'options',
614
+ query: this.ast,
615
+ options,
616
+ });
617
+ }
506
618
 
507
- if (_isScopeLike(arg)) {
619
+ debugLabel(label: string): Query$.Any {
620
+ if (this.ast.type === 'options') {
508
621
  return new QueryClass({
509
- type: 'from',
510
- query: this.ast,
511
- from: { _tag: 'scope', scope: arg },
622
+ type: 'options',
623
+ query: this.ast.query,
624
+ options: { ...this.ast.options, debugLabel: label },
512
625
  });
513
626
  }
514
-
515
- throw new TypeError('Database and Feed objects are not supported in query-lite sandbox');
516
- }
517
-
518
- options(options: QueryAST.QueryOptions): Query$.Any {
519
627
  return new QueryClass({
520
628
  type: 'options',
521
629
  query: this.ast,
522
- options,
630
+ options: { debugLabel: label },
523
631
  });
524
632
  }
525
633
  }
@@ -532,17 +640,178 @@ const isRef = (obj: any): obj is Ref.Ref<any> => {
532
640
  return obj && typeof obj === 'object' && RefTypeId in obj;
533
641
  };
534
642
 
535
- const makeTypeDxn = (typename: string) => {
643
+ const makeTypeDxn = (typename: string): DXN.DXN => {
536
644
  assertArgument(typeof typename === 'string', 'typename');
537
645
  assertArgument(!typename.startsWith('dxn:'), 'typename');
538
- return `dxn:type:${typename}`;
646
+ // Inline template (rather than `DXN.make`) to keep the value-side `@dxos/keys` import out of this bundle.
647
+ return `dxn:${typename}` as DXN.DXN;
648
+ };
649
+
650
+ const isDxnLike = (value: unknown): value is DXN.DXN => {
651
+ if (typeof value === 'string') {
652
+ return value.startsWith('dxn:');
653
+ }
654
+ return (
655
+ typeof value === 'object' &&
656
+ value !== null &&
657
+ 'toString' in value &&
658
+ typeof value.toString === 'function' &&
659
+ value.toString().startsWith('dxn:')
660
+ );
661
+ };
662
+
663
+ const isEchoUriLike = (value: unknown): value is EID.EID => {
664
+ if (typeof value === 'string') {
665
+ return value.startsWith('echo:') || value.startsWith('dxn:echo:') || value.startsWith('dxn:queue:');
666
+ }
667
+ return (
668
+ typeof value === 'object' &&
669
+ value !== null &&
670
+ 'toString' in value &&
671
+ typeof value.toString === 'function' &&
672
+ isEchoUriLike(value.toString())
673
+ );
539
674
  };
540
675
 
541
- const SCOPE_KEYS = new Set(['spaceIds', 'queues', 'allQueuesFromSpaces']);
676
+ const SCOPE_TAGS = new Set(['space', 'feed', 'registry']);
542
677
 
543
- const _isScopeLike = (value: unknown): value is QueryAST.Scope => {
544
- if (typeof value !== 'object' || value === null || Array.isArray(value)) {
545
- return false;
678
+ const _isScopeLike = (value: unknown): value is QueryAST.Scope | QueryAST.Scope[] => {
679
+ if (Array.isArray(value)) {
680
+ return value.every((item) => _isSingleScopeLike(item));
681
+ }
682
+ return _isSingleScopeLike(value);
683
+ };
684
+
685
+ const _isSingleScopeLike = (value: unknown): value is QueryAST.Scope => {
686
+ return (
687
+ typeof value === 'object' &&
688
+ value !== null &&
689
+ !Array.isArray(value) &&
690
+ '_tag' in value &&
691
+ typeof value._tag === 'string' &&
692
+ SCOPE_TAGS.has(value._tag)
693
+ );
694
+ };
695
+
696
+ const prettyFilter = (filter: QueryAST.Filter): string => {
697
+ switch (filter.type) {
698
+ case 'object': {
699
+ const parts: string[] = [];
700
+ if (filter.typename !== null) {
701
+ parts.push(JSON.stringify(filter.typename));
702
+ }
703
+ const propEntries = Object.entries(filter.props);
704
+ if (propEntries.length > 0) {
705
+ const propsStr = propEntries.map(([k, v]) => `${k}: ${prettyFilter(v)}`).join(', ');
706
+ parts.push(`{ ${propsStr} }`);
707
+ }
708
+ if (filter.id !== undefined) {
709
+ parts.push(`id: [${filter.id.join(', ')}]`);
710
+ }
711
+ return parts.length > 0 ? `Filter.type(${parts.join(', ')})` : 'Filter.everything()';
712
+ }
713
+ case 'compare':
714
+ return `Filter.${filter.operator}(${JSON.stringify(filter.value)})`;
715
+ case 'in':
716
+ return `Filter.in(${filter.values.map((v) => JSON.stringify(v)).join(', ')})`;
717
+ case 'contains':
718
+ return `Filter.contains(${JSON.stringify(filter.value)})`;
719
+ case 'range':
720
+ return `Filter.between(${JSON.stringify(filter.from)}, ${JSON.stringify(filter.to)})`;
721
+ case 'text-search':
722
+ return `Filter.text(${JSON.stringify(filter.text)})`;
723
+ case 'tag':
724
+ return `Filter.tag(${JSON.stringify(filter.tag)})`;
725
+ case 'child-of':
726
+ return `Filter.childOf([${filter.parents.map((parent) => JSON.stringify(parent)).join(', ')}], { transitive: ${filter.transitive} })`;
727
+ case 'timestamp':
728
+ return `Filter.${filter.field}.${filter.operator}(${filter.value})`;
729
+ case 'not':
730
+ return `Filter.not(${prettyFilter(filter.filter)})`;
731
+ case 'and':
732
+ return `Filter.and(${filter.filters.map(prettyFilter).join(', ')})`;
733
+ case 'or':
734
+ return `Filter.or(${filter.filters.map(prettyFilter).join(', ')})`;
735
+ }
736
+ };
737
+
738
+ const prettyQuery = (query: QueryAST.Query): string => {
739
+ switch (query.type) {
740
+ case 'select':
741
+ return `Query.select(${prettyFilter(query.filter)})`;
742
+ case 'filter':
743
+ return `${prettyQuery(query.selection)}.select(${prettyFilter(query.filter)})`;
744
+ case 'reference-traversal':
745
+ return `${prettyQuery(query.anchor)}.reference(${JSON.stringify(query.property)})`;
746
+ case 'incoming-references': {
747
+ const args: string[] = [];
748
+ if (query.typename !== null) {
749
+ args.push(JSON.stringify(query.typename));
750
+ }
751
+ if (query.property !== null) {
752
+ args.push(JSON.stringify(query.property));
753
+ }
754
+ return `${prettyQuery(query.anchor)}.referencedBy(${args.join(', ')})`;
755
+ }
756
+ case 'relation': {
757
+ const method =
758
+ query.direction === 'outgoing' ? 'sourceOf' : query.direction === 'incoming' ? 'targetOf' : 'relationOf';
759
+ const filterStr = query.filter !== undefined ? prettyFilter(query.filter) : '';
760
+ return `${prettyQuery(query.anchor)}.${method}(${filterStr})`;
761
+ }
762
+ case 'relation-traversal':
763
+ return `${prettyQuery(query.anchor)}.${query.direction}()`;
764
+ case 'hierarchy-traversal':
765
+ return query.direction === 'to-parent'
766
+ ? `${prettyQuery(query.anchor)}.parent()`
767
+ : `${prettyQuery(query.anchor)}.children()`;
768
+ case 'union':
769
+ return `Query.all(${query.queries.map(prettyQuery).join(', ')})`;
770
+ case 'set-difference':
771
+ return `Query.without(${prettyQuery(query.source)}, ${prettyQuery(query.exclude)})`;
772
+ case 'order': {
773
+ const orders = query.order.map((o) => {
774
+ if (o.kind === 'natural') {
775
+ return 'Order.natural';
776
+ }
777
+ if (o.kind === 'rank') {
778
+ return `Order.rank(${JSON.stringify(o.direction)})`;
779
+ }
780
+ return `Order.property(${JSON.stringify(o.property)}, ${JSON.stringify(o.direction)})`;
781
+ });
782
+ return `${prettyQuery(query.query)}.orderBy(${orders.join(', ')})`;
783
+ }
784
+ case 'options': {
785
+ const parts: string[] = [];
786
+ if (query.options.deleted !== undefined) {
787
+ parts.push(`deleted: ${JSON.stringify(query.options.deleted)}`);
788
+ }
789
+ if (query.options.debugLabel !== undefined) {
790
+ parts.push(`debugLabel: ${JSON.stringify(query.options.debugLabel)}`);
791
+ }
792
+ return `${prettyQuery(query.query)}.options({ ${parts.join(', ')} })`;
793
+ }
794
+ case 'from': {
795
+ if (query.from._tag === 'scope') {
796
+ if (query.from.scopes.length === 0) {
797
+ return `${prettyQuery(query.query)}.from('all-accessible-spaces')`;
798
+ }
799
+ const scopeStrs = query.from.scopes.map((scope) => {
800
+ if (scope._tag === 'space') {
801
+ return scope.includeAllFeeds
802
+ ? `{ space: ${JSON.stringify(scope.spaceId)}, includeAllFeeds: true }`
803
+ : `{ space: ${JSON.stringify(scope.spaceId)} }`;
804
+ }
805
+ if (scope._tag === 'feed') {
806
+ return `{ feed: ${String(scope.feedUri)} }`;
807
+ }
808
+ return `{ registry: ${JSON.stringify(scope.location)} }`;
809
+ });
810
+ return `${prettyQuery(query.query)}.from([${scopeStrs.join(', ')}])`;
811
+ }
812
+ return `${prettyQuery(query.query)}.from(${prettyQuery(query.from.query)})`;
813
+ }
814
+ case 'limit':
815
+ return `${prettyQuery(query.query)}.limit(${query.limit})`;
546
816
  }
547
- return Object.keys(value).every((key) => SCOPE_KEYS.has(key));
548
817
  };
@@ -39,14 +39,14 @@ describe('QuerySandbox', () => {
39
39
  const ast = sandbox.eval(trim`
40
40
  Query.select(Filter.type('org.dxos.type.person', { jobTitle: 'investor' }))
41
41
  .reference('organization')
42
- .targetOf('org.dxos.relation.has-subject')
42
+ .targetOf('org.dxos.relation.hasSubject')
43
43
  .source()
44
44
  `);
45
45
 
46
46
  expect(ast).toEqual(
47
47
  Query.select(Filter.type('org.dxos.type.person', { jobTitle: 'investor' }))
48
48
  .reference('organization')
49
- .targetOf('org.dxos.relation.has-subject')
49
+ .targetOf('org.dxos.relation.hasSubject')
50
50
  .source().ast,
51
51
  );
52
52
  });