feat: centralize application framework primitives
This commit is contained in:
1 parent
96e082b943
commit
a3ddd39b7b
73 files changed
+1429
-84
No files matched your search
@@ -22,7 +22,20 @@ import {
|
||||
/** Translate a key for the active language, interpolating `{param}` placeholders. */
|
||||
export type TFunction = (key: string, params?: Record<string, string | number>) => string;
|
||||
|
||||
export type Context = {
|
||||
export interface PaginationOptions {
|
||||
defaultLimit?: number;
|
||||
maxLimit?: number;
|
||||
}
|
||||
|
||||
export interface PaginationInput {
|
||||
limit: number;
|
||||
cursor?: string;
|
||||
}
|
||||
|
||||
export interface Context<
|
||||
Params extends Record<string, string> = Record<string, string>,
|
||||
User = unknown,
|
||||
> {
|
||||
/** The raw incoming web-standard Request. */
|
||||
req: Request;
|
||||
/** Parsed URL of the request (pathname, query, etc.). */
|
||||
@@ -32,17 +45,19 @@ export type Context = {
|
||||
/** Translate a key for the active language (identity until the runtime sets it). */
|
||||
t: TFunction;
|
||||
/** Dynamic route params, e.g. `/users/[id]` -> `{ id: "42" }`. */
|
||||
params: Record<string, string>;
|
||||
params: Params;
|
||||
/**
|
||||
* Per-request scratch space. Middleware can attach values here
|
||||
* (e.g. the authenticated user) and downstream handlers can read them.
|
||||
*/
|
||||
locals: Record<string, unknown>;
|
||||
/** Resource resolved by a declarative route guard, when present. */
|
||||
resource?: unknown;
|
||||
/**
|
||||
* The authenticated user for this request, or null when anonymous. Populated
|
||||
* by the `sessionAuth` middleware (or `logIn`); read via `getUser(ctx)`.
|
||||
*/
|
||||
user?: unknown;
|
||||
user?: User | null;
|
||||
/** Active tenant/workspace resolved by tenant middleware. */
|
||||
tenant?: Tenant;
|
||||
/** Request tracer installed by observability middleware. */
|
||||
@@ -59,7 +74,9 @@ export type Context = {
|
||||
session: SessionStore;
|
||||
/** Read-only localStorage snapshot sent by the browser for CSR data bindings. */
|
||||
localStorage: LocalStorageSnapshot;
|
||||
};
|
||||
/** Parse and bound the standard `limit` and `cursor` query parameters. */
|
||||
pagination(options?: PaginationOptions): PaginationInput;
|
||||
}
|
||||
|
||||
/** Calls the next middleware in the chain (or the final route handler). */
|
||||
export type Next = () => Promise<Response> | Response;
|
||||
@@ -99,12 +116,15 @@ export type PageMeta = SeoConfig;
|
||||
export type PageComponent = (ctx: Context) => string | Promise<string>;
|
||||
|
||||
/** Create a fresh context for an incoming request. */
|
||||
export function createContext(req: Request, url: URL): Context {
|
||||
export function createContext<Params extends Record<string, string> = Record<string, string>>(
|
||||
req: Request,
|
||||
url: URL,
|
||||
): Context<Params> {
|
||||
const cookies = createCookieStore(req);
|
||||
return {
|
||||
req,
|
||||
url,
|
||||
params: {},
|
||||
params: {} as Params,
|
||||
locals: {},
|
||||
lang: "",
|
||||
t: (key) => key,
|
||||
@@ -113,9 +133,24 @@ export function createContext(req: Request, url: URL): Context {
|
||||
// cookies get `Secure` behind a TLS-terminating proxy (matches CSRF cookies).
|
||||
session: createSessionStore(cookies, req, undefined, url.protocol === "https:"),
|
||||
localStorage: createLocalStorageSnapshot(req),
|
||||
pagination(options = {}) {
|
||||
const defaultLimit = positivePaginationInteger(options.defaultLimit, 25);
|
||||
const maxLimit = positivePaginationInteger(options.maxLimit, 100);
|
||||
const requested = Number(url.searchParams.get("limit"));
|
||||
const limit =
|
||||
Number.isInteger(requested) && requested > 0
|
||||
? Math.min(requested, maxLimit)
|
||||
: Math.min(defaultLimit, maxLimit);
|
||||
const cursor = url.searchParams.get("cursor")?.trim() || undefined;
|
||||
return { limit, cursor };
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function positivePaginationInteger(value: number | undefined, fallback: number): number {
|
||||
return Number.isInteger(value) && Number(value) > 0 ? Number(value) : fallback;
|
||||
}
|
||||
|
||||
/** Apply headers accumulated on the context, such as Set-Cookie. */
|
||||
export function withContextHeaders(ctx: Context, res: Response): Response {
|
||||
const headers = new Headers(res.headers);
|
||||
|
||||
Reference in new issue
Block a user