@travetto/model-firestore 8.0.0-alpha.2 → 8.0.0-alpha.21
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/README.md +1 -1
- package/package.json +13 -7
- package/src/service.ts +173 -49
package/README.md
CHANGED
|
@@ -17,7 +17,7 @@ This module provides an [Firestore](https://firebase.google.com/docs/firestore)-
|
|
|
17
17
|
|
|
18
18
|
Supported features:
|
|
19
19
|
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L11)
|
|
20
|
-
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/types/
|
|
20
|
+
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L16)
|
|
21
21
|
|
|
22
22
|
Out of the box, by installing the module, everything should be wired up by default.If you need to customize any aspect of the source or config, you can override and register it with the [Dependency Injection](https://github.com/travetto/travetto/tree/main/module/di#readme "Dependency registration/management and injection support.") module.
|
|
23
23
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@travetto/model-firestore",
|
|
3
|
-
"version": "8.0.0-alpha.
|
|
3
|
+
"version": "8.0.0-alpha.21",
|
|
4
4
|
"description": "Firestore backing for the travetto model module.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"typescript",
|
|
@@ -25,13 +25,14 @@
|
|
|
25
25
|
"directory": "module/model-firestore"
|
|
26
26
|
},
|
|
27
27
|
"dependencies": {
|
|
28
|
-
"@google-cloud/firestore": "^8.
|
|
29
|
-
"@travetto/config": "^8.0.0-alpha.
|
|
30
|
-
"@travetto/model": "^8.0.0-alpha.
|
|
31
|
-
"@travetto/
|
|
28
|
+
"@google-cloud/firestore": "^8.6.0",
|
|
29
|
+
"@travetto/config": "^8.0.0-alpha.18",
|
|
30
|
+
"@travetto/model": "^8.0.0-alpha.19",
|
|
31
|
+
"@travetto/model-indexed": "^8.0.0-alpha.21",
|
|
32
|
+
"@travetto/runtime": "^8.0.0-alpha.17"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
35
|
+
"@travetto/cli": "^8.0.0-alpha.24"
|
|
35
36
|
},
|
|
36
37
|
"peerDependenciesMeta": {
|
|
37
38
|
"@travetto/cli": {
|
|
@@ -39,7 +40,12 @@
|
|
|
39
40
|
}
|
|
40
41
|
},
|
|
41
42
|
"travetto": {
|
|
42
|
-
"displayName": "Firestore Model Support"
|
|
43
|
+
"displayName": "Firestore Model Support",
|
|
44
|
+
"build": {
|
|
45
|
+
"externalDependencies": [
|
|
46
|
+
"google-gax"
|
|
47
|
+
]
|
|
48
|
+
}
|
|
43
49
|
},
|
|
44
50
|
"private": false,
|
|
45
51
|
"publishConfig": {
|
package/src/service.ts
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
1
|
import { type DocumentData, FieldValue, Firestore, type Query } from '@google-cloud/firestore';
|
|
2
2
|
|
|
3
|
-
import { JSONUtil, ShutdownManager, type Class,
|
|
3
|
+
import { castTo, JSONUtil, ShutdownManager, type Class, RuntimeError } from '@travetto/runtime';
|
|
4
4
|
import { Injectable, PostConstruct } from '@travetto/di';
|
|
5
5
|
import {
|
|
6
|
-
type ModelCrudSupport, ModelRegistryIndex, type ModelStorageSupport,
|
|
7
|
-
type
|
|
8
|
-
ModelCrudUtil, ModelIndexedUtil,
|
|
6
|
+
type ModelCrudSupport, ModelRegistryIndex, type ModelStorageSupport, type ModelType, NotFoundError, type OptionalId, ModelCrudUtil,
|
|
7
|
+
type ModelListOptions,
|
|
9
8
|
} from '@travetto/model';
|
|
9
|
+
import {
|
|
10
|
+
type ModelIndexedSupport, type KeyedIndexSelection, type KeyedIndexBody, type ModelPageOptions, ModelIndexedUtil,
|
|
11
|
+
type SingleItemIndex, type SortedIndexSelection, type ModelPageResult, type SortedIndex, type FullKeyedIndexBody,
|
|
12
|
+
type FullKeyedIndexWithPartialBody, ModelIndexedComputedIndex, warnIfIndexedUniqueIndex, warnIfNonIndexedIndex,
|
|
13
|
+
type ModelIndexedSearchOptions, type SortedIndexSelectionType
|
|
14
|
+
} from '@travetto/model-indexed';
|
|
10
15
|
|
|
11
16
|
import type { FirestoreModelConfig } from './config.ts';
|
|
12
17
|
|
|
@@ -38,6 +43,72 @@ export class FirestoreModelService implements ModelCrudSupport, ModelStorageSupp
|
|
|
38
43
|
return this.client.collection(this.#resolveTable(cls));
|
|
39
44
|
}
|
|
40
45
|
|
|
46
|
+
async #getIdByIndex<
|
|
47
|
+
T extends ModelType,
|
|
48
|
+
K extends KeyedIndexSelection<T>,
|
|
49
|
+
S extends SortedIndexSelection<T>
|
|
50
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<string> {
|
|
51
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
52
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
53
|
+
const query = [...computed.allParts, ...(computed.idPart ? [computed.idPart] : [])].reduce<Query>(
|
|
54
|
+
(result, { path, value }) => result.where(path.join('.'), '==', value),
|
|
55
|
+
this.#getCollection(cls)
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const item = await query.get();
|
|
59
|
+
if (!item || item.empty) {
|
|
60
|
+
throw new NotFoundError(`${cls.name} Index=${idx}`, computed.getKey());
|
|
61
|
+
}
|
|
62
|
+
if (item.size > 1) {
|
|
63
|
+
throw new RuntimeError(`Multiple items found for ${cls.name} Index=${idx}`);
|
|
64
|
+
}
|
|
65
|
+
return item.docs[0].data().id;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
#buildIndexQuery<T extends ModelType>(cls: Class<T>, idx: SortedIndex<T>, body: KeyedIndexBody<T>): Query {
|
|
69
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
70
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate();
|
|
71
|
+
|
|
72
|
+
let query = computed.keyedParts.reduce<Query>((result, { path, value, state }) =>
|
|
73
|
+
result.where(path.join('.'), '==', (state === 'empty' ? null : value)), this.#getCollection(cls));
|
|
74
|
+
|
|
75
|
+
for (const { path, value } of idx.sortTemplate) {
|
|
76
|
+
query = query.orderBy(path.join('.'), value === 1 ? 'asc' : 'desc');
|
|
77
|
+
}
|
|
78
|
+
return query;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
async * #scanCollection<T extends ModelType>(
|
|
82
|
+
cls: Class<T>,
|
|
83
|
+
queryBuilder: () => Query,
|
|
84
|
+
options?: ModelListOptions & ModelPageOptions<number>
|
|
85
|
+
): AsyncIterable<{ items: T[], nextOffset?: number }> {
|
|
86
|
+
const limit = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
87
|
+
const batchSize = Math.min(options?.batchSizeHint ?? 100, limit);
|
|
88
|
+
|
|
89
|
+
let offset = options?.offset ?? 0;
|
|
90
|
+
let produced = 0;
|
|
91
|
+
|
|
92
|
+
while (!(options?.abort?.aborted) && produced < limit) {
|
|
93
|
+
const query = queryBuilder().limit(batchSize).offset(offset);
|
|
94
|
+
|
|
95
|
+
const { docs, size } = await query.get();
|
|
96
|
+
if (size === 0) {
|
|
97
|
+
break;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const remaining = (produced + size > limit) ? docs.slice(0, limit - produced) : docs;
|
|
101
|
+
|
|
102
|
+
offset += size;
|
|
103
|
+
|
|
104
|
+
const items = await ModelCrudUtil.filterOutNotFound(
|
|
105
|
+
remaining.map(item => ModelCrudUtil.load(cls, item.data()!)));
|
|
106
|
+
produced += items.length;
|
|
107
|
+
|
|
108
|
+
yield { items, nextOffset: offset };
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
41
112
|
@PostConstruct()
|
|
42
113
|
async initializeClient(): Promise<void> {
|
|
43
114
|
this.client = new Firestore({ ...this.config, useBigInt: true });
|
|
@@ -45,13 +116,16 @@ export class FirestoreModelService implements ModelCrudSupport, ModelStorageSupp
|
|
|
45
116
|
}
|
|
46
117
|
|
|
47
118
|
// Storage
|
|
48
|
-
async createStorage(): Promise<void> {
|
|
119
|
+
async createStorage(): Promise<void> {
|
|
120
|
+
for (const cls of ModelRegistryIndex.getClasses()) {
|
|
121
|
+
warnIfIndexedUniqueIndex(this, cls, ModelRegistryIndex.getIndices(cls));
|
|
122
|
+
warnIfNonIndexedIndex(this, cls, ModelRegistryIndex.getIndices(cls));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
49
125
|
async deleteStorage(): Promise<void> { }
|
|
50
126
|
|
|
51
|
-
async deleteModel(cls: Class): Promise<void> {
|
|
52
|
-
|
|
53
|
-
await this.delete(cls, item.id);
|
|
54
|
-
}
|
|
127
|
+
async deleteModel<T extends ModelType>(cls: Class<T>): Promise<void> {
|
|
128
|
+
await this.client.recursiveDelete(this.#getCollection(cls));
|
|
55
129
|
}
|
|
56
130
|
|
|
57
131
|
// Crud
|
|
@@ -105,63 +179,113 @@ export class FirestoreModelService implements ModelCrudSupport, ModelStorageSupp
|
|
|
105
179
|
}
|
|
106
180
|
}
|
|
107
181
|
|
|
108
|
-
async * list<T extends ModelType>(cls: Class<T
|
|
109
|
-
const
|
|
110
|
-
|
|
111
|
-
try {
|
|
112
|
-
yield await this.get(cls, item.id);
|
|
113
|
-
} catch (error) {
|
|
114
|
-
if (!(error instanceof NotFoundError)) {
|
|
115
|
-
throw error;
|
|
116
|
-
}
|
|
117
|
-
}
|
|
182
|
+
async * list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
183
|
+
for await (const { items } of this.#scanCollection(cls, () => this.#getCollection(cls), options)) {
|
|
184
|
+
yield items;
|
|
118
185
|
}
|
|
119
186
|
}
|
|
120
187
|
|
|
121
|
-
// Indexed
|
|
122
|
-
async #getIdByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<string> {
|
|
123
|
-
ModelCrudUtil.ensureNotSubType(cls);
|
|
124
|
-
|
|
125
|
-
const { fields } = ModelIndexedUtil.computeIndexParts(cls, idx, body);
|
|
126
|
-
const query = fields.reduce<Query>(
|
|
127
|
-
(result, { path, value }) => result.where(path.join('.'), '==', value),
|
|
128
|
-
this.#getCollection(cls)
|
|
129
|
-
);
|
|
130
|
-
|
|
131
|
-
const item = await query.get();
|
|
132
|
-
|
|
133
|
-
if (item && !item.empty) {
|
|
134
|
-
return item.docs[0].id;
|
|
135
|
-
}
|
|
136
|
-
throw new NotFoundError(`${cls.name} Index=${idx}`, ModelIndexedUtil.computeIndexKey(cls, idx, body, { separator: '; ' })?.key);
|
|
137
|
-
}
|
|
188
|
+
// Indexed contract
|
|
138
189
|
|
|
139
|
-
async getByIndex<
|
|
190
|
+
async getByIndex<
|
|
191
|
+
T extends ModelType,
|
|
192
|
+
K extends KeyedIndexSelection<T>,
|
|
193
|
+
S extends SortedIndexSelection<T>
|
|
194
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<T> {
|
|
140
195
|
return this.get(cls, await this.#getIdByIndex(cls, idx, body));
|
|
141
196
|
}
|
|
142
197
|
|
|
143
|
-
async deleteByIndex<
|
|
198
|
+
async deleteByIndex<
|
|
199
|
+
T extends ModelType,
|
|
200
|
+
K extends KeyedIndexSelection<T>,
|
|
201
|
+
S extends SortedIndexSelection<T>
|
|
202
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexBody<T, K, S>): Promise<void> {
|
|
144
203
|
return this.delete(cls, await this.#getIdByIndex(cls, idx, body));
|
|
145
204
|
}
|
|
146
205
|
|
|
147
|
-
upsertByIndex<
|
|
206
|
+
upsertByIndex<
|
|
207
|
+
T extends ModelType,
|
|
208
|
+
K extends KeyedIndexSelection<T>,
|
|
209
|
+
S extends SortedIndexSelection<T>
|
|
210
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: OptionalId<T>): Promise<T> {
|
|
148
211
|
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
149
212
|
}
|
|
150
213
|
|
|
151
|
-
|
|
152
|
-
|
|
214
|
+
updateByIndex<
|
|
215
|
+
T extends ModelType,
|
|
216
|
+
K extends KeyedIndexSelection<T>,
|
|
217
|
+
S extends SortedIndexSelection<T>
|
|
218
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: T): Promise<T> {
|
|
219
|
+
return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
|
|
220
|
+
}
|
|
153
221
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
222
|
+
async updatePartialByIndex<
|
|
223
|
+
T extends ModelType,
|
|
224
|
+
K extends KeyedIndexSelection<T>,
|
|
225
|
+
S extends SortedIndexSelection<T>
|
|
226
|
+
>(cls: Class<T>, idx: SingleItemIndex<T, K, S>, body: FullKeyedIndexWithPartialBody<T, K, S>): Promise<T> {
|
|
227
|
+
const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
|
|
228
|
+
return this.update(cls, item);
|
|
229
|
+
}
|
|
158
230
|
|
|
159
|
-
|
|
160
|
-
|
|
231
|
+
async pageByIndex<
|
|
232
|
+
T extends ModelType,
|
|
233
|
+
K extends KeyedIndexSelection<T>,
|
|
234
|
+
S extends SortedIndexSelection<T>
|
|
235
|
+
>(
|
|
236
|
+
cls: Class<T>,
|
|
237
|
+
idx: SortedIndex<T, K, S>,
|
|
238
|
+
body: KeyedIndexBody<T, K>,
|
|
239
|
+
options?: ModelPageOptions,
|
|
240
|
+
): Promise<ModelPageResult<T>> {
|
|
241
|
+
const items: T[] = [];
|
|
242
|
+
let nextOffset: number | undefined;
|
|
243
|
+
for await (const batch of this.#scanCollection(cls, () => this.#buildIndexQuery(cls, idx, body), {
|
|
244
|
+
limit: 100,
|
|
245
|
+
...options,
|
|
246
|
+
offset: options?.offset ? JSONUtil.fromBase64<number>(options.offset) : 0
|
|
247
|
+
})) {
|
|
248
|
+
items.push(...batch.items);
|
|
249
|
+
nextOffset = batch.nextOffset;
|
|
161
250
|
}
|
|
162
251
|
|
|
163
|
-
|
|
164
|
-
|
|
252
|
+
return { items, nextOffset: nextOffset ? JSONUtil.toBase64(nextOffset) : undefined };
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async * listByIndex<
|
|
256
|
+
T extends ModelType,
|
|
257
|
+
K extends KeyedIndexSelection<T>,
|
|
258
|
+
S extends SortedIndexSelection<T>
|
|
259
|
+
>(
|
|
260
|
+
cls: Class<T>,
|
|
261
|
+
idx: SortedIndex<T, K, S>,
|
|
262
|
+
body: KeyedIndexBody<T, K>,
|
|
263
|
+
options?: ModelListOptions
|
|
264
|
+
): AsyncIterable<T[]> {
|
|
265
|
+
for await (const { items } of this.#scanCollection(cls, () => this.#buildIndexQuery(cls, idx, body), options)) {
|
|
266
|
+
yield items;
|
|
165
267
|
}
|
|
166
268
|
}
|
|
269
|
+
|
|
270
|
+
async suggestByIndex<
|
|
271
|
+
T extends ModelType,
|
|
272
|
+
S extends SortedIndexSelection<T>,
|
|
273
|
+
K extends KeyedIndexSelection<T>,
|
|
274
|
+
B extends SortedIndexSelectionType<T, S> & string
|
|
275
|
+
>(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, prefix: B, options?: ModelIndexedSearchOptions): Promise<T[]> {
|
|
276
|
+
const results: T[] = [];
|
|
277
|
+
|
|
278
|
+
const field = idx.sortTemplate[0].path.join('.');
|
|
279
|
+
|
|
280
|
+
for await (const { items } of this.#scanCollection(cls,
|
|
281
|
+
() => this.#buildIndexQuery(cls, idx, body)
|
|
282
|
+
.where(field, '>=', prefix)
|
|
283
|
+
.where(field, '<', `${prefix}\uf8ff`),
|
|
284
|
+
{ limit: 10, ...options })
|
|
285
|
+
) {
|
|
286
|
+
results.push(...items);
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
return results;
|
|
290
|
+
}
|
|
167
291
|
}
|