/* ============================================================================ STRAPI WRITE - SERVER ONLY ---------------------------------------------------------------------------- ~/lib/strapi.ts is a read client: the site renders from the CMS and never mutates it. The two public forms in this section are the exception - an event registration and an application to a call - so the POST lives here, next to the routes that need it, and reuses that module's error type so a caller handles one failure shape. The `.server` suffix keeps it out of the browser bundle; it is imported by `action` only. ========================================================================= */ import { getServerEnv } from "~/lib/env.server"; import { StrapiError } from "~/lib/strapi"; const DEFAULT_TIMEOUT_MS = 10_000; export interface StrapiWriteInit { readonly timeoutMs?: number; readonly signal?: AbortSignal; readonly forwardedFor?: string; } interface StrapiErrorEnvelope { readonly error?: { readonly status?: number; readonly name?: string; readonly message?: string; readonly details?: unknown; }; } /** * `POST /api/{resource}` with Strapi's `{ data }` envelope, returning the * created entity. Every failure - transport, timeout, 4xx, 5xx - arrives as a * StrapiError, so the action can answer with one localised message instead of * leaking the CMS's own wording to the reader. */ export async function createStrapiEntry( resource: string, data: Readonly>, init: StrapiWriteInit = {}, ): Promise { const env = getServerEnv(); const url = `${env.STRAPI_URL}/api/${resource.replace(/^\/+/, "")}`; const headers: Record = { "content-type": "application/json", accept: "application/json", }; if (env.STRAPI_TOKEN) headers.authorization = `Bearer ${env.STRAPI_TOKEN}`; if (init.forwardedFor) headers["x-forwarded-for"] = init.forwardedFor; const timeoutMs = init.timeoutMs ?? DEFAULT_TIMEOUT_MS; const timeoutSignal = AbortSignal.timeout(timeoutMs); const signal = init.signal ? AbortSignal.any([init.signal, timeoutSignal]) : timeoutSignal; let response: Response; try { response = await fetch(url, { method: "POST", headers, body: JSON.stringify({ data }), signal, }); } catch (cause) { const timedOut = cause instanceof Error && cause.name === "TimeoutError"; throw new StrapiError({ message: timedOut ? `Strapi write timed out after ${timeoutMs}ms: ${url}` : `Strapi write failed: ${url}`, status: 0, url, cause, }); } if (!response.ok) { let body: StrapiErrorEnvelope["error"]; try { body = ((await response.json()) as StrapiErrorEnvelope).error; } catch { body = undefined; } throw new StrapiError({ message: body?.message ?? `Strapi responded ${response.status} ${response.statusText} for ${url}`, status: response.status, statusText: response.statusText, url, body, }); } if (response.status === 204) return undefined as T; try { const payload = (await response.json()) as { data?: T }; return payload.data as T; } catch (cause) { throw new StrapiError({ message: `Strapi returned a non-JSON body for ${url}`, status: response.status, statusText: response.statusText, url, cause, }); } }