@travetto/model-firestore 8.0.0-alpha.3 → 8.0.0-alpha.30

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 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."). 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).
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#L11)
20
- * [Indexed](https://github.com/travetto/travetto/tree/main/module/model/src/types/indexed.ts#L11)
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
- databaseURL?: string;
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.databaseURL && !Runtime.production) {
56
- this.projectId ??= 'trv-local-dev';
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
@@ -1,2 +1,2 @@
1
+ export * from './src/config.ts';
1
2
  export * from './src/service.ts';
2
- export * from './src/config.ts';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@travetto/model-firestore",
3
- "version": "8.0.0-alpha.3",
3
+ "version": "8.0.0-alpha.30",
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.3.0",
29
- "@travetto/config": "^8.0.0-alpha.3",
30
- "@travetto/model": "^8.0.0-alpha.3",
31
- "@travetto/runtime": "^8.0.0-alpha.3"
28
+ "@google-cloud/firestore": "^8.6.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.4"
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
- databaseURL?: string;
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.databaseURL && !Runtime.production) {
26
- this.projectId ??= 'trv-local-dev';
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, ModelRegistryIndex, type ModelStorageSupport,
7
- type ModelIndexedSupport, type ModelType, NotFoundError, type OptionalId,
8
- ModelCrudUtil, ModelIndexedUtil,
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) { this.config = config; }
57
+ constructor(config: FirestoreModelConfig) {
58
+ this.config = config;
59
+ }
28
60
 
29
61
  #resolveTable(cls: Class): string {
30
- let table = ModelRegistryIndex.getStoreName(cls);
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
- async deleteStorage(): Promise<void> { }
50
-
51
- async deleteModel(cls: Class): Promise<void> {
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 && result.exists) {
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).doc(id).set(cleaned, { mergeFields: Object.keys(full) });
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 * list<T extends ModelType>(cls: Class<T>): AsyncIterable<T> {
109
- const batch = await this.#getCollection(cls).select().get();
110
- for (const item of batch.docs) {
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
- 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
- }
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>(cls: Class<T>, idx: string, body: DeepPartial<T>): Promise<void> {
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>(cls: Class<T>, idx: string, body: OptionalId<T>): Promise<T> {
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
- async * listByIndex<T extends ModelType>(cls: Class<T>, idx: string, body: DeepPartial<T>): AsyncIterable<T> {
152
- ModelCrudUtil.ensureNotSubType(cls);
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
- const config = ModelRegistryIndex.getIndex(cls, idx, ['sorted', 'unsorted']);
155
- const { fields, sorted } = ModelIndexedUtil.computeIndexParts(cls, config, body, { emptySortValue: null });
156
- let query = fields.reduce<Query>((result, { path, value }) =>
157
- result.where(path.join('.'), '==', value), this.#getCollection(cls));
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
- if (sorted) {
160
- query = query.orderBy(sorted.path.join('.'), sorted.dir === 1 ? 'asc' : 'desc');
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
- for (const item of (await query.get()).docs) {
164
- yield await ModelCrudUtil.load(cls, item.data()!);
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
+ }
@@ -10,4 +10,4 @@ export const service: ServiceDescriptor = {
10
10
  FIRESTORE_PROJECT_ID: 'trv-local-dev'
11
11
  },
12
12
  image: `ridedott/firestore-emulator:${version}`
13
- };
13
+ };