# PocketBase Adapter: Firestore Compatibility Contract

Use this when the app UI looks correct but core functions (dashboard counts, appointments load, customer lists, quote generation flows) still fail after a Firebase→PocketBase migration.

## Why this matters

In this class of app, page modules are written against Firestore semantics. If `pocketbase.js` does not emulate that contract, you get broad functional breakage even when HTML/JS files appear aligned.

## Required `getDocs()` return shape

`getDocs(...)` must return a QuerySnapshot-like object, not a raw array:

- `docs` (array of doc snapshots)
- `size` (number)
- `empty` (boolean)
- `forEach(callback)`

Each doc in `docs` must expose:

- `id`
- `data()`
- `exists()` (function form for Firestore compatibility)
- optional `ref`

## Required `getDoc()` return shape

`getDoc(...)` must return a DocumentSnapshot-like object:

- `id`
- `data()`
- `exists()` **as a function** (not boolean property)
- `ref` (at minimum `{ id }`)

Why this matters: many legacy code paths call `if (snap.exists())` or check `snap.ref.id` in details/edit flows. Returning `exists: true` (boolean) breaks with runtime errors such as `snap.exists is not a function`.

## Required `addDoc()` behavior for sub-collection patterns

Legacy code often writes records via sub-collection syntax: `collection(db, 'users', uid, 'services')`. The adapter must inject the userId into the stored record so filtered queries (`where("userId", "==", uid)`) find it:

```javascript
async function addDoc(collRef, data) {
    const collName = collRef._name;
    const pbData = convertToPB(data);
    if (collRef._userId && !pbData.userId) {
        pbData.userId = collRef._userId;
    }
    const record = await pb.collection(collName).create(pbData);
    return { id: record.id };
}
```

Without this injection, records created via sub-collection refs appear to "save" (no error thrown) but never appear in subsequent filtered queries — users see "nothing saved" despite no error feedback.

This matters for code paths creating records under `users/{uid}/services`, `users/{uid}/customers`, `users/{uid}/appointments`, `users/{uid}/repairOrders`.

## Required input compatibility

`getDocs(...)` must accept both:

1. `getDocs(query(...))`
2. `getDocs(collection(...))`

Many legacy modules mix both patterns. Supporting only query objects silently breaks dashboard/customer/appointment code paths.

## `onSnapshot(...)` minimum compatibility

If realtime is shimmed, callback shape must still match caller expectations:

- For query/collection refs: callback receives snapshot object with `docs/size/empty/forEach`
- For doc refs: callback receives doc snapshot-like object

A one-shot shim is acceptable for non-realtime pages, but document that limitation and avoid claiming full realtime parity.

## ⚠️ `setDoc()` must use query-based upsert (PocketBase v0.23+)

**The old pattern is broken:**

```javascript
// BROKEN on PocketBase v0.23+
function setDoc(docRef, data) {
    const collName = docRef._collection;
    const docId = docRef._id;
    const pbData = convertToPB(data);
    return pb.collection(collName).update(docId, pbData).catch(() =>
        pb.collection(collName).create({ ...pbData, id: docId })
    ).then(rec => ({ id: rec.id }));
}
```

Two failures:

1. **Custom system IDs rejected.** PocketBase v0.23+ requires system IDs ≥ 15 characters. `'appSettings'` (11 chars), `'accountSettings'` (15 chars, barely valid), and similar short custom IDs cause `validation_min_text_constraint` or `validation_invalid_format` errors on `create`. Arbitrary strings with underscores or hyphens also fail format validation.

2. **Unknown fields dropped on create.** The `update().catch(() => create())` pattern tries to create with raw settings fields (`darkMode`, `taxRate`, ...) that aren't in the collection schema. PocketBase silently drops them or rejects the create.

**The correct pattern — query-based upsert by `name + userId`:**

```javascript
function setDoc(docRef, data) {
    const collName = docRef._collection;
    const docId = docRef._id;
    const pbData = convertToPB(data);
    const userId = docRef._userId || '';

    // Find existing record by name + userId, then update-or-create
    return pb.collection(collName).getFullList({
        filter: `name = "${docId}"${userId ? ` && userId = "${userId}"` : ''}`,
        requestKey: null
    }).then(records => {
        if (records.length > 0) {
            return pb.collection(collName).update(records[0].id, pbData);
        } else {
            return pb.collection(collName).create(pbData);
        }
    }).then(rec => ({ id: rec.id }));
}
```

This avoids setting system IDs entirely (lets PocketBase auto-generate them) and uses the `name` + `userId` fields for reliable lookup.

### Required schema for settings-style collections

