From 79367b3de01a553180025a367086a96fb6a4b56d Mon Sep 17 00:00:00 2001 From: myrmidex Date: Sun, 28 Jun 2026 02:32:39 +0200 Subject: [PATCH] 33 - Add cookie-session API client for the dev proxy --- frontend/src/lib/api.ts | 68 +++++++++++++++++++++++++++++++++++++++ frontend/src/types/api.ts | 19 +++++++++++ 2 files changed, 87 insertions(+) create mode 100644 frontend/src/lib/api.ts create mode 100644 frontend/src/types/api.ts diff --git a/frontend/src/lib/api.ts b/frontend/src/lib/api.ts new file mode 100644 index 0000000..37592fa --- /dev/null +++ b/frontend/src/lib/api.ts @@ -0,0 +1,68 @@ +/** + * Thin API client for the Symfony + API Platform backend. + * + * Auth is cookie-session (#30): every request sends credentials so the session + * cookie set by POST /api/login flows on subsequent calls. In dev the Vite proxy + * keeps /api same-origin, so no CORS is involved. JSON-LD (Hydra) is the wire + * format; PATCH uses application/merge-patch+json per API Platform. + */ + +const BASE = '/api' + +/** Thrown for any non-2xx response; carries the status and raw body. */ +export class ApiError extends Error { + readonly status: number + readonly body: string + + constructor(status: number, body: string) { + super(`API request failed with status ${status}`) + this.name = 'ApiError' + this.status = status + this.body = body + } +} + +async function request(path: string, init: RequestInit = {}): Promise { + const res = await fetch(`${BASE}${path}`, { + credentials: 'include', + ...init, + headers: { + Accept: 'application/ld+json', + ...init.headers, + }, + }) + + if (!res.ok) { + throw new ApiError(res.status, await res.text()) + } + + // 204 No Content (e.g. DELETE) has an empty body — don't parse it as JSON. + // This cast assumes 204 only occurs where T is void (delete); the API does + // not 204 on GET/POST/PATCH, so a typed get never reaches this path. + if (res.status === 204) { + return undefined as T + } + + return res.json() as Promise +} + +export const api = { + get: (path: string): Promise => request(path), + + post: (path: string, body: unknown): Promise => + request(path, { + method: 'POST', + body: JSON.stringify(body), + headers: { 'Content-Type': 'application/ld+json' }, + }), + + patch: (path: string, body: unknown): Promise => + request(path, { + method: 'PATCH', + body: JSON.stringify(body), + headers: { 'Content-Type': 'application/merge-patch+json' }, + }), + + delete: (path: string): Promise => + request(path, { method: 'DELETE' }), +} diff --git a/frontend/src/types/api.ts b/frontend/src/types/api.ts new file mode 100644 index 0000000..7347603 --- /dev/null +++ b/frontend/src/types/api.ts @@ -0,0 +1,19 @@ +/** + * JSON-LD / Hydra envelope shapes shared by every API Platform resource. + * Domain models (Scenario/Bucket/Stream) are added by the feature tickets that + * consume them — this file only types the transport envelope. + */ + +/** Fields every API Platform item carries in a JSON-LD response. */ +export interface HydraResource { + '@id': string + '@type': string +} + +/** A Hydra collection response: items live under `member`. */ +export interface HydraCollection { + '@id': string + '@type': string + member: T[] + totalItems: number +}