@dxos/echo-query 0.8.4-main.fffef41 → 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.
- package/LICENSE +102 -5
- package/README.md +1 -1
- package/dist/lib/{browser → neutral}/index.mjs +375 -20
- package/dist/lib/neutral/index.mjs.map +7 -0
- package/dist/lib/neutral/meta.json +1 -0
- package/dist/query-lite/index.d.ts +10764 -0
- package/dist/query-lite/index.d.ts.map +1 -0
- package/dist/query-lite/index.js +572 -375
- package/dist/query-lite/index.js.map +1 -0
- package/dist/types/src/index.d.ts +1 -0
- package/dist/types/src/index.d.ts.map +1 -1
- package/dist/types/src/parser/gen/index.d.ts.map +1 -1
- package/dist/types/src/parser/query-builder.d.ts +7 -0
- package/dist/types/src/parser/query-builder.d.ts.map +1 -1
- package/dist/types/src/query-lite/query-lite.d.ts +4 -4
- package/dist/types/src/query-lite/query-lite.d.ts.map +1 -1
- package/dist/types/src/sandbox/index.d.ts +2 -0
- package/dist/types/src/sandbox/index.d.ts.map +1 -0
- package/dist/types/src/sandbox/query-sandbox.d.ts +1 -1
- package/dist/types/src/sandbox/query-sandbox.d.ts.map +1 -1
- package/dist/types/src/sandbox/quickjs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +23 -24
- package/src/index.ts +1 -0
- package/src/parser/query-builder.ts +276 -10
- package/src/parser/query.test.ts +151 -31
- package/src/query-lite/query-lite.ts +446 -80
- package/src/sandbox/index.ts +5 -0
- package/src/sandbox/query-sandbox.test.ts +11 -10
- package/src/sandbox/query-sandbox.ts +1 -1
- package/src/sandbox/quickjs.ts +1 -2
- package/dist/lib/browser/index.mjs.map +0 -7
- package/dist/lib/browser/meta.json +0 -1
- package/dist/lib/node-esm/index.mjs +0 -563
- package/dist/lib/node-esm/index.mjs.map +0 -7
- package/dist/lib/node-esm/meta.json +0 -1
package/package.json
CHANGED
|
@@ -1,10 +1,14 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@dxos/echo-query",
|
|
3
|
-
"version": "0.8.4-
|
|
3
|
+
"version": "0.8.4-staging.60fe92afc8",
|
|
4
4
|
"description": "ECHO queries.",
|
|
5
5
|
"homepage": "https://dxos.org",
|
|
6
6
|
"bugs": "https://github.com/dxos/dxos/issues",
|
|
7
|
-
"
|
|
7
|
+
"repository": {
|
|
8
|
+
"type": "git",
|
|
9
|
+
"url": "https://github.com/dxos/dxos"
|
|
10
|
+
},
|
|
11
|
+
"license": "FSL-1.1-Apache-2.0",
|
|
8
12
|
"author": "info@dxos.org",
|
|
9
13
|
"sideEffects": false,
|
|
10
14
|
"type": "module",
|
|
@@ -15,37 +19,32 @@
|
|
|
15
19
|
".": {
|
|
16
20
|
"source": "./src/index.ts",
|
|
17
21
|
"types": "./dist/types/src/index.d.ts",
|
|
18
|
-
"
|
|
19
|
-
|
|
20
|
-
|
|
22
|
+
"default": "./dist/lib/neutral/index.mjs"
|
|
23
|
+
},
|
|
24
|
+
"./api.d.ts": "./dist/query-lite/index.d.ts"
|
|
21
25
|
},
|
|
22
26
|
"types": "dist/types/src/index.d.ts",
|
|
23
|
-
"typesVersions": {
|
|
24
|
-
"*": {}
|
|
25
|
-
},
|
|
26
27
|
"files": [
|
|
27
28
|
"dist",
|
|
28
29
|
"src"
|
|
29
30
|
],
|
|
30
31
|
"dependencies": {
|
|
31
|
-
"@lezer/common": "^1.
|
|
32
|
-
"@lezer/
|
|
33
|
-
"@
|
|
34
|
-
"@
|
|
35
|
-
"@dxos/
|
|
36
|
-
"@dxos/
|
|
37
|
-
"@dxos/
|
|
38
|
-
"@dxos/
|
|
39
|
-
"@dxos/
|
|
40
|
-
"@dxos/errors": "0.8.4-main.fffef41",
|
|
41
|
-
"@dxos/node-std": "0.8.4-main.fffef41",
|
|
42
|
-
"@dxos/util": "0.8.4-main.fffef41",
|
|
43
|
-
"@dxos/vendor-quickjs": "0.8.4-main.fffef41"
|
|
32
|
+
"@lezer/common": "^1.5.2",
|
|
33
|
+
"@lezer/lr": "^1.4.10",
|
|
34
|
+
"@dxos/context": "0.8.4-staging.60fe92afc8",
|
|
35
|
+
"@dxos/echo": "0.8.4-staging.60fe92afc8",
|
|
36
|
+
"@dxos/echo-protocol": "0.8.4-staging.60fe92afc8",
|
|
37
|
+
"@dxos/invariant": "0.8.4-staging.60fe92afc8",
|
|
38
|
+
"@dxos/node-std": "0.8.4-staging.60fe92afc8",
|
|
39
|
+
"@dxos/util": "0.8.4-staging.60fe92afc8",
|
|
40
|
+
"@dxos/vendor-quickjs": "0.8.4-staging.60fe92afc8"
|
|
44
41
|
},
|
|
45
42
|
"devDependencies": {
|
|
46
|
-
"@lezer/generator": "^1.
|
|
47
|
-
"
|
|
48
|
-
"
|
|
43
|
+
"@lezer/generator": "^1.8.0",
|
|
44
|
+
"tsdown": "^0.16.7",
|
|
45
|
+
"typescript": "^6.0.3",
|
|
46
|
+
"@dxos/echo-generator": "0.8.4-staging.60fe92afc8",
|
|
47
|
+
"@dxos/random": "0.8.4-staging.60fe92afc8"
|
|
49
48
|
},
|
|
50
49
|
"publishConfig": {
|
|
51
50
|
"access": "public"
|
package/src/index.ts
CHANGED
|
@@ -6,6 +6,7 @@ import { type Parser, type Tree, type TreeCursor } from '@lezer/common';
|
|
|
6
6
|
|
|
7
7
|
import { Filter, type Tag } from '@dxos/echo';
|
|
8
8
|
import { invariant } from '@dxos/invariant';
|
|
9
|
+
import { type URI } from '@dxos/keys';
|
|
9
10
|
|
|
10
11
|
import { QueryDSL } from './gen';
|
|
11
12
|
|
|
@@ -28,7 +29,7 @@ export class QueryBuilder {
|
|
|
28
29
|
*/
|
|
29
30
|
validate(input: string): boolean {
|
|
30
31
|
try {
|
|
31
|
-
const tree = this._parser.parse(input);
|
|
32
|
+
const tree = this._parser.parse(normalizeInput(input));
|
|
32
33
|
return tree.cursor().node.name === 'Query';
|
|
33
34
|
} catch {
|
|
34
35
|
return false;
|
|
@@ -40,8 +41,9 @@ export class QueryBuilder {
|
|
|
40
41
|
*/
|
|
41
42
|
build(input: string): BuildResult {
|
|
42
43
|
try {
|
|
43
|
-
const
|
|
44
|
-
|
|
44
|
+
const normalized = normalizeInput(input);
|
|
45
|
+
const tree = this._parser.parse(normalized);
|
|
46
|
+
return this.buildQuery(tree, normalized);
|
|
45
47
|
} catch {
|
|
46
48
|
return {};
|
|
47
49
|
}
|
|
@@ -245,8 +247,9 @@ export class QueryBuilder {
|
|
|
245
247
|
let exprEnd = cursor.to;
|
|
246
248
|
|
|
247
249
|
while (cursor.nextSibling() && depth > 0) {
|
|
248
|
-
if (cursor.node.name === '(')
|
|
249
|
-
|
|
250
|
+
if (cursor.node.name === '(') {
|
|
251
|
+
depth++;
|
|
252
|
+
} else if (cursor.node.name === ')') {
|
|
250
253
|
depth--;
|
|
251
254
|
if (depth === 0) {
|
|
252
255
|
exprEnd = cursor.from;
|
|
@@ -345,7 +348,12 @@ export class QueryBuilder {
|
|
|
345
348
|
|
|
346
349
|
const typename = this._getNodeText(cursor, input);
|
|
347
350
|
cursor.parent(); // Go back to TypeFilter.
|
|
348
|
-
|
|
351
|
+
// Inline the URI construction to keep runtime `@dxos/keys` values out of the query-lite bundle
|
|
352
|
+
// (which runs in a QuickJS sandbox without that dep). Callers may pass a bare typename
|
|
353
|
+
// (e.g. `com.example.task`) or a canonical URI (`dxn:…` / `echo:…`); prepend `dxn:` only for
|
|
354
|
+
// bare names detected by the absence of a URI scheme.
|
|
355
|
+
const uri = (/^[a-z][a-z0-9+.-]*:/i.test(typename) ? typename : `dxn:${typename}`) as URI.URI;
|
|
356
|
+
return Filter.type(uri);
|
|
349
357
|
}
|
|
350
358
|
|
|
351
359
|
/**
|
|
@@ -355,8 +363,8 @@ export class QueryBuilder {
|
|
|
355
363
|
cursor.firstChild(); // Move to String node.
|
|
356
364
|
const text = this._getNodeText(cursor, input);
|
|
357
365
|
cursor.parent(); // Go back to TextFilter.
|
|
358
|
-
// Remove quotes.
|
|
359
|
-
return Filter.text(text.slice(1, -1));
|
|
366
|
+
// Remove quotes and decode escapes.
|
|
367
|
+
return Filter.text(unescapeStringLiteral(text.slice(1, -1)));
|
|
360
368
|
}
|
|
361
369
|
|
|
362
370
|
/**
|
|
@@ -479,9 +487,9 @@ export class QueryBuilder {
|
|
|
479
487
|
|
|
480
488
|
switch (valueType) {
|
|
481
489
|
case 'String': {
|
|
482
|
-
// Remove quotes.
|
|
490
|
+
// Remove quotes and decode escapes.
|
|
483
491
|
const str = this._getNodeText(cursor, input);
|
|
484
|
-
return str.slice(1, -1);
|
|
492
|
+
return unescapeStringLiteral(str.slice(1, -1));
|
|
485
493
|
}
|
|
486
494
|
|
|
487
495
|
case 'Number':
|
|
@@ -537,3 +545,261 @@ export class QueryBuilder {
|
|
|
537
545
|
return input.slice(cursor.from, cursor.to);
|
|
538
546
|
}
|
|
539
547
|
}
|
|
548
|
+
|
|
549
|
+
const KEYWORDS = new Set(['AND', 'OR', 'NOT']);
|
|
550
|
+
const VALUE_LITERALS = new Set(['true', 'false', 'null']);
|
|
551
|
+
const SPECIAL_CHARS = /[\s(){}\[\],"']/;
|
|
552
|
+
const PROPERTY_KEY = /^[a-zA-Z_][a-zA-Z0-9_.]*$/;
|
|
553
|
+
const NUMBER_LITERAL = /^-?\d+(?:\.\d+)?(?:[eE][+-]?\d+)?$/;
|
|
554
|
+
|
|
555
|
+
// Common URL schemes — `http://...`, `mailto:rich@...` etc. should be searched as text,
|
|
556
|
+
// not auto-promoted to property filters.
|
|
557
|
+
const URL_SCHEMES = new Set([
|
|
558
|
+
'http',
|
|
559
|
+
'https',
|
|
560
|
+
'ftp',
|
|
561
|
+
'ftps',
|
|
562
|
+
'file',
|
|
563
|
+
'mailto',
|
|
564
|
+
'tel',
|
|
565
|
+
'sms',
|
|
566
|
+
'data',
|
|
567
|
+
'javascript',
|
|
568
|
+
'ws',
|
|
569
|
+
'wss',
|
|
570
|
+
'ssh',
|
|
571
|
+
'git',
|
|
572
|
+
]);
|
|
573
|
+
|
|
574
|
+
/**
|
|
575
|
+
* Normalize raw user input into a form the lezer grammar can parse.
|
|
576
|
+
* - Bare text fragments (e.g. `foo`) are wrapped in quotes so they parse as TextFilter.
|
|
577
|
+
* - Property values that aren't already quoted/numeric/boolean (e.g. `from:rich@dxos.org`) are quoted.
|
|
578
|
+
* - Tags, type filters, operators, parens, braces, quoted strings, and assignments pass through unchanged.
|
|
579
|
+
*/
|
|
580
|
+
export const normalizeInput = (input: string): string => {
|
|
581
|
+
const out: string[] = [];
|
|
582
|
+
let pos = 0;
|
|
583
|
+
while (pos < input.length) {
|
|
584
|
+
const currentChar = input[pos];
|
|
585
|
+
|
|
586
|
+
// Whitespace.
|
|
587
|
+
if (/\s/.test(currentChar)) {
|
|
588
|
+
out.push(currentChar);
|
|
589
|
+
pos++;
|
|
590
|
+
continue;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
// Quoted string (already a valid TextFilter / Value).
|
|
594
|
+
// Single-quote only opens a string if the previous char isn't a word-char,
|
|
595
|
+
// so apostrophes inside barewords (e.g. `don't`, `O'Connor`) stay attached.
|
|
596
|
+
const prevChar = pos > 0 ? input[pos - 1] : '';
|
|
597
|
+
const isStringOpener =
|
|
598
|
+
currentChar === '"' || (currentChar === "'" && (pos === 0 || !/[a-zA-Z0-9_]/.test(prevChar)));
|
|
599
|
+
if (isStringOpener) {
|
|
600
|
+
const quoteChar = currentChar;
|
|
601
|
+
let scanIndex = pos + 1;
|
|
602
|
+
while (scanIndex < input.length && input[scanIndex] !== quoteChar) {
|
|
603
|
+
if (input[scanIndex] === '\\' && scanIndex + 1 < input.length) {
|
|
604
|
+
scanIndex += 2;
|
|
605
|
+
} else {
|
|
606
|
+
scanIndex++;
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
out.push(input.slice(pos, Math.min(scanIndex + 1, input.length)));
|
|
610
|
+
pos = scanIndex + 1;
|
|
611
|
+
continue;
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
// Object/array literals: pass through (contents already structured).
|
|
615
|
+
if (currentChar === '{' || currentChar === '[') {
|
|
616
|
+
const closeChar = currentChar === '{' ? '}' : ']';
|
|
617
|
+
let depth = 1;
|
|
618
|
+
let scanIndex = pos + 1;
|
|
619
|
+
while (scanIndex < input.length && depth > 0) {
|
|
620
|
+
const innerChar = input[scanIndex];
|
|
621
|
+
if (innerChar === '"' || innerChar === "'") {
|
|
622
|
+
const quoteChar = innerChar;
|
|
623
|
+
scanIndex++;
|
|
624
|
+
while (scanIndex < input.length && input[scanIndex] !== quoteChar) {
|
|
625
|
+
if (input[scanIndex] === '\\' && scanIndex + 1 < input.length) {
|
|
626
|
+
scanIndex += 2;
|
|
627
|
+
} else {
|
|
628
|
+
scanIndex++;
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
scanIndex++;
|
|
632
|
+
} else if (innerChar === currentChar) {
|
|
633
|
+
depth++;
|
|
634
|
+
scanIndex++;
|
|
635
|
+
} else if (innerChar === closeChar) {
|
|
636
|
+
depth--;
|
|
637
|
+
scanIndex++;
|
|
638
|
+
} else {
|
|
639
|
+
scanIndex++;
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
out.push(input.slice(pos, scanIndex));
|
|
643
|
+
pos = scanIndex;
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
// Single-char structural tokens. `}` / `]` only reach here when unmatched —
|
|
648
|
+
// pass them through so the lezer parser can produce a clear error rather than spinning.
|
|
649
|
+
if (
|
|
650
|
+
currentChar === '(' ||
|
|
651
|
+
currentChar === ')' ||
|
|
652
|
+
currentChar === '=' ||
|
|
653
|
+
currentChar === ',' ||
|
|
654
|
+
currentChar === '}' ||
|
|
655
|
+
currentChar === ']'
|
|
656
|
+
) {
|
|
657
|
+
out.push(currentChar);
|
|
658
|
+
pos++;
|
|
659
|
+
continue;
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
// Relations.
|
|
663
|
+
if ((currentChar === '-' && input[pos + 1] === '>') || (currentChar === '<' && input[pos + 1] === '-')) {
|
|
664
|
+
out.push(input.slice(pos, pos + 2));
|
|
665
|
+
pos += 2;
|
|
666
|
+
continue;
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
// NOT prefix (`!`) — single token.
|
|
670
|
+
if (currentChar === '!') {
|
|
671
|
+
out.push(currentChar);
|
|
672
|
+
pos++;
|
|
673
|
+
continue;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
// Tag.
|
|
677
|
+
if (currentChar === '#') {
|
|
678
|
+
let scanIndex = pos + 1;
|
|
679
|
+
while (scanIndex < input.length && /[a-zA-Z0-9_-]/.test(input[scanIndex])) {
|
|
680
|
+
scanIndex++;
|
|
681
|
+
}
|
|
682
|
+
out.push(input.slice(pos, scanIndex));
|
|
683
|
+
pos = scanIndex;
|
|
684
|
+
continue;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
// Bareword: scan until next whitespace or special char.
|
|
688
|
+
let scanIndex = pos;
|
|
689
|
+
while (scanIndex < input.length) {
|
|
690
|
+
const innerChar = input[scanIndex];
|
|
691
|
+
if (innerChar === '"') {
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
if (innerChar === "'" && scanIndex > pos && !/[a-zA-Z0-9_]/.test(input[scanIndex - 1])) {
|
|
695
|
+
break;
|
|
696
|
+
}
|
|
697
|
+
if (innerChar === "'" && scanIndex === pos) {
|
|
698
|
+
break;
|
|
699
|
+
}
|
|
700
|
+
if (innerChar !== "'" && SPECIAL_CHARS.test(innerChar)) {
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
if (innerChar === '-' && input[scanIndex + 1] === '>') {
|
|
704
|
+
break;
|
|
705
|
+
}
|
|
706
|
+
if (innerChar === '<' && input[scanIndex + 1] === '-') {
|
|
707
|
+
break;
|
|
708
|
+
}
|
|
709
|
+
scanIndex++;
|
|
710
|
+
}
|
|
711
|
+
// Defensive: if no characters were consumed, advance one to avoid infinite loops.
|
|
712
|
+
if (scanIndex === pos) {
|
|
713
|
+
out.push(currentChar);
|
|
714
|
+
pos++;
|
|
715
|
+
continue;
|
|
716
|
+
}
|
|
717
|
+
const token = input.slice(pos, scanIndex);
|
|
718
|
+
pos = scanIndex;
|
|
719
|
+
|
|
720
|
+
// Operators.
|
|
721
|
+
if (KEYWORDS.has(token.toUpperCase())) {
|
|
722
|
+
out.push(token);
|
|
723
|
+
continue;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Property/type filter (`key:value`).
|
|
727
|
+
const colonIdx = token.indexOf(':');
|
|
728
|
+
if (colonIdx > 0) {
|
|
729
|
+
const key = token.slice(0, colonIdx);
|
|
730
|
+
const rest = token.slice(colonIdx + 1);
|
|
731
|
+
|
|
732
|
+
// type:typename — leave for grammar's TypeFilter (Identifier value).
|
|
733
|
+
if (key === 'type') {
|
|
734
|
+
out.push(token);
|
|
735
|
+
continue;
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
// URLs (`http://...`, `mailto:rich@...`) and URL-paths starting with `//`
|
|
739
|
+
// are searched as text rather than auto-promoted to property filters.
|
|
740
|
+
const isUrlScheme = URL_SCHEMES.has(key.toLowerCase()) || rest.startsWith('//');
|
|
741
|
+
if (!isUrlScheme && PROPERTY_KEY.test(key)) {
|
|
742
|
+
if (rest.length === 0) {
|
|
743
|
+
// Trailing colon while typing — pass through.
|
|
744
|
+
out.push(token);
|
|
745
|
+
continue;
|
|
746
|
+
}
|
|
747
|
+
const firstValueChar = rest[0];
|
|
748
|
+
if (firstValueChar === '"' || firstValueChar === "'") {
|
|
749
|
+
// Already quoted.
|
|
750
|
+
out.push(token);
|
|
751
|
+
continue;
|
|
752
|
+
}
|
|
753
|
+
if (firstValueChar === '{' || firstValueChar === '[') {
|
|
754
|
+
// Object/array literal value.
|
|
755
|
+
out.push(token);
|
|
756
|
+
continue;
|
|
757
|
+
}
|
|
758
|
+
if (VALUE_LITERALS.has(rest) || NUMBER_LITERAL.test(rest)) {
|
|
759
|
+
// Boolean / null / number literal.
|
|
760
|
+
out.push(token);
|
|
761
|
+
continue;
|
|
762
|
+
}
|
|
763
|
+
out.push(`${key}:"${escapeStringLiteral(rest)}"`);
|
|
764
|
+
continue;
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
// URL or unknown key shape — fall through to text.
|
|
768
|
+
}
|
|
769
|
+
|
|
770
|
+
// Identifier followed by `=` is the LHS of an Assignment — keep as-is.
|
|
771
|
+
if (PROPERTY_KEY.test(token)) {
|
|
772
|
+
let lookahead = pos;
|
|
773
|
+
while (lookahead < input.length && /\s/.test(input[lookahead])) {
|
|
774
|
+
lookahead++;
|
|
775
|
+
}
|
|
776
|
+
if (input[lookahead] === '=') {
|
|
777
|
+
out.push(token);
|
|
778
|
+
continue;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
782
|
+
// Bare text fragment: quote so it parses as TextFilter.
|
|
783
|
+
out.push(`"${escapeStringLiteral(token)}"`);
|
|
784
|
+
}
|
|
785
|
+
|
|
786
|
+
return out.join('');
|
|
787
|
+
};
|
|
788
|
+
|
|
789
|
+
/** Escape a raw value into a string literal body (without surrounding quotes). */
|
|
790
|
+
const escapeStringLiteral = (value: string): string => value.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
|
|
791
|
+
|
|
792
|
+
/** Decode `\\` and `\"` escapes inside a string literal body. */
|
|
793
|
+
const unescapeStringLiteral = (literalBody: string): string => {
|
|
794
|
+
let out = '';
|
|
795
|
+
for (let i = 0; i < literalBody.length; i++) {
|
|
796
|
+
const ch = literalBody[i];
|
|
797
|
+
if (ch === '\\' && i + 1 < literalBody.length) {
|
|
798
|
+
out += literalBody[i + 1];
|
|
799
|
+
i++;
|
|
800
|
+
} else {
|
|
801
|
+
out += ch;
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
return out;
|
|
805
|
+
};
|