// pocketbase.js — Firebase-compatible PocketBase adapter // Replace firebase.js with this file. All Firebase-using files work unchanged. // // Swap imports: // find . -name "*.js" -exec sed -i 's|from "./firebase.js"|from "./pocketbase.js"|g' {} + // find . -name "*.js" -exec sed -i 's|from "https://www\.gstatic\.com/firebasejs/...|from "./pocketbase.js"|g' {} + // // See firebase-to-pocketbase-migration skill for full migration guide. import PocketBase from "https://cdn.jsdelivr.net/npm/pocketbase@0.21.5/dist/pocketbase.es.mjs"; // ─── PocketBase Init ─────────────────────────────────────────────── const BASE_URL = window.location.origin; const pb = new PocketBase(BASE_URL); // ─── Auth (Firebase-compatible surface) ──────────────────────────── const auth = { get currentUser() { if (!pb.authStore.isValid) return null; const model = pb.authStore.model; // Only return users from 'users' collection, not superusers/admins if (model.collectionName !== 'users') return null; return { uid: model.id, email: model.email, displayName: model.name || null }; }, onAuthStateChanged(callback) { // Fire initial state synchronously (like Firebase does) const current = this.currentUser; callback(current); // Listen for future auth changes pb.authStore.onChange((token, model) => { const user = (model && model.collectionName === 'users') ? { uid: model.id, email: model.email, displayName: model.name || null } : null; callback(user); }); return () => {}; } }; // ─── Standalone Auth Functions ───────────────────────────────────── function onAuthStateChanged(authRef, callback) { return auth.onAuthStateChanged(callback); } async function signInWithEmailAndPassword(a, email, password) { await pb.collection('users').authWithPassword(email, password); return { user: auth.currentUser }; } async function createUserWithEmailAndPassword(a, email, password) { await pb.collection('users').create({ email, password, passwordConfirm: password, emailVisibility: true }); await pb.collection('users').authWithPassword(email, password); return { user: auth.currentUser }; } async function signOut(a) { pb.authStore.clear(); } async function sendPasswordResetEmail(a, email) { await pb.collection('users').requestPasswordReset(email); } async function updateProfile(user, data) { const updateData = {}; if (data.displayName) updateData.name = data.displayName; await pb.collection('users').update(user.uid, updateData); return { ...user, displayName: data.displayName || user.displayName }; } async function updatePassword(user, newPassword) { await pb.collection('users').update(user.uid, { password: newPassword, passwordConfirm: newPassword }); } async function reauthenticateWithCredential(user, credential) { await pb.collection('users').authWithPassword(credential._email, credential._password); } const EmailAuthProvider = { credential: (email, password) => ({ _email: email, _password: password }) }; // ─── Firestore-compatible DB Surface ─────────────────────────────── const db = {}; function collection(parent, name, ...subPath) { if (subPath.length === 0) { return { _type: 'collection', _name: name, _path: [name] }; } const collectionName = subPath[subPath.length - 1]; return { _type: 'collection', _name: collectionName, _path: [name, ...subPath], _userId: subPath[0] }; } function doc(db, ...path) { if (path.length === 2) return { _type: 'doc', _collection: path[0], _id: path[1] }; return { _type: 'doc', _collection: path[path.length - 2], _id: path[path.length - 1], _userId: path[1] }; } // ─── Query Builder ───────────────────────────────────────────────── function where(field, op, value) { return { _type: 'where', field, op, value }; } function orderBy(field, direction) { return { _type: 'orderBy', field, direction }; } function limit(n) { return { _type: 'limit', value: n }; } function query(collRef, ...constraints) { return { _type: 'query', collection: collRef, constraints }; } // ─── PocketBase Filter Builder ───────────────────────────────────── const OP_MAP = {'==': '=', '!=': '!=', '>': '>', '<': '<', '>=': '>=', '<=': '<=', 'in': '?=', 'not-in': '?!='}; function buildFilter(constraints) { const parts = []; for (const c of constraints) { if (c._type !== 'where') continue; const pbOp = OP_MAP[c.op] || '='; if (pbOp === '?=' && Array.isArray(c.value)) { const orParts = c.value.map(v => typeof v === 'string' ? `${c.field} ?= "${v}"` : `${c.field} ?= ${v}`); parts.push(`(${orParts.join(' || ')})`); } else if (pbOp === '?=') { const v = typeof c.value === 'string' ? `"${c.value}"` : c.value; parts.push(`${c.field} ${pbOp} ${v}`); } else { const v = typeof c.value === 'string' ? `"${c.value}"` : c.value; parts.push(`${c.field} ${pbOp} ${v}`); } } return parts.length > 0 ? parts.join(' && ') : ''; } function buildSort(constraints) { return constraints.filter(c => c._type === 'orderBy').map(c => (c.direction === 'desc' ? '-' : '+') + c.field); } // ─── CRUD Operations ─────────────────────────────────────────────── async function getDocs(q) { const collName = q.collection._name; const filter = buildFilter(q.constraints); const sort = buildSort(q.constraints); const limitVal = q.constraints.find(c => c._type === 'limit'); const options = { requestKey: null }; if (filter) options.filter = filter; if (sort.length > 0) options.sort = sort.join(','); if (limitVal) options.perPage = limitVal.value; try { const records = limitVal ? await pb.collection(collName).getList(1, limitVal.value, options) : await pb.collection(collName).getFullList(options); const items = limitVal ? records.items : records; const docs = items.map(rec => ({ id: rec.id, data: () => convertFromPB(rec), exists: true, ref: { id: rec.id } })); docs.forEach = function(cb) { this.forEach(cb); }; return docs; } catch (err) { console.warn(`PocketBase getDocs(${collName}):`, err.message); const empty = []; empty.forEach = function(cb) {}; return empty; } } async function addDoc(collRef, data) { const record = await pb.collection(collRef._name).create(convertToPB(data)); return { id: record.id }; } async function updateDoc(docRef, data) { await pb.collection(docRef._collection).update(docRef._id, convertToPB(data)); } async function deleteDoc(docRef) { await pb.collection(docRef._collection).delete(docRef._id); } async function getDoc(docRef) { try { const record = await pb.collection(docRef._collection).getOne(docRef._id); return { id: record.id, data: () => convertFromPB(record), exists: true }; } catch (err) { return { exists: false, data: () => null }; } } function setDoc(docRef, data) { const pbData = convertToPB(data); return pb.collection(docRef._collection).update(docRef._id, pbData).catch(() => pb.collection(docRef._collection).create({ ...pbData, id: docRef._id }) ).then(rec => ({ id: rec.id })); } function onSnapshot(queryOrDocRef, callback) { if (queryOrDocRef._type === 'query') { getDocs(queryOrDocRef).then(docs => callback({ docs, forEach: cb => docs.forEach(cb) })); } else { getDoc(queryOrDocRef).then(doc => callback(doc)); } return () => {}; } // ─── Timestamps ──────────────────────────────────────────────────── function serverTimestamp() { return new Date().toISOString(); } class Timestamp { constructor(date) { this._date = date || new Date(); } toDate() { return this._date; } toMillis() { return this._date.getTime(); } } // ─── Data Conversion ─────────────────────────────────────────────── // Add your app's timestamp field names here const TIMESTAMP_FIELDS = new Set([ 'writeupTime', 'createdAt', 'updatedAt', 'lastUpdated', 'appointmentDateTime', 'completionTime', 'lastActivity', 'timestamp', 'dateCreated', 'startDate', 'endDate', 'deadline' ]); function convertToPB(data) { const result = {}; for (const [key, value] of Object.entries(data)) { if (value === undefined) continue; if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) { result[key] = JSON.stringify(value); } else if (value instanceof Date) { result[key] = value.toISOString(); } else { result[key] = value; } } return result; } function convertFromPB(record) { const data = { ...record }; for (const field of TIMESTAMP_FIELDS) { if (typeof data[field] === 'string' && data[field].match(/^\d{4}-\d{2}-\d{2}T/)) { data[field] = new Timestamp(new Date(data[field])); } } for (const [key, value] of Object.entries(data)) { if (typeof value === 'string' && (value.startsWith('{') || value.startsWith('['))) { try { const p = JSON.parse(value); if (typeof p === 'object') data[key] = p; } catch (e) {} } } return data; } // ─── Exports ─────────────────────────────────────────────────────── export { auth, db }; export { signInWithEmailAndPassword, createUserWithEmailAndPassword, signOut, sendPasswordResetEmail, updateProfile, onAuthStateChanged, updatePassword, reauthenticateWithCredential, EmailAuthProvider }; export { collection, doc, query, where, orderBy, limit, getDocs, getDoc, addDoc, updateDoc, deleteDoc, setDoc, onSnapshot, serverTimestamp, Timestamp };