5.8 KiB
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
// 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
// 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
// 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
// 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
// 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 |