@dxos/echo 0.8.2-main.5885341 → 0.8.2-main.5ca3450

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/dist/lib/browser/index.mjs +48 -33
  2. package/dist/lib/browser/index.mjs.map +4 -4
  3. package/dist/lib/browser/meta.json +1 -1
  4. package/dist/lib/node/index.cjs +48 -29
  5. package/dist/lib/node/index.cjs.map +4 -4
  6. package/dist/lib/node/meta.json +1 -1
  7. package/dist/lib/node-esm/index.mjs +48 -33
  8. package/dist/lib/node-esm/index.mjs.map +4 -4
  9. package/dist/lib/node-esm/meta.json +1 -1
  10. package/dist/types/src/{Database.d.ts → experimental/database.d.ts} +1 -1
  11. package/dist/types/src/experimental/database.d.ts.map +1 -0
  12. package/dist/types/src/experimental/index.d.ts +1 -0
  13. package/dist/types/src/experimental/index.d.ts.map +1 -0
  14. package/dist/types/src/{Queue.d.ts → experimental/queue.d.ts} +1 -1
  15. package/dist/types/src/experimental/queue.d.ts.map +1 -0
  16. package/dist/types/src/{Space.d.ts → experimental/space.d.ts} +1 -1
  17. package/dist/types/src/experimental/space.d.ts.map +1 -0
  18. package/dist/types/src/index.d.ts +3 -6
  19. package/dist/types/src/index.d.ts.map +1 -1
  20. package/dist/types/src/query/api.d.ts +116 -0
  21. package/dist/types/src/query/api.d.ts.map +1 -0
  22. package/dist/types/src/query/ast.d.ts +188 -0
  23. package/dist/types/src/query/ast.d.ts.map +1 -0
  24. package/dist/types/src/query/query.test.d.ts +2 -0
  25. package/dist/types/src/query/query.test.d.ts.map +1 -0
  26. package/dist/types/src/type/Relation.d.ts +16 -0
  27. package/dist/types/src/type/Relation.d.ts.map +1 -0
  28. package/dist/types/src/type/Type.d.ts +80 -0
  29. package/dist/types/src/type/Type.d.ts.map +1 -0
  30. package/dist/types/src/type/Type.test.d.ts +2 -0
  31. package/dist/types/src/type/Type.test.d.ts.map +1 -0
  32. package/dist/types/src/type/index.d.ts +3 -0
  33. package/dist/types/src/type/index.d.ts.map +1 -0
  34. package/dist/types/tsconfig.tsbuildinfo +1 -1
  35. package/package.json +13 -13
  36. package/src/{Database.ts → experimental/database.ts} +1 -1
  37. package/src/experimental/index.ts +7 -0
  38. package/src/{Queue.ts → experimental/queue.ts} +1 -1
  39. package/src/index.ts +3 -7
  40. package/src/query/api.ts +291 -0
  41. package/src/query/ast.ts +149 -0
  42. package/src/query/query.test.ts +135 -0
  43. package/src/type/Relation.ts +17 -0
  44. package/src/type/Type.test.ts +106 -0
  45. package/src/type/Type.ts +143 -0
  46. package/src/type/index.ts +6 -0
  47. package/dist/types/src/Database.d.ts.map +0 -1
  48. package/dist/types/src/Queue.d.ts.map +0 -1
  49. package/dist/types/src/Space.d.ts.map +0 -1
  50. package/dist/types/src/Type.d.ts +0 -49
  51. package/dist/types/src/Type.d.ts.map +0 -1
  52. package/dist/types/src/api.test.d.ts +0 -2
  53. package/dist/types/src/api.test.d.ts.map +0 -1
  54. package/src/Type.ts +0 -99
  55. package/src/api.test.ts +0 -92
  56. /package/src/{Space.ts → experimental/space.ts} +0 -0
