@dxos/echo 0.8.2-main.5ca3450 → 0.8.2-main.600d381

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/src/query/ast.ts CHANGED
@@ -6,144 +6,174 @@ import { Schema } from 'effect';
6
6
 
7
7
  import { DXN } from '@dxos/echo-schema';
8
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,
9
+ const TypenameSpecifier = Schema.Union(DXN, Schema.Null).annotations({
10
+ description: 'DXN or null. Null means any type will match',
11
+ });
12
+
13
+ // 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.
14
+
15
+ const FilterObject_ = Schema.Struct({
16
+ type: Schema.Literal('object'),
17
+ typename: TypenameSpecifier,
18
+ props: Schema.Record({
19
+ key: Schema.String.annotations({ description: 'Property name' }),
20
+ value: Schema.suspend(() => Filter),
42
21
  }),
43
- );
22
+ });
23
+ interface FilterObject extends Schema.Schema.Type<typeof FilterObject_> {}
24
+ const FilterObject: Schema.Schema<FilterObject> = FilterObject_;
44
25
 
45
- export type Predicate = Schema.Schema.Type<typeof Predicate>;
26
+ const FilterCompare_ = Schema.Struct({
27
+ type: Schema.Literal('compare'),
28
+ operator: Schema.Literal('eq', 'neq', 'gt', 'gte', 'lt', 'lte'),
29
+ value: Schema.Unknown,
30
+ });
31
+ interface FilterCompare extends Schema.Schema.Type<typeof FilterCompare_> {}
32
+ const FilterCompare: Schema.Schema<FilterCompare> = FilterCompare_;
46
33
 
