WORKFLOW-APP/lib/pb.ts
SJCoetz bb97fef453 feat: initialize full ysterbaard frontend application
Add complete production-ready Ysterbaard PocketBase registry browser frontend:
- Next.js 14 App Router with TypeScript, Tailwind CSS, and TanStack React Query
- Secure server-side PocketBase API proxy to avoid exposing superuser tokens
- Read-only directors and companies registry browse pages with search, filtering, and pagination
- Client onboarding workflow for creating and managing client submissions
- Docker container configuration for easy deployment
- All supporting UI components, utility functions, and configuration files
2026-07-23 19:10:47 +02:00

104 lines
3.9 KiB
TypeScript

// lib/pb.ts
// SERVER-ONLY PocketBase client.
import "server-only";
const BASE_URL = process.env.PB_URL;
const TOKEN = process.env.PB_TOKEN;
if (!BASE_URL) throw new Error("PB_URL is not set.");
if (!TOKEN || TOKEN === "PASTE_SUPERUSER_JWT_HERE") throw new Error("PB_TOKEN missing");
export interface PBList<T> { page: number; perPage: number; totalItems: number; totalPages: number; items: T[]; }
export interface PBRecord { id: string; collectionId: string; collectionName: string; [key: string]: unknown; }
function buildUrl(path: string, query: Record<string, string | number | undefined> = {}): string {
const params = new URLSearchParams();
for (const [k, v] of Object.entries(query)) {
if (v !== undefined && v !== null && v !== "") params.set(k, String(v));
}
const qs = params.toString();
return `${BASE_URL}${path}${qs ? `?${qs}` : ""}`;
}
export async function pbFetch<T>(path: string, query: Record<string, string | number | undefined> = {}): Promise<T> {
const url = buildUrl(path, query);
const res = await fetch(url, {
headers: { Authorization: TOKEN as string },
cache: "no-store",
});
if (!res.ok) {
const body = await res.text();
throw new Error(`PocketBase ${res.status} on ${path}: ${body.slice(0, 200)}`);
}
return (await res.json()) as T;
}
export async function createRecordMultipart<T extends PBRecord>(collection: string, fields: Record<string, string>, files: { field: string; file: File }[] = []): Promise<T> {
const form = new FormData();
for (const [k, v] of Object.entries(fields)) form.set(k, v);
for (const { field, file } of files) form.append(field, file, file.name);
const url = `${BASE_URL}/api/collections/${collection}/records`;
const res = await fetch(url, {
method: "POST",
headers: { Authorization: TOKEN as string },
body: form,
cache: "no-store",
});
if (!res.ok) {
const body = await res.text();
throw new Error(`PocketBase ${res.status} on POST ${collection}: ${body.slice(0, 300)}`);
}
return (await res.json()) as T;
}
export async function updateRecord<T extends PBRecord>(collection: string, id: string, fields: Record<string, unknown>): Promise<T> {
const url = `${BASE_URL}/api/collections/${collection}/records/${encodeURIComponent(id)}`;
const res = await fetch(url, {
method: "PATCH",
headers: { Authorization: TOKEN as string, "Content-Type": "application/json" },
body: JSON.stringify(fields),
cache: "no-store",
});
if (!res.ok) {
const body = await res.text();
throw new Error(`PocketBase ${res.status} on PATCH ${collection}/${id}: ${body.slice(0, 300)}`);
}
return (await res.json()) as T;
}
export async function fetchCollection<T extends PBRecord>(collection: string, params: { page?: number; perPage?: number; sort?: string; filter?: string; expand?: string; fields?: string } = {}): Promise<PBList<T>> {
return pbFetch<PBList<T>>(`/api/collections/${collection}/records`, {
page: params.page ?? 1,
perPage: params.perPage ?? 25,
sort: params.sort,
filter: params.filter,
expand: params.expand,
fields: params.fields,
});
}
/**
* JSON create — for collections without file fields.
*/
export async function createRecord<T extends PBRecord>(
collection: string,
fields: Record<string, unknown>,
): Promise<T> {
const url = `${BASE_URL}/api/collections/${collection}/records`;
const res = await fetch(url, {
method: "POST",
headers: { Authorization: TOKEN as string, "Content-Type": "application/json" },
body: JSON.stringify(fields),
cache: "no-store",
});
if (!res.ok) {
const body = await res.text();
throw new Error(`PocketBase ${res.status} on POST ${collection}: ${body.slice(0, 300)}`);
}
return (await res.json()) as T;
}
export async function fetchRecord<T = unknown>(collection: string, id: string, expand?: string): Promise<T> {
return pbFetch<T>(`/api/collections/${collection}/records/${encodeURIComponent(id)}`, { expand });
}