33 - Add cookie-session API client for the dev proxy
This commit is contained in:
parent
28757d1e6b
commit
79367b3de0
2 changed files with 87 additions and 0 deletions
68
frontend/src/lib/api.ts
Normal file
68
frontend/src/lib/api.ts
Normal file
|
|
@ -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<T>(path: string, init: RequestInit = {}): Promise<T> {
|
||||||
|
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<T> never reaches this path.
|
||||||
|
if (res.status === 204) {
|
||||||
|
return undefined as T
|
||||||
|
}
|
||||||
|
|
||||||
|
return res.json() as Promise<T>
|
||||||
|
}
|
||||||
|
|
||||||
|
export const api = {
|
||||||
|
get: <T>(path: string): Promise<T> => request<T>(path),
|
||||||
|
|
||||||
|
post: <T>(path: string, body: unknown): Promise<T> =>
|
||||||
|
request<T>(path, {
|
||||||
|
method: 'POST',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: { 'Content-Type': 'application/ld+json' },
|
||||||
|
}),
|
||||||
|
|
||||||
|
patch: <T>(path: string, body: unknown): Promise<T> =>
|
||||||
|
request<T>(path, {
|
||||||
|
method: 'PATCH',
|
||||||
|
body: JSON.stringify(body),
|
||||||
|
headers: { 'Content-Type': 'application/merge-patch+json' },
|
||||||
|
}),
|
||||||
|
|
||||||
|
delete: (path: string): Promise<void> =>
|
||||||
|
request<void>(path, { method: 'DELETE' }),
|
||||||
|
}
|
||||||
19
frontend/src/types/api.ts
Normal file
19
frontend/src/types/api.ts
Normal file
|
|
@ -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<T> {
|
||||||
|
'@id': string
|
||||||
|
'@type': string
|
||||||
|
member: T[]
|
||||||
|
totalItems: number
|
||||||
|
}
|
||||||
Loading…
Reference in a new issue