@my-devkit/firebase 1.0.127 → 1.0.129

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.
@@ -1,4 +1,4 @@
1
- import { deserialize, Document, Logger, NotFoundError, serialize } from '@my-devkit/core';
1
+ import { _orderBy, Cacheable, deserialize, Document, Logger, NotFoundError, serialize } from '@my-devkit/core';
2
2
  import { getFirestore } from 'firebase-admin/firestore';
3
3
 
4
4
  import { Injectable, TransactionalClient } from './decorators';
@@ -9,41 +9,41 @@ import { ITransactionalClient } from './interfaces';
9
9
  @Injectable()
10
10
  export class FirestoreClient implements ITransactionalClient {
11
11
  private db = getFirestore();
12
- private operations: FirestoreClient.Operation[] = [];
13
- private cachedDocuments = new Map<string, Document>();
12
+ private operations = new Map<string, FirestoreClient.Operation>();
14
13
 
15
14
  public async save(): Promise<void> {
16
- Logger.info(`Saving ${this.operations.length} operations`);
15
+ Logger.info(`Saving ${this.operations.size} operations`);
17
16
 
18
- for (const operation of this.operations) {
19
- if (operation.action === FirestoreClient.Action.Create) {
17
+ for (const operation of Array.from(this.operations.values())) {
18
+ if (this.isCreateOperation(operation)) {
20
19
  Logger.info(`Create document ${operation.document._path}`);
21
20
  await this.db.doc(operation.document._path).set(serialize(operation.document));
22
- } else if (operation.action === FirestoreClient.Action.Update) {
21
+ } else if (this.isUpdateOperation(operation)) {
23
22
  Logger.info(`Update document ${operation.document._path}`);
24
23
  await this.db.doc(operation.document._path).set(serialize(operation.document));
25
- } else if (operation.action === FirestoreClient.Action.Delete) {
24
+ } else if (this.isDeleteOperation(operation)) {
26
25
  Logger.info(`Delete document ${operation.document._path}`);
27
26
  await this.db.doc(operation.document._path).delete();
28
27
  } else {
29
- throw new Error(`Transactional Repository: Not supported action ${operation.action}`);
28
+ throw new Error(`Not supported action ${operation.action}`);
30
29
  }
31
30
  }
32
31
  }
33
32
 
34
- public async _find<T>(path: string): Promise<T> {
33
+ public async _find<T extends Document>(path: string): Promise<T> {
35
34
  Logger.info(`Repository: find ${path}`);
36
- if (this.cachedDocuments.has(path)) {
35
+ if (this.operations.has(path)) {
37
36
  Logger.info(`Repository: document returned from transaction`);
38
- return <any>this.cachedDocuments.get(path);
37
+ const operation = this.operations.get(path);
38
+ return this.isDeleteOperation(operation) ? null : <T>operation.document;
39
39
  }
40
- const doc = await this.db.doc(path).get();
41
- const result = doc.data();
42
- return result ? deserialize(result) : null;
40
+
41
+ return this.queryDocument<T>(path);
43
42
  }
44
43
 
45
- public async _findWhere<T>(collection: string, whereClauses: [string, FirestoreClient.Operator, any | any[]][] = []): Promise<T> {
46
- const documents = await this._getWhere<T>(collection, whereClauses);
44
+ public async _findWhere<T extends Document>(collection: string, whereClauses: [string, FirestoreClient.Operator, any | any[]][] = [], options?: FirestoreClient.QueryOptions): Promise<T> {
45
+ const documents = await this._getWhere<T>(collection, whereClauses, options);
46
+
47
47
  if (documents.length > 1) {
48
48
  throw new Error(`Transactional Repository: Too many documents found (${documents.length})`);
49
49
  }
@@ -51,7 +51,7 @@ export class FirestoreClient implements ITransactionalClient {
51
51
  return documents[0] || null;
52
52
  }
53
53
 
54
- public async _get<T>(path: string): Promise<T> {
54
+ public async _get<T extends Document>(path: string): Promise<T> {
55
55
  Logger.info(`Repository: get ${path}`);
56
56
  const result = await this._find<T>(path);
57
57
  if (!result) {
@@ -61,52 +61,120 @@ export class FirestoreClient implements ITransactionalClient {
61
61
  return result;
62
62
  }
63
63
 
64
- public async _getAll<T>(collection: string): Promise<T[]> {
65
- Logger.info(`Repository: getAll ${collection}`);
66
- const documents: T[] = [];
67
- const querySnapshot = await this.getCollectionReference(collection).get();
68
- querySnapshot.forEach(doc => {
69
- documents.push(deserialize(doc.data()));
70
- });
71
- return documents;
64
+ public async _getAll<T extends Document>(collection: string, options?: FirestoreClient.QueryOptions): Promise<T[]> {
65
+ return this._getWhere(collection, [], options);
72
66
  }
73
67
 
74
- public async _getWhere<T>(collection: string, whereClauses: [string, FirestoreClient.Operator, any | any[]][] = [], orderBy: string = null): Promise<T[]> {
68
+ public async _getWhere<T extends Document>(collection: string, whereClauses: [string, FirestoreClient.Operator, any | any[]][] = [], options?: FirestoreClient.QueryOptions): Promise<T[]> {
75
69
  Logger.info(`Repository: getWhere ${collection} whereClauses: ${JSON.stringify(whereClauses)}`);
76
- const documents: T[] = [];
70
+ const documentMap = await this.queryCollection<T>(collection, whereClauses, options);
77
71
 
78
- let query = this.getCollectionReference(collection);
72
+ for (const operation of Array.from(this.operations.values())) {
73
+ const exists = documentMap.has(operation.document._path);
79
74
 
80
- whereClauses.forEach(where => {
81
- query = query.where(where[0], where[1], where[2]);
82
- });
75
+ switch (operation.action) {
76
+ case FirestoreClient.Action.Create:
77
+ if (this.doesDocumentMatchQuery(operation.document, collection, whereClauses)) {
78
+ documentMap.set(operation.document._path, <T>operation.document);
79
+ Logger.info(`Repository: Document ${operation.document._path} added to result`);
80
+ }
81
+ break;
82
+ case FirestoreClient.Action.Update:
83
+ if (this.doesDocumentMatchQuery(operation.document, collection, whereClauses)) {
84
+ Logger.info(`Repository: Document ${operation.document._path} ${exists ? 'updated from' : 'added to'} result`);
85
+ documentMap.set(operation.document._path, <T>operation.document);
86
+ } else if (exists) {
87
+ documentMap.delete(operation.document._path);
88
+ Logger.info(`Repository: Document ${operation.document._path} removed from result`);
89
+ }
90
+ break;
91
+ case FirestoreClient.Action.Delete:
92
+ if (exists) {
93
+ documentMap.delete(operation.document._path);
94
+ Logger.info(`Repository: Document ${operation.document._path} removed from result`);
95
+ }
96
+ break;
97
+ default:
98
+ throw new Error(`FirestoreClient: Unhandled existing operation ${operation.action} on ${operation.document._path}`);
99
+ }
100
+ }
83
101
 
84
- if (orderBy) {
85
- query = query.orderBy(orderBy);
102
+ Logger.info(`Repository: ${documentMap.size} documents found`);
103
+
104
+ let documents = Array.from(documentMap.values());
105
+
106
+ if (options?.orderBy) {
107
+ documents = _orderBy(documents, [options.orderBy], [options.orderByDirection || 'asc']);
86
108
  }
87
109
 
88
- const querySnapshot = await query.get();
89
- querySnapshot.forEach(doc => {
90
- documents.push(deserialize(doc.data()));
91
- });
110
+ if (options?.limit) {
111
+ documents = documents.slice(0, options.limit);
112
+ }
92
113
 
93
114
  return documents;
94
115
  }
95
116
 
96
117
 
97
- public createDocument(document: Document): void {
98
- this.operations.push({ document, action: FirestoreClient.Action.Create });
99
- this.cachedDocuments.set(document._path, document);
118
+ public createDocument<T extends Document>(document: T): void {
119
+ const existingOperation = this.operations.get(document._path);
120
+
121
+ if (existingOperation) {
122
+ switch (existingOperation.action) {
123
+ case FirestoreClient.Action.Create:
124
+ throw new Error(`FirestoreClient: can't create twice document ${document._path}`);
125
+ case FirestoreClient.Action.Update:
126
+ throw new Error(`FirestoreClient: can't create already existing document ${document._path}`);
127
+ case FirestoreClient.Action.Delete:
128
+ existingOperation.action = FirestoreClient.Action.Update;
129
+ existingOperation.document = document;
130
+ break;
131
+ default:
132
+ throw new Error(`FirestoreClient: Unhandled existing operation ${existingOperation.action} on ${document._path}`);
133
+ }
134
+ } else {
135
+ this.operations.set(document._path, { document, action: FirestoreClient.Action.Create });
136
+ }
100
137
  }
101
138
 
102
- public updateDocument(document: Document): void {
103
- this.operations.push({ document, action: FirestoreClient.Action.Update });
104
- this.cachedDocuments.set(document._path, document);
139
+ public updateDocument<T extends Document>(document: T): void {
140
+ const existingOperation = this.operations.get(document._path);
141
+
142
+ if (existingOperation) {
143
+ switch (existingOperation.action) {
144
+ case FirestoreClient.Action.Create:
145
+ case FirestoreClient.Action.Update:
146
+ existingOperation.document = document;
147
+ break;
148
+ case FirestoreClient.Action.Delete:
149
+ throw new Error(`FirestoreClient: can't update deleted document ${document._path}`);
150
+ default:
151
+ throw new Error(`FirestoreClient: Unhandled existing operation ${existingOperation.action} on ${document._path}`);
152
+ }
153
+ } else {
154
+ this.operations.set(document._path, { document, action: FirestoreClient.Action.Update });
155
+ }
105
156
  }
106
157
 
107
- public deleteDocument(document: Document): void {
108
- this.operations.push({ document, action: FirestoreClient.Action.Delete });
109
- this.cachedDocuments.delete(document._path);
158
+ public deleteDocument<T extends Document>(document: T): void {
159
+ const existingOperation = this.operations.get(document._path);
160
+
161
+ if (existingOperation) {
162
+ switch (existingOperation.action) {
163
+ case FirestoreClient.Action.Create:
164
+ this.operations.delete(document._path);
165
+ break;
166
+ case FirestoreClient.Action.Update:
167
+ existingOperation.document = document;
168
+ existingOperation.action = FirestoreClient.Action.Delete;
169
+ break;
170
+ case FirestoreClient.Action.Delete:
171
+ throw new Error(`FirestoreClient: can't delete already deleted document ${document._path}`);
172
+ default:
173
+ throw new Error(`FirestoreClient: Unhandled existing operation ${existingOperation.action} on ${document._path}`);
174
+ }
175
+ } else {
176
+ this.operations.set(document._path, { document, action: FirestoreClient.Action.Delete });
177
+ }
110
178
  }
111
179
 
112
180
  private getCollectionReference(collection: string): FirebaseFirestore.CollectionReference | FirebaseFirestore.Query {
@@ -116,6 +184,90 @@ export class FirestoreClient implements ITransactionalClient {
116
184
  return this.db.collection(collection);
117
185
  }
118
186
  }
187
+
188
+ @Cacheable()
189
+ private async queryCollection<T extends Document>(
190
+ collection: string,
191
+ whereClauses: [string, FirestoreClient.Operator, any | any[]][] = [],
192
+ options: FirestoreClient.QueryOptions
193
+ ): Promise<Map<string, T>> {
194
+ let query = this.getCollectionReference(collection);
195
+
196
+ whereClauses.forEach(where => {
197
+ query = query.where(where[0], where[1], where[2]);
198
+ });
199
+
200
+ if (options?.orderBy) {
201
+ query = query.orderBy(options.orderBy, options.orderByDirection || 'asc');
202
+ }
203
+
204
+ if (options?.limit) {
205
+ query = query.limit(options.limit);
206
+ }
207
+
208
+ const querySnapshot = await query.get();
209
+
210
+ const documents = new Map<string, T>();
211
+ querySnapshot.forEach(doc => {
212
+ const document = deserialize<T>(doc.data());
213
+ documents.set(document._path, document);
214
+ });
215
+ return documents;
216
+ }
217
+
218
+ @Cacheable()
219
+ private async queryDocument<T extends Document>(path: string): Promise<T> {
220
+ const doc = await this.db.doc(path).get();
221
+ const result = doc.data();
222
+ return result ? deserialize<T>(result) : null;
223
+ }
224
+
225
+ private doesDocumentMatchQuery<T extends Document>(document: T, collection: string, whereClauses: [string, FirestoreClient.Operator, any | any[]][]): boolean {
226
+ const serializedDocument = serialize(document);
227
+
228
+ return document._path.startsWith(`${collection}/`) && whereClauses.every(wc => {
229
+ const documentValue = serializedDocument[wc[0]];
230
+ const operator = wc[1];
231
+ const expectedValue = wc[2];
232
+
233
+ switch (operator) {
234
+ case '==':
235
+ return documentValue === expectedValue;
236
+ case '!=':
237
+ return documentValue !== undefined && documentValue !== expectedValue;
238
+ case '>':
239
+ return documentValue > expectedValue;
240
+ case '>=':
241
+ return documentValue >= expectedValue;
242
+ case '<':
243
+ return documentValue < expectedValue;
244
+ case '<=':
245
+ return documentValue <= expectedValue;
246
+ case 'array-contains':
247
+ return Array.isArray(documentValue) && documentValue.includes(expectedValue);
248
+ case 'array-contains-any':
249
+ return Array.isArray(documentValue) && Array.isArray(expectedValue) && documentValue.some(v => expectedValue.includes(v));
250
+ case 'in':
251
+ return Array.isArray(expectedValue) && expectedValue.includes(documentValue);
252
+ case 'not-in':
253
+ return Array.isArray(expectedValue) && !expectedValue.includes(documentValue);
254
+ default:
255
+ throw new Error(`Operator ${operator} not handled!`);
256
+ }
257
+ });
258
+ }
259
+
260
+ private isCreateOperation(operation: FirestoreClient.Operation): boolean {
261
+ return operation.action === FirestoreClient.Action.Create;
262
+ }
263
+
264
+ private isUpdateOperation(operation: FirestoreClient.Operation): boolean {
265
+ return operation.action === FirestoreClient.Action.Update;
266
+ }
267
+
268
+ private isDeleteOperation(operation: FirestoreClient.Operation): boolean {
269
+ return operation.action === FirestoreClient.Action.Delete;
270
+ }
119
271
  }
120
272
 
121
273
  export namespace FirestoreClient {
@@ -131,4 +283,10 @@ export namespace FirestoreClient {
131
283
  Delete = <any>'Delete'
132
284
  }
133
285
  export type Operator = FirebaseFirestore.WhereFilterOp;
286
+
287
+ export interface QueryOptions {
288
+ orderBy?: string;
289
+ orderByDirection?: FirebaseFirestore.OrderByDirection;
290
+ limit?: number;
291
+ }
134
292
  }