@dxos/echo-db 2.29.1 → 2.29.2-dev.f64f2a6f
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 +20 -23
- 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/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 +27 -27
- package/src/api/database.test.ts +13 -13
- package/src/api/database.ts +28 -29
- 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/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.test.ts
CHANGED
|
@@ -80,7 +80,7 @@ describe('Database', () => {
|
|
|
80
80
|
expect(item.id).not.toBeUndefined();
|
|
81
81
|
expect(item.model).toBeInstanceOf(ObjectModel);
|
|
82
82
|
|
|
83
|
-
const result = database.select().
|
|
83
|
+
const result = database.select().exec();
|
|
84
84
|
expect(result.expectOne()).toBeTruthy();
|
|
85
85
|
});
|
|
86
86
|
|
|
@@ -111,7 +111,7 @@ describe('Database', () => {
|
|
|
111
111
|
const parent = await database.createItem({ model: ObjectModel });
|
|
112
112
|
const child = await database.createItem({ model: ObjectModel, parent: parent.id });
|
|
113
113
|
|
|
114
|
-
const result = database.select().
|
|
114
|
+
const result = database.select().exec();
|
|
115
115
|
expect(result.entities).toHaveLength(2);
|
|
116
116
|
expect(result.entities).toEqual([parent, child]);
|
|
117
117
|
|
|
@@ -203,14 +203,14 @@ describe('Database', () => {
|
|
|
203
203
|
const database = await setupBackend(modelFactory);
|
|
204
204
|
|
|
205
205
|
{
|
|
206
|
-
const waiting = database.waitForItem({ type: 'example:type
|
|
207
|
-
const item = await database.createItem({ model: ObjectModel, type: 'example:type
|
|
206
|
+
const waiting = database.waitForItem({ type: 'example:type/test-1' });
|
|
207
|
+
const item = await database.createItem({ model: ObjectModel, type: 'example:type/test-1' });
|
|
208
208
|
expect(await promiseTimeout(waiting, 100, new Error('timeout'))).toEqual(item);
|
|
209
209
|
}
|
|
210
210
|
|
|
211
211
|
{
|
|
212
|
-
const item = await database.createItem({ model: ObjectModel, type: 'example:type
|
|
213
|
-
const waiting = database.waitForItem({ type: 'example:type
|
|
212
|
+
const item = await database.createItem({ model: ObjectModel, type: 'example:type/test-2' });
|
|
213
|
+
const waiting = database.waitForItem({ type: 'example:type/test-2' });
|
|
214
214
|
expect(await promiseTimeout(waiting, 100, new Error('timeout'))).toEqual(item);
|
|
215
215
|
}
|
|
216
216
|
});
|
|
@@ -223,7 +223,7 @@ describe('Database', () => {
|
|
|
223
223
|
database.createItem({ model: TestListModel })
|
|
224
224
|
));
|
|
225
225
|
|
|
226
|
-
const result = database.select().
|
|
226
|
+
const result = database.select().exec();
|
|
227
227
|
const items = result.entities;
|
|
228
228
|
expect(items).toHaveLength(10);
|
|
229
229
|
|
|
@@ -232,17 +232,17 @@ describe('Database', () => {
|
|
|
232
232
|
await update;
|
|
233
233
|
|
|
234
234
|
{
|
|
235
|
-
const result = database.select().
|
|
235
|
+
const result = database.select().exec();
|
|
236
236
|
expect(result.entities).toHaveLength(9);
|
|
237
237
|
}
|
|
238
238
|
|
|
239
239
|
{
|
|
240
|
-
const result = database.select().
|
|
240
|
+
const result = database.select().exec({ deleted: ItemFilterDeleted.SHOW_DELETED });
|
|
241
241
|
expect(result.entities).toHaveLength(10);
|
|
242
242
|
}
|
|
243
243
|
|
|
244
244
|
{
|
|
245
|
-
const result = database.select().
|
|
245
|
+
const result = database.select().exec({ deleted: ItemFilterDeleted.SHOW_DELETED_ONLY });
|
|
246
246
|
expect(result.entities).toHaveLength(1);
|
|
247
247
|
}
|
|
248
248
|
});
|
|
@@ -255,7 +255,7 @@ describe('Database', () => {
|
|
|
255
255
|
const item2 = await database.createItem({ model: ObjectModel });
|
|
256
256
|
|
|
257
257
|
// 1. Create a query
|
|
258
|
-
const query = database.select().
|
|
258
|
+
const query = database.select().exec();
|
|
259
259
|
const update = query.update.waitForCount(1);
|
|
260
260
|
|
|
261
261
|
// 2. Create a link
|
|
@@ -277,7 +277,7 @@ describe('Database', () => {
|
|
|
277
277
|
|
|
278
278
|
const parentItem = await database.createItem({ model: ObjectModel });
|
|
279
279
|
|
|
280
|
-
const query = database.select({ id: parentItem.id }).
|
|
280
|
+
const query = database.select({ id: parentItem.id }).exec();
|
|
281
281
|
const update = query.update.waitForCount(1);
|
|
282
282
|
|
|
283
283
|
const childItem = await database.createItem({
|
|
@@ -295,7 +295,7 @@ describe('Database', () => {
|
|
|
295
295
|
const database = await setupBackend(modelFactory);
|
|
296
296
|
|
|
297
297
|
await Promise.all(Array.from({ length: 8 }).map(() => database.createItem({ model: ObjectModel })));
|
|
298
|
-
const { value } = database.reduce(0).call((items) => items.length).
|
|
298
|
+
const { value } = database.reduce(0).call((items) => items.length).exec();
|
|
299
299
|
expect(value).toBe(8);
|
|
300
300
|
});
|
|
301
301
|
});
|
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
|
/**
|
|
@@ -159,7 +158,7 @@ export class Database {
|
|
|
159
158
|
* Waits for item matching the filter to be present and returns it.
|
|
160
159
|
*/
|
|
161
160
|
async waitForItem<T extends Model<any>> (filter: RootFilter): Promise<Item<T>> {
|
|
162
|
-
const result = this.select(filter).
|
|
161
|
+
const result = this.select(filter).exec();
|
|
163
162
|
await result.update.waitForCondition(() => result.entities.length > 0);
|
|
164
163
|
const item = result.expectOne();
|
|
165
164
|
assert(item, 'Possible condition detected.');
|
|
@@ -171,7 +170,7 @@ export class Database {
|
|
|
171
170
|
* @param filter
|
|
172
171
|
*/
|
|
173
172
|
select (filter?: RootFilter): Selection<Item<any>> {
|
|
174
|
-
return
|
|
173
|
+
return createSelection<void>(
|
|
175
174
|
() => this._itemManager.items,
|
|
176
175
|
() => this._itemManager.debouncedUpdate,
|
|
177
176
|
this,
|
|
@@ -186,7 +185,7 @@ export class Database {
|
|
|
186
185
|
* @param filter
|
|
187
186
|
*/
|
|
188
187
|
reduce<R> (result: R, filter?: RootFilter): Selection<Item<any>, R> {
|
|
189
|
-
return
|
|
188
|
+
return createSelection<R>(
|
|
190
189
|
() => this._itemManager.items,
|
|
191
190
|
() => this._itemManager.debouncedUpdate,
|
|
192
191
|
this,
|
|
@@ -205,7 +204,7 @@ export class Database {
|
|
|
205
204
|
}
|
|
206
205
|
|
|
207
206
|
private _assertInitialized () {
|
|
208
|
-
if (this._state !== State.
|
|
207
|
+
if (this._state !== State.INITIALIZED) {
|
|
209
208
|
throw new Error('Database not initialized.');
|
|
210
209
|
}
|
|
211
210
|
}
|
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
|
+
}
|