@travetto/model-firestore 8.0.0-alpha.3 → 8.0.0-alpha.31
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 +6 -6
- package/__index__.ts +1 -1
- package/package.json +13 -7
- package/src/config.ts +6 -7
- package/src/service.ts +200 -59
- package/support/cli.firestore_indexes.ts +120 -0
- package/support/service.firestore.ts +1 -1
package/README.md
CHANGED
|
@@ -13,11 +13,11 @@ npm install @travetto/model-firestore
|
|
|
13
13
|
yarn add @travetto/model-firestore
|
|
14
14
|
```
|
|
15
15
|
|
|
16
|
-
This module provides an [Firestore](https://firebase.google.com/docs/firestore)-based implementation of the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.").
|
|
16
|
+
This module provides an [Firestore](https://firebase.google.com/docs/firestore)-based implementation of the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations."). This source allows the [Data Modeling Support](https://github.com/travetto/travetto/tree/main/module/model#readme "Datastore abstraction for core operations.") module to read, write and query against [Firestore](https://firebase.google.com/docs/firestore).
|
|
17
17
|
|
|
18
18
|
Supported features:
|
|
19
|
-
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#
|
|
20
|
-
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/types/
|
|
19
|
+
* [CRUD](https://github.com/travetto/travetto/tree/main/module/model/src/types/crud.ts#L10)
|
|
20
|
+
* [Indexed](https://github.com/travetto/travetto/tree/main/module/model-indexed/src/types/service.ts#L21)
|
|
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
|
|
|
@@ -42,7 +42,7 @@ where the [FirestoreModelConfig](https://github.com/travetto/travetto/tree/main/
|
|
|
42
42
|
```typescript
|
|
43
43
|
@Config('model.firestore')
|
|
44
44
|
export class FirestoreModelConfig {
|
|
45
|
-
|
|
45
|
+
databaseId?: string;
|
|
46
46
|
credentialsFile?: string;
|
|
47
47
|
emulator?: string;
|
|
48
48
|
projectId?: string;
|
|
@@ -52,8 +52,8 @@ export class FirestoreModelConfig {
|
|
|
52
52
|
|
|
53
53
|
@PostConstruct()
|
|
54
54
|
async finalizeConfig(): Promise<void> {
|
|
55
|
-
if (!this.
|
|
56
|
-
this.projectId
|
|
55
|
+
if (!this.projectId && !Runtime.production) {
|
|
56
|
+
this.projectId = 'trv-local-dev';
|
|
57
57
|
this.emulator ??= 'localhost:7000'; // From docker
|
|
58
58
|
}
|
|
59
59
|
if (this.emulator) {
|
package/__index__.ts
CHANGED
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.31",
|
|
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.7.0",
|
|
29
|
+
"@travetto/config": "^8.0.0-alpha.23",
|
|
30
|
+
"@travetto/model": "^8.0.0-alpha.24",
|
|
31
|
+
"@travetto/model-indexed": "^8.0.0-alpha.26",
|
|
32
|
+
"@travetto/runtime": "^8.0.0-alpha.21"
|
|
32
33
|
},
|
|
33
34
|
"peerDependencies": {
|
|
34
|
-
"@travetto/cli": "^8.0.0-alpha.
|
|
35
|
+
"@travetto/cli": "^8.0.0-alpha.29"
|
|
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/config.ts
CHANGED
|
@@ -1,18 +1,17 @@
|
|
|
1
|
-
import { JSONUtil, Runtime, RuntimeResources } from '@travetto/runtime';
|
|
2
1
|
import { Config } from '@travetto/config';
|
|
3
|
-
import { Schema, SchemaValidator } from '@travetto/schema';
|
|
4
2
|
import { PostConstruct } from '@travetto/di';
|
|
3
|
+
import { JSONUtil, Runtime, RuntimeResources } from '@travetto/runtime';
|
|
4
|
+
import { Schema, SchemaValidator } from '@travetto/schema';
|
|
5
5
|
|
|
6
6
|
@Schema()
|
|
7
7
|
class FirestoreModelConfigCredentials {
|
|
8
8
|
client_email: string;
|
|
9
|
-
project_id: string;
|
|
10
9
|
private_key: string;
|
|
11
10
|
}
|
|
12
11
|
|
|
13
12
|
@Config('model.firestore')
|
|
14
13
|
export class FirestoreModelConfig {
|
|
15
|
-
|
|
14
|
+
databaseId?: string;
|
|
16
15
|
credentialsFile?: string;
|
|
17
16
|
emulator?: string;
|
|
18
17
|
projectId?: string;
|
|
@@ -22,8 +21,8 @@ export class FirestoreModelConfig {
|
|
|
22
21
|
|
|
23
22
|
@PostConstruct()
|
|
24
23
|
async finalizeConfig(): Promise<void> {
|
|
25
|
-
if (!this.
|
|
26
|
-
this.projectId
|
|
24
|
+
if (!this.projectId && !Runtime.production) {
|
|
25
|
+
this.projectId = 'trv-local-dev';
|
|
27
26
|
this.emulator ??= 'localhost:7000'; // From docker
|
|
28
27
|
}
|
|
29
28
|
if (this.emulator) {
|
|
@@ -35,4 +34,4 @@ export class FirestoreModelConfig {
|
|
|
35
34
|
await SchemaValidator.validate(FirestoreModelConfigCredentials, this.credentials);
|
|
36
35
|
}
|
|
37
36
|
}
|
|
38
|
-
}
|
|
37
|
+
}
|
package/src/service.ts
CHANGED
|
@@ -1,12 +1,35 @@
|
|
|
1
1
|
import { type DocumentData, FieldValue, Firestore, type Query } from '@google-cloud/firestore';
|
|
2
2
|
|
|
3
|
-
import { JSONUtil, ShutdownManager, type Class, type DeepPartial } from '@travetto/runtime';
|
|
4
3
|
import { Injectable, PostConstruct } from '@travetto/di';
|
|
5
4
|
import {
|
|
6
|
-
type ModelCrudSupport,
|
|
7
|
-
|
|
8
|
-
|
|
5
|
+
type ModelCrudSupport,
|
|
6
|
+
ModelCrudUtil,
|
|
7
|
+
type ModelListOptions,
|
|
8
|
+
ModelRegistryIndex,
|
|
9
|
+
type ModelStorageSupport,
|
|
10
|
+
type ModelType,
|
|
11
|
+
NotFoundError,
|
|
12
|
+
type OptionalId
|
|
9
13
|
} from '@travetto/model';
|
|
14
|
+
import {
|
|
15
|
+
type FullKeyedIndexBody,
|
|
16
|
+
type FullKeyedIndexWithPartialBody,
|
|
17
|
+
type KeyedIndexBody,
|
|
18
|
+
type KeyedIndexSelection,
|
|
19
|
+
ModelIndexedComputedIndex,
|
|
20
|
+
type ModelIndexedSearchOptions,
|
|
21
|
+
type ModelIndexedSupport,
|
|
22
|
+
ModelIndexedUtil,
|
|
23
|
+
type ModelPageOptions,
|
|
24
|
+
type ModelPageResult,
|
|
25
|
+
type SingleItemIndex,
|
|
26
|
+
type SortedIndex,
|
|
27
|
+
type SortedIndexSelection,
|
|
28
|
+
type SortedIndexSelectionType,
|
|
29
|
+
warnIfIndexedUniqueIndex,
|
|
30
|
+
warnIfNonIndexedIndex
|
|
31
|
+
} from '@travetto/model-indexed';
|
|
32
|
+
import { type Class, castTo, JSONUtil, RuntimeError, ShutdownManager } from '@travetto/runtime';
|
|
10
33
|
|
|
11
34
|
import type { FirestoreModelConfig } from './config.ts';
|
|
12
35
|
|
|
@@ -19,25 +42,97 @@ const setMissingValues = <T>(input: T, missingValue: unknown): T =>
|
|
|
19
42
|
*/
|
|
20
43
|
@Injectable()
|
|
21
44
|
export class FirestoreModelService implements ModelCrudSupport, ModelStorageSupport, ModelIndexedSupport {
|
|
45
|
+
static resolveTable(cls: Class, namespace?: string): string {
|
|
46
|
+
let table = ModelRegistryIndex.getStoreName(cls);
|
|
47
|
+
if (namespace) {
|
|
48
|
+
table = `${namespace}_${table}`;
|
|
49
|
+
}
|
|
50
|
+
return table;
|
|
51
|
+
}
|
|
22
52
|
|
|
23
53
|
idSource = ModelCrudUtil.uuidSource();
|
|
24
54
|
client: Firestore;
|
|
25
55
|
config: FirestoreModelConfig;
|
|
26
56
|
|
|
27
|
-
constructor(config: FirestoreModelConfig) {
|
|
57
|
+
constructor(config: FirestoreModelConfig) {
|
|
58
|
+
this.config = config;
|
|
59
|
+
}
|
|
28
60
|
|
|
29
61
|
#resolveTable(cls: Class): string {
|
|
30
|
-
|
|
31
|
-
if (this.config.namespace) {
|
|
32
|
-
table = `${this.config.namespace}_${table}`;
|
|
33
|
-
}
|
|
34
|
-
return table;
|
|
62
|
+
return FirestoreModelService.resolveTable(cls, this.config.namespace);
|
|
35
63
|
}
|
|
36
64
|
|
|
37
65
|
#getCollection(cls: Class): FirebaseFirestore.CollectionReference<DocumentData> {
|
|
38
66
|
return this.client.collection(this.#resolveTable(cls));
|
|
39
67
|
}
|
|
40
68
|
|
|
69
|
+
async #getIdByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
70
|
+
cls: Class<T>,
|
|
71
|
+
idx: SingleItemIndex<T, K, S>,
|
|
72
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
73
|
+
): Promise<string> {
|
|
74
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
75
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate({ sort: true });
|
|
76
|
+
const query = [...computed.allParts, ...(computed.idPart ? [computed.idPart] : [])].reduce<Query>(
|
|
77
|
+
(result, { path, value }) => result.where(path.join('.'), '==', value),
|
|
78
|
+
this.#getCollection(cls)
|
|
79
|
+
);
|
|
80
|
+
|
|
81
|
+
const item = await query.get();
|
|
82
|
+
if (!item || item.empty) {
|
|
83
|
+
throw new NotFoundError(`${cls.name} Index=${idx}`, computed.getKey());
|
|
84
|
+
}
|
|
85
|
+
if (item.size > 1) {
|
|
86
|
+
throw new RuntimeError(`Multiple items found for ${cls.name} Index=${idx}`);
|
|
87
|
+
}
|
|
88
|
+
return item.docs[0].data().id;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
#buildIndexQuery<T extends ModelType>(cls: Class<T>, idx: SortedIndex<T>, body: KeyedIndexBody<T>): Query {
|
|
92
|
+
ModelCrudUtil.ensureNotSubType(cls);
|
|
93
|
+
const computed = ModelIndexedComputedIndex.get(idx, body).validate();
|
|
94
|
+
|
|
95
|
+
let query = computed.keyedParts.reduce<Query>(
|
|
96
|
+
(result, { path, value, state }) => result.where(path.join('.'), '==', state === 'empty' ? null : value),
|
|
97
|
+
this.#getCollection(cls)
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
for (const { path, value } of idx.sortTemplate) {
|
|
101
|
+
query = query.orderBy(path.join('.'), value === 1 ? 'asc' : 'desc');
|
|
102
|
+
}
|
|
103
|
+
return query;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
async *#scanCollection<T extends ModelType>(
|
|
107
|
+
cls: Class<T>,
|
|
108
|
+
queryBuilder: () => Query,
|
|
109
|
+
options?: ModelListOptions & ModelPageOptions<number>
|
|
110
|
+
): AsyncIterable<{ items: T[]; nextOffset?: number }> {
|
|
111
|
+
const limit = options?.limit ?? Number.MAX_SAFE_INTEGER;
|
|
112
|
+
const batchSize = Math.min(options?.batchSizeHint ?? 100, limit);
|
|
113
|
+
|
|
114
|
+
let offset = options?.offset ?? 0;
|
|
115
|
+
let produced = 0;
|
|
116
|
+
|
|
117
|
+
while (!options?.abort?.aborted && produced < limit) {
|
|
118
|
+
const query = queryBuilder().limit(batchSize).offset(offset);
|
|
119
|
+
|
|
120
|
+
const { docs, size } = await query.get();
|
|
121
|
+
if (size === 0) {
|
|
122
|
+
break;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const remaining = produced + size > limit ? docs.slice(0, limit - produced) : docs;
|
|
126
|
+
|
|
127
|
+
offset += size;
|
|
128
|
+
|
|
129
|
+
const items = await ModelCrudUtil.filterOutNotFound(remaining.map(item => ModelCrudUtil.load(cls, item.data()!)));
|
|
130
|
+
produced += items.length;
|
|
131
|
+
|
|
132
|
+
yield { items, nextOffset: offset };
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
41
136
|
@PostConstruct()
|
|
42
137
|
async initializeClient(): Promise<void> {
|
|
43
138
|
this.client = new Firestore({ ...this.config, useBigInt: true });
|
|
@@ -45,20 +140,23 @@ export class FirestoreModelService implements ModelCrudSupport, ModelStorageSupp
|
|
|
45
140
|
}
|
|
46
141
|
|
|
47
142
|
// Storage
|
|
48
|
-
async createStorage(): Promise<void> {
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
for await (const item of this.list(cls)) {
|
|
53
|
-
await this.delete(cls, item.id);
|
|
143
|
+
async createStorage(): Promise<void> {
|
|
144
|
+
for (const cls of ModelRegistryIndex.getClasses()) {
|
|
145
|
+
warnIfIndexedUniqueIndex(this, cls, ModelRegistryIndex.getIndices(cls));
|
|
146
|
+
warnIfNonIndexedIndex(this, cls, ModelRegistryIndex.getIndices(cls));
|
|
54
147
|
}
|
|
55
148
|
}
|
|
149
|
+
async deleteStorage(): Promise<void> {}
|
|
150
|
+
|
|
151
|
+
async deleteModel<T extends ModelType>(cls: Class<T>): Promise<void> {
|
|
152
|
+
await this.client.recursiveDelete(this.#getCollection(cls));
|
|
153
|
+
}
|
|
56
154
|
|
|
57
155
|
// Crud
|
|
58
156
|
async get<T extends ModelType>(cls: Class<T>, id: string): Promise<T> {
|
|
59
157
|
const result = await this.#getCollection(cls).doc(id).get();
|
|
60
158
|
|
|
61
|
-
if (result
|
|
159
|
+
if (result?.exists) {
|
|
62
160
|
return await ModelCrudUtil.load(cls, result.data()!);
|
|
63
161
|
}
|
|
64
162
|
throw new NotFoundError(cls, id);
|
|
@@ -89,7 +187,9 @@ export class FirestoreModelService implements ModelCrudSupport, ModelStorageSupp
|
|
|
89
187
|
const id = item.id;
|
|
90
188
|
const full = await ModelCrudUtil.naivePartialUpdate(cls, () => this.get(cls, id), item, view);
|
|
91
189
|
const cleaned = setMissingValues(full, FieldValue.delete());
|
|
92
|
-
await this.#getCollection(cls)
|
|
190
|
+
await this.#getCollection(cls)
|
|
191
|
+
.doc(id)
|
|
192
|
+
.set(cleaned, { mergeFields: Object.keys(full) });
|
|
93
193
|
return this.get(cls, id);
|
|
94
194
|
}
|
|
95
195
|
|
|
@@ -105,63 +205,104 @@ export class FirestoreModelService implements ModelCrudSupport, ModelStorageSupp
|
|
|
105
205
|
}
|
|
106
206
|
}
|
|
107
207
|
|
|
108
|
-
async *
|
|
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
|
-
}
|
|
208
|
+
async *list<T extends ModelType>(cls: Class<T>, options?: ModelListOptions): AsyncIterable<T[]> {
|
|
209
|
+
for await (const { items } of this.#scanCollection(cls, () => this.#getCollection(cls), options)) {
|
|
210
|
+
yield items;
|
|
118
211
|
}
|
|
119
212
|
}
|
|
120
213
|
|
|
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
|
-
);
|
|
214
|
+
// Indexed contract
|
|
130
215
|
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
throw new NotFoundError(`${cls.name} Index=${idx}`, ModelIndexedUtil.computeIndexKey(cls, idx, body, { separator: '; ' })?.key);
|
|
137
|
-
}
|
|
138
|
-
|
|
139
|
-
async getByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<T> {
|
|
216
|
+
async getByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
217
|
+
cls: Class<T>,
|
|
218
|
+
idx: SingleItemIndex<T, K, S>,
|
|
219
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
220
|
+
): Promise<T> {
|
|
140
221
|
return this.get(cls, await this.#getIdByIndex(cls, idx, body));
|
|
141
222
|
}
|
|
142
223
|
|
|
143
|
-
async deleteByIndex<T extends ModelType
|
|
224
|
+
async deleteByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
225
|
+
cls: Class<T>,
|
|
226
|
+
idx: SingleItemIndex<T, K, S>,
|
|
227
|
+
body: FullKeyedIndexBody<T, K, S>
|
|
228
|
+
): Promise<void> {
|
|
144
229
|
return this.delete(cls, await this.#getIdByIndex(cls, idx, body));
|
|
145
230
|
}
|
|
146
231
|
|
|
147
|
-
upsertByIndex<T extends ModelType
|
|
232
|
+
upsertByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
233
|
+
cls: Class<T>,
|
|
234
|
+
idx: SingleItemIndex<T, K, S>,
|
|
235
|
+
body: OptionalId<T>
|
|
236
|
+
): Promise<T> {
|
|
148
237
|
return ModelIndexedUtil.naiveUpsert(this, cls, idx, body);
|
|
149
238
|
}
|
|
150
239
|
|
|
151
|
-
|
|
152
|
-
|
|
240
|
+
updateByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
241
|
+
cls: Class<T>,
|
|
242
|
+
idx: SingleItemIndex<T, K, S>,
|
|
243
|
+
body: T
|
|
244
|
+
): Promise<T> {
|
|
245
|
+
return ModelIndexedUtil.naiveUpdate(this, cls, idx, body);
|
|
246
|
+
}
|
|
153
247
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
248
|
+
async updatePartialByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
249
|
+
cls: Class<T>,
|
|
250
|
+
idx: SingleItemIndex<T, K, S>,
|
|
251
|
+
body: FullKeyedIndexWithPartialBody<T, K, S>
|
|
252
|
+
): Promise<T> {
|
|
253
|
+
const item = await ModelCrudUtil.naivePartialUpdate(cls, () => this.getByIndex(cls, idx, castTo(body)), castTo(body));
|
|
254
|
+
return this.update(cls, item);
|
|
255
|
+
}
|
|
158
256
|
|
|
159
|
-
|
|
160
|
-
|
|
257
|
+
async pageByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
258
|
+
cls: Class<T>,
|
|
259
|
+
idx: SortedIndex<T, K, S>,
|
|
260
|
+
body: KeyedIndexBody<T, K>,
|
|
261
|
+
options?: ModelPageOptions
|
|
262
|
+
): Promise<ModelPageResult<T>> {
|
|
263
|
+
const items: T[] = [];
|
|
264
|
+
let nextOffset: number | undefined;
|
|
265
|
+
for await (const batch of this.#scanCollection(cls, () => this.#buildIndexQuery(cls, idx, body), {
|
|
266
|
+
limit: 100,
|
|
267
|
+
...options,
|
|
268
|
+
offset: options?.offset ? JSONUtil.fromBase64<number>(options.offset) : 0
|
|
269
|
+
})) {
|
|
270
|
+
items.push(...batch.items);
|
|
271
|
+
nextOffset = batch.nextOffset;
|
|
161
272
|
}
|
|
162
273
|
|
|
163
|
-
|
|
164
|
-
|
|
274
|
+
return { items, nextOffset: nextOffset ? JSONUtil.toBase64(nextOffset) : undefined };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
async *listByIndex<T extends ModelType, K extends KeyedIndexSelection<T>, S extends SortedIndexSelection<T>>(
|
|
278
|
+
cls: Class<T>,
|
|
279
|
+
idx: SortedIndex<T, K, S>,
|
|
280
|
+
body: KeyedIndexBody<T, K>,
|
|
281
|
+
options?: ModelListOptions
|
|
282
|
+
): AsyncIterable<T[]> {
|
|
283
|
+
for await (const { items } of this.#scanCollection(cls, () => this.#buildIndexQuery(cls, idx, body), options)) {
|
|
284
|
+
yield items;
|
|
165
285
|
}
|
|
166
286
|
}
|
|
167
|
-
|
|
287
|
+
|
|
288
|
+
async suggestByIndex<
|
|
289
|
+
T extends ModelType,
|
|
290
|
+
S extends SortedIndexSelection<T>,
|
|
291
|
+
K extends KeyedIndexSelection<T>,
|
|
292
|
+
B extends SortedIndexSelectionType<T, S> & string
|
|
293
|
+
>(cls: Class<T>, idx: SortedIndex<T, K, S>, body: KeyedIndexBody<T, K>, prefix: B, options?: ModelIndexedSearchOptions): Promise<T[]> {
|
|
294
|
+
const results: T[] = [];
|
|
295
|
+
|
|
296
|
+
const field = idx.sortTemplate[0].path.join('.');
|
|
297
|
+
|
|
298
|
+
for await (const { items } of this.#scanCollection(
|
|
299
|
+
cls,
|
|
300
|
+
() => this.#buildIndexQuery(cls, idx, body).where(field, '>=', prefix).where(field, '<', `${prefix}\uf8ff`),
|
|
301
|
+
{ limit: 10, ...options }
|
|
302
|
+
)) {
|
|
303
|
+
results.push(...items);
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return results;
|
|
307
|
+
}
|
|
308
|
+
}
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
import cp from 'node:child_process';
|
|
2
|
+
import fs from 'node:fs/promises';
|
|
3
|
+
|
|
4
|
+
import { CliCommand, type CliCommandShape, CliModuleFlag } from '@travetto/cli';
|
|
5
|
+
import { DependencyRegistryIndex } from '@travetto/di';
|
|
6
|
+
import { ManifestFileUtil } from '@travetto/manifest';
|
|
7
|
+
import { ModelRegistryIndex } from '@travetto/model';
|
|
8
|
+
import { isModelIndexedIndex } from '@travetto/model-indexed';
|
|
9
|
+
import { Registry } from '@travetto/registry';
|
|
10
|
+
import { Env, ExecUtil, JSONUtil, Runtime } from '@travetto/runtime';
|
|
11
|
+
|
|
12
|
+
import { FirestoreModelConfig } from '../src/config.ts';
|
|
13
|
+
import { FirestoreModelService } from '../src/service.ts';
|
|
14
|
+
|
|
15
|
+
type FirestoreIndexField = {
|
|
16
|
+
fieldPath: string;
|
|
17
|
+
order: 'ASCENDING' | 'DESCENDING';
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
type FirestoreIndexDefinition = {
|
|
21
|
+
collectionGroup: string;
|
|
22
|
+
queryScope: 'COLLECTION' | 'COLLECTION_GROUP';
|
|
23
|
+
fields: FirestoreIndexField[];
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type FirestoreIndexSet = {
|
|
27
|
+
indexes: FirestoreIndexDefinition[];
|
|
28
|
+
fieldOverrides: unknown[];
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* Generate the Firestore composite indexes JSON for all registered models.
|
|
33
|
+
*
|
|
34
|
+
* The resulting JSON can be written to stdout or to a file path for use in
|
|
35
|
+
* firebase-cli deployments.
|
|
36
|
+
*/
|
|
37
|
+
@CliCommand()
|
|
38
|
+
export class FirestoreIndexesCommand implements CliCommandShape {
|
|
39
|
+
indexFile = 'firestore.indexes.json';
|
|
40
|
+
firebaseFile = 'firebase.json';
|
|
41
|
+
|
|
42
|
+
@CliModuleFlag({ short: 'm' })
|
|
43
|
+
module: string;
|
|
44
|
+
|
|
45
|
+
/** Should we deploy after writing the json file? */
|
|
46
|
+
deploy?: boolean;
|
|
47
|
+
|
|
48
|
+
finalize(): void {
|
|
49
|
+
Env.DEBUG.set(false);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
async main(): Promise<void> {
|
|
53
|
+
await Registry.init();
|
|
54
|
+
|
|
55
|
+
const config = await DependencyRegistryIndex.getInstance(FirestoreModelConfig);
|
|
56
|
+
|
|
57
|
+
const indexesList: FirestoreIndexDefinition[] = [];
|
|
58
|
+
|
|
59
|
+
for (const cls of ModelRegistryIndex.getClasses()) {
|
|
60
|
+
const indices = ModelRegistryIndex.getIndices(cls)
|
|
61
|
+
.filter(isModelIndexedIndex)
|
|
62
|
+
// We need at least 2 fields. All 1 field indices are already handled
|
|
63
|
+
.filter(idx => (idx.keyTemplate?.length ?? 0) + (idx.sortTemplate?.length ?? 0) >= 2);
|
|
64
|
+
|
|
65
|
+
for (const idx of indices) {
|
|
66
|
+
indexesList.push({
|
|
67
|
+
collectionGroup: FirestoreModelService.resolveTable(cls, config.namespace),
|
|
68
|
+
queryScope: 'COLLECTION',
|
|
69
|
+
fields: [...idx.keyTemplate, ...idx.sortTemplate].map(part => ({
|
|
70
|
+
fieldPath: part.path.join('.'),
|
|
71
|
+
order: part.value === -1 ? 'DESCENDING' : 'ASCENDING'
|
|
72
|
+
}))
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const outputData: FirestoreIndexSet = {
|
|
78
|
+
indexes: indexesList,
|
|
79
|
+
fieldOverrides: []
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
const text = JSONUtil.toUTF8Pretty(outputData);
|
|
83
|
+
|
|
84
|
+
await ManifestFileUtil.bufferedFileWrite(this.indexFile, text);
|
|
85
|
+
|
|
86
|
+
const firebaseLocation = Runtime.workspaceRelative(this.firebaseFile);
|
|
87
|
+
if (!(await fs.stat(firebaseLocation, { throwIfNoEntry: false }))) {
|
|
88
|
+
await ManifestFileUtil.bufferedFileWrite(firebaseLocation, '{}');
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const firebaseContext = JSONUtil.fromBinaryArray<{ firestore?: { database?: string; indexes?: string }[] }>(
|
|
92
|
+
await fs.readFile(firebaseLocation)
|
|
93
|
+
);
|
|
94
|
+
const existing = (firebaseContext.firestore ??= []);
|
|
95
|
+
const found = existing.find(x => x.database === config.databaseId);
|
|
96
|
+
|
|
97
|
+
let changed = true;
|
|
98
|
+
if (!found) {
|
|
99
|
+
existing.push({ indexes: this.indexFile, database: config.databaseId });
|
|
100
|
+
} else if (found.indexes !== this.indexFile) {
|
|
101
|
+
found.indexes = this.indexFile;
|
|
102
|
+
} else {
|
|
103
|
+
changed = false;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if (changed) {
|
|
107
|
+
await ManifestFileUtil.bufferedFileWrite(firebaseLocation, JSONUtil.toUTF8Pretty(firebaseContext));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (this.deploy) {
|
|
111
|
+
const child = cp.spawn(
|
|
112
|
+
'firebase',
|
|
113
|
+
['deploy', '--only', 'firestore:indexes'],
|
|
114
|
+
// Complete take over
|
|
115
|
+
{ stdio: 'inherit' }
|
|
116
|
+
);
|
|
117
|
+
await ExecUtil.getResult(child);
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
}
|