@dxos/echo-query 0.8.4-main.ef1bc66f44 → 0.8.4-main.effb148878
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/neutral/index.mjs +216 -18
- package/dist/lib/neutral/index.mjs.map +3 -3
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/query-lite/index.d.ts +1702 -515
- package/dist/query-lite/index.d.ts.map +1 -1
- package/dist/query-lite/index.js +201 -22
- package/dist/query-lite/index.js.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.map +1 -1
- 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 +17 -20
- package/src/parser/query-builder.ts +269 -9
- package/src/parser/query.test.ts +144 -31
- package/src/query-lite/query-lite.ts +336 -43
- package/src/sandbox/query-sandbox.test.ts +10 -10
- package/src/sandbox/query-sandbox.ts +1 -1
|
@@ -4,10 +4,12 @@
|
|
|
4
4
|
|
|
5
5
|
import type * as Schema from 'effect/Schema';
|
|
6
6
|
|
|
7
|
-
import type { Filter as Filter$, Order as Order$, Query as Query$, Ref } from '@dxos/echo';
|
|
7
|
+
import type { Filter as Filter$, Obj as Obj$, Order as Order$, Query as Query$, Ref, Type as Type$ } from '@dxos/echo';
|
|
8
8
|
import type { ForeignKey, QueryAST } from '@dxos/echo-protocol';
|
|
9
9
|
import { assertArgument } from '@dxos/invariant';
|
|
10
|
-
|
|
10
|
+
// `DXN`/`EchoURI` are type-only imports to keep the `query-lite` bundle free of
|
|
11
|
+
// `effect/Schema` (which pulls runtime helpers QuickJS can't parse — e.g. private class fields).
|
|
12
|
+
import type { DXN, EchoURI, ObjectId, URI } from '@dxos/keys';
|
|
11
13
|
|
|
12
14
|
//
|
|
13
15
|
// Light-weight implementation of query execution.
|
|
@@ -16,7 +18,7 @@ import type { DXN, ObjectId } from '@dxos/keys';
|
|
|
16
18
|
// TODO(wittjosiah): The `export * as ...` syntax causes tsdown to genereate multiple files which breaks the sandbox.
|
|
17
19
|
|
|
18
20
|
class OrderClass implements Order$.Any {
|
|
19
|
-
private static variance: Order$.Any['~Order'] = {} as Order$.Any['~Order'];
|
|
21
|
+
private static 'variance': Order$.Any['~Order'] = {} as Order$.Any['~Order'];
|
|
20
22
|
|
|
21
23
|
static is(value: unknown): value is Order$.Any {
|
|
22
24
|
return typeof value === 'object' && value !== null && '~Order' in value;
|
|
@@ -46,7 +48,7 @@ const Order2: typeof Order$ = Order1;
|
|
|
46
48
|
export { Order2 as Order };
|
|
47
49
|
|
|
48
50
|
class FilterClass implements Filter$.Any {
|
|
49
|
-
private static variance: Filter$.Any['~Filter'] = {} as Filter$.Any['~Filter'];
|
|
51
|
+
private static 'variance': Filter$.Any['~Filter'] = {} as Filter$.Any['~Filter'];
|
|
50
52
|
|
|
51
53
|
static is(value: unknown): value is Filter$.Any {
|
|
52
54
|
return typeof value === 'object' && value !== null && '~Filter' in value;
|
|
@@ -102,10 +104,12 @@ class FilterClass implements Filter$.Any {
|
|
|
102
104
|
});
|
|
103
105
|
}
|
|
104
106
|
|
|
105
|
-
static type<
|
|
106
|
-
|
|
107
|
-
props?: Filter$.Props<
|
|
108
|
-
): Filter$.Filter<
|
|
107
|
+
static type<T extends Type$.AnyEntity>(
|
|
108
|
+
type: T,
|
|
109
|
+
props?: Filter$.Props<Type$.InstanceType<T>>,
|
|
110
|
+
): Filter$.Filter<Type$.InstanceType<T>>;
|
|
111
|
+
static type(schema: string, props?: Filter$.Props<unknown>): Filter$.Filter<any>;
|
|
112
|
+
static type(schema: Type$.AnyEntity | string, props?: Filter$.Props<unknown>): Filter$.Filter<unknown> {
|
|
109
113
|
if (typeof schema !== 'string') {
|
|
110
114
|
throw new TypeError('expected typename as the first paramter');
|
|
111
115
|
}
|
|
@@ -124,10 +128,10 @@ class FilterClass implements Filter$.Any {
|
|
|
124
128
|
});
|
|
125
129
|
}
|
|
126
130
|
|
|
127
|
-
static
|
|
131
|
+
static typeURI(uri: URI.URI): Filter$.Any {
|
|
128
132
|
return new FilterClass({
|
|
129
133
|
type: 'object',
|
|
130
|
-
typename:
|
|
134
|
+
typename: uri,
|
|
131
135
|
props: {},
|
|
132
136
|
});
|
|
133
137
|
}
|
|
@@ -139,6 +143,16 @@ class FilterClass implements Filter$.Any {
|
|
|
139
143
|
});
|
|
140
144
|
}
|
|
141
145
|
|
|
146
|
+
static key(key: string, options?: Filter$.KeyFilterOptions): Filter$.Any {
|
|
147
|
+
return new FilterClass({
|
|
148
|
+
type: 'object',
|
|
149
|
+
typename: null,
|
|
150
|
+
props: {},
|
|
151
|
+
metaKey: key,
|
|
152
|
+
metaVersion: options?.version,
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
142
156
|
static props<T>(props: Filter$.Props<T>): Filter$.Filter<T> {
|
|
143
157
|
return new FilterClass({
|
|
144
158
|
type: 'object',
|
|
@@ -155,15 +169,15 @@ class FilterClass implements Filter$.Any {
|
|
|
155
169
|
});
|
|
156
170
|
}
|
|
157
171
|
|
|
158
|
-
static foreignKeys<S extends
|
|
159
|
-
schema: S
|
|
172
|
+
static foreignKeys<S extends Type$.AnyEntity | string>(
|
|
173
|
+
schema: S,
|
|
160
174
|
keys: ForeignKey[],
|
|
161
|
-
): Filter$.Filter<
|
|
175
|
+
): Filter$.Filter<S extends Type$.AnyEntity ? Type$.InstanceType<S> : unknown> {
|
|
162
176
|
assertArgument(typeof schema === 'string', 'schema');
|
|
163
177
|
assertArgument(!schema.startsWith('dxn:'), 'schema');
|
|
164
178
|
return new FilterClass({
|
|
165
179
|
type: 'object',
|
|
166
|
-
typename:
|
|
180
|
+
typename: makeTypeDxn(schema),
|
|
167
181
|
props: {},
|
|
168
182
|
foreignKeys: keys,
|
|
169
183
|
});
|
|
@@ -221,7 +235,7 @@ class FilterClass implements Filter$.Any {
|
|
|
221
235
|
});
|
|
222
236
|
}
|
|
223
237
|
|
|
224
|
-
static in<T>(...values: T[]): Filter$.Filter<T
|
|
238
|
+
static in<T>(...values: T[]): Filter$.Filter<T> {
|
|
225
239
|
return new FilterClass({
|
|
226
240
|
type: 'in',
|
|
227
241
|
values,
|
|
@@ -235,7 +249,7 @@ class FilterClass implements Filter$.Any {
|
|
|
235
249
|
});
|
|
236
250
|
}
|
|
237
251
|
|
|
238
|
-
static between<T>(from: T, to: T): Filter$.Filter<
|
|
252
|
+
static between<T>(from: T, to: T): Filter$.Filter<T> {
|
|
239
253
|
return new FilterClass({
|
|
240
254
|
type: 'range',
|
|
241
255
|
from,
|
|
@@ -243,6 +257,47 @@ class FilterClass implements Filter$.Any {
|
|
|
243
257
|
});
|
|
244
258
|
}
|
|
245
259
|
|
|
260
|
+
static updated(range: { after?: Date | number; before?: Date | number }): Filter$.Any {
|
|
261
|
+
return FilterClass._timeRangeFilter('updatedAt', range);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
static created(range: { after?: Date | number; before?: Date | number }): Filter$.Any {
|
|
265
|
+
return FilterClass._timeRangeFilter('createdAt', range);
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
static childOf(parents: unknown | unknown[], options?: { transitive?: boolean }): Filter$.Any {
|
|
269
|
+
const items = Array.isArray(parents) ? parents : [parents];
|
|
270
|
+
const dxns = items.map((item) => {
|
|
271
|
+
if (isEchoUriLike(item)) {
|
|
272
|
+
return item.toString() as EchoURI.EchoURI;
|
|
273
|
+
}
|
|
274
|
+
throw new TypeError('childOf requires EchoURI values in query-lite');
|
|
275
|
+
});
|
|
276
|
+
return new FilterClass({
|
|
277
|
+
type: 'child-of',
|
|
278
|
+
parents: dxns,
|
|
279
|
+
transitive: options?.transitive ?? true,
|
|
280
|
+
});
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
private static _timeRangeFilter(
|
|
284
|
+
field: 'updatedAt' | 'createdAt',
|
|
285
|
+
range: { after?: Date | number; before?: Date | number },
|
|
286
|
+
): Filter$.Any {
|
|
287
|
+
const toMs = (d: Date | number) => (typeof d === 'number' ? d : d.getTime());
|
|
288
|
+
const filters: Filter$.Any[] = [];
|
|
289
|
+
if (range.after != null) {
|
|
290
|
+
filters.push(new FilterClass({ type: 'timestamp', field, operator: 'gte', value: toMs(range.after) }));
|
|
291
|
+
}
|
|
292
|
+
if (range.before != null) {
|
|
293
|
+
filters.push(new FilterClass({ type: 'timestamp', field, operator: 'lte', value: toMs(range.before) }));
|
|
294
|
+
}
|
|
295
|
+
if (filters.length === 0) {
|
|
296
|
+
return FilterClass.everything();
|
|
297
|
+
}
|
|
298
|
+
return filters.length === 1 ? filters[0] : FilterClass.and(...filters);
|
|
299
|
+
}
|
|
300
|
+
|
|
246
301
|
static not<F extends Filter$.Any>(filter: F): Filter$.Filter<Filter$.Type<F>> {
|
|
247
302
|
return new FilterClass({
|
|
248
303
|
type: 'not',
|
|
@@ -268,6 +323,11 @@ class FilterClass implements Filter$.Any {
|
|
|
268
323
|
});
|
|
269
324
|
}
|
|
270
325
|
|
|
326
|
+
/** Returns a human-readable string representation of a Filter AST. */
|
|
327
|
+
static pretty(filter: Filter$.Any): string {
|
|
328
|
+
return prettyFilter(filter.ast);
|
|
329
|
+
}
|
|
330
|
+
|
|
271
331
|
private constructor(public readonly ast: QueryAST.Filter) {}
|
|
272
332
|
|
|
273
333
|
'~Filter' = FilterClass.variance;
|
|
@@ -328,7 +388,7 @@ const processPredicate = (predicate: any): QueryAST.Filter => {
|
|
|
328
388
|
};
|
|
329
389
|
|
|
330
390
|
class QueryClass implements Query$.Any {
|
|
331
|
-
private static variance: Query$.Any['~Query'] = {} as Query$.Any['~Query'];
|
|
391
|
+
private static 'variance': Query$.Any['~Query'] = {} as Query$.Any['~Query'];
|
|
332
392
|
|
|
333
393
|
static is(value: unknown): value is Query$.Any {
|
|
334
394
|
return typeof value === 'object' && value !== null && '~Query' in value;
|
|
@@ -345,7 +405,32 @@ class QueryClass implements Query$.Any {
|
|
|
345
405
|
});
|
|
346
406
|
}
|
|
347
407
|
|
|
348
|
-
|
|
408
|
+
select(filter: Filter$.Any | Filter$.Props<any>): Query$.Any {
|
|
409
|
+
if (FilterClass.is(filter)) {
|
|
410
|
+
return new QueryClass({
|
|
411
|
+
type: 'filter',
|
|
412
|
+
selection: this.ast,
|
|
413
|
+
filter: filter.ast,
|
|
414
|
+
});
|
|
415
|
+
} else {
|
|
416
|
+
return new QueryClass({
|
|
417
|
+
type: 'filter',
|
|
418
|
+
selection: this.ast,
|
|
419
|
+
filter: FilterClass.props(filter).ast,
|
|
420
|
+
});
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
|
|
424
|
+
static type<S extends Schema.Schema.All>(
|
|
425
|
+
schema: S,
|
|
426
|
+
predicates?: Filter$.Props<Schema.Schema.Type<S>>,
|
|
427
|
+
): Query$.Query<Schema.Schema.Type<S>>;
|
|
428
|
+
static type(type: Type$.Type, predicates?: Filter$.Props<Obj$.Unknown>): Query$.Query<Obj$.Unknown>;
|
|
429
|
+
static type(schema: string, predicates?: Filter$.Props<unknown>): Query$.Query<any>;
|
|
430
|
+
static type(schema: Schema.Schema.All | Type$.Type | string, predicates?: Filter$.Props<unknown>): Query$.Any {
|
|
431
|
+
if (typeof schema !== 'string') {
|
|
432
|
+
throw new TypeError('expected typename as the first paramter');
|
|
433
|
+
}
|
|
349
434
|
return new QueryClass({
|
|
350
435
|
type: 'select',
|
|
351
436
|
filter: FilterClass.type(schema, predicates).ast,
|
|
@@ -372,26 +457,49 @@ class QueryClass implements Query$.Any {
|
|
|
372
457
|
});
|
|
373
458
|
}
|
|
374
459
|
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
460
|
+
static from(source: any, options?: { includeFeeds?: boolean }): Query$.Any {
|
|
461
|
+
const baseQuery: QueryAST.Query = {
|
|
462
|
+
type: 'select',
|
|
463
|
+
filter: FilterClass.everything().ast,
|
|
464
|
+
};
|
|
465
|
+
const wrapper = new QueryClass(baseQuery);
|
|
466
|
+
return wrapper.from(source, options);
|
|
467
|
+
}
|
|
378
468
|
|
|
379
|
-
|
|
380
|
-
if (
|
|
469
|
+
from(arg: any, options?: { includeFeeds?: boolean }): Query$.Any {
|
|
470
|
+
if (arg === 'all-accessible-spaces') {
|
|
381
471
|
return new QueryClass({
|
|
382
|
-
type: '
|
|
383
|
-
|
|
384
|
-
|
|
472
|
+
type: 'from',
|
|
473
|
+
query: this.ast,
|
|
474
|
+
from: {
|
|
475
|
+
_tag: 'scope',
|
|
476
|
+
scope: {
|
|
477
|
+
...(options?.includeFeeds ? { allFeedsFromSpaces: true } : {}),
|
|
478
|
+
},
|
|
479
|
+
},
|
|
385
480
|
});
|
|
386
|
-
}
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
if (_isScopeLike(arg)) {
|
|
387
484
|
return new QueryClass({
|
|
388
|
-
type: '
|
|
389
|
-
|
|
390
|
-
|
|
485
|
+
type: 'from',
|
|
486
|
+
query: this.ast,
|
|
487
|
+
from: { _tag: 'scope', scope: arg },
|
|
391
488
|
});
|
|
392
489
|
}
|
|
490
|
+
|
|
491
|
+
throw new TypeError('Database and Feed objects are not supported in query-lite sandbox');
|
|
393
492
|
}
|
|
394
493
|
|
|
494
|
+
/** Returns a human-readable string representation of a Query AST. */
|
|
495
|
+
static pretty(query: Query$.Any): string {
|
|
496
|
+
return prettyQuery(query.ast);
|
|
497
|
+
}
|
|
498
|
+
|
|
499
|
+
constructor(public readonly ast: QueryAST.Query) {}
|
|
500
|
+
|
|
501
|
+
'~Query' = QueryClass.variance;
|
|
502
|
+
|
|
395
503
|
reference(key: string): Query$.Any {
|
|
396
504
|
return new QueryClass({
|
|
397
505
|
type: 'reference-traversal',
|
|
@@ -400,13 +508,11 @@ class QueryClass implements Query$.Any {
|
|
|
400
508
|
});
|
|
401
509
|
}
|
|
402
510
|
|
|
403
|
-
referencedBy(target?:
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
target)
|
|
409
|
-
: null;
|
|
511
|
+
referencedBy(target?: Type$.AnyEntity | string, key?: string): Query$.Any {
|
|
512
|
+
if (target !== undefined && typeof target !== 'string') {
|
|
513
|
+
throw new TypeError('referencedBy requires a typename string in query-lite');
|
|
514
|
+
}
|
|
515
|
+
const typename = target !== undefined ? makeTypeDxn(target) : null;
|
|
410
516
|
return new QueryClass({
|
|
411
517
|
type: 'incoming-references',
|
|
412
518
|
anchor: this.ast,
|
|
@@ -415,21 +521,37 @@ class QueryClass implements Query$.Any {
|
|
|
415
521
|
});
|
|
416
522
|
}
|
|
417
523
|
|
|
418
|
-
sourceOf(
|
|
524
|
+
sourceOf(
|
|
525
|
+
relation?: Type$.Relation<any, any, any, any> | string,
|
|
526
|
+
predicates?: Filter$.Props<unknown> | undefined,
|
|
527
|
+
): Query$.Any {
|
|
419
528
|
return new QueryClass({
|
|
420
529
|
type: 'relation',
|
|
421
530
|
anchor: this.ast,
|
|
422
531
|
direction: 'outgoing',
|
|
423
|
-
filter:
|
|
532
|
+
filter:
|
|
533
|
+
relation === undefined
|
|
534
|
+
? undefined
|
|
535
|
+
: typeof relation === 'string'
|
|
536
|
+
? FilterClass.type(relation, predicates).ast
|
|
537
|
+
: FilterClass.type(relation, predicates).ast,
|
|
424
538
|
});
|
|
425
539
|
}
|
|
426
540
|
|
|
427
|
-
targetOf(
|
|
541
|
+
targetOf(
|
|
542
|
+
relation?: Type$.Relation<any, any, any, any> | string,
|
|
543
|
+
predicates?: Filter$.Props<unknown> | undefined,
|
|
544
|
+
): Query$.Any {
|
|
428
545
|
return new QueryClass({
|
|
429
546
|
type: 'relation',
|
|
430
547
|
anchor: this.ast,
|
|
431
548
|
direction: 'incoming',
|
|
432
|
-
filter:
|
|
549
|
+
filter:
|
|
550
|
+
relation === undefined
|
|
551
|
+
? undefined
|
|
552
|
+
: typeof relation === 'string'
|
|
553
|
+
? FilterClass.type(relation, predicates).ast
|
|
554
|
+
: FilterClass.type(relation, predicates).ast,
|
|
433
555
|
});
|
|
434
556
|
}
|
|
435
557
|
|
|
@@ -488,6 +610,21 @@ class QueryClass implements Query$.Any {
|
|
|
488
610
|
options,
|
|
489
611
|
});
|
|
490
612
|
}
|
|
613
|
+
|
|
614
|
+
debugLabel(label: string): Query$.Any {
|
|
615
|
+
if (this.ast.type === 'options') {
|
|
616
|
+
return new QueryClass({
|
|
617
|
+
type: 'options',
|
|
618
|
+
query: this.ast.query,
|
|
619
|
+
options: { ...this.ast.options, debugLabel: label },
|
|
620
|
+
});
|
|
621
|
+
}
|
|
622
|
+
return new QueryClass({
|
|
623
|
+
type: 'options',
|
|
624
|
+
query: this.ast,
|
|
625
|
+
options: { debugLabel: label },
|
|
626
|
+
});
|
|
627
|
+
}
|
|
491
628
|
}
|
|
492
629
|
|
|
493
630
|
export const Query1: typeof Query$ = QueryClass;
|
|
@@ -498,8 +635,164 @@ const isRef = (obj: any): obj is Ref.Ref<any> => {
|
|
|
498
635
|
return obj && typeof obj === 'object' && RefTypeId in obj;
|
|
499
636
|
};
|
|
500
637
|
|
|
501
|
-
const makeTypeDxn = (typename: string) => {
|
|
638
|
+
const makeTypeDxn = (typename: string): DXN.DXN => {
|
|
502
639
|
assertArgument(typeof typename === 'string', 'typename');
|
|
503
640
|
assertArgument(!typename.startsWith('dxn:'), 'typename');
|
|
504
|
-
|
|
641
|
+
// Inline template (rather than `DXN.make`) to keep the value-side `@dxos/keys` import out of this bundle.
|
|
642
|
+
return `dxn:${typename}` as DXN.DXN;
|
|
643
|
+
};
|
|
644
|
+
|
|
645
|
+
const isDxnLike = (value: unknown): value is DXN.DXN => {
|
|
646
|
+
if (typeof value === 'string') {
|
|
647
|
+
return value.startsWith('dxn:');
|
|
648
|
+
}
|
|
649
|
+
return (
|
|
650
|
+
typeof value === 'object' &&
|
|
651
|
+
value !== null &&
|
|
652
|
+
'toString' in value &&
|
|
653
|
+
typeof value.toString === 'function' &&
|
|
654
|
+
value.toString().startsWith('dxn:')
|
|
655
|
+
);
|
|
656
|
+
};
|
|
657
|
+
|
|
658
|
+
const isEchoUriLike = (value: unknown): value is EchoURI.EchoURI => {
|
|
659
|
+
if (typeof value === 'string') {
|
|
660
|
+
return value.startsWith('echo:') || value.startsWith('dxn:echo:') || value.startsWith('dxn:queue:');
|
|
661
|
+
}
|
|
662
|
+
return (
|
|
663
|
+
typeof value === 'object' &&
|
|
664
|
+
value !== null &&
|
|
665
|
+
'toString' in value &&
|
|
666
|
+
typeof value.toString === 'function' &&
|
|
667
|
+
isEchoUriLike(value.toString())
|
|
668
|
+
);
|
|
669
|
+
};
|
|
670
|
+
|
|
671
|
+
const SCOPE_KEYS = new Set(['spaceIds', 'feeds', 'allFeedsFromSpaces']);
|
|
672
|
+
|
|
673
|
+
const _isScopeLike = (value: unknown): value is QueryAST.Scope => {
|
|
674
|
+
if (typeof value !== 'object' || value === null || Array.isArray(value)) {
|
|
675
|
+
return false;
|
|
676
|
+
}
|
|
677
|
+
return Object.keys(value).every((key) => SCOPE_KEYS.has(key));
|
|
678
|
+
};
|
|
679
|
+
|
|
680
|
+
const prettyFilter = (filter: QueryAST.Filter): string => {
|
|
681
|
+
switch (filter.type) {
|
|
682
|
+
case 'object': {
|
|
683
|
+
const parts: string[] = [];
|
|
684
|
+
if (filter.typename !== null) {
|
|
685
|
+
parts.push(JSON.stringify(filter.typename));
|
|
686
|
+
}
|
|
687
|
+
const propEntries = Object.entries(filter.props);
|
|
688
|
+
if (propEntries.length > 0) {
|
|
689
|
+
const propsStr = propEntries.map(([k, v]) => `${k}: ${prettyFilter(v)}`).join(', ');
|
|
690
|
+
parts.push(`{ ${propsStr} }`);
|
|
691
|
+
}
|
|
692
|
+
if (filter.id !== undefined) {
|
|
693
|
+
parts.push(`id: [${filter.id.join(', ')}]`);
|
|
694
|
+
}
|
|
695
|
+
return parts.length > 0 ? `Filter.type(${parts.join(', ')})` : 'Filter.everything()';
|
|
696
|
+
}
|
|
697
|
+
case 'compare':
|
|
698
|
+
return `Filter.${filter.operator}(${JSON.stringify(filter.value)})`;
|
|
699
|
+
case 'in':
|
|
700
|
+
return `Filter.in(${filter.values.map((v) => JSON.stringify(v)).join(', ')})`;
|
|
701
|
+
case 'contains':
|
|
702
|
+
return `Filter.contains(${JSON.stringify(filter.value)})`;
|
|
703
|
+
case 'range':
|
|
704
|
+
return `Filter.between(${JSON.stringify(filter.from)}, ${JSON.stringify(filter.to)})`;
|
|
705
|
+
case 'text-search':
|
|
706
|
+
return `Filter.text(${JSON.stringify(filter.text)})`;
|
|
707
|
+
case 'tag':
|
|
708
|
+
return `Filter.tag(${JSON.stringify(filter.tag)})`;
|
|
709
|
+
case 'child-of':
|
|
710
|
+
return `Filter.childOf([${filter.parents.map((parent) => JSON.stringify(parent)).join(', ')}], { transitive: ${filter.transitive} })`;
|
|
711
|
+
case 'timestamp':
|
|
712
|
+
return `Filter.${filter.field}.${filter.operator}(${filter.value})`;
|
|
713
|
+
case 'not':
|
|
714
|
+
return `Filter.not(${prettyFilter(filter.filter)})`;
|
|
715
|
+
case 'and':
|
|
716
|
+
return `Filter.and(${filter.filters.map(prettyFilter).join(', ')})`;
|
|
717
|
+
case 'or':
|
|
718
|
+
return `Filter.or(${filter.filters.map(prettyFilter).join(', ')})`;
|
|
719
|
+
}
|
|
720
|
+
};
|
|
721
|
+
|
|
722
|
+
const prettyQuery = (query: QueryAST.Query): string => {
|
|
723
|
+
switch (query.type) {
|
|
724
|
+
case 'select':
|
|
725
|
+
return `Query.select(${prettyFilter(query.filter)})`;
|
|
726
|
+
case 'filter':
|
|
727
|
+
return `${prettyQuery(query.selection)}.select(${prettyFilter(query.filter)})`;
|
|
728
|
+
case 'reference-traversal':
|
|
729
|
+
return `${prettyQuery(query.anchor)}.reference(${JSON.stringify(query.property)})`;
|
|
730
|
+
case 'incoming-references': {
|
|
731
|
+
const args: string[] = [];
|
|
732
|
+
if (query.typename !== null) {
|
|
733
|
+
args.push(JSON.stringify(query.typename));
|
|
734
|
+
}
|
|
735
|
+
if (query.property !== null) {
|
|
736
|
+
args.push(JSON.stringify(query.property));
|
|
737
|
+
}
|
|
738
|
+
return `${prettyQuery(query.anchor)}.referencedBy(${args.join(', ')})`;
|
|
739
|
+
}
|
|
740
|
+
case 'relation': {
|
|
741
|
+
const method =
|
|
742
|
+
query.direction === 'outgoing' ? 'sourceOf' : query.direction === 'incoming' ? 'targetOf' : 'relationOf';
|
|
743
|
+
const filterStr = query.filter !== undefined ? prettyFilter(query.filter) : '';
|
|
744
|
+
return `${prettyQuery(query.anchor)}.${method}(${filterStr})`;
|
|
745
|
+
}
|
|
746
|
+
case 'relation-traversal':
|
|
747
|
+
return `${prettyQuery(query.anchor)}.${query.direction}()`;
|
|
748
|
+
case 'hierarchy-traversal':
|
|
749
|
+
return query.direction === 'to-parent'
|
|
750
|
+
? `${prettyQuery(query.anchor)}.parent()`
|
|
751
|
+
: `${prettyQuery(query.anchor)}.children()`;
|
|
752
|
+
case 'union':
|
|
753
|
+
return `Query.all(${query.queries.map(prettyQuery).join(', ')})`;
|
|
754
|
+
case 'set-difference':
|
|
755
|
+
return `Query.without(${prettyQuery(query.source)}, ${prettyQuery(query.exclude)})`;
|
|
756
|
+
case 'order': {
|
|
757
|
+
const orders = query.order.map((o) => {
|
|
758
|
+
if (o.kind === 'natural') {
|
|
759
|
+
return 'Order.natural';
|
|
760
|
+
}
|
|
761
|
+
if (o.kind === 'rank') {
|
|
762
|
+
return `Order.rank(${JSON.stringify(o.direction)})`;
|
|
763
|
+
}
|
|
764
|
+
return `Order.property(${JSON.stringify(o.property)}, ${JSON.stringify(o.direction)})`;
|
|
765
|
+
});
|
|
766
|
+
return `${prettyQuery(query.query)}.orderBy(${orders.join(', ')})`;
|
|
767
|
+
}
|
|
768
|
+
case 'options': {
|
|
769
|
+
const parts: string[] = [];
|
|
770
|
+
if (query.options.deleted !== undefined) {
|
|
771
|
+
parts.push(`deleted: ${JSON.stringify(query.options.deleted)}`);
|
|
772
|
+
}
|
|
773
|
+
if (query.options.debugLabel !== undefined) {
|
|
774
|
+
parts.push(`debugLabel: ${JSON.stringify(query.options.debugLabel)}`);
|
|
775
|
+
}
|
|
776
|
+
return `${prettyQuery(query.query)}.options({ ${parts.join(', ')} })`;
|
|
777
|
+
}
|
|
778
|
+
case 'from': {
|
|
779
|
+
if (query.from._tag === 'scope') {
|
|
780
|
+
const scope = query.from.scope;
|
|
781
|
+
const parts: string[] = [];
|
|
782
|
+
if (scope.spaceIds !== undefined) {
|
|
783
|
+
parts.push(`spaceIds: [${scope.spaceIds.join(', ')}]`);
|
|
784
|
+
}
|
|
785
|
+
if (scope.feeds !== undefined) {
|
|
786
|
+
parts.push(`feeds: [${scope.feeds.join(', ')}]`);
|
|
787
|
+
}
|
|
788
|
+
if (scope.allFeedsFromSpaces !== undefined) {
|
|
789
|
+
parts.push(`allFeedsFromSpaces: ${scope.allFeedsFromSpaces}`);
|
|
790
|
+
}
|
|
791
|
+
return `${prettyQuery(query.query)}.from({ ${parts.join(', ')} })`;
|
|
792
|
+
}
|
|
793
|
+
return `${prettyQuery(query.query)}.from(${prettyQuery(query.from.query)})`;
|
|
794
|
+
}
|
|
795
|
+
case 'limit':
|
|
796
|
+
return `${prettyQuery(query.query)}.limit(${query.limit})`;
|
|
797
|
+
}
|
|
505
798
|
};
|
|
@@ -16,37 +16,37 @@ describe('QuerySandbox', () => {
|
|
|
16
16
|
|
|
17
17
|
test('works', { timeout: 10_000 }, async () => {
|
|
18
18
|
const ast = sandbox.eval(trim`
|
|
19
|
-
Query.select(Filter.typename('dxos.
|
|
19
|
+
Query.select(Filter.typename('org.dxos.type.person'))
|
|
20
20
|
`);
|
|
21
|
-
expect(ast).toEqual(Query.select(Filter.typename('dxos.
|
|
21
|
+
expect(ast).toEqual(Query.select(Filter.typename('org.dxos.type.person')).ast);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
test('works with just Filter passed in', () => {
|
|
25
25
|
const ast = sandbox.eval(trim`
|
|
26
|
-
Filter.typename('dxos.
|
|
26
|
+
Filter.typename('org.dxos.type.person')
|
|
27
27
|
`);
|
|
28
|
-
expect(ast).toEqual(Query.select(Filter.typename('dxos.
|
|
28
|
+
expect(ast).toEqual(Query.select(Filter.typename('org.dxos.type.person')).ast);
|
|
29
29
|
});
|
|
30
30
|
|
|
31
31
|
test('Order', () => {
|
|
32
32
|
const ast = sandbox.eval(trim`
|
|
33
|
-
Query.type('dxos.
|
|
33
|
+
Query.type('org.dxos.type.person').orderBy(Order.property('name', 'desc'))
|
|
34
34
|
`);
|
|
35
|
-
expect(ast).toEqual(Query.type('dxos.
|
|
35
|
+
expect(ast).toEqual(Query.type('org.dxos.type.person').orderBy(Order.property('name', 'desc')).ast);
|
|
36
36
|
});
|
|
37
37
|
|
|
38
38
|
test('traversal', () => {
|
|
39
39
|
const ast = sandbox.eval(trim`
|
|
40
|
-
Query.select(Filter.type('dxos.
|
|
40
|
+
Query.select(Filter.type('org.dxos.type.person', { jobTitle: 'investor' }))
|
|
41
41
|
.reference('organization')
|
|
42
|
-
.targetOf('dxos.
|
|
42
|
+
.targetOf('org.dxos.relation.hasSubject')
|
|
43
43
|
.source()
|
|
44
44
|
`);
|
|
45
45
|
|
|
46
46
|
expect(ast).toEqual(
|
|
47
|
-
Query.select(Filter.type('dxos.
|
|
47
|
+
Query.select(Filter.type('org.dxos.type.person', { jobTitle: 'investor' }))
|
|
48
48
|
.reference('organization')
|
|
49
|
-
.targetOf('dxos.
|
|
49
|
+
.targetOf('org.dxos.relation.hasSubject')
|
|
50
50
|
.source().ast,
|
|
51
51
|
);
|
|
52
52
|
});
|
|
@@ -49,7 +49,7 @@ export class QuerySandbox extends Resource {
|
|
|
49
49
|
|
|
50
50
|
/**
|
|
51
51
|
* Evaluates the query code.
|
|
52
|
-
* @param queryCode Example: `Query.select(Filter.typename('dxos.
|
|
52
|
+
* @param queryCode Example: `Query.select(Filter.typename('org.dxos.type.person'))`
|
|
53
53
|
*/
|
|
54
54
|
eval(queryCode: string): QueryAST.Query {
|
|
55
55
|
using context = this.#runtime.newContext();
|