first commit
This commit is contained in:
80
packages/core/src/plugin-utils.ts
Normal file
80
packages/core/src/plugin-utils.ts
Normal file
@@ -0,0 +1,80 @@
|
||||
/**
|
||||
* Shared utilities for plugin admin UIs.
|
||||
*
|
||||
* Plugin admin components (`admin.tsx`) run inside the EmDash admin dashboard.
|
||||
* This module provides the common helpers they all need: API fetching with CSRF
|
||||
* protection, response envelope unwrapping, and type narrowing.
|
||||
*
|
||||
* Import as: `import { apiFetch, parseApiResponse, isRecord } from "emdash/plugin-utils";`
|
||||
*/
|
||||
|
||||
/**
|
||||
* Fetch wrapper that adds the `X-EmDash-Request` CSRF protection header.
|
||||
*
|
||||
* All plugin admin API calls should use this instead of raw `fetch()`.
|
||||
* State-changing endpoints reject requests without this header.
|
||||
*/
|
||||
export function apiFetch(input: string | URL | Request, init?: RequestInit): Promise<Response> {
|
||||
const headers = new Headers(init?.headers);
|
||||
headers.set("X-EmDash-Request", "1");
|
||||
return fetch(input, { ...init, headers });
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse an API response, unwrapping the `{ data: T }` envelope.
|
||||
*
|
||||
* All plugin API routes return success responses wrapped in `{ data: ... }`
|
||||
* by `apiSuccess()`. This helper unwraps that envelope and handles errors.
|
||||
*
|
||||
* On error responses (non-2xx), throws an Error with the server's message
|
||||
* (from `{ error: { message } }`) or the fallback message.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* const res = await apiFetch("/_emdash/api/plugins/my-plugin/items");
|
||||
* const { items } = await parseApiResponse<{ items: Item[] }>(res, "Failed to load items");
|
||||
* ```
|
||||
*/
|
||||
export async function parseApiResponse<T>(
|
||||
response: Response,
|
||||
fallbackMessage = "Request failed",
|
||||
): Promise<T> {
|
||||
if (!response.ok) {
|
||||
throw new Error(await getErrorMessage(response, `${fallbackMessage}: ${response.statusText}`));
|
||||
}
|
||||
const body: { data: T } = await response.json();
|
||||
return body.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the error message from a failed API response.
|
||||
*
|
||||
* Error responses use the shape `{ error: { code, message } }`. This helper
|
||||
* parses that body and returns the message, falling back to the provided default.
|
||||
* Swallows JSON parse failures gracefully.
|
||||
*
|
||||
* @example
|
||||
* ```ts
|
||||
* if (!res.ok) {
|
||||
* setError(await getErrorMessage(res, "Failed to save"));
|
||||
* return;
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export async function getErrorMessage(response: Response, fallback: string): Promise<string> {
|
||||
const body: unknown = await response.json().catch(() => ({}));
|
||||
if (isRecord(body) && isRecord(body.error)) {
|
||||
const msg = body.error.message;
|
||||
if (typeof msg === "string") return msg;
|
||||
}
|
||||
return fallback;
|
||||
}
|
||||
|
||||
/**
|
||||
* Narrow `unknown` to a plain object record.
|
||||
*
|
||||
* Useful for safely inspecting untyped API responses before accessing properties.
|
||||
*/
|
||||
export function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return typeof value === "object" && value !== null && !Array.isArray(value);
|
||||
}
|
||||
Reference in New Issue
Block a user