140 lines
5.8 KiB
Markdown
140 lines
5.8 KiB
Markdown
# 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` |
|