@dxos/echo 0.8.4-main.c1de068 → 0.8.4-main.dedc0f3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@dxos/echo",
3
- "version": "0.8.4-main.c1de068",
3
+ "version": "0.8.4-main.dedc0f3",
4
4
  "description": "ECHO API",
5
5
  "homepage": "https://dxos.org",
6
6
  "bugs": "https://github.com/dxos/dxos/issues",
@@ -10,31 +10,37 @@
10
10
  "type": "module",
11
11
  "exports": {
12
12
  ".": {
13
+ "source": "./src/index.ts",
13
14
  "types": "./dist/types/src/index.d.ts",
14
15
  "browser": "./dist/lib/browser/index.mjs",
15
16
  "node": "./dist/lib/node-esm/index.mjs"
16
17
  },
17
18
  "./Type": {
19
+ "source": "./src/Type.ts",
18
20
  "types": "./dist/types/src/Type.d.ts",
19
21
  "browser": "./dist/lib/browser/Type.mjs",
20
22
  "node": "./dist/lib/node-esm/Type.mjs"
21
23
  },
22
24
  "./Obj": {
25
+ "source": "./src/Obj.ts",
23
26
  "types": "./dist/types/src/Obj.d.ts",
24
27
  "browser": "./dist/lib/browser/Obj.mjs",
25
28
  "node": "./dist/lib/node-esm/Obj.mjs"
26
29
  },
27
30
  "./Relation": {
31
+ "source": "./src/Relation.ts",
28
32
  "types": "./dist/types/src/Relation.d.ts",
29
33
  "browser": "./dist/lib/browser/Relation.mjs",
30
34
  "node": "./dist/lib/node-esm/Relation.mjs"
31
35
  },
32
36
  "./Ref": {
37
+ "source": "./src/Ref.ts",
33
38
  "types": "./dist/types/src/Ref.d.ts",
34
39
  "browser": "./dist/lib/browser/Ref.mjs",
35
40
  "node": "./dist/lib/node-esm/Ref.mjs"
36
41
  },
37
42
  "./testing": {
43
+ "source": "./src/testing/types.ts",
38
44
  "types": "./dist/types/src/testing/types.d.ts",
39
45
  "browser": "./dist/lib/browser/testing/index.mjs",
40
46
  "node": "./dist/lib/node-esm/testing/index.mjs"
@@ -66,18 +72,18 @@
66
72
  ],
67
73
  "dependencies": {
68
74
  "@preact/signals-core": "^1.9.0",
69
- "effect": "3.17.0",
70
- "@dxos/debug": "0.8.4-main.c1de068",
71
- "@dxos/echo-protocol": "0.8.4-main.c1de068",
72
- "@dxos/echo-schema": "0.8.4-main.c1de068",
73
- "@dxos/echo-signals": "0.8.4-main.c1de068",
74
- "@dxos/invariant": "0.8.4-main.c1de068",
75
- "@dxos/effect": "0.8.4-main.c1de068",
76
- "@dxos/keys": "0.8.4-main.c1de068",
77
- "@dxos/live-object": "0.8.4-main.c1de068",
78
- "@dxos/log": "0.8.4-main.c1de068",
79
- "@dxos/node-std": "0.8.4-main.c1de068",
80
- "@dxos/util": "0.8.4-main.c1de068"
75
+ "effect": "3.17.7",
76
+ "@dxos/debug": "0.8.4-main.dedc0f3",
77
+ "@dxos/echo-protocol": "0.8.4-main.dedc0f3",
78
+ "@dxos/echo-schema": "0.8.4-main.dedc0f3",
79
+ "@dxos/effect": "0.8.4-main.dedc0f3",
80
+ "@dxos/echo-signals": "0.8.4-main.dedc0f3",
81
+ "@dxos/invariant": "0.8.4-main.dedc0f3",
82
+ "@dxos/keys": "0.8.4-main.dedc0f3",
83
+ "@dxos/live-object": "0.8.4-main.dedc0f3",
84
+ "@dxos/node-std": "0.8.4-main.dedc0f3",
85
+ "@dxos/log": "0.8.4-main.dedc0f3",
86
+ "@dxos/util": "0.8.4-main.dedc0f3"
81
87
  },
82
88
  "publishConfig": {
83
89
  "access": "public"
package/src/Obj.ts CHANGED
@@ -3,41 +3,57 @@
3
3
  //
4
4
 
5
5
  import { Schema } from 'effect';
6
+ import { dual } from 'effect/Function';
6
7
 
7
8
  import * as EchoSchema from '@dxos/echo-schema';
8
9
  import { assertArgument, invariant } from '@dxos/invariant';
9
10
  import { type DXN } from '@dxos/keys';
10
- import type * as LiveObject from '@dxos/live-object';
11
+ import * as LiveObject from '@dxos/live-object';
11
12
  import { live } from '@dxos/live-object';
12
- import { assumeType } from '@dxos/util';
13
+ import { assumeType, deepMapValues } from '@dxos/util';
13
14
 
14
15
  import type * as Ref from './Ref';
15
16
  import type * as Relation from './Relation';
16
17
  import type * as Type from './Type';
17
18
 
18
- // NOTE: Don't export: Obj.Any and Obj.Obj form the public API.
19
- interface ObjBase extends Type.OfKind<EchoSchema.EntityKind.Object> {
20
- readonly id: EchoSchema.ObjectId;
21
- }
19
+ /**
20
+ * NOTE: Don't export: Obj.Any and Obj.Obj form the public API.
21
+ */
22
+ interface BaseObj extends EchoSchema.HasId, Type.OfKind<EchoSchema.EntityKind.Object> {}
22
23
 
23
24
  /**
24
25
  * Object type with specific properties.
25
26
  */
26
- export type Obj<Props> = ObjBase & Props;
27
+ export type Obj<Props> = BaseObj & Props;
27
28
 
28
29
  /**
29
30
  * Base type for all ECHO objects.
30
31
  */
31
- export interface Any extends ObjBase {}
32
+ export interface Any extends BaseObj {}
33
+
34
+ type Props<T = any> = { id?: EchoSchema.ObjectId } & Type.Properties<T>;
35
+
36
+ export type MakeProps<T extends Type.Obj.Any> = NoInfer<Props<Schema.Schema.Type<T>>> & {
37
+ [Meta]?: Partial<EchoSchema.ObjectMeta>;
38
+ };
32
39
 
33
- type Props<T> = { id?: EchoSchema.ObjectId } & Type.Properties<T>;
40
+ export const Meta: unique symbol = EchoSchema.MetaId as any;
34
41
 
35
- export type MakeProps<S extends Type.Obj.Any> = NoInfer<Props<Schema.Schema.Type<S>>>;
42
+ // TODO(dmaretskyi): Expose Meta = EchoSchema.MetaId.
36
43
 
37
44
  /**
38
45
  * Creates new object.
46
+ * @param schema - Object schema.
47
+ * @param props - Object properties.
48
+ * @param meta - Object metadata (deprecated) -- pass with Obj.Meta.
49
+ *
50
+ * Meta can be passed as a symbol in `props`.
51
+ *
52
+ * Example:
53
+ * ```ts
54
+ * const obj = Obj.make(Person, { [Obj.Meta]: { keys: [...] }, name: 'John' });
55
+ * ```
39
56
  */
40
- // TODO(dmaretskyi): Move meta into props.
41
57
  export const make = <S extends Type.Obj.Any>(
42
58
  schema: S,
43
59
  props: MakeProps<S>,
@@ -45,6 +61,7 @@ export const make = <S extends Type.Obj.Any>(
45
61
  ): LiveObject.Live<Schema.Schema.Type<S>> => {
46
62
  assertArgument(
47
63
  EchoSchema.getTypeAnnotation(schema)?.kind === EchoSchema.EntityKind.Object,
64
+ 'schema',
48
65
  'Expected an object schema',
49
66
  );
50
67
 
@@ -90,8 +107,8 @@ export const instanceOf: {
90
107
  export const getSchema = EchoSchema.getSchema;
91
108
 
92
109
  // TODO(dmaretskyi): Allow returning undefined.
93
- export const getDXN = (obj: Any): DXN => {
94
- assertArgument(!Schema.isSchema(obj), 'Object should not be a schema.');
110
+ export const getDXN = (obj: Any | Relation.Any): DXN => {
111
+ assertArgument(!Schema.isSchema(obj), 'obj', 'Object should not be a schema.');
95
112
  const dxn = EchoSchema.getObjectDXN(obj);
96
113
  invariant(dxn != null, 'Invalid object.');
97
114
  return dxn;
@@ -108,7 +125,7 @@ export const getTypeDXN = EchoSchema.getType;
108
125
  * @returns The typename of the object's type.
109
126
  * @example `example.com/type/Contact`
110
127
  */
111
- export const getTypename = (obj: Any): string | undefined => {
128
+ export const getTypename = (obj: Any | Relation.Any): string | undefined => {
112
129
  const schema = getSchema(obj);
113
130
  if (schema == null) {
114
131
  // Try to extract typename from DXN.
@@ -119,27 +136,61 @@ export const getTypename = (obj: Any): string | undefined => {
119
136
  };
120
137
 
121
138
  // TODO(dmaretskyi): Allow returning undefined.
122
- export const getMeta = (obj: Any): EchoSchema.ObjectMeta => {
139
+ export const getMeta = (obj: Any | Relation.Any): EchoSchema.ObjectMeta => {
123
140
  const meta = EchoSchema.getMeta(obj);
124
141
  invariant(meta != null, 'Invalid object.');
125
142
  return meta;
126
143
  };
127
144
 
145
+ /**
146
+ * @returns Foreign keys for the object from the specified source.
147
+ */
148
+ export const getKeys: {
149
+ (obj: Any | Relation.Any, source: string): EchoSchema.ForeignKey[];
150
+ (source: string): (obj: Any | Relation.Any) => EchoSchema.ForeignKey[];
151
+ } = dual(2, (obj: Any | Relation.Any, source?: string): EchoSchema.ForeignKey[] => {
152
+ const meta = EchoSchema.getMeta(obj);
153
+ invariant(meta != null, 'Invalid object.');
154
+ return meta.keys.filter((key) => key.source === source);
155
+ });
156
+
157
+ /**
158
+ * Delete all keys from the object for the specified source.
159
+ * @param obj
160
+ * @param source
161
+ */
162
+ export const deleteKeys = (obj: Any | Relation.Any, source: string) => {
163
+ const meta = EchoSchema.getMeta(obj);
164
+ for (let i = 0; i < meta.keys.length; i++) {
165
+ if (meta.keys[i].source === source) {
166
+ meta.keys.splice(i, 1);
167
+ i--;
168
+ }
169
+ }
170
+ };
171
+
128
172
  // TODO(dmaretskyi): Default to `false`.
129
- export const isDeleted = (obj: Any): boolean => {
173
+ export const isDeleted = (obj: Any | Relation.Any): boolean => {
130
174
  const deleted = EchoSchema.isDeleted(obj);
131
175
  invariant(typeof deleted === 'boolean', 'Invalid object.');
132
176
  return deleted;
133
177
  };
134
178
 
135
179
  // TODO(burdon): Rename "label"
136
- export const getLabel = (obj: Any): string | undefined => {
180
+ export const getLabel = (obj: Any | Relation.Any): string | undefined => {
137
181
  const schema = getSchema(obj);
138
182
  if (schema != null) {
139
183
  return EchoSchema.getLabel(schema, obj);
140
184
  }
141
185
  };
142
186
 
187
+ export const setLabel = (obj: Any | Relation.Any, label: string) => {
188
+ const schema = getSchema(obj);
189
+ if (schema != null) {
190
+ EchoSchema.setLabel(schema, obj, label);
191
+ }
192
+ };
193
+
143
194
  /**
144
195
  * JSON representation of an object.
145
196
  */
@@ -150,6 +201,7 @@ export type JSON = EchoSchema.ObjectJSON;
150
201
  *
151
202
  * The same algorithm is used when calling the standard `JSON.stringify(obj)` function.
152
203
  */
204
+ // TODO(burdon): Base util type for Obj/Relation?
153
205
  export const toJSON = (obj: Any | Relation.Any): JSON => EchoSchema.objectToJSON(obj);
154
206
 
155
207
  /**
@@ -157,6 +209,139 @@ export const toJSON = (obj: Any | Relation.Any): JSON => EchoSchema.objectToJSON
157
209
  * References and schemas will be resolvable if the `refResolver` is provided.
158
210
  *
159
211
  * The function need to be async to support resolving the schema as well as the relation endpoints.
212
+ *
213
+ * @param options.refResolver - Resolver for references. Produces hydrated references that can be resolved.
214
+ * @param options.dxn - Override object DXN. Changes the result of `Obj.getDXN`.
160
215
  */
161
- export const fromJSON: (json: unknown, options?: { refResolver?: Ref.Resolver }) => Promise<Any> =
216
+ export const fromJSON: (json: unknown, options?: { refResolver?: Ref.Resolver; dxn?: DXN }) => Promise<Any> =
162
217
  EchoSchema.objectFromJSON as any;
218
+
219
+ /**
220
+ * Returns an immutable snapshot of an object.
221
+ */
222
+ export const getSnapshot: <T extends Any>(obj: Obj<T>) => T = LiveObject.getSnapshot;
223
+
224
+ export type CloneOptions = {
225
+ /**
226
+ * Retain the original object's ID.
227
+ * @default false
228
+ */
229
+ retainId?: boolean;
230
+ };
231
+
232
+ /**
233
+ * Clones an object or relation.
234
+ * This does not clone referenced objects, only the properties in the object.
235
+ * @returns A new object with the same schema and properties.
236
+ */
237
+ export const clone = <T extends Any | Relation.Any>(obj: T, opts?: CloneOptions): T => {
238
+ const { id, ...data } = obj;
239
+ const schema = getSchema(obj);
240
+ invariant(schema != null, 'Object should have a schema');
241
+ const props: any = deepMapValues(data, (value, recurse) => {
242
+ if (EchoSchema.Ref.isRef(value)) {
243
+ return value;
244
+ }
245
+ return recurse(value);
246
+ });
247
+ if (opts?.retainId) {
248
+ props.id = id;
249
+ }
250
+ const meta = getMeta(obj);
251
+ props[EchoSchema.MetaId] = deepMapValues(meta, (value, recurse) => {
252
+ if (EchoSchema.Ref.isRef(value)) {
253
+ return value;
254
+ }
255
+ return recurse(value);
256
+ });
257
+ return make(schema, props);
258
+ };
259
+
260
+ export const VersionTypeId = EchoSchema.VersionTypeId;
261
+ export type VersionType = typeof VersionTypeId;
262
+
263
+ /**
264
+ * Represent object version.
265
+ * May be backed by Automerge.
266
+ * Objects with no history are not versioned.
267
+ */
268
+ export interface Version {
269
+ [VersionTypeId]: {};
270
+
271
+ /**
272
+ * Whether the object is versioned.
273
+ */
274
+ versioned: boolean;
275
+
276
+ /**
277
+ * Automerge heads.
278
+ */
279
+ automergeHeads?: string[];
280
+ }
281
+
282
+ const unversioned: Version = {
283
+ [VersionTypeId]: {},
284
+ versioned: false,
285
+ };
286
+
287
+ /**
288
+ * Checks that `obj` is a version object.
289
+ */
290
+ export const isVersion = (obj: unknown): obj is Version => {
291
+ return obj != null && typeof obj === 'object' && VersionTypeId in obj;
292
+ };
293
+
294
+ /**
295
+ * Returns the version of the object.
296
+ */
297
+ export const version = (obj: Any | Relation.Any): Version => {
298
+ const version = (obj as any)[EchoSchema.ObjectVersionId];
299
+ if (version === undefined) {
300
+ return unversioned;
301
+ }
302
+ return version;
303
+ };
304
+
305
+ /**
306
+ * Checks that `version` is a valid version object.
307
+ */
308
+ export const versionValid = (version: Version): boolean => {
309
+ assertArgument(isVersion(version), 'version', 'Invalid version object');
310
+ return !!version.versioned;
311
+ };
312
+
313
+ export type VersionCompareResult = 'unversioned' | 'equal' | 'different';
314
+
315
+ /**
316
+ * Compares two versions.
317
+ * @param version1
318
+ * @param version2
319
+ * @returns 'unversioned' if either object is unversioned, 'equal' if the versions are equal, 'different' if the versions are different.
320
+ */
321
+ export const compareVersions = (version1: Version, version2: Version): VersionCompareResult => {
322
+ assertArgument(isVersion(version1), 'version1', 'Invalid version object');
323
+ assertArgument(isVersion(version2), 'version2', 'Invalid version object');
324
+
325
+ if (!versionValid(version1) || !versionValid(version2)) {
326
+ return 'unversioned';
327
+ }
328
+
329
+ if (version1.automergeHeads?.length !== version2.automergeHeads?.length) {
330
+ return 'different';
331
+ }
332
+ if (version1.automergeHeads?.some((head) => !version2.automergeHeads?.includes(head))) {
333
+ return 'different';
334
+ }
335
+
336
+ return 'equal';
337
+ };
338
+
339
+ export const encodeVersion = (version: Version): string => {
340
+ return JSON.stringify(version);
341
+ };
342
+
343
+ export const decodeVersion = (version: string): Version => {
344
+ const parsed = JSON.parse(version);
345
+ parsed[VersionTypeId] = {};
346
+ return parsed;
347
+ };
package/src/Relation.ts CHANGED
@@ -14,22 +14,23 @@ import { assumeType } from '@dxos/util';
14
14
  import type * as Obj from './Obj';
15
15
  import type * as Type from './Type';
16
16
 
17
- // NOTE: Don't export: Relation.Relation and Relation.Any form the public API.
18
- interface RelationBase<Source, Target>
19
- extends Type.Relation.Endpoints<Source, Target>,
20
- Type.OfKind<EchoSchema.EntityKind.Relation> {
21
- readonly id: EchoSchema.ObjectId;
22
- }
17
+ /**
18
+ * NOTE: Don't export: Relation.Relation and Relation.Any form the public API.
19
+ */
20
+ interface BaseRelation<Source, Target>
21
+ extends EchoSchema.HasId,
22
+ Type.Relation.Endpoints<Source, Target>,
23
+ Type.OfKind<EchoSchema.EntityKind.Relation> {}
23
24
 
24
25
  /**
25
26
  * Relation type with specific properties.
26
27
  */
27
- export type Relation<Source extends Obj.Any, Target extends Obj.Any, Props> = RelationBase<Source, Target> & Props;
28
+ export type Relation<Source extends Obj.Any, Target extends Obj.Any, Props> = BaseRelation<Source, Target> & Props;
28
29
 
29
30
  /**
30
31
  * Base type for all ECHO relations.
31
32
  */
32
- export interface Any extends RelationBase<Obj.Any, Obj.Any> {}
33
+ export interface Any extends BaseRelation<Obj.Any, Obj.Any> {}
33
34
 
34
35
  // TODO(dmaretskyi): Has to be `unique symbol`.
35
36
  export const Source: unique symbol = EchoSchema.RelationSourceId as any;
@@ -59,6 +60,7 @@ export const make = <S extends Type.Relation.Any>(
59
60
  ): Live<Schema.Schema.Type<S> & Type.OfKind<EchoSchema.EntityKind.Relation>> => {
60
61
  assertArgument(
61
62
  EchoSchema.getTypeAnnotation(schema)?.kind === EchoSchema.EntityKind.Relation,
63
+ 'schema',
62
64
  'Expected a relation schema',
63
65
  );
64
66
 
package/src/Type.ts CHANGED
@@ -39,7 +39,7 @@ export type Schema = EchoSchema.EchoSchema;
39
39
  /**
40
40
  * Returns all properties of an object or relation except for the id and kind.
41
41
  */
42
- export type Properties<T> = Omit<T, 'id' | KindId | RelationModule.Source | RelationModule.Target>;
42
+ export type Properties<T = any> = Omit<T, 'id' | KindId | RelationModule.Source | RelationModule.Target>;
43
43
 
44
44
  //
45
45
  // Obj
@@ -80,6 +80,7 @@ export namespace Obj {
80
80
  * NOTE: This is not an instance type.
81
81
  */
82
82
  // TODO(dmaretskyi): If schema was covariant, we could specify props in here, like `id: ObjectId`.
83
+ // TODO(burdon): This erases the ECHO type info (e.g., id, typename).
83
84
  export type Any = Schema.Schema.AnyNoContext;
84
85
  }
85
86
 
@@ -87,6 +88,7 @@ export namespace Obj {
87
88
  // Expando
88
89
  //
89
90
 
91
+ // TODO(burdon): We're using Expando in many places as a base type.
90
92
  export interface Expando extends OfKind<EchoSchema.EntityKind.Object> {
91
93
  [key: string]: any;
92
94
  }
package/src/index.ts CHANGED
@@ -9,5 +9,6 @@ export * as Relation from './Relation';
9
9
  export * as Type from './Type';
10
10
 
11
11
  export { DXN } from '@dxos/keys';
12
- export { Filter, Query } from './query';
12
+ export { Filter, Order, Query } from './query';
13
+ export { QueryAST } from '@dxos/echo-protocol';
13
14
  export { type Live } from '@dxos/live-object';
@@ -9,11 +9,12 @@ import { QueryAST } from '@dxos/echo-protocol';
9
9
  import { DXN } from '@dxos/keys';
10
10
  import { log } from '@dxos/log';
11
11
 
12
- import { Filter, Query } from './dsl';
13
12
  import * as Obj from '../Obj';
14
13
  import * as Ref from '../Ref';
15
14
  import * as Type from '../Type';
16
15
 
16
+ import { Filter, Order, Query } from './dsl';
17
+
17
18
  //
18
19
  // Example schema
19
20
  //
@@ -73,6 +74,14 @@ describe('query api', () => {
73
74
  console.log('getAllPeople', JSON.stringify(getAllPeople.ast, null, 2));
74
75
  });
75
76
 
77
+ test('get all people ordered by name', () => {
78
+ const getAllPeopleOrderedByName = Query.type(Person).orderBy(Order.property('name', 'asc'));
79
+
80
+ log('query', { ast: getAllPeopleOrderedByName.ast });
81
+ Schema.validateSync(QueryAST.Query)(getAllPeopleOrderedByName.ast);
82
+ console.log('getAllPeopleOrderedByName', JSON.stringify(getAllPeopleOrderedByName.ast, null, 2));
83
+ });
84
+
76
85
  test('get all people named Fred', () => {
77
86
  const PeopleNamedFred = Query.select(Filter.type(Person, { name: 'Fred' }));
78
87
 
package/src/query/dsl.ts CHANGED
@@ -3,6 +3,7 @@
3
3
  //
4
4
 
5
5
  import { Schema } from 'effect';
6
+ import type { NonEmptyArray } from 'effect/Array';
6
7
  import type { Simplify } from 'effect/Schema';
7
8
 
8
9
  import { raise } from '@dxos/debug';
@@ -17,6 +18,35 @@ import type * as Type from '../Type';
17
18
  // TODO(dmaretskyi): Split up into interfaces for objects and relations so they can have separate verbs.
18
19
  // TODO(dmaretskyi): Undirected relation traversals.
19
20
 
21
+ export interface Order<T> {
22
+ // TODO(dmaretskyi): See new effect-schema approach to variance.
23
+ '~Order': { value: T };
24
+
25
+ ast: QueryAST.Order;
26
+ }
27
+
28
+ class OrderClass implements Order<any> {
29
+ private static variance: Order<any>['~Order'] = {} as Order<any>['~Order'];
30
+
31
+ static is(value: unknown): value is Order<any> {
32
+ return typeof value === 'object' && value !== null && '~Order' in value;
33
+ }
34
+
35
+ constructor(public readonly ast: QueryAST.Order) {}
36
+
37
+ '~Order' = OrderClass.variance;
38
+ }
39
+
40
+ export namespace Order {
41
+ export const natural: Order<any> = new OrderClass({ kind: 'natural' });
42
+ export const property = <T>(property: keyof T & string, direction: QueryAST.OrderDirection): Order<T> =>
43
+ new OrderClass({
44
+ kind: 'property',
45
+ property,
46
+ direction,
47
+ });
48
+ }
49
+
20
50
  export interface Query<T> {
21
51
  // TODO(dmaretskyi): See new effect-schema approach to variance.
22
52
  '~Query': { value: T };
@@ -85,6 +115,14 @@ export interface Query<T> {
85
115
  */
86
116
  target(): Query<Type.Relation.Target<T>>;
87
117
 
118
+ /**
119
+ * Order the query results.
120
+ * Orders are specified in priority order. The first order will be applied first, etc.
121
+ * @param order - Order to sort the results.
122
+ * @returns Query for the ordered results.
123
+ */
124
+ orderBy(...order: NonEmptyArray<Order<T>>): Query<T>;
125
+
88
126
  /**
89
127
  * Add options to a query.
90
128
  */
@@ -94,6 +132,9 @@ export interface Query<T> {
94
132
  interface QueryAPI {
95
133
  is(value: unknown): value is Query.Any;
96
134
 
135
+ /** Construct a query from an ast. */
136
+ fromAst(ast: QueryAST.Query): Query<any>;
137
+
97
138
  /**
98
139
  * Select objects based on a filter.
99
140
  * @param filter - Filter to select the objects.
@@ -329,6 +370,7 @@ class FilterClass implements Filter<any> {
329
370
  static ids(...ids: ObjectId[]): Filter<any> {
330
371
  assertArgument(
331
372
  ids.every((id) => ObjectId.isValid(id)),
373
+ 'ids',
332
374
  'ids must be valid',
333
375
  );
334
376
 
@@ -357,7 +399,7 @@ class FilterClass implements Filter<any> {
357
399
  }
358
400
 
359
401
  static typename(typename: string): Filter<any> {
360
- assertArgument(!typename.startsWith('dxn:'), 'Typename must no be qualified');
402
+ assertArgument(!typename.startsWith('dxn:'), 'typename', 'Typename must no be qualified');
361
403
  return new FilterClass({
362
404
  type: 'object',
363
405
  typename: DXN.fromTypename(typename).toString(),
@@ -506,7 +548,11 @@ type RefPropKey<T> = keyof T & string;
506
548
  const propsFilterToAst = (predicates: Filter.Props<any>): Pick<QueryAST.FilterObject, 'id' | 'props'> => {
507
549
  let idFilter: readonly ObjectId[] | undefined;
508
550
  if ('id' in predicates) {
509
- assertArgument(typeof predicates.id === 'string' || Array.isArray(predicates.id), 'invalid id filter');
551
+ assertArgument(
552
+ typeof predicates.id === 'string' || Array.isArray(predicates.id),
553
+ 'predicates.id',
554
+ 'invalid id filter',
555
+ );
510
556
  idFilter = typeof predicates.id === 'string' ? [predicates.id] : predicates.id;
511
557
  Schema.Array(ObjectId).pipe(Schema.validateSync)(idFilter);
512
558
  }
@@ -528,6 +574,10 @@ class QueryClass implements Query<any> {
528
574
  return typeof value === 'object' && value !== null && '~Query' in value;
529
575
  }
530
576
 
577
+ static fromAst(ast: QueryAST.Query): Query<any> {
578
+ return new QueryClass(ast);
579
+ }
580
+
531
581
  static select<F extends Filter.Any>(filter: F): Query<Filter.Type<F>> {
532
582
  return new QueryClass({
533
583
  type: 'select',
@@ -634,6 +684,14 @@ class QueryClass implements Query<any> {
634
684
  });
635
685
  }
636
686
 
687
+ orderBy(...order: Order<any>[]): Query<any> {
688
+ return new QueryClass({
689
+ type: 'order',
690
+ query: this.ast,
691
+ order: order.map((o) => o.ast),
692
+ });
693
+ }
694
+
637
695
  options(options: QueryAST.QueryOptions): Query<any> {
638
696
  return new QueryClass({
639
697
  type: 'options',
@@ -6,9 +6,9 @@ import { Schema } from 'effect';
6
6
  import { describe, test } from 'vitest';
7
7
 
8
8
  import { raise } from '@dxos/debug';
9
- import { FormatEnum, FormatAnnotation } from '@dxos/echo-schema';
9
+ import { FormatAnnotation, FormatEnum } from '@dxos/echo-schema';
10
10
 
11
- import { Obj, Ref, Relation, Type, type Live } from '../index';
11
+ import { type Live, Obj, Ref, Relation, Type } from '../index';
12
12
 
13
13
  namespace Testing {
14
14
  export const Organization = Schema.Struct({
@@ -151,6 +151,13 @@ describe('Experimental API review', () => {
151
151
  expect(Relation.getTarget(worksFor)).to.eq(organization);
152
152
  });
153
153
 
154
+ test('version', ({ expect }) => {
155
+ const person = Obj.make(Testing.Person, { name: 'Test' });
156
+ const version = Obj.version(person);
157
+ expect(Obj.isVersion(version)).to.be.true;
158
+ expect(Obj.versionValid(version)).to.be.false;
159
+ });
160
+
154
161
  test.skip('type narrowing', () => {
155
162
  const a: Obj.Any | Relation.Any = null as any;
156
163