First commit

This commit is contained in:
Fabian Stamm 2025-02-18 19:26:42 +01:00
commit 7d13a3ea8b
7 changed files with 267 additions and 0 deletions

2
.gitignore vendored Normal file
View File

@ -0,0 +1,2 @@
dist/
node_modules

29
package.json Normal file
View File

@ -0,0 +1,29 @@
{
"name": "solid-pb",
"version": "1.0.0",
"type": "module",
"license": "MIT",
"main": "./dist/index.js",
"exports": {
".": {
"default": "./dist/index.js"
}
},
"types": "./dist/index.d.ts",
"scripts": {
"dev": "tsc -w",
"build": "tsc -w",
"prepublish": "tsc"
},
"dependencies": {
"@nedpals/pbf": "^1.3.2"
},
"peerDependencies": {
"pocketbase": "^0.25.1",
"solid-js": "^1.9.4",
"zod": "^3.24.2"
},
"devDependencies": {
"typescript": "^5.7.3"
}
}

15
src/context.ts Normal file
View File

@ -0,0 +1,15 @@
import { createContext, useContext } from "solid-js";
import PocketBaseClient from "pocketbase";
const PBContext = createContext<PocketBaseClient>();
export const usePB = () => {
const ctx = useContext(PBContext)
if (!ctx) throw new Error("This hook must be used within a PBContext.Provider!");
return ctx;
};
export default PBContext;

159
src/hooks.ts Normal file
View File

@ -0,0 +1,159 @@
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 { usePB } from "./context.js";
interface IUseRecordOptions {
realtime?: boolean;
validator?: z.AnyZodObject;
expand?: string[];
}
export function useRecord<T>(collection: string, id: () => string, options?: IUseRecordOptions) {
const pb = usePB();
const requestKey = crypto.randomUUID();
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<pbf.Filter> | undefined,
expand?: string[];
}
export function useRecords<T>(collection: string, options: IUseRecordsOptions = { expand: [] }) {
const pb = usePB();
const requestKey = crypto.randomUUID();
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<T & RecordModel>) => {
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;
}

3
src/index.ts Normal file
View File

@ -0,0 +1,3 @@
export { default as PBContext, usePB } from "./context.js";
export { useRecord, useRecords } from "./hooks.js";

12
tsconfig.json Normal file
View File

@ -0,0 +1,12 @@
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "nodenext",
"target": "ESNext",
"declaration": true,
"outDir": "dist",
},
"include": [
"src/**/*.ts"
]
}

47
yarn.lock Normal file
View File

@ -0,0 +1,47 @@
# THIS IS AN AUTOGENERATED FILE. DO NOT EDIT THIS FILE DIRECTLY.
# yarn lockfile v1
"@nedpals/pbf@^1.3.2":
version "1.3.2"
resolved "https://npm.hibas123.de/@nedpals/pbf/-/pbf-1.3.2.tgz#53b00c65103fbaa90c217ea0a4183119d9f9817e"
integrity sha512-vTEUIbVF8AiufclVzor2HWMoTrnIZn74NTu+B7HlNlB+QR9HEq/9Rf/hufX9QOt54iOoJukP2x3ASsDUKl48MQ==
csstype@^3.1.0:
version "3.1.3"
resolved "https://npm.hibas123.de/csstype/-/csstype-3.1.3.tgz#d80ff294d114fb0e6ac500fbf85b60137d7eff81"
integrity sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==
pocketbase@^0.25.1:
version "0.25.1"
resolved "https://npm.hibas123.de/pocketbase/-/pocketbase-0.25.1.tgz#e2bb606d2e39a992d9b328d67a24a9879ba7337d"
integrity sha512-2IH0KLI/qMNR/E17C7BGWX2FxW7Tead+igLHOWZ45P56v/NyVT18Jnmddeft+3qWWGL1Hog2F8bc4orWV/+Fcg==
seroval-plugins@^1.1.0:
version "1.2.1"
resolved "https://npm.hibas123.de/seroval-plugins/-/seroval-plugins-1.2.1.tgz#fa535e70ade8af553634b2b5c80d8a6fd8c2ff72"
integrity sha512-H5vs53+39+x4Udwp4J5rNZfgFuA+Lt+uU+09w1gYBVWomtAl98B+E9w7yC05Xc81/HgLvJdlyqJbU0fJCKCmdw==
seroval@^1.1.0:
version "1.2.1"
resolved "https://npm.hibas123.de/seroval/-/seroval-1.2.1.tgz#fc671d63445923ab64f7abaf3967c83901382f40"
integrity sha512-yBxFFs3zmkvKNmR0pFSU//rIsYjuX418TnlDmc2weaq5XFDqDIV/NOMPBoLrbxjLH42p4UzRuXHryXh9dYcKcw==
solid-js@^1.9.4:
version "1.9.4"
resolved "https://npm.hibas123.de/solid-js/-/solid-js-1.9.4.tgz#da9b5645f10875a631d93335cd50525ff36b6c27"
integrity sha512-ipQl8FJ31bFUoBNScDQTG3BjN6+9Rg+Q+f10bUbnO6EOTTf5NGerJeHc7wyu5I4RMHEl/WwZwUmy/PTRgxxZ8g==
dependencies:
csstype "^3.1.0"
seroval "^1.1.0"
seroval-plugins "^1.1.0"
typescript@^5.7.3:
version "5.7.3"
resolved "https://npm.hibas123.de/typescript/-/typescript-5.7.3.tgz#919b44a7dbb8583a9b856d162be24a54bf80073e"
integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==
zod@^3.24.2:
version "3.24.2"
resolved "https://npm.hibas123.de/zod/-/zod-3.24.2.tgz#8efa74126287c675e92f46871cfc8d15c34372b3"
integrity sha512-lY7CDW43ECgW9u1TcT3IoXHflywfVqDYze4waEz812jR/bZ8FHDsl7pFQoSZTz5N+2NqRXs8GBwnAwo3ZNxqhQ==