forked from EduCraft/curriculum-project-hub
50 lines
1.4 KiB
TypeScript
50 lines
1.4 KiB
TypeScript
/** 与 /database/api/* 的约定一致;401 时清空会话(回到登录视图)。 */
|
|
|
|
import { me } from "./stores.js";
|
|
|
|
export class ApiError extends Error {
|
|
constructor(
|
|
readonly status: number,
|
|
readonly code: string,
|
|
message: string,
|
|
readonly details?: Record<string, unknown>,
|
|
) {
|
|
super(message);
|
|
this.name = "ApiError";
|
|
}
|
|
}
|
|
|
|
export class UnauthenticatedError extends Error {
|
|
constructor() {
|
|
super("unauthenticated");
|
|
this.name = "UnauthenticatedError";
|
|
}
|
|
}
|
|
|
|
interface RequestOpts {
|
|
readonly method?: string;
|
|
readonly body?: unknown;
|
|
}
|
|
|
|
export async function api<T = unknown>(path: string, opts: RequestOpts = {}): Promise<T> {
|
|
const res = await fetch(path, {
|
|
credentials: "same-origin",
|
|
method: opts.method ?? "GET",
|
|
...(opts.body !== undefined
|
|
? { headers: { "Content-Type": "application/json" }, body: JSON.stringify(opts.body) }
|
|
: {}),
|
|
});
|
|
if (res.status === 401) {
|
|
me.set(null);
|
|
throw new UnauthenticatedError();
|
|
}
|
|
if (res.status === 204) return null as T;
|
|
const text = await res.text();
|
|
const data = text === "" ? null : (JSON.parse(text) as unknown);
|
|
if (!res.ok) {
|
|
const err = (data as { error?: { code?: string; message?: string } } | null)?.error ?? {};
|
|
throw new ApiError(res.status, err.code ?? "unknown", err.message ?? res.statusText, err as Record<string, unknown>);
|
|
}
|
|
return data as T;
|
|
}
|