/** * Entity loading patterns for route handlers */ import { type AuthSession, formPolicyFor, type Guard } from "#routes/auth.ts"; import { pageGuardAt } from "#routes/route-gates.ts"; import { adminDestinationAt } from "#shared/admin-surface.ts"; import { createAuthedHandler } from "#shared/app-forms.ts"; import type { FormParams } from "#shared/form-data.ts"; import type { ParamsRoute, ResponseHandler } from "#shared/response-steps.ts"; /** * Resolve a nullable promise, calling handler if found or returning 404. * Use for any route that loads a model and returns 404 when missing. */ export const orNotFound = async ( load: Promise, handler: ResponseHandler<[data: T]>, ): Promise => { const data = await load; return data ? handler(data) : (await import("#routes/response.ts")).notFoundResponse(); }; /** Loads a record by its id, or null when there is none. The one loader shape * every gated `:id` route (and the Site-tab editors) is built on. */ export type EntityLoader = (id: number) => Promise; /** Load through a parent, returning null before the child lookup when missing. */ export const throughParent = async ( load: Promise, child: (parent: Parent) => Context | null | Promise, ): Promise => { const parent = await load; return parent === null ? null : child(parent); }; /** Handler for a POST that carries a record id, the session, and the form. */ export type IdFormHandler = ResponseHandler< [id: number, session: AuthSession, form: FormParams] >; /** * Generic wrapper: load entity, return 404 if missing, otherwise call handler. * Curried so the handler is specified first, then the load function. */ export const withEntity = (handler: ResponseHandler<[entity: T]>) => (load: () => Promise): Promise => orNotFound(load(), handler); /** The `{ id: number }` params a single-`:id` route receives. */ export type IdParam = { id: number }; /** Route handler that takes request + { id } params */ export type IdRouteHandler = ParamsRoute; /** Wrap a plain `(request, id)` action as an `:id` route handler — unpacking * the id from the route params so the action never sees the params shape. */ export const idRouteFor = (run: (request: Request, id: number) => Promise): IdRouteHandler => (request, params) => run(request, params.id); /** Route params for attendee-scoped routes */ export type AttendeeRouteParams = { attendeeId: number }; /** Route params for attendee + listing-scoped routes */ export type AttendeeListingRouteParams = { attendeeId: number; listingId: number; }; /** * Gated entity route factory: authenticate first, load from the typed route * params, return 404 when missing, then run the action. The action also receives * the request and params for routes that need more than the loaded entity. */ export type EntityHandler = ( auth: Guard, ) => ( action: ResponseHandler< [entity: T, ...context: TContext, request: Request, params: TParams] >, ) => ParamsRoute; export const createEntityHandler = ( load: (params: TParams) => Promise, ): EntityHandler => (auth) => (action) => (request, params) => auth(request, (...context) => withEntity((entity) => action(entity, ...context, request, params))( () => load(params), ), ); /** The common `:id` form of {@link createEntityHandler}. */ export const createIdEntityHandler = ( load: EntityLoader, ): EntityHandler => createEntityHandler(({ id }: IdParam) => load(id)); /** * Record GET-by-ID page route factory. * Loads the entity by ID, returns 404 if missing, renders with session * context, and gates by the route's declaration. */ export const entityPageById = ( pattern: string, load: EntityLoader, render: ResponseHandler<[entity: T, session: AuthSession]>, ): IdRouteHandler => createIdEntityHandler(load)(pageGuardAt(pattern))(render); /** Record POST-by-ID route: the route's declared form policy gates it, CSRF * check included. */ export const entityFormById = ( pattern: string, handler: IdFormHandler, ): IdRouteHandler => createAuthedHandler<{ id: number }>({ auth: formPolicyFor(adminDestinationAt(pattern)), handle: ({ form, params, session }) => handler(params.id, session, form), });