@dxos/echo-query 0.8.4-main.a4bbb77 → 0.8.4-main.ae835ea

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 (41) hide show
  1. package/README.md +1 -1
  2. package/dist/lib/browser/index.mjs +128 -70
  3. package/dist/lib/browser/index.mjs.map +3 -3
  4. package/dist/lib/browser/meta.json +1 -1
  5. package/dist/lib/node-esm/index.mjs +128 -70
  6. package/dist/lib/node-esm/index.mjs.map +3 -3
  7. package/dist/lib/node-esm/meta.json +1 -1
  8. package/dist/query-lite/index.js +387 -0
  9. package/dist/types/src/parser/gen/query.terms.d.ts +1 -1
  10. package/dist/types/src/parser/gen/query.terms.d.ts.map +1 -1
  11. package/dist/types/src/parser/query-builder.d.ts +14 -5
  12. package/dist/types/src/parser/query-builder.d.ts.map +1 -1
  13. package/dist/types/src/query-lite/index.d.ts +2 -0
  14. package/dist/types/src/query-lite/index.d.ts.map +1 -0
  15. package/dist/types/src/query-lite/query-lite.d.ts +8 -0
  16. package/dist/types/src/query-lite/query-lite.d.ts.map +1 -0
  17. package/dist/types/src/sandbox/query-sandbox.d.ts +21 -0
  18. package/dist/types/src/sandbox/query-sandbox.d.ts.map +1 -0
  19. package/dist/types/src/sandbox/query-sandbox.test.d.ts +2 -0
  20. package/dist/types/src/sandbox/query-sandbox.test.d.ts.map +1 -0
  21. package/dist/types/src/sandbox/quickjs.d.ts +8 -0
  22. package/dist/types/src/sandbox/quickjs.d.ts.map +1 -0
  23. package/dist/types/src/sandbox/quickjs.test.d.ts +2 -0
  24. package/dist/types/src/sandbox/quickjs.test.d.ts.map +1 -0
  25. package/dist/types/tsconfig.tsbuildinfo +1 -1
  26. package/package.json +16 -7
  27. package/src/env.d.ts +8 -0
  28. package/src/parser/gen/query.terms.ts +24 -23
  29. package/src/parser/gen/query.ts +8 -8
  30. package/src/parser/query-builder.ts +90 -21
  31. package/src/parser/query.grammar +8 -2
  32. package/src/parser/query.test.ts +77 -21
  33. package/src/query-lite/index.ts +5 -0
  34. package/src/query-lite/query-lite.ts +467 -0
  35. package/src/sandbox/query-sandbox.test.ts +53 -0
  36. package/src/sandbox/query-sandbox.ts +72 -0
  37. package/src/sandbox/quickjs.test.ts +67 -0
  38. package/src/sandbox/quickjs.ts +34 -0
  39. package/dist/types/src/search.test.d.ts +0 -2
  40. package/dist/types/src/search.test.d.ts.map +0 -1
  41. package/src/search.test.ts +0 -49
