@dxos/echo-query 0.8.4-main.fd6878d → 0.8.4-staging.60fe92afc8

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 (64) hide show
  1. package/LICENSE +102 -5
  2. package/README.md +2 -2
  3. package/dist/lib/neutral/index.mjs +918 -0
  4. package/dist/lib/neutral/index.mjs.map +7 -0
  5. package/dist/lib/neutral/meta.json +1 -0
  6. package/dist/query-lite/index.d.ts +10764 -0
  7. package/dist/query-lite/index.d.ts.map +1 -0
  8. package/dist/query-lite/index.js +584 -0
  9. package/dist/query-lite/index.js.map +1 -0
  10. package/dist/types/src/index.d.ts +2 -0
  11. package/dist/types/src/index.d.ts.map +1 -1
  12. package/dist/types/src/parser/gen/index.d.ts +8 -0
  13. package/dist/types/src/parser/gen/index.d.ts.map +1 -0
  14. package/dist/types/src/parser/gen/query.d.ts +3 -0
  15. package/dist/types/src/parser/gen/query.d.ts.map +1 -0
  16. package/dist/types/src/parser/gen/query.terms.d.ts +2 -0
  17. package/dist/types/src/parser/gen/query.terms.d.ts.map +1 -0
  18. package/dist/types/src/parser/index.d.ts +3 -0
  19. package/dist/types/src/parser/index.d.ts.map +1 -0
  20. package/dist/types/src/parser/query-builder.d.ts +89 -0
  21. package/dist/types/src/parser/query-builder.d.ts.map +1 -0
  22. package/dist/types/src/parser/query.test.d.ts +2 -0
  23. package/dist/types/src/parser/query.test.d.ts.map +1 -0
  24. package/dist/types/src/query-lite/index.d.ts +2 -0
  25. package/dist/types/src/query-lite/index.d.ts.map +1 -0
  26. package/dist/types/src/query-lite/query-lite.d.ts +8 -0
  27. package/dist/types/src/query-lite/query-lite.d.ts.map +1 -0
  28. package/dist/types/src/sandbox/index.d.ts +2 -0
  29. package/dist/types/src/sandbox/index.d.ts.map +1 -0
  30. package/dist/types/src/sandbox/query-sandbox.d.ts +21 -0
  31. package/dist/types/src/sandbox/query-sandbox.d.ts.map +1 -0
  32. package/dist/types/src/sandbox/query-sandbox.test.d.ts +2 -0
  33. package/dist/types/src/sandbox/query-sandbox.test.d.ts.map +1 -0
  34. package/dist/types/src/sandbox/quickjs.d.ts +8 -0
  35. package/dist/types/src/sandbox/quickjs.d.ts.map +1 -0
  36. package/dist/types/src/sandbox/quickjs.test.d.ts +2 -0
  37. package/dist/types/src/sandbox/quickjs.test.d.ts.map +1 -0
  38. package/dist/types/tsconfig.tsbuildinfo +1 -1
  39. package/package.json +27 -14
  40. package/src/env.d.ts +8 -0
  41. package/src/index.ts +3 -0
  42. package/src/parser/gen/index.ts +13 -0
  43. package/src/parser/gen/query.terms.ts +27 -0
  44. package/src/parser/gen/query.ts +18 -0
  45. package/src/parser/index.ts +6 -0
  46. package/src/parser/query-builder.ts +805 -0
  47. package/src/parser/query.grammar +130 -0
  48. package/src/parser/query.test.ts +536 -0
  49. package/src/query-lite/index.ts +5 -0
  50. package/src/query-lite/query-lite.ts +833 -0
  51. package/src/sandbox/index.ts +5 -0
  52. package/src/sandbox/query-sandbox.test.ts +54 -0
  53. package/src/sandbox/query-sandbox.ts +72 -0
  54. package/src/sandbox/quickjs.test.ts +67 -0
  55. package/src/sandbox/quickjs.ts +33 -0
  56. package/dist/lib/browser/index.mjs +0 -2
  57. package/dist/lib/browser/index.mjs.map +0 -7
  58. package/dist/lib/browser/meta.json +0 -1
  59. package/dist/lib/node-esm/index.mjs +0 -2
  60. package/dist/lib/node-esm/index.mjs.map +0 -7
  61. package/dist/lib/node-esm/meta.json +0 -1
  62. package/dist/types/src/search.test.d.ts +0 -2
  63. package/dist/types/src/search.test.d.ts.map +0 -1
  64. 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,536 @@
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
+ import { DXN } from '@dxos/keys';
10
+
11
+ import { QueryDSL } from './gen';
12
+ import { type BuildResult, QueryBuilder, normalizeInput } from './query-builder';
13
+
14
+ // TODO(burdon): Ref/Relation traversal.
15
+
16
+ describe('query', () => {
17
+ it('parse', ({ expect }) => {
18
+ const queryParser = QueryDSL.Parser.configure({ strict: true });
19
+
20
+ type Test = { input: string; expected: string[] };
21
+ const tests: Test[] = [
22
+ // Tags
23
+ {
24
+ input: '#foo',
25
+ expected: ['Query', 'Filter', 'TagFilter', 'Tag'],
26
+ },
27
+ {
28
+ input: '#foo AND #bar',
29
+ expected: ['Query', 'Filter', 'TagFilter', 'Tag', 'And', 'Filter', 'TagFilter', 'Tag'],
30
+ },
31
+ {
32
+ input: '#foo #bar',
33
+ expected: ['Query', 'Filter', 'TagFilter', 'Tag', 'Filter', 'TagFilter', 'Tag'],
34
+ },
35
+ // Text
36
+ {
37
+ input: '"foo"',
38
+ expected: ['Query', 'Filter', 'TextFilter', 'String'],
39
+ },
40
+ {
41
+ input: '"foo" OR "bar"',
42
+ expected: ['Query', 'Filter', 'TextFilter', 'String', 'Or', 'Filter', 'TextFilter', 'String'],
43
+ },
44
+ // Mixed
45
+ {
46
+ input: '#foo AND "bar"',
47
+ expected: ['Query', 'Filter', 'TagFilter', 'Tag', 'And', 'Filter', 'TextFilter', 'String'],
48
+ },
49
+ {
50
+ input: '#foo "bar"',
51
+ expected: ['Query', 'Filter', 'TagFilter', 'Tag', 'Filter', 'TextFilter', 'String'],
52
+ },
53
+ // Type
54
+ {
55
+ input: 'type:org.dxos.type.person',
56
+ expected: [
57
+ 'Query',
58
+ // type:org.dxos.type.person
59
+ 'Filter',
60
+ 'TypeFilter',
61
+ 'TypeKeyword',
62
+ ':',
63
+ 'Identifier',
64
+ ],
65
+ },
66
+ {
67
+ input: '{ name: "DXOS" }',
68
+ expected: [
69
+ 'Query',
70
+ // { name: "DXOS" }
71
+ 'Filter',
72
+ 'ObjectLiteral',
73
+ '{',
74
+ 'ObjectProperty',
75
+ 'Identifier',
76
+ ':',
77
+ 'Value',
78
+ 'String',
79
+ '}',
80
+ ],
81
+ },
82
+ {
83
+ input: '{ value: 100 }',
84
+ expected: [
85
+ 'Query',
86
+ // { value: 100 }
87
+ 'Filter',
88
+ 'ObjectLiteral',
89
+ '{',
90
+ 'ObjectProperty',
91
+ 'Identifier',
92
+ ':',
93
+ 'Value',
94
+ 'Number',
95
+ '}',
96
+ ],
97
+ },
98
+ {
99
+ input: '{ value: true }',
100
+ expected: [
101
+ 'Query',
102
+ // { value: true }
103
+ 'Filter',
104
+ 'ObjectLiteral',
105
+ '{',
106
+ 'ObjectProperty',
107
+ 'Identifier',
108
+ ':',
109
+ 'Value',
110
+ 'Boolean',
111
+ '}',
112
+ ],
113
+ },
114
+ {
115
+ input: 'type:org.dxos.type.person OR type:org.dxos.type.organization',
116
+ expected: [
117
+ 'Query',
118
+ // type:org.dxos.type.person
119
+ 'Filter',
120
+ 'TypeFilter',
121
+ 'TypeKeyword',
122
+ ':',
123
+ 'Identifier',
124
+ // OR
125
+ 'Or',
126
+ // type:org.dxos.type.organization
127
+ 'Filter',
128
+ 'TypeFilter',
129
+ 'TypeKeyword',
130
+ ':',
131
+ 'Identifier',
132
+ ],
133
+ },
134
+ {
135
+ input: '(type:org.dxos.type.person OR type:org.dxos.type.organization) AND { name: "DXOS" }',
136
+ expected: [
137
+ 'Query',
138
+ '(',
139
+ // type:org.dxos.type.person
140
+ 'Filter',
141
+ 'TypeFilter',
142
+ 'TypeKeyword',
143
+ ':',
144
+ 'Identifier',
145
+ // OR
146
+ 'Or',
147
+ // type:org.dxos.type.organization
148
+ 'Filter',
149
+ 'TypeFilter',
150
+ 'TypeKeyword',
151
+ ':',
152
+ 'Identifier',
153
+ ')',
154
+ 'And',
155
+ // { name: "DXOS" }
156
+ 'Filter',
157
+ 'ObjectLiteral',
158
+ '{',
159
+ 'ObjectProperty',
160
+ 'Identifier',
161
+ ':',
162
+ 'Value',
163
+ 'String',
164
+ '}',
165
+ ],
166
+ },
167
+ {
168
+ input: 'type:org.dxos.type.person -> type:org.dxos.type.organization',
169
+ expected: [
170
+ 'Query',
171
+ // type:org.dxos.type.person
172
+ 'Filter',
173
+ 'TypeFilter',
174
+ 'TypeKeyword',
175
+ ':',
176
+ 'Identifier',
177
+ 'Relation',
178
+ 'ArrowRight',
179
+ // type:org.dxos.type.organization
180
+ 'Filter',
181
+ 'TypeFilter',
182
+ 'TypeKeyword',
183
+ ':',
184
+ 'Identifier',
185
+ ],
186
+ },
187
+ {
188
+ input: 'type:org.dxos.type.organization <- type:org.dxos.type.person',
189
+ expected: [
190
+ 'Query',
191
+ // type:org.dxos.type.organization
192
+ 'Filter',
193
+ 'TypeFilter',
194
+ 'TypeKeyword',
195
+ ':',
196
+ 'Identifier',
197
+ 'Relation',
198
+ 'ArrowLeft',
199
+ // type:org.dxos.type.person
200
+ 'Filter',
201
+ 'TypeFilter',
202
+ 'TypeKeyword',
203
+ ':',
204
+ 'Identifier',
205
+ ],
206
+ },
207
+ {
208
+ // Persons for Organizations with name "DXOS"
209
+ // TODO(burdon): Filter relations.
210
+ input: '((type:org.dxos.type.organization AND { name: "DXOS" }) -> type:org.dxos.type.person)',
211
+ expected: [
212
+ 'Query',
213
+ '(',
214
+ '(',
215
+ 'Filter',
216
+ 'TypeFilter',
217
+ 'TypeKeyword',
218
+ ':',
219
+ 'Identifier',
220
+ 'And',
221
+ 'Filter',
222
+ 'ObjectLiteral',
223
+ '{',
224
+ 'ObjectProperty',
225
+ 'Identifier',
226
+ ':',
227
+ 'Value',
228
+ 'String',
229
+ '}',
230
+ ')',
231
+ 'Relation',
232
+ 'ArrowRight',
233
+ 'Filter',
234
+ 'TypeFilter',
235
+ 'TypeKeyword',
236
+ ':',
237
+ 'Identifier',
238
+ ')',
239
+ ],
240
+ },
241
+ {
242
+ input: 'type:org.dxos.type.person and { name: "DXOS" }',
243
+ expected: [
244
+ 'Query',
245
+ 'Filter',
246
+ 'TypeFilter',
247
+ 'TypeKeyword',
248
+ ':',
249
+ 'Identifier',
250
+ 'And',
251
+ 'Filter',
252
+ 'ObjectLiteral',
253
+ '{',
254
+ 'ObjectProperty',
255
+ 'Identifier',
256
+ ':',
257
+ 'Value',
258
+ 'String',
259
+ '}',
260
+ ],
261
+ },
262
+ {
263
+ input: 'x = ( type: org.dxos.type.person )',
264
+ expected: [
265
+ 'Query',
266
+ 'Assignment',
267
+ 'Identifier',
268
+ '=',
269
+ '(',
270
+ 'Filter',
271
+ 'TypeFilter',
272
+ 'TypeKeyword',
273
+ ':',
274
+ 'Identifier',
275
+ ')',
276
+ ],
277
+ },
278
+ ];
279
+
280
+ for (const { input, expected } of tests) {
281
+ let tree: Tree;
282
+ try {
283
+ tree = queryParser.parse(input);
284
+ } catch (err) {
285
+ console.error(new Error(`Failed to parse: ${input}`, { cause: err }));
286
+ continue;
287
+ }
288
+
289
+ const cursor = tree.cursor();
290
+ const result: string[] = [];
291
+ do {
292
+ result.push(cursor.node.name);
293
+ } while (cursor.next());
294
+ expect(result, input).toEqual(expected);
295
+ }
296
+ });
297
+
298
+ it('build', ({ expect }) => {
299
+ const queryBuilder = new QueryBuilder({
300
+ tag_1: Tag.make({ label: 'foo' }),
301
+ tag_2: Tag.make({ label: 'bar' }),
302
+ });
303
+
304
+ // TODO(burdon): Test "not"
305
+ type Test = { input: string; expected: BuildResult };
306
+ const tests: Test[] = [
307
+ // Types
308
+ {
309
+ input: 'type:org.dxos.type.person',
310
+ expected: {
311
+ filter: Filter.type(DXN.make('org.dxos.type.person')),
312
+ },
313
+ },
314
+ // Tags
315
+ {
316
+ input: '#foo',
317
+ expected: {
318
+ filter: Filter.tag('tag_1'),
319
+ },
320
+ },
321
+ {
322
+ input: '#foo AND #bar',
323
+ expected: {
324
+ filter: Filter.and(Filter.tag('tag_1'), Filter.tag('tag_2')),
325
+ },
326
+ },
327
+ {
328
+ input: '#foo #bar',
329
+ expected: {
330
+ filter: Filter.and(Filter.tag('tag_1'), Filter.tag('tag_2')),
331
+ },
332
+ },
333
+ // Text
334
+ {
335
+ input: '"test"',
336
+ expected: {
337
+ filter: Filter.text('test'),
338
+ },
339
+ },
340
+ // Mixed
341
+ {
342
+ input: '#foo "test"',
343
+ expected: {
344
+ filter: Filter.and(Filter.tag('tag_1'), Filter.text('test')),
345
+ },
346
+ },
347
+ // Props
348
+ {
349
+ input: '{ name: "DXOS" }',
350
+ expected: {
351
+ filter: Filter.props({ name: 'DXOS' }),
352
+ },
353
+ },
354
+ {
355
+ input: '{ value: 100 }',
356
+ expected: {
357
+ filter: Filter.props({ value: 100 }),
358
+ },
359
+ },
360
+ {
361
+ input: 'type:org.dxos.type.person OR type:org.dxos.type.organization',
362
+ expected: {
363
+ filter: Filter.or(
364
+ Filter.type(DXN.make('org.dxos.type.person')),
365
+ Filter.type(DXN.make('org.dxos.type.organization')),
366
+ ),
367
+ },
368
+ },
369
+ {
370
+ input: '(type:org.dxos.type.person OR type:org.dxos.type.organization) AND { name: "DXOS" }',
371
+ expected: {
372
+ filter: Filter.and(
373
+ Filter.or(
374
+ Filter.type(DXN.make('org.dxos.type.person')),
375
+ Filter.type(DXN.make('org.dxos.type.organization')),
376
+ ),
377
+ Filter.props({ name: 'DXOS' }),
378
+ ),
379
+ },
380
+ },
381
+ {
382
+ input: 'type:org.dxos.type.person and { name: "DXOS" }',
383
+ expected: {
384
+ filter: Filter.and(Filter.type(DXN.make('org.dxos.type.person')), Filter.props({ name: 'DXOS' })),
385
+ },
386
+ },
387
+ // Assignment
388
+ {
389
+ input: 'x = ( type:org.dxos.type.person )',
390
+ expected: {
391
+ name: 'x',
392
+ filter: Filter.type(DXN.make('org.dxos.type.person')),
393
+ },
394
+ },
395
+ {
396
+ input: 'x = ( #foo AND "bar" )',
397
+ expected: {
398
+ name: 'x',
399
+ filter: Filter.and(Filter.tag('tag_1'), Filter.text('bar')),
400
+ },
401
+ },
402
+ // TODO(burdon): Convert Query/Filter expr to AST.
403
+ // TODO(burdon): Person -> Organization (many-to-many relation).
404
+ // Get Research Note objects for Organization objects for Person objects with jobTitle.
405
+ //
406
+ // Cypher: MATCH (p:Person)-[:WorksAt]->(o:Organization)<-[:HasSubject]-(r:ResearchNote) WHERE p.jotTitle IS NOT NULL
407
+ // ((type:Person AND { jobTitle: "investor" }) -[:WorksAt]-> type:Organization) <-[:HasSubject]- type:ResearchNote
408
+ //
409
+ // {
410
+ // input: '',
411
+ // expected: Query.select(Filter.typename('org.dxos.type.person', { jobTitle: 'investor' }))
412
+ // .reference('organization')
413
+ // .targetOf(Relation.of('org.dxos.relation.hasSubject')) // TODO(burdon): Invert?
414
+ // .source(),
415
+ // },
416
+ ];
417
+
418
+ tests.forEach(({ input, expected }) => {
419
+ const result = queryBuilder.build(input);
420
+ expect(result, JSON.stringify({ input, result, expected }, null, 2)).toEqual(expected);
421
+ });
422
+ });
423
+
424
+ test('normalizeInput', ({ expect }) => {
425
+ type Test = { input: string; expected: string };
426
+ const tests: Test[] = [
427
+ { input: 'foo', expected: '"foo"' },
428
+ { input: 'foo bar', expected: '"foo" "bar"' },
429
+ { input: 'foo bar', expected: '"foo" "bar"' },
430
+ { input: '"already" bare', expected: '"already" "bare"' },
431
+ { input: 'from:rich@dxos.org', expected: 'from:"rich@dxos.org"' },
432
+ { input: 'from:rich@dxos.org urgent', expected: 'from:"rich@dxos.org" "urgent"' },
433
+ { input: 'name:DXOS', expected: 'name:"DXOS"' },
434
+ { input: 'count:42', expected: 'count:42' },
435
+ { input: 'active:true', expected: 'active:true' },
436
+ { input: 'value:null', expected: 'value:null' },
437
+ { input: 'name:"DXOS"', expected: 'name:"DXOS"' },
438
+ { input: 'type:org.dxos.type.person', expected: 'type:org.dxos.type.person' },
439
+ { input: '#tag', expected: '#tag' },
440
+ { input: '#tag foo', expected: '#tag "foo"' },
441
+ { input: 'foo AND bar', expected: '"foo" AND "bar"' },
442
+ { input: 'foo OR bar', expected: '"foo" OR "bar"' },
443
+ { input: 'NOT foo', expected: 'NOT "foo"' },
444
+ { input: '!foo', expected: '!"foo"' },
445
+ { input: '(foo bar)', expected: '("foo" "bar")' },
446
+ { input: 'x = ( foo )', expected: 'x = ( "foo" )' },
447
+ { input: '{ name: "DXOS" }', expected: '{ name: "DXOS" }' },
448
+ // Apostrophes inside barewords don't open a quoted string.
449
+ { input: "don't", expected: '"don\'t"' },
450
+ { input: "O'Connor", expected: '"O\'Connor"' },
451
+ { input: "don't worry", expected: '"don\'t" "worry"' },
452
+ // Unmatched closing brace/bracket — passed through, no infinite loop.
453
+ { input: 'foo}', expected: '"foo"}' },
454
+ { input: 'foo]bar', expected: '"foo"]"bar"' },
455
+ // Genuine single-quoted string still works.
456
+ { input: "'foo bar'", expected: "'foo bar'" },
457
+ // URLs are searched as text rather than auto-promoted to property filters.
458
+ { input: 'https://dxos.org', expected: '"https://dxos.org"' },
459
+ { input: 'http://example.com/foo', expected: '"http://example.com/foo"' },
460
+ { input: 'mailto:rich@dxos.org', expected: '"mailto:rich@dxos.org"' },
461
+ // Escapes round-trip: backslashes are escaped in the literal body.
462
+ { input: 'foo\\bar', expected: '"foo\\\\bar"' },
463
+ ];
464
+
465
+ for (const { input, expected } of tests) {
466
+ expect(normalizeInput(input), input).toEqual(expected);
467
+ }
468
+ });
469
+
470
+ test('build with property and text fragments', ({ expect }) => {
471
+ const queryBuilder = new QueryBuilder({
472
+ tag_1: Tag.make({ label: 'foo' }),
473
+ });
474
+
475
+ type Test = { input: string; expected: BuildResult };
476
+ const tests: Test[] = [
477
+ // Property filter from `key:value` text input.
478
+ {
479
+ input: 'from:rich@dxos.org',
480
+ expected: { filter: Filter.props({ from: 'rich@dxos.org' }) },
481
+ },
482
+ {
483
+ input: 'name:DXOS',
484
+ expected: { filter: Filter.props({ name: 'DXOS' }) },
485
+ },
486
+ // Bare text fragment becomes text search.
487
+ {
488
+ input: 'urgent',
489
+ expected: { filter: Filter.text('urgent') },
490
+ },
491
+ // Multiple bare fragments are AND-joined.
492
+ {
493
+ input: 'urgent review',
494
+ expected: { filter: Filter.and(Filter.text('urgent'), Filter.text('review')) },
495
+ },
496
+ // Mixed property + text fragment.
497
+ {
498
+ input: 'from:rich@dxos.org urgent',
499
+ expected: {
500
+ filter: Filter.and(Filter.props({ from: 'rich@dxos.org' }), Filter.text('urgent')),
501
+ },
502
+ },
503
+ // Tag + text fragment.
504
+ {
505
+ input: '#foo bar',
506
+ expected: { filter: Filter.and(Filter.tag('tag_1'), Filter.text('bar')) },
507
+ },
508
+ // Three fragments AND-joined.
509
+ {
510
+ input: 'a b c',
511
+ expected: {
512
+ filter: Filter.and(Filter.text('a'), Filter.text('b'), Filter.text('c')),
513
+ },
514
+ },
515
+ // URLs are text-searched, not promoted to property filters.
516
+ {
517
+ input: 'https://dxos.org',
518
+ expected: { filter: Filter.text('https://dxos.org') },
519
+ },
520
+ {
521
+ input: 'mailto:rich@dxos.org',
522
+ expected: { filter: Filter.text('mailto:rich@dxos.org') },
523
+ },
524
+ // Escapes decode back to the original input.
525
+ {
526
+ input: 'foo\\bar',
527
+ expected: { filter: Filter.text('foo\\bar') },
528
+ },
529
+ ];
530
+
531
+ tests.forEach(({ input, expected }) => {
532
+ const result = queryBuilder.build(input);
533
+ expect(result, JSON.stringify({ input, result, expected }, null, 2)).toEqual(expected);
534
+ });
535
+ });
536
+ });
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './query-lite';