firestore-schema-kit 1.0.0 → 2.0.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/operations.js DELETED
@@ -1,419 +0,0 @@
1
- import {
2
- collection,
3
- doc,
4
- addDoc,
5
- setDoc as _setDoc,
6
- updateDoc as _updateDoc,
7
- deleteDoc as _deleteDoc,
8
- getDoc as _getDoc,
9
- getDocs as _getDocs,
10
- onSnapshot as _onSnapshot,
11
- query,
12
- writeBatch,
13
- runTransaction as _runTransaction,
14
- serverTimestamp,
15
- Timestamp,
16
- } from 'firebase/firestore'
17
-
18
- // ─── Helpers ──────────────────────────────────────────────────────────────────
19
-
20
- /**
21
- * Validate that `schema` is a CollectionSchema produced by defineCollection().
22
- */
23
- function assertSchema(schema, opName) {
24
- if (!schema || schema._type !== 'CollectionSchema') {
25
- throw new TypeError(
26
- `${opName}: first argument must be a CollectionSchema from defineCollection(). Got: ${typeof schema}`
27
- )
28
- }
29
- }
30
-
31
- /**
32
- * Resolve a string ID or an existing DocumentReference into a DocumentReference.
33
- */
34
- function resolveDocRef(db, schema, idOrRef) {
35
- if (typeof idOrRef === 'string') {
36
- return doc(db, schema._name, idOrRef)
37
- }
38
- // Already a DocumentReference
39
- if (idOrRef && typeof idOrRef.path === 'string') {
40
- return idOrRef
41
- }
42
- throw new TypeError(
43
- `Expected a document ID (string) or DocumentReference, got: ${typeof idOrRef}`
44
- )
45
- }
46
-
47
- /**
48
- * Get a CollectionReference for the schema.
49
- */
50
- function collectionRef(db, schema) {
51
- return collection(db, schema._name)
52
- }
53
-
54
- /**
55
- * Map a Firestore DocumentSnapshot → plain data object with `id` field attached.
56
- * Returns null if the document doesn't exist.
57
- */
58
- function docToData(snapshot) {
59
- if (!snapshot.exists()) return null
60
- return { id: snapshot.id, ...snapshot.data() }
61
- }
62
-
63
- /**
64
- * Map a QuerySnapshot → array of plain data objects with `id` field attached.
65
- */
66
- function snapshotToArray(querySnapshot) {
67
- return querySnapshot.docs.map((d) => ({ id: d.id, ...d.data() }))
68
- }
69
-
70
- // ─── Factory ─────────────────────────────────────────────────────────────────
71
- // All operations are created via initFirestoreSchema(db) so db doesn't need
72
- // to be passed on every call.
73
-
74
- export function initFirestoreSchema(db) {
75
- if (!db) throw new Error('initFirestoreSchema: db (Firestore instance) is required')
76
-
77
- // ── createDoc ─────────────────────────────────────────────────────────────
78
- /**
79
- * Add a new document with an auto-generated ID.
80
- * Equivalent to Firestore's addDoc().
81
- *
82
- * @returns {Promise<DocumentReference>}
83
- *
84
- * @example
85
- * const ref = await createDoc(usersSchema, {
86
- * name: 'Alice',
87
- * email: 'alice@example.com',
88
- * createdAt: serverTimestamp(),
89
- * })
90
- * console.log(ref.id) // auto-generated ID
91
- */
92
- async function createDoc(schema, data) {
93
- assertSchema(schema, 'createDoc')
94
- const validated = schema.validate(data)
95
- return addDoc(collectionRef(db, schema), validated)
96
- }
97
-
98
- // ── setDoc ────────────────────────────────────────────────────────────────
99
- /**
100
- * Write a document with a specific ID (overwrites by default).
101
- * Pass { merge: true } to merge instead of overwrite.
102
- * Equivalent to Firestore's setDoc().
103
- *
104
- * @param {CollectionSchema} schema
105
- * @param {string | DocumentReference} idOrRef
106
- * @param {object} data
107
- * @param {{ merge?: boolean, mergeFields?: string[] }} [options]
108
- *
109
- * @example
110
- * await setDoc(usersSchema, 'user-123', { name: 'Alice', email: 'alice@example.com', createdAt: serverTimestamp() })
111
- * await setDoc(usersSchema, 'user-123', { name: 'Alice Updated' }, { merge: true })
112
- */
113
- async function setDoc(schema, idOrRef, data, options = {}) {
114
- assertSchema(schema, 'setDoc')
115
- const ref = resolveDocRef(db, schema, idOrRef)
116
- // merge/mergeFields: only validate fields present in data (partial)
117
- const validated = (options.merge || options.mergeFields)
118
- ? schema.validatePartial(data)
119
- : schema.validate(data)
120
- return _setDoc(ref, validated, options)
121
- }
122
-
123
- // ── updateDoc ─────────────────────────────────────────────────────────────
124
- /**
125
- * Partially update a document — only fields provided will be written.
126
- * The document must already exist.
127
- * Equivalent to Firestore's updateDoc().
128
- *
129
- * @example
130
- * await updateDoc(usersSchema, 'user-123', { name: 'Alice Updated' })
131
- */
132
- async function updateDoc(schema, idOrRef, data) {
133
- assertSchema(schema, 'updateDoc')
134
- const ref = resolveDocRef(db, schema, idOrRef)
135
- const validated = schema.validatePartial(data)
136
- return _updateDoc(ref, validated)
137
- }
138
-
139
- // ── deleteDoc ─────────────────────────────────────────────────────────────
140
- /**
141
- * Delete a document by ID or ref.
142
- * No schema validation needed — it's just a deletion.
143
- *
144
- * @example
145
- * await deleteDoc(usersSchema, 'user-123')
146
- */
147
- async function deleteDoc(schema, idOrRef) {
148
- assertSchema(schema, 'deleteDoc')
149
- const ref = resolveDocRef(db, schema, idOrRef)
150
- return _deleteDoc(ref)
151
- }
152
-
153
- // ── getDoc ────────────────────────────────────────────────────────────────
154
- /**
155
- * Fetch a single document by ID or ref.
156
- * Returns { id, ...data } or null if not found.
157
- *
158
- * @returns {Promise<{ id: string } & T | null>}
159
- *
160
- * @example
161
- * const user = await getDoc(usersSchema, 'user-123')
162
- * if (!user) console.log('Not found')
163
- */
164
- async function getDoc(schema, idOrRef) {
165
- assertSchema(schema, 'getDoc')
166
- const ref = resolveDocRef(db, schema, idOrRef)
167
- const snapshot = await _getDoc(ref)
168
- return docToData(snapshot)
169
- }
170
-
171
- // ── getDocs ───────────────────────────────────────────────────────────────
172
- /**
173
- * Fetch multiple documents from a collection, with optional query constraints.
174
- * Returns an array of { id, ...data } objects.
175
- *
176
- * @param {CollectionSchema} schema
177
- * @param {...QueryConstraint} constraints - where(), orderBy(), limit(), startAfter(), etc.
178
- * @returns {Promise<Array<{ id: string } & T>>}
179
- *
180
- * @example
181
- * import { where, orderBy, limit } from 'firebase/firestore'
182
- *
183
- * const users = await getDocs(usersSchema)
184
- * const admins = await getDocs(usersSchema, where('role', '==', 'admin'))
185
- * const recent = await getDocs(usersSchema, orderBy('createdAt', 'desc'), limit(10))
186
- */
187
- async function getDocs(schema, ...constraints) {
188
- assertSchema(schema, 'getDocs')
189
- const ref = collectionRef(db, schema)
190
- const q = constraints.length ? query(ref, ...constraints) : ref
191
- const snapshot = await _getDocs(q)
192
- return snapshotToArray(snapshot)
193
- }
194
-
195
- // ── getDocRef ─────────────────────────────────────────────────────────────
196
- /**
197
- * Get a Firestore DocumentReference for a given schema and ID.
198
- * Useful when you need the ref itself (e.g. for storing references).
199
- *
200
- * @example
201
- * const ref = getDocRef(usersSchema, 'user-123')
202
- * await setDoc(postsSchema, 'post-1', { author: ref, ... })
203
- */
204
- function getDocRef(schema, id) {
205
- assertSchema(schema, 'getDocRef')
206
- return doc(db, schema._name, id)
207
- }
208
-
209
- /**
210
- * Get a Firestore CollectionReference for a schema.
211
- *
212
- * @example
213
- * const ref = getCollectionRef(usersSchema)
214
- */
215
- function getCollectionRef(schema) {
216
- assertSchema(schema, 'getCollectionRef')
217
- return collectionRef(db, schema)
218
- }
219
-
220
- // ── onDocSnapshot ─────────────────────────────────────────────────────────
221
- /**
222
- * Listen to real-time updates on a single document.
223
- * Returns an unsubscribe function.
224
- *
225
- * @param {CollectionSchema} schema
226
- * @param {string | DocumentReference} idOrRef
227
- * @param {(data: T | null) => void} callback - called with { id, ...data } or null
228
- * @param {(error: Error) => void} [onError]
229
- * @returns {Unsubscribe}
230
- *
231
- * @example
232
- * const unsub = onDocSnapshot(usersSchema, 'user-123', (user) => {
233
- * if (user) console.log(user.name)
234
- * })
235
- * // Later: unsub()
236
- */
237
- function onDocSnapshot(schema, idOrRef, callback, onError) {
238
- assertSchema(schema, 'onDocSnapshot')
239
- const ref = resolveDocRef(db, schema, idOrRef)
240
- return _onSnapshot(
241
- ref,
242
- (snapshot) => callback(docToData(snapshot)),
243
- onError
244
- )
245
- }
246
-
247
- // ── onCollectionSnapshot ──────────────────────────────────────────────────
248
- /**
249
- * Listen to real-time updates on a collection or query.
250
- * Returns an unsubscribe function.
251
- *
252
- * @param {CollectionSchema} schema
253
- * @param {(docs: Array<T>) => void} callback - called with array of { id, ...data }
254
- * @param {...QueryConstraint} constraints - optional where(), orderBy(), limit(), etc.
255
- * @returns {Unsubscribe}
256
- *
257
- * @example
258
- * const unsub = onCollectionSnapshot(
259
- * usersSchema,
260
- * (users) => console.log(users),
261
- * where('role', '==', 'admin'),
262
- * orderBy('name')
263
- * )
264
- * // Later: unsub()
265
- */
266
- function onCollectionSnapshot(schema, callback, ...constraints) {
267
- assertSchema(schema, 'onCollectionSnapshot')
268
- const ref = collectionRef(db, schema)
269
- const q = constraints.length ? query(ref, ...constraints) : ref
270
- return _onSnapshot(
271
- q,
272
- (snapshot) => callback(snapshotToArray(snapshot)),
273
- )
274
- }
275
-
276
- // ── Batch Writes ──────────────────────────────────────────────────────────
277
- /**
278
- * Create a schema-aware write batch.
279
- * All operations are validated before being added to the batch.
280
- *
281
- * @example
282
- * const batch = createBatch()
283
- *
284
- * batch.create(usersSchema, { name: 'Alice', email: 'alice@example.com', createdAt: serverTimestamp() })
285
- * batch.set(usersSchema, 'user-456', { name: 'Bob', email: 'bob@example.com', createdAt: serverTimestamp() })
286
- * batch.update(usersSchema, 'user-123', { name: 'Alice Updated' })
287
- * batch.delete(usersSchema, 'user-789')
288
- *
289
- * await batch.commit()
290
- */
291
- function createBatch() {
292
- const batch = writeBatch(db)
293
-
294
- return {
295
- /**
296
- * Add a new document with auto-generated ID to the batch.
297
- * Note: Firestore's batch doesn't support addDoc — this uses doc() with a generated ref.
298
- */
299
- create(schema, data) {
300
- assertSchema(schema, 'batch.create')
301
- const validated = schema.validate(data)
302
- const ref = doc(collectionRef(db, schema))
303
- batch.set(ref, validated)
304
- return ref // return ref so caller can use the ID
305
- },
306
-
307
- /** Set (overwrite) a document in the batch */
308
- set(schema, idOrRef, data, options = {}) {
309
- assertSchema(schema, 'batch.set')
310
- const ref = resolveDocRef(db, schema, idOrRef)
311
- const validated = (options.merge || options.mergeFields)
312
- ? schema.validatePartial(data)
313
- : schema.validate(data)
314
- batch.set(ref, validated, options)
315
- return this
316
- },
317
-
318
- /** Partially update a document in the batch */
319
- update(schema, idOrRef, data) {
320
- assertSchema(schema, 'batch.update')
321
- const ref = resolveDocRef(db, schema, idOrRef)
322
- const validated = schema.validatePartial(data)
323
- batch.update(ref, validated)
324
- return this
325
- },
326
-
327
- /** Delete a document in the batch */
328
- delete(schema, idOrRef) {
329
- assertSchema(schema, 'batch.delete')
330
- const ref = resolveDocRef(db, schema, idOrRef)
331
- batch.delete(ref)
332
- return this
333
- },
334
-
335
- /** Commit all batched operations */
336
- commit() {
337
- return batch.commit()
338
- },
339
- }
340
- }
341
-
342
- // ── Transactions ──────────────────────────────────────────────────────────
343
- /**
344
- * Run a schema-aware transaction.
345
- * The transaction callback receives a schema-aware transaction object.
346
- *
347
- * @param {(t: SchemaTransaction) => Promise<T>} updateFn
348
- * @returns {Promise<T>}
349
- *
350
- * @example
351
- * await runTransaction(async (t) => {
352
- * const user = await t.get(usersSchema, 'user-123')
353
- * if (!user) throw new Error('User not found')
354
- *
355
- * await t.update(usersSchema, 'user-123', {
356
- * points: user.points + 10
357
- * })
358
- * })
359
- */
360
- async function runTransaction(updateFn) {
361
- return _runTransaction(db, (firestoreTx) => {
362
- const t = {
363
- /** Get a document within the transaction. Returns { id, ...data } or null. */
364
- async get(schema, idOrRef) {
365
- assertSchema(schema, 'transaction.get')
366
- const ref = resolveDocRef(db, schema, idOrRef)
367
- const snapshot = await firestoreTx.get(ref)
368
- return docToData(snapshot)
369
- },
370
-
371
- /** Set (overwrite) a document within the transaction */
372
- set(schema, idOrRef, data, options = {}) {
373
- assertSchema(schema, 'transaction.set')
374
- const ref = resolveDocRef(db, schema, idOrRef)
375
- const validated = (options.merge || options.mergeFields)
376
- ? schema.validatePartial(data)
377
- : schema.validate(data)
378
- firestoreTx.set(ref, validated, options)
379
- return this
380
- },
381
-
382
- /** Partially update a document within the transaction */
383
- update(schema, idOrRef, data) {
384
- assertSchema(schema, 'transaction.update')
385
- const ref = resolveDocRef(db, schema, idOrRef)
386
- const validated = schema.validatePartial(data)
387
- firestoreTx.update(ref, validated)
388
- return this
389
- },
390
-
391
- /** Delete a document within the transaction */
392
- delete(schema, idOrRef) {
393
- assertSchema(schema, 'transaction.delete')
394
- const ref = resolveDocRef(db, schema, idOrRef)
395
- firestoreTx.delete(ref)
396
- return this
397
- },
398
- }
399
-
400
- return updateFn(t)
401
- })
402
- }
403
-
404
- // ── Return all operations ─────────────────────────────────────────────────
405
- return {
406
- createDoc,
407
- setDoc,
408
- updateDoc,
409
- deleteDoc,
410
- getDoc,
411
- getDocs,
412
- getDocRef,
413
- getCollectionRef,
414
- onDocSnapshot,
415
- onCollectionSnapshot,
416
- createBatch,
417
- runTransaction,
418
- }
419
- }