import z, { ZodObject, ZodRawShape, ZodType } from "zod"; import { Action } from "./action.ts"; export class Route { #pattern?: URLPattern; declare readonly args: RouteArgs; declare readonly context: RouteContext; constructor(readonly state: TRouteState) {} /** * HTTP Method */ get method(): RouteMethod { return this.state.method; } /** * URL pattern of the route. */ get pattern(): URLPattern { if (this.#pattern === undefined) { this.#pattern = new URLPattern({ pathname: this.path }); } return this.#pattern; } /** * URL path */ get path(): string { return this.state.path; } /** * Check if the provided URL matches the route pattern. * * @param url - HTTP request.url */ match(url: string): boolean { return this.pattern.test(url); } /** * Extract parameters from the provided URL based on the route pattern. * * @param url - HTTP request.url */ getParsedParams(url: string): TRouteState["params"] extends ZodObject ? z.infer : object { const params = this.pattern.exec(url)?.pathname.groups; if (params === undefined) { return {}; } return this.state.params?.parse(params) ?? params; } /** * Params allows for custom casting of URL parameters. If a parameter does not * have a corresponding zod schema the default param type is "string". * * @param params - URL params. * * @examples * * ```ts * route * .post("/foo/:bar") * .params({ * bar: z.number({ coerce: true }) * }) * .handle(async ({ params: { bar } }) => { * console.log(typeof bar); // => number * }); * ``` */ params(params: TParams): Route & { params: ZodObject }> { return new Route({ ...this.state, params }) as any; } /** * Search allows for custom casting of URL search parameters. If a parameter does * not have a corresponding zod schema the default param type is "string". * * @param search - URL search arguments. * * @examples * * ```ts * route * .post("/foo") * .search({ * bar: z.number({ coerce: true }) * }) * .handle(async ({ search: { bar } }) => { * console.log(typeof bar); // => number * }); * ``` */ search(search: TSearch): Route & { search: ZodObject }> { return new Route({ ...this.state, search }) as any; } /** * Shape of the body this route expects to receive. This is used by all * mutator routes and has no effect when defined on "GET" methods. * * @param body - Body the route expects. * * @examples * * ```ts * route * .post("/foo") * .body( * z.object({ * bar: z.number() * }) * ) * .handle(async ({ bar }) => { * console.log(typeof bar); // => number * }); * ``` */ body(body: TBody): Route & { body: TBody }> { return new Route({ ...this.state, body }); } /** * List of route level middleware action to execute before running the * route handler. * * @param actions - Actions to execute on this route. * * @examples * * ```ts * const hasFooBar = action * .make("hasFooBar") * .response(z.object({ foobar: z.number() })) * .handle(async () => { * return { * foobar: 1, * }; * }); * * route * .post("/foo") * .actions([hasFooBar]) * .handle(async ({ foobar }) => { * console.log(typeof foobar); // => number * }); * ``` */ actions(actions: TAction[]): Route & { actions: TAction[] }> { return new Route({ ...this.state, actions }); } /** * Shape of the response this route produces. This is used by the transform * tools to ensure the client receives parsed data. * * @param response - Response shape of the route. * * @examples * * ```ts * route * .post("/foo") * .response( * z.object({ * bar: z.number() * }) * ) * .handle(async () => { * return { * bar: 1 * } * }); * ``` */ response(output: TResponse): Route & { output: TResponse }> { return new Route({ ...this.state, output }); } /** * Server handler callback method. * * @param handle - Handle function to trigger when the route is executed. */ handle>(handle: THandleFn): Route & { handle: THandleFn }> { return new Route({ ...this.state, handle }); } } /* |-------------------------------------------------------------------------------- | Factories |-------------------------------------------------------------------------------- */ /** * Route factories allowing for easy generation of relay compliant routes. */ export const route = { /** * Create a new "POST" route for the given path. * * @param path - Path to generate route for. * * @examples * * ```ts * route * .post("/foo") * .body( * z.object({ bar: z.string() }) * ); * ``` */ post(path: TPath) { return new Route({ method: "POST", path }); }, /** * Create a new "GET" route for the given path. * * @param path - Path to generate route for. * * @examples * * ```ts * route.get("/foo"); * ``` */ get(path: TPath) { return new Route({ method: "GET", path }); }, /** * Create a new "PUT" route for the given path. * * @param path - Path to generate route for. * * @examples * * ```ts * route * .put("/foo") * .body( * z.object({ bar: z.string() }) * ); * ``` */ put(path: TPath) { return new Route({ method: "PUT", path }); }, /** * Create a new "PATCH" route for the given path. * * @param path - Path to generate route for. * * @examples * * ```ts * route * .patch("/foo") * .body( * z.object({ bar: z.string() }) * ); * ``` */ patch(path: TPath) { return new Route({ method: "PATCH", path }); }, /** * Create a new "DELETE" route for the given path. * * @param path - Path to generate route for. * * @examples * * ```ts * route.delete("/foo"); * ``` */ delete(path: TPath) { return new Route({ method: "DELETE", path }); }, }; /* |-------------------------------------------------------------------------------- | Types |-------------------------------------------------------------------------------- */ type RouteState = { method: RouteMethod; path: string; params?: ZodObject; search?: ZodObject; body?: ZodObject; actions?: Array; output?: ZodType; handle?: HandleFn; }; export type RouteMethod = "POST" | "GET" | "PUT" | "PATCH" | "DELETE"; export type HandleFn = (context: TContext) => TResponse extends ZodType ? Promise> : Promise; type RouteContext = (TRouteState["params"] extends ZodObject ? z.infer : object) & (TRouteState["search"] extends ZodObject ? z.infer : object) & (TRouteState["body"] extends ZodObject ? z.infer : object) & (TRouteState["actions"] extends Array ? UnionToIntersection> : object); type RouteArgs = [ ...TupleIfZod, ...TupleIfZod, ...TupleIfZod, ]; type TupleIfZod = TState extends ZodObject ? [z.infer] : []; type MergeAction> = TActions[number] extends Action ? (TActionState["output"] extends ZodObject ? z.infer : object) : object; type UnionToIntersection = (U extends any ? (k: U) => void : never) extends (k: infer I) => void ? I : never;