firetender-admin 0.10.0

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.
Files changed (43) hide show
  1. package/LICENSE +20 -0
  2. package/README.md +349 -0
  3. package/dist/FiretenderCollection.d.ts +116 -0
  4. package/dist/FiretenderCollection.js +225 -0
  5. package/dist/FiretenderCollection.js.map +1 -0
  6. package/dist/FiretenderDoc.d.ts +172 -0
  7. package/dist/FiretenderDoc.js +361 -0
  8. package/dist/FiretenderDoc.js.map +1 -0
  9. package/dist/errors.d.ts +30 -0
  10. package/dist/errors.js +49 -0
  11. package/dist/errors.js.map +1 -0
  12. package/dist/firestore-deps-admin.d.ts +26 -0
  13. package/dist/firestore-deps-admin.js +62 -0
  14. package/dist/firestore-deps-admin.js.map +1 -0
  15. package/dist/firestore-deps-web.d.ts +10 -0
  16. package/dist/firestore-deps-web.js +29 -0
  17. package/dist/firestore-deps-web.js.map +1 -0
  18. package/dist/firestore-deps.d.ts +26 -0
  19. package/dist/firestore-deps.js +62 -0
  20. package/dist/firestore-deps.js.map +1 -0
  21. package/dist/index.d.ts +6 -0
  22. package/dist/index.js +18 -0
  23. package/dist/index.js.map +1 -0
  24. package/dist/proxies.d.ts +62 -0
  25. package/dist/proxies.js +243 -0
  26. package/dist/proxies.js.map +1 -0
  27. package/dist/timestamps.d.ts +37 -0
  28. package/dist/timestamps.js +58 -0
  29. package/dist/timestamps.js.map +1 -0
  30. package/dist/ts-helpers.d.ts +14 -0
  31. package/dist/ts-helpers.js +19 -0
  32. package/dist/ts-helpers.js.map +1 -0
  33. package/package.json +72 -0
  34. package/src/FiretenderCollection.ts +311 -0
  35. package/src/FiretenderDoc.ts +483 -0
  36. package/src/errors.ts +47 -0
  37. package/src/firestore-deps-admin.ts +111 -0
  38. package/src/firestore-deps-web.ts +14 -0
  39. package/src/firestore-deps.ts +111 -0
  40. package/src/index.ts +29 -0
  41. package/src/proxies.ts +286 -0
  42. package/src/timestamps.ts +62 -0
  43. package/src/ts-helpers.ts +39 -0
