From f2348b0177aee872b67a744284722f1ba202029d Mon Sep 17 00:00:00 2001 From: Julien Oculi Date: Thu, 13 Jun 2024 12:22:49 +0200 Subject: [PATCH] feat(api): :sparkles: add api communication helpers --- .vscode/settings.json | 5 +++- src/utils.ts | 69 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 1 deletion(-) create mode 100644 src/utils.ts diff --git a/.vscode/settings.json b/.vscode/settings.json index 91e6151..80257b1 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -18,6 +18,9 @@ "*.tsx": "${capture}.*" }, "cSpell.words": [ + "magiclink", + "RPID", + "simplewebauthn", "startserviceworker", "technoshop", "Technoshop", @@ -25,7 +28,7 @@ ], "cssvar.enable": true, "cssvar.files": ["./_fresh/*"], - "conventionalCommits.scopes": ["css", "config", "ui", "pwa"], + "conventionalCommits.scopes": ["css", "config", "ui", "pwa", "api"], "[ignore]": { "editor.defaultFormatter": "foxundermoon.shell-format" } diff --git a/src/utils.ts b/src/utils.ts new file mode 100644 index 0000000..ce4530a --- /dev/null +++ b/src/utils.ts @@ -0,0 +1,69 @@ +import { JsonValue } from '$std/json/common.ts' + +export type JsonCompatible = JsonValue | { toJSON(): JsonValue } | unknown + +export function respondApi< + Kind extends ApiPayload['kind'], + Payload extends JsonCompatible, +>(kind: Kind, payload?: Payload, status?: number, statusText?: string): Response { + if (kind === 'error') { + return Response.json({ + kind: 'error', + error: String(payload ?? ''), + } as ApiPayload, { + status: status ?? 500, + statusText + }) + } + + return Response.json({ + kind: 'success', + data: payload ?? null, + } as ApiPayload) +} + +export async function requestApi< + Payload extends JsonCompatible | undefined, + ApiResponse extends JsonCompatible, +>( + route: string, + method: 'GET' | 'POST' | 'DELETE' | 'PATCH', + payload?: Payload | null, +): Promise { + const csrf = getCookie('_CSRF') ?? '' + + const base = new URL('/api/', location.origin) + const endpoint = new URL(route.startsWith('/') ? `.${route}` : route, base.href) + + const response = await fetch(endpoint, { + method, + headers: { + 'Content-Type': 'application/json; charset=utf-8', + 'X-CSRF-TOKEN': csrf + }, + body: payload ? JSON.stringify(payload) : null, + }) + const apiPayload = await response.json() as ApiPayload + + if (apiPayload.kind === 'error') { + throw new Error(`api request error while getting "${endpoint.href}"`, { + cause: apiPayload.error, + }) + } + + return apiPayload.data +} + +export type ApiPayload = { + kind: 'success' + data: ApiResponse +} | { + kind: 'error' + error: string +} + +function getCookie(name: string): string | undefined { + const cookiesEntries = document.cookie.split(';').map(cookie => cookie.trim().split('=')) + const cookies = Object.fromEntries(cookiesEntries) + return cookies[name] +} \ No newline at end of file