initial commit
This commit is contained in:
+114
@@ -0,0 +1,114 @@
|
||||
# PocketBase Settings Save/Load with JSON Fields
|
||||
|
||||
## Problem
|
||||
|
||||
PocketBase collections with a fixed schema (e.g., `id`, `userId`, `name` text fields) can't store arbitrary key-value settings like `{darkMode: true, taxRate: 9.25, ...}`. Every field written to a PocketBase record MUST be defined in the collection schema, or it's **silently dropped** (HTTP 200, data lost).
|
||||
|
||||
Additionally, **PocketBase v0.23+** enforces a minimum 15-character ID with format validation (lowercase alphanumeric only). Trying to `setDoc` with an ID like `appSettings` (11 chars) fails silently — the adapter's `update('appSettings', data)` → 404, fallback `create({...data, id:'appSettings'})` → 400 `validation_invalid_format`.
|
||||
|
||||
## The Fix: Three Layers
|
||||
|
||||
### 1. Add a `json` field to the collection
|
||||
|
||||
```python
|
||||
# PATCH the collection to add a JSON field
|
||||
api("PATCH", f"/api/collections/settings", {
|
||||
"fields": [
|
||||
...existing fields...,
|
||||
{"id": "json_data", "name": "data", "type": "json", "required": False, "system": False}
|
||||
],
|
||||
...keep existing rules...
|
||||
}, token)
|
||||
```
|
||||
|
||||
The `json` field type accepts arbitrary objects — PocketBase stores and retrieves them natively.
|
||||
|
||||
### 2. Fix `setDoc` in the adapter — query-based upsert
|
||||
|
||||
Don't try to set PocketBase's system `id` field to custom values. Instead, query by `name + userId` to find the record, then update or create:
|
||||
|
||||
```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
|
||||
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 }));
|
||||
}
|
||||
```
|
||||
|
||||
### 3. Fix `getDoc` — fallback to name-based lookup
|
||||
|
||||
```javascript
|
||||
async function getDoc(docRef) {
|
||||
const collName = docRef._collection;
|
||||
const docId = docRef._id;
|
||||
const userId = docRef._userId || '';
|
||||
|
||||
try {
|
||||
// Try direct ID lookup first (for backward compat)
|
||||
let record = await pb.collection(collName).getOne(docId);
|
||||
return { id: record.id, data: () => convertFromPB(record), exists: () => true, ref: { id: record.id } };
|
||||
} catch (err) {
|
||||
// Fallback: query by name + userId
|
||||
try {
|
||||
const filter = `name = "${docId}"${userId ? ` && userId = "${userId}"` : ''}`;
|
||||
const records = await pb.collection(collName).getFullList({ filter: 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) {}
|
||||
return { exists: () => false, data: () => null, ref: { id: docId } };
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Fix save/load callers — wrap data in `data` json field
|
||||
|
||||
**Save:**
|
||||
```javascript
|
||||
// BEFORE (fails — PocketBase drops unknown fields)
|
||||
await setDoc(userSettingsRef, { ...appSettings, lastUpdated: new Date().toISOString() });
|
||||
|
||||
// AFTER (works — settings go into the 'data' json field)
|
||||
await setDoc(userSettingsRef, {
|
||||
data: { ...appSettings, lastUpdated: new Date().toISOString() },
|
||||
userId: currentUser.uid,
|
||||
name: 'appSettings'
|
||||
});
|
||||
```
|
||||
|
||||
**Load:**
|
||||
```javascript
|
||||
// BEFORE (reads top-level fields — gets nothing useful)
|
||||
const firebaseSettings = settingsDoc.data();
|
||||
appSettings = { ...appSettings, ...firebaseSettings };
|
||||
|
||||
// AFTER (reads from 'data' json field)
|
||||
const record = settingsDoc.data();
|
||||
if (record && record.data) {
|
||||
const { lastUpdated, ...cleanSettings } = record.data;
|
||||
appSettings = { ...appSettings, ...cleanSettings };
|
||||
}
|
||||
```
|
||||
|
||||
## Full Pattern Checklist
|
||||
|
||||
When settings aren't persisting:
|
||||
- [ ] Does the collection have a `json` field for arbitrary data?
|
||||
- [ ] Is `setDoc` using query-based upsert (not trying to set system IDs)?
|
||||
- [ ] Are all save callers wrapping data in `{data: {...}, userId, name}`?
|
||||
- [ ] Are all load callers reading from `record.data` instead of top-level fields?
|
||||
- [ ] Does every save path include `userId` and `name` for collection rule compliance?
|
||||
Reference in New Issue
Block a user