@dxos/echo-query 0.8.4-main.ef1bc66f44 → 0.8.4-main.f466a3d56e

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.
@@ -16,7 +16,7 @@ import type { DXN, ObjectId } from '@dxos/keys';
16
16
  // TODO(wittjosiah): The `export * as ...` syntax causes tsdown to genereate multiple files which breaks the sandbox.
17
17
 
18
18
  class OrderClass implements Order$.Any {
19
- private static variance: Order$.Any['~Order'] = {} as Order$.Any['~Order'];
19
+ private static 'variance': Order$.Any['~Order'] = {} as Order$.Any['~Order'];
20
20
 
21
21
  static is(value: unknown): value is Order$.Any {
22
22
  return typeof value === 'object' && value !== null && '~Order' in value;
@@ -46,7 +46,7 @@ const Order2: typeof Order$ = Order1;
46
46
  export { Order2 as Order };
47
47
 
48
48
  class FilterClass implements Filter$.Any {
49
- private static variance: Filter$.Any['~Filter'] = {} as Filter$.Any['~Filter'];
49
+ private static 'variance': Filter$.Any['~Filter'] = {} as Filter$.Any['~Filter'];
50
50
 
51
51
  static is(value: unknown): value is Filter$.Any {
52
52
  return typeof value === 'object' && value !== null && '~Filter' in value;
@@ -111,7 +111,7 @@ class FilterClass implements Filter$.Any {
111
111
  }
112
112
  return new FilterClass({
113
113
  type: 'object',
114
- typename: makeTypeDxn(schema),
114
+ typename: makeTypeDXN(schema),
115
115
  ...propsFilterToAst(props ?? {}),
116
116
  });
117
117
  }
@@ -119,7 +119,7 @@ class FilterClass implements Filter$.Any {
119
119
  static typename(typename: string): Filter$.Any {
120
120
  return new FilterClass({
121
121
  type: 'object',
122
- typename: makeTypeDxn(typename),
122
+ typename: makeTypeDXN(typename),
123
123
  props: {},
124
124
  });
125
125
  }
@@ -139,6 +139,16 @@ class FilterClass implements Filter$.Any {
139
139
  });
140
140
  }
141
141
 
142
+ static key(key: string, options?: Filter$.KeyFilterOptions): Filter$.Any {
143
+ return new FilterClass({
144
+ type: 'object',
145
+ typename: null,
146
+ props: {},
147
+ metaKey: key,
148
+ metaVersion: options?.version,
149
+ });
150
+ }
151
+
142
152
  static props<T>(props: Filter$.Props<T>): Filter$.Filter<T> {
143
153
  return new FilterClass({
144
154
  type: 'object',
@@ -221,7 +231,7 @@ class FilterClass implements Filter$.Any {
221
231
  });
222
232
  }
223
233
 
224
- static in<T>(...values: T[]): Filter$.Filter<T | undefined> {
234
+ static in<T>(...values: T[]): Filter$.Filter<T> {
225
235
  return new FilterClass({
226
236
  type: 'in',
227
237
  values,
@@ -235,7 +245,7 @@ class FilterClass implements Filter$.Any {
235
245
  });
236
246
  }
237
247
 
238
- static between<T>(from: T, to: T): Filter$.Filter<unknown> {
248
+ static between<T>(from: T, to: T): Filter$.Filter<T> {
239
249
  return new FilterClass({
240
250
  type: 'range',
241
251
  from,
@@ -243,6 +253,47 @@ class FilterClass implements Filter$.Any {
243
253
  });
244
254
  }
245
255
 
256
+ static updated(range: { after?: Date | number; before?: Date | number }): Filter$.Any {
257
+ return FilterClass._timeRangeFilter('updatedAt', range);
258
+ }
259
+
260
+ static created(range: { after?: Date | number; before?: Date | number }): Filter$.Any {
261
+ return FilterClass._timeRangeFilter('createdAt', range);
262
+ }
263
+
264
+ static childOf(parents: unknown | DXN | (unknown | DXN)[], options?: { transitive?: boolean }): Filter$.Any {
265
+ const items = Array.isArray(parents) ? parents : [parents];
266
+ const dxns = items.map((item) => {
267
+ if (isDxnLike(item)) {
268
+ return item.toString();
269
+ }
270
+ throw new TypeError('childOf requires DXN values in query-lite');
271
+ });
272
+ return new FilterClass({
273
+ type: 'child-of',
274
+ parents: dxns,
275
+ transitive: options?.transitive ?? true,
276
+ });
277
+ }
278
+
279
+ private static _timeRangeFilter(
280
+ field: 'updatedAt' | 'createdAt',
281
+ range: { after?: Date | number; before?: Date | number },
282
+ ): Filter$.Any {
283
+ const toMs = (d: Date | number) => (typeof d === 'number' ? d : d.getTime());
284
+ const filters: Filter$.Any[] = [];
285
+ if (range.after != null) {
286
+ filters.push(new FilterClass({ type: 'timestamp', field, operator: 'gte', value: toMs(range.after) }));
287
+ }
288
+ if (range.before != null) {
289
+ filters.push(new FilterClass({ type: 'timestamp', field, operator: 'lte', value: toMs(range.before) }));
290
+ }
291
+ if (filters.length === 0) {
292
+ return FilterClass.everything();
293
+ }
294
+ return filters.length === 1 ? filters[0] : FilterClass.and(...filters);
295
+ }
296
+
246
297
  static not<F extends Filter$.Any>(filter: F): Filter$.Filter<Filter$.Type<F>> {
247
298
  return new FilterClass({
248
299
  type: 'not',
@@ -268,6 +319,11 @@ class FilterClass implements Filter$.Any {
268
319
  });
269
320
  }
270
321
 
322
+ /** Returns a human-readable string representation of a Filter AST. */
323
+ static pretty(filter: Filter$.Any): string {
324
+ return prettyFilter(filter.ast);
325
+ }
326
+
271
327
  private constructor(public readonly ast: QueryAST.Filter) {}
272
328
 
273
329
  '~Filter' = FilterClass.variance;
@@ -328,7 +384,7 @@ const processPredicate = (predicate: any): QueryAST.Filter => {
328
384
  };
329
385
 
330
386
  class QueryClass implements Query$.Any {
331
- private static variance: Query$.Any['~Query'] = {} as Query$.Any['~Query'];
387
+ private static 'variance': Query$.Any['~Query'] = {} as Query$.Any['~Query'];
332
388
 
333
389
  static is(value: unknown): value is Query$.Any {
334
390
  return typeof value === 'object' && value !== null && '~Query' in value;
@@ -345,6 +401,22 @@ class QueryClass implements Query$.Any {
345
401
  });
346
402
  }
347
403
 
404
+ select(filter: Filter$.Any | Filter$.Props<any>): Query$.Any {
405
+ if (FilterClass.is(filter)) {
406
+ return new QueryClass({
407
+ type: 'filter',
408
+ selection: this.ast,
409
+ filter: filter.ast,
410
+ });
411
+ } else {
412
+ return new QueryClass({
413
+ type: 'filter',
414
+ selection: this.ast,
415
+ filter: FilterClass.props(filter).ast,
416
+ });
417
+ }
418
+ }
419
+
348
420
  static type(schema: Schema.Schema.All | string, predicates?: Filter$.Props<unknown>): Query$.Any {
349
421
  return new QueryClass({
350
422
  type: 'select',
@@ -372,26 +444,49 @@ class QueryClass implements Query$.Any {
372
444
  });
373
445
  }
374
446
 
375
- constructor(public readonly ast: QueryAST.Query) {}
376
-
377
- '~Query' = QueryClass.variance;
447
+ static from(source: any, options?: { includeFeeds?: boolean }): Query$.Any {
448
+ const baseQuery: QueryAST.Query = {
449
+ type: 'select',
450
+ filter: FilterClass.everything().ast,
451
+ };
452
+ const wrapper = new QueryClass(baseQuery);
453
+ return wrapper.from(source, options);
454
+ }
378
455
 
379
- select(filter: Filter$.Any | Filter$.Props<any>): Query$.Any {
380
- if (FilterClass.is(filter)) {
456
+ from(arg: any, options?: { includeFeeds?: boolean }): Query$.Any {
457
+ if (arg === 'all-accessible-spaces') {
381
458
  return new QueryClass({
382
- type: 'filter',
383
- selection: this.ast,
384
- filter: filter.ast,
459
+ type: 'from',
460
+ query: this.ast,
461
+ from: {
462
+ _tag: 'scope',
463
+ scope: {
464
+ ...(options?.includeFeeds ? { allFeedsFromSpaces: true } : {}),
465
+ },
466
+ },
385
467
  });
386
- } else {
468
+ }
469
+
470
+ if (_isScopeLike(arg)) {
387
471
  return new QueryClass({
388
- type: 'filter',
389
- selection: this.ast,
390
- filter: FilterClass.props(filter).ast,
472
+ type: 'from',
473
+ query: this.ast,
474
+ from: { _tag: 'scope', scope: arg },
391
475
  });
392
476
  }
477
+
478
+ throw new TypeError('Database and Feed objects are not supported in query-lite sandbox');
393
479
  }
394
480
 
481
+ /** Returns a human-readable string representation of a Query AST. */
482
+ static pretty(query: Query$.Any): string {
483
+ return prettyQuery(query.ast);
484
+ }
485
+
486
+ constructor(public readonly ast: QueryAST.Query) {}
487
+
488
+ '~Query' = QueryClass.variance;
489
+
395
490
  reference(key: string): Query$.Any {
396
491
  return new QueryClass({
397
492
  type: 'reference-traversal',
@@ -415,21 +510,21 @@ class QueryClass implements Query$.Any {
415
510
  });
416
511
  }
417
512
 
418
- sourceOf(relation: Schema.Schema.All | string, predicates?: Filter$.Props<unknown> | undefined): Query$.Any {
513
+ sourceOf(relation?: Schema.Schema.All | string, predicates?: Filter$.Props<unknown> | undefined): Query$.Any {
419
514
  return new QueryClass({
420
515
  type: 'relation',
421
516
  anchor: this.ast,
422
517
  direction: 'outgoing',
423
- filter: FilterClass.type(relation, predicates).ast,
518
+ filter: relation !== undefined ? FilterClass.type(relation, predicates).ast : undefined,
424
519
  });
425
520
  }
426
521
 
427
- targetOf(relation: Schema.Schema.All | string, predicates?: Filter$.Props<unknown> | undefined): Query$.Any {
522
+ targetOf(relation?: Schema.Schema.All | string, predicates?: Filter$.Props<unknown> | undefined): Query$.Any {
428
523
  return new QueryClass({
429
524
  type: 'relation',
430
525
  anchor: this.ast,
431
526
  direction: 'incoming',
432
- filter: FilterClass.type(relation, predicates).ast,
527
+ filter: relation !== undefined ? FilterClass.type(relation, predicates).ast : undefined,
433
528
  });
434
529
  }
435
530
 
@@ -488,6 +583,21 @@ class QueryClass implements Query$.Any {
488
583
  options,
489
584
  });
490
585
  }
586
+
587
+ debugLabel(label: string): Query$.Any {
588
+ if (this.ast.type === 'options') {
589
+ return new QueryClass({
590
+ type: 'options',
591
+ query: this.ast.query,
592
+ options: { ...this.ast.options, debugLabel: label },
593
+ });
594
+ }
595
+ return new QueryClass({
596
+ type: 'options',
597
+ query: this.ast,
598
+ options: { debugLabel: label },
599
+ });
600
+ }
491
601
  }
492
602
 
493
603
  export const Query1: typeof Query$ = QueryClass;
@@ -498,8 +608,147 @@ const isRef = (obj: any): obj is Ref.Ref<any> => {
498
608
  return obj && typeof obj === 'object' && RefTypeId in obj;
499
609
  };
500
610
 
501
- const makeTypeDxn = (typename: string) => {
611
+ const makeTypeDXN = (typename: string) => {
502
612
  assertArgument(typeof typename === 'string', 'typename');
503
613
  assertArgument(!typename.startsWith('dxn:'), 'typename');
504
614
  return `dxn:type:${typename}`;
505
615
  };
616
+
617
+ const isDxnLike = (value: unknown): value is DXN => {
618
+ return (
619
+ typeof value === 'object' &&
620
+ value !== null &&
621
+ 'toString' in value &&
622
+ typeof value.toString === 'function' &&
623
+ value.toString().startsWith('dxn:')
624
+ );
625
+ };
626
+
627
+ const SCOPE_KEYS = new Set(['spaceIds', 'feeds', 'allFeedsFromSpaces']);
628
+
629
+ const _isScopeLike = (value: unknown): value is QueryAST.Scope => {
630
+ if (typeof value !== 'object' || value === null || Array.isArray(value)) {
631
+ return false;
632
+ }
633
+ return Object.keys(value).every((key) => SCOPE_KEYS.has(key));
634
+ };
635
+
636
+ const prettyFilter = (filter: QueryAST.Filter): string => {
637
+ switch (filter.type) {
638
+ case 'object': {
639
+ const parts: string[] = [];
640
+ if (filter.typename !== null) {
641
+ parts.push(JSON.stringify(filter.typename));
642
+ }
643
+ const propEntries = Object.entries(filter.props);
644
+ if (propEntries.length > 0) {
645
+ const propsStr = propEntries.map(([k, v]) => `${k}: ${prettyFilter(v)}`).join(', ');
646
+ parts.push(`{ ${propsStr} }`);
647
+ }
648
+ if (filter.id !== undefined) {
649
+ parts.push(`id: [${filter.id.join(', ')}]`);
650
+ }
651
+ return parts.length > 0 ? `Filter.type(${parts.join(', ')})` : 'Filter.everything()';
652
+ }
653
+ case 'compare':
654
+ return `Filter.${filter.operator}(${JSON.stringify(filter.value)})`;
655
+ case 'in':
656
+ return `Filter.in(${filter.values.map((v) => JSON.stringify(v)).join(', ')})`;
657
+ case 'contains':
658
+ return `Filter.contains(${JSON.stringify(filter.value)})`;
659
+ case 'range':
660
+ return `Filter.between(${JSON.stringify(filter.from)}, ${JSON.stringify(filter.to)})`;
661
+ case 'text-search':
662
+ return `Filter.text(${JSON.stringify(filter.text)})`;
663
+ case 'tag':
664
+ return `Filter.tag(${JSON.stringify(filter.tag)})`;
665
+ case 'child-of':
666
+ return `Filter.childOf([${filter.parents.map((parent) => JSON.stringify(parent)).join(', ')}], { transitive: ${filter.transitive} })`;
667
+ case 'timestamp':
668
+ return `Filter.${filter.field}.${filter.operator}(${filter.value})`;
669
+ case 'not':
670
+ return `Filter.not(${prettyFilter(filter.filter)})`;
671
+ case 'and':
672
+ return `Filter.and(${filter.filters.map(prettyFilter).join(', ')})`;
673
+ case 'or':
674
+ return `Filter.or(${filter.filters.map(prettyFilter).join(', ')})`;
675
+ }
676
+ };
677
+
678
+ const prettyQuery = (query: QueryAST.Query): string => {
679
+ switch (query.type) {
680
+ case 'select':
681
+ return `Query.select(${prettyFilter(query.filter)})`;
682
+ case 'filter':
683
+ return `${prettyQuery(query.selection)}.select(${prettyFilter(query.filter)})`;
684
+ case 'reference-traversal':
685
+ return `${prettyQuery(query.anchor)}.reference(${JSON.stringify(query.property)})`;
686
+ case 'incoming-references': {
687
+ const args: string[] = [];
688
+ if (query.typename !== null) {
689
+ args.push(JSON.stringify(query.typename));
690
+ }
691
+ if (query.property !== null) {
692
+ args.push(JSON.stringify(query.property));
693
+ }
694
+ return `${prettyQuery(query.anchor)}.referencedBy(${args.join(', ')})`;
695
+ }
696
+ case 'relation': {
697
+ const method =
698
+ query.direction === 'outgoing' ? 'sourceOf' : query.direction === 'incoming' ? 'targetOf' : 'relationOf';
699
+ const filterStr = query.filter !== undefined ? prettyFilter(query.filter) : '';
700
+ return `${prettyQuery(query.anchor)}.${method}(${filterStr})`;
701
+ }
702
+ case 'relation-traversal':
703
+ return `${prettyQuery(query.anchor)}.${query.direction}()`;
704
+ case 'hierarchy-traversal':
705
+ return query.direction === 'to-parent'
706
+ ? `${prettyQuery(query.anchor)}.parent()`
707
+ : `${prettyQuery(query.anchor)}.children()`;
708
+ case 'union':
709
+ return `Query.all(${query.queries.map(prettyQuery).join(', ')})`;
710
+ case 'set-difference':
711
+ return `Query.without(${prettyQuery(query.source)}, ${prettyQuery(query.exclude)})`;
712
+ case 'order': {
713
+ const orders = query.order.map((o) => {
714
+ if (o.kind === 'natural') {
715
+ return 'Order.natural';
716
+ }
717
+ if (o.kind === 'rank') {
718
+ return `Order.rank(${JSON.stringify(o.direction)})`;
719
+ }
720
+ return `Order.property(${JSON.stringify(o.property)}, ${JSON.stringify(o.direction)})`;
721
+ });
722
+ return `${prettyQuery(query.query)}.orderBy(${orders.join(', ')})`;
723
+ }
724
+ case 'options': {
725
+ const parts: string[] = [];
726
+ if (query.options.deleted !== undefined) {
727
+ parts.push(`deleted: ${JSON.stringify(query.options.deleted)}`);
728
+ }
729
+ if (query.options.debugLabel !== undefined) {
730
+ parts.push(`debugLabel: ${JSON.stringify(query.options.debugLabel)}`);
731
+ }
732
+ return `${prettyQuery(query.query)}.options({ ${parts.join(', ')} })`;
733
+ }
734
+ case 'from': {
735
+ if (query.from._tag === 'scope') {
736
+ const scope = query.from.scope;
737
+ const parts: string[] = [];
738
+ if (scope.spaceIds !== undefined) {
739
+ parts.push(`spaceIds: [${scope.spaceIds.join(', ')}]`);
740
+ }
741
+ if (scope.feeds !== undefined) {
742
+ parts.push(`feeds: [${scope.feeds.join(', ')}]`);
743
+ }
744
+ if (scope.allFeedsFromSpaces !== undefined) {
745
+ parts.push(`allFeedsFromSpaces: ${scope.allFeedsFromSpaces}`);
746
+ }
747
+ return `${prettyQuery(query.query)}.from({ ${parts.join(', ')} })`;
748
+ }
749
+ return `${prettyQuery(query.query)}.from(${prettyQuery(query.from.query)})`;
750
+ }
751
+ case 'limit':
752
+ return `${prettyQuery(query.query)}.limit(${query.limit})`;
753
+ }
754
+ };
@@ -16,37 +16,37 @@ describe('QuerySandbox', () => {
16
16
 
17
17
  test('works', { timeout: 10_000 }, async () => {
18
18
  const ast = sandbox.eval(trim`
19
- Query.select(Filter.typename('dxos.org/type/Person'))
19
+ Query.select(Filter.typename('org.dxos.type.person'))
20
20
  `);
21
- expect(ast).toEqual(Query.select(Filter.typename('dxos.org/type/Person')).ast);
21
+ expect(ast).toEqual(Query.select(Filter.typename('org.dxos.type.person')).ast);
22
22
  });
23
23
 
24
24
  test('works with just Filter passed in', () => {
25
25
  const ast = sandbox.eval(trim`
26
- Filter.typename('dxos.org/type/Person')
26
+ Filter.typename('org.dxos.type.person')
27
27
  `);
28
- expect(ast).toEqual(Query.select(Filter.typename('dxos.org/type/Person')).ast);
28
+ expect(ast).toEqual(Query.select(Filter.typename('org.dxos.type.person')).ast);
29
29
  });
30
30
 
31
31
  test('Order', () => {
32
32
  const ast = sandbox.eval(trim`
33
- Query.type('dxos.org/type/Person').orderBy(Order.property('name', 'desc'))
33
+ Query.type('org.dxos.type.person').orderBy(Order.property('name', 'desc'))
34
34
  `);
35
- expect(ast).toEqual(Query.type('dxos.org/type/Person').orderBy(Order.property('name', 'desc')).ast);
35
+ expect(ast).toEqual(Query.type('org.dxos.type.person').orderBy(Order.property('name', 'desc')).ast);
36
36
  });
37
37
 
38
38
  test('traversal', () => {
39
39
  const ast = sandbox.eval(trim`
40
- Query.select(Filter.type('dxos.org/type/Person', { jobTitle: 'investor' }))
40
+ Query.select(Filter.type('org.dxos.type.person', { jobTitle: 'investor' }))
41
41
  .reference('organization')
42
- .targetOf('dxos.org/relation/HasSubject')
42
+ .targetOf('org.dxos.relation.hasSubject')
43
43
  .source()
44
44
  `);
45
45
 
46
46
  expect(ast).toEqual(
47
- Query.select(Filter.type('dxos.org/type/Person', { jobTitle: 'investor' }))
47
+ Query.select(Filter.type('org.dxos.type.person', { jobTitle: 'investor' }))
48
48
  .reference('organization')
49
- .targetOf('dxos.org/relation/HasSubject')
49
+ .targetOf('org.dxos.relation.hasSubject')
50
50
  .source().ast,
51
51
  );
52
52
  });
@@ -49,7 +49,7 @@ export class QuerySandbox extends Resource {
49
49
 
50
50
  /**
51
51
  * Evaluates the query code.
52
- * @param queryCode Example: `Query.select(Filter.typename('dxos.org/type/Person'))`
52
+ * @param queryCode Example: `Query.select(Filter.typename('org.dxos.type.person'))`
53
53
  */
54
54
  eval(queryCode: string): QueryAST.Query {
55
55
  using context = this.#runtime.newContext();