UNPKG

@beignet/core

Version:

Core framework primitives for Beignet

192 lines (174 loc) 5.46 kB
/** * Error catalog and AppError definitions for Beignet */ import type { StandardSchemaV1 } from "@standard-schema/spec"; type ErrorDetailsSchema = StandardSchemaV1<unknown, unknown>; const APP_ERROR_BRAND = Symbol.for("beignet.AppError"); /** * Definition for one application error catalog entry. */ export interface ErrorDef< TDetails extends ErrorDetailsSchema | undefined = | ErrorDetailsSchema | undefined, > { /** Unique, stable code used across layers, such as `"POST_NOT_FOUND"`. */ code: string; /** HTTP status code returned when this error crosses the HTTP boundary. */ status: number; /** Default human-readable message. */ message: string; /** * Optional schema for the structured details value. * * Beignet uses this for TypeScript inference. Details are not runtime * validated by `AppError`; validate before constructing errors when needed. * Details become public response data when the error crosses the HTTP * boundary, so use app-owned safe fields instead of raw diagnostics. */ details?: TDetails; } /** * App error catalog keyed by developer-friendly names. * * Keys are local identifiers used by `createAppError(...)`; the public stable * error code lives on each `ErrorDef.code`. */ export type ErrorCatalog = Record<string, ErrorDef>; /** * Infer the details input type for an error definition. */ export type InferErrorDetails<TDef extends ErrorDef> = TDef extends { details: ErrorDetailsSchema; } ? StandardSchemaV1.InferInput<TDef["details"]> : unknown; /** * Define an application error catalog without losing literal key and code types. */ export function defineErrors<const T extends ErrorCatalog>(defs: T): T { return defs; } /** * Application error thrown from use cases, policies, and route handlers. * * The server maps `AppError` instances to Beignet's standard error envelope and * marks them as route-owned errors when the contract declares the catalog entry. */ export class AppError<TDef extends ErrorDef = ErrorDef> extends Error { readonly [APP_ERROR_BRAND] = true; /** Error definition from the catalog. */ readonly def: TDef; /** * Optional structured details for clients or UI. * * Beignet does not automatically redact route-owned error details. Keep * provider errors, stack traces, secrets, and private content in `cause`, * logs, or error reporting instead. */ readonly details?: InferErrorDetails<TDef>; /** * Optional HTTP response headers set when this error crosses the HTTP * boundary, such as `Retry-After` on a 429. Headers are public response * data; only include values that are safe to expose. */ readonly headers?: Record<string, string>; constructor( def: TDef, details?: InferErrorDetails<TDef>, overrideMessage?: string, options?: { cause?: unknown; headers?: Record<string, string> }, ) { super(overrideMessage ?? def.message, { cause: options?.cause }); this.name = "AppError"; this.def = def; this.details = details; this.headers = options?.headers; } /** * Stable public error code. */ get code(): string { return this.def.code; } /** * HTTP status code associated with this error. */ get status(): number { return this.def.status; } } /** * Callable helper for creating `AppError` instances from a catalog. */ export type AppErrorCreator<TCatalog extends ErrorCatalog> = { /** * Original catalog bound to this creator. */ catalog: TCatalog; /** * Create an `AppError` from a catalog key. */ <Key extends keyof TCatalog>( key: Key, options?: { cause?: unknown; details?: InferErrorDetails<TCatalog[Key]>; headers?: Record<string, string>; message?: string; }, ): AppError<TCatalog[Key]>; }; /** * Create a callable `AppError` helper bound to a specific catalog. * * The returned function validates the catalog key and preserves each entry's * details type for `options.details`. */ export function createAppError<const T extends ErrorCatalog>( catalog: T, ): AppErrorCreator<T> { const appError = (<Key extends keyof T>( key: Key, options?: { cause?: unknown; details?: InferErrorDetails<T[Key]>; headers?: Record<string, string>; message?: string; }, ): AppError<T[Key]> => { const def = catalog[key as keyof T] as T[typeof key] | undefined; if (!def) { throw new Error(`Unknown error catalog key: ${String(key)}`); } return new AppError<T[typeof key]>( def, options?.details, options?.message, { cause: options?.cause, headers: options?.headers, }, ); }) as AppErrorCreator<T>; appError.catalog = catalog; return appError; } /** * Check whether an unknown value is a Beignet `AppError`. */ export function isAppError(err: unknown): err is AppError<ErrorDef> { if (err instanceof AppError) return true; if (typeof err !== "object" || err === null) return false; const value = err as Record<PropertyKey, unknown>; if (value[APP_ERROR_BRAND] === true) return true; if (value.name !== "AppError") return false; const def = value.def; if (typeof def !== "object" || def === null) return false; const errorDef = def as Record<string, unknown>; return ( typeof errorDef.code === "string" && typeof errorDef.status === "number" && typeof errorDef.message === "string" ); }