From a889ba6263024a7201c20b91c1ad19df1bea2233 Mon Sep 17 00:00:00 2001 From: Ian Macartney Date: Mon, 5 Aug 2024 16:17:49 -0400 Subject: [PATCH] explore composition --- convex/corsHttpRouterExample.ts | 20 +- .../convex-helpers/server/corsHttpRouter.ts | 246 ++++++++---------- 2 files changed, 122 insertions(+), 144 deletions(-) diff --git a/convex/corsHttpRouterExample.ts b/convex/corsHttpRouterExample.ts index 20645f8b..1c7dfe50 100644 --- a/convex/corsHttpRouterExample.ts +++ b/convex/corsHttpRouterExample.ts @@ -1,36 +1,38 @@ -import { corsHttpRouter } from "../packages/convex-helpers/server/corsHttpRouter"; +import { HttpRouter } from "convex/server"; +import { routeWithCors } from "../packages/convex-helpers/server/corsHttpRouter"; import { httpAction } from "./_generated/server"; const everythingHandler = httpAction(async () => { return new Response(JSON.stringify([{ fact: "Hello, world!" }])); }); -const http = corsHttpRouter({ +const http = new HttpRouter(); +const corsRoute = routeWithCors(http, { allowedOrigins: ["*"], }); /** * Exact routes will match /fact exactly */ -http.corsRoute({ +corsRoute({ path: "/fact", method: "GET", handler: everythingHandler, }); -http.corsRoute({ +corsRoute({ path: "/fact", method: "POST", handler: everythingHandler, }); -http.corsRoute({ +corsRoute({ path: "/fact", method: "PATCH", handler: everythingHandler, }); -http.corsRoute({ +corsRoute({ path: "/fact", method: "DELETE", handler: everythingHandler, @@ -54,13 +56,13 @@ http.route({ /** * Prefix routes will match /dynamicFact/123 and /dynamicFact/456 etc. */ -http.corsRoute({ +corsRoute({ pathPrefix: "/dynamicFact/", method: "GET", handler: everythingHandler, }); -http.corsRoute({ +corsRoute({ pathPrefix: "/dynamicFact/", method: "PATCH", handler: everythingHandler, @@ -69,7 +71,7 @@ http.corsRoute({ /** * Per-path "allowedOrigins" will override the default "allowedOrigins" for that route */ -http.corsRoute({ +corsRoute({ path: "/specialRouteOnlyForThisOrigin", method: "GET", handler: httpAction(async () => { diff --git a/packages/convex-helpers/server/corsHttpRouter.ts b/packages/convex-helpers/server/corsHttpRouter.ts index 60c3cdb0..3362fb3a 100644 --- a/packages/convex-helpers/server/corsHttpRouter.ts +++ b/packages/convex-helpers/server/corsHttpRouter.ts @@ -30,38 +30,25 @@ type RouteSpecWithCors = RouteSpec & { /** * Factory function to create a new CorsHttpRouter instance. * @param allowedOrigins An array of allowed origins for CORS. - * @returns A new CorsHttpRouter instance. + * @returns A function like http.route that adds cors as well. */ -export const corsHttpRouter = ({ - allowedOrigins, -}: { - allowedOrigins: string[]; -}) => new CorsHttpRouter({ allowedOrigins }); - -export class CorsHttpRouter extends HttpRouter { - allowedOrigins: string[]; - - /** - * Constructor for CorsHttpRouter. - * @param allowedOrigins An array of allowed origins for CORS. - */ - constructor({ allowedOrigins }: { allowedOrigins: string[] }) { - super(); - this.allowedOrigins = allowedOrigins; - } - - /** - * Overrides the route method to add CORS support. - * @param routeSpec The route specification to be added. - */ - corsRoute = (routeSpec: RouteSpecWithCors): void => { +export const routeWithCors = + ( + http: HttpRouter, + { + allowedOrigins: defaultAllowedOrigins, + }: { + allowedOrigins: string[]; + }, + ) => + (routeSpec: RouteSpecWithCors): void => { const tempRouter = httpRouter(); - tempRouter.exactRoutes = this.exactRoutes; - tempRouter.prefixRoutes = this.prefixRoutes; + tempRouter.exactRoutes = http.exactRoutes; + tempRouter.prefixRoutes = http.prefixRoutes; - const allowedOrigins = routeSpec.allowedOrigins ?? this.allowedOrigins; + const allowedOrigins = routeSpec.allowedOrigins ?? defaultAllowedOrigins; - const routeSpecWithCors = this.createRouteSpecWithCors( + const routeSpecWithCors = createRouteSpecWithCors( routeSpec, allowedOrigins, ); @@ -72,139 +59,128 @@ export class CorsHttpRouter extends HttpRouter { * accordingly. */ if ("path" in routeSpec) { - this.handleExactRoute(tempRouter, routeSpec, allowedOrigins); + handleExactRoute(tempRouter, routeSpec, allowedOrigins); } else { - this.handlePrefixRoute(tempRouter, routeSpec, allowedOrigins); + handlePrefixRoute(tempRouter, routeSpec, allowedOrigins); } /** * Copy the routes from the temporary router to the main router. */ - this.mergeRoutes(tempRouter); + http.exactRoutes = new Map(tempRouter.exactRoutes); + http.prefixRoutes = new Map(tempRouter.prefixRoutes); }; +/** + * Handles exact route matching and adds OPTIONS handler. + * @param tempRouter Temporary router instance. + * @param routeSpec Route specification for exact matching. + */ +function handleExactRoute( + tempRouter: HttpRouter, + routeSpec: RouteSpecWithPath, + allowedOrigins: string[], +): void { /** - * Handles exact route matching and adds OPTIONS handler. - * @param tempRouter Temporary router instance. - * @param routeSpec Route specification for exact matching. + * exactRoutes is defined as a Map> + * where the KEY is the PATH and the VALUE is a map of methods and handlers */ - private handleExactRoute( - tempRouter: HttpRouter, - routeSpec: RouteSpecWithPath, - allowedOrigins: string[], - ): void { - /** - * exactRoutes is defined as a Map> - * where the KEY is the PATH and the VALUE is a map of methods and handlers - */ - const currentMethodsForPath = tempRouter.exactRoutes.get(routeSpec.path); - - /** - * createOptionsHandlerForMethods is a helper function that creates - * an OPTIONS handler for all registered HTTP methods for the given path - */ - const optionsHandler = this.createOptionsHandlerForMethods( - Array.from(currentMethodsForPath?.keys() ?? []), - allowedOrigins, - ); - - /** - * Add the OPTIONS handler for the given path - */ - currentMethodsForPath?.set("OPTIONS", optionsHandler); - - /** - * Add the updated methods for the given path to the exactRoutes map - */ - tempRouter.exactRoutes.set(routeSpec.path, new Map(currentMethodsForPath)); - } + const currentMethodsForPath = tempRouter.exactRoutes.get(routeSpec.path); /** - * Handles prefix route matching and adds OPTIONS handler. - * @param tempRouter Temporary router instance. - * @param routeSpec Route specification for prefix matching. + * createOptionsHandlerForMethods is a helper function that creates + * an OPTIONS handler for all registered HTTP methods for the given path */ - private handlePrefixRoute( - tempRouter: HttpRouter, - routeSpec: RouteSpecWithPathPrefix, - allowedOrigins: string[], - ): void { - /** - * prefixRoutes is structured differently than exactRoutes. It's defined as - * a Map> where the KEY is the - * METHOD and the VALUE is a map of paths and handlers. - */ - const currentMethods = tempRouter.prefixRoutes.keys(); - const optionsHandler = this.createOptionsHandlerForMethods( - Array.from(currentMethods ?? []), - allowedOrigins, - ); + const optionsHandler = createOptionsHandlerForMethods( + Array.from(currentMethodsForPath?.keys() ?? []), + allowedOrigins, + ); - /** - * Add the OPTIONS handler for the given path prefix - */ - const optionsPrefixes = - tempRouter.prefixRoutes.get("OPTIONS") || - new Map(); - optionsPrefixes.set(routeSpec.pathPrefix, optionsHandler); + /** + * Add the OPTIONS handler for the given path + */ + currentMethodsForPath?.set("OPTIONS", optionsHandler); - /** - * Add the updated methods for the given path to the prefixRoutes map - */ - tempRouter.prefixRoutes.set("OPTIONS", optionsPrefixes); - } + /** + * Add the updated methods for the given path to the exactRoutes map + */ + tempRouter.exactRoutes.set(routeSpec.path, new Map(currentMethodsForPath)); +} +/** + * Handles prefix route matching and adds OPTIONS handler. + * @param tempRouter Temporary router instance. + * @param routeSpec Route specification for prefix matching. + */ +function handlePrefixRoute( + tempRouter: HttpRouter, + routeSpec: RouteSpecWithPathPrefix, + allowedOrigins: string[], +): void { /** - * Creates a new route specification with CORS support. - * @param routeSpec Original route specification. - * @returns Modified route specification with CORS handler. + * prefixRoutes is structured differently than exactRoutes. It's defined as + * a Map> where the KEY is the + * METHOD and the VALUE is a map of paths and handlers. */ - private createRouteSpecWithCors( - routeSpec: RouteSpec, - allowedOrigins: string[], - ): RouteSpec { - const httpCorsHandler = handleCors({ - originalHandler: routeSpec.handler, - allowedOrigins: allowedOrigins, - allowedMethods: [routeSpec.method], - }); - return { - ...("path" in routeSpec - ? { path: routeSpec.path } - : { pathPrefix: routeSpec.pathPrefix }), - method: routeSpec.method, - handler: httpCorsHandler, - }; - - throw new Error("Invalid routeSpec"); - } + const currentMethods = tempRouter.prefixRoutes.keys(); + const optionsHandler = createOptionsHandlerForMethods( + Array.from(currentMethods ?? []), + allowedOrigins, + ); /** - * Creates an OPTIONS handler for the given HTTP methods. - * @param methods Array of HTTP methods to be allowed. - * @returns A CORS-enabled OPTIONS handler. + * Add the OPTIONS handler for the given path prefix */ - private createOptionsHandlerForMethods( - methods: string[], - allowedOrigins: string[], - ): PublicHttpAction { - return handleCors({ - allowedOrigins: allowedOrigins, - allowedMethods: methods, - }); - } + const optionsPrefixes = + tempRouter.prefixRoutes.get("OPTIONS") || + new Map(); + optionsPrefixes.set(routeSpec.pathPrefix, optionsHandler); /** - * Finalizes the routes by copying them from the temporary router. - * @param router Temporary router with updated routes. + * Add the updated methods for the given path to the prefixRoutes map */ - private mergeRoutes(router: HttpRouter): void { - this.exactRoutes = new Map(router.exactRoutes); - this.prefixRoutes = new Map(router.prefixRoutes); - } + tempRouter.prefixRoutes.set("OPTIONS", optionsPrefixes); +} + +/** + * Creates a new route specification with CORS support. + * @param routeSpec Original route specification. + * @returns Modified route specification with CORS handler. + */ +function createRouteSpecWithCors( + routeSpec: RouteSpec, + allowedOrigins: string[], +): RouteSpec { + const httpCorsHandler = handleCors({ + originalHandler: routeSpec.handler, + allowedOrigins: allowedOrigins, + allowedMethods: [routeSpec.method], + }); + return { + ...("path" in routeSpec + ? { path: routeSpec.path } + : { pathPrefix: routeSpec.pathPrefix }), + method: routeSpec.method, + handler: httpCorsHandler, + }; +} + +/** + * Creates an OPTIONS handler for the given HTTP methods. + * @param methods Array of HTTP methods to be allowed. + * @returns A CORS-enabled OPTIONS handler. + */ +function createOptionsHandlerForMethods( + methods: string[], + allowedOrigins: string[], +): PublicHttpAction { + return handleCors({ + allowedOrigins: allowedOrigins, + allowedMethods: methods, + }); } -export default corsHttpRouter; +export default routeWithCors; /** * handleCors() is a higher-order function that wraps a Convex HTTP action handler to add CORS support.