When using this pattern, the target collection must have these fields:

| Field    | Type   | Required | Purpose |
|----------|--------|----------|---------|
| `userId` | text   | yes      | Owner ID for collection rules (`userId = @request.auth.id`) |
| `name`   | text   | yes      | Document key — matches the 4th argument of `doc(db, 'users', uid, 'settings', 'name')` |
| `data`   | json   | no       | Arbitrary settings payload (entire `appSettings` object) |

Collection rules should be:
- `listRule`: `userId = @request.auth.id`
- `viewRule`: `userId = @request.auth.id`
- `createRule`: `@request.auth.id != ""`
- `updateRule`: `userId = @request.auth.id`
- `deleteRule`: `userId = @request.auth.id`

## ⚠️ `getDoc()` needs name-based fallback

When records are created without predictable system IDs (auto-generated by PocketBase), a direct `getOne(docId)` call uses the doc ID from the Firestore ref path, which won't match the system ID. Example:

```javascript
// Firestore ref: doc(db, 'users', uid, 'settings', 'appSettings')
// Adapter creates: { _collection: 'settings', _id: 'appSettings', _userId: uid }
// PocketBase getOne('appSettings') fails — no record with that system ID exists
```

**Fix:** Add a name-based fallback query when `getOne(id)` returns 404:

```javascript
async function getDoc(docRef) {
    const collName = docRef._collection;
    const docId = docRef._id;
    const userId = docRef._userId || '';

    try {
        // Try direct lookup by system ID (for legacy records)
        let record = await pb.collection(collName).getOne(docId);
        return { id: record.id, data: () => convertFromPB(record),
                 exists: () => true, ref: { id: record.id } };
    } catch (err) {
        // Fallback: look up by name (for query-based-created records)
        try {
            const filter = `name = "${docId}"${userId ? ` && userId = "${userId}"` : ''}`;
            const records = await pb.collection(collName).getFullList({
                filter, requestKey: null
            });
            if (records.length > 0) {
                const record = records[0];
                return { id: record.id, data: () => convertFromPB(record),
                         exists: () => true, ref: { id: record.id } };
            }
        } catch (e2) { /* not found */ }
        return { exists: () => false, data: () => null, ref: { id: docId } };
    }
}
```

## Settings storage pattern for PocketBase

The app's `settings.js` uses `saveAllSettings()` and `loadSettings()` to persist a complex settings object. After migrating to PocketBase:

**Save:**
```javascript
// Instead of spreading fields:
// setDoc(ref, { ...appSettings, lastUpdated: ... });  // FAILS: unknown fields
// Use the 'data' json field:
setDoc(ref, {
    data: { ...appSettings, lastUpdated: new Date().toISOString() },
    userId: currentUser.uid,
    name: 'appSettings'
});
```

**Load:**
```javascript
// Instead of reading raw fields:
// const { lastUpdated, ...clean } = settingsDoc.data();
// appSettings = { ...appSettings, ...clean };
// Use the 'data' json field:
if (record && record.data) {
    const { lastUpdated, ...cleanSettings } = record.data;
    appSettings = { ...appSettings, ...cleanSettings };
}
```

Apply the same pattern for account settings (`name: 'accountSettings'`) and any settings-type document.

## Fast verification checklist

Search target code for these usage patterns:

- `querySnapshot.docs.map(...)`
- `if (querySnapshot.empty) ...`
- `querySnapshot.size`
- `querySnapshot.forEach(...)`
- `getDocs(collection(` (not just query)
- `setDoc(..., { merge: true })` — adapter ignores merge
- `setDoc(..., { ...spread, lastUpdated })` — check if target collection has a `data` json field
- `getDoc(doc(db, 'users', uid, 'settings', name))` — check if `getDoc` has name-based fallback

If these appear, adapter must provide full snapshot compatibility and query-based setDoc/getDoc.

## Practical migration pattern

1. First restore file parity vs known-good reference (HTML + page JS).
2. Normalize backend import path (`firebase.js` → `pocketbase.js`).
3. Fix adapter contract (`getDocs`, `onSnapshot`, snapshot shape).
4. **Fix `setDoc` and `getDoc` for settings-style docs** (query-based upsert + name fallback).
5. **Update all `handleSaveAccountSettings()` sites** across every page's JS file to use `{ data: {...}, userId, name }` pattern.
6. **Ensure the `settings` collection has a `data` json field** added to its schema.
7. Re-check the four core flows:
   - quote generation
   - appointment setting
   - dashboard actions/cards/modals
   - customer page search/edit/actions

This avoids wasting time patching dozens of per-page handlers when the real fault is the adapter contract.