@@ -0,0 +1,291 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { type Schema } from 'effect';
6
+
7
+ import { raise } from '@dxos/debug';
8
+ import { getSchemaDXN, type Ref } from '@dxos/echo-schema';
9
+
10
+ import type * as QueryAST from './ast';
11
+ import type { Relation } from '..';
12
+
13
+ // TODO(dmaretskyi): Split up into interfaces for objects and relations so they can have separate verbs.
14
+ // TODO(dmaretskyi): Undirected relation traversals.
15
+
16
+ export interface Query<T> {
17
+ // TODO(dmaretskyi): See new effect-schema approach to variance.
18
+ '~Query': { value: T };
19
+
20
+ ast: QueryAST.AST;
21
+
22
+ /**
23
+ * Traverse an outgoing reference.
24
+ * @param key - Property path inside T that is a reference.
25
+ * @returns Query for the target of the reference.
26
+ */
27
+ reference<K extends RefPropKey<T>>(key: K): Query<Ref.Target<T[K]>>;
28
+
29
+ /**
30
+ * Find objects referencing this object.
31
+ * @param target - Schema of the referencing object.
32
+ * @param key - Property path inside the referencing object that is a reference.
33
+ * @returns Query for the referencing objects.
34
+ */
35
+ // TODO(dmaretskyi): any way to enforce `Ref.Target<Schema.Schema.Type<S>[key]> == T`?
36
+ referencedBy<S extends Schema.Schema.All>(
37
+ target: S,
38
+ key: RefPropKey<Schema.Schema.Type<S>>,
39
+ ): Query<Schema.Schema.Type<S>>;
40
+
41
+ /**
42
+ * Find relations where this object is the source.
43
+ * @returns Query for the relation objects.
44
+ * @param relation - Schema of the relation.
45
+ * @param predicates - Predicates to filter the relation objects.
46
+ */
47
+ sourceOf<S extends Schema.Schema.All>(
48
+ relation: S,
49
+ predicates?: PredicateSet<Schema.Schema.Type<S>>,
50
+ ): Query<Schema.Schema.Type<S>>;
51
+
52
+ /**
53
+ * Find relations where this object is the target.
54
+ * @returns Query for the relation objects.
55
+ * @param relation - Schema of the relation.
56
+ * @param predicates - Predicates to filter the relation objects.
57
+ */
58
+ targetOf<S extends Schema.Schema.All>(
59
+ relation: S,
60
+ predicates?: PredicateSet<Schema.Schema.Type<S>>,
61
+ ): Query<Schema.Schema.Type<S>>;
62
+
63
+ /**
64
+ * For a query for relations, get the source objects.
65
+ * @returns Query for the source objects.
66
+ */
67
+ source(): Query<Relation.Source<T>>;
68
+
69
+ /**
70
+ * For a query for relations, get the target objects.
71
+ * @returns Query for the target objects.
72
+ */
73
+ target(): Query<Relation.Target<T>>;
74
+ }
75
+
76
+ interface QueryAPI {
77
+ /**
78
+ * Query for objects of a given schema.
79
+ * @param schema - Schema of the objects.
80
+ * @param predicates - Predicates to filter the objects.
81
+ * @returns Query for the objects.
82
+ */
83
+ type<S extends Schema.Schema.All>(
84
+ schema: S,
85
+ predicates?: PredicateSet<Schema.Schema.Type<S>>,
86
+ ): Query<Schema.Schema.Type<S>>;
87
+
88
+ /**
89
+ * Full-text or vector search.
90
+ */
91
+ text<S extends Schema.Schema.All>(
92
+ // TODO(dmaretskyi): Allow passing an array of schema here.
93
+ schema: S,
94
+ // TODO(dmaretskyi): Consider passing a vector here, but really the embedding should be done on the query-executor side.
95
+ text: string,
96
+ options?: Query.TextSearchOptions,
97
+ ): Query<Schema.Schema.Type<S>>;
98
+
99
+ /**
100
+ * Combine results of multiple queries.
101
+ * @param queries - Queries to combine.
102
+ * @returns Query for the combined results.
103
+ */
104
+ all<T>(...queries: Query<T>[]): Query<T>;
105
+
106
+ /**
107
+ * Predicate for property to be greater than the provided value.
108
+ */
109
+ gt<T>(value: T): Predicate<T>;
110
+
111
+ /**
112
+ * Predicate for property to be greater than or equal to the provided value.
113
+ */
114
+ gte<T>(value: T): Predicate<T>;
115
+
116
+ /**
117
+ * Predicate for property to be less than the provided value.
118
+ */
119
+ lt<T>(value: T): Predicate<T>;
120
+
121
+ /**
122
+ * Predicate for property to be less than or equal to the provided value.
123
+ */
124
+ lte<T>(value: T): Predicate<T>;
125
+
126
+ /**
127
+ * Predicate for property to be in the provided array.
128
+ * @param values - Values to check against.
129
+ */
130
+ in<T>(...values: T[]): Predicate<T>;
131
+
132
+ /**
133
+ * Predicate for property to be in the provided range.
134
+ * @param from - Start of the range (inclusive).
135
+ * @param to - End of the range (exclusive).
136
+ */
137
+ range<T>(from: T, to: T): Predicate<T>;
138
+
139
+ // TODO(dmaretskyi): Add `Query.match` to support pattern matching on string props.
140
+ }
141
+
142
+ export declare namespace Query {
143
+ export type TextSearchOptions = {
144
+ type?: 'full-text' | 'vector';
145
+ };
146
+ }
147
+
148
+ export interface Predicate<T> {
149
+ // TODO(dmaretskyi): See new effect-schema approach to variance.
150
+ '~Predicate': { value: T };
151
+
152
+ ast: QueryAST.Predicate;
153
+ }
154
+
155
+ const Predicate = {
156
+ variance: {} as Predicate<any>['~Predicate'],
157
+
158
+ make: <T>(ast: QueryAST.Predicate): Predicate<T> => ({ ast, '~Predicate': Predicate.variance }) as Predicate<T>,
159
+ };
160
+
161
+ type PredicateSet<T> = {
162
+ // Predicate or a value as a shorthand for `eq`.
163
+ [K in keyof T & string]?: Predicate<T[K]> | T[K];
164
+ };
165
+
166
+ /**
167
+ * All property paths inside T that are references.
168
+ */
169
+ type RefPropKey<T> = { [K in keyof T]: T[K] extends Ref<infer _U> ? K : never }[keyof T] & string;
170
+
171
+ const predicateSetToAst = (predicates: PredicateSet<any>): QueryAST.PredicateSet => {
172
+ return Object.fromEntries(
173
+ Object.entries(predicates).map(([key, predicate]) => [key, predicate.ast]),
174
+ ) as QueryAST.PredicateSet;
175
+ };
176
+
177
+ class QueryClass implements Query<any> {
178
+ private static variance: Query<any>['~Query'] = {} as Query<any>['~Query'];
179
+
180
+ static type(schema: Schema.Schema.All, predicates?: PredicateSet<unknown>): Query<any> {
181
+ const dxn = getSchemaDXN(schema) ?? raise(new TypeError('Schema has no DXN'));
182
+ return new QueryClass({
183
+ type: 'type',
184
+ typename: dxn.toString(),
185
+ predicates: predicates ? predicateSetToAst(predicates) : undefined,
186
+ });
187
+ }
188
+
189
+ static text(schema: Schema.Schema.All, text: string, options?: Query.TextSearchOptions): Query<any> {
190
+ const dxn = getSchemaDXN(schema) ?? raise(new TypeError('Schema has no DXN'));
191
+ return new QueryClass({
192
+ type: 'text-search',
193
+ typename: dxn.toString(),
194
+ text,
195
+ searchKind: options?.type,
196
+ });
197
+ }
198
+
199
+ static all(...queries: Query<any>[]): Query<any> {
200
+ return new QueryClass({
201
+ type: 'union',
202
+ queries: queries.map((q) => q.ast),
203
+ });
204
+ }
205
+
206
+ static gt(value: unknown): Predicate<any> {
207
+ return Predicate.make({ type: 'gt', value });
208
+ }
209
+
210
+ static gte(value: unknown): Predicate<any> {
211
+ return Predicate.make({ type: 'gte', value });
212
+ }
213
+
214
+ static lt(value: unknown): Predicate<any> {
215
+ return Predicate.make({ type: 'lt', value });
216
+ }
217
+
218
+ static lte(value: unknown): Predicate<any> {
219
+ return Predicate.make({ type: 'lte', value });
220
+ }
221
+
222
+ static in(...values: unknown[]): Predicate<any> {
223
+ return Predicate.make({ type: 'in', values });
224
+ }
225
+
226
+ static range(from: unknown, to: unknown): Predicate<any> {
227
+ return Predicate.make({ type: 'range', from, to });
228
+ }
229
+
230
+ constructor(public readonly ast: QueryAST.AST) {}
231
+
232
+ '~Query' = QueryClass.variance;
233
+
234
+ reference(key: string): Query<any> {
235
+ return new QueryClass({
236
+ type: 'reference-traversal',
237
+ anchor: this.ast,
238
+ property: key,
239
+ });
240
+ }
241
+
242
+ referencedBy(target: Schema.Schema.All, key: string): Query<any> {
243
+ const dxn = getSchemaDXN(target) ?? raise(new TypeError('Target schema has no DXN'));
244
+ return new QueryClass({
245
+ type: 'incoming-references',
246
+ anchor: this.ast,
247
+ property: key,
248
+ typename: dxn.toString(),
249
+ });
250
+ }
251
+
252
+ sourceOf(relation: Schema.Schema.All, predicates?: PredicateSet<unknown> | undefined): Query<any> {
253
+ const dxn = getSchemaDXN(relation) ?? raise(new TypeError('Relation schema has no DXN'));
254
+ return new QueryClass({
255
+ type: 'relation',
256
+ anchor: this.ast,
257
+ direction: 'outgoing',
258
+ typename: dxn.toString(),
259
+ predicates: predicates ? predicateSetToAst(predicates) : undefined,
260
+ });
261
+ }
262
+
263
+ targetOf(relation: Schema.Schema.All, predicates?: PredicateSet<unknown> | undefined): Query<any> {
264
+ const dxn = getSchemaDXN(relation) ?? raise(new TypeError('Relation schema has no DXN'));
265
+ return new QueryClass({
266
+ type: 'relation',
267
+ anchor: this.ast,
268
+ direction: 'incoming',
269
+ typename: dxn.toString(),
270
+ predicates: predicates ? predicateSetToAst(predicates) : undefined,
271
+ });
272
+ }
273
+
274
+ source(): Query<any> {
275
+ return new QueryClass({
276
+ type: 'relation-traversal',
277
+ anchor: this.ast,
278
+ direction: 'source',
279
+ });
280
+ }
281
+
282
+ target(): Query<any> {
283
+ return new QueryClass({
284
+ type: 'relation-traversal',
285
+ anchor: this.ast,
286
+ direction: 'target',
287
+ });
288
+ }
289
+ }
290
+
291
+ export const Query: QueryAPI = QueryClass;
@@ -0,0 +1,149 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Schema } from 'effect';
6
+
7
+ import { DXN } from '@dxos/echo-schema';
8
+
9
+ export const Predicate = Schema.Union(
10
+ Schema.Struct({
11
+ type: Schema.Literal('eq'),
12
+ value: Schema.Any,
13
+ }),
14
+ Schema.Struct({
15
+ type: Schema.Literal('neq'),
16
+ value: Schema.Any,
17
+ }),
18
+ Schema.Struct({
19
+ type: Schema.Literal('gt'),
20
+ value: Schema.Any,
21
+ }),
22
+ Schema.Struct({
23
+ type: Schema.Literal('gte'),
24
+ value: Schema.Any,
25
+ }),
26
+ Schema.Struct({
27
+ type: Schema.Literal('lt'),
28
+ value: Schema.Any,
29
+ }),
30
+ Schema.Struct({
31
+ type: Schema.Literal('lte'),
32
+ value: Schema.Any,
33
+ }),
34
+ Schema.Struct({
35
+ type: Schema.Literal('in'),
36
+ values: Schema.Array(Schema.Any),
37
+ }),
38
+ Schema.Struct({
39
+ type: Schema.Literal('range'),
40
+ from: Schema.Any,
41
+ to: Schema.Any,
42
+ }),
43
+ );
44
+
45
+ export type Predicate = Schema.Schema.Type<typeof Predicate>;
46
+
47
+ export const PredicateSet = Schema.Record({
48
+ key: Schema.String.annotations({ description: 'Property name' }),
49
+ value: Predicate,
50
+ });
51
+
52
+ export type PredicateSet = Schema.Schema.Type<typeof PredicateSet>;
53
+
54
+ const TypenameSpecifier = Schema.Union(DXN, Schema.Null).annotations({
55
+ description: 'DXN or null. Null means any type will match',
56
+ });
57
+
58
+ // NOTE: This pattern with 3 definitions per schema is need to make the types opaque, and circular references in AST to not cause compiler errors.
59
+
60
+ /**
61
+ * Query objects by type, id, and/or predicates.
62
+ */
63
+ const ASTTypeClause_ = Schema.Struct({
64
+ type: Schema.Literal('type'),
65
+ typename: TypenameSpecifier,
66
+ id: Schema.optional(Schema.String),
67
+ predicates: Schema.optional(PredicateSet),
68
+ });
69
+ interface ASTTypeClause extends Schema.Schema.Type<typeof ASTTypeClause_> {}
70
+ const ASTTypeClause: Schema.Schema<ASTTypeClause> = ASTTypeClause_;
71
+
72
+ const ASTTextSearchClause_ = Schema.Struct({
73
+ type: Schema.Literal('text-search'),
74
+ typename: TypenameSpecifier,
75
+ text: Schema.String,
76
+ searchKind: Schema.optional(Schema.Literal('full-text', 'vector')),
77
+ });
78
+ interface ASTTextSearchClause extends Schema.Schema.Type<typeof ASTTextSearchClause_> {}
79
+ const ASTTextSearchClause: Schema.Schema<ASTTextSearchClause> = ASTTextSearchClause_;
80
+
81
+ /**
82
+ * Traverse references from an anchor object.
83
+ */
84
+ const ASTReferenceTraversalClause_ = Schema.Struct({
85
+ type: Schema.Literal('reference-traversal'),
86
+ anchor: Schema.suspend(() => AST),
87
+ property: Schema.String,
88
+ });
89
+ interface ASTReferenceTraversalClause extends Schema.Schema.Type<typeof ASTReferenceTraversalClause_> {}
90
+ const ASTReferenceTraversalClause: Schema.Schema<ASTReferenceTraversalClause> = ASTReferenceTraversalClause_;
91
+
92
+ /**
93
+ * Traverse incoming references to an anchor object.
94
+ */
95
+ const ASTIncomingReferencesClause_ = Schema.Struct({
96
+ type: Schema.Literal('incoming-references'),
97
+ anchor: Schema.suspend(() => AST),
98
+ property: Schema.String,
99
+ typename: TypenameSpecifier,
100
+ });
101
+ interface ASTIncomingReferencesClause extends Schema.Schema.Type<typeof ASTIncomingReferencesClause_> {}
102
+ const ASTIncomingReferencesClause: Schema.Schema<ASTIncomingReferencesClause> = ASTIncomingReferencesClause_;
103
+
104
+ /**
105
+ * Traverse relations connecting to an anchor object.
106
+ */
107
+ const ASTRelationClause_ = Schema.Struct({
108
+ type: Schema.Literal('relation'),
109
+ anchor: Schema.suspend(() => AST),
110
+ direction: Schema.Literal('outgoing', 'incoming', 'both'),
111
+ typename: TypenameSpecifier,
112
+ predicates: Schema.optional(PredicateSet),
113
+ });
114
+ interface ASTRelationClause extends Schema.Schema.Type<typeof ASTRelationClause_> {}
115
+ const ASTRelationClause: Schema.Schema<ASTRelationClause> = ASTRelationClause_;
116
+
117
+ /**
118
+ * Traverse into the source or target of a relation.
119
+ */
120
+ const ASTRelationTraversalClause_ = Schema.Struct({
121
+ type: Schema.Literal('relation-traversal'),
122
+ anchor: Schema.suspend(() => AST),
123
+ direction: Schema.Literal('source', 'target', 'both'),
124
+ });
125
+ interface ASTRelationTraversalClause extends Schema.Schema.Type<typeof ASTRelationTraversalClause_> {}
126
+ const ASTRelationTraversalClause: Schema.Schema<ASTRelationTraversalClause> = ASTRelationTraversalClause_;
127
+
128
+ /**
129
+ * Union of multiple queries.
130
+ */
131
+ const ASTUnionClause_ = Schema.Struct({
132
+ type: Schema.Literal('union'),
133
+ queries: Schema.Array(Schema.suspend(() => AST)),
134
+ });
135
+ interface ASTUnionClause extends Schema.Schema.Type<typeof ASTUnionClause_> {}
136
+ const ASTUnionClause: Schema.Schema<ASTUnionClause> = ASTUnionClause_;
137
+
138
+ const AST_ = Schema.Union(
139
+ ASTTypeClause,
140
+ ASTTextSearchClause,
141
+ ASTReferenceTraversalClause,
142
+ ASTIncomingReferencesClause,
143
+ ASTRelationClause,
144
+ ASTRelationTraversalClause,
145
+ ASTUnionClause,
146
+ );
147
+
148
+ export type AST = Schema.Schema.Type<typeof AST_>;
149
+ export const AST: Schema.Schema<AST> = AST_;
@@ -0,0 +1,135 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Schema } from 'effect';
6
+ import { describe, test } from 'vitest';
7
+
8
+ import { create } from '@dxos/echo-schema';
9
+ import { log } from '@dxos/log';
10
+
11
+ import { Query } from './api';
12
+ import { Type, Relation } from '..';
13
+
14
+ //
15
+ // Example schema
16
+ //
17
+
18
+ const Person = Schema.Struct({
19
+ name: Schema.String,
20
+ email: Schema.optional(Schema.String),
21
+ }).pipe(Type.def({ typename: 'dxos.org/type/Person', version: '0.1.0' }));
22
+ interface Person extends Schema.Schema.Type<typeof Person> {}
23
+
24
+ const Org = Schema.Struct({
25
+ name: Schema.String,
26
+ }).pipe(Type.def({ typename: 'dxos.org/type/Org', version: '0.1.0' }));
27
+ interface Org extends Schema.Schema.Type<typeof Org> {}
28
+
29
+ const WorksFor = Schema.Struct({
30
+ since: Schema.String,
31
+ }).pipe(Relation.def({ typename: 'dxos.org/type/WorksFor', version: '0.1.0', source: Person, target: Org }));
32
+ interface WorksFor extends Schema.Schema.Type<typeof WorksFor> {}
33
+
34
+ const Task = Schema.Struct({
35
+ title: Schema.String,
36
+ createdAt: Schema.String,
37
+ assignee: Type.Ref(Person),
38
+ }).pipe(Type.def({ typename: 'dxos.org/type/Task', version: '0.1.0' }));
39
+ interface Task extends Schema.Schema.Type<typeof Task> {}
40
+
41
+ //
42
+ // Example queries
43
+ //
44
+
45
+ describe('query api', () => {
46
+ test('get all people', () => {
47
+ // Query<Person>
48
+ const getAllPeople = Query.type(Person);
49
+
50
+ log.info('query', { ast: getAllPeople.ast });
51
+ });
52
+
53
+ test('get all people named Fred', () => {
54
+ // Query<Person>
55
+ const getAllPeopleNamedFred = Query.type(Person, { name: 'Fred' });
56
+
57
+ log.info('query', { ast: getAllPeopleNamedFred.ast });
58
+ });
59
+
60
+ test('get all orgs Fred worked for since 2020', () => {
61
+ // Query<Org>
62
+ const fred = create(Person, { name: 'Fred' });
63
+ const getAllOrgsFredWorkedForSince2020 = Query.type(Person, { id: fred.id })
64
+ .sourceOf(WorksFor, { since: Query.gt('2020') })
65
+ .target();
66
+
67
+ log.info('query', { ast: getAllOrgsFredWorkedForSince2020.ast });
68
+ });
69
+
70
+ test('get all tasks for Fred', () => {
71
+ // Query<Task>
72
+ const fred = create(Person, { name: 'Fred' });
73
+ const getAllTasksForFred = Query.type(Person, { id: fred.id }).referencedBy(Task, 'assignee');
74
+
75
+ log.info('query', { ast: getAllTasksForFred.ast });
76
+ });
77
+
78
+ test('get all tasks for employees of Cyberdyne', () => {
79
+ // Query<Task>
80
+ const allTasksForEmployeesOfCyberdyne = Query.type(Org, { name: 'Cyberdyne' })
81
+ .targetOf(WorksFor)
82
+ .source()
83
+ .referencedBy(Task, 'assignee');
84
+
85
+ log.info('query', { ast: allTasksForEmployeesOfCyberdyne.ast });
86
+ });
87
+
88
+ test('get all people or orgs', () => {
89
+ // Query<Person | Org>
90
+ const allPeopleOrOrgs = Query.all(Query.type(Person), Query.type(Org));
91
+
92
+ log.info('query', { ast: allPeopleOrOrgs.ast });
93
+ });
94
+
95
+ test('get assignees of all tasks created after 2020', () => {
96
+ // Query<Person>
97
+ const assigneesOfAllTasksCreatedAfter2020 = Query.type(Task, { createdAt: Query.gt('2020') }).reference('assignee');
98
+
99
+ log.info('query', { ast: assigneesOfAllTasksCreatedAfter2020.ast });
100
+ });
101
+
102
+ test('contact full-text search', () => {
103
+ // Query<Person>
104
+ const contactFullTextSearch = Query.text(Person, 'Bill');
105
+
106
+ log.info('query', { ast: contactFullTextSearch.ast });
107
+ });
108
+
109
+ // TODO(burdon): Experimental.
110
+ test.skip('chain', () => {
111
+ const db: any = null;
112
+ const Query: any = null;
113
+ const Filter: any = null;
114
+
115
+ const x = db.exec(Query.select({ id: '123' })).first();
116
+ const y = db.exec(Query.select(Filter.type(Person)).first());
117
+
118
+ const q = Query
119
+ //
120
+ .selectAll()
121
+ .select({ id: '123' })
122
+ // NOTE: Can't support functions since they can't be serialized (to server).
123
+ // .filter((object) => Math.random() > 0.5)
124
+ .select(Filter.type(Person))
125
+ .select(Filter.props({ name: 'Fred' }))
126
+ .select({ age: Filter.gt(40) })
127
+ .select({ date: Filter.between(Date.now(), Date.now() + 1000 * 60 * 60 * 24) })
128
+ .select({ id: Filter.in(['1', '2', '3']) })
129
+ .select(Filter.and(Filter.type(Person), Filter.props({ id: Filter.in(['1', '2', '3']) })))
130
+ .target()
131
+ .select();
132
+
133
+ log.info('stuff', { x, y, q });
134
+ });
135
+ });
@@ -0,0 +1,17 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { EchoRelation, type RelationSourceTargetRefs } from '@dxos/echo-schema';
6
+
7
+ export const def = EchoRelation;
8
+
9
+ /**
10
+ * Get relation target type.
11
+ */
12
+ export type Target<A> = A extends RelationSourceTargetRefs<infer T, infer _S> ? T : never;
13
+
14
+ /**
15
+ * Get relation source type.
16
+ */
17
+ export type Source<A> = A extends RelationSourceTargetRefs<infer _T, infer S> ? S : never;
@@ -0,0 +1,106 @@
1
+ //
2
+ // Copyright 2025 DXOS.org
3
+ //
4
+
5
+ import { Schema } from 'effect';
6
+ import { describe, test } from 'vitest';
7
+
8
+ import { raise } from '@dxos/debug';
9
+ import { FormatEnum, FormatAnnotation } from '@dxos/echo-schema';
10
+
11
+ import { Type } from './Type';
12
+
13
+ namespace Testing {
14
+ export const Organization = Schema.Struct({
15
+ id: Type.ObjectId,
16
+ name: Schema.String,
17
+ }).pipe(
18
+ Type.def({
19
+ typename: 'example.com/type/Organization',
20
+ version: '0.1.0',
21
+ }),
22
+ );
23
+
24
+ export interface Organization extends Schema.Schema.Type<typeof Organization> {}
25
+
26
+ export const Person = Schema.Struct({
27
+ name: Schema.String,
28
+ dob: Schema.optional(Schema.String),
29
+ email: Schema.optional(Schema.String.pipe(FormatAnnotation.set(FormatEnum.Email))),
30
+ organization: Schema.optional(Type.Ref(Organization)),
31
+ }).pipe(
32
+ Type.def({
33
+ typename: 'example.com/type/Person',
34
+ version: '0.1.0',
35
+ }),
36
+ );
37
+
38
+ export interface Person extends Schema.Schema.Type<typeof Person> {}
39
+
40
+ export const Message = Schema.Struct({
41
+ // TODO(burdon): Support S.Date; Custom Timestamp (with defaults).
42
+ // TODO(burdon): Support defaults (update create and create).
43
+ timestamp: Schema.String.pipe(
44
+ Schema.propertySignature,
45
+ Schema.withConstructorDefault(() => new Date().toISOString()),
46
+ ),
47
+ });
48
+
49
+ export const WorksFor = Schema.Struct({
50
+ // id: Type.ObjectId,
51
+ role: Schema.String,
52
+ }).pipe(
53
+ // Relation.def
54
+ Type.def({
55
+ typename: 'example.com/type/WorksFor',
56
+ version: '0.1.0',
57
+ // source: Person,
58
+ // target: Organization,
59
+ }),
60
+ );
61
+
62
+ export interface WorksFor extends Schema.Schema.Type<typeof WorksFor> {}
63
+
64
+ // TODO(burdon): Fix (Type.def currently removes TypeLiteral that implements the `make` function)..
65
+ // }).pipe(
66
+ // Type.def({
67
+ // typename: 'example.com/type/Message',
68
+ // version: '0.1.0',
69
+ // }),
70
+ // );
71
+
72
+ export interface Message extends Schema.Schema.Type<typeof Message> {}
73
+ }
74
+
75
+ describe('Experimental API review', () => {
76
+ test('type checks', ({ expect }) => {
77
+ const contact = Type.create(Testing.Person, { name: 'Test' });
78
+ const type: Schema.Schema<Testing.Person> = Type.getSchema(contact) ?? raise(new Error('No schema found'));
79
+
80
+ expect(Type.getDXN(type)?.typename).to.eq(Testing.Person.typename);
81
+ expect(Type.getTypename(type)).to.eq('example.com/type/Person');
82
+ expect(Type.getVersion(type)).to.eq('0.1.0');
83
+ expect(Type.getMeta(type)).to.deep.eq({
84
+ kind: Type.Kind.Object,
85
+ typename: 'example.com/type/Person',
86
+ version: '0.1.0',
87
+ });
88
+ });
89
+
90
+ test('instance checks', ({ expect }) => {
91
+ const organization = Type.create(Testing.Organization, { name: 'DXOS' });
92
+ const contact = Type.create(Testing.Person, { name: 'Test', organization: Type.ref(organization) });
93
+
94
+ expect(Schema.is(Testing.Person)(contact)).to.be.true;
95
+ expect(Testing.Person.instanceOf(contact)).to.be.true;
96
+ expect(Type.instanceOf(Testing.Person, contact)).to.be.true;
97
+ expect(Type.instanceOf(Testing.Organization, organization)).to.be.true;
98
+ });
99
+
100
+ test('default props', ({ expect }) => {
101
+ // TODO(burdon): Doesn't work after pipe(Type.def).
102
+ // Property 'make' does not exist on type 'EchoObjectSchema<Struct<{ timestamp: PropertySignature<":", string, never, ":", string, true, never>; }>>'.ts(2339)
103
+ const message = Type.create(Testing.Message, Testing.Message.make({}));
104
+ expect(message.timestamp).to.exist;
105
+ });
106
+ });