@dxos/echo-query 0.8.4-main.c85a9c8dae → 0.8.4-main.d05539e30a

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.
@@ -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;
@@ -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',
@@ -381,26 +453,40 @@ class QueryClass implements Query$.Any {
381
453
  return wrapper.from(source, options);
382
454
  }
383
455
 
384
- constructor(public readonly ast: QueryAST.Query) {}
385
-
386
- '~Query' = QueryClass.variance;
387
-
388
- select(filter: Filter$.Any | Filter$.Props<any>): Query$.Any {
389
- if (FilterClass.is(filter)) {
456
+ from(arg: any, options?: { includeFeeds?: boolean }): Query$.Any {
457
+ if (arg === 'all-accessible-spaces') {
390
458
  return new QueryClass({
391
- type: 'filter',
392
- selection: this.ast,
393
- filter: filter.ast,
459
+ type: 'from',
460
+ query: this.ast,
461
+ from: {
462
+ _tag: 'scope',
463
+ scope: {
464
+ ...(options?.includeFeeds ? { allFeedsFromSpaces: true } : {}),
465
+ },
466
+ },
394
467
  });
395
- } else {
468
+ }
469
+
470
+ if (_isScopeLike(arg)) {
396
471
  return new QueryClass({
397
- type: 'filter',
398
- selection: this.ast,
399
- filter: FilterClass.props(filter).ast,
472
+ type: 'from',
473
+ query: this.ast,
474
+ from: { _tag: 'scope', scope: arg },
400
475
  });
401
476
  }
477
+
478
+ throw new TypeError('Database and Feed objects are not supported in query-lite sandbox');
479
+ }
480
+
481
+ /** Returns a human-readable string representation of a Query AST. */
482
+ static pretty(query: Query$.Any): string {
483
+ return prettyQuery(query.ast);
402
484
  }
403
485
 
486
+ constructor(public readonly ast: QueryAST.Query) {}
487
+
488
+ '~Query' = QueryClass.variance;
489
+
404
490
  reference(key: string): Query$.Any {
405
491
  return new QueryClass({
406
492
  type: 'reference-traversal',
@@ -490,36 +576,26 @@ class QueryClass implements Query$.Any {
490
576
  });
491
577
  }
492
578
 
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
- }
579
+ options(options: QueryAST.QueryOptions): Query$.Any {
580
+ return new QueryClass({
581
+ type: 'options',
582
+ query: this.ast,
583
+ options,
584
+ });
585
+ }
506
586
 
507
- if (_isScopeLike(arg)) {
587
+ debugLabel(label: string): Query$.Any {
588
+ if (this.ast.type === 'options') {
508
589
  return new QueryClass({
509
- type: 'from',
510
- query: this.ast,
511
- from: { _tag: 'scope', scope: arg },
590
+ type: 'options',
591
+ query: this.ast.query,
592
+ options: { ...this.ast.options, debugLabel: label },
512
593
  });
513
594
  }
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
595
  return new QueryClass({
520
596
  type: 'options',
521
597
  query: this.ast,
522
- options,
598
+ options: { debugLabel: label },
523
599
  });
524
600
  }
525
601
  }
@@ -532,13 +608,23 @@ const isRef = (obj: any): obj is Ref.Ref<any> => {
532
608
  return obj && typeof obj === 'object' && RefTypeId in obj;
533
609
  };
534
610
 
535
- const makeTypeDxn = (typename: string) => {
611
+ const makeTypeDXN = (typename: string) => {
536
612
  assertArgument(typeof typename === 'string', 'typename');
537
613
  assertArgument(!typename.startsWith('dxn:'), 'typename');
538
614
  return `dxn:type:${typename}`;
539
615
  };
540
616
 
541
- const SCOPE_KEYS = new Set(['spaceIds', 'queues', 'allQueuesFromSpaces']);
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']);
542
628
 
543
629
  const _isScopeLike = (value: unknown): value is QueryAST.Scope => {
544
630
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
@@ -546,3 +632,123 @@ const _isScopeLike = (value: unknown): value is QueryAST.Scope => {
546
632
  }
547
633
  return Object.keys(value).every((key) => SCOPE_KEYS.has(key));
548
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();