# PocketBase Adapter — Firestore Compatibility Patterns Critical return-shape requirements that the PocketBase adapter MUST satisfy for multi-file Firebase apps to work without per-file changes. ## 1. `getDoc` — `exists()` MUST be a function ```javascript // BUG: returns boolean — every consumer calls .exists() and gets TypeError return { exists: true, data: () => ... }; // FIX: returns function — matches Firestore SDK API return { id: record.id, data: () => convertFromPB(record), exists: () => true, // <-- function, not boolean ref: { id: record.id } }; ``` **Why this bites you:** The Firebase SDK documents `exists()` as a method. Every consumer in the migrated app does `if (snap.exists()) { ... }`. A boolean `true` is truthy so `if (snap.exists)` evaluates `if (true)` and the branch runs. But if the record is **not found**, the boolean `false` means `if (false)` skips the branch — which happens to pass in that case. The real crash is: `snap.exists()` returning `false` vs boolean `false` being the same — but a missing `exists` key entirely kills settings pages, customer details, and any code path that calls `exists()`. ## 2. `getDocs` — MUST return snapshot shape, not raw array ```javascript // Firestore consumer code (ubiquitous): const results = await getDocs(q); results.docs.map(doc => ({ id: doc.id, ...doc.data() })); results.forEach(doc => { ... }); if (results.empty) { ... } console.log(results.size); // <-- also used // PocketBase adapter BUG: returns a bare array // → docs.map crashes (TypeError: .docs is undefined) // → .empty, .size are undefined // FIX: createQuerySnapshot() helper function createQuerySnapshot(docs) { const docList = Array.isArray(docs) ? docs : []; return { docs: docList, size: docList.length, empty: docList.length === 0, forEach(callback) { docList.forEach(callback); } }; } async function getDocs(qOrCollectionRef) { // ... do query, get items from PocketBase ... const docs = items.map(rec => ({ id: rec.id, data: () => convertFromPB(rec), exists: true, ref: { id: rec.id } })); return createQuerySnapshot(docs); } ``` ## 3. `addDoc` — MUST inject userId from sub-collection refs ```javascript // App code (Firebase sub-collection pattern): const servicesRef = collection(db, 'users', currentUser.uid, 'services'); await addDoc(servicesRef, { name: 'Brake Pads', price: 199.99 }); // PocketBase adapter — the ref has _userId tracked but addDoc ignores it // BUG: record stored in 'services' collection WITHOUT userId field // → queries with where('userId', '==', currentUser.uid) return nothing // FIX: async function addDoc(collRef, data) { const pbData = convertToPB(data); if (collRef._userId && !pbData.userId) { pbData.userId = collRef._userId; // <-- inject from ref } const record = await pb.collection(collRef._name).create(pbData); return { id: record.id }; } ``` ## 4. `onSnapshot` — MUST pass proper snapshot shape ```javascript // Consumer code expects: onSnapshot(query, (snapshot) => { snapshot.forEach(doc => { ... }); // <-- pulls from snapshot.docs snapshot.docs.map(...); // <-- same snapshot.empty; // <-- also used }); // BUG: bare getDocs().then(docs => callback(snapshot from createQuerySnapshot)) // must pass the full snapshot, not raw docs // FIX: function onSnapshot(queryOrDocRef, callback) { if (queryOrDocRef && (queryOrDocRef._type === 'query' || queryOrDocRef._type === 'collection')) { getDocs(queryOrDocRef).then(snapshot => callback(snapshot)); } else { getDoc(queryOrDocRef).then(doc => callback(doc)); } return () => {}; } ``` ## Diagnosis: How to find these bugs ```javascript // In browser DevTools console: // 1) Check getDocs return shape const snap = await getDocs(query(collection(db, 'repairOrders'), where('userId', '==', '...'))); console.assert(Array.isArray(snap.docs), 'getDocs.docs must be array'); console.assert(typeof snap.empty === 'boolean', 'getDocs.empty must be boolean'); console.assert(typeof snap.size === 'number', 'getDocs.size must be number'); console.assert(typeof snap.forEach === 'function', 'getDocs.forEach must be function'); // 2) Check getDoc return shape const doc = await getDoc(doc(db, 'repairOrders', 'some-id')); console.assert(typeof doc.exists === 'function', 'getDoc.exists must be function, got ' + typeof doc.exists); console.assert(typeof doc.data === 'function', 'getDoc.data must be function'); // 3) Check addDoc injects userId const ref = collection(db, 'users', 'test-uid', 'services'); const result = await addDoc(ref, { name: 'test' }); const saved = await getDoc(doc(db, 'services', result.id)); console.assert(saved.data().userId === 'test-uid', 'addDoc must inject userId from ref'); ``` ## Common consumer patterns across app files These patterns appear in `dashboard.js`, `appointments.js`, `customers.js`, `repair-orders.js`, `settings.js`, `quote-tab-manager.js`, `customer-lookup.js`, and `shared/data-manager.js`: | Pattern | Found in | Breaks if... | |---|---|---| | `querySnapshot.docs.map(...)` | All files | `getDocs` returns raw array | | `querySnapshot.empty` | `repair-orders.js`, `appointments.js`, `settings.js` | Snapshot has no `.empty` | | `querySnapshot.size` | `dashboard.js`, `repair-orders.js`, `customers.js`, `customer-lookup.js` | Snapshot has no `.size` | | `querySnapshot.forEach(doc => {})` | `customers.js`, `customer-lookup.js`, `dashboard.js`, `settings.js`, `shared/data-manager.js` | Snapshot has no `.forEach` | | `if (docSnap.exists())` | `quote-tab-manager.js`, `repair-orders.js`, `settings.js`, `dashboard.js`, `invoice-manager.js`, `customers.js` | `exists` is boolean not function | | `collection(db, 'users', uid, '...')` | All files (sub-collection pattern) | `addDoc` doesn't inject `userId` |