UNPKG

convex

Version:

Client for the Convex Cloud

180 lines (179 loc) 6.43 kB
"use strict"; import { performJsSyscall } from "./impl/syscall.js"; export const ROUTABLE_HTTP_METHODS = [ "GET", "POST", "PUT", "DELETE", "OPTIONS", "PATCH" ]; export function normalizeMethod(method) { if (method === "HEAD") return "GET"; return method; } export const httpRouter = () => new HttpRouter(); export class HttpRouter { constructor() { this.exactRoutes = /* @__PURE__ */ new Map(); this.prefixRoutes = /* @__PURE__ */ new Map(); this.isRouter = true; /** * Specify an HttpAction to be used to respond to requests * for an HTTP method (e.g. "GET") and a path or pathPrefix. * * Paths must begin with a slash. Path prefixes must also end in a slash. * * ```js * // matches `/profile` (but not `/profile/`) * http.route({ path: "/profile", method: "GET", handler: getProfile}) * * // matches `/profiles/`, `/profiles/abc`, and `/profiles/a/c/b` (but not `/profile`) * http.route({ pathPrefix: "/profile/", method: "GET", handler: getProfile}) * ``` */ this.route = (spec) => { if (!spec.handler) throw new Error(`route requires handler`); if (!spec.method) throw new Error(`route requires method`); const { method, handler } = spec; if (!ROUTABLE_HTTP_METHODS.includes(method)) { throw new Error( `'${method}' is not an allowed HTTP method (like GET, POST, PUT etc.)` ); } if ("path" in spec) { if (!spec.path.startsWith("/")) { throw new Error(`path '${spec.path}' does not start with a /`); } const prefixes = this.prefixRoutes.get(method) || /* @__PURE__ */ new Map(); for (const [prefix, _] of prefixes.entries()) { if (spec.path.startsWith(prefix)) { throw new Error( `${spec.method} path ${spec.path} is shadowed by pathPrefix ${prefix}` ); } } const methods = this.exactRoutes.has(spec.path) ? this.exactRoutes.get(spec.path) : /* @__PURE__ */ new Map(); if (methods.has(method)) { throw new Error( `Path '${spec.path}' for method ${method} already in use` ); } methods.set(method, handler); this.exactRoutes.set(spec.path, methods); } else if ("pathPrefix" in spec) { if (!spec.pathPrefix.startsWith("/")) { throw new Error( `pathPrefix '${spec.pathPrefix}' does not start with a /` ); } if (!spec.pathPrefix.endsWith("/")) { throw new Error(`pathPrefix ${spec.pathPrefix} must end with a /`); } const prefixes = this.prefixRoutes.get(method) || /* @__PURE__ */ new Map(); for (const [prefix, _] of prefixes.entries()) { if (spec.pathPrefix.startsWith(prefix)) { throw new Error( `${spec.method} pathPrefix ${spec.pathPrefix} is shadowed by pathPrefix ${prefix}` ); } } prefixes.set(spec.pathPrefix, handler); this.prefixRoutes.set(method, prefixes); } else { throw new Error( `Invalid httpRouter route entry: must contain either field 'path' or 'pathPrefix'` ); } }; /** * Returns a list of routed HTTP actions. * * These are used to populate the list of routes shown in the Functions page of the Convex dashboard. * * @returns - an array of [path, method, endpoint] tuples. */ this.getRoutes = () => { const exactPaths = [...this.exactRoutes.keys()].sort(); const exact = exactPaths.flatMap( (path) => [...this.exactRoutes.get(path).keys()].sort().map( (method) => [path, method, this.exactRoutes.get(path).get(method)] ) ); const prefixPathMethods = [...this.prefixRoutes.keys()].sort(); const prefixes = prefixPathMethods.flatMap( (method) => [...this.prefixRoutes.get(method).keys()].sort().map( (pathPrefix) => [ `${pathPrefix}*`, method, this.prefixRoutes.get(method).get(pathPrefix) ] ) ); return [...exact, ...prefixes]; }; /** * Returns the appropriate HTTP action and its routed request path and method. * * The path and method returned are used for logging and metrics, and should * match up with one of the routes returned by `getRoutes`. * * For example, * * ```js * http.route({ pathPrefix: "/profile/", method: "GET", handler: getProfile}); * * http.lookup("/profile/abc", "GET") // returns [getProfile, "GET", "/profile/*"] *``` * * @returns - a tuple [{@link PublicHttpAction}, method, path] or null. */ this.lookup = (path, method) => { method = normalizeMethod(method); const exactMatch = this.exactRoutes.get(path)?.get(method); if (exactMatch) return [exactMatch, method, path]; const prefixes = this.prefixRoutes.get(method) || /* @__PURE__ */ new Map(); for (const [pathPrefix, endpoint] of prefixes.entries()) { if (path.startsWith(pathPrefix)) { return [endpoint, method, `${pathPrefix}*`]; } } return null; }; /** * Given a JSON string representation of a Request object, return a Response * by routing the request and running the appropriate endpoint or returning * a 404 Response. * * @param argsStr - a JSON string representing a Request object. * * @returns - a Response object. */ this.runRequest = async (argsStr) => { const request = performJsSyscall("requestFromConvexJson", { convexJson: JSON.parse(argsStr) }); const pathname = new URL(request.url).pathname; const method = request.method; const match = this.lookup(pathname, method); if (!match) { const response2 = new Response(`No HttpAction routed for ${pathname}`, { status: 404 }); return JSON.stringify( performJsSyscall("convexJsonFromResponse", { response: response2 }) ); } const [endpoint, _method, _path] = match; const response = await endpoint.invokeHttpAction(request); return JSON.stringify( performJsSyscall("convexJsonFromResponse", { response }) ); }; } } //# sourceMappingURL=router.js.map