82 lines
2.7 KiB
TypeScript
82 lines
2.7 KiB
TypeScript
/**
|
|
* Thin PostgREST client using fetch — no Supabase SDK dependency.
|
|
* Used by both the Worker (history queries) and the Durable Object (message persistence).
|
|
*/
|
|
export class PostgREST {
|
|
private baseUrl: string;
|
|
private serviceRoleKey: string;
|
|
|
|
constructor(supabaseUrl: string, serviceRoleKey: string) {
|
|
this.baseUrl = `${supabaseUrl}/rest/v1`;
|
|
this.serviceRoleKey = serviceRoleKey;
|
|
}
|
|
|
|
private headers(): Record<string, string> {
|
|
return {
|
|
"apikey": this.serviceRoleKey,
|
|
"Authorization": `Bearer ${this.serviceRoleKey}`,
|
|
"Content-Type": "application/json",
|
|
"Prefer": "return=representation",
|
|
};
|
|
}
|
|
|
|
/** Insert a row and return the inserted data. */
|
|
async insert<T>(table: string, data: Record<string, unknown>): Promise<T[]> {
|
|
const res = await fetch(`${this.baseUrl}/${table}`, {
|
|
method: "POST",
|
|
headers: this.headers(),
|
|
body: JSON.stringify(data),
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`PostgREST insert failed (${res.status}): ${body}`);
|
|
}
|
|
return res.json() as Promise<T[]>;
|
|
}
|
|
|
|
/** Upsert a row (requires Prefer: resolution=merge-duplicates). */
|
|
async upsert<T>(table: string, data: Record<string, unknown>, onConflict: string): Promise<T[]> {
|
|
const headers = this.headers();
|
|
headers["Prefer"] = "return=representation,resolution=merge-duplicates";
|
|
const res = await fetch(`${this.baseUrl}/${table}?on_conflict=${onConflict}`, {
|
|
method: "POST",
|
|
headers,
|
|
body: JSON.stringify(data),
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`PostgREST upsert failed (${res.status}): ${body}`);
|
|
}
|
|
return res.json() as Promise<T[]>;
|
|
}
|
|
|
|
/** Select rows with PostgREST query string. */
|
|
async select<T>(table: string, query: string): Promise<T[]> {
|
|
const res = await fetch(`${this.baseUrl}/${table}?${query}`, {
|
|
method: "GET",
|
|
headers: this.headers(),
|
|
});
|
|
if (!res.ok) {
|
|
const body = await res.text();
|
|
throw new Error(`PostgREST select failed (${res.status}): ${body}`);
|
|
}
|
|
return res.json() as Promise<T[]>;
|
|
}
|
|
|
|
/** Select with exact count header for unread queries. */
|
|
async count(table: string, query: string): Promise<number> {
|
|
const headers = this.headers();
|
|
headers["Prefer"] = "count=exact";
|
|
headers["Range-Unit"] = "items";
|
|
headers["Range"] = "0-0";
|
|
const res = await fetch(`${this.baseUrl}/${table}?${query}`, {
|
|
method: "HEAD",
|
|
headers,
|
|
});
|
|
const contentRange = res.headers.get("Content-Range");
|
|
if (!contentRange) return 0;
|
|
// Content-Range format: "0-0/42" or "*/0"
|
|
const total = contentRange.split("/")[1];
|
|
return total === "*" ? 0 : parseInt(total, 10);
|
|
}
|
|
}
|