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.
- package/LICENSE +20 -0
- package/README.md +349 -0
- package/dist/FiretenderCollection.d.ts +116 -0
- package/dist/FiretenderCollection.js +225 -0
- package/dist/FiretenderCollection.js.map +1 -0
- package/dist/FiretenderDoc.d.ts +172 -0
- package/dist/FiretenderDoc.js +361 -0
- package/dist/FiretenderDoc.js.map +1 -0
- package/dist/errors.d.ts +30 -0
- package/dist/errors.js +49 -0
- package/dist/errors.js.map +1 -0
- package/dist/firestore-deps-admin.d.ts +26 -0
- package/dist/firestore-deps-admin.js +62 -0
- package/dist/firestore-deps-admin.js.map +1 -0
- package/dist/firestore-deps-web.d.ts +10 -0
- package/dist/firestore-deps-web.js +29 -0
- package/dist/firestore-deps-web.js.map +1 -0
- package/dist/firestore-deps.d.ts +26 -0
- package/dist/firestore-deps.js +62 -0
- package/dist/firestore-deps.js.map +1 -0
- package/dist/index.d.ts +6 -0
- package/dist/index.js +18 -0
- package/dist/index.js.map +1 -0
- package/dist/proxies.d.ts +62 -0
- package/dist/proxies.js +243 -0
- package/dist/proxies.js.map +1 -0
- package/dist/timestamps.d.ts +37 -0
- package/dist/timestamps.js +58 -0
- package/dist/timestamps.js.map +1 -0
- package/dist/ts-helpers.d.ts +14 -0
- package/dist/ts-helpers.js +19 -0
- package/dist/ts-helpers.js.map +1 -0
- package/package.json +72 -0
- package/src/FiretenderCollection.ts +311 -0
- package/src/FiretenderDoc.ts +483 -0
- package/src/errors.ts +47 -0
- package/src/firestore-deps-admin.ts +111 -0
- package/src/firestore-deps-web.ts +14 -0
- package/src/firestore-deps.ts +111 -0
- package/src/index.ts +29 -0
- package/src/proxies.ts +286 -0
- package/src/timestamps.ts +62 -0
- 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
|
+
}
|