Files
Epicnabbo-Catalogus-Updated…/src/lib/api.ts
T
openhands 8c12fc6c60 refactor(cms): deduplicate shared admin components and fix studio batch completion
- fix(studio): stop markBatchDone infinite recursion so batches complete
- refactor(api): merge api-response into api and drop the duplicate module
- refactor(media): extract shared media loader and URL validator
- refactor(ui): extract shared LoadingSpinner for site and admin groups
- refactor(dates): consolidate raw date formatting into formatDate util
- refactor(logs): share a single generic log-list loader across tables
- refactor(theme): merge both ColorField components and reuse contrast helpers
- refactor(import): extract shared ImportErrorBanner and SearchInput
- chore(): remove dead theme-editor-tabs after inlining tab components
2026-09-05 20:58:42 +02:00

71 lines
2.3 KiB
TypeScript

import { NextResponse } from "next/server";
/**
* JSON response helper for the public REST API. Serialises BigInt (DB ids)
* to strings — JSON.stringify throws on BigInt otherwise — and sets CORS to
* APP_URL so the game client / external integrations can read it.
*/
const CORS_ORIGIN = process.env.APP_URL ?? "http://localhost:3000";
export function apiJson(data: unknown, init?: ResponseInit): NextResponse {
const body = JSON.stringify(data, (_k, v) =>
typeof v === "bigint" ? v.toString() : v,
);
return new NextResponse(body, {
status: init?.status ?? 200,
headers: {
"content-type": "application/json; charset=utf-8",
"access-control-allow-origin": CORS_ORIGIN,
"cache-control": "no-store",
...(init?.headers ?? {}),
},
});
}
/** Standard success envelope: { ok: true, ...data }. */
export function apiOk(data?: Record<string, unknown>): NextResponse {
return apiJson({ ok: true, ...data });
}
/** Standard error envelope. */
export function apiError(message: string, status = 400): NextResponse {
return apiJson({ error: message }, { status });
}
/** Safe response for temporary infrastructure failures. */
export function apiUnavailable(
message = "Service temporarily unavailable",
): NextResponse {
return apiError(message, 503);
}
/** Parse an unsigned, non-zero database identifier without Number precision loss. */
export function positiveBigInt(raw: string | null): bigint | null {
if (!raw || !/^\d+$/.test(raw)) return null;
const value = BigInt(raw);
return value > 0n ? value : null;
}
/** Clamp a ?page / ?perPage pair from search params. */
export function pagination(
searchParams: URLSearchParams,
defaultPer = 20,
maxPer = 100,
) {
const requestedPage = positiveInteger(searchParams.get("page"), 1);
const requestedPerPage = positiveInteger(
searchParams.get("perPage"),
defaultPer,
);
const perPage = Math.min(maxPer, requestedPerPage);
const maxSafePage = Math.floor(Number.MAX_SAFE_INTEGER / perPage) + 1;
const page = Math.min(requestedPage, maxSafePage);
return { page, perPage, skip: (page - 1) * perPage, take: perPage };
}
function positiveInteger(raw: string | null, fallback: number): number {
if (!raw || !/^\d+$/.test(raw)) return fallback;
const value = Number(raw);
return Number.isSafeInteger(value) && value > 0 ? value : fallback;
}