47
- export const PredicateSet = Schema.Record({
48
- key: Schema.String.annotations({ description: 'Property name' }),
49
- value: Predicate,
34
+ const FilterIn_ = Schema.Struct({
35
+ type: Schema.Literal('in'),
36
+ values: Schema.Array(Schema.Any),
50
37
  });
38
+ interface FilterIn extends Schema.Schema.Type<typeof FilterIn_> {}
39
+ const FilterIn: Schema.Schema<FilterIn> = FilterIn_;
51
40
 
52
- export type PredicateSet = Schema.Schema.Type<typeof PredicateSet>;
41
+ const FilterRange_ = Schema.Struct({
42
+ type: Schema.Literal('range'),
43
+ from: Schema.Any,
44
+ to: Schema.Any,
45
+ });
46
+ interface FilterRange extends Schema.Schema.Type<typeof FilterRange_> {}
47
+ const FilterRange: Schema.Schema<FilterRange> = FilterRange_;
53
48
 
54
- const TypenameSpecifier = Schema.Union(DXN, Schema.Null).annotations({
55
- description: 'DXN or null. Null means any type will match',
49
+ const FilterTextSearch_ = Schema.Struct({
50
+ type: Schema.Literal('text-search'),
51
+ typename: TypenameSpecifier,
52
+ text: Schema.String,
53
+ searchKind: Schema.optional(Schema.Literal('full-text', 'vector')),
56
54
  });
55
+ interface FilterTextSearch extends Schema.Schema.Type<typeof FilterTextSearch_> {}
56
+ const FilterTextSearch: Schema.Schema<FilterTextSearch> = FilterTextSearch_;
57
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.
58
+ const FilterNot_ = Schema.Struct({
59
+ type: Schema.Literal('not'),
60
+ filter: Schema.suspend(() => Filter),
61
+ });
62
+ interface FilterNot extends Schema.Schema.Type<typeof FilterNot_> {}
63
+ const FilterNot: Schema.Schema<FilterNot> = FilterNot_;
64
+
65
+ const FilterAnd_ = Schema.Struct({
66
+ type: Schema.Literal('and'),
67
+ filters: Schema.Array(Schema.suspend(() => Filter)),
68
+ });
69
+ interface FilterAnd extends Schema.Schema.Type<typeof FilterAnd_> {}
70
+ const FilterAnd: Schema.Schema<FilterAnd> = FilterAnd_;
71
+
72
+ const FilterOr_ = Schema.Struct({
73
+ type: Schema.Literal('or'),
74
+ filters: Schema.Array(Schema.suspend(() => Filter)),
75
+ });
76
+ interface FilterOr extends Schema.Schema.Type<typeof FilterOr_> {}
77
+ const FilterOr: Schema.Schema<FilterOr> = FilterOr_;
78
+
79
+ export const Filter = Schema.Union(
80
+ FilterObject,
81
+ FilterTextSearch,
82
+ FilterCompare,
83
+ FilterIn,
84
+ FilterRange,
85
+ FilterNot,
86
+ FilterAnd,
87
+ FilterOr,
88
+ );
89
+ export type Filter = Schema.Schema.Type<typeof Filter>;
59
90
 
60
91
  /**
61
92
  * Query objects by type, id, and/or predicates.
62
93
  */
63
- const ASTTypeClause_ = Schema.Struct({
64
- type: Schema.Literal('type'),
65
- typename: TypenameSpecifier,
66
- id: Schema.optional(Schema.String),
67
- predicates: Schema.optional(PredicateSet),
94
+ const QuerySelectClause_ = Schema.Struct({
95
+ type: Schema.Literal('select'),
96
+ filter: Schema.suspend(() => Filter),
68
97
  });
69
- interface ASTTypeClause extends Schema.Schema.Type<typeof ASTTypeClause_> {}
70
- const ASTTypeClause: Schema.Schema<ASTTypeClause> = ASTTypeClause_;
98
+ interface QuerySelectClause extends Schema.Schema.Type<typeof QuerySelectClause_> {}
99
+ const QuerySelectClause: Schema.Schema<QuerySelectClause> = QuerySelectClause_;
71
100
 
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')),
101
+ /**
102
+ * Filter objects from selection.
103
+ */
104
+ const QueryFilterClause_ = Schema.Struct({
105
+ type: Schema.Literal('filter'),
106
+ selection: Schema.suspend(() => Query),
107
+ filter: Schema.suspend(() => Filter),
77
108
  });
78
- interface ASTTextSearchClause extends Schema.Schema.Type<typeof ASTTextSearchClause_> {}
79
- const ASTTextSearchClause: Schema.Schema<ASTTextSearchClause> = ASTTextSearchClause_;
109
+ interface QueryFilterClause extends Schema.Schema.Type<typeof QueryFilterClause_> {}
110
+ const QueryFilterClause: Schema.Schema<QueryFilterClause> = QueryFilterClause_;
80
111
 
81
112
  /**
82
113
  * Traverse references from an anchor object.
83
114
  */
84
- const ASTReferenceTraversalClause_ = Schema.Struct({
115
+ const QueryReferenceTraversalClause_ = Schema.Struct({
85
116
  type: Schema.Literal('reference-traversal'),
86
- anchor: Schema.suspend(() => AST),
117
+ anchor: Schema.suspend(() => Query),
87
118
  property: Schema.String,
88
119
  });
89
- interface ASTReferenceTraversalClause extends Schema.Schema.Type<typeof ASTReferenceTraversalClause_> {}
90
- const ASTReferenceTraversalClause: Schema.Schema<ASTReferenceTraversalClause> = ASTReferenceTraversalClause_;
120
+ interface QueryReferenceTraversalClause extends Schema.Schema.Type<typeof QueryReferenceTraversalClause_> {}
121
+ const QueryReferenceTraversalClause: Schema.Schema<QueryReferenceTraversalClause> = QueryReferenceTraversalClause_;
91
122
 
92
123
  /**
93
124
  * Traverse incoming references to an anchor object.
94
125
  */
95
- const ASTIncomingReferencesClause_ = Schema.Struct({
126
+ const QueryIncomingReferencesClause_ = Schema.Struct({
96
127
  type: Schema.Literal('incoming-references'),
97
- anchor: Schema.suspend(() => AST),
128
+ anchor: Schema.suspend(() => Query),
98
129
  property: Schema.String,
99
130
  typename: TypenameSpecifier,
100
131
  });
101
- interface ASTIncomingReferencesClause extends Schema.Schema.Type<typeof ASTIncomingReferencesClause_> {}
102
- const ASTIncomingReferencesClause: Schema.Schema<ASTIncomingReferencesClause> = ASTIncomingReferencesClause_;
132
+ interface QueryIncomingReferencesClause extends Schema.Schema.Type<typeof QueryIncomingReferencesClause_> {}
133
+ const QueryIncomingReferencesClause: Schema.Schema<QueryIncomingReferencesClause> = QueryIncomingReferencesClause_;
103
134
 
104
135
  /**
105
136
  * Traverse relations connecting to an anchor object.
106
137
  */
107
- const ASTRelationClause_ = Schema.Struct({
138
+ const QueryRelationClause_ = Schema.Struct({
108
139
  type: Schema.Literal('relation'),
109
- anchor: Schema.suspend(() => AST),
140
+ anchor: Schema.suspend(() => Query),
110
141
  direction: Schema.Literal('outgoing', 'incoming', 'both'),
111
- typename: TypenameSpecifier,
112
- predicates: Schema.optional(PredicateSet),
142
+ filter: Schema.optional(Schema.suspend(() => Filter)),
113
143
  });
114
- interface ASTRelationClause extends Schema.Schema.Type<typeof ASTRelationClause_> {}
115
- const ASTRelationClause: Schema.Schema<ASTRelationClause> = ASTRelationClause_;
144
+ interface QueryRelationClause extends Schema.Schema.Type<typeof QueryRelationClause_> {}
145
+ const QueryRelationClause: Schema.Schema<QueryRelationClause> = QueryRelationClause_;
116
146
 
117
147
  /**
118
148
  * Traverse into the source or target of a relation.
119
149
  */
120
- const ASTRelationTraversalClause_ = Schema.Struct({
150
+ const QueryRelationTraversalClause_ = Schema.Struct({
121
151
  type: Schema.Literal('relation-traversal'),
122
- anchor: Schema.suspend(() => AST),
152
+ anchor: Schema.suspend(() => Query),
123
153
  direction: Schema.Literal('source', 'target', 'both'),
124
154
  });
125
- interface ASTRelationTraversalClause extends Schema.Schema.Type<typeof ASTRelationTraversalClause_> {}
126
- const ASTRelationTraversalClause: Schema.Schema<ASTRelationTraversalClause> = ASTRelationTraversalClause_;
155
+ interface QueryRelationTraversalClause extends Schema.Schema.Type<typeof QueryRelationTraversalClause_> {}
156
+ const QueryRelationTraversalClause: Schema.Schema<QueryRelationTraversalClause> = QueryRelationTraversalClause_;
127
157
 
128
158
  /**
129
159
  * Union of multiple queries.
130
160
  */
131
- const ASTUnionClause_ = Schema.Struct({
161
+ const QueryUnionClause_ = Schema.Struct({
132
162
  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,
163
+ queries: Schema.Array(Schema.suspend(() => Query)),
164
+ });
165
+ interface QueryUnionClause extends Schema.Schema.Type<typeof QueryUnionClause_> {}
166
+ const QueryUnionClause: Schema.Schema<QueryUnionClause> = QueryUnionClause_;
167
+
168
+ const Query_ = Schema.Union(
169
+ QuerySelectClause,
170
+ QueryFilterClause,
171
+ QueryReferenceTraversalClause,
172
+ QueryIncomingReferencesClause,
173
+ QueryRelationClause,
174
+ QueryRelationTraversalClause,
175
+ QueryUnionClause,
146
176
  );
147
177
 
148
- export type AST = Schema.Schema.Type<typeof AST_>;
149
- export const AST: Schema.Schema<AST> = AST_;
178
+ export type Query = Schema.Schema.Type<typeof Query_>;
179
+ export const Query: Schema.Schema<Query> = Query_;
@@ -8,7 +8,8 @@ import { describe, test } from 'vitest';
8
8
  import { create } from '@dxos/echo-schema';
9
9
  import { log } from '@dxos/log';
10
10
 
11
- import { Query } from './api';
11
+ import { Filter, Query } from './api';
12
+ import * as QueryAST from './ast';
12
13
  import { Type, Relation } from '..';
13
14
 
14
15
  //
@@ -18,6 +19,7 @@ import { Type, Relation } from '..';
18
19
  const Person = Schema.Struct({
19
20
  name: Schema.String,
20
21
  email: Schema.optional(Schema.String),
22
+ age: Schema.optional(Schema.Number),
21
23
  }).pipe(Type.def({ typename: 'dxos.org/type/Person', version: '0.1.0' }));
22
24
  interface Person extends Schema.Schema.Type<typeof Person> {}
23
25
 
@@ -48,88 +50,97 @@ describe('query api', () => {
48
50
  const getAllPeople = Query.type(Person);
49
51
 
50
52
  log.info('query', { ast: getAllPeople.ast });
53
+ Schema.validateSync(QueryAST.Query)(getAllPeople.ast);
51
54
  });
52
55
 
53
56
  test('get all people named Fred', () => {
54
57
  // Query<Person>
55
- const getAllPeopleNamedFred = Query.type(Person, { name: 'Fred' });
58
+ const getAllPeopleNamedFred = Query.select(Filter.type(Person, { name: 'Fred' }));
56
59
 
57
60
  log.info('query', { ast: getAllPeopleNamedFred.ast });
61
+ Schema.validateSync(QueryAST.Query)(getAllPeopleNamedFred.ast);
58
62
  });
59
63
 
60
64
  test('get all orgs Fred worked for since 2020', () => {
61
65
  // Query<Org>
62
66
  const fred = create(Person, { name: 'Fred' });
63
- const getAllOrgsFredWorkedForSince2020 = Query.type(Person, { id: fred.id })
64
- .sourceOf(WorksFor, { since: Query.gt('2020') })
67
+ const getAllOrgsFredWorkedForSince2020 = Query.select(Filter.type(Person, { id: fred.id }))
68
+ .sourceOf(WorksFor, { since: Filter.gt('2020') })
65
69
  .target();
66
70
 
67
71
  log.info('query', { ast: getAllOrgsFredWorkedForSince2020.ast });
72
+ Schema.validateSync(QueryAST.Query)(getAllOrgsFredWorkedForSince2020.ast);
68
73
  });
69
74
 
70
75
  test('get all tasks for Fred', () => {
71
76
  // Query<Task>
72
77
  const fred = create(Person, { name: 'Fred' });
73
- const getAllTasksForFred = Query.type(Person, { id: fred.id }).referencedBy(Task, 'assignee');
78
+ const getAllTasksForFred = Query.select(Filter.type(Person, { id: fred.id })).referencedBy(Task, 'assignee');
74
79
 
75
80
  log.info('query', { ast: getAllTasksForFred.ast });
81
+ Schema.validateSync(QueryAST.Query)(getAllTasksForFred.ast);
76
82
  });
77
83
 
78
84
  test('get all tasks for employees of Cyberdyne', () => {
79
85
  // Query<Task>
80
- const allTasksForEmployeesOfCyberdyne = Query.type(Org, { name: 'Cyberdyne' })
86
+ const allTasksForEmployeesOfCyberdyne = Query.select(Filter.type(Org, { name: 'Cyberdyne' }))
81
87
  .targetOf(WorksFor)
82
88
  .source()
83
89
  .referencedBy(Task, 'assignee');
84
90
 
85
91
  log.info('query', { ast: allTasksForEmployeesOfCyberdyne.ast });
92
+ Schema.validateSync(QueryAST.Query)(allTasksForEmployeesOfCyberdyne.ast);
86
93
  });
87
94
 
88
95
  test('get all people or orgs', () => {
89
96
  // Query<Person | Org>
90
- const allPeopleOrOrgs = Query.all(Query.type(Person), Query.type(Org));
97
+ const allPeopleOrOrgs = Query.all(Query.select(Filter.type(Person)), Query.select(Filter.type(Org)));
91
98
 
92
99
  log.info('query', { ast: allPeopleOrOrgs.ast });
100
+ Schema.validateSync(QueryAST.Query)(allPeopleOrOrgs.ast);
93
101
  });
94
102
 
95
103
  test('get assignees of all tasks created after 2020', () => {
96
104
  // Query<Person>
97
- const assigneesOfAllTasksCreatedAfter2020 = Query.type(Task, { createdAt: Query.gt('2020') }).reference('assignee');
105
+ const assigneesOfAllTasksCreatedAfter2020 = Query.select(
106
+ Filter.type(Task, { createdAt: Filter.gt('2020') }),
107
+ ).reference('assignee');
98
108
 
99
109
  log.info('query', { ast: assigneesOfAllTasksCreatedAfter2020.ast });
110
+ Schema.validateSync(QueryAST.Query)(assigneesOfAllTasksCreatedAfter2020.ast);
100
111
  });
101
112
 
102
113
  test('contact full-text search', () => {
103
114
  // Query<Person>
104
- const contactFullTextSearch = Query.text(Person, 'Bill');
115
+ const contactFullTextSearch = Query.select(Filter.text(Person, 'Bill'));
105
116
 
106
117
  log.info('query', { ast: contactFullTextSearch.ast });
118
+ Schema.validateSync(QueryAST.Query)(contactFullTextSearch.ast);
107
119
  });
108
120
 
109
- // TODO(burdon): Experimental.
110
121
  test.skip('chain', () => {
111
- const db: any = null;
112
- const Query: any = null;
113
- const Filter: any = null;
122
+ // NOTE: Can't support props without type since they can't be inferred.
123
+ // const f1: Filter<Person> = Filter.props({ name: 'Fred' });
114
124
 
115
- const x = db.exec(Query.select({ id: '123' })).first();
116
- const y = db.exec(Query.select(Filter.type(Person)).first());
125
+ // const x = Query.select(Filter.props({ id: '123' }));
126
+ const y = Query.select(Filter.type(Person));
127
+
128
+ const fOr = Filter.or(Filter.type(Person, { id: Filter.in('1', '2', '3') }), Filter.type(Org));
129
+
130
+ const fAnd = Filter.and(
131
+ Filter.type(Person, { id: Filter.in('1', '2', '3') }),
132
+ Filter.type(Person, { name: 'Fred' }),
133
+ );
117
134
 
118
135
  const q = Query
119
136
  //
120
- .selectAll()
121
- .select({ id: '123' })
122
137
  // NOTE: Can't support functions since they can't be serialized (to server).
123
138
  // .filter((object) => Math.random() > 0.5)
124
139
  .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 });
140
+ .select(Filter.type(Person, { name: 'Fred' }))
141
+ .select({ age: Filter.between(20, 40) })
142
+ .select(Filter.and(Filter.type(Person), Filter.type(Person, { name: Filter.in('bob', 'bill') })));
143
+
144
+ log.info('stuff', { fOr, fAnd, q, y });
134
145
  });
135
146
  });
@@ -37,14 +37,25 @@ namespace Testing {
37
37
 
38
38
  export interface Person extends Schema.Schema.Type<typeof Person> {}
39
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
- });
40
+ // export const WorksFor = S.Struct({
41
+ // id: Type.ObjectId,
42
+ // since: S.String,
43
+ // jobTitle: S.String,
44
+ // ...Range({ from, to }),
45
+ // ...Provenance({ source: 'duckduckgo.com', confidence: 0.9 }), // keys
46
+ // ...Relation.make({ source: Contact, target: Organization }),
47
+ // }).pipe(
48
+ // Relation.def({
49
+ // typename: 'example.com/relation/WorksFor',
50
+ // version: '0.1.0',
51
+ // }),
52
+ // );
53
+
54
+ // {
55
+ // const contact = db.add(create(Contact, { name: 'Test' }));
56
+ // const organization = db.add(create(Organization, { name: 'DXOS' }));
57
+ // db.add(create(WorksFor, { source: contact, target: organization }));
58
+ // }
48
59
 
49
60
  export const WorksFor = Schema.Struct({
50
61
  // id: Type.ObjectId,
@@ -61,13 +72,23 @@ namespace Testing {
61
72
 
62
73
  export interface WorksFor extends Schema.Schema.Type<typeof WorksFor> {}
63
74
 
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
- // );
75
+ // TODO(burdon): Fix (Type.def currently removes TypeLiteral that implements the `make` function).
76
+ // Property 'make' does not exist on type 'EchoObjectSchema<Struct<{ timestamp: PropertySignature<":", string, never, ":", string, true, never>; }>>'.ts(2339)
77
+ export const MessageStruct = Schema.Struct({
78
+ // TODO(burdon): Support S.Date; Custom Timestamp (with defaults).
79
+ // TODO(burdon): Support defaults (update create and create).
80
+ timestamp: Schema.String.pipe(
81
+ Schema.propertySignature,
82
+ Schema.withConstructorDefault(() => new Date().toISOString()),
83
+ ),
84
+ });
85
+
86
+ export const Message = MessageStruct.pipe(
87
+ Type.def({
88
+ typename: 'example.com/type/Message',
89
+ version: '0.1.0',
90
+ }),
91
+ );
71
92
 
72
93
  export interface Message extends Schema.Schema.Type<typeof Message> {}
73
94
  }
@@ -98,9 +119,7 @@ describe('Experimental API review', () => {
98
119
  });
99
120
 
100
121
  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({}));
122
+ const message = Type.create(Testing.Message, Testing.MessageStruct.make({}));
104
123
  expect(message.timestamp).to.exist;
105
124
  });
106
125
  });