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,311 @@
1
+ import { z } from "zod";
2
+
3
+ import { addContextToError, FiretenderUsageError } from "./errors";
4
+ import {
5
+ collection,
6
+ collectionGroup,
7
+ CollectionReference,
8
+ deleteDoc,
9
+ doc,
10
+ DocumentReference,
11
+ Firestore,
12
+ getDocs,
13
+ Query,
14
+ query,
15
+ QueryConstraint,
16
+ QuerySnapshot,
17
+ } from "./firestore-deps";
18
+ import { FiretenderDoc, FiretenderDocOptions } from "./FiretenderDoc";
19
+ import { DeepPartial } from "./ts-helpers";
20
+
21
+ /**
22
+ * A representation of a Firestore collection or subcollection.
23
+ *
24
+ * It represents a given "collection path": the collection names from a document
25
+ * reference, sans IDs. All docs at /databases/{db}/documents/foo/{*}/bar/{*}
26
+ * are covered by a FiretenderCollection for the path ["foo", "bar"].
27
+ */
28
+ export class FiretenderCollection<SchemaType extends z.SomeZodObject> {
29
+ /** Zod schema used to parse and validate the document's data */
30
+ readonly schema: SchemaType;
31
+
32
+ /** Firestore object: the thing you get from getFirestore() */
33
+ readonly firestore: Firestore;
34
+
35
+ /** The collection path of this object: a series of collection names */
36
+ readonly collectionPath: string[];
37
+
38
+ /** Function to return the initial values when creating a new document. */
39
+ readonly baseInitialDataFactory:
40
+ | (() => DeepPartial<z.input<SchemaType>>)
41
+ | undefined;
42
+
43
+ /**
44
+ * @param schema the Zod object schema describing the documents in this
45
+ * collection.
46
+ * @param firestore the thing you get from getFirestore().
47
+ * @param collectionPath the path of this collection in Firestore: the names
48
+ * of any parent collections and of this collection.
49
+ * @param baseInitialData (optional) an object or object factory providing
50
+ * default field values for this collection.
51
+ */
52
+ constructor(
53
+ schema: SchemaType,
54
+ firestore: Firestore,
55
+ collectionPath: [string, ...string[]] | string,
56
+ baseInitialData:
57
+ | (() => DeepPartial<z.input<SchemaType>>)
58
+ | DeepPartial<z.input<SchemaType>>
59
+ | undefined = undefined
60
+ ) {
61
+ this.schema = schema;
62
+ this.firestore = firestore;
63
+ this.collectionPath = [collectionPath].flat();
64
+ if (baseInitialData) {
65
+ if (typeof baseInitialData === "function") {
66
+ this.baseInitialDataFactory = baseInitialData;
67
+ } else {
68
+ this.baseInitialDataFactory = () => baseInitialData;
69
+ }
70
+ }
71
+ }
72
+
73
+ /**
74
+ * Returns a FiretenderDoc representing a new document in this collection.
75
+ *
76
+ * This method initializes the FiretenderDoc but does not create it in
77
+ * Firestore. To do so, call the doc's write() method.
78
+ *
79
+ * @param id the ID or array of IDs giving the path of the new document.
80
+ * Firestore will generate a random doc ID if it is omitted. If this is a
81
+ * subcollection, the ID(s) for the parent collection(s) are required.
82
+ * @param initialData the document's initial data, which is merged with (and
83
+ * potentially overwrites) field values specified in the constructor.
84
+ * @param options optional parameters for the resulting FiretenderDoc; see
85
+ * FiretenderDocOptions for detail.
86
+ */
87
+ newDoc(
88
+ id: string[] | string | undefined = undefined,
89
+ initialData: DeepPartial<z.input<SchemaType>> | undefined = undefined,
90
+ options: FiretenderDocOptions = {}
91
+ ): FiretenderDoc<SchemaType> {
92
+ const ids = id instanceof Array ? id : id ? [id] : [];
93
+ let ref: DocumentReference | CollectionReference | undefined =
94
+ this.makeDocRefInternal(ids);
95
+ if (!ref) {
96
+ ref = this.makeCollectionRefInternal(ids);
97
+ }
98
+ if (!ref) {
99
+ throw new FiretenderUsageError(
100
+ "newDoc() requires an ID path for all collections and subcollections, except optionally the last."
101
+ );
102
+ }
103
+ const data = {};
104
+ if (this.baseInitialDataFactory) {
105
+ Object.assign(data, this.baseInitialDataFactory());
106
+ }
107
+ if (initialData) {
108
+ Object.assign(data, initialData);
109
+ }
110
+ return FiretenderDoc.createNewDoc(this.schema, ref, data, options);
111
+ }
112
+
113
+ /**
114
+ * Returns a FiretenderDoc representing an existing Firestore document in this
115
+ * collection.
116
+ *
117
+ * This method initializes the FiretenderDoc but does not load its data. If
118
+ * the doc does not exist in Firestore, calling load() will throw an error.
119
+ *
120
+ * @param id the ID or array of IDs specifying the desired document.
121
+ * @param options optional parameters for the resulting FiretenderDoc; see
122
+ * FiretenderDocOptions for detail.
123
+ */
124
+ existingDoc(
125
+ id: string[] | string,
126
+ options: FiretenderDocOptions = {}
127
+ ): FiretenderDoc<SchemaType> {
128
+ const ref = this.makeDocRefInternal([id].flat());
129
+ if (!ref) {
130
+ throw new FiretenderUsageError(
131
+ "existingDoc() requires a full ID path for this collection and its parent collections, if any."
132
+ );
133
+ }
134
+ return new FiretenderDoc(this.schema, ref, options);
135
+ }
136
+
137
+ /**
138
+ * Returns an array of all the documents in this collection.
139
+ *
140
+ * If the collection may contain a large number of documents, use query() with
141
+ * the limit() and startAfter() constraints to paginate the results.
142
+ *
143
+ * @param id (optional) when querying a subcollection, the ID(s) of its parent
144
+ * collection(s).
145
+ */
146
+ async getAllDocs(
147
+ id: string[] | string | undefined = undefined
148
+ ): Promise<FiretenderDoc<SchemaType>[]> {
149
+ const ids = id instanceof Array ? id : id ? [id] : [];
150
+ const collectionRef = this.makeCollectionRefInternal(ids);
151
+ if (!collectionRef) {
152
+ throw new FiretenderUsageError(
153
+ "When querying a subcollection, getAllDocs() requires the IDs of all parent collections."
154
+ );
155
+ }
156
+ return this.getAndWrapDocs(collectionRef);
157
+ }
158
+
159
+ /**
160
+ * Returns an array of the documents matching the given query.
161
+ *
162
+ * @param id (optional) when querying a subcollection, the ID(s) of its parent
163
+ * collection(s); omit when querying a top-level collection or when querying
164
+ * all docs in this subcollection, regardless of parent.
165
+ * @param ...whereClauses the where(), limit(), orderBy(), startAfter(), etc.
166
+ * constraints defining this query.
167
+ */
168
+ async query(
169
+ idOrWhereClause: string | string[] | QueryConstraint,
170
+ ...moreWhereClauses: QueryConstraint[]
171
+ ): Promise<FiretenderDoc<SchemaType>[]> {
172
+ let ids: string[];
173
+ let whereClauses: QueryConstraint[];
174
+ if (idOrWhereClause instanceof Array) {
175
+ ids = idOrWhereClause;
176
+ whereClauses = moreWhereClauses;
177
+ } else if (typeof idOrWhereClause === "string") {
178
+ ids = [idOrWhereClause];
179
+ whereClauses = moreWhereClauses;
180
+ } else {
181
+ ids = [];
182
+ whereClauses = [idOrWhereClause, ...moreWhereClauses];
183
+ }
184
+ let ref: CollectionReference | Query | undefined =
185
+ this.makeCollectionRefInternal(ids);
186
+ if (!ref) {
187
+ ref = collectionGroup(
188
+ this.firestore,
189
+ this.collectionPath[this.collectionPath.length - 1]
190
+ );
191
+ }
192
+ return this.getAndWrapDocs(query(ref, ...whereClauses));
193
+ }
194
+
195
+ /**
196
+ * Deletes the given document from this collection.
197
+ *
198
+ * The document's subcollections (if any) are not deleted. To delete them,
199
+ * first use query() to get its subcollection docs, then call delete() on each
200
+ * one. Please note that the Firestore guide recommends only performing such
201
+ * unbounded batched deletions from a trusted server environment.
202
+ *
203
+ * @param id full path ID of the target document.
204
+ */
205
+ async delete(id: string[] | string): Promise<void> {
206
+ const ref = this.makeDocRefInternal([id].flat());
207
+ if (!ref) {
208
+ throw new FiretenderUsageError(
209
+ "delete() requires the full ID path of the target document."
210
+ );
211
+ }
212
+ try {
213
+ await deleteDoc(ref);
214
+ } catch (error) {
215
+ addContextToError(error, "deleteDoc", ref);
216
+ }
217
+ }
218
+
219
+ /**
220
+ * Returns a document reference for the specified ID.
221
+ *
222
+ * @param id the ID or array of IDs specifying the desired document.
223
+ */
224
+ makeDocRef(id: string[] | string): DocumentReference {
225
+ const ids = [id].flat();
226
+ const ref = this.makeDocRefInternal(ids);
227
+ if (!ref) {
228
+ throw new FiretenderUsageError(
229
+ `Document refs for /${this.collectionPath.join("/*")}/* require ${
230
+ this.collectionPath.length
231
+ } document IDs; received ${ids.length}.`
232
+ );
233
+ }
234
+ return ref;
235
+ }
236
+
237
+ /**
238
+ * Returns a collection reference for the specified ID.
239
+ *
240
+ * @param id the ID or array of IDs specifying the desired collection.
241
+ */
242
+ makeCollectionRef(
243
+ id: string[] | string | undefined = undefined
244
+ ): CollectionReference {
245
+ const ids = id ? [id].flat() : [];
246
+ const ref = this.makeCollectionRefInternal(ids);
247
+ if (!ref) {
248
+ throw new FiretenderUsageError(
249
+ `Collection refs for /${this.collectionPath.join("/*")} require ${
250
+ this.collectionPath.length - 1
251
+ } document IDs; received ${ids.length}.`
252
+ );
253
+ }
254
+ return ref;
255
+ }
256
+
257
+ //////////////////////////////////////////////////////////////////////////////
258
+ // Private functions
259
+
260
+ /**
261
+ * Builds a doc ref from the given IDs, or returns "undefined" if the IDs do
262
+ * not correctly specify a doc path.
263
+ */
264
+ private makeDocRefInternal(ids: string[]): DocumentReference | undefined {
265
+ if (ids.length !== this.collectionPath.length) {
266
+ return undefined;
267
+ }
268
+ const path = ids.flatMap((id, i) => [this.collectionPath[i], id]);
269
+ return doc(this.firestore, path[0], ...path.slice(1));
270
+ }
271
+
272
+ /**
273
+ * Builds a collection ref from the given IDs, or returns "undefined" if the
274
+ * IDs do not correctly specify a collection path.
275
+ */
276
+ private makeCollectionRefInternal(
277
+ ids: string[]
278
+ ): CollectionReference | undefined {
279
+ if (ids.length !== this.collectionPath.length - 1) {
280
+ return undefined;
281
+ }
282
+ const subPath = ids.flatMap((id, i) => [id, this.collectionPath[i + 1]]);
283
+ return collection(this.firestore, this.collectionPath[0], ...subPath);
284
+ }
285
+
286
+ /**
287
+ * Executes the given query and returns an array of the results, wrapped in
288
+ * FiretenderDoc objects.
289
+ */
290
+ private async getAndWrapDocs(
291
+ query: CollectionReference | Query
292
+ ): Promise<FiretenderDoc<SchemaType>[]> {
293
+ let querySnapshot: QuerySnapshot;
294
+ try {
295
+ querySnapshot = await getDocs(query);
296
+ } catch (error) {
297
+ addContextToError(
298
+ error,
299
+ "getDocs",
300
+ query instanceof CollectionReference ? query : undefined
301
+ );
302
+ throw error;
303
+ }
304
+ return querySnapshot.docs.map(
305
+ (queryDoc) =>
306
+ new FiretenderDoc(this.schema, queryDoc.ref, {
307
+ initialData: queryDoc.data(),
308
+ })
309
+ );
310
+ }
311
+ }