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
package/LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ # MIT License
2
+
3
+ Copyright 2022 Jacob M. Hartman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy of
6
+ this software and associated documentation files (the "Software"), to deal in
7
+ the Software without restriction, including without limitation the rights to
8
+ use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9
+ the Software, and to permit persons to whom the Software is furnished to do so,
10
+ subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17
+ FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18
+ COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19
+ IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20
+ CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,349 @@
1
+ # Firetender
2
+
3
+ Firetender takes your [Zod](https://github.com/colinhacks/zod) data schema ...
4
+
5
+ ```javascript
6
+ const itemSchema = z.object({
7
+ description: z.string().optional(),
8
+ count: z.number().nonnegative().integer(),
9
+ tags: z.array(z.string()).default([]),
10
+ });
11
+ ```
12
+
13
+ associates it with a Firestore collection ...
14
+
15
+ ```javascript
16
+ const itemCollection = new FiretenderCollection(itemSchema, db, "items");
17
+ ```
18
+
19
+ and provides you with typesafe, validated Firestore documents that are easy to
20
+ use and understand.
21
+
22
+ ```javascript
23
+ // Add a document to the collection.
24
+ await itemCollection.newDoc("foo", { count: 0, tags: ["needs +1"] }).write();
25
+
26
+ // Read the document "bar", then update it.
27
+ const itemDoc = await itemCollection.existingDoc("bar").load();
28
+ const count = itemDoc.r.count;
29
+ await itemDoc.update((item) => {
30
+ item.tags.push("needs +1");
31
+ });
32
+
33
+ // Increment the count of all docs with a "needs +1" tag.
34
+ await Promise.all(
35
+ itemCollection
36
+ .query(where("tags", "array-contains", "needs +1"))
37
+ .map((itemDoc) =>
38
+ itemDoc.update((item) => {
39
+ item.count += 1;
40
+ delete item.tags["needs +1"];
41
+ })
42
+ )
43
+ );
44
+ ```
45
+
46
+ Changes to the document data are monitored, and only modified fields are updated
47
+ on Firestore.
48
+
49
+ ## Usage
50
+
51
+ To illustrate in more detail, let's run through the basics of defining,
52
+ creating, modifying, and copying a Firestore document.
53
+
54
+ ### Initialize Cloud Firestore
55
+
56
+ The first step is the usual Firestore configuration and initialization. See
57
+ the [Firestore
58
+ quickstart](https://firebase.google.com/docs/firestore/quickstart) for details.
59
+
60
+ ```javascript
61
+ import { doc, initializeApp } from "firebase/app";
62
+ import { getFirestore } from "firebase/firestore";
63
+
64
+ // TODO: Replace the following with your app's Firebase project configuration.
65
+ // See: https://firebase.google.com/docs/web/learn-more#config-object
66
+ const firebaseConfig = {
67
+ // ...
68
+ };
69
+
70
+ const app = initializeApp(firebaseConfig);
71
+ const firestore = getFirestore(app);
72
+ ```
73
+
74
+ ### Define a collection and its schema
75
+
76
+ Firetender uses [Zod](https://github.com/colinhacks/zod) to define the schema
77
+ and validation rules for a collection's documents; if you've used Joi or Yup,
78
+ you will find Zod very similar. In the example below, I've defined a schema for
79
+ types of pizza. I was a little hungry when I wrote this.
80
+
81
+ ```javascript
82
+ import {
83
+ FiretenderCollection,
84
+ nowTimestamp,
85
+ timestampSchema
86
+ } from "firetender";
87
+ import { z } from "zod";
88
+
89
+ const pizzaSchema = z.object({
90
+ name: z.string(),
91
+ description: z.string().optional(),
92
+ creationTime: timestampSchema,
93
+ toppings: z.record(
94
+ z.string(),
95
+ z.object({
96
+ isIncluded: z.boolean().default(true),
97
+ surcharge: z.number().positive().optional(),
98
+ placement: z.enum(["left", "right", "entire"]).default("entire"),
99
+ })
100
+ .refine((topping) => topping.isIncluded || topping.surcharge, {
101
+ message: "Toppings that are not included must have a surcharge.",
102
+ path: ["surcharge"],
103
+ })
104
+ ),
105
+ basePrice: z.number().optional(),
106
+ tags: z.array(z.string()).default([]),
107
+ });
108
+
109
+ const pizzaCollection = new FiretenderCollection(
110
+ pizzaSchema,
111
+ firestore,
112
+ "pizzas",
113
+ { creationTime: nowTimestamp() }
114
+ );
115
+ ```
116
+
117
+ Optional records and arrays should typically use `.default()` to provide an
118
+ empty instances when missing. That isn't required, but it makes accessing these
119
+ fields simpler because they will always be defined. The downside is that empty
120
+ fields are not pruned and will appear in Firestore.
121
+
122
+ ### Add a document
123
+
124
+ Let's add a document to the `pizzas` collection, with an ID of `margherita`. We
125
+ use the collection's `.newDoc()` to produce a `FiretenderDoc` representing a new
126
+ document, initialized with validated data. This object is purely local until it
127
+ is written to Firestore by calling `.write()`. Don't forget to do that.
128
+
129
+ ```javascript
130
+ const docRef = doc(db, "pizzas", "margherita");
131
+ const pizza = pizzaFactory.newDoc(docRef, {
132
+ name: "Margherita",
133
+ description: "Neapolitan style pizza"
134
+ toppings: { "fresh mozzarella": {}, "fresh basil": {} },
135
+ tags: ["traditional"],
136
+ });
137
+ await pizza.write();
138
+ ```
139
+
140
+ If you don't care about the doc ID, pass a collection reference to `.newDoc()`
141
+ and Firestore will assign an ID at random. This ID can be read from `.id` or
142
+ `.docRef` after the document has been written.
143
+
144
+ ### Read and modify a document
145
+
146
+ To access an existing document, pass its reference to the collection's
147
+ `.existingDoc()` method. To read it, call `.load()` and access its data with
148
+ the `.r` property; see the example below. To make changes, use `.w` then call
149
+ `.write()`. Reading and updating can be done in combination:
150
+
151
+ ```javascript
152
+ const meats = ["pepperoni", "chicken", "sausage"];
153
+ const pizza = await pizzaCollection.existingDoc(docRef).load();
154
+ const isMeatIncluded = Object.entries(pizza.r.toppings).some(
155
+ ([name, topping]) => topping.isIncluded && name in meats
156
+ );
157
+ if (!isMeatIncluded) {
158
+ pizza.w.toppings.tags.push("vegetarian");
159
+ }
160
+ await pizza.write();
161
+ ```
162
+
163
+ The `.r` and `.w` properties point to the same data, with the read-only accessor
164
+ typed accordingly. Reading from `.r` is more efficient, as `.w` builds a chain
165
+ of proxies to track updates.
166
+
167
+ ### Update a document in a single call
168
+
169
+ The `.update()` method is a convenience method to load and update a document.
170
+ It allows a slightly cleaner implementation of the above example --- and saves
171
+ you from forgetting to call `.write()`!
172
+
173
+ ```javascript
174
+ const meats = ["pepperoni", "chicken", "sausage"];
175
+ await pizzaCollection.existingDoc(docRef).update((pizza) => {
176
+ const isMeatIncluded = Object.entries(pizza.r.toppings).some(
177
+ ([name, topping]) => topping.isIncluded && name in meats
178
+ );
179
+ if (!isMeatIncluded) {
180
+ pizza.w.toppings.tags.push("vegetarian");
181
+ }
182
+ });
183
+ ```
184
+
185
+ ### Make a copy
186
+
187
+ Finally, use `.copy()` to get a deep copy of the document. If an ID is not
188
+ specified, it will be assigned randomly when the new doc is added to Firestore.
189
+ The copy is solely local until `.write()` is called.
190
+
191
+ ```javascript
192
+ const sourceRef = doc(db, "pizza", "margherita");
193
+ const sourcePizza = await pizzaCollection.existingDoc(sourceRef).load();
194
+ const newPizza = sourcePizza.copy("meaty margh");
195
+ newPizza.name = "Meaty Margh";
196
+ newPizza.toppings.sausage = {};
197
+ newPizza.toppings.pepperoni = { included: false, surcharge: 1.25 };
198
+ newPizza.toppings.chicken = { included: false, surcharge: 1.50 };
199
+ delete newPizza.description;
200
+ delete newPizza.toppings["fresh basil"];
201
+ delete newPizza.tags.vegetarian;
202
+ newPizza.write();
203
+ ```
204
+
205
+ Note the use of the `delete` operator to remove optional fields and record and
206
+ array items.
207
+
208
+ ### Get all docs in a collection
209
+
210
+ You can retrieve all the documents in a collection or subcollection:
211
+
212
+ ```javascript
213
+ const docs = await pizzaCollection().getAllDocs();
214
+ ```
215
+
216
+ `docs` will contain an array of `FiretenderDoc` objects for all entries in the
217
+ pizzas collection. To get the contents of a subcollection, provide the ID(s) of
218
+ its parent collection (and subcollections) to `getAllDocs()`.
219
+
220
+ ### Query a collection or subcollection
221
+
222
+ To query a collection, call `query()` and pass in `where` clauses. The
223
+ [Firestore how-to
224
+ guide](https://firebase.google.com/docs/firestore/query-data/queries) provides
225
+ many examples of simple and compound queries.
226
+
227
+ ```javascript
228
+ const veggieOptions = await pizzaCollection.query(
229
+ where("tags", "array-contains", "vegetarian")
230
+ );
231
+ const cheapClassics = await pizzaCollection.query(
232
+ where("baseprice", "<=", 10),
233
+ where("tags", "array-contains", "traditional")
234
+ );
235
+ ```
236
+
237
+ To query a specific subcollection, provide the ID(s) of its parent collection
238
+ (and subcollections) as the first argument of `query()`.
239
+
240
+ To perform a collection group query across all instances of a particular
241
+ subcollection, leave out the IDs. From the [Firestore how-to
242
+ example](https://firebase.google.com/docs/firestore/query-data/queries#collection-group-query),
243
+ you could retrieve all parks from all cities with this query:
244
+
245
+ ```javascript
246
+ const cityLandmarkSchema = z.object({
247
+ name: z.string(),
248
+ type: z.string(),
249
+ });
250
+ const cityLandmarkCollection = new FiretenderCollection(
251
+ cityLandmarkSchema,
252
+ [firestore, "cities", "landmarks"],
253
+ {}
254
+ );
255
+
256
+ const beijingParks = await cityLandmarkCollection.query(
257
+ "BJ",
258
+ where("type", "==", "park"))
259
+ );
260
+ // Resulting array contains the document for Jingshan Park.
261
+
262
+ const allParks = await cityLandmarkCollection.query(
263
+ where("type", "==", "park")
264
+ );
265
+ // Resulting array has docs for Griffith Park, Ueno Park, and Jingshan Park.
266
+ ```
267
+
268
+ ### Delete a document
269
+
270
+ To delete a document from the cities example:
271
+
272
+ ```javascript
273
+ const citySchema = z.object({ /* ... */ });
274
+ const cityCollection = new FiretenderCollection(
275
+ citySchema, [firestore, "cities"], {}
276
+ );
277
+ await cityCollection.delete("LA");
278
+ ```
279
+
280
+ Subcollections are not deleted; in this example, the LA landmark docs would
281
+ remain. To also delete a document's subcollections, use `query()` to get lists
282
+ of its subcollections' docs, then call `delete()` on each doc. The Firestore
283
+ guide recommends only performing such unbounded batched deletions from a trusted
284
+ server environment.
285
+
286
+ ### Update all matching documents
287
+
288
+ In an inventory of items, markup by 10% all items awaiting a price increase.
289
+
290
+ ```javascript
291
+ const itemSchema = z.object({
292
+ name: z.string(),
293
+ price: z.number().nonnegative(),
294
+ tags: z.array(z.string()),
295
+ });
296
+ const inventoryCollection = new FiretenderCollection(itemSchema, [
297
+ firestore,
298
+ "inventory",
299
+ ]);
300
+
301
+ await Promise.all(
302
+ inventoryCollection
303
+ .query(where("tags", "array-contains", "awaiting-price-increase"))
304
+ .map((itemDoc) =>
305
+ itemDoc.update((data) => {
306
+ data.price *= 1.1;
307
+ delete data.tags["awaiting-price-increase"];
308
+ })
309
+ )
310
+ );
311
+ ```
312
+
313
+ ## TODO
314
+
315
+ The [full list of issues](https://github.com/jakes-space/firetender/issues) is
316
+ tracked on Github. Here are some features on the roadmap:
317
+
318
+ * Documentation
319
+ * Compile JSDoc to an API reference page in markdown.
320
+ ([#13](https://github.com/jakes-space/firetender/issues/13))
321
+ * Concurrency
322
+ * Listen for changes and update the object if it has not been locally
323
+ modified. Provide an onChange() callback option.
324
+ ([#14](https://github.com/jakes-space/firetender/issues/14))
325
+ * Support the Firestore transaction API.
326
+ ([#15](https://github.com/jakes-space/firetender/issues/15))
327
+ * Improved timestamp handling, tests ([multiple
328
+ issues](https://github.com/jakes-space/firetender/issues?q=timestamp))
329
+
330
+ ## Alternatives
331
+
332
+ This project is not stable yet. If you're looking for a more mature Firestore
333
+ helper, check out:
334
+
335
+ * [Vuefire](https://github.com/vuejs/vuefire) and
336
+ [Reactfire](https://github.com/FirebaseExtended/reactfire) for integration
337
+ with their respective frameworks.
338
+
339
+ * [Fireschema](https://github.com/yarnaimo/fireschema): Another strongly typed
340
+ framework for building and using schemas in Firestore.
341
+
342
+ * [firestore-fp](https://github.com/mobily/firestore-fp): If you like functional
343
+ programming.
344
+
345
+ * [simplyfire](https://github.com/coturiv/simplyfire): Another
346
+ simplified API that is focused more on querying. (And kudos to the author for
347
+ its great name.)
348
+
349
+ I'm sure there are many more, and apologies if I missed your favorite.
@@ -0,0 +1,116 @@
1
+ import { z } from "zod";
2
+ import { CollectionReference, DocumentReference, Firestore, QueryConstraint } from "./firestore-deps";
3
+ import { FiretenderDoc, FiretenderDocOptions } from "./FiretenderDoc";
4
+ import { DeepPartial } from "./ts-helpers";
5
+ /**
6
+ * A representation of a Firestore collection or subcollection.
7
+ *
8
+ * It represents a given "collection path": the collection names from a document
9
+ * reference, sans IDs. All docs at /databases/{db}/documents/foo/{*}/bar/{*}
10
+ * are covered by a FiretenderCollection for the path ["foo", "bar"].
11
+ */
12
+ export declare class FiretenderCollection<SchemaType extends z.SomeZodObject> {
13
+ /** Zod schema used to parse and validate the document's data */
14
+ readonly schema: SchemaType;
15
+ /** Firestore object: the thing you get from getFirestore() */
16
+ readonly firestore: Firestore;
17
+ /** The collection path of this object: a series of collection names */
18
+ readonly collectionPath: string[];
19
+ /** Function to return the initial values when creating a new document. */
20
+ readonly baseInitialDataFactory: (() => DeepPartial<z.input<SchemaType>>) | undefined;
21
+ /**
22
+ * @param schema the Zod object schema describing the documents in this
23
+ * collection.
24
+ * @param firestore the thing you get from getFirestore().
25
+ * @param collectionPath the path of this collection in Firestore: the names
26
+ * of any parent collections and of this collection.
27
+ * @param baseInitialData (optional) an object or object factory providing
28
+ * default field values for this collection.
29
+ */
30
+ constructor(schema: SchemaType, firestore: Firestore, collectionPath: [string, ...string[]] | string, baseInitialData?: (() => DeepPartial<z.input<SchemaType>>) | DeepPartial<z.input<SchemaType>> | undefined);
31
+ /**
32
+ * Returns a FiretenderDoc representing a new document in this collection.
33
+ *
34
+ * This method initializes the FiretenderDoc but does not create it in
35
+ * Firestore. To do so, call the doc's write() method.
36
+ *
37
+ * @param id the ID or array of IDs giving the path of the new document.
38
+ * Firestore will generate a random doc ID if it is omitted. If this is a
39
+ * subcollection, the ID(s) for the parent collection(s) are required.
40
+ * @param initialData the document's initial data, which is merged with (and
41
+ * potentially overwrites) field values specified in the constructor.
42
+ * @param options optional parameters for the resulting FiretenderDoc; see
43
+ * FiretenderDocOptions for detail.
44
+ */
45
+ newDoc(id?: string[] | string | undefined, initialData?: DeepPartial<z.input<SchemaType>> | undefined, options?: FiretenderDocOptions): FiretenderDoc<SchemaType>;
46
+ /**
47
+ * Returns a FiretenderDoc representing an existing Firestore document in this
48
+ * collection.
49
+ *
50
+ * This method initializes the FiretenderDoc but does not load its data. If
51
+ * the doc does not exist in Firestore, calling load() will throw an error.
52
+ *
53
+ * @param id the ID or array of IDs specifying the desired document.
54
+ * @param options optional parameters for the resulting FiretenderDoc; see
55
+ * FiretenderDocOptions for detail.
56
+ */
57
+ existingDoc(id: string[] | string, options?: FiretenderDocOptions): FiretenderDoc<SchemaType>;
58
+ /**
59
+ * Returns an array of all the documents in this collection.
60
+ *
61
+ * If the collection may contain a large number of documents, use query() with
62
+ * the limit() and startAfter() constraints to paginate the results.
63
+ *
64
+ * @param id (optional) when querying a subcollection, the ID(s) of its parent
65
+ * collection(s).
66
+ */
67
+ getAllDocs(id?: string[] | string | undefined): Promise<FiretenderDoc<SchemaType>[]>;
68
+ /**
69
+ * Returns an array of the documents matching the given query.
70
+ *
71
+ * @param id (optional) when querying a subcollection, the ID(s) of its parent
72
+ * collection(s); omit when querying a top-level collection or when querying
73
+ * all docs in this subcollection, regardless of parent.
74
+ * @param ...whereClauses the where(), limit(), orderBy(), startAfter(), etc.
75
+ * constraints defining this query.
76
+ */
77
+ query(idOrWhereClause: string | string[] | QueryConstraint, ...moreWhereClauses: QueryConstraint[]): Promise<FiretenderDoc<SchemaType>[]>;
78
+ /**
79
+ * Deletes the given document from this collection.
80
+ *
81
+ * The document's subcollections (if any) are not deleted. To delete them,
82
+ * first use query() to get its subcollection docs, then call delete() on each
83
+ * one. Please note that the Firestore guide recommends only performing such
84
+ * unbounded batched deletions from a trusted server environment.
85
+ *
86
+ * @param id full path ID of the target document.
87
+ */
88
+ delete(id: string[] | string): Promise<void>;
89
+ /**
90
+ * Returns a document reference for the specified ID.
91
+ *
92
+ * @param id the ID or array of IDs specifying the desired document.
93
+ */
94
+ makeDocRef(id: string[] | string): DocumentReference;
95
+ /**
96
+ * Returns a collection reference for the specified ID.
97
+ *
98
+ * @param id the ID or array of IDs specifying the desired collection.
99
+ */
100
+ makeCollectionRef(id?: string[] | string | undefined): CollectionReference;
101
+ /**
102
+ * Builds a doc ref from the given IDs, or returns "undefined" if the IDs do
103
+ * not correctly specify a doc path.
104
+ */
105
+ private makeDocRefInternal;
106
+ /**
107
+ * Builds a collection ref from the given IDs, or returns "undefined" if the
108
+ * IDs do not correctly specify a collection path.
109
+ */
110
+ private makeCollectionRefInternal;
111
+ /**
112
+ * Executes the given query and returns an array of the results, wrapped in
113
+ * FiretenderDoc objects.
114
+ */
115
+ private getAndWrapDocs;
116
+ }