@dxos/echo-query 0.8.4-main.dedc0f3 → 0.8.4-main.e00bdcdb52

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.
Files changed (63) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/neutral/index.mjs +917 -0
  3. package/dist/lib/neutral/index.mjs.map +7 -0
  4. package/dist/lib/neutral/meta.json +1 -0
  5. package/dist/query-lite/index.d.ts +9975 -0
  6. package/dist/query-lite/index.d.ts.map +1 -0
  7. package/dist/query-lite/index.js +548 -0
  8. package/dist/query-lite/index.js.map +1 -0
  9. package/dist/types/src/index.d.ts +2 -0
  10. package/dist/types/src/index.d.ts.map +1 -1
  11. package/dist/types/src/parser/gen/index.d.ts +8 -0
  12. package/dist/types/src/parser/gen/index.d.ts.map +1 -0
  13. package/dist/types/src/parser/gen/query.d.ts +3 -0
  14. package/dist/types/src/parser/gen/query.d.ts.map +1 -0
  15. package/dist/types/src/parser/gen/query.terms.d.ts +2 -0
  16. package/dist/types/src/parser/gen/query.terms.d.ts.map +1 -0
  17. package/dist/types/src/parser/index.d.ts +3 -0
  18. package/dist/types/src/parser/index.d.ts.map +1 -0
  19. package/dist/types/src/parser/query-builder.d.ts +89 -0
  20. package/dist/types/src/parser/query-builder.d.ts.map +1 -0
  21. package/dist/types/src/parser/query.test.d.ts +2 -0
  22. package/dist/types/src/parser/query.test.d.ts.map +1 -0
  23. package/dist/types/src/query-lite/index.d.ts +2 -0
  24. package/dist/types/src/query-lite/index.d.ts.map +1 -0
  25. package/dist/types/src/query-lite/query-lite.d.ts +8 -0
  26. package/dist/types/src/query-lite/query-lite.d.ts.map +1 -0
  27. package/dist/types/src/sandbox/index.d.ts +2 -0
  28. package/dist/types/src/sandbox/index.d.ts.map +1 -0
  29. package/dist/types/src/sandbox/query-sandbox.d.ts +21 -0
  30. package/dist/types/src/sandbox/query-sandbox.d.ts.map +1 -0
  31. package/dist/types/src/sandbox/query-sandbox.test.d.ts +2 -0
  32. package/dist/types/src/sandbox/query-sandbox.test.d.ts.map +1 -0
  33. package/dist/types/src/sandbox/quickjs.d.ts +8 -0
  34. package/dist/types/src/sandbox/quickjs.d.ts.map +1 -0
  35. package/dist/types/src/sandbox/quickjs.test.d.ts +2 -0
  36. package/dist/types/src/sandbox/quickjs.test.d.ts.map +1 -0
  37. package/dist/types/tsconfig.tsbuildinfo +1 -1
  38. package/package.json +29 -13
  39. package/src/env.d.ts +8 -0
  40. package/src/index.ts +3 -0
  41. package/src/parser/gen/index.ts +13 -0
  42. package/src/parser/gen/query.terms.ts +27 -0
  43. package/src/parser/gen/query.ts +18 -0
  44. package/src/parser/index.ts +6 -0
  45. package/src/parser/query-builder.ts +799 -0
  46. package/src/parser/query.grammar +130 -0
  47. package/src/parser/query.test.ts +529 -0
  48. package/src/query-lite/index.ts +5 -0
  49. package/src/query-lite/query-lite.ts +744 -0
  50. package/src/sandbox/index.ts +5 -0
  51. package/src/sandbox/query-sandbox.test.ts +53 -0
  52. package/src/sandbox/query-sandbox.ts +72 -0
  53. package/src/sandbox/quickjs.test.ts +67 -0
  54. package/src/sandbox/quickjs.ts +33 -0
  55. package/dist/lib/browser/index.mjs +0 -2
  56. package/dist/lib/browser/index.mjs.map +0 -7
  57. package/dist/lib/browser/meta.json +0 -1
  58. package/dist/lib/node-esm/index.mjs +0 -2
  59. package/dist/lib/node-esm/index.mjs.map +0 -7
  60. package/dist/lib/node-esm/meta.json +0 -1
  61. package/dist/types/src/search.test.d.ts +0 -2
  62. package/dist/types/src/search.test.d.ts.map +0 -1
  63. package/src/search.test.ts +0 -49
