fire2mongo 1.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/README.md ADDED
@@ -0,0 +1,651 @@
1
+ # fire2mongo
2
+
3
+ Drop-in TypeScript replacement for `firebase/firestore` backed by MongoDB.
4
+ Change one import line — the rest of your code stays identical.
5
+
6
+ ```ts
7
+ // Before
8
+ import { doc, getDoc, query, where } from 'firebase/firestore';
9
+
10
+ // After
11
+ import { doc, getDoc, query, where } from 'fire2mongo';
12
+ ```
13
+
14
+ ---
15
+
16
+ ## Table of Contents
17
+
18
+ 1. [Installation](#installation)
19
+ 2. [Setup](#setup)
20
+ 3. [Quick Migration](#quick-migration)
21
+ 4. [API Reference](#api-reference)
22
+ - [References](#references)
23
+ - [Reading Data](#reading-data)
24
+ - [Writing Data](#writing-data)
25
+ - [Querying](#querying)
26
+ - [FieldValue](#fieldvalue)
27
+ - [Timestamp](#timestamp)
28
+ - [Batch Writes](#batch-writes)
29
+ - [Transactions](#transactions)
30
+ 5. [Subcollections](#subcollections)
31
+ 6. [Collection Registry](#collection-registry)
32
+ 7. [Connection Management](#connection-management)
33
+ 8. [Known Limitations](#known-limitations)
34
+ 9. [Publishing](#publishing)
35
+
36
+ ---
37
+
38
+ ## Installation
39
+
40
+ ```bash
41
+ npm install fire2mongo mongodb
42
+ ```
43
+
44
+ > `mongodb` is a peer dependency — install it alongside fire2mongo.
45
+
46
+ ---
47
+
48
+ ## Setup
49
+
50
+ ### 1. Initialize the connection
51
+
52
+ Call `initMongoDB()` once at app startup. It is safe to call multiple times — the connection is cached after the first call.
53
+
54
+ ```ts
55
+ import { initMongoDB } from 'fire2mongo';
56
+
57
+ await initMongoDB({
58
+ uri: process.env.MONGODB_URI!,
59
+ dbName: process.env.MONGODB_DB!,
60
+ });
61
+ ```
62
+
63
+ **Next.js App Router** — call it in a shared singleton file (e.g. `lib/mongodb.ts`) imported by your route handlers:
64
+
65
+ ```ts
66
+ // lib/mongodb.ts
67
+ import { initMongoDB } from 'fire2mongo';
68
+
69
+ let initialized = false;
70
+
71
+ export async function ensureDb() {
72
+ if (initialized) return;
73
+ await initMongoDB({ uri: process.env.MONGODB_URI!, dbName: process.env.MONGODB_DB! });
74
+ initialized = true;
75
+ }
76
+ ```
77
+
78
+ ### 2. Register your collections
79
+
80
+ Every Firebase collection name must be registered before use. Do this once — in the same startup file or a dedicated `collections.ts`.
81
+
82
+ ```ts
83
+ import { registerCollections } from 'fire2mongo';
84
+
85
+ registerCollections({
86
+ users: 'users',
87
+ orders: 'orders',
88
+ products: 'inventory_items', // Firebase name → actual MongoDB collection name
89
+ });
90
+ ```
91
+
92
+ Or one at a time:
93
+
94
+ ```ts
95
+ import { registerCollection } from 'fire2mongo';
96
+
97
+ registerCollection('users');
98
+ registerCollection('products', { mongoCollection: 'inventory_items' });
99
+ ```
100
+
101
+ If you call `getDoc` on an unregistered collection, fire2mongo throws a clear error listing every registered name:
102
+
103
+ ```
104
+ [fire2mongo] No collection registered for "items".
105
+ Registered: users, orders, products.
106
+ Call registerCollection("items") during app startup.
107
+ ```
108
+
109
+ ---
110
+
111
+ ## Quick Migration
112
+
113
+ The only thing you need to change in existing Firebase code is the import path (and remove the Firebase `db` import).
114
+
115
+ ```ts
116
+ // ── BEFORE ──────────────────────────────────────────────────────────────────
117
+ import {
118
+ doc, collection, getDoc, getDocs, setDoc, addDoc, updateDoc, deleteDoc,
119
+ query, where, orderBy, limit,
120
+ Timestamp, arrayUnion, arrayRemove, increment,
121
+ } from 'firebase/firestore';
122
+ import { db } from '@/lib/firebase';
123
+
124
+ // ── AFTER ────────────────────────────────────────────────────────────────────
125
+ import {
126
+ doc, collection, getDoc, getDocs, setDoc, addDoc, updateDoc, deleteDoc,
127
+ query, where, orderBy, limit,
128
+ Timestamp, arrayUnion, arrayRemove, increment,
129
+ } from 'fire2mongo';
130
+ import { db } from 'fire2mongo'; // db = {} no-op — kept so existing code compiles
131
+ ```
132
+
133
+ Everything below the import line is unchanged.
134
+
135
+ ---
136
+
137
+ ## API Reference
138
+
139
+ ### References
140
+
141
+ #### `doc(db, collection, id)`
142
+
143
+ Returns a `DocumentReference` for a specific document.
144
+
145
+ ```ts
146
+ import { doc, db } from 'fire2mongo';
147
+
148
+ const userRef = doc(db, 'users', userId);
149
+ const orderRef = doc(db, 'orders', orderId);
150
+ ```
151
+
152
+ #### `doc(collectionRef)` — auto-generate ID
153
+
154
+ Pass a `CollectionReference` (no ID) to get a reference with an auto-generated ObjectId.
155
+
156
+ ```ts
157
+ const newRef = doc(collection(db, 'users'));
158
+ console.log(newRef.id); // auto-generated ObjectId string
159
+ ```
160
+
161
+ #### `collection(db, path)`
162
+
163
+ Returns a `CollectionReference`.
164
+
165
+ ```ts
166
+ import { collection, db } from 'fire2mongo';
167
+
168
+ const usersCol = collection(db, 'users');
169
+ const ordersCol = collection(db, 'orders');
170
+ ```
171
+
172
+ ---
173
+
174
+ ### Reading Data
175
+
176
+ #### `getDoc(ref)` — single document
177
+
178
+ ```ts
179
+ import { doc, getDoc, db } from 'fire2mongo';
180
+
181
+ const snap = await getDoc(doc(db, 'users', userId));
182
+
183
+ if (snap.exists()) {
184
+ const user = snap.data(); // typed as your document type
185
+ console.log(snap.id); // document id string
186
+ } else {
187
+ console.log('Not found');
188
+ }
189
+ ```
190
+
191
+ With generics:
192
+
193
+ ```ts
194
+ interface User { name: string; email: string; role: string; }
195
+
196
+ const snap = await getDoc<User>(doc(db, 'users', userId));
197
+ const user = snap.data(); // User | undefined
198
+ ```
199
+
200
+ #### `getDocs(queryOrRef)` — multiple documents
201
+
202
+ ```ts
203
+ import { collection, getDocs, db } from 'fire2mongo';
204
+
205
+ // Entire collection
206
+ const snap = await getDocs(collection(db, 'users'));
207
+
208
+ snap.forEach(doc => {
209
+ console.log(doc.id, doc.data());
210
+ });
211
+
212
+ console.log(snap.size); // number of documents
213
+ console.log(snap.empty); // boolean
214
+ ```
215
+
216
+ With a query:
217
+
218
+ ```ts
219
+ import { query, where, orderBy, limit, getDocs, collection, db } from 'fire2mongo';
220
+
221
+ const q = query(
222
+ collection(db, 'orders'),
223
+ where('status', '==', 'PENDING'),
224
+ orderBy('createdAt', 'desc'),
225
+ limit(20),
226
+ );
227
+
228
+ const snap = await getDocs(q);
229
+ const orders = snap.docs.map(d => d.data());
230
+ ```
231
+
232
+ ---
233
+
234
+ ### Writing Data
235
+
236
+ #### `setDoc(ref, data)` — create or overwrite
237
+
238
+ ```ts
239
+ import { doc, setDoc, db } from 'fire2mongo';
240
+
241
+ await setDoc(doc(db, 'users', userId), {
242
+ name: 'Alice',
243
+ email: 'alice@example.com',
244
+ createdAt: Timestamp.now(),
245
+ });
246
+ ```
247
+
248
+ #### `setDoc(ref, data, { merge: true })` — partial update (merge)
249
+
250
+ Only the provided fields are updated. Existing fields not in `data` are left unchanged.
251
+
252
+ ```ts
253
+ await setDoc(doc(db, 'users', userId), { name: 'Alice Updated' }, { merge: true });
254
+ ```
255
+
256
+ #### `addDoc(collectionRef, data)` — create with auto-generated ID
257
+
258
+ ```ts
259
+ import { collection, addDoc, db } from 'fire2mongo';
260
+
261
+ const ref = await addDoc(collection(db, 'orders'), {
262
+ customerId: 'cust_123',
263
+ total: 4500,
264
+ status: 'PENDING',
265
+ createdAt: Timestamp.now(),
266
+ });
267
+
268
+ console.log('New order ID:', ref.id);
269
+ ```
270
+
271
+ #### `updateDoc(ref, partialData)` — partial update
272
+
273
+ Only the provided fields are updated. Does not overwrite the entire document.
274
+
275
+ ```ts
276
+ import { doc, updateDoc, db } from 'fire2mongo';
277
+
278
+ await updateDoc(doc(db, 'orders', orderId), {
279
+ status: 'SHIPPED',
280
+ shippedAt: Timestamp.now(),
281
+ });
282
+ ```
283
+
284
+ #### `deleteDoc(ref)` — delete a document
285
+
286
+ ```ts
287
+ import { doc, deleteDoc, db } from 'fire2mongo';
288
+
289
+ await deleteDoc(doc(db, 'orders', orderId));
290
+ ```
291
+
292
+ ---
293
+
294
+ ### Querying
295
+
296
+ Build queries with `query()` and any combination of `where`, `orderBy`, `limit`, and `offset`.
297
+
298
+ ```ts
299
+ import { query, where, orderBy, limit, offset } from 'fire2mongo';
300
+
301
+ const q = query(
302
+ collection(db, 'orders'),
303
+ where('status', '==', 'ACTIVE'),
304
+ orderBy('createdAt', 'desc'),
305
+ limit(10),
306
+ offset(20), // skip first 20
307
+ );
308
+ ```
309
+
310
+ #### `where(field, operator, value)`
311
+
312
+ | Operator | MongoDB equivalent |
313
+ |---|---|
314
+ | `'=='` | `{ $eq }` |
315
+ | `'!='` | `{ $ne }` |
316
+ | `'<'` | `{ $lt }` |
317
+ | `'<='` | `{ $lte }` |
318
+ | `'>'` | `{ $gt }` |
319
+ | `'>='` | `{ $gte }` |
320
+ | `'in'` | `{ $in }` |
321
+ | `'not-in'` | `{ $nin }` |
322
+ | `'array-contains'` | `{ $elemMatch: { $eq } }` |
323
+ | `'array-contains-any'` | `{ $in }` |
324
+
325
+ ```ts
326
+ where('status', '==', 'ACTIVE')
327
+ where('age', '>=', 18)
328
+ where('role', 'in', ['admin', 'manager'])
329
+ where('tags', 'array-contains', 'premium')
330
+ ```
331
+
332
+ #### `and(...constraints)` — explicit AND grouping
333
+
334
+ ```ts
335
+ import { and } from 'fire2mongo';
336
+
337
+ const q = query(
338
+ collection(db, 'users'),
339
+ and(
340
+ where('role', '==', 'staff'),
341
+ where('branch', '==', 'HQ'),
342
+ ),
343
+ );
344
+ ```
345
+
346
+ #### `or(...constraints)` — OR grouping
347
+
348
+ ```ts
349
+ import { or } from 'fire2mongo';
350
+
351
+ const q = query(
352
+ collection(db, 'orders'),
353
+ or(
354
+ where('status', '==', 'PENDING'),
355
+ where('status', '==', 'PROCESSING'),
356
+ ),
357
+ );
358
+ ```
359
+
360
+ #### `orderBy(field, direction?)`
361
+
362
+ ```ts
363
+ orderBy('createdAt', 'desc') // newest first
364
+ orderBy('name', 'asc') // alphabetical (default)
365
+ ```
366
+
367
+ #### `limit(n)` / `offset(n)`
368
+
369
+ ```ts
370
+ limit(10) // return max 10 documents
371
+ offset(20) // skip first 20 documents (use for pagination)
372
+ ```
373
+
374
+ ---
375
+
376
+ ### FieldValue
377
+
378
+ Atomic write operations — use inside `updateDoc` or `setDoc`.
379
+
380
+ #### `arrayUnion(...elements)` — add to array (no duplicates)
381
+
382
+ ```ts
383
+ import { arrayUnion, updateDoc, doc, db } from 'fire2mongo';
384
+
385
+ await updateDoc(doc(db, 'users', userId), {
386
+ tags: arrayUnion('premium', 'verified'),
387
+ });
388
+ // MongoDB: $addToSet: { $each: ['premium', 'verified'] }
389
+ ```
390
+
391
+ #### `arrayRemove(...elements)` — remove from array
392
+
393
+ ```ts
394
+ import { arrayRemove } from 'fire2mongo';
395
+
396
+ await updateDoc(doc(db, 'users', userId), {
397
+ tags: arrayRemove('trial'),
398
+ });
399
+ // MongoDB: $pull: { $in: ['trial'] }
400
+ ```
401
+
402
+ #### `increment(n)` — increment a numeric field
403
+
404
+ ```ts
405
+ import { increment } from 'fire2mongo';
406
+
407
+ await updateDoc(doc(db, 'posts', postId), {
408
+ viewCount: increment(1),
409
+ likeCount: increment(-1),
410
+ });
411
+ // MongoDB: $inc: { viewCount: 1, likeCount: -1 }
412
+ ```
413
+
414
+ You can mix FieldValue operations with regular field updates in a single call:
415
+
416
+ ```ts
417
+ await updateDoc(ref, {
418
+ status: 'ACTIVE', // regular $set
419
+ tags: arrayUnion('vip'), // $addToSet
420
+ loginCount: increment(1), // $inc
421
+ });
422
+ ```
423
+
424
+ ---
425
+
426
+ ### Timestamp
427
+
428
+ Firebase-compatible `Timestamp` class. Stored as a JavaScript `Date` in MongoDB.
429
+
430
+ ```ts
431
+ import { Timestamp } from 'fire2mongo';
432
+
433
+ // Create
434
+ const now = Timestamp.now();
435
+ const fromDate = Timestamp.fromDate(new Date('2025-01-01'));
436
+ const fromMs = Timestamp.fromMillis(Date.now());
437
+
438
+ // Convert
439
+ now.toDate() // → Date
440
+ now.toMillis() // → number (epoch ms)
441
+ now.seconds // → number
442
+ now.nanoseconds // → number
443
+
444
+ // Compare
445
+ now.isEqual(fromMs) // → boolean
446
+
447
+ // Use in writes
448
+ await setDoc(ref, {
449
+ createdAt: Timestamp.now(),
450
+ scheduledAt: Timestamp.fromDate(new Date('2025-12-31')),
451
+ });
452
+ ```
453
+
454
+ ---
455
+
456
+ ### Batch Writes
457
+
458
+ Queue multiple write operations and execute them together. All ops run in parallel via `Promise.all`.
459
+
460
+ > **Note:** Batch writes are **not atomic**. If one operation fails, others may already have applied. For atomic operations, use MongoDB sessions directly.
461
+
462
+ ```ts
463
+ import { writeBatch, doc, db } from 'fire2mongo';
464
+
465
+ const batch = writeBatch(db);
466
+
467
+ batch.set(doc(db, 'users', 'user1'), { name: 'Alice', status: 'ACTIVE' });
468
+ batch.update(doc(db, 'orders', 'order1'), { status: 'SHIPPED' });
469
+ batch.delete(doc(db, 'drafts', 'draft1'));
470
+
471
+ await batch.commit();
472
+ ```
473
+
474
+ ---
475
+
476
+ ### Transactions
477
+
478
+ Best-effort transaction — reads are immediate, writes are collected and executed in parallel after the callback resolves.
479
+
480
+ > **Note:** This is **not true ACID**. There is no rollback on failure. For true atomic transactions use the MongoDB driver directly with sessions.
481
+
482
+ ```ts
483
+ import { runTransaction, doc, db } from 'fire2mongo';
484
+
485
+ await runTransaction(db, async (tx) => {
486
+ const snap = await tx.get(doc(db, 'counters', 'global'));
487
+ const current = snap.data()?.value ?? 0;
488
+
489
+ tx.update(doc(db, 'counters', 'global'), { value: current + 1 });
490
+ });
491
+ ```
492
+
493
+ ---
494
+
495
+ ## Subcollections
496
+
497
+ Firebase subcollections are mapped to flat MongoDB collections using `_` as a separator. Parent IDs are automatically injected into documents on every write — no manual wiring needed.
498
+
499
+ ### Naming rule
500
+
501
+ ```
502
+ Firebase path MongoDB collection Auto-injected fields
503
+ ────────────────────────────────────── ──────────────────────── ────────────────────
504
+ orders/{orderId}/items orders_items ordersId
505
+ users/{userId}/sessions users_sessions usersId
506
+ users/{userId}/sessions/{id}/logs users_sessions_logs usersId, sessionsId
507
+ ```
508
+
509
+ ### Reading a subcollection
510
+
511
+ ```ts
512
+ import { collection, getDocs, query, where, db } from 'fire2mongo';
513
+
514
+ // All items for a specific order
515
+ const itemsCol = collection(db, 'orders', orderId, 'items');
516
+ const snap = await getDocs(itemsCol);
517
+
518
+ // With a filter
519
+ const q = query(itemsCol, where('status', '==', 'ACTIVE'));
520
+ const snap = await getDocs(q);
521
+ ```
522
+
523
+ ### Writing to a subcollection
524
+
525
+ ```ts
526
+ import { collection, addDoc, doc, setDoc, db } from 'fire2mongo';
527
+
528
+ // addDoc — auto ID, ordersId injected automatically
529
+ const ref = await addDoc(collection(db, 'orders', orderId, 'items'), {
530
+ productId: 'prod_123',
531
+ quantity: 2,
532
+ price: 1500,
533
+ });
534
+ // Stored as: { _id: <ObjectId>, ordersId: orderId, productId: ..., quantity: ..., price: ... }
535
+
536
+ // setDoc — specific ID
537
+ await setDoc(doc(db, 'orders', orderId, 'items', itemId), {
538
+ productId: 'prod_456',
539
+ quantity: 1,
540
+ });
541
+ // Stored as: { _id: itemId, ordersId: orderId, ... }
542
+ ```
543
+
544
+ ### Register subcollection names
545
+
546
+ Subcollections use the flattened MongoDB collection name for registration:
547
+
548
+ ```ts
549
+ registerCollections({
550
+ orders: 'orders',
551
+ orders_items: 'orders_items', // subcollection
552
+ });
553
+ ```
554
+
555
+ ---
556
+
557
+ ## Collection Registry
558
+
559
+ The registry maps Firebase collection names (used in `doc()` / `collection()` calls) to actual MongoDB collection names. This allows your Firebase-style code to stay unchanged even if MongoDB uses different collection names.
560
+
561
+ ```ts
562
+ import { registerCollection, registerCollections, hasCollection, clearRegistry } from 'fire2mongo';
563
+
564
+ // Register one
565
+ registerCollection('users');
566
+ registerCollection('products', { mongoCollection: 'inventory_items' });
567
+
568
+ // Register many at once
569
+ registerCollections({
570
+ users: 'users',
571
+ orders: 'kms_orders',
572
+ products: 'inventory_items',
573
+ orders_items: 'orders_items',
574
+ });
575
+
576
+ // Check if registered
577
+ hasCollection('users') // true
578
+ hasCollection('unknown') // false
579
+
580
+ // Clear all (useful in tests)
581
+ clearRegistry();
582
+ ```
583
+
584
+ ---
585
+
586
+ ## Connection Management
587
+
588
+ ```ts
589
+ import { initMongoDB, getDb, closeMongoDB } from 'fire2mongo';
590
+
591
+ // Initialize (call once at startup)
592
+ await initMongoDB({ uri: 'mongodb://...', dbName: 'mydb' });
593
+
594
+ // Access the raw Db instance (for advanced MongoDB operations)
595
+ const db = getDb();
596
+ const col = db.collection('my_collection');
597
+
598
+ // Close (graceful shutdown / tests)
599
+ await closeMongoDB();
600
+ ```
601
+
602
+ `getDb()` throws if `initMongoDB()` has not been called:
603
+
604
+ ```
605
+ [fire2mongo] MongoDB not initialized. Call initMongoDB() before using any firestore functions.
606
+ ```
607
+
608
+ ---
609
+
610
+ ## Known Limitations
611
+
612
+ | Limitation | Detail |
613
+ |---|---|
614
+ | **Server-side only** | Uses the `mongodb` Node.js driver. Not compatible with browser or edge runtimes. |
615
+ | **No real-time listeners** | `onSnapshot` is not implemented. |
616
+ | **Batch is not atomic** | `writeBatch.commit()` runs ops via `Promise.all` — no session or rollback. |
617
+ | **Transaction is not ACID** | `runTransaction` collects writes and runs them in parallel. No rollback on failure. |
618
+ | **`setDoc` checks existence** | Without `{ merge: true }`, `setDoc` calls `findOne` first to decide between insert and replace. |
619
+ | **No cursor pagination** | `startAfter()` is not supported. Use `offset()` for page-based pagination. |
620
+ | **ObjectId IDs** | Auto-generated IDs use MongoDB `ObjectId`. Firebase UID strings are stored as-is in the `id` field. |
621
+
622
+ ---
623
+
624
+ ## Publishing
625
+
626
+ ```bash
627
+ # Build CJS + ESM + type declarations
628
+ npm run build
629
+
630
+ # Verify types
631
+ npm run typecheck
632
+
633
+ # Publish to npm
634
+ npm publish --access public
635
+ ```
636
+
637
+ Build output in `dist/`:
638
+
639
+ ```
640
+ dist/
641
+ index.js — CommonJS
642
+ index.mjs — ES Module
643
+ index.d.ts — TypeScript declarations (CJS)
644
+ index.d.mts — TypeScript declarations (ESM)
645
+ ```
646
+
647
+ ---
648
+
649
+ ## License
650
+
651
+ MIT