Files
silo/web/src/api/client.ts
Forbes 50923cf56d feat: production release with React SPA, file attachments, and deploy tooling
Backend:
- Add file_handlers.go: presigned upload/download for item attachments
- Add item_files.go: item file and thumbnail DB operations
- Add migration 011: item_files table and thumbnail_key column
- Update items/projects/relationships DB with extended field support
- Update routes: React SPA serving from web/dist, file upload endpoints
- Update auth handlers and middleware for cookie + bearer token auth
- Remove Go HTML templates (replaced by React SPA)
- Update storage client for presigned URL generation

Frontend:
- Add TagInput component for tag/keyword entry
- Add SVG assets for Silo branding and UI icons
- Update API client and types for file uploads, auth, extended fields
- Update AuthContext for session-based auth flow
- Update LoginPage, ProjectsPage, SchemasPage, SettingsPage
- Fix tsconfig.node.json

Deployment:
- Update config.prod.yaml: single-binary SPA layout at /opt/silo
- Update silod.service: ReadOnlyPaths for /opt/silo
- Add scripts/deploy.sh: build, package, ship, migrate, start
- Update docker-compose.yaml and Dockerfile
- Add frontend-spec.md design document
2026-02-07 13:35:22 -06:00

70 lines
1.5 KiB
TypeScript

import type { ErrorResponse } from "./types";
export class ApiError extends Error {
constructor(
public status: number,
public error: string,
message?: string,
) {
super(message ?? error);
this.name = "ApiError";
}
}
async function request<T>(url: string, options?: RequestInit): Promise<T> {
const res = await fetch(url, {
...options,
credentials: "include",
headers: {
"Content-Type": "application/json",
...options?.headers,
},
});
if (res.status === 401) {
throw new ApiError(401, "unauthorized");
}
if (!res.ok) {
let body: ErrorResponse | undefined;
try {
body = (await res.json()) as ErrorResponse;
} catch {
// non-JSON error response
}
throw new ApiError(
res.status,
body?.error ?? `HTTP ${res.status}`,
body?.message,
);
}
if (res.status === 204) {
return undefined as T;
}
return res.json() as Promise<T>;
}
export function get<T>(url: string): Promise<T> {
return request<T>(url);
}
export function post<T>(url: string, body?: unknown): Promise<T> {
return request<T>(url, {
method: "POST",
body: body != null ? JSON.stringify(body) : undefined,
});
}
export function put<T>(url: string, body?: unknown): Promise<T> {
return request<T>(url, {
method: "PUT",
body: body != null ? JSON.stringify(body) : undefined,
});
}
export function del(url: string): Promise<void> {
return request<void>(url, { method: "DELETE" });
}