Files
2026-07-12 10:17:17 -04:00

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. getDocexists() 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