initial commit
This commit is contained in:
@@ -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,
|
||||
});
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user