@dxos/echo-db 2.29.2-dev.8c2ad8e5 → 2.29.2-dev.f6ed60b8
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/dist/src/api/database.d.ts +11 -7
- package/dist/src/api/database.d.ts.map +1 -1
- package/dist/src/api/database.js +22 -24
- package/dist/src/api/database.js.map +1 -1
- package/dist/src/api/database.test.js +13 -13
- package/dist/src/api/database.test.js.map +1 -1
- package/dist/src/api/item.d.ts.map +1 -1
- package/dist/src/api/item.js +1 -1
- package/dist/src/api/item.js.map +1 -1
- package/dist/src/api/result-set.d.ts.map +1 -1
- package/dist/src/api/result-set.js +1 -0
- package/dist/src/api/result-set.js.map +1 -1
- package/dist/src/api/selection/index.d.ts +5 -0
- package/dist/src/api/selection/index.d.ts.map +1 -0
- package/dist/src/api/selection/index.js +20 -0
- package/dist/src/api/selection/index.js.map +1 -0
- package/dist/src/api/selection/queries.d.ts +51 -0
- package/dist/src/api/selection/queries.d.ts.map +1 -0
- package/dist/src/api/selection/queries.js +70 -0
- package/dist/src/api/selection/queries.js.map +1 -0
- package/dist/src/api/selection/result.d.ts +50 -0
- package/dist/src/api/selection/result.d.ts.map +1 -0
- package/dist/src/api/selection/result.js +91 -0
- package/dist/src/api/selection/result.js.map +1 -0
- package/dist/src/api/selection/selection.d.ts +96 -0
- package/dist/src/api/selection/selection.d.ts.map +1 -0
- package/dist/src/api/selection/selection.js +164 -0
- package/dist/src/api/selection/selection.js.map +1 -0
- package/dist/src/api/{selection.test.d.ts → selection/selection.test.d.ts} +0 -0
- package/dist/src/api/selection/selection.test.d.ts.map +1 -0
- package/dist/src/api/{selection.test.js → selection/selection.test.js} +46 -44
- package/dist/src/api/selection/selection.test.js.map +1 -0
- package/dist/src/api/selection/util.d.ts +7 -0
- package/dist/src/api/selection/util.d.ts.map +1 -0
- package/dist/src/api/selection/util.js +25 -0
- package/dist/src/api/selection/util.js.map +1 -0
- package/dist/src/echo.test.js +20 -20
- package/dist/src/echo.test.js.map +1 -1
- package/dist/src/halo/contact-manager.js +2 -2
- package/dist/src/halo/contact-manager.js.map +1 -1
- package/dist/src/halo/halo.d.ts +1 -1
- package/dist/src/halo/halo.d.ts.map +1 -1
- package/dist/src/halo/halo.js +2 -2
- package/dist/src/halo/halo.js.map +1 -1
- package/dist/src/halo/preferences.js +6 -6
- package/dist/src/halo/preferences.js.map +1 -1
- package/dist/src/parties/party-core.test.js +3 -3
- package/dist/src/parties/party-core.test.js.map +1 -1
- package/dist/src/parties/party-factory.d.ts.map +1 -1
- package/dist/src/parties/party-factory.js +3 -3
- package/dist/src/parties/party-factory.js.map +1 -1
- package/dist/src/parties/party-internal.js +3 -3
- package/dist/src/parties/party-internal.js.map +1 -1
- package/dist/src/parties/party-manager.js +1 -1
- package/dist/src/parties/party-manager.js.map +1 -1
- package/dist/src/parties/party-manager.test.js +8 -10
- package/dist/src/parties/party-manager.test.js.map +1 -1
- package/dist/src/testing/testing-factories.d.ts +2 -2
- package/dist/src/testing/testing-factories.d.ts.map +1 -1
- package/dist/src/testing/testing-factories.js +1 -1
- package/dist/src/testing/testing-factories.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +17 -17
- package/src/api/database.test.ts +13 -13
- package/src/api/database.ts +30 -30
- package/src/api/item.ts +2 -2
- package/src/api/result-set.ts +1 -0
- package/src/api/selection/index.ts +8 -0
- package/src/api/selection/queries.ts +108 -0
- package/src/api/selection/result.ts +112 -0
- package/src/api/{selection.test.ts → selection/selection.test.ts} +50 -48
- package/src/api/{selection.ts → selection/selection.ts} +30 -231
- package/src/api/selection/util.ts +27 -0
- package/src/echo.test.ts +20 -20
- package/src/halo/contact-manager.ts +2 -2
- package/src/halo/halo.ts +2 -3
- package/src/halo/preferences.ts +6 -6
- package/src/parties/party-core.test.ts +3 -3
- package/src/parties/party-factory.ts +3 -4
- package/src/parties/party-internal.ts +3 -3
- package/src/parties/party-manager.test.ts +8 -10
- package/src/parties/party-manager.ts +1 -1
- package/src/testing/testing-factories.ts +3 -3
- package/dist/src/api/selection.d.ts +0 -183
- package/dist/src/api/selection.d.ts.map +0 -1
- package/dist/src/api/selection.js +0 -308
- package/dist/src/api/selection.js.map +0 -1
- package/dist/src/api/selection.test.d.ts.map +0 -1
- package/dist/src/api/selection.test.js.map +0 -1
package/src/api/database.ts
CHANGED
|
@@ -14,16 +14,16 @@ import { DatabaseBackend, DataServiceHost, ItemManager } from '../database';
|
|
|
14
14
|
import { Entity } from './entity';
|
|
15
15
|
import { Item } from './item';
|
|
16
16
|
import { Link } from './link';
|
|
17
|
-
import { RootFilter, Selection,
|
|
17
|
+
import { RootFilter, Selection, createSelection } from './selection';
|
|
18
18
|
|
|
19
|
-
export interface
|
|
19
|
+
export interface CreateItemOption<M extends Model> {
|
|
20
20
|
model?: ModelConstructor<M>
|
|
21
21
|
type?: ItemType
|
|
22
22
|
parent?: ItemID
|
|
23
23
|
props?: any // TODO(marik-d): Type this better. Rename properties?
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
export interface
|
|
26
|
+
export interface CreateLinkOptions<M extends Model, L extends Model, R extends Model> {
|
|
27
27
|
model?: ModelConstructor<M>
|
|
28
28
|
type?: ItemType
|
|
29
29
|
source: Item<L>
|
|
@@ -31,20 +31,19 @@ export interface LinkCreationOptions<M extends Model, L extends Model, R extends
|
|
|
31
31
|
props?: any // TODO(marik-d): Type this better.
|
|
32
32
|
}
|
|
33
33
|
|
|
34
|
-
enum State {
|
|
35
|
-
|
|
36
|
-
|
|
34
|
+
export enum State {
|
|
35
|
+
NULL = 'NULL',
|
|
36
|
+
INITIALIZED = 'INITIALIZED',
|
|
37
37
|
DESTROYED = 'DESTROYED',
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
41
|
-
* Represents a shared dataset containing queryable Items that are constructed from an ordered stream
|
|
42
|
-
* of mutations.
|
|
41
|
+
* Represents a shared dataset containing queryable Items that are constructed from an ordered stream of mutations.
|
|
43
42
|
*/
|
|
44
43
|
export class Database {
|
|
45
44
|
private readonly _itemManager: ItemManager;
|
|
46
45
|
|
|
47
|
-
private _state = State.
|
|
46
|
+
private _state = State.NULL;
|
|
48
47
|
|
|
49
48
|
/**
|
|
50
49
|
* Creates a new database instance. `database.initialize()` must be called afterwards to complete the initialization.
|
|
@@ -57,6 +56,10 @@ export class Database {
|
|
|
57
56
|
this._itemManager = new ItemManager(this._modelFactory, memberKey, this._backend.getWriteStream());
|
|
58
57
|
}
|
|
59
58
|
|
|
59
|
+
get state () {
|
|
60
|
+
return this._state;
|
|
61
|
+
}
|
|
62
|
+
|
|
60
63
|
get isReadOnly () {
|
|
61
64
|
return this._backend.isReadOnly;
|
|
62
65
|
}
|
|
@@ -71,26 +74,26 @@ export class Database {
|
|
|
71
74
|
|
|
72
75
|
/**
|
|
73
76
|
* Fired immediately after any update in the entities.
|
|
74
|
-
*
|
|
75
77
|
* If the information about which entity got updated is not required prefer using `update`.
|
|
76
78
|
*/
|
|
79
|
+
// TODO(burdon): Unused?
|
|
77
80
|
get entityUpdate (): Event<Entity<any>> {
|
|
78
81
|
return this._itemManager.update;
|
|
79
82
|
}
|
|
80
83
|
|
|
81
84
|
@synchronized
|
|
82
85
|
async initialize () {
|
|
83
|
-
if (this._state !== State.
|
|
86
|
+
if (this._state !== State.NULL) {
|
|
84
87
|
throw new Error('Invalid state: database was already initialized.');
|
|
85
88
|
}
|
|
86
89
|
|
|
87
90
|
await this._backend.open(this._itemManager, this._modelFactory);
|
|
88
|
-
this._state = State.
|
|
91
|
+
this._state = State.INITIALIZED;
|
|
89
92
|
}
|
|
90
93
|
|
|
91
94
|
@synchronized
|
|
92
95
|
async destroy () {
|
|
93
|
-
if (this._state === State.DESTROYED || this._state === State.
|
|
96
|
+
if (this._state === State.DESTROYED || this._state === State.NULL) {
|
|
94
97
|
return;
|
|
95
98
|
}
|
|
96
99
|
|
|
@@ -101,7 +104,7 @@ export class Database {
|
|
|
101
104
|
/**
|
|
102
105
|
* Creates a new item with the given queryable type and model.
|
|
103
106
|
*/
|
|
104
|
-
async createItem <M extends Model<any>> (options:
|
|
107
|
+
async createItem <M extends Model<any>> (options: CreateItemOption<M> = {}): Promise<Item<M>> {
|
|
105
108
|
this._assertInitialized();
|
|
106
109
|
if (!options.model) {
|
|
107
110
|
options.model = ObjectModel as any as ModelConstructor<M>;
|
|
@@ -109,22 +112,21 @@ export class Database {
|
|
|
109
112
|
|
|
110
113
|
validateModelClass(options.model);
|
|
111
114
|
|
|
112
|
-
if (options.type && typeof options.type !== 'string') {
|
|
115
|
+
if (options.type && typeof options.type !== 'string' as ItemType) {
|
|
113
116
|
throw new TypeError('Invalid type.');
|
|
114
117
|
}
|
|
115
118
|
|
|
116
|
-
if (options.parent && typeof options.parent !== 'string') {
|
|
119
|
+
if (options.parent && typeof options.parent !== 'string' as ItemID) {
|
|
117
120
|
throw new TypeError('Optional parent item id must be a string id of an existing item.');
|
|
118
121
|
}
|
|
119
122
|
|
|
120
123
|
// TODO(burdon): Get modelType from somewhere other than `ObjectModel.meta.type`.
|
|
121
|
-
|
|
124
|
+
return await this._itemManager.createItem(
|
|
122
125
|
options.model.meta.type, options.type, options.parent, options.props) as any;
|
|
123
|
-
return item;
|
|
124
126
|
}
|
|
125
127
|
|
|
126
128
|
async createLink<M extends Model<any>, S extends Model<any>, T extends Model<any>> (
|
|
127
|
-
options:
|
|
129
|
+
options: CreateLinkOptions<M, S, T>
|
|
128
130
|
): Promise<Link<M, S, T>> {
|
|
129
131
|
this._assertInitialized();
|
|
130
132
|
|
|
@@ -135,15 +137,12 @@ export class Database {
|
|
|
135
137
|
|
|
136
138
|
validateModelClass(model);
|
|
137
139
|
|
|
138
|
-
if (options.type && typeof options.type !== 'string') {
|
|
140
|
+
if (options.type && typeof options.type !== 'string' as ItemType) {
|
|
139
141
|
throw new TypeError('Invalid type.');
|
|
140
142
|
}
|
|
141
143
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return this._itemManager
|
|
146
|
-
.createLink(model.meta.type, options.type, options.source.id, options.target.id, options.props);
|
|
144
|
+
return this._itemManager.createLink(
|
|
145
|
+
model.meta.type, options.type, options.source.id, options.target.id, options.props);
|
|
147
146
|
}
|
|
148
147
|
|
|
149
148
|
/**
|
|
@@ -158,11 +157,12 @@ export class Database {
|
|
|
158
157
|
/**
|
|
159
158
|
* Waits for item matching the filter to be present and returns it.
|
|
160
159
|
*/
|
|
160
|
+
// TODO(burdon): Generalize waitForCondition.
|
|
161
161
|
async waitForItem<T extends Model<any>> (filter: RootFilter): Promise<Item<T>> {
|
|
162
|
-
const result = this.select(filter).
|
|
162
|
+
const result = this.select(filter).exec();
|
|
163
163
|
await result.update.waitForCondition(() => result.entities.length > 0);
|
|
164
164
|
const item = result.expectOne();
|
|
165
|
-
assert(item, 'Possible condition detected.');
|
|
165
|
+
assert(item, 'Possible race condition detected.');
|
|
166
166
|
return item as Item<T>;
|
|
167
167
|
}
|
|
168
168
|
|
|
@@ -171,7 +171,7 @@ export class Database {
|
|
|
171
171
|
* @param filter
|
|
172
172
|
*/
|
|
173
173
|
select (filter?: RootFilter): Selection<Item<any>> {
|
|
174
|
-
return
|
|
174
|
+
return createSelection<void>(
|
|
175
175
|
() => this._itemManager.items,
|
|
176
176
|
() => this._itemManager.debouncedUpdate,
|
|
177
177
|
this,
|
|
@@ -186,7 +186,7 @@ export class Database {
|
|
|
186
186
|
* @param filter
|
|
187
187
|
*/
|
|
188
188
|
reduce<R> (result: R, filter?: RootFilter): Selection<Item<any>, R> {
|
|
189
|
-
return
|
|
189
|
+
return createSelection<R>(
|
|
190
190
|
() => this._itemManager.items,
|
|
191
191
|
() => this._itemManager.debouncedUpdate,
|
|
192
192
|
this,
|
|
@@ -205,7 +205,7 @@ export class Database {
|
|
|
205
205
|
}
|
|
206
206
|
|
|
207
207
|
private _assertInitialized () {
|
|
208
|
-
if (this._state !== State.
|
|
208
|
+
if (this._state !== State.INITIALIZED) {
|
|
209
209
|
throw new Error('Database not initialized.');
|
|
210
210
|
}
|
|
211
211
|
}
|
package/src/api/item.ts
CHANGED
|
@@ -10,7 +10,7 @@ import { Model, StateManager } from '@dxos/model-factory';
|
|
|
10
10
|
import { ItemManager } from '../database';
|
|
11
11
|
import { Entity } from './entity';
|
|
12
12
|
import type { Link } from './link';
|
|
13
|
-
import { Selection,
|
|
13
|
+
import { Selection, createItemSelection } from './selection';
|
|
14
14
|
|
|
15
15
|
const log = debug('dxos:echo-db:item');
|
|
16
16
|
|
|
@@ -102,7 +102,7 @@ export class Item<M extends Model | null = Model> extends Entity<M> {
|
|
|
102
102
|
* Returns a selection context, which can be used to traverse the object graph starting from this item.
|
|
103
103
|
*/
|
|
104
104
|
select (): Selection<Item<any>> {
|
|
105
|
-
return
|
|
105
|
+
return createItemSelection(this as Item, this._itemManager.debouncedUpdate, undefined);
|
|
106
106
|
}
|
|
107
107
|
|
|
108
108
|
/**
|
package/src/api/result-set.ts
CHANGED
|
@@ -9,6 +9,7 @@ import { Event, ReadOnlyEvent } from '@dxos/async';
|
|
|
9
9
|
/**
|
|
10
10
|
* Reactive query results.
|
|
11
11
|
*/
|
|
12
|
+
// TODO(burdon): Replace with Selection?
|
|
12
13
|
export class ResultSet<T> {
|
|
13
14
|
private readonly _resultsUpdate = new Event<T[]>();
|
|
14
15
|
private readonly _itemUpdate: ReadOnlyEvent;
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import { ItemID } from '@dxos/echo-protocol';
|
|
6
|
+
|
|
7
|
+
import { Entity } from '../entity';
|
|
8
|
+
import { Item } from '../item';
|
|
9
|
+
import { Link } from '../link';
|
|
10
|
+
import { coerceToId, OneOrMultiple, testOneOrMultiple } from './util';
|
|
11
|
+
|
|
12
|
+
//
|
|
13
|
+
// Types
|
|
14
|
+
//
|
|
15
|
+
|
|
16
|
+
export type ItemIdFilter = {
|
|
17
|
+
id: ItemID
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export type ItemFilter = {
|
|
21
|
+
type?: OneOrMultiple<string>
|
|
22
|
+
parent?: ItemID | Item
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export type LinkFilter = {
|
|
26
|
+
type?: OneOrMultiple<string>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export type Predicate<T extends Entity> = (entity: T) => boolean;
|
|
30
|
+
|
|
31
|
+
export type RootFilter = ItemIdFilter | ItemFilter | Predicate<Item>
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Visitor callback.
|
|
35
|
+
* The visitor is passed the current entities and result (accumulator),
|
|
36
|
+
* which may be modified and returned.
|
|
37
|
+
*/
|
|
38
|
+
export type Callable<T extends Entity, R> = (entities: T[], result: R) => R
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Controls how deleted items are filtered.
|
|
42
|
+
*/
|
|
43
|
+
export enum ItemFilterDeleted {
|
|
44
|
+
/**
|
|
45
|
+
* Do not return deleted items. Default behaviour.
|
|
46
|
+
*/
|
|
47
|
+
HIDE_DELETED = 0,
|
|
48
|
+
/**
|
|
49
|
+
* Return deleted and regular items.
|
|
50
|
+
*/
|
|
51
|
+
SHOW_DELETED = 1,
|
|
52
|
+
/**
|
|
53
|
+
* Return only deleted items.
|
|
54
|
+
*/
|
|
55
|
+
SHOW_DELETED_ONLY = 2
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export type QueryOptions = {
|
|
59
|
+
/**
|
|
60
|
+
* Controls how deleted items are filtered.
|
|
61
|
+
*/
|
|
62
|
+
deleted?: ItemFilterDeleted
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
//
|
|
66
|
+
// Filters
|
|
67
|
+
//
|
|
68
|
+
|
|
69
|
+
export const filterToPredicate = (filter: ItemFilter | ItemIdFilter | Predicate<any>): Predicate<any> => {
|
|
70
|
+
if (typeof filter === 'function') {
|
|
71
|
+
return filter;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return itemFilterToPredicate(filter);
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
export const itemFilterToPredicate = (filter: ItemFilter | ItemIdFilter): Predicate<Item> => {
|
|
78
|
+
if ('id' in filter) {
|
|
79
|
+
return item => item.id === filter.id;
|
|
80
|
+
} else {
|
|
81
|
+
return item =>
|
|
82
|
+
(!filter.type || testOneOrMultiple(filter.type, item.type)) &&
|
|
83
|
+
(!filter.parent || item.parent?.id === coerceToId(filter.parent));
|
|
84
|
+
}
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export const linkFilterToPredicate = (filter: LinkFilter): Predicate<Link> => {
|
|
88
|
+
return link => (!filter.type || testOneOrMultiple(filter.type, link.type));
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
export const createQueryOptionsFilter = ({
|
|
92
|
+
deleted = ItemFilterDeleted.HIDE_DELETED
|
|
93
|
+
}: QueryOptions): Predicate<Entity> => {
|
|
94
|
+
return entity => {
|
|
95
|
+
if (entity.model === null) {
|
|
96
|
+
return false;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
switch (deleted) {
|
|
100
|
+
case ItemFilterDeleted.HIDE_DELETED:
|
|
101
|
+
return !(entity instanceof Item) || !entity.deleted;
|
|
102
|
+
case ItemFilterDeleted.SHOW_DELETED:
|
|
103
|
+
return true;
|
|
104
|
+
case ItemFilterDeleted.SHOW_DELETED_ONLY:
|
|
105
|
+
return entity instanceof Item && entity.deleted;
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
};
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
//
|
|
2
|
+
// Copyright 2020 DXOS.org
|
|
3
|
+
//
|
|
4
|
+
|
|
5
|
+
import assert from 'assert';
|
|
6
|
+
|
|
7
|
+
import { Event } from '@dxos/async';
|
|
8
|
+
|
|
9
|
+
import { Database } from '../database';
|
|
10
|
+
import { Entity } from '../entity';
|
|
11
|
+
import { dedupe } from './util';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Represents where the selection has started.
|
|
15
|
+
*/
|
|
16
|
+
export type SelectionRoot = Database | Entity
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Returned from each stage of the visitor.
|
|
20
|
+
*/
|
|
21
|
+
export type SelectionContext<T extends Entity, R> = [entities: T[], result?: R]
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Query subscription.
|
|
25
|
+
* Represents a live-query (subscription) that can notify about future updates to the relevant subset of items.
|
|
26
|
+
*/
|
|
27
|
+
export class SelectionResult<T extends Entity, R = any> {
|
|
28
|
+
/**
|
|
29
|
+
* Fired when there are updates in the selection.
|
|
30
|
+
* Only update that are relevant to the selection cause the update.
|
|
31
|
+
*/
|
|
32
|
+
readonly update = new Event<SelectionResult<T>>(); // TODO(burdon): Result result object.
|
|
33
|
+
|
|
34
|
+
private _lastResult: SelectionContext<T, R> = [[]];
|
|
35
|
+
|
|
36
|
+
constructor (
|
|
37
|
+
private readonly _execute: () => SelectionContext<T, R>,
|
|
38
|
+
private readonly _update: Event<Entity[]>,
|
|
39
|
+
private readonly _root: SelectionRoot,
|
|
40
|
+
private readonly _reducer: boolean
|
|
41
|
+
) {
|
|
42
|
+
this.refresh();
|
|
43
|
+
|
|
44
|
+
// Re-run if deps change.
|
|
45
|
+
this.update.addEffect(() => _update.on(currentEntities => {
|
|
46
|
+
const [previousEntities] = this._lastResult;
|
|
47
|
+
this.refresh();
|
|
48
|
+
|
|
49
|
+
// Filters mutation events only if selection (since we can't reason about deps of call methods).
|
|
50
|
+
const set = new Set([...previousEntities, ...this._lastResult![0]]);
|
|
51
|
+
if (this._reducer || currentEntities.some(entity => set.has(entity as any))) {
|
|
52
|
+
this.update.emit(this);
|
|
53
|
+
}
|
|
54
|
+
}));
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
toString () {
|
|
58
|
+
const [entities] = this._lastResult;
|
|
59
|
+
return `SelectionResult<${JSON.stringify({
|
|
60
|
+
entities: entities.length
|
|
61
|
+
})}>`;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Re-run query.
|
|
66
|
+
*/
|
|
67
|
+
refresh () {
|
|
68
|
+
const [entities, result] = this._execute();
|
|
69
|
+
this._lastResult = [dedupe(entities), result];
|
|
70
|
+
return this;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* The root of the selection. Either a database or an item. Must be a stable reference.
|
|
75
|
+
*/
|
|
76
|
+
get root (): SelectionRoot {
|
|
77
|
+
return this._root;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Get the result of this selection.
|
|
82
|
+
*/
|
|
83
|
+
get entities (): T[] {
|
|
84
|
+
if (!this._lastResult) {
|
|
85
|
+
this.refresh();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
const [entities] = this._lastResult!;
|
|
89
|
+
return entities;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
/**
|
|
93
|
+
* Returns the selection or reducer result.
|
|
94
|
+
*/
|
|
95
|
+
get value (): R extends void ? T[] : R {
|
|
96
|
+
if (!this._lastResult) {
|
|
97
|
+
this.refresh();
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const [entities, value] = this._lastResult!;
|
|
101
|
+
return (this._reducer ? value : entities) as any;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Return the first element if the set has exactly one element.
|
|
106
|
+
*/
|
|
107
|
+
expectOne (): T {
|
|
108
|
+
const entities = this.entities;
|
|
109
|
+
assert(entities.length === 1, `Expected one result; got ${entities.length}`);
|
|
110
|
+
return entities[0];
|
|
111
|
+
}
|
|
112
|
+
}
|
|
@@ -11,10 +11,11 @@ import { ItemID, ItemType } from '@dxos/echo-protocol';
|
|
|
11
11
|
import { ModelFactory } from '@dxos/model-factory';
|
|
12
12
|
import { ObjectModel } from '@dxos/object-model';
|
|
13
13
|
|
|
14
|
-
import { Entity } from '
|
|
15
|
-
import { Item } from '
|
|
16
|
-
import { Link } from '
|
|
17
|
-
import { RootFilter
|
|
14
|
+
import { Entity } from '../entity';
|
|
15
|
+
import { Item } from '../item';
|
|
16
|
+
import { Link } from '../link';
|
|
17
|
+
import { RootFilter } from './queries';
|
|
18
|
+
import { createSelection } from './selection';
|
|
18
19
|
|
|
19
20
|
// Use to prevent ultra-long diffs.
|
|
20
21
|
const ids = (entities: Entity[]) => entities.map(entity => entity.id);
|
|
@@ -23,8 +24,9 @@ const modelFactory = new ModelFactory().registerModel(ObjectModel);
|
|
|
23
24
|
|
|
24
25
|
const createModel = (id: ItemID) => modelFactory.createModel(ObjectModel.meta.type, id, {}, PublicKey.random());
|
|
25
26
|
|
|
26
|
-
const createItem = (id: ItemID, type: ItemType, parent?: Item<any>) =>
|
|
27
|
-
new Item(null as any, id, type, createModel(id), undefined, parent);
|
|
27
|
+
const createItem = (id: ItemID, type: ItemType, parent?: Item<any>) => {
|
|
28
|
+
return new Item(null as any, id, type, createModel(id), undefined, parent);
|
|
29
|
+
};
|
|
28
30
|
|
|
29
31
|
const createLink = (id: ItemID, type: ItemType, source: Item<any>, target: Item<any>) => {
|
|
30
32
|
const link = new Link(null as any, id, type, createModel(id), {
|
|
@@ -40,11 +42,11 @@ const createLink = (id: ItemID, type: ItemType, source: Item<any>, target: Item<
|
|
|
40
42
|
return link;
|
|
41
43
|
};
|
|
42
44
|
|
|
43
|
-
const
|
|
44
|
-
|
|
45
|
+
const createRootSelection = (filter?: RootFilter) =>
|
|
46
|
+
createSelection<void>(() => items, () => new Event(), null as any, filter, undefined);
|
|
45
47
|
|
|
46
48
|
const createReducer = <R>(result: R) =>
|
|
47
|
-
|
|
49
|
+
createSelection<R>(() => items, () => new Event(), null as any, undefined, result);
|
|
48
50
|
|
|
49
51
|
// TODO(burdon): Use more complex data set (org, person, project, task).
|
|
50
52
|
|
|
@@ -90,34 +92,34 @@ describe('Selection', () => {
|
|
|
90
92
|
describe('root', () => {
|
|
91
93
|
test('all', () => {
|
|
92
94
|
expect(
|
|
93
|
-
|
|
94
|
-
.
|
|
95
|
+
createRootSelection()
|
|
96
|
+
.exec().entities
|
|
95
97
|
).toHaveLength(items.length);
|
|
96
98
|
});
|
|
97
99
|
|
|
98
100
|
test('by id', () => {
|
|
99
101
|
expect(
|
|
100
|
-
|
|
101
|
-
.
|
|
102
|
+
createRootSelection({ id: org1.id })
|
|
103
|
+
.exec().entities
|
|
102
104
|
).toEqual([org1]);
|
|
103
105
|
|
|
104
106
|
expect(
|
|
105
|
-
|
|
106
|
-
.
|
|
107
|
+
createRootSelection({ id: org2.id })
|
|
108
|
+
.exec().entities
|
|
107
109
|
).toEqual([org2]);
|
|
108
110
|
});
|
|
109
111
|
|
|
110
112
|
test('single type', () => {
|
|
111
113
|
expect(
|
|
112
|
-
|
|
113
|
-
.
|
|
114
|
+
createRootSelection({ type: ITEM_PROJECT })
|
|
115
|
+
.exec().entities
|
|
114
116
|
).toHaveLength(3);
|
|
115
117
|
});
|
|
116
118
|
|
|
117
119
|
test('multiple types', () => {
|
|
118
120
|
expect(
|
|
119
|
-
|
|
120
|
-
.
|
|
121
|
+
createRootSelection({ type: [ITEM_ORG, ITEM_PROJECT] })
|
|
122
|
+
.exec().entities
|
|
121
123
|
).toHaveLength(5);
|
|
122
124
|
});
|
|
123
125
|
});
|
|
@@ -125,33 +127,33 @@ describe('Selection', () => {
|
|
|
125
127
|
describe('filter', () => {
|
|
126
128
|
test('invalid', () => {
|
|
127
129
|
expect(
|
|
128
|
-
|
|
129
|
-
.filter({ type: 'dxos:type
|
|
130
|
-
.
|
|
130
|
+
createRootSelection()
|
|
131
|
+
.filter({ type: 'dxos:type/invalid' })
|
|
132
|
+
.exec().entities
|
|
131
133
|
).toHaveLength(0);
|
|
132
134
|
});
|
|
133
135
|
|
|
134
136
|
test('single type', () => {
|
|
135
137
|
expect(
|
|
136
|
-
|
|
138
|
+
createRootSelection()
|
|
137
139
|
.filter({ type: ITEM_PROJECT })
|
|
138
|
-
.
|
|
140
|
+
.exec().entities
|
|
139
141
|
).toHaveLength(3);
|
|
140
142
|
});
|
|
141
143
|
|
|
142
144
|
test('multiple types', () => {
|
|
143
145
|
expect(
|
|
144
|
-
|
|
146
|
+
createRootSelection()
|
|
145
147
|
.filter({ type: [ITEM_ORG, ITEM_PROJECT] })
|
|
146
|
-
.
|
|
148
|
+
.exec().entities
|
|
147
149
|
).toHaveLength(5);
|
|
148
150
|
});
|
|
149
151
|
|
|
150
152
|
test('by function', () => {
|
|
151
153
|
expect(
|
|
152
|
-
|
|
154
|
+
createRootSelection()
|
|
153
155
|
.filter(item => item.type === ITEM_ORG)
|
|
154
|
-
.
|
|
156
|
+
.exec().entities
|
|
155
157
|
).toHaveLength(2);
|
|
156
158
|
});
|
|
157
159
|
});
|
|
@@ -159,10 +161,10 @@ describe('Selection', () => {
|
|
|
159
161
|
describe('children', () => {
|
|
160
162
|
test('from multiple items', () => {
|
|
161
163
|
expect(ids(
|
|
162
|
-
|
|
164
|
+
createRootSelection()
|
|
163
165
|
.filter({ type: ITEM_ORG })
|
|
164
166
|
.children({ type: ITEM_PROJECT })
|
|
165
|
-
.
|
|
167
|
+
.exec().entities
|
|
166
168
|
)).toStrictEqual(ids([
|
|
167
169
|
project1,
|
|
168
170
|
project2,
|
|
@@ -172,9 +174,9 @@ describe('Selection', () => {
|
|
|
172
174
|
|
|
173
175
|
test('from single item', () => {
|
|
174
176
|
expect(ids(
|
|
175
|
-
|
|
177
|
+
createRootSelection({ id: org1.id })
|
|
176
178
|
.children()
|
|
177
|
-
.
|
|
179
|
+
.exec().entities
|
|
178
180
|
)).toStrictEqual(ids([
|
|
179
181
|
project1,
|
|
180
182
|
project2,
|
|
@@ -187,10 +189,10 @@ describe('Selection', () => {
|
|
|
187
189
|
describe('parent', () => {
|
|
188
190
|
test('from multiple items', () => {
|
|
189
191
|
expect(ids(
|
|
190
|
-
|
|
192
|
+
createRootSelection()
|
|
191
193
|
.filter({ type: ITEM_PROJECT })
|
|
192
194
|
.parent()
|
|
193
|
-
.
|
|
195
|
+
.exec().entities
|
|
194
196
|
)).toStrictEqual(ids([
|
|
195
197
|
org1,
|
|
196
198
|
org2
|
|
@@ -199,9 +201,9 @@ describe('Selection', () => {
|
|
|
199
201
|
|
|
200
202
|
test('from single item', () => {
|
|
201
203
|
expect(ids(
|
|
202
|
-
|
|
204
|
+
createRootSelection({ id: project1.id })
|
|
203
205
|
.parent()
|
|
204
|
-
.
|
|
206
|
+
.exec().entities
|
|
205
207
|
)).toStrictEqual(ids([
|
|
206
208
|
org1
|
|
207
209
|
]));
|
|
@@ -209,9 +211,9 @@ describe('Selection', () => {
|
|
|
209
211
|
|
|
210
212
|
test('is empty', () => {
|
|
211
213
|
expect(
|
|
212
|
-
|
|
214
|
+
createRootSelection({ id: org1.id })
|
|
213
215
|
.parent()
|
|
214
|
-
.
|
|
216
|
+
.exec().entities
|
|
215
217
|
).toEqual([]);
|
|
216
218
|
});
|
|
217
219
|
});
|
|
@@ -219,10 +221,10 @@ describe('Selection', () => {
|
|
|
219
221
|
describe('links', () => {
|
|
220
222
|
test('links from single item', () => {
|
|
221
223
|
expect(ids(
|
|
222
|
-
|
|
224
|
+
createRootSelection({ id: project1.id })
|
|
223
225
|
.links()
|
|
224
226
|
.target()
|
|
225
|
-
.
|
|
227
|
+
.exec().entities
|
|
226
228
|
)).toStrictEqual(ids([
|
|
227
229
|
person1,
|
|
228
230
|
person2
|
|
@@ -231,18 +233,18 @@ describe('Selection', () => {
|
|
|
231
233
|
|
|
232
234
|
test('links from multiple items', () => {
|
|
233
235
|
expect(
|
|
234
|
-
|
|
236
|
+
createRootSelection({ type: ITEM_PROJECT })
|
|
235
237
|
.links()
|
|
236
|
-
.
|
|
238
|
+
.exec().entities
|
|
237
239
|
).toHaveLength(links.length);
|
|
238
240
|
});
|
|
239
241
|
|
|
240
242
|
test('sources', () => {
|
|
241
243
|
expect(ids(
|
|
242
|
-
|
|
244
|
+
createRootSelection({ type: ITEM_PERSON })
|
|
243
245
|
.refs()
|
|
244
246
|
.source()
|
|
245
|
-
.
|
|
247
|
+
.exec().entities
|
|
246
248
|
)).toStrictEqual(ids([
|
|
247
249
|
project1,
|
|
248
250
|
project2
|
|
@@ -252,7 +254,7 @@ describe('Selection', () => {
|
|
|
252
254
|
|
|
253
255
|
describe('reducer', () => {
|
|
254
256
|
test('simple reducer', () => {
|
|
255
|
-
const query = createReducer(0).call((items, count) => count + items.length).
|
|
257
|
+
const query = createReducer(0).call((items, count) => count + items.length).exec();
|
|
256
258
|
expect(query.value).toEqual(items.length);
|
|
257
259
|
});
|
|
258
260
|
|
|
@@ -272,7 +274,7 @@ describe('Selection', () => {
|
|
|
272
274
|
return { ...rest, numLinks: numLinks + links.length, stage: 'c' };
|
|
273
275
|
})
|
|
274
276
|
.target()
|
|
275
|
-
.
|
|
277
|
+
.exec();
|
|
276
278
|
|
|
277
279
|
expect(query.value).toEqual({ numItems: 5, numLinks: 4, stage: 'c' });
|
|
278
280
|
});
|
|
@@ -282,9 +284,9 @@ describe('Selection', () => {
|
|
|
282
284
|
test('events get filtered correctly', async () => {
|
|
283
285
|
const update = new Event<Entity[]>();
|
|
284
286
|
|
|
285
|
-
const query =
|
|
287
|
+
const query = createSelection<void>(() => items, () => update, null as any, { type: ITEM_ORG }, undefined)
|
|
286
288
|
.children()
|
|
287
|
-
.
|
|
289
|
+
.exec();
|
|
288
290
|
|
|
289
291
|
{
|
|
290
292
|
const promise = query.update.waitForCount(1);
|