import { createEffect, onCleanup } from "solid-js"; import { createStore } from "solid-js/store"; import { RecordModel, RecordSubscription } from "pocketbase"; import * as pbf from "@nedpals/pbf"; import { z } from "zod"; import { nanoid } from "nanoid"; import { usePB } from "./context.js"; interface IUseRecordOptions { realtime?: boolean; validator?: z.AnyZodObject; expand?: string[]; } export function useRecord(collection: string, id: () => string, options?: IUseRecordOptions) { const pb = usePB(); const requestKey = nanoid(); const [record, setRecord] = createStore<{ loading: boolean, record: T & RecordModel | null, error: Error | null, }>({ loading: true, error: null, record: null, }); const v = options?.validator ?? ({ parse: (i) => id }); createEffect(() => { const cid = id(); const setRecordChecked: typeof setRecord = (...args) => { if (cid !== id()) return; (setRecord as any)(...args); } setRecordChecked("loading", () => true) setRecordChecked("error", () => null) pb.collection(collection).getOne(cid, { expand: options?.expand ? options.expand.join(",") : undefined, requestKey, }).then((value) => { setRecordChecked("record", () => v.parse(value) as T & RecordModel) }).catch((error) => { setRecordChecked("error", () => error as Error) }).finally(() => { setRecordChecked("loading", () => false) }); const sub = pb.realtime.subscribe(collection + "/" + cid, (value) => { //TODO: Check what actions exist and if some needs special handling setRecordChecked("record", () => v.parse(value.record) as T & RecordModel) }, { query: { expand: options?.expand ? options.expand.join(",") : undefined, requestKey: requestKey + "-rt", } }); sub.catch(err => { console.error("Error in realtime subscription", err) }); onCleanup(() => { pb.cancelRequest(requestKey); pb.cancelRequest(requestKey + "-rt"); sub.then(s => s()); }); }) return record; } interface IUseRecordsOptions { realtime?: boolean; validator?: z.AnyZodObject; filter?: () => pbf.MaybeFilter | undefined, expand?: string[]; } export function useRecords(collection: string, options: IUseRecordsOptions = { expand: [] }) { const pb = usePB(); const requestKey = nanoid(); const fstr = () => options.filter && options.filter() ? pbf.stringify(options.filter()) : undefined; const v = options.validator ?? ({ parse: (i) => i }); const [records, setRecords] = createStore<{ loading: boolean, records: (T & RecordModel)[], error: Error | null, }>({ records: [], loading: true, error: null, }); createEffect(() => { const cf = fstr(); const setRecordChecked: typeof setRecords = (...args) => { if (cf !== fstr()) return; (setRecords as any)(...args); } setRecordChecked("loading", () => true) setRecordChecked("error", () => null) pb.collection(collection).getFullList({ requestKey, filter: cf, expand: options?.expand ? options.expand.join(",") : undefined, }).then((values) => { setRecordChecked("records", () => values.map(value => v.parse(value) as T & RecordModel)) }).catch((error) => { setRecordChecked("error", () => error as Error) }).finally(() => { setRecordChecked("loading", () => false) }); const sub = pb.realtime.subscribe(collection, (event: RecordSubscription) => { console.log("Event", event); const rec = v.parse(event.record) as T & RecordModel; switch (event.action) { case "create": setRecordChecked("records", (records) => [...records, rec]); break; case "update": setRecordChecked("records", (records) => records.map(r => r.id === rec.id ? rec : r)); break; case "delete": setRecordChecked("records", (records) => records.filter(r => r.id !== rec.id)); break; default: console.log("Unknown action", event.action); } }, { requestKey: requestKey + "-rt", query: { filter: cf, expand: options?.expand ? options.expand.join(",") : undefined, } }); sub.then(unsub => { console.log("Subscribed to realtime changes!"); }); sub.catch(err => { console.error("Error in realtime subscription", err) }); onCleanup(() => { pb.cancelRequest(requestKey); pb.cancelRequest(requestKey + "-rt"); sub.then(s => s()); }); }) return records; }