@@ -0,0 +1,130 @@
1
+ //
2
+ // lezer Query grammar
3
+ //
4
+
5
+ @top Query { expression }
6
+
7
+ expression {
8
+ Assignment |
9
+ Filter |
10
+ !Not Not expression |
11
+ expression !ImplicitAnd expression |
12
+ expression !And And expression |
13
+ expression !Or Or expression |
14
+ expression !Relation Relation expression |
15
+ "(" expression ")"
16
+ }
17
+
18
+ Assignment {
19
+ Identifier "=" "(" expression ")"
20
+ }
21
+
22
+ Not {
23
+ @specialize<Identifier, "NOT" | "not" | "!">
24
+ }
25
+ And {
26
+ @specialize<Identifier, "AND" | "and">
27
+ }
28
+ Or {
29
+ @specialize<Identifier, "OR" | "or">
30
+ }
31
+
32
+ Relation {
33
+ ArrowRight | ArrowLeft
34
+ }
35
+
36
+ //
37
+ // Filters
38
+ //
39
+
40
+ Filter {
41
+ TagFilter |
42
+ TextFilter |
43
+ TypeFilter |
44
+ PropertyFilter |
45
+ ObjectLiteral
46
+ }
47
+
48
+ TagFilter {
49
+ Tag
50
+ }
51
+
52
+ TextFilter {
53
+ String
54
+ }
55
+
56
+ TypeFilter {
57
+ @specialize[@name=TypeKeyword]<Identifier, "type"> ":" Identifier
58
+ }
59
+
60
+ PropertyFilter {
61
+ PropertyPath ":" Value
62
+ }
63
+
64
+ PropertyPath {
65
+ Identifier ("." Identifier)*
66
+ }
67
+
68
+ ObjectLiteral {
69
+ "{" (ObjectProperty ("," ObjectProperty)*)? "}"
70
+ }
71
+
72
+ ObjectProperty {
73
+ Identifier ":" Value
74
+ }
75
+
76
+ ArrayLiteral {
77
+ "[" (Value ("," Value)*)? "]"
78
+ }
79
+
80
+ Value {
81
+ String |
82
+ Number |
83
+ Boolean |
84
+ Null |
85
+ ObjectLiteral |
86
+ ArrayLiteral
87
+ }
88
+
89
+ @tokens {
90
+ // Supports variables and DXNs
91
+ Identifier {
92
+ $[a-zA-Z_]$[a-zA-Z0-9_./\-]*
93
+ }
94
+
95
+ Tag {
96
+ "#" $[a-zA-Z0-9_\-]+
97
+ }
98
+
99
+ String {
100
+ '"' (!["\\] | "\\" _)* '"' |
101
+ "'" (!['\\] | "\\" _)* "'"
102
+ }
103
+
104
+ Number {
105
+ "-"? @digit+ ("." @digit+)? (("e" | "E") ("+" | "-")? @digit+)?
106
+ }
107
+
108
+ Boolean { "true" | "false" }
109
+
110
+ Null { "null" }
111
+
112
+ ArrowRight { "->" }
113
+ ArrowLeft { "<-" }
114
+
115
+ space { @whitespace+ }
116
+
117
+ "{" "}" "[" "]" "(" ")"
118
+ ":" "," "." "="
119
+ }
120
+
121
+ @skip { space }
122
+
123
+ @precedence {
124
+ Not @right,
125
+ ImplicitAnd @left,
126
+ And @left,
127
+ Or @left,
128
+ Relation @left,
129
+ Assignment @right
130
+ }
@@ -0,0 +1,529 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Tree } from '@lezer/common';
6
+ import { describe, it, test } from 'vitest';
7
+
8
+ import { Filter, Tag } from '@dxos/echo';
9
+
10
+ import { QueryDSL } from './gen';
11
+ import { type BuildResult, QueryBuilder, normalizeInput } from './query-builder';
12
+
13
+ // TODO(burdon): Ref/Relation traversal.
14
+
15
+ describe('query', () => {
16
+ it('parse', ({ expect }) => {
17
+ const queryParser = QueryDSL.Parser.configure({ strict: true });
18
+
19
+ type Test = { input: string; expected: string[] };
20
+ const tests: Test[] = [
21
+ // Tags
22
+ {
23
+ input: '#foo',
24
+ expected: ['Query', 'Filter', 'TagFilter', 'Tag'],
25
+ },
26
+ {
27
+ input: '#foo AND #bar',
28
+ expected: ['Query', 'Filter', 'TagFilter', 'Tag', 'And', 'Filter', 'TagFilter', 'Tag'],
29
+ },
30
+ {
31
+ input: '#foo #bar',
32
+ expected: ['Query', 'Filter', 'TagFilter', 'Tag', 'Filter', 'TagFilter', 'Tag'],
33
+ },
34
+ // Text
35
+ {
36
+ input: '"foo"',
37
+ expected: ['Query', 'Filter', 'TextFilter', 'String'],
38
+ },
39
+ {
40
+ input: '"foo" OR "bar"',
41
+ expected: ['Query', 'Filter', 'TextFilter', 'String', 'Or', 'Filter', 'TextFilter', 'String'],
42
+ },
43
+ // Mixed
44
+ {
45
+ input: '#foo AND "bar"',
46
+ expected: ['Query', 'Filter', 'TagFilter', 'Tag', 'And', 'Filter', 'TextFilter', 'String'],
47
+ },
48
+ {
49
+ input: '#foo "bar"',
50
+ expected: ['Query', 'Filter', 'TagFilter', 'Tag', 'Filter', 'TextFilter', 'String'],
51
+ },
52
+ // Type
53
+ {
54
+ input: 'type:org.dxos.type.person',
55
+ expected: [
56
+ 'Query',
57
+ // type:org.dxos.type.person
58
+ 'Filter',
59
+ 'TypeFilter',
60
+ 'TypeKeyword',
61
+ ':',
62
+ 'Identifier',
63
+ ],
64
+ },
65
+ {
66
+ input: '{ name: "DXOS" }',
67
+ expected: [
68
+ 'Query',
69
+ // { name: "DXOS" }
70
+ 'Filter',
71
+ 'ObjectLiteral',
72
+ '{',
73
+ 'ObjectProperty',
74
+ 'Identifier',
75
+ ':',
76
+ 'Value',
77
+ 'String',
78
+ '}',
79
+ ],
80
+ },
81
+ {
82
+ input: '{ value: 100 }',
83
+ expected: [
84
+ 'Query',
85
+ // { value: 100 }
86
+ 'Filter',
87
+ 'ObjectLiteral',
88
+ '{',
89
+ 'ObjectProperty',
90
+ 'Identifier',
91
+ ':',
92
+ 'Value',
93
+ 'Number',
94
+ '}',
95
+ ],
96
+ },
97
+ {
98
+ input: '{ value: true }',
99
+ expected: [
100
+ 'Query',
101
+ // { value: true }
102
+ 'Filter',
103
+ 'ObjectLiteral',
104
+ '{',
105
+ 'ObjectProperty',
106
+ 'Identifier',
107
+ ':',
108
+ 'Value',
109
+ 'Boolean',
110
+ '}',
111
+ ],
112
+ },
113
+ {
114
+ input: 'type:org.dxos.type.person OR type:org.dxos.type.organization',
115
+ expected: [
116
+ 'Query',
117
+ // type:org.dxos.type.person
118
+ 'Filter',
119
+ 'TypeFilter',
120
+ 'TypeKeyword',
121
+ ':',
122
+ 'Identifier',
123
+ // OR
124
+ 'Or',
125
+ // type:org.dxos.type.organization
126
+ 'Filter',
127
+ 'TypeFilter',
128
+ 'TypeKeyword',
129
+ ':',
130
+ 'Identifier',
131
+ ],
132
+ },
133
+ {
134
+ input: '(type:org.dxos.type.person OR type:org.dxos.type.organization) AND { name: "DXOS" }',
135
+ expected: [
136
+ 'Query',
137
+ '(',
138
+ // type:org.dxos.type.person
139
+ 'Filter',
140
+ 'TypeFilter',
141
+ 'TypeKeyword',
142
+ ':',
143
+ 'Identifier',
144
+ // OR
145
+ 'Or',
146
+ // type:org.dxos.type.organization
147
+ 'Filter',
148
+ 'TypeFilter',
149
+ 'TypeKeyword',
150
+ ':',
151
+ 'Identifier',
152
+ ')',
153
+ 'And',
154
+ // { name: "DXOS" }
155
+ 'Filter',
156
+ 'ObjectLiteral',
157
+ '{',
158
+ 'ObjectProperty',
159
+ 'Identifier',
160
+ ':',
161
+ 'Value',
162
+ 'String',
163
+ '}',
164
+ ],
165
+ },
166
+ {
167
+ input: 'type:org.dxos.type.person -> type:org.dxos.type.organization',
168
+ expected: [
169
+ 'Query',
170
+ // type:org.dxos.type.person
171
+ 'Filter',
172
+ 'TypeFilter',
173
+ 'TypeKeyword',
174
+ ':',
175
+ 'Identifier',
176
+ 'Relation',
177
+ 'ArrowRight',
178
+ // type:org.dxos.type.organization
179
+ 'Filter',
180
+ 'TypeFilter',
181
+ 'TypeKeyword',
182
+ ':',
183
+ 'Identifier',
184
+ ],
185
+ },
186
+ {
187
+ input: 'type:org.dxos.type.organization <- type:org.dxos.type.person',
188
+ expected: [
189
+ 'Query',
190
+ // type:org.dxos.type.organization
191
+ 'Filter',
192
+ 'TypeFilter',
193
+ 'TypeKeyword',
194
+ ':',
195
+ 'Identifier',
196
+ 'Relation',
197
+ 'ArrowLeft',
198
+ // type:org.dxos.type.person
199
+ 'Filter',
200
+ 'TypeFilter',
201
+ 'TypeKeyword',
202
+ ':',
203
+ 'Identifier',
204
+ ],
205
+ },
206
+ {
207
+ // Persons for Organizations with name "DXOS"
208
+ // TODO(burdon): Filter relations.
209
+ input: '((type:org.dxos.type.organization AND { name: "DXOS" }) -> type:org.dxos.type.person)',
210
+ expected: [
211
+ 'Query',
212
+ '(',
213
+ '(',
214
+ 'Filter',
215
+ 'TypeFilter',
216
+ 'TypeKeyword',
217
+ ':',
218
+ 'Identifier',
219
+ 'And',
220
+ 'Filter',
221
+ 'ObjectLiteral',
222
+ '{',
223
+ 'ObjectProperty',
224
+ 'Identifier',
225
+ ':',
226
+ 'Value',
227
+ 'String',
228
+ '}',
229
+ ')',
230
+ 'Relation',
231
+ 'ArrowRight',
232
+ 'Filter',
233
+ 'TypeFilter',
234
+ 'TypeKeyword',
235
+ ':',
236
+ 'Identifier',
237
+ ')',
238
+ ],
239
+ },
240
+ {
241
+ input: 'type:org.dxos.type.person and { name: "DXOS" }',
242
+ expected: [
243
+ 'Query',
244
+ 'Filter',
245
+ 'TypeFilter',
246
+ 'TypeKeyword',
247
+ ':',
248
+ 'Identifier',
249
+ 'And',
250
+ 'Filter',
251
+ 'ObjectLiteral',
252
+ '{',
253
+ 'ObjectProperty',
254
+ 'Identifier',
255
+ ':',
256
+ 'Value',
257
+ 'String',
258
+ '}',
259
+ ],
260
+ },
261
+ {
262
+ input: 'x = ( type: org.dxos.type.person )',
263
+ expected: [
264
+ 'Query',
265
+ 'Assignment',
266
+ 'Identifier',
267
+ '=',
268
+ '(',
269
+ 'Filter',
270
+ 'TypeFilter',
271
+ 'TypeKeyword',
272
+ ':',
273
+ 'Identifier',
274
+ ')',
275
+ ],
276
+ },
277
+ ];
278
+
279
+ for (const { input, expected } of tests) {
280
+ let tree: Tree;
281
+ try {
282
+ tree = queryParser.parse(input);
283
+ } catch (err) {
284
+ console.error(new Error(`Failed to parse: ${input}`, { cause: err }));
285
+ continue;
286
+ }
287
+
288
+ const cursor = tree.cursor();
289
+ const result: string[] = [];
290
+ do {
291
+ result.push(cursor.node.name);
292
+ } while (cursor.next());
293
+ expect(result, input).toEqual(expected);
294
+ }
295
+ });
296
+
297
+ it('build', ({ expect }) => {
298
+ const queryBuilder = new QueryBuilder({
299
+ tag_1: Tag.make({ label: 'foo' }),
300
+ tag_2: Tag.make({ label: 'bar' }),
301
+ });
302
+
303
+ // TODO(burdon): Test "not"
304
+ type Test = { input: string; expected: BuildResult };
305
+ const tests: Test[] = [
306
+ // Types
307
+ {
308
+ input: 'type:org.dxos.type.person',
309
+ expected: {
310
+ filter: Filter.typename('org.dxos.type.person'),
311
+ },
312
+ },
313
+ // Tags
314
+ {
315
+ input: '#foo',
316
+ expected: {
317
+ filter: Filter.tag('tag_1'),
318
+ },
319
+ },
320
+ {
321
+ input: '#foo AND #bar',
322
+ expected: {
323
+ filter: Filter.and(Filter.tag('tag_1'), Filter.tag('tag_2')),
324
+ },
325
+ },
326
+ {
327
+ input: '#foo #bar',
328
+ expected: {
329
+ filter: Filter.and(Filter.tag('tag_1'), Filter.tag('tag_2')),
330
+ },
331
+ },
332
+ // Text
333
+ {
334
+ input: '"test"',
335
+ expected: {
336
+ filter: Filter.text('test'),
337
+ },
338
+ },
339
+ // Mixed
340
+ {
341
+ input: '#foo "test"',
342
+ expected: {
343
+ filter: Filter.and(Filter.tag('tag_1'), Filter.text('test')),
344
+ },
345
+ },
346
+ // Props
347
+ {
348
+ input: '{ name: "DXOS" }',
349
+ expected: {
350
+ filter: Filter.props({ name: 'DXOS' }),
351
+ },
352
+ },
353
+ {
354
+ input: '{ value: 100 }',
355
+ expected: {
356
+ filter: Filter.props({ value: 100 }),
357
+ },
358
+ },
359
+ {
360
+ input: 'type:org.dxos.type.person OR type:org.dxos.type.organization',
361
+ expected: {
362
+ filter: Filter.or(Filter.typename('org.dxos.type.person'), Filter.typename('org.dxos.type.organization')),
363
+ },
364
+ },
365
+ {
366
+ input: '(type:org.dxos.type.person OR type:org.dxos.type.organization) AND { name: "DXOS" }',
367
+ expected: {
368
+ filter: Filter.and(
369
+ Filter.or(Filter.typename('org.dxos.type.person'), Filter.typename('org.dxos.type.organization')),
370
+ Filter.props({ name: 'DXOS' }),
371
+ ),
372
+ },
373
+ },
374
+ {
375
+ input: 'type:org.dxos.type.person and { name: "DXOS" }',
376
+ expected: {
377
+ filter: Filter.and(Filter.typename('org.dxos.type.person'), Filter.props({ name: 'DXOS' })),
378
+ },
379
+ },
380
+ // Assignment
381
+ {
382
+ input: 'x = ( type:org.dxos.type.person )',
383
+ expected: {
384
+ name: 'x',
385
+ filter: Filter.typename('org.dxos.type.person'),
386
+ },
387
+ },
388
+ {
389
+ input: 'x = ( #foo AND "bar" )',
390
+ expected: {
391
+ name: 'x',
392
+ filter: Filter.and(Filter.tag('tag_1'), Filter.text('bar')),
393
+ },
394
+ },
395
+ // TODO(burdon): Convert Query/Filter expr to AST.
396
+ // TODO(burdon): Person -> Organization (many-to-many relation).
397
+ // Get Research Note objects for Organization objects for Person objects with jobTitle.
398
+ //
399
+ // Cypher: MATCH (p:Person)-[:WorksAt]->(o:Organization)<-[:HasSubject]-(r:ResearchNote) WHERE p.jotTitle IS NOT NULL
400
+ // ((type:Person AND { jobTitle: "investor" }) -[:WorksAt]-> type:Organization) <-[:HasSubject]- type:ResearchNote
401
+ //
402
+ // {
403
+ // input: '',
404
+ // expected: Query.select(Filter.typename('org.dxos.type.person', { jobTitle: 'investor' }))
405
+ // .reference('organization')
406
+ // .targetOf(Relation.of('org.dxos.relation.hasSubject')) // TODO(burdon): Invert?
407
+ // .source(),
408
+ // },
409
+ ];
410
+
411
+ tests.forEach(({ input, expected }) => {
412
+ const result = queryBuilder.build(input);
413
+ expect(result, JSON.stringify({ input, result, expected }, null, 2)).toEqual(expected);
414
+ });
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
+ });
529
+ });
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './query-lite';