214 lines
6.5 KiB
TypeScript
214 lines
6.5 KiB
TypeScript
/**
|
|
* @fileoverview Utility functions for Aurora API route handlers.
|
|
* Provides helpers for response formatting, parameter parsing, and validation.
|
|
*/
|
|
|
|
import { z, ZodError, type ZodSchema } from "zod";
|
|
import type { ApiErrorResponse } from "./types";
|
|
|
|
/**
|
|
* JSON replacer function that handles BigInt serialization.
|
|
* Converts BigInt values to strings for JSON compatibility.
|
|
*/
|
|
export function jsonReplacer(_key: string, value: unknown): unknown {
|
|
return typeof value === "bigint" ? value.toString() : value;
|
|
}
|
|
|
|
/**
|
|
* Creates a JSON response with proper content-type header and BigInt handling.
|
|
*
|
|
* @param data - The data to serialize as JSON
|
|
* @param status - HTTP status code (default: 200)
|
|
* @returns A Response object with JSON content
|
|
*
|
|
* @example
|
|
* return jsonResponse({ items: [...], total: 10 });
|
|
* return jsonResponse({ success: true, item }, 201);
|
|
*/
|
|
export function jsonResponse<T>(data: T, status: number = 200): Response {
|
|
return new Response(JSON.stringify(data, jsonReplacer), {
|
|
status,
|
|
headers: { "Content-Type": "application/json" }
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Creates a standardized error response.
|
|
*
|
|
* @param error - Error message
|
|
* @param status - HTTP status code (default: 500)
|
|
* @param details - Optional additional error details
|
|
* @returns A Response object with error JSON
|
|
*
|
|
* @example
|
|
* return errorResponse("Item not found", 404);
|
|
* return errorResponse("Validation failed", 400, "Name is required");
|
|
*/
|
|
export function errorResponse(
|
|
error: string,
|
|
status: number = 500,
|
|
details?: string
|
|
): Response {
|
|
const body: ApiErrorResponse = { error };
|
|
if (details) body.details = details;
|
|
|
|
return Response.json(body, { status });
|
|
}
|
|
|
|
/**
|
|
* Creates a validation error response from a ZodError.
|
|
*
|
|
* @param zodError - The ZodError from a failed parse
|
|
* @returns A 400 Response with validation issue details
|
|
*/
|
|
export function validationErrorResponse(zodError: ZodError): Response {
|
|
return Response.json(
|
|
{
|
|
error: "Invalid payload",
|
|
issues: zodError.issues.map(issue => ({
|
|
path: issue.path,
|
|
message: issue.message
|
|
}))
|
|
},
|
|
{ status: 400 }
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Parses and validates a request body against a Zod schema.
|
|
*
|
|
* @param req - The HTTP request
|
|
* @param schema - Zod schema to validate against
|
|
* @returns Validated data or an error Response
|
|
*
|
|
* @example
|
|
* const result = await parseBody(req, CreateItemSchema);
|
|
* if (result instanceof Response) return result; // Validation failed
|
|
* const data = result; // Type-safe validated data
|
|
*/
|
|
export async function parseBody<T extends ZodSchema>(
|
|
req: Request,
|
|
schema: T
|
|
): Promise<z.infer<T> | Response> {
|
|
try {
|
|
const rawBody = await req.json();
|
|
const parsed = schema.safeParse(rawBody);
|
|
|
|
if (!parsed.success) {
|
|
return validationErrorResponse(parsed.error);
|
|
}
|
|
|
|
return parsed.data;
|
|
} catch (e) {
|
|
return errorResponse("Invalid JSON body", 400);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Parses query parameters against a Zod schema.
|
|
*
|
|
* @param url - The URL containing query parameters
|
|
* @param schema - Zod schema to validate against
|
|
* @returns Validated query params or an error Response
|
|
*/
|
|
export function parseQuery<T extends ZodSchema>(
|
|
url: URL,
|
|
schema: T
|
|
): z.infer<T> | Response {
|
|
const params: Record<string, string> = {};
|
|
url.searchParams.forEach((value, key) => {
|
|
params[key] = value;
|
|
});
|
|
|
|
const parsed = schema.safeParse(params);
|
|
|
|
if (!parsed.success) {
|
|
return validationErrorResponse(parsed.error);
|
|
}
|
|
|
|
return parsed.data;
|
|
}
|
|
|
|
/**
|
|
* Extracts a numeric ID from a URL path segment.
|
|
*
|
|
* @param pathname - The URL pathname
|
|
* @param position - Position from the end (0 = last segment, 1 = second-to-last, etc.)
|
|
* @returns The parsed integer ID or null if invalid
|
|
*
|
|
* @example
|
|
* parseIdFromPath("/api/items/123") // returns 123
|
|
* parseIdFromPath("/api/items/abc") // returns null
|
|
* parseIdFromPath("/api/users/456/inventory", 1) // returns 456
|
|
*/
|
|
export function parseIdFromPath(pathname: string, position: number = 0): number | null {
|
|
const segments = pathname.split("/").filter(Boolean);
|
|
const segment = segments[segments.length - 1 - position];
|
|
|
|
if (!segment) return null;
|
|
|
|
const id = parseInt(segment, 10);
|
|
return isNaN(id) ? null : id;
|
|
}
|
|
|
|
/**
|
|
* Extracts a string ID (like Discord snowflake) from a URL path segment.
|
|
*
|
|
* @param pathname - The URL pathname
|
|
* @param position - Position from the end (0 = last segment)
|
|
* @returns The string ID or null if segment doesn't exist
|
|
*/
|
|
export function parseStringIdFromPath(pathname: string, position: number = 0): string | null {
|
|
const segments = pathname.split("/").filter(Boolean);
|
|
const segment = segments[segments.length - 1 - position];
|
|
return segment || null;
|
|
}
|
|
|
|
/**
|
|
* Checks if a pathname matches a pattern with optional parameter placeholders.
|
|
*
|
|
* @param pathname - The actual URL pathname
|
|
* @param pattern - The pattern to match (use :id for numeric params, :param for string params)
|
|
* @returns True if the pattern matches
|
|
*
|
|
* @example
|
|
* matchPath("/api/items/123", "/api/items/:id") // true
|
|
* matchPath("/api/items", "/api/items/:id") // false
|
|
*/
|
|
export function matchPath(pathname: string, pattern: string): boolean {
|
|
const pathParts = pathname.split("/").filter(Boolean);
|
|
const patternParts = pattern.split("/").filter(Boolean);
|
|
|
|
if (pathParts.length !== patternParts.length) return false;
|
|
|
|
return patternParts.every((part, i) => {
|
|
if (part.startsWith(":")) return true; // Matches any value
|
|
return part === pathParts[i];
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Wraps an async route handler with consistent error handling.
|
|
* Catches all errors and returns appropriate error responses.
|
|
*
|
|
* @param handler - The async handler function
|
|
* @param logContext - Context string for error logging
|
|
* @returns A wrapped handler with error handling
|
|
*/
|
|
export function withErrorHandling(
|
|
handler: () => Promise<Response>,
|
|
logContext: string
|
|
): Promise<Response> {
|
|
return handler().catch((error: unknown) => {
|
|
// Dynamic import to avoid circular dependencies
|
|
return import("@shared/lib/logger").then(({ logger }) => {
|
|
logger.error("web", `Error in ${logContext}`, error);
|
|
return errorResponse(
|
|
`Failed to ${logContext.toLowerCase()}`,
|
|
500,
|
|
error instanceof Error ? error.message : String(error)
|
|
);
|
|
});
|
|
});
|
|
}
|