import type { Context, Middleware, Next } from '@oak/oak'; import { ZodError, type ZodType } from 'zod'; import { createApiErrorResponseBody, createApiSuccessResponseBody } from '../api/rest-api-response-zod.js'; import { returnOakErrorResponse } from './oak-response.js'; import { NotFoundError, ForbiddenError } from './errors.js'; export type GenericOakMiddlewareErrorHandler = ({ error, ctx, next, }: { error: Error; ctx: Context; next: Next; }) => void; export function withZodValidation(schema: T): Middleware { return async (ctx: Context, next: Next) => { try { // Get query parameters const params = Object.fromEntries(ctx.request.url.searchParams.entries()); // Only try to parse body for non-GET requests let body = {}; if (ctx.request.method !== 'GET') { body = await ctx.request.body.json(); } // Merge query parameters with the body and validate them const validatedData = schema.parse({ ...params, ...body }); // Attach validated data to state for use in downstream handlers ctx.state.validatedData = validatedData; await next(); } catch (error) { if (error instanceof ZodError) { ctx.response.status = 400; ctx.response.body = createApiErrorResponseBody({ status: 400 }, [ { message: 'Validation error', code: 'VALIDATION_ERROR', details: error.issues.map((issue) => ({ message: issue.message, code: issue.code, path: issue.path.join('.'), })), }, ]); } else { throw error; } } }; } export function withErrorHandler(): Middleware { return async (ctx: Context, next: Next) => { try { await next(); } catch (error) { // Determine appropriate status code based on error type let statusCode = 500; let errorCode = 'INTERNAL_SERVER_ERROR'; let message = 'Internal server error'; if (error instanceof Error) { // Handle authentication errors if (error.name === 'MissingAuthorizationTokenError') { statusCode = 403; errorCode = 'MISSING_AUTHORIZATION_TOKEN'; message = 'Missing authorization token'; } else if (error.name === 'InvalidTokenError' || error.name === 'TokenExpiredError') { statusCode = 401; errorCode = 'INVALID_TOKEN'; message = 'Invalid or expired token'; } else if (error.name === 'ForbiddenError' || error instanceof ForbiddenError) { statusCode = 403; errorCode = 'FORBIDDEN'; message = error.message || 'Access forbidden'; } else if (error.name === 'NotFoundError' || error instanceof NotFoundError) { statusCode = 404; errorCode = 'NOT_FOUND'; message = error.message || 'Resource not found'; } else if (error.name === 'ValidationError' || error instanceof ZodError) { statusCode = 400; errorCode = 'VALIDATION_ERROR'; message = error.message || 'Validation failed'; } else { // Use the error message if available message = error.message || 'Internal server error'; errorCode = (error.cause as string) || 'INTERNAL_SERVER_ERROR'; } } returnOakErrorResponse(ctx, { meta: { status: statusCode, }, errors: [ { code: errorCode, message, }, ], }); } }; } export function withResponseSchema(): Middleware { return async (ctx: Context, next: Next) => { await next(); // Only wrap successful responses if (ctx.response.status < 400 && ctx.response.body) { // Check if response is already in the correct format const body = ctx.response.body; if (body && typeof body === 'object' && 'data' in body && 'meta' in body) { return; // Response is already wrapped, don't wrap it again } ctx.response.body = createApiSuccessResponseBody( { status: ctx.response.status, }, ctx.response.body, ); } }; }