@dxos/echo-query 0.8.4-main.40e3dcdf1b → 0.8.4-main.43cb759274
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 +215 -17
- package/dist/lib/neutral/index.mjs.map +3 -3
- package/dist/lib/neutral/meta.json +1 -1
- package/dist/query-lite/index.d.ts +1424 -977
- package/dist/query-lite/index.d.ts.map +1 -1
- package/dist/query-lite/index.js +202 -44
- 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.map +1 -1
- package/dist/types/src/sandbox/quickjs.d.ts.map +1 -1
- package/dist/types/tsconfig.tsbuildinfo +1 -1
- package/package.json +15 -21
- package/src/parser/query-builder.ts +269 -9
- package/src/parser/query.test.ts +116 -3
- package/src/query-lite/query-lite.ts +342 -73
- package/src/sandbox/query-sandbox.test.ts +2 -2
|
@@ -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`/`EID` 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, EID, EntityId, URI } from '@dxos/keys';
|
|
11
13
|
|
|
12
14
|
//
|
|
13
15
|
// Light-weight implementation of query execution.
|
|
@@ -83,9 +85,9 @@ class FilterClass implements Filter$.Any {
|
|
|
83
85
|
});
|
|
84
86
|
}
|
|
85
87
|
|
|
86
|
-
static id(...ids:
|
|
88
|
+
static id(...ids: EntityId[]): Filter$.Any {
|
|
87
89
|
// assertArgument(
|
|
88
|
-
// ids.every((id) =>
|
|
90
|
+
// ids.every((id) => EntityId.isValid(id)),
|
|
89
91
|
// 'ids',
|
|
90
92
|
// 'ids must be valid',
|
|
91
93
|
// );
|
|
@@ -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 EID.EID;
|
|
273
|
+
}
|
|
274
|
+
throw new TypeError('childOf requires EID 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;
|
|
@@ -283,7 +343,7 @@ export { Filter1 as Filter };
|
|
|
283
343
|
type RefPropKey<T> = keyof T & string;
|
|
284
344
|
|
|
285
345
|
const propsFilterToAst = (predicates: Filter$.Props<any>): Pick<QueryAST.FilterObject, 'id' | 'props'> => {
|
|
286
|
-
let idFilter: readonly
|
|
346
|
+
let idFilter: readonly EntityId[] | undefined;
|
|
287
347
|
if ('id' in predicates) {
|
|
288
348
|
assertArgument(
|
|
289
349
|
typeof predicates.id === 'string' || Array.isArray(predicates.id),
|
|
@@ -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,35 +457,54 @@ class QueryClass implements Query$.Any {
|
|
|
372
457
|
});
|
|
373
458
|
}
|
|
374
459
|
|
|
375
|
-
static from(
|
|
460
|
+
static from(...args: any[]): Query$.Any {
|
|
376
461
|
const baseQuery: QueryAST.Query = {
|
|
377
462
|
type: 'select',
|
|
378
463
|
filter: FilterClass.everything().ast,
|
|
379
464
|
};
|
|
380
465
|
const wrapper = new QueryClass(baseQuery);
|
|
381
|
-
return wrapper.from(
|
|
466
|
+
return (wrapper.from as (...args: any[]) => Query$.Any)(...args);
|
|
382
467
|
}
|
|
383
468
|
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
469
|
+
from(...args: any[]): Query$.Any {
|
|
470
|
+
// Variadic raw scopes: `.from(Scope.space(), Scope.registry())`.
|
|
471
|
+
if (args.length > 1 && args.every((arg) => _isScopeLike(arg))) {
|
|
472
|
+
return new QueryClass({
|
|
473
|
+
type: 'from',
|
|
474
|
+
query: this.ast,
|
|
475
|
+
from: { _tag: 'scope', scopes: args as QueryAST.Scope[] },
|
|
476
|
+
});
|
|
477
|
+
}
|
|
387
478
|
|
|
388
|
-
|
|
389
|
-
if (
|
|
479
|
+
const [arg] = args;
|
|
480
|
+
if (arg === 'all-accessible-spaces') {
|
|
390
481
|
return new QueryClass({
|
|
391
|
-
type: '
|
|
392
|
-
|
|
393
|
-
|
|
482
|
+
type: 'from',
|
|
483
|
+
query: this.ast,
|
|
484
|
+
from: { _tag: 'scope', scopes: [] },
|
|
394
485
|
});
|
|
395
|
-
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
if (_isScopeLike(arg)) {
|
|
396
489
|
return new QueryClass({
|
|
397
|
-
type: '
|
|
398
|
-
|
|
399
|
-
|
|
490
|
+
type: 'from',
|
|
491
|
+
query: this.ast,
|
|
492
|
+
from: { _tag: 'scope', scopes: Array.isArray(arg) ? arg : [arg] },
|
|
400
493
|
});
|
|
401
494
|
}
|
|
495
|
+
|
|
496
|
+
throw new TypeError('Database and Feed objects are not supported in query-lite sandbox');
|
|
402
497
|
}
|
|
403
498
|
|
|
499
|
+
/** Returns a human-readable string representation of a Query AST. */
|
|
500
|
+
static pretty(query: Query$.Any): string {
|
|
501
|
+
return prettyQuery(query.ast);
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
constructor(public readonly ast: QueryAST.Query) {}
|
|
505
|
+
|
|
506
|
+
'~Query' = QueryClass.variance;
|
|
507
|
+
|
|
404
508
|
reference(key: string): Query$.Any {
|
|
405
509
|
return new QueryClass({
|
|
406
510
|
type: 'reference-traversal',
|
|
@@ -409,13 +513,11 @@ class QueryClass implements Query$.Any {
|
|
|
409
513
|
});
|
|
410
514
|
}
|
|
411
515
|
|
|
412
|
-
referencedBy(target?:
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
target)
|
|
418
|
-
: null;
|
|
516
|
+
referencedBy(target?: Type$.AnyEntity | string, key?: string): Query$.Any {
|
|
517
|
+
if (target !== undefined && typeof target !== 'string') {
|
|
518
|
+
throw new TypeError('referencedBy requires a typename string in query-lite');
|
|
519
|
+
}
|
|
520
|
+
const typename = target !== undefined ? makeTypeDxn(target) : null;
|
|
419
521
|
return new QueryClass({
|
|
420
522
|
type: 'incoming-references',
|
|
421
523
|
anchor: this.ast,
|
|
@@ -424,21 +526,37 @@ class QueryClass implements Query$.Any {
|
|
|
424
526
|
});
|
|
425
527
|
}
|
|
426
528
|
|
|
427
|
-
sourceOf(
|
|
529
|
+
sourceOf(
|
|
530
|
+
relation?: Type$.Relation<any, any, any, any> | string,
|
|
531
|
+
predicates?: Filter$.Props<unknown> | undefined,
|
|
532
|
+
): Query$.Any {
|
|
428
533
|
return new QueryClass({
|
|
429
534
|
type: 'relation',
|
|
430
535
|
anchor: this.ast,
|
|
431
536
|
direction: 'outgoing',
|
|
432
|
-
filter:
|
|
537
|
+
filter:
|
|
538
|
+
relation === undefined
|
|
539
|
+
? undefined
|
|
540
|
+
: typeof relation === 'string'
|
|
541
|
+
? FilterClass.type(relation, predicates).ast
|
|
542
|
+
: FilterClass.type(relation, predicates).ast,
|
|
433
543
|
});
|
|
434
544
|
}
|
|
435
545
|
|
|
436
|
-
targetOf(
|
|
546
|
+
targetOf(
|
|
547
|
+
relation?: Type$.Relation<any, any, any, any> | string,
|
|
548
|
+
predicates?: Filter$.Props<unknown> | undefined,
|
|
549
|
+
): Query$.Any {
|
|
437
550
|
return new QueryClass({
|
|
438
551
|
type: 'relation',
|
|
439
552
|
anchor: this.ast,
|
|
440
553
|
direction: 'incoming',
|
|
441
|
-
filter:
|
|
554
|
+
filter:
|
|
555
|
+
relation === undefined
|
|
556
|
+
? undefined
|
|
557
|
+
: typeof relation === 'string'
|
|
558
|
+
? FilterClass.type(relation, predicates).ast
|
|
559
|
+
: FilterClass.type(relation, predicates).ast,
|
|
442
560
|
});
|
|
443
561
|
}
|
|
444
562
|
|
|
@@ -490,36 +608,26 @@ class QueryClass implements Query$.Any {
|
|
|
490
608
|
});
|
|
491
609
|
}
|
|
492
610
|
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
scope: {
|
|
501
|
-
...(options?.includeFeeds ? { allQueuesFromSpaces: true } : {}),
|
|
502
|
-
},
|
|
503
|
-
},
|
|
504
|
-
});
|
|
505
|
-
}
|
|
611
|
+
options(options: QueryAST.QueryOptions): Query$.Any {
|
|
612
|
+
return new QueryClass({
|
|
613
|
+
type: 'options',
|
|
614
|
+
query: this.ast,
|
|
615
|
+
options,
|
|
616
|
+
});
|
|
617
|
+
}
|
|
506
618
|
|
|
507
|
-
|
|
619
|
+
debugLabel(label: string): Query$.Any {
|
|
620
|
+
if (this.ast.type === 'options') {
|
|
508
621
|
return new QueryClass({
|
|
509
|
-
type: '
|
|
510
|
-
query: this.ast,
|
|
511
|
-
|
|
622
|
+
type: 'options',
|
|
623
|
+
query: this.ast.query,
|
|
624
|
+
options: { ...this.ast.options, debugLabel: label },
|
|
512
625
|
});
|
|
513
626
|
}
|
|
514
|
-
|
|
515
|
-
throw new TypeError('Database and Feed objects are not supported in query-lite sandbox');
|
|
516
|
-
}
|
|
517
|
-
|
|
518
|
-
options(options: QueryAST.QueryOptions): Query$.Any {
|
|
519
627
|
return new QueryClass({
|
|
520
628
|
type: 'options',
|
|
521
629
|
query: this.ast,
|
|
522
|
-
options,
|
|
630
|
+
options: { debugLabel: label },
|
|
523
631
|
});
|
|
524
632
|
}
|
|
525
633
|
}
|
|
@@ -532,17 +640,178 @@ const isRef = (obj: any): obj is Ref.Ref<any> => {
|
|
|
532
640
|
return obj && typeof obj === 'object' && RefTypeId in obj;
|
|
533
641
|
};
|
|
534
642
|
|
|
535
|
-
const makeTypeDxn = (typename: string) => {
|
|
643
|
+
const makeTypeDxn = (typename: string): DXN.DXN => {
|
|
536
644
|
assertArgument(typeof typename === 'string', 'typename');
|
|
537
645
|
assertArgument(!typename.startsWith('dxn:'), 'typename');
|
|
538
|
-
|
|
646
|
+
// Inline template (rather than `DXN.make`) to keep the value-side `@dxos/keys` import out of this bundle.
|
|
647
|
+
return `dxn:${typename}` as DXN.DXN;
|
|
648
|
+
};
|
|
649
|
+
|
|
650
|
+
const isDxnLike = (value: unknown): value is DXN.DXN => {
|
|
651
|
+
if (typeof value === 'string') {
|
|
652
|
+
return value.startsWith('dxn:');
|
|
653
|
+
}
|
|
654
|
+
return (
|
|
655
|
+
typeof value === 'object' &&
|
|
656
|
+
value !== null &&
|
|
657
|
+
'toString' in value &&
|
|
658
|
+
typeof value.toString === 'function' &&
|
|
659
|
+
value.toString().startsWith('dxn:')
|
|
660
|
+
);
|
|
661
|
+
};
|
|
662
|
+
|
|
663
|
+
const isEchoUriLike = (value: unknown): value is EID.EID => {
|
|
664
|
+
if (typeof value === 'string') {
|
|
665
|
+
return value.startsWith('echo:') || value.startsWith('dxn:echo:') || value.startsWith('dxn:queue:');
|
|
666
|
+
}
|
|
667
|
+
return (
|
|
668
|
+
typeof value === 'object' &&
|
|
669
|
+
value !== null &&
|
|
670
|
+
'toString' in value &&
|
|
671
|
+
typeof value.toString === 'function' &&
|
|
672
|
+
isEchoUriLike(value.toString())
|
|
673
|
+
);
|
|
539
674
|
};
|
|
540
675
|
|
|
541
|
-
const
|
|
676
|
+
const SCOPE_TAGS = new Set(['space', 'feed', 'registry']);
|
|
542
677
|
|
|
543
|
-
const _isScopeLike = (value: unknown): value is QueryAST.Scope => {
|
|
544
|
-
if (
|
|
545
|
-
return
|
|
678
|
+
const _isScopeLike = (value: unknown): value is QueryAST.Scope | QueryAST.Scope[] => {
|
|
679
|
+
if (Array.isArray(value)) {
|
|
680
|
+
return value.every((item) => _isSingleScopeLike(item));
|
|
681
|
+
}
|
|
682
|
+
return _isSingleScopeLike(value);
|
|
683
|
+
};
|
|
684
|
+
|
|
685
|
+
const _isSingleScopeLike = (value: unknown): value is QueryAST.Scope => {
|
|
686
|
+
return (
|
|
687
|
+
typeof value === 'object' &&
|
|
688
|
+
value !== null &&
|
|
689
|
+
!Array.isArray(value) &&
|
|
690
|
+
'_tag' in value &&
|
|
691
|
+
typeof value._tag === 'string' &&
|
|
692
|
+
SCOPE_TAGS.has(value._tag)
|
|
693
|
+
);
|
|
694
|
+
};
|
|
695
|
+
|
|
696
|
+
const prettyFilter = (filter: QueryAST.Filter): string => {
|
|
697
|
+
switch (filter.type) {
|
|
698
|
+
case 'object': {
|
|
699
|
+
const parts: string[] = [];
|
|
700
|
+
if (filter.typename !== null) {
|
|
701
|
+
parts.push(JSON.stringify(filter.typename));
|
|
702
|
+
}
|
|
703
|
+
const propEntries = Object.entries(filter.props);
|
|
704
|
+
if (propEntries.length > 0) {
|
|
705
|
+
const propsStr = propEntries.map(([k, v]) => `${k}: ${prettyFilter(v)}`).join(', ');
|
|
706
|
+
parts.push(`{ ${propsStr} }`);
|
|
707
|
+
}
|
|
708
|
+
if (filter.id !== undefined) {
|
|
709
|
+
parts.push(`id: [${filter.id.join(', ')}]`);
|
|
710
|
+
}
|
|
711
|
+
return parts.length > 0 ? `Filter.type(${parts.join(', ')})` : 'Filter.everything()';
|
|
712
|
+
}
|
|
713
|
+
case 'compare':
|
|
714
|
+
return `Filter.${filter.operator}(${JSON.stringify(filter.value)})`;
|
|
715
|
+
case 'in':
|
|
716
|
+
return `Filter.in(${filter.values.map((v) => JSON.stringify(v)).join(', ')})`;
|
|
717
|
+
case 'contains':
|
|
718
|
+
return `Filter.contains(${JSON.stringify(filter.value)})`;
|
|
719
|
+
case 'range':
|
|
720
|
+
return `Filter.between(${JSON.stringify(filter.from)}, ${JSON.stringify(filter.to)})`;
|
|
721
|
+
case 'text-search':
|
|
722
|
+
return `Filter.text(${JSON.stringify(filter.text)})`;
|
|
723
|
+
case 'tag':
|
|
724
|
+
return `Filter.tag(${JSON.stringify(filter.tag)})`;
|
|
725
|
+
case 'child-of':
|
|
726
|
+
return `Filter.childOf([${filter.parents.map((parent) => JSON.stringify(parent)).join(', ')}], { transitive: ${filter.transitive} })`;
|
|
727
|
+
case 'timestamp':
|
|
728
|
+
return `Filter.${filter.field}.${filter.operator}(${filter.value})`;
|
|
729
|
+
case 'not':
|
|
730
|
+
return `Filter.not(${prettyFilter(filter.filter)})`;
|
|
731
|
+
case 'and':
|
|
732
|
+
return `Filter.and(${filter.filters.map(prettyFilter).join(', ')})`;
|
|
733
|
+
case 'or':
|
|
734
|
+
return `Filter.or(${filter.filters.map(prettyFilter).join(', ')})`;
|
|
735
|
+
}
|
|
736
|
+
};
|
|
737
|
+
|
|
738
|
+
const prettyQuery = (query: QueryAST.Query): string => {
|
|
739
|
+
switch (query.type) {
|
|
740
|
+
case 'select':
|
|
741
|
+
return `Query.select(${prettyFilter(query.filter)})`;
|
|
742
|
+
case 'filter':
|
|
743
|
+
return `${prettyQuery(query.selection)}.select(${prettyFilter(query.filter)})`;
|
|
744
|
+
case 'reference-traversal':
|
|
745
|
+
return `${prettyQuery(query.anchor)}.reference(${JSON.stringify(query.property)})`;
|
|
746
|
+
case 'incoming-references': {
|
|
747
|
+
const args: string[] = [];
|
|
748
|
+
if (query.typename !== null) {
|
|
749
|
+
args.push(JSON.stringify(query.typename));
|
|
750
|
+
}
|
|
751
|
+
if (query.property !== null) {
|
|
752
|
+
args.push(JSON.stringify(query.property));
|
|
753
|
+
}
|
|
754
|
+
return `${prettyQuery(query.anchor)}.referencedBy(${args.join(', ')})`;
|
|
755
|
+
}
|
|
756
|
+
case 'relation': {
|
|
757
|
+
const method =
|
|
758
|
+
query.direction === 'outgoing' ? 'sourceOf' : query.direction === 'incoming' ? 'targetOf' : 'relationOf';
|
|
759
|
+
const filterStr = query.filter !== undefined ? prettyFilter(query.filter) : '';
|
|
760
|
+
return `${prettyQuery(query.anchor)}.${method}(${filterStr})`;
|
|
761
|
+
}
|
|
762
|
+
case 'relation-traversal':
|
|
763
|
+
return `${prettyQuery(query.anchor)}.${query.direction}()`;
|
|
764
|
+
case 'hierarchy-traversal':
|
|
765
|
+
return query.direction === 'to-parent'
|
|
766
|
+
? `${prettyQuery(query.anchor)}.parent()`
|
|
767
|
+
: `${prettyQuery(query.anchor)}.children()`;
|
|
768
|
+
case 'union':
|
|
769
|
+
return `Query.all(${query.queries.map(prettyQuery).join(', ')})`;
|
|
770
|
+
case 'set-difference':
|
|
771
|
+
return `Query.without(${prettyQuery(query.source)}, ${prettyQuery(query.exclude)})`;
|
|
772
|
+
case 'order': {
|
|
773
|
+
const orders = query.order.map((o) => {
|
|
774
|
+
if (o.kind === 'natural') {
|
|
775
|
+
return 'Order.natural';
|
|
776
|
+
}
|
|
777
|
+
if (o.kind === 'rank') {
|
|
778
|
+
return `Order.rank(${JSON.stringify(o.direction)})`;
|
|
779
|
+
}
|
|
780
|
+
return `Order.property(${JSON.stringify(o.property)}, ${JSON.stringify(o.direction)})`;
|
|
781
|
+
});
|
|
782
|
+
return `${prettyQuery(query.query)}.orderBy(${orders.join(', ')})`;
|
|
783
|
+
}
|
|
784
|
+
case 'options': {
|
|
785
|
+
const parts: string[] = [];
|
|
786
|
+
if (query.options.deleted !== undefined) {
|
|
787
|
+
parts.push(`deleted: ${JSON.stringify(query.options.deleted)}`);
|
|
788
|
+
}
|
|
789
|
+
if (query.options.debugLabel !== undefined) {
|
|
790
|
+
parts.push(`debugLabel: ${JSON.stringify(query.options.debugLabel)}`);
|
|
791
|
+
}
|
|
792
|
+
return `${prettyQuery(query.query)}.options({ ${parts.join(', ')} })`;
|
|
793
|
+
}
|
|
794
|
+
case 'from': {
|
|
795
|
+
if (query.from._tag === 'scope') {
|
|
796
|
+
if (query.from.scopes.length === 0) {
|
|
797
|
+
return `${prettyQuery(query.query)}.from('all-accessible-spaces')`;
|
|
798
|
+
}
|
|
799
|
+
const scopeStrs = query.from.scopes.map((scope) => {
|
|
800
|
+
if (scope._tag === 'space') {
|
|
801
|
+
return scope.includeAllFeeds
|
|
802
|
+
? `{ space: ${JSON.stringify(scope.spaceId)}, includeAllFeeds: true }`
|
|
803
|
+
: `{ space: ${JSON.stringify(scope.spaceId)} }`;
|
|
804
|
+
}
|
|
805
|
+
if (scope._tag === 'feed') {
|
|
806
|
+
return `{ feed: ${String(scope.feedUri)} }`;
|
|
807
|
+
}
|
|
808
|
+
return `{ registry: ${JSON.stringify(scope.location)} }`;
|
|
809
|
+
});
|
|
810
|
+
return `${prettyQuery(query.query)}.from([${scopeStrs.join(', ')}])`;
|
|
811
|
+
}
|
|
812
|
+
return `${prettyQuery(query.query)}.from(${prettyQuery(query.from.query)})`;
|
|
813
|
+
}
|
|
814
|
+
case 'limit':
|
|
815
|
+
return `${prettyQuery(query.query)}.limit(${query.limit})`;
|
|
546
816
|
}
|
|
547
|
-
return Object.keys(value).every((key) => SCOPE_KEYS.has(key));
|
|
548
817
|
};
|
|
@@ -39,14 +39,14 @@ describe('QuerySandbox', () => {
|
|
|
39
39
|
const ast = sandbox.eval(trim`
|
|
40
40
|
Query.select(Filter.type('org.dxos.type.person', { jobTitle: 'investor' }))
|
|
41
41
|
.reference('organization')
|
|
42
|
-
.targetOf('org.dxos.relation.
|
|
42
|
+
.targetOf('org.dxos.relation.hasSubject')
|
|
43
43
|
.source()
|
|
44
44
|
`);
|
|
45
45
|
|
|
46
46
|
expect(ast).toEqual(
|
|
47
47
|
Query.select(Filter.type('org.dxos.type.person', { jobTitle: 'investor' }))
|
|
48
48
|
.reference('organization')
|
|
49
|
-
.targetOf('org.dxos.relation.
|
|
49
|
+
.targetOf('org.dxos.relation.hasSubject')
|
|
50
50
|
.source().ast,
|
|
51
51
|
);
|
|
52
52
|
});
|