@dxos/echo-query 0.8.4-main.74a063c4e0 → 0.8.4-main.765dc60934

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/echo-query",
3
- "version": "0.8.4-main.74a063c4e0",
3
+ "version": "0.8.4-main.765dc60934",
4
4
  "description": "ECHO queries.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -8,7 +8,7 @@
8
8
  "type": "git",
9
9
  "url": "https://github.com/dxos/dxos"
10
10
  },
11
- "license": "MIT",
11
+ "license": "FSL-1.1-Apache-2.0",
12
12
  "author": "info@dxos.org",
13
13
  "sideEffects": false,
14
14
  "type": "module",
@@ -24,9 +24,6 @@
24
24
  "./api.d.ts": "./dist/query-lite/index.d.ts"
25
25
  },
26
26
  "types": "dist/types/src/index.d.ts",
27
- "typesVersions": {
28
- "*": {}
29
- },
30
27
  "files": [
31
28
  "dist",
32
29
  "src"
@@ -35,22 +32,22 @@
35
32
  "@lezer/common": "^1.2.2",
36
33
  "@lezer/lezer": "^1.1.2",
37
34
  "@lezer/lr": "^1.4.2",
38
- "@dxos/context": "0.8.4-main.74a063c4e0",
39
- "@dxos/echo-protocol": "0.8.4-main.74a063c4e0",
40
- "@dxos/echo": "0.8.4-main.74a063c4e0",
41
- "@dxos/errors": "0.8.4-main.74a063c4e0",
42
- "@dxos/invariant": "0.8.4-main.74a063c4e0",
43
- "@dxos/node-std": "0.8.4-main.74a063c4e0",
44
- "@dxos/util": "0.8.4-main.74a063c4e0",
45
- "@dxos/vendor-quickjs": "0.8.4-main.74a063c4e0",
46
- "@dxos/debug": "0.8.4-main.74a063c4e0"
35
+ "@dxos/debug": "0.8.4-main.765dc60934",
36
+ "@dxos/echo": "0.8.4-main.765dc60934",
37
+ "@dxos/context": "0.8.4-main.765dc60934",
38
+ "@dxos/echo-protocol": "0.8.4-main.765dc60934",
39
+ "@dxos/invariant": "0.8.4-main.765dc60934",
40
+ "@dxos/node-std": "0.8.4-main.765dc60934",
41
+ "@dxos/errors": "0.8.4-main.765dc60934",
42
+ "@dxos/util": "0.8.4-main.765dc60934",
43
+ "@dxos/vendor-quickjs": "0.8.4-main.765dc60934"
47
44
  },
48
45
  "devDependencies": {
49
46
  "@lezer/generator": "^1.7.1",
50
47
  "tsdown": "^0.16.7",
51
- "typescript": "^5.9.3",
52
- "@dxos/echo-generator": "0.8.4-main.74a063c4e0",
53
- "@dxos/random": "0.8.4-main.74a063c4e0"
48
+ "typescript": "^6.0.3",
49
+ "@dxos/echo-generator": "0.8.4-main.765dc60934",
50
+ "@dxos/random": "0.8.4-main.765dc60934"
54
51
  },