@@ -0,0 +1,483 @@
1
+ import { z } from "zod";
2
+
3
+ import {
4
+ addContextToError,
5
+ FiretenderInternalError,
6
+ FiretenderIOError,
7
+ FiretenderUsageError,
8
+ } from "./errors";
9
+ import {
10
+ addDoc,
11
+ collection,
12
+ CollectionReference,
13
+ doc,
14
+ DocumentReference,
15
+ DocumentSnapshot,
16
+ getDoc,
17
+ setDoc,
18
+ updateDoc,
19
+ } from "./firestore-deps";
20
+ import { watchFieldForChanges } from "./proxies";
21
+ import { assertIsDefined, DeepReadonly } from "./ts-helpers";
22
+
23
+ /**
24
+ * Public options for initializing a FiretenderDoc object.
25
+ *
26
+ * These will be added as needed (e.g., "readonly" for issue #1, possibly
27
+ * "queryPageLength" for issue #21).
28
+ */
29
+ // eslint-disable-next-line @typescript-eslint/ban-types
30
+ export type FiretenderDocOptions = {};
31
+
32
+ /**
33
+ * All options when initializing a FiretenderDoc object.
34
+ *
35
+ * This type includes options meant for internal use (createDoc, initialData)
36
+ * as well as the options in FiretenderDocOptions.
37
+ */
38
+ export type AllFiretenderDocOptions = FiretenderDocOptions & {
39
+ /**
40
+ * Does this FiretenderDoc represent a new document in Firestore?
41
+ */
42
+ createDoc?: true;
43
+
44
+ /**
45
+ * The document's initial data, which must define a valid instance of the
46
+ * document according to its schema.
47
+ */
48
+ initialData?: Record<string, any>;
49
+ };
50
+
51
+ /**
52
+ * A local representation of a Firestore document.
53
+ */
54
+ export class FiretenderDoc<SchemaType extends z.SomeZodObject> {
55
+ /** Zod schema used to parse and validate the document's data */
56
+ readonly schema: SchemaType;
57
+
58
+ /** Firestore reference to this doc, or collection in which to create it */
59
+ private ref: DocumentReference | CollectionReference;
60
+
61
+ /** Firestore document ID; undefined for new docs not yet on Firestore */
62
+ private docID: string | undefined = undefined;
63
+
64
+ /** Is this a doc we presume does not yet exist in Firestore? */
65
+ private isNewDoc: boolean;
66
+
67
+ /** Use addDoc or setDoc to write all the data? If not, use updateDoc. */
68
+ private isSettingNewContents: boolean;
69
+
70
+ /** Local copy of the document data, parsed into the Zod type */
71
+ private data: z.infer<SchemaType> | undefined = undefined;
72
+
73
+ /** Proxy to intercept write (.w) access to the data and track the changes */
74
+ private dataProxy: ProxyHandler<z.infer<SchemaType>> | undefined = undefined;
75
+
76
+ /** Map from the dot-delimited field path (per updateDoc()) to new value */
77
+ private updates = new Map<string, any>();
78
+
79
+ /**
80
+ * If a load() call is already in progress, this is a list of promise
81
+ * resolutions to be called once the load is complete. Otherwise undefined.
82
+ */
83
+ private resolvesWaitingForLoad:
84
+ | { resolve: () => void; reject: (reason?: any) => void }[]
85
+ | undefined;
86
+
87
+ /**
88
+ * @param schema the Zod object schema describing this document's data.
89
+ * @param ref either a document reference specifying the full path of the
90
+ * document, or a collection reference specifying where a new document will
91
+ * be created.
92
+ * @param options optional parameters for the resulting FiretenderDoc; see
93
+ * FiretenderDocOptions for detail.
94
+ */
95
+ constructor(
96
+ schema: SchemaType,
97
+ ref: DocumentReference | CollectionReference,
98
+ options: AllFiretenderDocOptions = {}
99
+ ) {
100
+ this.schema = schema;
101
+ this.ref = ref;
102
+ this.isNewDoc = options.createDoc ?? false;
103
+ this.isSettingNewContents = this.isNewDoc;
104
+ if (options.initialData) {
105
+ this.data = schema.parse(options.initialData);
106
+ } else if (this.isNewDoc) {
107
+ throw ReferenceError(
108
+ "Initial data must be given when creating a new doc."
109
+ );
110
+ }
111
+ if (this.ref instanceof DocumentReference) {
112
+ this.docID = this.ref.path.split("/").pop();
113
+ } else if (!this.isNewDoc) {
114
+ throw TypeError(
115
+ "FiretenderDoc can only take a collection reference when creating a new document. Use .createNewDoc() if this is your intent."
116
+ );
117
+ }
118
+ }
119
+
120
+ /**
121
+ * Returns a FiretenderDoc representing a new Firestore document.
122
+ *
123
+ * This method does not create the document in Firestore. To do so, call the
124
+ * write() method.
125
+ *
126
+ * @param schema the Zod object schema describing this document's data.
127
+ * @param ref either a document reference specifying the full path of the
128
+ * document, or a collection reference specifying where a new document will
129
+ * be created.
130
+ * @param initialData the document's initial data, which must define a valid
131
+ * instance of this document according to its schema.
132
+ * @param options optional parameters for the resulting FiretenderDoc; see
133
+ * FiretenderDocOptions for detail.
134
+ */
135
+ static createNewDoc<SchemaType1 extends z.SomeZodObject>(
136
+ schema: SchemaType1,
137
+ ref: DocumentReference | CollectionReference,
138
+ initialData: z.input<SchemaType1>,
139
+ options: FiretenderDocOptions = {}
140
+ ): FiretenderDoc<SchemaType1> {
141
+ const mergedOptions: AllFiretenderDocOptions = {
142
+ ...options,
143
+ createDoc: true,
144
+ initialData,
145
+ };
146
+ return new FiretenderDoc(schema, ref, mergedOptions);
147
+ }
148
+
149
+ /**
150
+ * Creates a copy of this document. Returns a deep copy of its data with a
151
+ * specified or undefined Firestore ID and reference.
152
+ *
153
+ * This method does not create the document in Firestore. To do so, call the
154
+ * write() method. If a document ID or reference is not provided, those will
155
+ * be unset until the write.
156
+ *
157
+ * The location of the new document depends on the type of the `dest`
158
+ * argument:
159
+ * - `undefined` (default): It will be in the same collection and will be
160
+ * assigned a random ID when written to Firestore.
161
+ * - `string`: It will be in the same collection and have a document ID given
162
+ * by `dest`.
163
+ * - `string[]`: It will be in the specified subcollection and receive a
164
+ * random ID (if `type` does not give an ID for the deepest subcollection)
165
+ * or have the fully specified Firestore path (if `type` does).
166
+ * - `DocumentReference`: It will have the given Firestore reference.
167
+ * - `CollectionReference`: It will be in the given subcollection and have a
168
+ * randomly assigned ID upon writing.
169
+ *
170
+ * @param dest the location of the new document; see above for details.
171
+ * @param options optional parameters for the resulting FiretenderDoc; see
172
+ * FiretenderDocOptions for detail.
173
+ */
174
+ copy(
175
+ dest:
176
+ | DocumentReference
177
+ | CollectionReference
178
+ | string
179
+ | string[]
180
+ | undefined = undefined,
181
+ options: AllFiretenderDocOptions = {}
182
+ ): FiretenderDoc<SchemaType> {
183
+ if (!this.data) {
184
+ throw new FiretenderUsageError(
185
+ "You must call load() before making a copy."
186
+ );
187
+ }
188
+ let ref: DocumentReference | CollectionReference;
189
+ if (
190
+ dest instanceof DocumentReference ||
191
+ dest instanceof CollectionReference
192
+ ) {
193
+ ref = dest;
194
+ } else if (Array.isArray(dest)) {
195
+ const path = this.ref.path.split("/");
196
+ if (path.length % 2 === 0) {
197
+ // If this doc has a ID for the deepest collection, remove it so that
198
+ // path always starts as a collection path.
199
+ path.length -= 1;
200
+ }
201
+ const collectionDepth = (path.length + 1) / 2;
202
+ if (dest.length < collectionDepth - 1 || dest.length > collectionDepth) {
203
+ throw new FiretenderUsageError(
204
+ "copy() with a path array requires an ID for all collections and subcollections, except optionally the last."
205
+ );
206
+ }
207
+ dest.forEach((id, index) => {
208
+ path[index * 2 + 1] = id;
209
+ });
210
+ ref =
211
+ dest.length === collectionDepth
212
+ ? doc(this.ref.firestore, path[0], ...path.slice(1))
213
+ : collection(this.ref.firestore, path[0], ...path.slice(1));
214
+ } else {
215
+ // For a string or undefined ...
216
+ const collectionRef =
217
+ this.ref instanceof DocumentReference ? this.ref.parent : this.ref;
218
+ if (dest) {
219
+ ref = doc(collectionRef, dest);
220
+ } else {
221
+ ref = collectionRef;
222
+ }
223
+ }
224
+ const mergedOptions: AllFiretenderDocOptions = {
225
+ ...options,
226
+ createDoc: true,
227
+ initialData: this.data,
228
+ };
229
+ return new FiretenderDoc(this.schema, ref, mergedOptions);
230
+ }
231
+
232
+ /**
233
+ * The document's ID string.
234
+ *
235
+ * @throws Throws an error if the document does not yet have an ID.
236
+ */
237
+ get id(): string {
238
+ if (!this.docID) {
239
+ throw new FiretenderUsageError(
240
+ "id can only be accessed after the new doc has been written."
241
+ );
242
+ }
243
+ return this.docID;
244
+ }
245
+
246
+ /**
247
+ * The document's Firestore reference.
248
+ *
249
+ * @throws Throws an error if the document does not yet have a reference.
250
+ */
251
+ get docRef(): DocumentReference {
252
+ if (!(this.ref instanceof DocumentReference)) {
253
+ throw new FiretenderUsageError(
254
+ "docRef can only be accessed after the new doc has been written."
255
+ );
256
+ }
257
+ return this.ref;
258
+ }
259
+
260
+ /**
261
+ * Is this a new doc that has not yet been written to Firestore?
262
+ */
263
+ isNew(): boolean {
264
+ return this.isNewDoc;
265
+ }
266
+
267
+ /**
268
+ * Does the document contain data, either because it was successfully loaded
269
+ * or is newly created?
270
+ */
271
+ isLoaded(): boolean {
272
+ return this.data !== undefined;
273
+ }
274
+
275
+ /**
276
+ * Does this document contain data that has not yet been written to Firestore?
277
+ */
278
+ isPendingWrite(): boolean {
279
+ return this.isSettingNewContents || this.updates.size > 0;
280
+ }
281
+
282
+ /**
283
+ * Loads this document's data from Firestore.
284
+ *
285
+ * @param force force a read from Firestore. Normally load() does nothing if
286
+ * the document already contains data.
287
+ */
288
+ async load(force = false): Promise<this> {
289
+ if (this.isNewDoc || this.ref instanceof CollectionReference) {
290
+ throw new FiretenderUsageError(
291
+ "load() should not be called for new documents."
292
+ );
293
+ }
294
+ if (!this.data || force) {
295
+ if (this.resolvesWaitingForLoad !== undefined) {
296
+ await new Promise<void>((resolve, reject) => {
297
+ assertIsDefined(this.resolvesWaitingForLoad);
298
+ this.resolvesWaitingForLoad.push({ resolve, reject });
299
+ });
300
+ } else {
301
+ this.resolvesWaitingForLoad = [];
302
+ let snapshot: DocumentSnapshot;
303
+ try {
304
+ snapshot = await getDoc(this.ref);
305
+ } catch (error) {
306
+ addContextToError(error, "getDoc", this.ref);
307
+ throw error;
308
+ }
309
+ // DocumentSnapshot.prototype.exists is a boolean for
310
+ // "firebase-admin/firestore" and a function for "firebase/firestore".
311
+ if (
312
+ (typeof snapshot.exists === "boolean" && !snapshot.exists) ||
313
+ (typeof snapshot.exists === "function" && !(snapshot.exists as any)())
314
+ ) {
315
+ const error = new FiretenderIOError(
316
+ `Document does not exist: "${this.ref.path}"`
317
+ );
318
+ this.resolvesWaitingForLoad.forEach((wait) => wait.reject(error));
319
+ throw error;
320
+ }
321
+ this.data = this.schema.parse(snapshot.data());
322
+ // Dereference the old proxy, if any, to force a recapture of data.
323
+ this.dataProxy = undefined;
324
+ this.resolvesWaitingForLoad.forEach((wait) => wait.resolve());
325
+ this.resolvesWaitingForLoad = undefined;
326
+ }
327
+ }
328
+ return this;
329
+ }
330
+
331
+ /**
332
+ * Read-only accessor to the contents of this document.
333
+ */
334
+ get r(): DeepReadonly<z.infer<SchemaType>> {
335
+ if (!this.data) {
336
+ throw new FiretenderUsageError(
337
+ "load() must be called before reading the document."
338
+ );
339
+ }
340
+ return this.data as DeepReadonly<z.infer<SchemaType>>;
341
+ }
342
+
343
+ /**
344
+ * Writable accessor to update the contents of this document.
345
+ *
346
+ * Only use this accessor when making changes to the doc. The .r accessor is
347
+ * considerably more efficient when reading.
348
+ */
349
+ get w(): z.infer<SchemaType> {
350
+ if (this.isSettingNewContents) {
351
+ // No need to monitor changes if we're setting rather than updating.
352
+ return this.data as z.infer<SchemaType>;
353
+ }
354
+ if (!this.dataProxy) {
355
+ if (!this.data) {
356
+ // TODO #23: Consider being able to update a doc without loading it.
357
+ throw new FiretenderUsageError(
358
+ "load() must be called before updating the document."
359
+ );
360
+ }
361
+ this.dataProxy = watchFieldForChanges(
362
+ [],
363
+ this.schema,
364
+ this.data,
365
+ this.addToUpdateList.bind(this)
366
+ );
367
+ }
368
+ return this.dataProxy as z.infer<SchemaType>;
369
+ }
370
+
371
+ /**
372
+ * Writable accessor to overwrite all the document data.
373
+ */
374
+ set w(newData: z.input<SchemaType>) {
375
+ this.data = this.schema.parse(newData);
376
+ this.isSettingNewContents = true;
377
+ this.dataProxy = undefined;
378
+ }
379
+
380
+ /**
381
+ * Writes the document or any updates to Firestore.
382
+ */
383
+ async write(): Promise<this> {
384
+ // For new docs, this.data should contain its initial state.
385
+ if (this.isSettingNewContents) {
386
+ assertIsDefined(this.data);
387
+ if (this.ref instanceof DocumentReference) {
388
+ try {
389
+ await setDoc(this.ref, this.data);
390
+ } catch (error) {
391
+ addContextToError(error, "setDoc", this.ref, this.data);
392
+ throw error;
393
+ }
394
+ } else {
395
+ try {
396
+ this.ref = await addDoc(this.ref, this.data);
397
+ } catch (error: any) {
398
+ addContextToError(error, "addDoc", this.ref, this.data);
399
+ throw error;
400
+ }
401
+ this.docID = this.ref.path.split("/").pop(); // ID is last part of path.
402
+ }
403
+ this.isSettingNewContents = false;
404
+ this.isNewDoc = false;
405
+ }
406
+ // For existing docs, this.updates should contain a list of changes.
407
+ else {
408
+ if (!(this.ref instanceof DocumentReference)) {
409
+ // We should never get here.
410
+ throw new FiretenderInternalError(
411
+ "Internal error. Firetender object should always reference a document when updating an existing doc."
412
+ );
413
+ }
414
+ if (this.updates.size > 0) {
415
+ const updateData = Object.fromEntries(this.updates);
416
+ try {
417
+ await updateDoc(this.ref, updateData);
418
+ } catch (error: any) {
419
+ addContextToError(error, "updateDoc", this.ref, updateData);
420
+ throw error;
421
+ }
422
+ this.updates.clear();
423
+ }
424
+ }
425
+ return this;
426
+ }
427
+
428
+ /**
429
+ * Updates the document's data with a single call.
430
+ *
431
+ * This function loads the document's data, if necessary; calls the given
432
+ * function to make changes to the data; then write the changes to Firestore.
433
+ * If nothing else, it helps you avoid forgetting to call .write()!
434
+ *
435
+ * @param mutator function that accepts a writable data object and makes
436
+ * changes to it.
437
+ */
438
+ async update(mutator: (data: z.infer<SchemaType>) => void): Promise<this> {
439
+ await this.load();
440
+ mutator(this.w);
441
+ await this.write();
442
+ return this;
443
+ }
444
+
445
+ //////////////////////////////////////////////////////////////////////////////
446
+ // Private functions
447
+
448
+ /**
449
+ * Adds a field and its new value to the list of updates to be passed to
450
+ * Firestore's updateDoc(). Called when the proxies detect changes to the
451
+ * document data.
452
+ */
453
+ private addToUpdateList<FieldSchemaType extends z.ZodTypeAny>(
454
+ fieldPath: string[],
455
+ newValue: z.infer<FieldSchemaType>
456
+ ): void {
457
+ let pathString = "";
458
+ if (this.updates.size > 0) {
459
+ // If there is already a list of mutations to send to Firestore, check if
460
+ // a parent of this update is in it. Objects in the update list are
461
+ // references into this.data, so the parent field will automatically
462
+ // reflect this change; no additional Firestore mutation is needed.
463
+ if (
464
+ fieldPath.some((field, i) => {
465
+ pathString = pathString ? `${pathString}.${field}` : field;
466
+ return i < fieldPath.length - 1 && this.updates.has(pathString);
467
+ })
468
+ ) {
469
+ return;
470
+ }
471
+ // Remove any previous updates that this one overwrites.
472
+ this.updates.forEach((value, key) => {
473
+ if (key.startsWith(pathString)) {
474
+ this.updates.delete(key);
475
+ }
476
+ });
477
+ } else {
478
+ // Shortcut for the common case of a single update being made.
479
+ pathString = fieldPath.join(".");
480
+ }
481
+ this.updates.set(pathString, newValue);
482
+ }
483
+ }
package/src/errors.ts ADDED
@@ -0,0 +1,47 @@
1
+ import { CollectionReference, DocumentReference } from "./firestore-deps";
2
+
3
+ export class FiretenderError extends Error {}
4
+
5
+ /**
6
+ * Something went wrong with a Firestore operation, such as trying to load a
7
+ * document that does not exist.
8
+ */
9
+ export class FiretenderIOError extends FiretenderError {}
10
+
11
+ /**
12
+ * The caller did something wrong: a function was called at the wrong time or
13
+ * with the wrong parameters.
14
+ */
15
+ export class FiretenderUsageError extends FiretenderError {}
16
+
17
+ /**
18
+ * Something went wrong internally. These errors indicate a bug in Firetender.
19
+ */
20
+ export class FiretenderInternalError extends FiretenderError {}
21
+
22
+ /**
23
+ * Adds a "firetenderContext" property to the given error.
24
+ *
25
+ * @param error the error to which the context is added. If `error` is not an
26
+ * object, this call does not modify it.
27
+ * @param call the name of the Firestore function in which the error occurred.
28
+ * @param ref the path of the target document or collection, if any.
29
+ * @param data arbitrary data associated with this call.
30
+ */
31
+ export function addContextToError(
32
+ error: any,
33
+ call: string,
34
+ ref?: DocumentReference | CollectionReference,
35
+ data?: any
36
+ ) {
37
+ if (typeof error !== "object") {
38
+ return;
39
+ }
40
+ error.firetenderContext = { call };
41
+ if (ref) {
42
+ error.firetenderContext.ref = ref.path;
43
+ }
44
+ if (data !== undefined) {
45
+ error.firetenderContext.data = data;
46
+ }
47
+ }
@@ -0,0 +1,111 @@
1
+ /**
2
+ * Provides all dependencies normally imported from "firebase/firestore".
3
+ *
4
+ * For web clients, the "firebase/firestore" API is simply re-exported.
5
+ *
6
+ * For the server client API, ...
7
+ */
8
+
9
+ import {
10
+ CollectionGroup,
11
+ CollectionReference,
12
+ DocumentReference,
13
+ DocumentSnapshot,
14
+ FieldValue,
15
+ Filter,
16
+ Firestore,
17
+ Query,
18
+ QuerySnapshot,
19
+ Timestamp,
20
+ UpdateData,
21
+ WithFieldValue,
22
+ } from "@google-cloud/firestore";
23
+
24
+ export const FIRESTORE_DEPS_TYPE: "web" | "admin" = "admin";
25
+
26
+ export {
27
+ CollectionReference,
28
+ DocumentReference,
29
+ DocumentSnapshot,
30
+ Firestore,
31
+ Query,
32
+ QuerySnapshot,
33
+ Timestamp,
34
+ };
35
+
36
+ export type QueryConstraint = Filter;
37
+ export const where = Filter.where;
38
+ export const arrayRemove = FieldValue.arrayRemove;
39
+ export const deleteField = FieldValue.delete;
40
+ export const serverTimestamp = FieldValue.serverTimestamp;
41
+
42
+ export const isServerTimestamp = (x: any) => x instanceof FieldValue;
43
+
44
+ export const addDoc = <T>(
45
+ ref: CollectionReference<T>,
46
+ data: WithFieldValue<T>
47
+ ): Promise<DocumentReference<T>> => ref.add(data);
48
+
49
+ export const collection = (
50
+ firestoreOrRef: Firestore | CollectionReference,
51
+ path: string,
52
+ ...pathSegments: string[]
53
+ ): CollectionReference =>
54
+ firestoreOrRef instanceof Firestore
55
+ ? firestoreOrRef.collection([path, ...pathSegments].join("/"))
56
+ : firestoreOrRef.firestore.collection(
57
+ [firestoreOrRef.path, path, ...pathSegments].join("/")
58
+ );
59
+
60
+ export const collectionGroup = (
61
+ firestore: Firestore,
62
+ collectionID: string
63
+ ): CollectionGroup => firestore.collectionGroup(collectionID);
64
+
65
+ export const deleteDoc = async (
66
+ ref: DocumentReference<unknown>
67
+ ): Promise<void> => {
68
+ await ref.delete();
69
+ };
70
+
71
+ export const doc = (
72
+ firestoreOrRef: Firestore | CollectionReference,
73
+ path?: string,
74
+ ...pathSegments: string[]
75
+ ): DocumentReference =>
76
+ firestoreOrRef instanceof Firestore
77
+ ? firestoreOrRef.doc([path, ...pathSegments].join("/"))
78
+ : path
79
+ ? firestoreOrRef.doc([path, ...pathSegments].join("/"))
80
+ : firestoreOrRef.doc();
81
+
82
+ export const getDoc = <T>(
83
+ ref: DocumentReference<T>
84
+ ): Promise<DocumentSnapshot<T>> => ref.get();
85
+
86
+ export const getDocs = <T>(query: Query<T>): Promise<QuerySnapshot<T>> =>
87
+ query.get();
88
+
89
+ export const query = <T>(
90
+ ref: Query<T>,
91
+ ...queryConstraints: QueryConstraint[]
92
+ ): Query<T> =>
93
+ queryConstraints.length === 0
94
+ ? ref
95
+ : queryConstraints.length === 1
96
+ ? ref.where(queryConstraints[0])
97
+ : ref.where(Filter.and(...queryConstraints));
98
+
99
+ export const setDoc = async <T>(
100
+ ref: DocumentReference<T>,
101
+ data: WithFieldValue<T>
102
+ ): Promise<void> => {
103
+ await ref.set(data);
104
+ };
105
+
106
+ export const updateDoc = async <T>(
107
+ ref: DocumentReference<T>,
108
+ data: UpdateData<T>
109
+ ): Promise<void> => {
110
+ await ref.update(data);
111
+ };
@@ -0,0 +1,14 @@
1
+ /**
2
+ * Provides all dependencies normally imported from "firebase/firestore".
3
+ *
4
+ * For web clients, the "firebase/firestore" API is simply re-exported.
5
+ *
6
+ * For the server client API, ...
7
+ */
8
+
9
+ export * from "firebase/firestore";
10
+
11
+ export const FIRESTORE_DEPS_TYPE: "web" | "admin" = "web";
12
+
13
+ export const isServerTimestamp = (data: any) =>
14
+ data._methodName === "serverTimestamp";