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