55
52
  "publishConfig": {
56
53
  "access": "public"
@@ -28,7 +28,7 @@ export class QueryBuilder {
28
28
  */
29
29
  validate(input: string): boolean {
30
30
  try {
31
- const tree = this._parser.parse(input);
31
+ const tree = this._parser.parse(normalizeInput(input));
32
32
  return tree.cursor().node.name === 'Query';
33
33
  } catch {
34
34
  return false;
@@ -40,8 +40,9 @@ export class QueryBuilder {
40
40
  */
41
41
  build(input: string): BuildResult {
42
42
  try {
43
- const tree = this._parser.parse(input);
44
- return this.buildQuery(tree, input);
43
+ const normalized = normalizeInput(input);
44
+ const tree = this._parser.parse(normalized);
45
+ return this.buildQuery(tree, normalized);
45
46
  } catch {
46
47
  return {};
47
48
  }
@@ -245,8 +246,9 @@ export class QueryBuilder {
245
246
  let exprEnd = cursor.to;
246
247
 
247
248
  while (cursor.nextSibling() && depth > 0) {
248
- if (cursor.node.name === '(') depth++;
249
- else if (cursor.node.name === ')') {
249
+ if (cursor.node.name === '(') {
250
+ depth++;
251
+ } else if (cursor.node.name === ')') {
250
252
  depth--;
251
253
  if (depth === 0) {
252
254
  exprEnd = cursor.from;
@@ -355,8 +357,8 @@ export class QueryBuilder {
355
357
  cursor.firstChild(); // Move to String node.
356
358
  const text = this._getNodeText(cursor, input);
357
359
  cursor.parent(); // Go back to TextFilter.
358
- // Remove quotes.
359
- return Filter.text(text.slice(1, -1));
360
+ // Remove quotes and decode escapes.
361
+ return Filter.text(unescapeStringLiteral(text.slice(1, -1)));
360
362
  }
361
363
 
362
364
  /**
@@ -479,9 +481,9 @@ export class QueryBuilder {
479
481
 
480
482
  switch (valueType) {
481
483
  case 'String': {
482
- // Remove quotes.
484
+ // Remove quotes and decode escapes.
483
485
  const str = this._getNodeText(cursor, input);
484
- return str.slice(1, -1);
486
+ return unescapeStringLiteral(str.slice(1, -1));
485
487
  }
486
488
 
487
489
  case 'Number':
@@ -537,3 +539,261 @@ export class QueryBuilder {
537
539
  return input.slice(cursor.from, cursor.to);
538
540
  }
539
541
  }
542
+
543
+ const KEYWORDS = new Set(['AND', 'OR', 'NOT']);
544
+ const VALUE_LITERALS = new Set(['true', 'false', 'null']);
545
+ const SPECIAL_CHARS = /[\s(){}\[\],"']/;
546
+ const PROPERTY_KEY = /^[a-zA-Z_][a-zA-Z0-9_.]*$/;
547
+ const NUMBER_LITERAL = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
548
+
549
+ // Common URL schemes — `http://...`, `mailto:rich@...` etc. should be searched as text,
550
+ // not auto-promoted to property filters.
551
+ const URL_SCHEMES = new Set([
552
+ 'http',
553
+ 'https',
554
+ 'ftp',
555
+ 'ftps',
556
+ 'file',
557
+ 'mailto',
558
+ 'tel',
559
+ 'sms',
560
+ 'data',
561
+ 'javascript',
562
+ 'ws',
563
+ 'wss',
564
+ 'ssh',
565
+ 'git',
566
+ ]);
567
+
568
+ /**
569
+ * Normalize raw user input into a form the lezer grammar can parse.
570
+ * - Bare text fragments (e.g. `foo`) are wrapped in quotes so they parse as TextFilter.
571
+ * - Property values that aren't already quoted/numeric/boolean (e.g. `from:rich@dxos.org`) are quoted.
572
+ * - Tags, type filters, operators, parens, braces, quoted strings, and assignments pass through unchanged.
573
+ */
574
+ export const normalizeInput = (input: string): string => {
575
+ const out: string[] = [];
576
+ let pos = 0;
577
+ while (pos < input.length) {
578
+ const currentChar = input[pos];
579
+
580
+ // Whitespace.
581
+ if (/\s/.test(currentChar)) {
582
+ out.push(currentChar);
583
+ pos++;
584
+ continue;
585
+ }
586
+
587
+ // Quoted string (already a valid TextFilter / Value).
588
+ // Single-quote only opens a string if the previous char isn't a word-char,
589
+ // so apostrophes inside barewords (e.g. `don't`, `O'Connor`) stay attached.
590
+ const prevChar = pos > 0 ? input[pos - 1] : '';
591
+ const isStringOpener =
592
+ currentChar === '"' || (currentChar === "'" && (pos === 0 || !/[a-zA-Z0-9_]/.test(prevChar)));
593
+ if (isStringOpener) {
594
+ const quoteChar = currentChar;
595
+ let scanIndex = pos + 1;
596
+ while (scanIndex < input.length && input[scanIndex] !== quoteChar) {
597
+ if (input[scanIndex] === '\\' && scanIndex + 1 < input.length) {
598
+ scanIndex += 2;
599
+ } else {
600
+ scanIndex++;
601
+ }
602
+ }
603
+ out.push(input.slice(pos, Math.min(scanIndex + 1, input.length)));
604
+ pos = scanIndex + 1;
605
+ continue;
606
+ }
607
+
608
+ // Object/array literals: pass through (contents already structured).
609
+ if (currentChar === '{' || currentChar === '[') {
610
+ const closeChar = currentChar === '{' ? '}' : ']';
611
+ let depth = 1;
612
+ let scanIndex = pos + 1;
613
+ while (scanIndex < input.length && depth > 0) {
614
+ const innerChar = input[scanIndex];
615
+ if (innerChar === '"' || innerChar === "'") {
616
+ const quoteChar = innerChar;
617
+ scanIndex++;
618
+ while (scanIndex < input.length && input[scanIndex] !== quoteChar) {
619
+ if (input[scanIndex] === '\\' && scanIndex + 1 < input.length) {
620
+ scanIndex += 2;
621
+ } else {
622
+ scanIndex++;
623
+ }
624
+ }
625
+ scanIndex++;
626
+ } else if (innerChar === currentChar) {
627
+ depth++;
628
+ scanIndex++;
629
+ } else if (innerChar === closeChar) {
630
+ depth--;
631
+ scanIndex++;
632
+ } else {
633
+ scanIndex++;
634
+ }
635
+ }
636
+ out.push(input.slice(pos, scanIndex));
637
+ pos = scanIndex;
638
+ continue;
639
+ }
640
+
641
+ // Single-char structural tokens. `}` / `]` only reach here when unmatched —
642
+ // pass them through so the lezer parser can produce a clear error rather than spinning.
643
+ if (
644
+ currentChar === '(' ||
645
+ currentChar === ')' ||
646
+ currentChar === '=' ||
647
+ currentChar === ',' ||
648
+ currentChar === '}' ||
649
+ currentChar === ']'
650
+ ) {
651
+ out.push(currentChar);
652
+ pos++;
653
+ continue;
654
+ }
655
+
656
+ // Relations.
657
+ if ((currentChar === '-' && input[pos + 1] === '>') || (currentChar === '<' && input[pos + 1] === '-')) {
658
+ out.push(input.slice(pos, pos + 2));
659
+ pos += 2;
660
+ continue;
661
+ }
662
+
663
+ // NOT prefix (`!`) — single token.
664
+ if (currentChar === '!') {
665
+ out.push(currentChar);
666
+ pos++;
667
+ continue;
668
+ }
669
+
670
+ // Tag.
671
+ if (currentChar === '#') {
672
+ let scanIndex = pos + 1;
673
+ while (scanIndex < input.length && /[a-zA-Z0-9_-]/.test(input[scanIndex])) {
674
+ scanIndex++;
675
+ }
676
+ out.push(input.slice(pos, scanIndex));
677
+ pos = scanIndex;
678
+ continue;
679
+ }
680
+
681
+ // Bareword: scan until next whitespace or special char.
682
+ let scanIndex = pos;
683
+ while (scanIndex < input.length) {
684
+ const innerChar = input[scanIndex];
685
+ if (innerChar === '"') {
686
+ break;
687
+ }
688
+ if (innerChar === "'" && scanIndex > pos && !/[a-zA-Z0-9_]/.test(input[scanIndex - 1])) {
689
+ break;
690
+ }
691
+ if (innerChar === "'" && scanIndex === pos) {
692
+ break;
693
+ }
694
+ if (innerChar !== "'" && SPECIAL_CHARS.test(innerChar)) {
695
+ break;
696
+ }
697
+ if (innerChar === '-' && input[scanIndex + 1] === '>') {
698
+ break;
699
+ }
700
+ if (innerChar === '<' && input[scanIndex + 1] === '-') {
701
+ break;
702
+ }
703
+ scanIndex++;
704
+ }
705
+ // Defensive: if no characters were consumed, advance one to avoid infinite loops.
706
+ if (scanIndex === pos) {
707
+ out.push(currentChar);
708
+ pos++;
709
+ continue;
710
+ }
711
+ const token = input.slice(pos, scanIndex);
712
+ pos = scanIndex;
713
+
714
+ // Operators.
715
+ if (KEYWORDS.has(token.toUpperCase())) {
716
+ out.push(token);
717
+ continue;
718
+ }
719
+
720
+ // Property/type filter (`key:value`).
721
+ const colonIdx = token.indexOf(':');
722
+ if (colonIdx > 0) {
723
+ const key = token.slice(0, colonIdx);
724
+ const rest = token.slice(colonIdx + 1);
725
+
726
+ // type:typename — leave for grammar's TypeFilter (Identifier value).
727
+ if (key === 'type') {
728
+ out.push(token);
729
+ continue;
730
+ }
731
+
732
+ // URLs (`http://...`, `mailto:rich@...`) and URL-paths starting with `//`
733
+ // are searched as text rather than auto-promoted to property filters.
734
+ const isUrlScheme = URL_SCHEMES.has(key.toLowerCase()) || rest.startsWith('//');
735
+ if (!isUrlScheme && PROPERTY_KEY.test(key)) {
736
+ if (rest.length === 0) {
737
+ // Trailing colon while typing — pass through.
738
+ out.push(token);
739
+ continue;
740
+ }
741
+ const firstValueChar = rest[0];
742
+ if (firstValueChar === '"' || firstValueChar === "'") {
743
+ // Already quoted.
744
+ out.push(token);
745
+ continue;
746
+ }
747
+ if (firstValueChar === '{' || firstValueChar === '[') {
748
+ // Object/array literal value.
749
+ out.push(token);
750
+ continue;
751
+ }
752
+ if (VALUE_LITERALS.has(rest) || NUMBER_LITERAL.test(rest)) {
753
+ // Boolean / null / number literal.
754
+ out.push(token);
755
+ continue;
756
+ }
757
+ out.push(`${key}:"${escapeStringLiteral(rest)}"`);
758
+ continue;
759
+ }
760
+
761
+ // URL or unknown key shape — fall through to text.
762
+ }
763
+
764
+ // Identifier followed by `=` is the LHS of an Assignment — keep as-is.
765
+ if (PROPERTY_KEY.test(token)) {
766
+ let lookahead = pos;
767
+ while (lookahead < input.length && /\s/.test(input[lookahead])) {
768
+ lookahead++;
769
+ }
770
+ if (input[lookahead] === '=') {
771
+ out.push(token);
772
+ continue;
773
+ }
774
+ }
775
+
776
+ // Bare text fragment: quote so it parses as TextFilter.
777
+ out.push(`"${escapeStringLiteral(token)}"`);
778
+ }
779
+
780
+ return out.join('');
781
+ };
782
+
783
+ /** Escape a raw value into a string literal body (without surrounding quotes). */
784
+ const escapeStringLiteral = (value: string): string => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
785
+
786
+ /** Decode `\\` and `\"` escapes inside a string literal body. */
787
+ const unescapeStringLiteral = (literalBody: string): string => {
788
+ let out = '';
789
+ for (let i = 0; i < literalBody.length; i++) {
790
+ const ch = literalBody[i];
791
+ if (ch === '\\' && i + 1 < literalBody.length) {
792
+ out += literalBody[i + 1];
793
+ i++;
794
+ } else {
795
+ out += ch;
796
+ }
797
+ }
798
+ return out;
799
+ };
@@ -3,12 +3,12 @@
3
3
  //
4
4
 
5
5
  import { type Tree } from '@lezer/common';
6
- import { describe, it } from 'vitest';
6
+ import { describe, it, test } from 'vitest';
7
7
 
8
8
  import { Filter, Tag } from '@dxos/echo';
9
9
 
10
10
  import { QueryDSL } from './gen';
11
- import { type BuildResult, QueryBuilder } from './query-builder';
11
+ import { type BuildResult, QueryBuilder, normalizeInput } from './query-builder';
12
12
 
13
13
  // TODO(burdon): Ref/Relation traversal.
14
14
 
@@ -413,4 +413,117 @@ describe('query', () => {
413
413
  expect(result, JSON.stringify({ input, result, expected }, null, 2)).toEqual(expected);
414
414
  });
415
415
  });
416
+
417
+ test('normalizeInput', ({ expect }) => {
418
+ type Test = { input: string; expected: string };
419
+ const tests: Test[] = [
420
+ { input: 'foo', expected: '"foo"' },
421
+ { input: 'foo bar', expected: '"foo" "bar"' },
422
+ { input: 'foo bar', expected: '"foo" "bar"' },
423
+ { input: '"already" bare', expected: '"already" "bare"' },
424
+ { input: 'from:rich@dxos.org', expected: 'from:"rich@dxos.org"' },
425
+ { input: 'from:rich@dxos.org urgent', expected: 'from:"rich@dxos.org" "urgent"' },
426
+ { input: 'name:DXOS', expected: 'name:"DXOS"' },
427
+ { input: 'count:42', expected: 'count:42' },
428
+ { input: 'active:true', expected: 'active:true' },
429
+ { input: 'value:null', expected: 'value:null' },
430
+ { input: 'name:"DXOS"', expected: 'name:"DXOS"' },
431
+ { input: 'type:org.dxos.type.person', expected: 'type:org.dxos.type.person' },
432
+ { input: '#tag', expected: '#tag' },
433
+ { input: '#tag foo', expected: '#tag "foo"' },
434
+ { input: 'foo AND bar', expected: '"foo" AND "bar"' },
435
+ { input: 'foo OR bar', expected: '"foo" OR "bar"' },
436
+ { input: 'NOT foo', expected: 'NOT "foo"' },
437
+ { input: '!foo', expected: '!"foo"' },
438
+ { input: '(foo bar)', expected: '("foo" "bar")' },
439
+ { input: 'x = ( foo )', expected: 'x = ( "foo" )' },
440
+ { input: '{ name: "DXOS" }', expected: '{ name: "DXOS" }' },
441
+ // Apostrophes inside barewords don't open a quoted string.
442
+ { input: "don't", expected: '"don\'t"' },
443
+ { input: "O'Connor", expected: '"O\'Connor"' },
444
+ { input: "don't worry", expected: '"don\'t" "worry"' },
445
+ // Unmatched closing brace/bracket — passed through, no infinite loop.
446
+ { input: 'foo}', expected: '"foo"}' },
447
+ { input: 'foo]bar', expected: '"foo"]"bar"' },
448
+ // Genuine single-quoted string still works.
449
+ { input: "'foo bar'", expected: "'foo bar'" },
450
+ // URLs are searched as text rather than auto-promoted to property filters.
451
+ { input: 'https://dxos.org', expected: '"https://dxos.org"' },
452
+ { input: 'http://example.com/foo', expected: '"http://example.com/foo"' },
453
+ { input: 'mailto:rich@dxos.org', expected: '"mailto:rich@dxos.org"' },
454
+ // Escapes round-trip: backslashes are escaped in the literal body.
455
+ { input: 'foo\\bar', expected: '"foo\\\\bar"' },
456
+ ];
457
+
458
+ for (const { input, expected } of tests) {
459
+ expect(normalizeInput(input), input).toEqual(expected);
460
+ }
461
+ });
462
+
463
+ test('build with property and text fragments', ({ expect }) => {
464
+ const queryBuilder = new QueryBuilder({
465
+ tag_1: Tag.make({ label: 'foo' }),
466
+ });
467
+
468
+ type Test = { input: string; expected: BuildResult };
469
+ const tests: Test[] = [
470
+ // Property filter from `key:value` text input.
471
+ {
472
+ input: 'from:rich@dxos.org',
473
+ expected: { filter: Filter.props({ from: 'rich@dxos.org' }) },
474
+ },
475
+ {
476
+ input: 'name:DXOS',
477
+ expected: { filter: Filter.props({ name: 'DXOS' }) },
478
+ },
479
+ // Bare text fragment becomes text search.
480
+ {
481
+ input: 'urgent',
482
+ expected: { filter: Filter.text('urgent') },
483
+ },
484
+ // Multiple bare fragments are AND-joined.
485
+ {
486
+ input: 'urgent review',
487
+ expected: { filter: Filter.and(Filter.text('urgent'), Filter.text('review')) },
488
+ },
489
+ // Mixed property + text fragment.
490
+ {
491
+ input: 'from:rich@dxos.org urgent',
492
+ expected: {
493
+ filter: Filter.and(Filter.props({ from: 'rich@dxos.org' }), Filter.text('urgent')),
494
+ },
495
+ },
496
+ // Tag + text fragment.
497
+ {
498
+ input: '#foo bar',
499
+ expected: { filter: Filter.and(Filter.tag('tag_1'), Filter.text('bar')) },
500
+ },
501
+ // Three fragments AND-joined.
502
+ {
503
+ input: 'a b c',
504
+ expected: {
505
+ filter: Filter.and(Filter.text('a'), Filter.text('b'), Filter.text('c')),
506
+ },
507
+ },
508
+ // URLs are text-searched, not promoted to property filters.
509
+ {
510
+ input: 'https://dxos.org',
511
+ expected: { filter: Filter.text('https://dxos.org') },
512
+ },
513
+ {
514
+ input: 'mailto:rich@dxos.org',
515
+ expected: { filter: Filter.text('mailto:rich@dxos.org') },
516
+ },
517
+ // Escapes decode back to the original input.
518
+ {
519
+ input: 'foo\\bar',
520
+ expected: { filter: Filter.text('foo\\bar') },
521
+ },
522
+ ];
523
+
524
+ tests.forEach(({ input, expected }) => {
525
+ const result = queryBuilder.build(input);
526
+ expect(result, JSON.stringify({ input, result, expected }, null, 2)).toEqual(expected);
527
+ });
528
+ });
416
529
  });
@@ -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',
@@ -244,14 +254,29 @@ class FilterClass implements Filter$.Any {
244
254
  }
245
255
 
246
256
  static updated(range: { after?: Date | number; before?: Date | number }): Filter$.Any {
247
- return FilterClass.#timeRangeFilter('updatedAt', range);
257
+ return FilterClass._timeRangeFilter('updatedAt', range);
248
258
  }
249
259
 
250
260
  static created(range: { after?: Date | number; before?: Date | number }): Filter$.Any {
251
- return FilterClass.#timeRangeFilter('createdAt', range);
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
+ });
252
277
  }
253
278
 
254
- static #timeRangeFilter(
279
+ private static _timeRangeFilter(
255
280
  field: 'updatedAt' | 'createdAt',
256
281
  range: { after?: Date | number; before?: Date | number },
257
282
  ): Filter$.Any {
@@ -436,7 +461,7 @@ class QueryClass implements Query$.Any {
436
461
  from: {
437
462
  _tag: 'scope',
438
463
  scope: {
439
- ...(options?.includeFeeds ? { allQueuesFromSpaces: true } : {}),
464
+ ...(options?.includeFeeds ? { allFeedsFromSpaces: true } : {}),
440
465
  },
441
466
  },
442
467
  });
@@ -558,6 +583,21 @@ class QueryClass implements Query$.Any {
558
583
  options,
559
584
  });
560
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
+ }
561
601
  }
562
602
 
563
603
  export const Query1: typeof Query$ = QueryClass;
@@ -568,13 +608,23 @@ const isRef = (obj: any): obj is Ref.Ref<any> => {
568
608
  return obj && typeof obj === 'object' && RefTypeId in obj;
569
609
  };
570
610
 
571
- const makeTypeDxn = (typename: string) => {
611
+ const makeTypeDXN = (typename: string) => {
572
612
  assertArgument(typeof typename === 'string', 'typename');
573
613
  assertArgument(!typename.startsWith('dxn:'), 'typename');
574
614
  return `dxn:type:${typename}`;
575
615
  };
576
616
 
577
- 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']);
578
628
 
579
629
  const _isScopeLike = (value: unknown): value is QueryAST.Scope => {
580
630
  if (typeof value !== 'object' || value === null || Array.isArray(value)) {
@@ -612,6 +662,8 @@ const prettyFilter = (filter: QueryAST.Filter): string => {
612
662
  return `Filter.text(${JSON.stringify(filter.text)})`;
613
663
  case 'tag':
614
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} })`;
615
667
  case 'timestamp':
616
668
  return `Filter.${filter.field}.${filter.operator}(${filter.value})`;
617
669
  case 'not':
@@ -674,6 +726,9 @@ const prettyQuery = (query: QueryAST.Query): string => {
674
726
  if (query.options.deleted !== undefined) {
675
727
  parts.push(`deleted: ${JSON.stringify(query.options.deleted)}`);
676
728
  }
729
+ if (query.options.debugLabel !== undefined) {
730
+ parts.push(`debugLabel: ${JSON.stringify(query.options.debugLabel)}`);
731
+ }
677
732
  return `${prettyQuery(query.query)}.options({ ${parts.join(', ')} })`;
678
733
  }
679
734
  case 'from': {
@@ -683,11 +738,11 @@ const prettyQuery = (query: QueryAST.Query): string => {
683
738
  if (scope.spaceIds !== undefined) {
684
739
  parts.push(`spaceIds: [${scope.spaceIds.join(', ')}]`);
685
740
  }
686
- if (scope.queues !== undefined) {
687
- parts.push(`queues: [${scope.queues.join(', ')}]`);
741
+ if (scope.feeds !== undefined) {
742
+ parts.push(`feeds: [${scope.feeds.join(', ')}]`);
688
743
  }
689
- if (scope.allQueuesFromSpaces !== undefined) {
690
- parts.push(`allQueuesFromSpaces: ${scope.allQueuesFromSpaces}`);
744
+ if (scope.allFeedsFromSpaces !== undefined) {
745
+ parts.push(`allFeedsFromSpaces: ${scope.allFeedsFromSpaces}`);
691
746
  }
692
747
  return `${prettyQuery(query.query)}.from({ ${parts.join(', ')} })`;
693
748
  }