UNPKG

winterspec

Version:

Write Winter-CG compatible routes with filesystem routing and tons of features

158 lines (157 loc) 6.92 kB
import { z } from "zod"; import { WinterSpecMultiPartFormDataResponse, WinterSpecJsonResponse, WinterSpecResponse, WinterSpecCustomResponse, } from "./types/web-handler.js"; import { withMethods } from "./middleware/with-methods.js"; import { withInputValidation } from "./middleware/with-input-validation.js"; import { withUnhandledExceptionHandling } from "./middleware/with-unhandled-exception-handling.js"; import { ResponseValidationError } from "./middleware/http-exceptions.js"; import { withResponseObjectCheck } from "./middleware/with-response-object-check.js"; const attachMetadataToRouteFn = ({ globalSpec, routeSpec }, routeFn) => { routeFn._globalSpec = globalSpec; routeFn._routeSpec = routeSpec; return routeFn; }; export const createWithWinterSpec = (globalSpec) => { return (routeSpec) => (routeFn) => attachMetadataToRouteFn({ globalSpec, routeSpec, }, async (request, ctx) => { const onMultipleAuthMiddlewareFailures = globalSpec.onMultipleAuthMiddlewareFailures ?? routeSpec.onMultipleAuthMiddlewareFailures; const supportedAuthMiddlewares = new Set(routeSpec.auth == null || routeSpec.auth === "none" ? [] : Array.isArray(routeSpec.auth) ? routeSpec.auth : [routeSpec.auth]); const authMiddlewares = Object.entries(globalSpec.authMiddleware) .filter(([k, _]) => supportedAuthMiddlewares.has(k)) .map(([_, v]) => v); return await wrapMiddlewares([ // Injected into the VM when running in WinterCG emulation mode // @ts-expect-error ...(typeof _injectedWinterSpecMiddleware !== "undefined" ? // @ts-expect-error _injectedWinterSpecMiddleware : []), withUnhandledExceptionHandling, // this serializes responses that are returned by middleware WITHOUT // validating them against the routeSpec // // this allows returning WinterSpecResponse.json or ctx.json in a // middleware, instead of having to return a raw Response // // this is needed, for instance, when an error middleware returns an // error response that does not match the routeSpec's response shape serializeResponse(globalSpec, routeSpec, false), ...(globalSpec.beforeAuthMiddleware ?? []), firstAuthMiddlewareThatSucceeds(authMiddlewares, onMultipleAuthMiddlewareFailures), ...(globalSpec.afterAuthMiddleware ?? []), ...(routeSpec.middleware ?? []), withResponseObjectCheck, withMethods(routeSpec.methods), withInputValidation({ supportedArrayFormats: globalSpec.supportedArrayFormats ?? [ "brackets", "comma", "repeat", ], commonParams: routeSpec.commonParams, formData: routeSpec.multiPartFormData, jsonBody: routeSpec.jsonBody, queryParams: routeSpec.queryParams, routeParams: routeSpec.routeParams, urlEncodedFormData: routeSpec.urlEncodedFormData, }), // this serializes responses that are returned by the route function, // validating them against the routeSpec serializeResponse(globalSpec, routeSpec), ], routeFn, request, ctx); }); }; /** * Ensure that the default response is always merged with the final output * response from the route function. * * Without this, headers accumulated through middleware would never make their * way to the final response * * This also handles validation of the response and serializing it from an * WinterSpecResponse to a wintercg-compatible Response */ function serializeResponse(globalSpec, routeSpec, skipValidation = false) { return async (req, ctx, next) => { const rawResponse = await next(req, ctx); const statusCode = rawResponse instanceof WinterSpecResponse ? rawResponse.statusCode() : rawResponse.status; const isSuccess = statusCode >= 200 && statusCode < 300; try { const response = serializeToResponse(isSuccess && !skipValidation && (globalSpec.shouldValidateResponses ?? true), routeSpec, rawResponse); return response; } catch (err) { throw new ResponseValidationError(err); } }; } export async function wrapMiddlewares(middlewares, routeFn, request, ctx) { return await middlewares.reduceRight((next, middleware) => { return async (req, ctx) => { return middleware(req, ctx, next); }; }, async (request, ctx) => routeFn(request, ctx))(request, ctx); } function serializeToResponse(shouldValidateResponse, routeSpec, response) { if (!shouldValidateResponse) { return "serializeToResponse" in response ? response.serializeToResponse(z.any()) : response; } if (response instanceof WinterSpecResponse) { if (response instanceof WinterSpecJsonResponse) { return response.serializeToResponse(routeSpec.jsonResponse ?? z.any()); } if (response instanceof WinterSpecMultiPartFormDataResponse) { return response.serializeToResponse(routeSpec.multipartFormDataResponse ?? z.any()); } if (response instanceof WinterSpecCustomResponse) { return response.serializeToResponse(z.any()); } } if ("serializeToResponse" in response) { throw new Error("Unknown Response type"); } return response; } function firstAuthMiddlewareThatSucceeds(authMiddlewares, onMultipleAuthMiddlewareFailures) { return async (req, ctx, next) => { if (authMiddlewares.length === 0) { return next(req, ctx); } let errors = []; let didAuthMiddlewareThrow = true; for (const middleware of authMiddlewares) { try { return await middleware(req, ctx, (...args) => { // Otherwise errors unrelated to auth thrown by built-in middleware (withMethods, withValidation) will be caught here didAuthMiddlewareThrow = false; return next(...args); }); } catch (error) { if (didAuthMiddlewareThrow) { errors.push(error); continue; } else { throw error; } } } if (onMultipleAuthMiddlewareFailures && didAuthMiddlewareThrow) { onMultipleAuthMiddlewareFailures(errors); } throw errors[errors.length - 1]; }; }