initial commit

This commit is contained in:
2026-09-11 02:46:56 +02:00
commit 20b384039c
288 changed files with 46557 additions and 0 deletions
@@ -0,0 +1,112 @@
/* ============================================================================
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;
}
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<T>(
resource: string,
data: Readonly<Record<string, unknown>>,
init: StrapiWriteInit = {},
): Promise<T> {
const env = getServerEnv();
const url = `${env.STRAPI_URL}/api/${resource.replace(/^\/+/, "")}`;
const headers: Record<string, string> = {
"content-type": "application/json",
accept: "application/json",
};
if (env.STRAPI_TOKEN) headers.authorization = `Bearer ${env.STRAPI_TOKEN}`;
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,
});
}
}