@dxos/echo-query 0.8.4-main.e99c46d → 0.8.4-main.ead640a
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/README.md +1 -1
- package/dist/lib/browser/index.mjs +528 -0
- package/dist/lib/browser/index.mjs.map +4 -4
- package/dist/lib/browser/meta.json +1 -1
- package/dist/lib/node-esm/index.mjs +528 -0
- package/dist/lib/node-esm/index.mjs.map +4 -4
- package/dist/lib/node-esm/meta.json +1 -1
- package/dist/query-lite/index.js +387 -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 +8 -0
- package/dist/types/src/parser/gen/index.d.ts.map +1 -0
- package/dist/types/src/parser/gen/query.d.ts +3 -0
- package/dist/types/src/parser/gen/query.d.ts.map +1 -0
- package/dist/types/src/parser/gen/query.terms.d.ts +2 -0
- package/dist/types/src/parser/gen/query.terms.d.ts.map +1 -0
- package/dist/types/src/parser/index.d.ts +3 -0
- package/dist/types/src/parser/index.d.ts.map +1 -0
- package/dist/types/src/parser/query-builder.d.ts +74 -0
- package/dist/types/src/parser/query-builder.d.ts.map +1 -0
- package/dist/types/src/parser/query.test.d.ts +2 -0
- package/dist/types/src/parser/query.test.d.ts.map +1 -0
- package/dist/types/src/query-lite/index.d.ts +2 -0
- package/dist/types/src/query-lite/index.d.ts.map +1 -0
- package/dist/types/src/query-lite/query-lite.d.ts +8 -0
- package/dist/types/src/query-lite/query-lite.d.ts.map +1 -0
- package/dist/types/src/sandbox/query-sandbox.d.ts +21 -0
- package/dist/types/src/sandbox/query-sandbox.d.ts.map +1 -0
- package/dist/types/src/sandbox/query-sandbox.test.d.ts +2 -0
- package/dist/types/src/sandbox/query-sandbox.test.d.ts.map +1 -0
- package/dist/types/src/sandbox/quickjs.d.ts +8 -0
- package/dist/types/src/sandbox/quickjs.d.ts.map +1 -0
- package/dist/types/src/sandbox/quickjs.test.d.ts +2 -0
- package/dist/types/src/sandbox/quickjs.test.d.ts.map +1 -0
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +20 -6
- package/src/env.d.ts +8 -0
- package/src/index.ts +2 -0
- package/src/parser/gen/index.ts +13 -0
- package/src/parser/gen/query.terms.ts +26 -0
- package/src/parser/gen/query.ts +18 -0
- package/src/parser/index.ts +6 -0
- package/src/parser/query-builder.ts +486 -0
- package/src/parser/query.grammar +124 -0
- package/src/parser/query.test.ts +363 -0
- package/src/query-lite/index.ts +5 -0
- package/src/query-lite/query-lite.ts +467 -0
- package/src/sandbox/query-sandbox.test.ts +52 -0
- package/src/sandbox/query-sandbox.ts +72 -0
- package/src/sandbox/quickjs.test.ts +67 -0
- package/src/sandbox/quickjs.ts +34 -0
- package/dist/types/src/search.test.d.ts +0 -2
- package/dist/types/src/search.test.d.ts.map +0 -1
- package/src/search.test.ts +0 -49
|
@@ -0,0 +1,467 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import type * as Schema from 'effect/Schema';
|
|
6
|
+
|
|
7
|
+
import type { Filter, Order, Query, Ref } from '@dxos/echo';
|
|
8
|
+
import type * as Echo from '@dxos/echo';
|
|
9
|
+
import type { ForeignKey, QueryAST } from '@dxos/echo-protocol';
|
|
10
|
+
import { assertArgument } from '@dxos/invariant';
|
|
11
|
+
import type { DXN, ObjectId } from '@dxos/keys';
|
|
12
|
+
|
|
13
|
+
//
|
|
14
|
+
// Light-weight implementation of query execution.
|
|
15
|
+
//
|
|
16
|
+
|
|
17
|
+
class OrderClass implements Echo.Order<any> {
|
|
18
|
+
private static variance: Echo.Order<any>['~Order'] = {} as Echo.Order<any>['~Order'];
|
|
19
|
+
|
|
20
|
+
static is(value: unknown): value is Echo.Order<any> {
|
|
21
|
+
return typeof value === 'object' && value !== null && '~Order' in value;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
constructor(public readonly ast: QueryAST.Order) {}
|
|
25
|
+
|
|
26
|
+
'~Order' = OrderClass.variance;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
namespace Order1 {
|
|
30
|
+
export const natural: Echo.Order<any> = new OrderClass({ kind: 'natural' });
|
|
31
|
+
export const property = <T>(property: keyof T & string, direction: QueryAST.OrderDirection): Echo.Order<T> =>
|
|
32
|
+
new OrderClass({
|
|
33
|
+
kind: 'property',
|
|
34
|
+
property,
|
|
35
|
+
direction,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
const Order2: typeof Echo.Order = Order1;
|
|
40
|
+
export { Order2 as Order };
|
|
41
|
+
|
|
42
|
+
class FilterClass implements Echo.Filter<any> {
|
|
43
|
+
private static variance: Echo.Filter<any>['~Filter'] = {} as Echo.Filter<any>['~Filter'];
|
|
44
|
+
|
|
45
|
+
static is(value: unknown): value is Echo.Filter<any> {
|
|
46
|
+
return typeof value === 'object' && value !== null && '~Filter' in value;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
static fromAst(ast: QueryAST.Filter): Filter<any> {
|
|
50
|
+
return new FilterClass(ast);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static everything(): FilterClass {
|
|
54
|
+
return new FilterClass({
|
|
55
|
+
type: 'object',
|
|
56
|
+
typename: null,
|
|
57
|
+
props: {},
|
|
58
|
+
});
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
static nothing(): FilterClass {
|
|
62
|
+
return new FilterClass({
|
|
63
|
+
type: 'not',
|
|
64
|
+
filter: {
|
|
65
|
+
type: 'object',
|
|
66
|
+
typename: null,
|
|
67
|
+
props: {},
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
static relation() {
|
|
73
|
+
return new FilterClass({
|
|
74
|
+
type: 'object',
|
|
75
|
+
typename: null,
|
|
76
|
+
props: {},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
static ids(...ids: ObjectId[]): Echo.Filter<any> {
|
|
81
|
+
// assertArgument(
|
|
82
|
+
// ids.every((id) => ObjectId.isValid(id)),
|
|
83
|
+
// 'ids',
|
|
84
|
+
// 'ids must be valid',
|
|
85
|
+
// );
|
|
86
|
+
|
|
87
|
+
if (ids.length === 0) {
|
|
88
|
+
return FilterClass.nothing();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return new FilterClass({
|
|
92
|
+
type: 'object',
|
|
93
|
+
typename: null,
|
|
94
|
+
id: ids,
|
|
95
|
+
props: {},
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
static type<S extends Schema.Schema.All>(
|
|
100
|
+
schema: S | string,
|
|
101
|
+
props?: Echo.Filter.Props<Schema.Schema.Type<S>>,
|
|
102
|
+
): Echo.Filter<Schema.Schema.Type<S>> {
|
|
103
|
+
if (typeof schema !== 'string') {
|
|
104
|
+
throw new TypeError('expected typename as the first paramter');
|
|
105
|
+
}
|
|
106
|
+
return new FilterClass({
|
|
107
|
+
type: 'object',
|
|
108
|
+
typename: makeTypeDxn(schema),
|
|
109
|
+
...propsFilterToAst(props ?? {}),
|
|
110
|
+
});
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
static typename(typename: string): Echo.Filter<any> {
|
|
114
|
+
return new FilterClass({
|
|
115
|
+
type: 'object',
|
|
116
|
+
typename: makeTypeDxn(typename),
|
|
117
|
+
props: {},
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
static typeDXN(dxn: DXN): Echo.Filter<any> {
|
|
122
|
+
return new FilterClass({
|
|
123
|
+
type: 'object',
|
|
124
|
+
typename: dxn.toString(),
|
|
125
|
+
props: {},
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
static tag(tag: string): Echo.Filter<any> {
|
|
130
|
+
return new FilterClass({
|
|
131
|
+
type: 'tag',
|
|
132
|
+
tag,
|
|
133
|
+
});
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
static props<T>(props: Echo.Filter.Props<T>): Echo.Filter<T> {
|
|
137
|
+
return new FilterClass({
|
|
138
|
+
type: 'object',
|
|
139
|
+
typename: null,
|
|
140
|
+
...propsFilterToAst(props),
|
|
141
|
+
});
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
static text(text: string, options?: Echo.Query.TextSearchOptions): Echo.Filter<any> {
|
|
145
|
+
return new FilterClass({
|
|
146
|
+
type: 'text-search',
|
|
147
|
+
text,
|
|
148
|
+
searchKind: options?.type,
|
|
149
|
+
});
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
static foreignKeys<S extends Schema.Schema.All>(
|
|
153
|
+
schema: S | string,
|
|
154
|
+
keys: ForeignKey[],
|
|
155
|
+
): Echo.Filter<Schema.Schema.Type<S>> {
|
|
156
|
+
assertArgument(typeof schema === 'string', 'schema');
|
|
157
|
+
assertArgument(!schema.startsWith('dxn:'), 'schema');
|
|
158
|
+
return new FilterClass({
|
|
159
|
+
type: 'object',
|
|
160
|
+
typename: `dxn:type:${schema}`,
|
|
161
|
+
props: {},
|
|
162
|
+
foreignKeys: keys,
|
|
163
|
+
});
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
static eq<T>(value: T): Echo.Filter<T> {
|
|
167
|
+
if (!isRef(value) && typeof value === 'object' && value !== null) {
|
|
168
|
+
throw new TypeError('Cannot use object as a value for eq filter');
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
return new FilterClass({
|
|
172
|
+
type: 'compare',
|
|
173
|
+
operator: 'eq',
|
|
174
|
+
value: isRef(value) ? value.noInline().encode() : value,
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
static neq<T>(value: T): Echo.Filter<T> {
|
|
179
|
+
return new FilterClass({
|
|
180
|
+
type: 'compare',
|
|
181
|
+
operator: 'neq',
|
|
182
|
+
value,
|
|
183
|
+
});
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
static gt<T>(value: T): Echo.Filter<T> {
|
|
187
|
+
return new FilterClass({
|
|
188
|
+
type: 'compare',
|
|
189
|
+
operator: 'gt',
|
|
190
|
+
value,
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
static gte<T>(value: T): Echo.Filter<T> {
|
|
195
|
+
return new FilterClass({
|
|
196
|
+
type: 'compare',
|
|
197
|
+
operator: 'gte',
|
|
198
|
+
value,
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
static lt<T>(value: T): Echo.Filter<T> {
|
|
203
|
+
return new FilterClass({
|
|
204
|
+
type: 'compare',
|
|
205
|
+
operator: 'lt',
|
|
206
|
+
value,
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
static lte<T>(value: T): Echo.Filter<T> {
|
|
211
|
+
return new FilterClass({
|
|
212
|
+
type: 'compare',
|
|
213
|
+
operator: 'lte',
|
|
214
|
+
value,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
static in<T>(...values: T[]): Echo.Filter<T> {
|
|
219
|
+
return new FilterClass({
|
|
220
|
+
type: 'in',
|
|
221
|
+
values,
|
|
222
|
+
});
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
static contains<T>(value: T): Echo.Filter<readonly T[] | undefined> {
|
|
226
|
+
return new FilterClass({
|
|
227
|
+
type: 'contains',
|
|
228
|
+
value,
|
|
229
|
+
});
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
static between<T>(from: T, to: T): Echo.Filter<unknown> {
|
|
233
|
+
return new FilterClass({
|
|
234
|
+
type: 'range',
|
|
235
|
+
from,
|
|
236
|
+
to,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
static not<F extends Echo.Filter.Any>(filter: F): Echo.Filter<Echo.Filter.Type<F>> {
|
|
241
|
+
return new FilterClass({
|
|
242
|
+
type: 'not',
|
|
243
|
+
filter: filter.ast,
|
|
244
|
+
});
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
static and<T>(...filters: Echo.Filter<T>[]): Echo.Filter<T> {
|
|
248
|
+
return new FilterClass({
|
|
249
|
+
type: 'and',
|
|
250
|
+
filters: filters.map((f) => f.ast),
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
static or<T>(...filters: Echo.Filter<T>[]): Echo.Filter<T> {
|
|
255
|
+
return new FilterClass({
|
|
256
|
+
type: 'or',
|
|
257
|
+
filters: filters.map((f) => f.ast),
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
private constructor(public readonly ast: QueryAST.Filter) {}
|
|
262
|
+
|
|
263
|
+
'~Filter' = FilterClass.variance;
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
export const Filter1: typeof Echo.Filter = FilterClass;
|
|
267
|
+
export { Filter1 as Filter };
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* All property paths inside T that are references.
|
|
271
|
+
*/
|
|
272
|
+
// TODO(dmaretskyi): Filter only properties that are references (or optional references, or unions that include references).
|
|
273
|
+
type RefPropKey<T> = keyof T & string;
|
|
274
|
+
|
|
275
|
+
const propsFilterToAst = (predicates: Echo.Filter.Props<any>): Pick<QueryAST.FilterObject, 'id' | 'props'> => {
|
|
276
|
+
let idFilter: readonly ObjectId[] | undefined;
|
|
277
|
+
if ('id' in predicates) {
|
|
278
|
+
assertArgument(
|
|
279
|
+
typeof predicates.id === 'string' || Array.isArray(predicates.id),
|
|
280
|
+
'predicates.id',
|
|
281
|
+
'invalid id filter',
|
|
282
|
+
);
|
|
283
|
+
idFilter = typeof predicates.id === 'string' ? [predicates.id] : predicates.id;
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
return {
|
|
287
|
+
id: idFilter,
|
|
288
|
+
props: Object.fromEntries(
|
|
289
|
+
Object.entries(predicates)
|
|
290
|
+
.filter(([prop, _value]) => prop !== 'id')
|
|
291
|
+
.map(([prop, predicate]) => [prop, processPredicate(predicate)]),
|
|
292
|
+
) as Record<string, QueryAST.Filter>,
|
|
293
|
+
};
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
const processPredicate = (predicate: any): QueryAST.Filter => {
|
|
297
|
+
if (FilterClass.is(predicate)) {
|
|
298
|
+
return predicate.ast;
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (Array.isArray(predicate)) {
|
|
302
|
+
throw new Error('Array predicates are not yet supported.');
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
if (!isRef(predicate) && typeof predicate === 'object' && predicate !== null) {
|
|
306
|
+
const nestedProps = Object.fromEntries(
|
|
307
|
+
Object.entries(predicate).map(([key, value]) => [key, processPredicate(value)]),
|
|
308
|
+
);
|
|
309
|
+
|
|
310
|
+
return {
|
|
311
|
+
type: 'object',
|
|
312
|
+
typename: null,
|
|
313
|
+
props: nestedProps,
|
|
314
|
+
};
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return FilterClass.eq(predicate).ast;
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
class QueryClass implements Echo.Query<any> {
|
|
321
|
+
private static variance: Echo.Query<any>['~Query'] = {} as Echo.Query<any>['~Query'];
|
|
322
|
+
|
|
323
|
+
static is(value: unknown): value is Echo.Query<any> {
|
|
324
|
+
return typeof value === 'object' && value !== null && '~Query' in value;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
static fromAst(ast: QueryAST.Query): Echo.Query<any> {
|
|
328
|
+
return new QueryClass(ast);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
static select<F extends Echo.Filter.Any>(filter: F): Echo.Query<Echo.Filter.Type<F>> {
|
|
332
|
+
return new QueryClass({
|
|
333
|
+
type: 'select',
|
|
334
|
+
filter: filter.ast,
|
|
335
|
+
});
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
static type(schema: Schema.Schema.All | string, predicates?: Echo.Filter.Props<unknown>): Query<any> {
|
|
339
|
+
return new QueryClass({
|
|
340
|
+
type: 'select',
|
|
341
|
+
filter: FilterClass.type(schema, predicates).ast,
|
|
342
|
+
});
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
static all(...queries: Query<any>[]): Query<any> {
|
|
346
|
+
if (queries.length === 0) {
|
|
347
|
+
throw new TypeError(
|
|
348
|
+
'Query.all combines results of multiple queries, to query all objects use Query.select(Filter.everything())',
|
|
349
|
+
);
|
|
350
|
+
}
|
|
351
|
+
return new QueryClass({
|
|
352
|
+
type: 'union',
|
|
353
|
+
queries: queries.map((q) => q.ast),
|
|
354
|
+
});
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
static without<T>(source: Query<T>, exclude: Query<T>): Query<T> {
|
|
358
|
+
return new QueryClass({
|
|
359
|
+
type: 'set-difference',
|
|
360
|
+
source: source.ast,
|
|
361
|
+
exclude: exclude.ast,
|
|
362
|
+
});
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
constructor(public readonly ast: QueryAST.Query) {}
|
|
366
|
+
|
|
367
|
+
'~Query' = QueryClass.variance;
|
|
368
|
+
|
|
369
|
+
select(filter: Filter<any> | Filter.Props<any>): Query<any> {
|
|
370
|
+
if (FilterClass.is(filter)) {
|
|
371
|
+
return new QueryClass({
|
|
372
|
+
type: 'filter',
|
|
373
|
+
selection: this.ast,
|
|
374
|
+
filter: filter.ast,
|
|
375
|
+
});
|
|
376
|
+
} else {
|
|
377
|
+
return new QueryClass({
|
|
378
|
+
type: 'filter',
|
|
379
|
+
selection: this.ast,
|
|
380
|
+
filter: FilterClass.props(filter).ast,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
reference(key: string): Query<any> {
|
|
386
|
+
return new QueryClass({
|
|
387
|
+
type: 'reference-traversal',
|
|
388
|
+
anchor: this.ast,
|
|
389
|
+
property: key,
|
|
390
|
+
});
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
referencedBy(target: Schema.Schema.All | string, key: string): Query<any> {
|
|
394
|
+
assertArgument(typeof target === 'string', 'target');
|
|
395
|
+
assertArgument(!target.startsWith('dxn:'), 'target');
|
|
396
|
+
return new QueryClass({
|
|
397
|
+
type: 'incoming-references',
|
|
398
|
+
anchor: this.ast,
|
|
399
|
+
property: key,
|
|
400
|
+
typename: target,
|
|
401
|
+
});
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
sourceOf(relation: Schema.Schema.All | string, predicates?: Filter.Props<unknown> | undefined): Query<any> {
|
|
405
|
+
return new QueryClass({
|
|
406
|
+
type: 'relation',
|
|
407
|
+
anchor: this.ast,
|
|
408
|
+
direction: 'outgoing',
|
|
409
|
+
filter: FilterClass.type(relation, predicates).ast,
|
|
410
|
+
});
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
targetOf(relation: Schema.Schema.All | string, predicates?: Filter.Props<unknown> | undefined): Query<any> {
|
|
414
|
+
return new QueryClass({
|
|
415
|
+
type: 'relation',
|
|
416
|
+
anchor: this.ast,
|
|
417
|
+
direction: 'incoming',
|
|
418
|
+
filter: FilterClass.type(relation, predicates).ast,
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
source(): Query<any> {
|
|
423
|
+
return new QueryClass({
|
|
424
|
+
type: 'relation-traversal',
|
|
425
|
+
anchor: this.ast,
|
|
426
|
+
direction: 'source',
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
target(): Query<any> {
|
|
431
|
+
return new QueryClass({
|
|
432
|
+
type: 'relation-traversal',
|
|
433
|
+
anchor: this.ast,
|
|
434
|
+
direction: 'target',
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
orderBy(...order: Order<any>[]): Query<any> {
|
|
439
|
+
return new QueryClass({
|
|
440
|
+
type: 'order',
|
|
441
|
+
query: this.ast,
|
|
442
|
+
order: order.map((o) => o.ast),
|
|
443
|
+
});
|
|
444
|
+
}
|
|
445
|
+
|
|
446
|
+
options(options: QueryAST.QueryOptions): Query<any> {
|
|
447
|
+
return new QueryClass({
|
|
448
|
+
type: 'options',
|
|
449
|
+
query: this.ast,
|
|
450
|
+
options,
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
|
|
455
|
+
export const Query1: typeof Echo.Query = QueryClass;
|
|
456
|
+
export { Query1 as Query };
|
|
457
|
+
|
|
458
|
+
const RefTypeId: unique symbol = Symbol('@dxos/echo-query/Ref');
|
|
459
|
+
const isRef = (obj: any): obj is Ref.Ref<any> => {
|
|
460
|
+
return obj && typeof obj === 'object' && RefTypeId in obj;
|
|
461
|
+
};
|
|
462
|
+
|
|
463
|
+
const makeTypeDxn = (typename: string) => {
|
|
464
|
+
assertArgument(typeof typename === 'string', 'typename');
|
|
465
|
+
assertArgument(!typename.startsWith('dxn:'), 'typename');
|
|
466
|
+
return `dxn:type:${typename}`;
|
|
467
|
+
};
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { afterAll, beforeAll, describe, expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { Filter, Order, Query } from '@dxos/echo';
|
|
8
|
+
|
|
9
|
+
import { QuerySandbox } from './query-sandbox';
|
|
10
|
+
|
|
11
|
+
describe('QuerySandbox', () => {
|
|
12
|
+
const sandbox = new QuerySandbox();
|
|
13
|
+
beforeAll(() => sandbox.open());
|
|
14
|
+
afterAll(() => sandbox.close());
|
|
15
|
+
|
|
16
|
+
test('works', { timeout: 10_000 }, async () => {
|
|
17
|
+
const ast = sandbox.eval(`
|
|
18
|
+
Query.select(Filter.typename('dxos.org/type/Person'))
|
|
19
|
+
`);
|
|
20
|
+
expect(ast).toEqual(Query.select(Filter.typename('dxos.org/type/Person')).ast);
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
test('works with just Filter passed in', () => {
|
|
24
|
+
const ast = sandbox.eval(`
|
|
25
|
+
Filter.typename('dxos.org/type/Person')
|
|
26
|
+
`);
|
|
27
|
+
expect(ast).toEqual(Query.select(Filter.typename('dxos.org/type/Person')).ast);
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test('Order', () => {
|
|
31
|
+
const ast = sandbox.eval(`
|
|
32
|
+
Query.type('dxos.org/type/Person').orderBy(Order.property('name', 'desc'))
|
|
33
|
+
`);
|
|
34
|
+
expect(ast).toEqual(Query.type('dxos.org/type/Person').orderBy(Order.property('name', 'desc')).ast);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test('traversal', () => {
|
|
38
|
+
const ast = sandbox.eval(`
|
|
39
|
+
Query.select(Filter.type('example.com/type/Person', { jobTitle: 'investor' }))
|
|
40
|
+
.reference('organization')
|
|
41
|
+
.targetOf('example.com/relation/ResearchOn')
|
|
42
|
+
.source()
|
|
43
|
+
`);
|
|
44
|
+
|
|
45
|
+
expect(ast).toEqual(
|
|
46
|
+
Query.select(Filter.type('example.com/type/Person', { jobTitle: 'investor' }))
|
|
47
|
+
.reference('organization')
|
|
48
|
+
.targetOf('example.com/relation/ResearchOn')
|
|
49
|
+
.source().ast,
|
|
50
|
+
);
|
|
51
|
+
});
|
|
52
|
+
});
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { Resource } from '@dxos/context';
|
|
6
|
+
import { Query, type QueryAST } from '@dxos/echo';
|
|
7
|
+
import { trim } from '@dxos/util';
|
|
8
|
+
import { type QuickJSRuntime, type QuickJSWASMModule, createQuickJS } from '@dxos/vendor-quickjs';
|
|
9
|
+
|
|
10
|
+
import envCode from '#query-lite?raw';
|
|
11
|
+
|
|
12
|
+
import { unwrapResult } from './quickjs';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Evaluates queries written in JavaScript using QuickJS.
|
|
16
|
+
* Queries are expected to use the echo Query API.
|
|
17
|
+
* `Query`, `Filter` and `Order` are provided as globals.
|
|
18
|
+
*/
|
|
19
|
+
export class QuerySandbox extends Resource {
|
|
20
|
+
// Caching the wasm module.
|
|
21
|
+
private static quickJS: Promise<QuickJSWASMModule> | null = null;
|
|
22
|
+
static getQuickJS() {
|
|
23
|
+
if (!QuerySandbox.quickJS) {
|
|
24
|
+
QuerySandbox.quickJS = createQuickJS();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return QuerySandbox.quickJS;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
#runtime!: QuickJSRuntime;
|
|
31
|
+
|
|
32
|
+
protected override async _open() {
|
|
33
|
+
const quickJS = await QuerySandbox.getQuickJS();
|
|
34
|
+
this.#runtime = quickJS.newRuntime({
|
|
35
|
+
moduleLoader: (moduleName, _context) => {
|
|
36
|
+
switch (moduleName) {
|
|
37
|
+
case 'dxos:query-lite':
|
|
38
|
+
return envCode;
|
|
39
|
+
default:
|
|
40
|
+
throw new Error(`Module not found: ${moduleName}`);
|
|
41
|
+
}
|
|
42
|
+
},
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
protected override async _close() {
|
|
47
|
+
this.#runtime.dispose();
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Evaluates the query code.
|
|
52
|
+
* @param queryCode Example: `Query.select(Filter.typename('dxos.org/type/Person'))`
|
|
53
|
+
*/
|
|
54
|
+
eval(queryCode: string): QueryAST.Query {
|
|
55
|
+
using context = this.#runtime.newContext();
|
|
56
|
+
const globals = trim`
|
|
57
|
+
import { Filter, Order, Query } from 'dxos:query-lite';
|
|
58
|
+
globalThis.Filter = Filter;
|
|
59
|
+
globalThis.Order = Order;
|
|
60
|
+
globalThis.Query = Query;
|
|
61
|
+
`;
|
|
62
|
+
|
|
63
|
+
unwrapResult(context, context.evalCode(globals)).dispose();
|
|
64
|
+
using query = unwrapResult(context, context.evalCode(queryCode));
|
|
65
|
+
const result = context.dump(query);
|
|
66
|
+
if ('~Filter' in result) {
|
|
67
|
+
return Query.select(result).ast;
|
|
68
|
+
} else {
|
|
69
|
+
return result.ast;
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { expect, test } from 'vitest';
|
|
6
|
+
|
|
7
|
+
import { createQuickJS } from '@dxos/vendor-quickjs';
|
|
8
|
+
|
|
9
|
+
import { unwrapResult } from './quickjs';
|
|
10
|
+
|
|
11
|
+
test('works', async () => {
|
|
12
|
+
const QuickJS = await createQuickJS();
|
|
13
|
+
using context = QuickJS.newContext();
|
|
14
|
+
|
|
15
|
+
using world = context.newString('world');
|
|
16
|
+
context.setProp(context.global, 'NAME', world);
|
|
17
|
+
|
|
18
|
+
using result = unwrapResult(context, context.evalCode(`"Hello " + NAME + "!"`));
|
|
19
|
+
expect(context.dump(result)).toBe('Hello world!');
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
test('errors get propagated', async () => {
|
|
23
|
+
const QuickJS = await createQuickJS();
|
|
24
|
+
using runtime = QuickJS.newRuntime();
|
|
25
|
+
using context = runtime.newContext();
|
|
26
|
+
expect(() => unwrapResult(context, context.evalCode(`throw new Error('test')`))).toThrow();
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
test('global variables are shared in a context', async () => {
|
|
30
|
+
const QuickJS = await createQuickJS();
|
|
31
|
+
const context = QuickJS.newContext();
|
|
32
|
+
|
|
33
|
+
unwrapResult(context, context.evalCode('globalThis.name = "world"')).dispose();
|
|
34
|
+
using result = unwrapResult(context, context.evalCode(`"Hello " + name + "!"`));
|
|
35
|
+
expect(context.dump(result)).toBe('Hello world!');
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
test('global variables are shared in a context created from runtime', async () => {
|
|
39
|
+
const QuickJS = await createQuickJS();
|
|
40
|
+
using runtime = QuickJS.newRuntime();
|
|
41
|
+
using context = runtime.newContext();
|
|
42
|
+
|
|
43
|
+
unwrapResult(context, context.evalCode('globalThis.name = "world"'));
|
|
44
|
+
using result = unwrapResult(context, context.evalCode(`"Hello " + name + "!"`));
|
|
45
|
+
expect(context.dump(result)).toBe('Hello world!');
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
test('load module', async () => {
|
|
49
|
+
const QuickJS = await createQuickJS();
|
|
50
|
+
using runtime = QuickJS.newRuntime({
|
|
51
|
+
moduleLoader: (name, _context) => {
|
|
52
|
+
switch (name) {
|
|
53
|
+
case 'test:name':
|
|
54
|
+
return `
|
|
55
|
+
export const name = 'world';
|
|
56
|
+
`;
|
|
57
|
+
default:
|
|
58
|
+
throw new Error('unknown module');
|
|
59
|
+
}
|
|
60
|
+
},
|
|
61
|
+
});
|
|
62
|
+
using context = runtime.newContext();
|
|
63
|
+
|
|
64
|
+
unwrapResult(context, context.evalCode('import { name } from "test:name"; globalThis.name = name;')).dispose();
|
|
65
|
+
using result = unwrapResult(context, context.evalCode(`"Hello " + name + "!"`));
|
|
66
|
+
expect(context.dump(result)).toBe('Hello world!');
|
|
67
|
+
});
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2025 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { type QuickJSContext, type QuickJSHandle, type SuccessOrFail } from '@dxos/vendor-quickjs';
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Unwraps a result and throws the underlying error.
|
|
9
|
+
*
|
|
10
|
+
* Replacement for `QuickJScontext.unwrapResult` because that seems to cause an OOM.
|
|
11
|
+
*/
|
|
12
|
+
// TODO(burdon): Factor out.
|
|
13
|
+
export const unwrapResult = <T>(context: QuickJSContext, result: SuccessOrFail<T, QuickJSHandle>): T => {
|
|
14
|
+
if (result.error) {
|
|
15
|
+
const contextError = context.dump(result.error);
|
|
16
|
+
result.error.dispose();
|
|
17
|
+
if (
|
|
18
|
+
typeof contextError === 'object' &&
|
|
19
|
+
typeof contextError.name === 'string' &&
|
|
20
|
+
typeof contextError.message === 'string' &&
|
|
21
|
+
typeof contextError.stack === 'string'
|
|
22
|
+
) {
|
|
23
|
+
const error = new Error(contextError.message);
|
|
24
|
+
Object.defineProperty(error, 'name', { value: contextError.name });
|
|
25
|
+
const originalStack = error.stack;
|
|
26
|
+
error.stack = `${contextError.name}: ${contextError.message}\n${contextError.stack}${originalStack?.split('\n').slice(1).join('\n') ?? ''}`;
|
|
27
|
+
throw error;
|
|
28
|
+
} else {
|
|
29
|
+
throw new Error(String(contextError));
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
return result.value;
|
|
34
|
+
};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"search.test.d.ts","sourceRoot":"","sources":["../../../src/search.test.ts"],"names":[],"mappings":""}
|