package/package.json CHANGED
@@ -1,13 +1,16 @@
1
1
  {
2
2
  "name": "@dxos/echo-query",
3
- "version": "0.8.4-main.a4bbb77",
3
+ "version": "0.8.4-main.ae835ea",
4
4
  "description": "ECHO queries.",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
7
7
  "license": "MIT",
8
8
  "author": "info@dxos.org",
9
- "sideEffects": true,
9
+ "sideEffects": false,
10
10
  "type": "module",
11
+ "imports": {
12
+ "#query-lite": "./dist/query-lite/index.js"
13
+ },
11
14
  "exports": {
12
15
  ".": {
13
16
  "source": "./src/index.ts",
@@ -29,14 +32,20 @@
29
32
  "@lezer/lezer": "^1.1.2",
30
33
  "@lezer/lr": "^1.4.2",
31
34
  "@orama/orama": "^3.1.7",
32
- "@dxos/echo": "0.8.4-main.a4bbb77",
33
- "@dxos/node-std": "0.8.4-main.a4bbb77",
34
- "@dxos/util": "0.8.4-main.a4bbb77"
35
+ "@dxos/debug": "0.8.4-main.ae835ea",
36
+ "@dxos/context": "0.8.4-main.ae835ea",
37
+ "@dxos/echo": "0.8.4-main.ae835ea",
38
+ "@dxos/echo-protocol": "0.8.4-main.ae835ea",
39
+ "@dxos/errors": "0.8.4-main.ae835ea",
40
+ "@dxos/node-std": "0.8.4-main.ae835ea",
41
+ "@dxos/invariant": "0.8.4-main.ae835ea",
42
+ "@dxos/util": "0.8.4-main.ae835ea",
43
+ "@dxos/vendor-quickjs": "0.8.4-main.ae835ea"
35
44
  },
36
45
  "devDependencies": {
37
46
  "@lezer/generator": "^1.7.1",
38
- "@dxos/echo-generator": "0.8.4-main.a4bbb77",
39
- "@dxos/random": "0.8.4-main.a4bbb77"
47
+ "@dxos/echo-generator": "0.8.4-main.ae835ea",
48
+ "@dxos/random": "0.8.4-main.ae835ea"
40
49
  },
41
50
  "publishConfig": {
42
51
  "access": "public"
package/src/env.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ declare module '#query-lite?raw' {
6
+ const code: string;
7
+ export default code;
8
+ }
@@ -1,26 +1,27 @@
1
1
  // This file was generated by lezer-generator. You probably shouldn't edit it.
2
2
  export const
3
3
  Query = 1,
4
- Filter = 2,
5
- TagFilter = 3,
6
- Tag = 4,
7
- TextFilter = 5,
8
- String = 6,
9
- TypeFilter = 7,
10
- Identifier = 8,
11
- TypeKeyword = 9,
12
- PropertyFilter = 11,
13
- PropertyPath = 12,
14
- Value = 14,
15
- Number = 15,
16
- Boolean = 16,
17
- Null = 17,
18
- ObjectLiteral = 18,
19
- ObjectProperty = 20,
20
- ArrayLiteral = 23,
21
- Not = 26,
22
- And = 27,
23
- Or = 28,
24
- Relation = 29,
25
- ArrowRight = 30,
26
- ArrowLeft = 31
4
+ Assignment = 2,
5
+ Identifier = 3,
6
+ Filter = 7,
7
+ TagFilter = 8,
8
+ Tag = 9,
9
+ TextFilter = 10,
10
+ String = 11,
11
+ TypeFilter = 12,
12
+ TypeKeyword = 13,
13
+ PropertyFilter = 15,
14
+ PropertyPath = 16,
15
+ Value = 18,
16
+ Number = 19,
17
+ Boolean = 20,
18
+ Null = 21,
19
+ ObjectLiteral = 22,
20
+ ObjectProperty = 24,
21
+ ArrayLiteral = 27,
22
+ Not = 30,
23
+ And = 31,
24
+ Or = 32,
25
+ Relation = 33,
26
+ ArrowRight = 34,
27
+ ArrowLeft = 35
@@ -1,18 +1,18 @@
1
1
  // This file was generated by lezer-generator. You probably shouldn't edit it.
2
2
  import {LRParser} from "@lezer/lr"
3
- const spec_Identifier = {__proto__:null,type:18, NOT:82, not:82, "!":82, AND:84, and:84, OR:86, or:86}
3
+ const spec_Identifier = {__proto__:null,type:26, NOT:86, not:86, "!":86, AND:88, and:88, OR:90, or:90}
4
4
  export const parser = LRParser.deserialize({
5
5
  version: 14,
6
- states: "(QOVQPOOOOQO'#C_'#C_OOQO'#Ca'#CaOnQPO'#CcOsQPO'#ChO{QPO'#CnO!TQPO'#CgOOQO'#C^'#C^OOQO'#Cv'#CvOOQO'#DU'#DUOVQPO'#DUQ!YQPOOOVQPO'#DUO!jQPO,58}O!oQPO'#DOO!tQPO,59SO!|QPO'#CpOOQO,59Y,59YO#RQPO,59YO#ZQQO,59RO#oQPO,59pOOQO'#Cw'#CwOOQO'#Cx'#CxOOQO'#Cy'#CyOVQPO,59pOVQPO,59pOVQPO,59pO$jQPO,59pOOQO1G.i1G.iOOQO,59j,59jOOQO-E6|-E6|O#ZQQO,59[O$}QPO'#DPO%SQPO1G.tOOQO1G.t1G.tO%[QQO'#CsOOQO'#Cj'#CjOOQO1G.m1G.mO%cQPO1G/[O%yQPO1G/[O&aQPO1G/[OOQO1G/[1G/[OOQO1G.v1G.vOOQO,59k,59kOOQO-E6}-E6}OOQO7+$`7+$`OOQO,59_,59_O&wQPO,59_O#ZQQO'#DQO'PQPO1G.yOOQO1G.y1G.yOOQO,59l,59lOOQO-E7O-E7OOOQO7+$e7+$e",
7
- stateData: "'X~OwOS~OSPOUQOWSOXROcTOp[OyWO~OY]O~O]^OY[X~OW`OfaO~OYcO~OngOogOzeO{fO~PVOWlO~OWmO~O]^OY[a~OYoO~OepOfrO~OUtO_tO`tOatOcTOhsO~OyWOSxaUxaWxaXxacxanxaoxapxauxazxa{xaqxa~OngOogOqyOzeO{fO~PVOW`O~OepOf}O~Oi!OO~P#ZOnxioxiuxizxi{xiqxi~PVOzeOnxioxiuxi{xiqxi~PVOzeO{fOnxioxiuxiqxi~PVOe!QOi!SO~Oe!QOi!VO~O",
8
- goto: "%TyPPz!XP!XP!XPPP!X!fP!sPPP#PP#dPP#^PP#j#x$O$TPPPP$X$_$ePPP$kgXOYZ[hijkvwxgVOYZ[hijkvwxgUOYZ[hijkvwxQucQzoQ!PsR!T!QfVOYZ[hijkvwxXtcos!QQbTR{piYOYZ[dhijkvwxXhZkwxViZkxTjZkQ_SRn_QqbR|qQ!R!PR!U!RQZO^dYZdkvwxQk[QvhQwiRxj",
9
- nodeNames: "⚠ Query Filter TagFilter Tag TextFilter String TypeFilter Identifier TypeKeyword : PropertyFilter PropertyPath . Value Number Boolean Null ObjectLiteral { ObjectProperty , } ArrayLiteral [ ] Not And Or Relation ArrowRight ArrowLeft ( )",
10
- maxTerm: 43,
6
+ states: "(jOVQPOOOnQPO'#ClOOQO'#Cd'#CdOOQO'#Cf'#CfOyQPO'#ChO!OQPO'#CrO!WQPO'#CkOOQO'#Cc'#CcOOQO'#Cz'#CzOOQO'#DW'#DWOVQPO'#DWQ!]QPOOOVQPO'#DWO!mQPO,58xO!rQPO'#DQO!wQPO,59WO#PQPO,59SO#UQPO'#CtOOQO,59^,59^O#ZQPO,59^O#cQQO,59VO#wQPO,59rOOQO'#C{'#C{OOQO'#C|'#C|OOQO'#C}'#C}OVQPO,59rOVQPO,59rOVQPO,59rO$rQPO,59rOVQPO1G.dOOQO,59l,59lOOQO-E7O-E7OOOQO1G.n1G.nO#cQQO,59`O%VQPO'#DRO%[QPO1G.xOOQO1G.x1G.xO%dQQO'#CwOOQO'#Cn'#CnOOQO1G.q1G.qO%kQPO1G/^O&RQPO1G/^O&iQPO1G/^OOQO1G/^1G/^O'PQPO7+$OOOQO1G.z1G.zOOQO,59m,59mOOQO-E7P-E7POOQO7+$d7+$dOOQO,59c,59cO'dQPO,59cOOQO<<Gj<<GjO#cQQO'#DSO'lQPO1G.}OOQO1G.}1G.}OOQO,59n,59nOOQO-E7Q-E7QOOQO7+$i7+$i",
7
+ stateData: "'t~OyOS~ORPOT[OXQOZRO]SOgTO{WO~OS]Oa^O^`X~O^`O~ORaOjbO~O^dO~OrhOshO|fO}gO~PVOTmO~ORnO~Oa^O^`a~ORpO~O^qO~OirOjtO~OZvOcvOdvOevOgTOluO~O{WORzaTzaXzaZza]zagzarzaszawza|za}zaUza~OU{OrhOshO|fO}gO~PVORaO~OirOj!QO~Om!RO~P#cOrzisziwzi|zi}ziUzi~PVO|fOrzisziwzi}ziUzi~PVO|fO}gOrzisziwziUzi~PVOU!TOrhOshO|fO}gO~PVOi!UOm!WO~Oi!UOm!ZO~O",
8
+ goto: "%h{PP|PPPP|!]P!]P!]PP!]!lP!{PPP#XP#nPP#hPP#t$U$]$cPP$h$n$tPPP$zkXOYZ[ijklmxyz|kVOYZ[ijklmxyz|kUOYZ[ijklmxyz|QwdQ}qQ!SuR!X!UjVOYZ[ijklmxyz|Xvdqu!UQcTR!OrmYOYZ[eijklmxyz|ZiZlyz|XjZlz|VkZl|Q_PRo_QscR!PsQ!V!SR!Y!VQZO`eYZelxyz|Ql[QxiQyjQzkR|m",
9
+ nodeNames: "⚠ Query Assignment Identifier = ( ) Filter TagFilter Tag TextFilter String TypeFilter TypeKeyword : PropertyFilter PropertyPath . Value Number Boolean Null ObjectLiteral { ObjectProperty , } ArrayLiteral [ ] Not And Or Relation ArrowRight ArrowLeft",
10
+ maxTerm: 45,
11
11
  skippedNodes: [0],
12
12
  repeatNodeCount: 3,
13
- tokenData: "2w~RrX^#]pq#]rs$Qst%nwx&fxy'}yz(S|}(X}!O(^!O!P)}!Q![(g![!]*S!^!_*X!c!}*d!}#O+O#P#Q+T#R#S*d#T#Y*d#Y#Z+Y#Z#b*d#b#c.i#c#h*d#h#i1Z#i#o*d#o#p2m#q#r2r#y#z#]$f$g#]#BY#BZ#]$IS$I_#]$I|$JO#]$JT$JU#]$KV$KW#]&FU&FV#]~#bYw~X^#]pq#]#y#z#]$f$g#]#BY#BZ#]$IS$I_#]$I|$JO#]$JT$JU#]$KV$KW#]&FU&FV#]~$TVOr$Qrs$js#O$Q#O#P$o#P;'S$Q;'S;=`%h<%lO$Q~$oOU~~$rRO;'S$Q;'S;=`${;=`O$Q~%OWOr$Qrs$js#O$Q#O#P$o#P;'S$Q;'S;=`%h;=`<%l$Q<%lO$Q~%kP;=`<%l$Q~%qT}!O&Q!Q![&Q!c!}&Q#R#S&Q#T#o&Q~&VTS~}!O&Q!Q![&Q!c!}&Q#R#S&Q#T#o&Q~&iVOw&fwx$jx#O&f#O#P'O#P;'S&f;'S;=`'w<%lO&f~'RRO;'S&f;'S;=`'[;=`O&f~'_WOw&fwx$jx#O&f#O#P'O#P;'S&f;'S;=`'w;=`<%l&f<%lO&f~'zP;=`<%l&f~(SOp~~(XOq~~(^Oe~~(aQ!Q![(g!`!a)x~(lS_~!O!P(x!Q![(g!g!h)^#X#Y)^~({P!Q![)O~)TR_~!Q![)O!g!h)^#X#Y)^~)aR{|)j}!O)j!Q![)p~)mP!Q![)p~)uP_~!Q![)p~)}On~~*SO]~~*XOY~~*[P}!O*_~*dOo~P*iVWP}!O*d!O!P*d!P!Q*d!Q![*d!c!}*d#R#S*d#T#o*d~+TOh~~+YOi~R+_WWP}!O*d!O!P*d!P!Q*d!Q![*d!c!}*d#R#S*d#T#U+w#U#o*dR+|XWP}!O*d!O!P*d!P!Q*d!Q![*d!c!}*d#R#S*d#T#`*d#`#a,i#a#o*dR,nXWP}!O*d!O!P*d!P!Q*d!Q![*d!c!}*d#R#S*d#T#g*d#g#h-Z#h#o*dR-`XWP}!O*d!O!P*d!P!Q*d!Q![*d!c!}*d#R#S*d#T#X*d#X#Y-{#Y#o*dR.SV`QWP}!O*d!O!P*d!P!Q*d!Q![*d!c!}*d#R#S*d#T#o*dR.nXWP}!O*d!O!P*d!P!Q*d!Q![*d!c!}*d#R#S*d#T#i*d#i#j/Z#j#o*dR/`XWP}!O*d!O!P*d!P!Q*d!Q![*d!c!}*d#R#S*d#T#`*d#`#a/{#a#o*dR0QXWP}!O*d!O!P*d!P!Q*d!Q![*d!c!}*d#R#S*d#T#`*d#`#a0m#a#o*dR0tVWPaQ}!O*d!O!P*d!P!Q*d!Q![*d!c!}*d#R#S*d#T#o*dR1`XWP}!O*d!O!P*d!P!Q*d!Q![*d!c!}*d#R#S*d#T#f*d#f#g1{#g#o*dR2QXWP}!O*d!O!P*d!P!Q*d!Q![*d!c!}*d#R#S*d#T#i*d#i#j-Z#j#o*d~2rOc~~2wOf~",
13
+ tokenData: "3P~RsX^#`pq#`rs$Tst%qwx&ixy(Qyz(V|}([}!O(a!O!P*Q!Q![(j![!]*V!^!_*[!_!`*g!c!}*l!}#O+W#P#Q+]#R#S*l#T#Y*l#Y#Z+b#Z#b*l#b#c.q#c#h*l#h#i1c#i#o*l#o#p2u#q#r2z#y#z#`$f$g#`#BY#BZ#`$IS$I_#`$I|$JO#`$JT$JU#`$KV$KW#`&FU&FV#`~#eYy~X^#`pq#`#y#z#`$f$g#`#BY#BZ#`$IS$I_#`$I|$JO#`$JT$JU#`$KV$KW#`&FU&FV#`~$WVOr$Trs$ms#O$T#O#P$r#P;'S$T;'S;=`%k<%lO$T~$rOZ~~$uRO;'S$T;'S;=`%O;=`O$T~%RWOr$Trs$ms#O$T#O#P$r#P;'S$T;'S;=`%k;=`<%l$T<%lO$T~%nP;=`<%l$T~%tT}!O&T!Q![&T!c!}&T#R#S&T#T#o&T~&YTX~}!O&T!Q![&T!c!}&T#R#S&T#T#o&T~&lVOw&iwx$mx#O&i#O#P'R#P;'S&i;'S;=`'z<%lO&i~'URO;'S&i;'S;=`'_;=`O&i~'bWOw&iwx$mx#O&i#O#P'R#P;'S&i;'S;=`'z;=`<%l&i<%lO&i~'}P;=`<%l&i~(VOT~~([OU~~(aOi~~(dQ!Q![(j!`!a){~(oSc~!O!P({!Q![(j!g!h)a#X#Y)a~)OP!Q![)R~)WRc~!Q![)R!g!h)a#X#Y)a~)dR{|)m}!O)m!Q![)s~)pP!Q![)s~)xPc~!Q![)s~*QOr~~*VOa~~*[O^~~*_P}!O*b~*gOs~~*lOS~P*qVRP}!O*l!O!P*l!P!Q*l!Q![*l!c!}*l#R#S*l#T#o*l~+]Ol~~+bOm~R+gWRP}!O*l!O!P*l!P!Q*l!Q![*l!c!}*l#R#S*l#T#U,P#U#o*lR,UXRP}!O*l!O!P*l!P!Q*l!Q![*l!c!}*l#R#S*l#T#`*l#`#a,q#a#o*lR,vXRP}!O*l!O!P*l!P!Q*l!Q![*l!c!}*l#R#S*l#T#g*l#g#h-c#h#o*lR-hXRP}!O*l!O!P*l!P!Q*l!Q![*l!c!}*l#R#S*l#T#X*l#X#Y.T#Y#o*lR.[VdQRP}!O*l!O!P*l!P!Q*l!Q![*l!c!}*l#R#S*l#T#o*lR.vXRP}!O*l!O!P*l!P!Q*l!Q![*l!c!}*l#R#S*l#T#i*l#i#j/c#j#o*lR/hXRP}!O*l!O!P*l!P!Q*l!Q![*l!c!}*l#R#S*l#T#`*l#`#a0T#a#o*lR0YXRP}!O*l!O!P*l!P!Q*l!Q![*l!c!}*l#R#S*l#T#`*l#`#a0u#a#o*lR0|VRPeQ}!O*l!O!P*l!P!Q*l!Q![*l!c!}*l#R#S*l#T#o*lR1hXRP}!O*l!O!P*l!P!Q*l!Q![*l!c!}*l#R#S*l#T#f*l#f#g2T#g#o*lR2YXRP}!O*l!O!P*l!P!Q*l!Q![*l!c!}*l#R#S*l#T#i*l#i#j-c#j#o*l~2zOg~~3POj~",
14
14
  tokenizers: [0, 1],
15
15
  topRules: {"Query":[0,1]},
16
- specialized: [{term: 8, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
16
+ specialized: [{term: 3, get: (value: keyof typeof spec_Identifier) => spec_Identifier[value] || -1}],
17
17
  tokenPrec: 0
18
18
  })
@@ -4,10 +4,14 @@
4
4
 
5
5
  import { type Parser, type Tree, type TreeCursor } from '@lezer/common';
6
6
 
7
- import { Filter } from '@dxos/echo';
7
+ import { Filter, type Tag } from '@dxos/echo';
8
+ import { invariant } from '@dxos/invariant';
8
9
 
9
10
  import { QueryDSL } from './gen';
10
11
 
12
+ // TODO(burdon): Return Query AST.
13
+ export type BuildResult = { filter?: Filter.Any; name?: string };
14
+
11
15
  /**
12
16
  * Stateless query builder that parses DSL trees into filters.
13
17
  *
@@ -15,7 +19,9 @@ import { QueryDSL } from './gen';
15
19
  * To modify the functionality, create a minimal breaking test and direct the LLM to fix either the grammar or builder.
16
20
  */
17
21
  export class QueryBuilder {
18
- constructor(private readonly _parser: Parser = QueryDSL.Parser.configure({ strict: true })) {}
22
+ private readonly _parser: Parser = QueryDSL.Parser.configure({ strict: true });
23
+
24
+ constructor(private readonly _tags?: Tag.TagMap) {}
19
25
 
20
26
  /**
21
27
  * Check valid input.
@@ -32,24 +38,24 @@ export class QueryBuilder {
32
38
  /**
33
39
  * Build a query from the input string.
34
40
  */
35
- build(input: string): Filter.Any | null {
41
+ build(input: string): BuildResult {
36
42
  try {
37
43
  const tree = this._parser.parse(input);
38
44
  return this.buildQuery(tree, input);
39
45
  } catch {
40
- return null;
46
+ return {};
41
47
  }
42
48
  }
43
49
 
44
50
  /**
45
51
  * Build a query from a parsed DSL tree.
46
52
  */
47
- buildQuery(tree: Tree, input: string): Filter.Any {
53
+ buildQuery(tree: Tree, input: string): BuildResult {
48
54
  const cursor = tree.cursor();
49
55
 
50
56
  // Start at root (Query node).
51
57
  if (cursor.node.name !== 'Query') {
52
- return Filter.nothing();
58
+ return {};
53
59
  }
54
60
 
55
61
  // Check if Query has multiple children (binary expression).
@@ -61,26 +67,76 @@ export class QueryBuilder {
61
67
  cursor.parent();
62
68
  }
63
69
 
70
+ // Check if this is an assignment.
71
+ const hasAssignment = children.some((child) => child.name === 'Assignment');
72
+ if (hasAssignment) {
73
+ return this._parseAssignment(cursor, input);
74
+ }
75
+
64
76
  // If we have an operator in the children, or multiple expressions (implicit AND), parse as binary expression.
65
77
  const hasOperator = children.some((child) => child.name === 'And' || child.name === 'Or');
66
78
  const hasMultipleExpressions =
67
79
  children.filter((child) => child.name === 'Filter' || child.name === 'Not' || child.name === '(').length > 1;
68
80
  if (hasOperator || hasMultipleExpressions) {
69
- return this._parseBinaryExpression(cursor, input);
81
+ const filter = this._parseBinaryExpression(cursor, input);
82
+ return { filter };
70
83
  }
71
84
 
72
85
  // Otherwise, parse the single expression.
73
86
  if (!cursor.firstChild()) {
74
- return Filter.nothing();
87
+ return { filter: Filter.nothing() };
75
88
  }
76
89
 
77
- return this._parseExpression(cursor, input);
90
+ const filter = this._parseExpression(cursor, input);
91
+ return { filter };
92
+ }
93
+
94
+ /**
95
+ * Parse an assignment node.
96
+ */
97
+ private _parseAssignment(cursor: TreeCursor, input: string): BuildResult {
98
+ if (!cursor.firstChild()) {
99
+ return {};
100
+ }
101
+
102
+ let name: string | undefined;
103
+ let filter: Filter.Any | undefined;
104
+
105
+ // Find the Assignment node
106
+ do {
107
+ if (cursor.node.name === 'Assignment') {
108
+ // Get the full assignment text first
109
+ const assignmentText = this._getNodeText(cursor, input);
110
+
111
+ if (cursor.firstChild()) {
112
+ // First child should be the variable name (Identifier)
113
+ name = this._getNodeText(cursor, input);
114
+
115
+ // Find the parentheses in the assignment text and extract the content
116
+ const openParenIndex = assignmentText.indexOf('(');
117
+ const closeParenIndex = assignmentText.lastIndexOf(')');
118
+
119
+ if (openParenIndex !== -1 && closeParenIndex !== -1 && closeParenIndex > openParenIndex) {
120
+ const subInput = assignmentText.slice(openParenIndex + 1, closeParenIndex).trim();
121
+ const subTree = this._parser.parse(subInput);
122
+ const subResult = this.buildQuery(subTree, subInput);
123
+ filter = subResult.filter;
124
+ }
125
+
126
+ cursor.parent(); // Back to Assignment
127
+ }
128
+ break;
129
+ }
130
+ } while (cursor.nextSibling());
131
+
132
+ cursor.parent(); // Back to Query
133
+ return { filter, name };
78
134
  }
79
135
 
80
136
  /**
81
137
  * Parse an expression node.
82
138
  */
83
- private _parseExpression(cursor: TreeCursor, input: string): Filter.Any {
139
+ private _parseExpression(cursor: TreeCursor, input: string): Filter.Any | undefined {
84
140
  const nodeName = cursor.node.name;
85
141
 
86
142
  switch (nodeName) {
@@ -91,7 +147,7 @@ export class QueryBuilder {
91
147
  // Move past NOT token to the expression.
92
148
  cursor.nextSibling();
93
149
  const notFilter = this._parseExpression(cursor, input);
94
- return Filter.not(notFilter);
150
+ return notFilter ? Filter.not(notFilter) : undefined;
95
151
  }
96
152
 
97
153
  case 'And':
@@ -201,15 +257,25 @@ export class QueryBuilder {
201
257
  // Parse the expression inside parentheses as a subtree.
202
258
  const subInput = input.slice(exprStart, exprEnd);
203
259
  const subTree = this._parser.parse(subInput);
204
- filters.push(this.buildQuery(subTree, subInput));
260
+ const subResult = this.buildQuery(subTree, subInput);
261
+ if (subResult.filter) {
262
+ filters.push(subResult.filter);
263
+ }
205
264
  } else {
206
265
  // Simple parenthesized expression.
207
- filters.push(this._parseExpression(cursor, input));
266
+ const subFilter = this._parseExpression(cursor, input);
267
+ if (subFilter) {
268
+ filters.push(subFilter);
269
+ }
270
+
208
271
  // Skip until we find the closing parenthesis.
209
272
  while (cursor.nextSibling() && cursor.node.name !== ')') {}
210
273
  }
211
274
  } else if (nodeName !== ')') {
212
- filters.push(this._parseExpression(cursor, input));
275
+ const subFilter = this._parseExpression(cursor, input);
276
+ if (subFilter) {
277
+ filters.push(subFilter);
278
+ }
213
279
  }
214
280
  } while (cursor.nextSibling());
215
281
 
@@ -230,17 +296,18 @@ export class QueryBuilder {
230
296
  /**
231
297
  * Parse a Filter node.
232
298
  */
233
- private _parseFilter(cursor: TreeCursor, input: string): Filter.Any {
299
+ private _parseFilter(cursor: TreeCursor, input: string): Filter.Any | undefined {
234
300
  if (!cursor.firstChild()) {
235
301
  return Filter.nothing();
236
302
  }
237
303
 
304
+ let result: Filter.Any | undefined = undefined;
238
305
  const filterType = cursor.node.name;
239
- let result: Filter.Any;
240
-
241
306
  switch (filterType) {
242
307
  case 'TagFilter':
243
- result = this._parseTagFilter(cursor, input);
308
+ if (this._tags) {
309
+ result = this._parseTagFilter(cursor, input);
310
+ }
244
311
  break;
245
312
 
246
313
  case 'TextFilter':
@@ -397,9 +464,11 @@ export class QueryBuilder {
397
464
  /**
398
465
  * Parse a TagFilter node (#tag).
399
466
  */
400
- private _parseTagFilter(cursor: TreeCursor, input: string): Filter.Any {
401
- const tag = this._getNodeText(cursor, input);
402
- return Filter.tag(tag.slice(1));
467
+ private _parseTagFilter(cursor: TreeCursor, input: string): Filter.Any | undefined {
468
+ invariant(this._tags);
469
+ const str = this._getNodeText(cursor, input).slice(1).toLowerCase();
470
+ const [key] = Object.entries(this._tags!).find(([, value]) => value.label.toLowerCase() === str) ?? [];
471
+ return key ? Filter.tag(key) : undefined;
403
472
  }
404
473
 
405
474
  /**
@@ -5,6 +5,7 @@
5
5
  @top Query { expression }
6
6
 
7
7
  expression {
8
+ Assignment |
8
9
  Filter |
9
10
  !Not Not expression |
10
11
  expression !ImplicitAnd expression |
@@ -14,6 +15,10 @@ expression {
14
15
  "(" expression ")"
15
16
  }
16
17
 
18
+ Assignment {
19
+ Identifier "=" "(" expression ")"
20
+ }
21
+
17
22
  Not {
18
23
  @specialize<Identifier, "NOT" | "not" | "!">
19
24
  }
@@ -110,7 +115,7 @@ Value {
110
115
  space { @whitespace+ }
111
116
 
112
117
  "{" "}" "[" "]" "(" ")"
113
- ":" "," "."
118
+ ":" "," "." "="
114
119
  }
115
120
 
116
121
  @skip { space }
@@ -120,5 +125,6 @@ Value {
120
125
  ImplicitAnd @left,
121
126
  And @left,
122
127
  Or @left,
123
- Relation @left
128
+ Relation @left,
129
+ Assignment @right
124
130
  }
@@ -5,10 +5,10 @@
5
5
  import { type Tree } from '@lezer/common';
6
6
  import { describe, it } from 'vitest';
7
7
 
8
- import { Filter } from '@dxos/echo';
8
+ import { Filter, Tag } from '@dxos/echo';
9
9
 
10
10
  import { QueryDSL } from './gen';
11
- import { QueryBuilder } from './query-builder';
11
+ import { type BuildResult, QueryBuilder } from './query-builder';
12
12
 
13
13
  // TODO(burdon): Ref/Relation traversal.
14
14
 
@@ -258,6 +258,22 @@ describe('query', () => {
258
258
  '}',
259
259
  ],
260
260
  },
261
+ {
262
+ input: 'x = ( type: dxos.org/type/Person )',
263
+ expected: [
264
+ 'Query',
265
+ 'Assignment',
266
+ 'Identifier',
267
+ '=',
268
+ '(',
269
+ 'Filter',
270
+ 'TypeFilter',
271
+ 'TypeKeyword',
272
+ ':',
273
+ 'Identifier',
274
+ ')',
275
+ ],
276
+ },
261
277
  ];
262
278
 
263
279
  for (const { input, expected } of tests) {
@@ -279,75 +295,115 @@ describe('query', () => {
279
295
  });
280
296
 
281
297
  it('build', ({ expect }) => {
282
- const queryBuilder = new QueryBuilder();
298
+ const queryBuilder = new QueryBuilder({
299
+ tag_1: Tag.make({ label: 'foo' }),
300
+ tag_2: Tag.make({ label: 'bar' }),
301
+ });
283
302
 
284
303
  // TODO(burdon): Test "not"
285
- type Test = { input: string; expected: Filter.Any };
304
+ type Test = { input: string; expected: BuildResult };
286
305
  const tests: Test[] = [
287
306
  // Types
288
307
  {
289
308
  input: 'type:dxos.org/type/Person',
290
- expected: Filter.typename('dxos.org/type/Person'),
309
+ expected: {
310
+ filter: Filter.typename('dxos.org/type/Person'),
311
+ },
291
312
  },
292
313
  // Tags
293
314
  {
294
315
  input: '#foo',
295
- expected: Filter.tag('foo'),
316
+ expected: {
317
+ filter: Filter.tag('tag_1'),
318
+ },
296
319
  },
297
320
  {
298
321
  input: '#foo AND #bar',
299
- expected: Filter.and(Filter.tag('foo'), Filter.tag('bar')),
322
+ expected: {
323
+ filter: Filter.and(Filter.tag('tag_1'), Filter.tag('tag_2')),
324
+ },
300
325
  },
301
326
  {
302
327
  input: '#foo #bar',
303
- expected: Filter.and(Filter.tag('foo'), Filter.tag('bar')),
328
+ expected: {
329
+ filter: Filter.and(Filter.tag('tag_1'), Filter.tag('tag_2')),
330
+ },
304
331
  },
305
332
  // Text
306
333
  {
307
334
  input: '"test"',
308
- expected: Filter.text('test'),
335
+ expected: {
336
+ filter: Filter.text('test'),
337
+ },
309
338
  },
310
339
  // Mixed
311
340
  {
312
341
  input: '#foo "test"',
313
- expected: Filter.and(Filter.tag('foo'), Filter.text('test')),
342
+ expected: {
343
+ filter: Filter.and(Filter.tag('tag_1'), Filter.text('test')),
344
+ },
314
345
  },
315
346
  // Props
316
347
  {
317
348
  input: '{ name: "DXOS" }',
318
- expected: Filter.props({ name: 'DXOS' }),
349
+ expected: {
350
+ filter: Filter.props({ name: 'DXOS' }),
351
+ },
319
352
  },
320
353
  {
321
354
  input: '{ value: 100 }',
322
- expected: Filter.props({ value: 100 }),
355
+ expected: {
356
+ filter: Filter.props({ value: 100 }),
357
+ },
323
358
  },
324
359
  {
325
360
  input: 'type:dxos.org/type/Person OR type:dxos.org/type/Organization',
326
- expected: Filter.or(Filter.typename('dxos.org/type/Person'), Filter.typename('dxos.org/type/Organization')),
361
+ expected: {
362
+ filter: Filter.or(Filter.typename('dxos.org/type/Person'), Filter.typename('dxos.org/type/Organization')),
363
+ },
327
364
  },
328
365
  {
329
366
  input: '(type:dxos.org/type/Person OR type:dxos.org/type/Organization) AND { name: "DXOS" }',
330
- expected: Filter.and(
331
- Filter.or(Filter.typename('dxos.org/type/Person'), Filter.typename('dxos.org/type/Organization')),
332
- Filter.props({ name: 'DXOS' }),
333
- ),
367
+ expected: {
368
+ filter: Filter.and(
369
+ Filter.or(Filter.typename('dxos.org/type/Person'), Filter.typename('dxos.org/type/Organization')),
370
+ Filter.props({ name: 'DXOS' }),
371
+ ),
372
+ },
334
373
  },
335
374
  {
336
375
  input: 'type:dxos.org/type/Person and { name: "DXOS" }',
337
- expected: Filter.and(Filter.typename('dxos.org/type/Person'), Filter.props({ name: 'DXOS' })),
376
+ expected: {
377
+ filter: Filter.and(Filter.typename('dxos.org/type/Person'), Filter.props({ name: 'DXOS' })),
378
+ },
379
+ },
380
+ // Assignment
381
+ {
382
+ input: 'x = ( type:dxos.org/type/Person )',
383
+ expected: {
384
+ name: 'x',
385
+ filter: Filter.typename('dxos.org/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
+ },
338
394
  },
339
395
  // TODO(burdon): Convert Query/Filter expr to AST.
340
396
  // TODO(burdon): Person -> Organization (many-to-many relation).
341
397
  // Get Research Note objects for Organization objects for Person objects with jobTitle.
342
398
  //
343
- // Cypher: MATCH (p:Person)-[:WorksAt]->(o:Organization)<-[:ResearchOn]-(r:ResearchNote) WHERE p.jotTitle IS NOT NULL
344
- // ((type:Person AND { jobTitle: "investor" }) -[:WorksAt]-> type:Organization) <-[:ResearchOn]- type:ResearchNote
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
345
401
  //
346
402
  // {
347
403
  // input: '',
348
404
  // expected: Query.select(Filter.typename('dxos.org/type/Person', { jobTitle: 'investor' }))
349
405
  // .reference('organization')
350
- // .targetOf(Relation.of('dxos.org/relation/ResearchOn')) // TODO(burdon): Invert?
406
+ // .targetOf(Relation.of('dxos.org/relation/HasSubject')) // TODO(burdon): Invert?
351
407
  // .source(),
352
408
  // },
353
409
  ];
@@ -0,0 +1,5 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ export * from './query-lite';