Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

explore composition #197

Draft
wants to merge 1 commit into
base: cors-helper
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
20 changes: 11 additions & 9 deletions convex/corsHttpRouterExample.ts
Original file line number Diff line number Diff line change
@@ -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,
Expand All @@ -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,
Expand All @@ -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 () => {
Expand Down
246 changes: 111 additions & 135 deletions packages/convex-helpers/server/corsHttpRouter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
);
Expand All @@ -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<string, Map<string, PublicHttpAction>>
* 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<string, Map<string, PublicHttpAction>>
* 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<string, Map<string, PublicHttpAction>> 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<string, PublicHttpAction>();
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<string, Map<string, PublicHttpAction>> 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<string, PublicHttpAction>();
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.
Expand Down
Loading