@forinda/kickjs 5.15.0 → 5.16.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{application-BiyBwafB.d.mts → application-CAnpDQ8d.d.mts} +18 -4
- package/dist/application-CAnpDQ8d.d.mts.map +1 -0
- package/dist/application-DnnqoDWx.mjs +12 -0
- package/dist/application-DnnqoDWx.mjs.map +1 -0
- package/dist/{bootstrap-Dl4f-ghy.d.mts → bootstrap-8DDzBjhx.d.mts} +2 -2
- package/dist/{bootstrap-Dl4f-ghy.d.mts.map → bootstrap-8DDzBjhx.d.mts.map} +1 -1
- package/dist/{bootstrap-Bwtats8z.mjs → bootstrap-CFK2aAjA.mjs} +3 -3
- package/dist/{bootstrap-Bwtats8z.mjs.map → bootstrap-CFK2aAjA.mjs.map} +1 -1
- package/dist/config/index.mjs +2 -2
- package/dist/{config-C7r8WBXM.mjs → config-BW5taOVX.mjs} +3 -3
- package/dist/{config-C7r8WBXM.mjs.map → config-BW5taOVX.mjs.map} +1 -1
- package/dist/{container-RkKhAKP7.mjs → container-D1gfs6__.mjs} +3 -3
- package/dist/{container-RkKhAKP7.mjs.map → container-D1gfs6__.mjs.map} +1 -1
- package/dist/{context-BLZGcP_k.mjs → context-MD7Mc_o_.mjs} +3 -3
- package/dist/{context-BLZGcP_k.mjs.map → context-MD7Mc_o_.mjs.map} +1 -1
- package/dist/core/adapter.mjs +1 -1
- package/dist/core/app-module.mjs +1 -1
- package/dist/core/cache.mjs +1 -1
- package/dist/core/container.mjs +2 -2
- package/dist/core/cron.mjs +2 -2
- package/dist/core/decorators.mjs +2 -2
- package/dist/core/decorators.mjs.map +1 -1
- package/dist/core/errors.mjs +1 -1
- package/dist/core/errors.mjs.map +1 -1
- package/dist/core/interfaces.mjs +1 -1
- package/dist/core/logger.d.mts +1 -1
- package/dist/core/logger.mjs +2 -2
- package/dist/core/logger.mjs.map +1 -1
- package/dist/core/path.mjs +1 -1
- package/dist/core/path.mjs.map +1 -1
- package/dist/core/plugin.mjs +1 -1
- package/dist/core/reactivity.mjs +1 -1
- package/dist/{core-DQB-4vvK.mjs → core-Db2uyqtM.mjs} +3 -3
- package/dist/{core-DQB-4vvK.mjs.map → core-Db2uyqtM.mjs.map} +1 -1
- package/dist/{env-Y-B4xhwA.mjs → env-nwsfCpdy.mjs} +3 -3
- package/dist/{env-Y-B4xhwA.mjs.map → env-nwsfCpdy.mjs.map} +1 -1
- package/dist/http/application.d.mts +1 -1
- package/dist/http/application.mjs +2 -2
- package/dist/http/bootstrap.d.mts +1 -1
- package/dist/http/bootstrap.mjs +2 -2
- package/dist/http/context.mjs +2 -2
- package/dist/http/middleware/cors.mjs +1 -1
- package/dist/http/middleware/csrf.mjs +1 -1
- package/dist/http/middleware/csrf.mjs.map +1 -1
- package/dist/http/middleware/error-handler.mjs +2 -2
- package/dist/http/middleware/error-handler.mjs.map +1 -1
- package/dist/http/middleware/helmet.mjs +1 -1
- package/dist/http/middleware/helmet.mjs.map +1 -1
- package/dist/http/middleware/rate-limit.mjs +1 -1
- package/dist/http/middleware/rate-limit.mjs.map +1 -1
- package/dist/http/middleware/request-id.mjs +1 -1
- package/dist/http/middleware/request-logger.mjs +2 -2
- package/dist/http/middleware/request-logger.mjs.map +1 -1
- package/dist/http/middleware/session.mjs +1 -1
- package/dist/http/middleware/session.mjs.map +1 -1
- package/dist/http/middleware/spa.mjs +2 -2
- package/dist/http/middleware/upload.mjs +1 -1
- package/dist/http/middleware/upload.mjs.map +1 -1
- package/dist/http/middleware/validate.mjs +2 -2
- package/dist/http/middleware/validate.mjs.map +1 -1
- package/dist/http/middleware/views.mjs +2 -2
- package/dist/http/query/index.mjs +2 -2
- package/dist/http/router-builder.mjs +2 -2
- package/dist/http/router-builder.mjs.map +1 -1
- package/dist/index.d.mts +3 -3
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{logger-BnBVjmDa.d.mts → logger-COlEkn_C.d.mts} +9 -1
- package/dist/logger-COlEkn_C.d.mts.map +1 -0
- package/dist/{metadata-CR5EKL_4.mjs → metadata-Cll3P0N0.mjs} +2 -2
- package/dist/{metadata-CR5EKL_4.mjs.map → metadata-Cll3P0N0.mjs.map} +1 -1
- package/dist/{query-CVJ_WX-p.mjs → query-D-FiLfmd.mjs} +2 -2
- package/dist/{query-CVJ_WX-p.mjs.map → query-D-FiLfmd.mjs.map} +1 -1
- package/package.json +4 -4
- package/dist/application-BiyBwafB.d.mts.map +0 -1
- package/dist/application-CHkpRiHo.mjs +0 -12
- package/dist/application-CHkpRiHo.mjs.map +0 -1
- package/dist/logger-BnBVjmDa.d.mts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-BLZGcP_k.mjs","names":[],"sources":["../src/http/request-store.ts","../src/http/context.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\nimport type { MetaValue } from '../core/execution-context'\n\n/** Per-request storage for REQUEST-scoped DI and request context propagation */\nexport interface RequestStore {\n /** Unique request identifier */\n requestId: string\n /**\n * Per-request singleton cache for REQUEST-scoped services. Keys are\n * heterogeneous (class constructors, `InjectionToken` objects, raw\n * strings), so the map type stays `Map<any, any>`.\n */\n instances: Map<any, any>\n /**\n * Per-request key/value bag. Canonical backing store for\n * `RequestContext.set/get` and the Context Contributor pipeline (#107)\n * since Phase 3. Anything written here is visible to every concrete\n * execution context constructed for the same request, regardless of\n * which middleware/contributor wrote it.\n *\n * Keys are constrained to `string` per the locked decision in\n * `architecture.md` §20.12 (#4); the value type is `unknown` because\n * the concrete shape at each key is determined by the consumer's\n * `ContextMeta` augmentation, not by the store itself.\n *\n * Prefer the {@link getRequestValue} helper over reaching into this\n * map directly — it threads the augmented `ContextMeta` shape through\n * `MetaValue<K>` so consumers get typed reads without an `as` cast.\n * Writes should flow through `ctx.set(key, value)` or a Context\n * Contributor's return value — see the `setRequestValue` note below\n * for why this package intentionally does NOT ship a service-level\n * write helper.\n */\n values: Map<string, unknown>\n}\n\n/** AsyncLocalStorage instance shared across the framework */\nexport const requestStore = new AsyncLocalStorage<RequestStore>()\n\n/**\n * Get the current request store. Throws if called outside a request context.\n * Used by the Container to resolve REQUEST-scoped dependencies.\n */\nexport function getRequestStore(): RequestStore {\n const store = requestStore.getStore()\n if (!store) {\n throw new Error(\n 'No active request context. REQUEST-scoped services can only be resolved during an HTTP request.',\n )\n }\n return store\n}\n\n/**\n * Read a per-request `ContextMeta`-keyed value from outside a controller\n * (services, repositories, anywhere with no `ctx` reference). Returns\n * the augmented type for the key — `MetaValue<K>` — so consumers get the\n * same static safety as `ctx.get(key)` without holding the context.\n *\n * Returns `undefined` outside an active request frame (background jobs,\n * startup code, tests without `requestScopeMiddleware()`) — the helper\n * is intentionally null-tolerant so calling it from a service that\n * runs in both request and non-request paths doesn't throw.\n *\n * @example\n * ```ts\n * declare module '@forinda/kickjs' {\n * interface ContextMeta { tenant: { id: string; name: string } }\n * }\n *\n * @Service()\n * class AuditService {\n * log(action: string) {\n * const tenant = getRequestValue('tenant') // typed: { id; name } | undefined\n * db.audit.insert({ action, tenantId: tenant?.id })\n * }\n * }\n * ```\n */\nexport function getRequestValue<K extends string>(key: K): MetaValue<K> | undefined {\n return requestStore.getStore()?.values.get(key) as MetaValue<K> | undefined\n}\n\n// Intentionally NO `setRequestValue` export.\n//\n// Writes are deliberately context-scoped — only the controlled write\n// surfaces should mutate the per-request Map:\n//\n// 1. A Context Contributor's `resolve()` / `onError()` return value\n// (the runner does `ctx.set(reg.key, value)` on the contributor's\n// behalf). This is the canonical way to populate a key.\n// 2. A controller / middleware / contributor that holds a\n// `RequestContext` instance can call `ctx.set(key, value)`\n// directly. The write is auditable inline at the call site.\n//\n// Letting arbitrary services reach in and mutate the store from\n// anywhere produces \"spooky action at a distance\" — keys appear in\n// the request bag without an obvious source, and tracing which\n// service polluted what becomes a grep exercise. Services that need\n// to publish per-request state should return the value to the caller\n// (controller or contributor) and let *that* layer write it via\n// `ctx.set`. If a future need genuinely requires a service-level\n// write surface, expose a narrow function (`recordTrace`,\n// `markStartTime`) on a service that captures the side effect, not a\n// generic `setRequestValue`.\n","/// <reference types=\"multer\" />\nimport type { Request, Response, NextFunction } from 'express'\nimport { type ExecutionContext, type MetaValue } from '../core/execution-context'\nimport { normalizeProblem, type ProblemDetails, type ValidationError } from '../core/errors'\nimport { requestStore } from './request-store'\nimport {\n parseQuery,\n type QueryFieldConfig,\n type PaginatedResponse,\n type TypedParsedQuery,\n} from './query'\n\n/**\n * Input shape for the typed problem-detail convenience methods on\n * {@link ProblemSender} (`ctx.problem.notFound`, `.badRequest`, etc.) —\n * RFC 9457 details minus the `status`, which each method pre-fills.\n */\ntype ProblemInput = Omit<ProblemDetails, 'status'>\n\n/**\n * Callable response helper for RFC 9457 — Problem Details responses.\n * Returned by `ctx.problem`. Invoke directly for a fully-specified\n * payload, or use the named convenience methods to pre-fill `status` +\n * `title` for the common codes.\n *\n * Each call sets `Content-Type: application/problem+json` and serializes\n * the canonical shape (defaulting `type` to `about:blank` and `title` to\n * the IANA reason phrase per RFC 9457 §3.1.1 / §3.1.4).\n *\n * @example\n * ```ts\n * ctx.problem({\n * type: 'https://api.example.com/problems/out-of-credit',\n * status: 403,\n * detail: 'Your balance is 30, but that costs 50.',\n * balance: 30, // extension per §3.2\n * })\n *\n * ctx.problem.notFound({ detail: 'User abc not found' })\n * ctx.problem.validation(zodResult.error.issues)\n * ```\n */\ninterface ProblemSender {\n (input: ProblemDetails): Response\n /** 400 Bad Request — RFC 9457. */\n badRequest(input?: ProblemInput): Response\n /** 401 Unauthorized — RFC 9457. */\n unauthorized(input?: ProblemInput): Response\n /** 403 Forbidden — RFC 9457. */\n forbidden(input?: ProblemInput): Response\n /** 404 Not Found — RFC 9457. */\n notFound(input?: ProblemInput): Response\n /** 409 Conflict — RFC 9457. */\n conflict(input?: ProblemInput): Response\n /** 422 Unprocessable Entity — RFC 9457. */\n unprocessable(input?: ProblemInput): Response\n /** 429 Too Many Requests — RFC 9457. */\n tooManyRequests(input?: ProblemInput): Response\n /** 500 Internal Server Error — RFC 9457. */\n internal(input?: ProblemInput): Response\n /**\n * 422 with Zod issues serialized into the `errors` extension per\n * RFC 9457 §3.2's array convention. Pass `error.issues` from a Zod\n * `safeParse` failure or `error.issues` from a thrown ZodError.\n */\n validation(issues: any[], input?: ProblemInput): Response\n}\n\n/**\n * Recursively marks every property of `T` as `readonly`. Preserves\n * `Function`, `Date`, and `RegExp` unchanged, and converts `Map` / `Set`\n * to `ReadonlyMap` / `ReadonlySet` so their mutating methods (`set`,\n * `delete`, `add`, `clear`) are removed from the public surface.\n * Tuples keep their positional element types — only their per-element\n * mutability is locked.\n *\n * Exposed as a utility for adopters who want to opt into compile-time\n * sealing of their own shapes. **No longer applied to\n * `RequestContext.{body,params,query,headers,file,files}`** — that\n * broke TS narrowing / discriminated-union drilldowns on typed request\n * payloads. Runtime read-only enforcement now lives in\n * {@link makeReadOnlyProxy} (dev-only Proxy that warns on writes,\n * no-op in production).\n *\n * Type-only. No runtime cost. `DeepReadonly<any>` is `any` (TS\n * distributes conditional types over `any`).\n */\nexport type DeepReadonly<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends Date | RegExp\n ? T\n : T extends Map<infer K, infer V>\n ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>\n : T extends Set<infer V>\n ? ReadonlySet<DeepReadonly<V>>\n : T extends readonly (infer U)[]\n ? // Distinguish a regular array from a tuple. A regular `T[]` widens\n // its `length` to `number`; a tuple's `length` is a literal\n // (e.g. `2`). The literal branch maps over `keyof T` so each\n // positional slot keeps its own type — `DeepReadonly<[1, 2]>` →\n // `readonly [1, 2]`, not `readonly (1 | 2)[]`.\n number extends T['length']\n ? ReadonlyArray<DeepReadonly<U>>\n : { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T extends object\n ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T\n\n/**\n * Re-export the augmentable {@link ContextMeta} registry from core/.\n * Declared in `core/execution-context.ts` so non-HTTP transports\n * (WS/queue/cron, V2) share one declaration site. Apps still augment\n * via the canonical `declare module '@forinda/kickjs'` form.\n */\nexport type { ContextMeta } from '../core/execution-context'\n\n/**\n * Symbol key under which the per-request `AbortController` backing\n * `RequestContext.signal` is cached on the Express `req` object.\n * Shared across every `RequestContext` instance built for the same\n * request (router-builder constructs separate wrappers for\n * middleware, the contributor pipeline, and the main handler — they\n * all need the same signal).\n */\nconst signalControllerKey = Symbol.for('kickjs.requestContext.signalController')\n\n/**\n * Symbol key under which per-target read-only Proxy wrappers are\n * cached on the Express `req` object. Used by {@link makeReadOnlyProxy}\n * so repeat access of `ctx.body` / `ctx.params` / etc. returns the same\n * Proxy instance (cheap, and stable under `===` for adopters comparing\n * values across middleware).\n */\nconst readOnlyProxyCacheKey = Symbol.for('kickjs.requestContext.readOnlyProxyCache')\n\n/**\n * `true` iff `v` is a plain object (`{}` / `Object.create(null)`) or an\n * array — the two shapes JSON.parse / Express body-parser produce and\n * the only ones the read-only Proxy recurses into. Class instances,\n * `Date`, `RegExp`, `Map`, `Set`, `Buffer`, streams, and functions\n * pass through unwrapped so their method dispatch and `instanceof`\n * checks keep working.\n */\nfunction isPlainObjectOrArray(v: unknown): v is object {\n if (v === null || typeof v !== 'object') return false\n if (Array.isArray(v)) return true\n const proto = Object.getPrototypeOf(v)\n return proto === Object.prototype || proto === null\n}\n\n/**\n * In dev, wrap `target` in a `Proxy` whose `set` / `deleteProperty` /\n * `defineProperty` traps log a warning and otherwise leave the\n * underlying object untouched. A `get` trap recursively wraps nested\n * plain objects and arrays on access, so deep mutations\n * (`ctx.body.user.name = 'x'`, `ctx.files[0].fieldname = 'y'`,\n * `ctx.body.tags.push(...)`) all surface a warning — matching the\n * prior `DeepReadonly<T>` contract at runtime instead of at the type\n * level. In production (`process.env.NODE_ENV === 'production'`) the\n * proxy is bypassed entirely and `target` is returned as-is so the\n * hot path stays zero-cost.\n *\n * Why a Proxy instead of `DeepReadonly<T>` at the type level: the\n * recursive conditional type interfered with TS narrowing (notably\n * discriminated unions on `ctx.body`) and slowed type-checking on\n * deeply-nested Zod-inferred shapes. Compile-time sealing is gone;\n * runtime warning catches the same class of bug during dev and tests.\n *\n * Trap returns `true` so strict-mode call sites (which is all of them\n * — ES modules are strict) don't *throw* on assignment — they just\n * get a warning + the underlying value stays untouched. Adopters\n * discover the violation when the subsequent read returns the\n * original value, not the one they \"set\".\n *\n * Targets are cached per-`req` under {@link readOnlyProxyCacheKey} so\n * (1) repeat getter access returns the same proxy (`ctx.body ===\n * ctx.body`, `ctx.body.user === ctx.body.user`); and (2) cyclic refs\n * (`body.self = body`) terminate via the cache hit on the second\n * visit. Primitives, `null`, and non-plain values pass through.\n */\nfunction makeReadOnlyProxy<T>(target: T, req: object, label: string): T {\n if (process.env.NODE_ENV === 'production') return target\n if (!isPlainObjectOrArray(target)) return target\n\n type Cache = WeakMap<object, unknown>\n const reqObj = req as Record<symbol, Cache | undefined>\n let cache = reqObj[readOnlyProxyCacheKey]\n if (!cache) {\n cache = new WeakMap()\n reqObj[readOnlyProxyCacheKey] = cache\n }\n const targetObj = target as unknown as object\n const cached = cache.get(targetObj)\n if (cached) return cached as T\n\n const warnWrite = (verb: string, prop: PropertyKey) => {\n console.warn(\n `[kickjs] Attempted to ${verb} \\`ctx.${label}.${String(prop)}\\` — ` +\n `request data is read-only. ` +\n `Stash computed values via \\`ctx.set('key', value)\\` or a Context Contributor, ` +\n `or reach \\`ctx.req.${label}\\` if you genuinely need the raw mutable handle.`,\n )\n }\n\n const proxy = new Proxy(targetObj, {\n get(t, prop, receiver) {\n const value = Reflect.get(t, prop, receiver)\n // Recurse only into plain objects / arrays. Functions (incl.\n // array methods like `.push`) pass through with `receiver` as\n // their `this`, so internal `this[i] = x` writes still hit the\n // `set` trap and warn.\n if (isPlainObjectOrArray(value)) {\n return makeReadOnlyProxy(value, req, `${label}.${String(prop)}`)\n }\n return value\n },\n set(_t, prop) {\n warnWrite('assign', prop)\n return true\n },\n deleteProperty(_t, prop) {\n warnWrite('delete', prop)\n return true\n },\n defineProperty(_t, prop) {\n warnWrite('Object.defineProperty on', prop)\n return true\n },\n })\n cache.set(targetObj, proxy)\n return proxy as T\n}\n\n/**\n * The shape of a single route entry in `KickRoutes`. Generated by\n * `kick typegen` and consumed via the `Ctx<T>` helper. Each field is\n * `unknown` by default and is filled in as the typegen learns more\n * about the route (URL params from the path, body/query from validation\n * schemas, etc.).\n */\nexport interface RouteShape {\n params?: unknown\n body?: unknown\n query?: unknown\n response?: unknown\n}\n\n/**\n * Unified request/response abstraction passed to every controller method.\n * Shields handlers from raw Express objects and provides convenience methods.\n *\n * Implements {@link ExecutionContext} so the transport-agnostic Context\n * Contributor pipeline (#107) can run against this concrete HTTP context\n * the same way it will run against future WS / queue / cron contexts.\n */\nexport class RequestContext<TBody = any, TParams = any, TQuery = any> implements ExecutionContext {\n constructor(\n public readonly req: Request,\n public readonly res: Response,\n public readonly next: NextFunction,\n ) {}\n\n /**\n * Read-side accessor for the per-request metadata map.\n *\n * Returns the AsyncLocalStorage map (`requestStore.getStore().values`)\n * shared with the Context Contributor pipeline when an ALS frame is\n * active. Returns `null` outside any frame — read-side callers\n * (`ctx.get`, `ctx.user`, `ctx.tenantId`, `ctx.roles`) treat that as\n * \"no metadata\" and fall through to their `req.*` defaults. Reads\n * never throw because outside an ALS frame nobody could have written\n * via `ctx.set` anyway, so an empty answer is the truthful one.\n */\n private metadataReadOnly(): Map<string, any> | null {\n return (requestStore.getStore()?.values as Map<string, any> | undefined) ?? null\n }\n\n /**\n * Write-side accessor for the per-request metadata map.\n *\n * Returns the same map as {@link metadataReadOnly} when an ALS frame\n * is active. Throws otherwise — writing to a fallback map outside\n * the ALS frame would silently lose data because other call sites\n * (downstream contributors, request-logger, etc.) read from the\n * canonical store and would never see it. Mount\n * `requestScopeMiddleware()` (Application does this for you in\n * `'auto'` mode) or wrap your test setup in\n * `requestStore.run({...}, () => …)` to fix.\n */\n private metadataForWrite(): Map<string, any> {\n const store = requestStore.getStore()\n if (store) return store.values as Map<string, any>\n throw new Error(\n 'RequestContext.set called outside an AsyncLocalStorage frame. ' +\n 'Mount requestScopeMiddleware() before constructing RequestContext, ' +\n 'or wrap test setup in requestStore.run({ requestId, instances: new Map(), values: new Map() }, () => ...).',\n )\n }\n\n // ── Request Data ────────────────────────────────────────────────────\n\n /**\n * Parsed request body. Wrapped in a dev-only Proxy that warns when\n * adopters try to mutate Zod-validated payloads in place — compute\n * new values and stash them via `ctx.set('key', value)` or a Context\n * Contributor instead. Reach for `ctx.req.body` if you genuinely\n * need the raw, mutable Express handle (rare).\n *\n * In production the Proxy is bypassed and `this.req.body` is\n * returned as-is — no runtime cost on the hot path.\n */\n get body(): TBody {\n return makeReadOnlyProxy<TBody>(this.req.body, this.req, 'body')\n }\n\n /** Path parameters parsed from the route — read-only, see {@link body}. */\n get params(): TParams {\n return makeReadOnlyProxy<TParams>(this.req.params as TParams, this.req, 'params')\n }\n\n /** Parsed query string — read-only, see {@link body}. */\n get query(): TQuery {\n return makeReadOnlyProxy<TQuery>(this.req.query as TQuery, this.req, 'query')\n }\n\n /** Inbound HTTP headers — read-only, see {@link body}. */\n get headers(): Request['headers'] {\n return makeReadOnlyProxy<Request['headers']>(this.req.headers, this.req, 'headers')\n }\n\n get requestId(): string | undefined {\n return (this.req as any).requestId ?? (this.req.headers['x-request-id'] as string | undefined)\n }\n\n /**\n * `AbortSignal` that fires when the underlying HTTP request is\n * closed — either because the client disconnected before the\n * response was sent, or because Node's socket-close event ran\n * after a normal response. Multiple `RequestContext` wrappers for\n * the same request (router-builder constructs one per\n * middleware/contributor pipeline / main handler) share a single\n * `AbortController` cached on the underlying `req` object, so the\n * signal is stable across the request lifecycle.\n *\n * Thread it through anything that takes an `AbortSignal` so the\n * work cancels as soon as the client gives up — e.g. a kickjs-db\n * relational query:\n *\n * ```ts\n * @Get('/:id/full')\n * async show(ctx: RequestContext) {\n * const row = await this.repo.findFullById(ctx.params.id, ctx.signal)\n * if (!row) return ctx.notFound()\n * ctx.json(row)\n * }\n * ```\n *\n * The repo passes `signal` to `db.query.<table>.findUnique({ signal })`;\n * if the client disconnects mid-flight, the in-flight query rejects\n * with `RelationalQueryCancelledError` instead of consuming the\n * connection until completion.\n *\n * Implementation note — both `req.on('close')` and `res.on('close')`\n * are wired so the signal aborts regardless of whether the close\n * event lands on the request stream or the response stream first.\n * Subsequent fires are no-ops (`AbortController.abort()` is\n * idempotent).\n */\n get signal(): AbortSignal {\n const req = this.req as Request & { [signalControllerKey]?: AbortController }\n const cached = req[signalControllerKey]\n if (cached) return cached.signal\n\n const ctrl = new AbortController()\n req[signalControllerKey] = ctrl\n const abort = () => ctrl.abort('request closed')\n this.req.once('close', abort)\n this.res.once('close', abort)\n return ctrl.signal\n }\n\n /** Session data (requires session middleware) */\n get session(): any {\n return (this.req as any).session\n }\n\n /**\n * The authenticated user set by AuthAdapter.\n * Reads from the per-request metadata store first (`ctx.set('user', ...)`),\n * then falls back to `req.user` (set directly by AuthAdapter middleware).\n *\n * Extend the `AuthUser` interface via module augmentation in `@forinda/kickjs-auth`\n * to get full typing.\n *\n * @example\n * ```ts\n * @Get('/me')\n * @Authenticated()\n * getProfile(ctx: RequestContext) {\n * const user = ctx.user\n * if (!user) return ctx.notFound()\n * return ctx.json({ user })\n * }\n * ```\n */\n get user(): MetaValue<'user', Record<string, any>> | undefined {\n return this.metadataReadOnly()?.get('user') ?? (this.req as any).user\n }\n\n /**\n * Tenant identifier for multi-tenant apps. Reads `user.tenantId` —\n * populated either by `AuthAdapter.testMode({ tenantId })`, a tenant\n * resolver middleware, or manually on the strategy's `mapPayload`.\n *\n * Augment `AuthUser` (or `ContextMeta['user']`) to widen the return\n * type if your user shape guarantees a non-`string | undefined` id.\n */\n get tenantId(): string | undefined {\n const u = this.user as Record<string, any> | undefined\n return u?.tenantId\n }\n\n /**\n * Effective role list for the current user. Prefers `user.tenantRoles`\n * (populated by `AuthAdapterOptions.roleResolver` under a tenant) and\n * falls back to `user.roles`. Returns an empty array when the user is\n * absent or the shape has neither field.\n */\n get roles(): string[] {\n const u = this.user as Record<string, any> | undefined\n return u?.tenantRoles ?? u?.roles ?? []\n }\n\n // ── Query String Parsing ───────────────────────────────────────────\n\n /**\n * Parse the request query string into structured filters, sort, pagination, and search.\n * Pass the result to an ORM query builder adapter (Drizzle, Prisma, Sequelize, etc.).\n *\n * Generic over the field config: when passed an inline literal (or\n * `as const`), `filters[].field` and `sort[].field` are narrowed to\n * the exact whitelisted field names. Pass nothing or a non-literal\n * config and you get the loose `string` fallback.\n *\n * @param fieldConfig - Optional whitelist for filterable, sortable, and searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * const parsed = ctx.qs({\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt', 'title'],\n * } as const)\n * parsed.filters[0].field // typed as 'status' | 'priority'\n * parsed.sort[0].field // typed as 'createdAt' | 'title'\n * }\n * ```\n */\n qs<TConfig extends QueryFieldConfig | undefined = undefined>(\n fieldConfig?: TConfig,\n ): TypedParsedQuery<TConfig> {\n return parseQuery<TConfig>(this.req.query as Record<string, any>, fieldConfig)\n }\n\n // ── File Uploads ────────────────────────────────────────────────────\n\n /**\n * Single uploaded file (requires `@FileUpload({ mode: 'single' })`).\n * Wrapped in a dev-only Proxy that warns on mutation — see {@link body}.\n * File metadata is set by the upload middleware and shouldn't be\n * mutated downstream.\n */\n get file(): Express.Multer.File | undefined {\n const f = this.req.file\n return f ? makeReadOnlyProxy<Express.Multer.File>(f, this.req, 'file') : undefined\n }\n\n /**\n * Array of uploaded files (requires `@FileUpload({ mode: 'array' })`).\n * The array (and each entry in dev) is wrapped in a read-only Proxy\n * that warns on `push`/`pop` and per-file mutation. Use\n * `ctx.req.files` if you genuinely need the mutable Express handle.\n */\n get files(): Express.Multer.File[] | undefined {\n // multer's `Request['files']` is a union — array (mode: 'array' /\n // 'any') or `{ [fieldname]: File[] }` (mode: 'fields'). The\n // `@FileUpload({ mode: 'array' })` decorator only sets the array\n // shape, but `Request['files']` is the broad union. Narrow back to\n // `File[]` here for the common-case getter; adopters using\n // `mode: 'fields'` can read `ctx.req.files` directly for the\n // dictionary shape.\n const f = this.req.files\n if (!Array.isArray(f)) return undefined\n return makeReadOnlyProxy<Express.Multer.File[]>(f, this.req, 'files')\n }\n\n // ── Metadata Store ──────────────────────────────────────────────────\n\n /**\n * Read a value from the per-request metadata store.\n *\n * When `ContextMeta` has been augmented with a matching key, the return\n * type is inferred automatically. For ad-hoc keys, pass a generic:\n * `ctx.get<MyType>('custom')`.\n */\n get<K extends string>(key: K): MetaValue<K> | undefined {\n return this.metadataReadOnly()?.get(key) as MetaValue<K> | undefined\n }\n\n /**\n * Write a value to the per-request metadata store. Throws when no\n * AsyncLocalStorage frame is active — see {@link metadataForWrite}\n * for why a silent fallback is the wrong default.\n */\n set<K extends string>(key: K, value: MetaValue<K>): void {\n this.metadataForWrite().set(key, value)\n }\n\n // ── Response Helpers ────────────────────────────────────────────────\n\n json(data: any, status = 200) {\n return this.res.status(status).json(data)\n }\n\n created(data: any) {\n return this.res.status(201).json(data)\n }\n\n noContent() {\n return this.res.status(204).end()\n }\n\n /**\n * @deprecated Prefer {@link ProblemSender.notFound | ctx.problem.notFound()},\n * which emits an RFC 9457 — Problem Details response. This helper stays\n * for backward compatibility and will not be removed.\n */\n notFound(message = 'Not Found') {\n return this.res.status(404).json({ message })\n }\n\n /**\n * @deprecated Prefer {@link ProblemSender.badRequest | ctx.problem.badRequest()},\n * which emits an RFC 9457 — Problem Details response. This helper stays\n * for backward compatibility and will not be removed.\n */\n badRequest(message: string) {\n return this.res.status(400).json({ message })\n }\n\n // ── RFC 9457 Problem Details ───────────────────────────────────────\n\n /**\n * RFC 9457 — Problem Details for HTTP APIs response helper.\n * https://datatracker.ietf.org/doc/html/rfc9457\n *\n * `ctx.problem(...)` sends a canonical problem-detail response with\n * `Content-Type: application/problem+json`. The convenience methods\n * (`ctx.problem.notFound`, `.badRequest`, etc.) pre-fill `status` +\n * `title` for the common codes.\n *\n * `type` defaults to `about:blank` (§3.1.1); `title` defaults to the\n * IANA reason phrase for `status` (§3.1.4). Extension members per §3.2\n * pass through unchanged.\n *\n * @example\n * ```ts\n * ctx.problem({\n * type: 'https://api.example.com/problems/out-of-credit',\n * status: 403,\n * detail: 'Your balance is 30, but that costs 50.',\n * instance: `/account/${ctx.user.id}/messages/${ctx.params.id}`,\n * balance: 30,\n * })\n *\n * ctx.problem.notFound({ detail: 'User abc not found' })\n * ctx.problem.validation(zodResult.error.issues)\n * ```\n *\n * @see {@link ProblemException} for the exception-throwing equivalent\n * (use it from services where `ctx` isn't in scope).\n */\n get problem(): ProblemSender {\n const send = (input: ProblemDetails): Response => this.sendProblem(input)\n send.badRequest = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 400, ...input })\n send.unauthorized = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 401, ...input })\n send.forbidden = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 403, ...input })\n send.notFound = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 404, ...input })\n send.conflict = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 409, ...input })\n send.unprocessable = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 422, ...input })\n send.tooManyRequests = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 429, ...input })\n send.internal = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 500, ...input })\n send.validation = (issues: any[], input: ProblemInput = {}): Response => {\n const errors: ValidationError[] = (issues || []).map((issue: any) => ({\n field: issue.path?.join?.('.') ?? '',\n message: issue.message ?? '',\n code: issue.code,\n }))\n return this.sendProblem({\n status: 422,\n detail: issues?.[0]?.message,\n ...input,\n errors,\n })\n }\n return send as ProblemSender\n }\n\n private sendProblem(input: ProblemDetails): Response {\n const body = normalizeProblem(input)\n this.res.setHeader('Content-Type', 'application/problem+json')\n return this.res.status(body.status).json(body)\n }\n\n html(content: string, status = 200) {\n return this.res.status(status).type('html').send(content)\n }\n\n download(buffer: Buffer, filename: string, contentType = 'application/octet-stream') {\n this.res.setHeader('Content-Disposition', `attachment; filename=\"${filename}\"`)\n this.res.setHeader('Content-Type', contentType)\n return this.res.send(buffer)\n }\n\n /**\n * Render a template using the registered view engine (EJS, Pug, Handlebars, etc.).\n * Requires a ViewAdapter to be configured in bootstrap().\n *\n * @param template - Template name (without extension, relative to viewsDir)\n * @param data - Data to pass to the template\n *\n * @example\n * ```ts\n * ctx.render('dashboard', { user, title: 'Dashboard' })\n * ctx.render('emails/welcome', { name: 'Alice' })\n * ```\n */\n render(template: string, data: Record<string, any> = {}) {\n return this.res.render(template, data)\n }\n\n /**\n * Parse query params and return a standardized paginated response.\n * Calls `ctx.qs()` internally, then wraps your data with pagination meta.\n *\n * @param fetcher - Async function that receives ParsedQuery and returns `{ data, total }`\n * @param fieldConfig - Optional whitelist for filterable, sortable, searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * return ctx.paginate(\n * async (parsed) => {\n * const data = await db.select().from(users)\n * .where(query.where).limit(parsed.pagination.limit)\n * .offset(parsed.pagination.offset).all()\n * const total = await db.select({ count: count() }).from(users).get()\n * return { data, total: total?.count ?? 0 }\n * },\n * { filterable: ['name', 'role'], sortable: ['createdAt'] },\n * )\n * }\n * ```\n */\n async paginate<T, TConfig extends QueryFieldConfig | undefined = undefined>(\n fetcher: (parsed: TypedParsedQuery<TConfig>) => Promise<{ data: T[]; total: number }>,\n fieldConfig?: TConfig,\n ) {\n const parsed = this.qs<TConfig>(fieldConfig)\n const { data, total } = await fetcher(parsed)\n const { page, limit } = parsed.pagination\n const totalPages = Math.ceil(total / limit) || 1\n\n const response: PaginatedResponse<T> = {\n data,\n meta: {\n page,\n limit,\n total,\n totalPages,\n hasNext: page < totalPages,\n hasPrev: page > 1,\n },\n }\n\n return this.json(response)\n }\n\n // ── Server-Sent Events ──────────────────────────────────────────────\n\n /**\n * Start an SSE (Server-Sent Events) stream.\n * Sets the correct headers and returns helpers to send events.\n *\n * @example\n * ```ts\n * @Get('/events')\n * async stream(ctx: RequestContext) {\n * const sse = ctx.sse()\n *\n * const interval = setInterval(() => {\n * sse.send({ time: new Date().toISOString() }, 'tick')\n * }, 1000)\n *\n * sse.onClose(() => clearInterval(interval))\n * }\n * ```\n */\n sse() {\n this.res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n })\n this.res.flushHeaders()\n\n const closeCallbacks: Array<() => void> = []\n\n this.req.on('close', () => {\n for (const cb of closeCallbacks) cb()\n })\n\n return {\n /** Send an SSE event with optional event name and id */\n send: (data: any, event?: string, id?: string) => {\n if (id) this.res.write(`id: ${id}\\n`)\n if (event) this.res.write(`event: ${event}\\n`)\n this.res.write(`data: ${JSON.stringify(data)}\\n\\n`)\n },\n /** Send a comment (keeps connection alive) */\n comment: (text: string) => {\n this.res.write(`: ${text}\\n\\n`)\n },\n /** Register a callback when the client disconnects */\n onClose: (fn: () => void) => {\n closeCallbacks.push(fn)\n },\n /** End the SSE stream */\n close: () => {\n this.res.end()\n },\n }\n }\n}\n\n/**\n * Short alias for `RequestContext` parameterised by a route shape.\n *\n * Use with `kick typegen`'s generated `KickRoutes` namespace for\n * fully-typed `ctx.params`, `ctx.body`, and `ctx.query`. Metadata\n * typing (`ctx.get()`, `ctx.set()`, `ctx.user`) comes from\n * `ContextMeta` augmentation and works on all `RequestContext` instances.\n *\n * @example\n * ```ts\n * @Get('/:id')\n * getUser(ctx: Ctx<KickRoutes.UserController['getUser']>) {\n * ctx.params.id // typed from RouteShape\n * ctx.body // typed from RouteShape\n * ctx.user // typed from ContextMeta\n * ctx.get('tenant') // typed from ContextMeta\n * }\n * ```\n */\nexport type Ctx<TRoute extends RouteShape = RouteShape> = RequestContext<\n TRoute extends { body: infer B } ? B : any,\n TRoute extends { params: infer P } ? P : any,\n TRoute extends { query: infer Q } ? Q : any\n>\n\n/**\n * Global ambient registry of typed routes, populated by `kick typegen`.\n *\n * Each interface inside `KickRoutes` corresponds to a controller class;\n * each property is one route method on that controller. The values\n * conform to `RouteShape`.\n *\n * Empty by default — declarations come from `.kickjs/types/routes.d.ts`\n * generated alongside the rest of the typegen output.\n */\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace KickRoutes {}\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"context-MD7Mc_o_.mjs","names":[],"sources":["../src/http/request-store.ts","../src/http/context.ts"],"sourcesContent":["import { AsyncLocalStorage } from 'node:async_hooks'\nimport type { MetaValue } from '../core/execution-context'\n\n/** Per-request storage for REQUEST-scoped DI and request context propagation */\nexport interface RequestStore {\n /** Unique request identifier */\n requestId: string\n /**\n * Per-request singleton cache for REQUEST-scoped services. Keys are\n * heterogeneous (class constructors, `InjectionToken` objects, raw\n * strings), so the map type stays `Map<any, any>`.\n */\n instances: Map<any, any>\n /**\n * Per-request key/value bag. Canonical backing store for\n * `RequestContext.set/get` and the Context Contributor pipeline (#107)\n * since Phase 3. Anything written here is visible to every concrete\n * execution context constructed for the same request, regardless of\n * which middleware/contributor wrote it.\n *\n * Keys are constrained to `string` per the locked decision in\n * `architecture.md` §20.12 (#4); the value type is `unknown` because\n * the concrete shape at each key is determined by the consumer's\n * `ContextMeta` augmentation, not by the store itself.\n *\n * Prefer the {@link getRequestValue} helper over reaching into this\n * map directly — it threads the augmented `ContextMeta` shape through\n * `MetaValue<K>` so consumers get typed reads without an `as` cast.\n * Writes should flow through `ctx.set(key, value)` or a Context\n * Contributor's return value — see the `setRequestValue` note below\n * for why this package intentionally does NOT ship a service-level\n * write helper.\n */\n values: Map<string, unknown>\n}\n\n/** AsyncLocalStorage instance shared across the framework */\nexport const requestStore = new AsyncLocalStorage<RequestStore>()\n\n/**\n * Get the current request store. Throws if called outside a request context.\n * Used by the Container to resolve REQUEST-scoped dependencies.\n */\nexport function getRequestStore(): RequestStore {\n const store = requestStore.getStore()\n if (!store) {\n throw new Error(\n 'No active request context. REQUEST-scoped services can only be resolved during an HTTP request.',\n )\n }\n return store\n}\n\n/**\n * Read a per-request `ContextMeta`-keyed value from outside a controller\n * (services, repositories, anywhere with no `ctx` reference). Returns\n * the augmented type for the key — `MetaValue<K>` — so consumers get the\n * same static safety as `ctx.get(key)` without holding the context.\n *\n * Returns `undefined` outside an active request frame (background jobs,\n * startup code, tests without `requestScopeMiddleware()`) — the helper\n * is intentionally null-tolerant so calling it from a service that\n * runs in both request and non-request paths doesn't throw.\n *\n * @example\n * ```ts\n * declare module '@forinda/kickjs' {\n * interface ContextMeta { tenant: { id: string; name: string } }\n * }\n *\n * @Service()\n * class AuditService {\n * log(action: string) {\n * const tenant = getRequestValue('tenant') // typed: { id; name } | undefined\n * db.audit.insert({ action, tenantId: tenant?.id })\n * }\n * }\n * ```\n */\nexport function getRequestValue<K extends string>(key: K): MetaValue<K> | undefined {\n return requestStore.getStore()?.values.get(key) as MetaValue<K> | undefined\n}\n\n// Intentionally NO `setRequestValue` export.\n//\n// Writes are deliberately context-scoped — only the controlled write\n// surfaces should mutate the per-request Map:\n//\n// 1. A Context Contributor's `resolve()` / `onError()` return value\n// (the runner does `ctx.set(reg.key, value)` on the contributor's\n// behalf). This is the canonical way to populate a key.\n// 2. A controller / middleware / contributor that holds a\n// `RequestContext` instance can call `ctx.set(key, value)`\n// directly. The write is auditable inline at the call site.\n//\n// Letting arbitrary services reach in and mutate the store from\n// anywhere produces \"spooky action at a distance\" — keys appear in\n// the request bag without an obvious source, and tracing which\n// service polluted what becomes a grep exercise. Services that need\n// to publish per-request state should return the value to the caller\n// (controller or contributor) and let *that* layer write it via\n// `ctx.set`. If a future need genuinely requires a service-level\n// write surface, expose a narrow function (`recordTrace`,\n// `markStartTime`) on a service that captures the side effect, not a\n// generic `setRequestValue`.\n","/// <reference types=\"multer\" />\nimport type { Request, Response, NextFunction } from 'express'\nimport { type ExecutionContext, type MetaValue } from '../core/execution-context'\nimport { normalizeProblem, type ProblemDetails, type ValidationError } from '../core/errors'\nimport { requestStore } from './request-store'\nimport {\n parseQuery,\n type QueryFieldConfig,\n type PaginatedResponse,\n type TypedParsedQuery,\n} from './query'\n\n/**\n * Input shape for the typed problem-detail convenience methods on\n * {@link ProblemSender} (`ctx.problem.notFound`, `.badRequest`, etc.) —\n * RFC 9457 details minus the `status`, which each method pre-fills.\n */\ntype ProblemInput = Omit<ProblemDetails, 'status'>\n\n/**\n * Callable response helper for RFC 9457 — Problem Details responses.\n * Returned by `ctx.problem`. Invoke directly for a fully-specified\n * payload, or use the named convenience methods to pre-fill `status` +\n * `title` for the common codes.\n *\n * Each call sets `Content-Type: application/problem+json` and serializes\n * the canonical shape (defaulting `type` to `about:blank` and `title` to\n * the IANA reason phrase per RFC 9457 §3.1.1 / §3.1.4).\n *\n * @example\n * ```ts\n * ctx.problem({\n * type: 'https://api.example.com/problems/out-of-credit',\n * status: 403,\n * detail: 'Your balance is 30, but that costs 50.',\n * balance: 30, // extension per §3.2\n * })\n *\n * ctx.problem.notFound({ detail: 'User abc not found' })\n * ctx.problem.validation(zodResult.error.issues)\n * ```\n */\ninterface ProblemSender {\n (input: ProblemDetails): Response\n /** 400 Bad Request — RFC 9457. */\n badRequest(input?: ProblemInput): Response\n /** 401 Unauthorized — RFC 9457. */\n unauthorized(input?: ProblemInput): Response\n /** 403 Forbidden — RFC 9457. */\n forbidden(input?: ProblemInput): Response\n /** 404 Not Found — RFC 9457. */\n notFound(input?: ProblemInput): Response\n /** 409 Conflict — RFC 9457. */\n conflict(input?: ProblemInput): Response\n /** 422 Unprocessable Entity — RFC 9457. */\n unprocessable(input?: ProblemInput): Response\n /** 429 Too Many Requests — RFC 9457. */\n tooManyRequests(input?: ProblemInput): Response\n /** 500 Internal Server Error — RFC 9457. */\n internal(input?: ProblemInput): Response\n /**\n * 422 with Zod issues serialized into the `errors` extension per\n * RFC 9457 §3.2's array convention. Pass `error.issues` from a Zod\n * `safeParse` failure or `error.issues` from a thrown ZodError.\n */\n validation(issues: any[], input?: ProblemInput): Response\n}\n\n/**\n * Recursively marks every property of `T` as `readonly`. Preserves\n * `Function`, `Date`, and `RegExp` unchanged, and converts `Map` / `Set`\n * to `ReadonlyMap` / `ReadonlySet` so their mutating methods (`set`,\n * `delete`, `add`, `clear`) are removed from the public surface.\n * Tuples keep their positional element types — only their per-element\n * mutability is locked.\n *\n * Exposed as a utility for adopters who want to opt into compile-time\n * sealing of their own shapes. **No longer applied to\n * `RequestContext.{body,params,query,headers,file,files}`** — that\n * broke TS narrowing / discriminated-union drilldowns on typed request\n * payloads. Runtime read-only enforcement now lives in\n * {@link makeReadOnlyProxy} (dev-only Proxy that warns on writes,\n * no-op in production).\n *\n * Type-only. No runtime cost. `DeepReadonly<any>` is `any` (TS\n * distributes conditional types over `any`).\n */\nexport type DeepReadonly<T> = T extends (...args: never[]) => unknown\n ? T\n : T extends Date | RegExp\n ? T\n : T extends Map<infer K, infer V>\n ? ReadonlyMap<DeepReadonly<K>, DeepReadonly<V>>\n : T extends Set<infer V>\n ? ReadonlySet<DeepReadonly<V>>\n : T extends readonly (infer U)[]\n ? // Distinguish a regular array from a tuple. A regular `T[]` widens\n // its `length` to `number`; a tuple's `length` is a literal\n // (e.g. `2`). The literal branch maps over `keyof T` so each\n // positional slot keeps its own type — `DeepReadonly<[1, 2]>` →\n // `readonly [1, 2]`, not `readonly (1 | 2)[]`.\n number extends T['length']\n ? ReadonlyArray<DeepReadonly<U>>\n : { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T extends object\n ? { readonly [K in keyof T]: DeepReadonly<T[K]> }\n : T\n\n/**\n * Re-export the augmentable {@link ContextMeta} registry from core/.\n * Declared in `core/execution-context.ts` so non-HTTP transports\n * (WS/queue/cron, V2) share one declaration site. Apps still augment\n * via the canonical `declare module '@forinda/kickjs'` form.\n */\nexport type { ContextMeta } from '../core/execution-context'\n\n/**\n * Symbol key under which the per-request `AbortController` backing\n * `RequestContext.signal` is cached on the Express `req` object.\n * Shared across every `RequestContext` instance built for the same\n * request (router-builder constructs separate wrappers for\n * middleware, the contributor pipeline, and the main handler — they\n * all need the same signal).\n */\nconst signalControllerKey = Symbol.for('kickjs.requestContext.signalController')\n\n/**\n * Symbol key under which per-target read-only Proxy wrappers are\n * cached on the Express `req` object. Used by {@link makeReadOnlyProxy}\n * so repeat access of `ctx.body` / `ctx.params` / etc. returns the same\n * Proxy instance (cheap, and stable under `===` for adopters comparing\n * values across middleware).\n */\nconst readOnlyProxyCacheKey = Symbol.for('kickjs.requestContext.readOnlyProxyCache')\n\n/**\n * `true` iff `v` is a plain object (`{}` / `Object.create(null)`) or an\n * array — the two shapes JSON.parse / Express body-parser produce and\n * the only ones the read-only Proxy recurses into. Class instances,\n * `Date`, `RegExp`, `Map`, `Set`, `Buffer`, streams, and functions\n * pass through unwrapped so their method dispatch and `instanceof`\n * checks keep working.\n */\nfunction isPlainObjectOrArray(v: unknown): v is object {\n if (v === null || typeof v !== 'object') return false\n if (Array.isArray(v)) return true\n const proto = Object.getPrototypeOf(v)\n return proto === Object.prototype || proto === null\n}\n\n/**\n * In dev, wrap `target` in a `Proxy` whose `set` / `deleteProperty` /\n * `defineProperty` traps log a warning and otherwise leave the\n * underlying object untouched. A `get` trap recursively wraps nested\n * plain objects and arrays on access, so deep mutations\n * (`ctx.body.user.name = 'x'`, `ctx.files[0].fieldname = 'y'`,\n * `ctx.body.tags.push(...)`) all surface a warning — matching the\n * prior `DeepReadonly<T>` contract at runtime instead of at the type\n * level. In production (`process.env.NODE_ENV === 'production'`) the\n * proxy is bypassed entirely and `target` is returned as-is so the\n * hot path stays zero-cost.\n *\n * Why a Proxy instead of `DeepReadonly<T>` at the type level: the\n * recursive conditional type interfered with TS narrowing (notably\n * discriminated unions on `ctx.body`) and slowed type-checking on\n * deeply-nested Zod-inferred shapes. Compile-time sealing is gone;\n * runtime warning catches the same class of bug during dev and tests.\n *\n * Trap returns `true` so strict-mode call sites (which is all of them\n * — ES modules are strict) don't *throw* on assignment — they just\n * get a warning + the underlying value stays untouched. Adopters\n * discover the violation when the subsequent read returns the\n * original value, not the one they \"set\".\n *\n * Targets are cached per-`req` under {@link readOnlyProxyCacheKey} so\n * (1) repeat getter access returns the same proxy (`ctx.body ===\n * ctx.body`, `ctx.body.user === ctx.body.user`); and (2) cyclic refs\n * (`body.self = body`) terminate via the cache hit on the second\n * visit. Primitives, `null`, and non-plain values pass through.\n */\nfunction makeReadOnlyProxy<T>(target: T, req: object, label: string): T {\n if (process.env.NODE_ENV === 'production') return target\n if (!isPlainObjectOrArray(target)) return target\n\n type Cache = WeakMap<object, unknown>\n const reqObj = req as Record<symbol, Cache | undefined>\n let cache = reqObj[readOnlyProxyCacheKey]\n if (!cache) {\n cache = new WeakMap()\n reqObj[readOnlyProxyCacheKey] = cache\n }\n const targetObj = target as unknown as object\n const cached = cache.get(targetObj)\n if (cached) return cached as T\n\n const warnWrite = (verb: string, prop: PropertyKey) => {\n console.warn(\n `[kickjs] Attempted to ${verb} \\`ctx.${label}.${String(prop)}\\` — ` +\n `request data is read-only. ` +\n `Stash computed values via \\`ctx.set('key', value)\\` or a Context Contributor, ` +\n `or reach \\`ctx.req.${label}\\` if you genuinely need the raw mutable handle.`,\n )\n }\n\n const proxy = new Proxy(targetObj, {\n get(t, prop, receiver) {\n const value = Reflect.get(t, prop, receiver)\n // Recurse only into plain objects / arrays. Functions (incl.\n // array methods like `.push`) pass through with `receiver` as\n // their `this`, so internal `this[i] = x` writes still hit the\n // `set` trap and warn.\n if (isPlainObjectOrArray(value)) {\n return makeReadOnlyProxy(value, req, `${label}.${String(prop)}`)\n }\n return value\n },\n set(_t, prop) {\n warnWrite('assign', prop)\n return true\n },\n deleteProperty(_t, prop) {\n warnWrite('delete', prop)\n return true\n },\n defineProperty(_t, prop) {\n warnWrite('Object.defineProperty on', prop)\n return true\n },\n })\n cache.set(targetObj, proxy)\n return proxy as T\n}\n\n/**\n * The shape of a single route entry in `KickRoutes`. Generated by\n * `kick typegen` and consumed via the `Ctx<T>` helper. Each field is\n * `unknown` by default and is filled in as the typegen learns more\n * about the route (URL params from the path, body/query from validation\n * schemas, etc.).\n */\nexport interface RouteShape {\n params?: unknown\n body?: unknown\n query?: unknown\n response?: unknown\n}\n\n/**\n * Unified request/response abstraction passed to every controller method.\n * Shields handlers from raw Express objects and provides convenience methods.\n *\n * Implements {@link ExecutionContext} so the transport-agnostic Context\n * Contributor pipeline (#107) can run against this concrete HTTP context\n * the same way it will run against future WS / queue / cron contexts.\n */\nexport class RequestContext<TBody = any, TParams = any, TQuery = any> implements ExecutionContext {\n constructor(\n public readonly req: Request,\n public readonly res: Response,\n public readonly next: NextFunction,\n ) {}\n\n /**\n * Read-side accessor for the per-request metadata map.\n *\n * Returns the AsyncLocalStorage map (`requestStore.getStore().values`)\n * shared with the Context Contributor pipeline when an ALS frame is\n * active. Returns `null` outside any frame — read-side callers\n * (`ctx.get`, `ctx.user`, `ctx.tenantId`, `ctx.roles`) treat that as\n * \"no metadata\" and fall through to their `req.*` defaults. Reads\n * never throw because outside an ALS frame nobody could have written\n * via `ctx.set` anyway, so an empty answer is the truthful one.\n */\n private metadataReadOnly(): Map<string, any> | null {\n return (requestStore.getStore()?.values as Map<string, any> | undefined) ?? null\n }\n\n /**\n * Write-side accessor for the per-request metadata map.\n *\n * Returns the same map as {@link metadataReadOnly} when an ALS frame\n * is active. Throws otherwise — writing to a fallback map outside\n * the ALS frame would silently lose data because other call sites\n * (downstream contributors, request-logger, etc.) read from the\n * canonical store and would never see it. Mount\n * `requestScopeMiddleware()` (Application does this for you in\n * `'auto'` mode) or wrap your test setup in\n * `requestStore.run({...}, () => …)` to fix.\n */\n private metadataForWrite(): Map<string, any> {\n const store = requestStore.getStore()\n if (store) return store.values as Map<string, any>\n throw new Error(\n 'RequestContext.set called outside an AsyncLocalStorage frame. ' +\n 'Mount requestScopeMiddleware() before constructing RequestContext, ' +\n 'or wrap test setup in requestStore.run({ requestId, instances: new Map(), values: new Map() }, () => ...).',\n )\n }\n\n // ── Request Data ────────────────────────────────────────────────────\n\n /**\n * Parsed request body. Wrapped in a dev-only Proxy that warns when\n * adopters try to mutate Zod-validated payloads in place — compute\n * new values and stash them via `ctx.set('key', value)` or a Context\n * Contributor instead. Reach for `ctx.req.body` if you genuinely\n * need the raw, mutable Express handle (rare).\n *\n * In production the Proxy is bypassed and `this.req.body` is\n * returned as-is — no runtime cost on the hot path.\n */\n get body(): TBody {\n return makeReadOnlyProxy<TBody>(this.req.body, this.req, 'body')\n }\n\n /** Path parameters parsed from the route — read-only, see {@link body}. */\n get params(): TParams {\n return makeReadOnlyProxy<TParams>(this.req.params as TParams, this.req, 'params')\n }\n\n /** Parsed query string — read-only, see {@link body}. */\n get query(): TQuery {\n return makeReadOnlyProxy<TQuery>(this.req.query as TQuery, this.req, 'query')\n }\n\n /** Inbound HTTP headers — read-only, see {@link body}. */\n get headers(): Request['headers'] {\n return makeReadOnlyProxy<Request['headers']>(this.req.headers, this.req, 'headers')\n }\n\n get requestId(): string | undefined {\n return (this.req as any).requestId ?? (this.req.headers['x-request-id'] as string | undefined)\n }\n\n /**\n * `AbortSignal` that fires when the underlying HTTP request is\n * closed — either because the client disconnected before the\n * response was sent, or because Node's socket-close event ran\n * after a normal response. Multiple `RequestContext` wrappers for\n * the same request (router-builder constructs one per\n * middleware/contributor pipeline / main handler) share a single\n * `AbortController` cached on the underlying `req` object, so the\n * signal is stable across the request lifecycle.\n *\n * Thread it through anything that takes an `AbortSignal` so the\n * work cancels as soon as the client gives up — e.g. a kickjs-db\n * relational query:\n *\n * ```ts\n * @Get('/:id/full')\n * async show(ctx: RequestContext) {\n * const row = await this.repo.findFullById(ctx.params.id, ctx.signal)\n * if (!row) return ctx.notFound()\n * ctx.json(row)\n * }\n * ```\n *\n * The repo passes `signal` to `db.query.<table>.findUnique({ signal })`;\n * if the client disconnects mid-flight, the in-flight query rejects\n * with `RelationalQueryCancelledError` instead of consuming the\n * connection until completion.\n *\n * Implementation note — both `req.on('close')` and `res.on('close')`\n * are wired so the signal aborts regardless of whether the close\n * event lands on the request stream or the response stream first.\n * Subsequent fires are no-ops (`AbortController.abort()` is\n * idempotent).\n */\n get signal(): AbortSignal {\n const req = this.req as Request & { [signalControllerKey]?: AbortController }\n const cached = req[signalControllerKey]\n if (cached) return cached.signal\n\n const ctrl = new AbortController()\n req[signalControllerKey] = ctrl\n const abort = () => ctrl.abort('request closed')\n this.req.once('close', abort)\n this.res.once('close', abort)\n return ctrl.signal\n }\n\n /** Session data (requires session middleware) */\n get session(): any {\n return (this.req as any).session\n }\n\n /**\n * The authenticated user set by AuthAdapter.\n * Reads from the per-request metadata store first (`ctx.set('user', ...)`),\n * then falls back to `req.user` (set directly by AuthAdapter middleware).\n *\n * Extend the `AuthUser` interface via module augmentation in `@forinda/kickjs-auth`\n * to get full typing.\n *\n * @example\n * ```ts\n * @Get('/me')\n * @Authenticated()\n * getProfile(ctx: RequestContext) {\n * const user = ctx.user\n * if (!user) return ctx.notFound()\n * return ctx.json({ user })\n * }\n * ```\n */\n get user(): MetaValue<'user', Record<string, any>> | undefined {\n return this.metadataReadOnly()?.get('user') ?? (this.req as any).user\n }\n\n /**\n * Tenant identifier for multi-tenant apps. Reads `user.tenantId` —\n * populated either by `AuthAdapter.testMode({ tenantId })`, a tenant\n * resolver middleware, or manually on the strategy's `mapPayload`.\n *\n * Augment `AuthUser` (or `ContextMeta['user']`) to widen the return\n * type if your user shape guarantees a non-`string | undefined` id.\n */\n get tenantId(): string | undefined {\n const u = this.user as Record<string, any> | undefined\n return u?.tenantId\n }\n\n /**\n * Effective role list for the current user. Prefers `user.tenantRoles`\n * (populated by `AuthAdapterOptions.roleResolver` under a tenant) and\n * falls back to `user.roles`. Returns an empty array when the user is\n * absent or the shape has neither field.\n */\n get roles(): string[] {\n const u = this.user as Record<string, any> | undefined\n return u?.tenantRoles ?? u?.roles ?? []\n }\n\n // ── Query String Parsing ───────────────────────────────────────────\n\n /**\n * Parse the request query string into structured filters, sort, pagination, and search.\n * Pass the result to an ORM query builder adapter (Drizzle, Prisma, Sequelize, etc.).\n *\n * Generic over the field config: when passed an inline literal (or\n * `as const`), `filters[].field` and `sort[].field` are narrowed to\n * the exact whitelisted field names. Pass nothing or a non-literal\n * config and you get the loose `string` fallback.\n *\n * @param fieldConfig - Optional whitelist for filterable, sortable, and searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * const parsed = ctx.qs({\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt', 'title'],\n * } as const)\n * parsed.filters[0].field // typed as 'status' | 'priority'\n * parsed.sort[0].field // typed as 'createdAt' | 'title'\n * }\n * ```\n */\n qs<TConfig extends QueryFieldConfig | undefined = undefined>(\n fieldConfig?: TConfig,\n ): TypedParsedQuery<TConfig> {\n return parseQuery<TConfig>(this.req.query as Record<string, any>, fieldConfig)\n }\n\n // ── File Uploads ────────────────────────────────────────────────────\n\n /**\n * Single uploaded file (requires `@FileUpload({ mode: 'single' })`).\n * Wrapped in a dev-only Proxy that warns on mutation — see {@link body}.\n * File metadata is set by the upload middleware and shouldn't be\n * mutated downstream.\n */\n get file(): Express.Multer.File | undefined {\n const f = this.req.file\n return f ? makeReadOnlyProxy<Express.Multer.File>(f, this.req, 'file') : undefined\n }\n\n /**\n * Array of uploaded files (requires `@FileUpload({ mode: 'array' })`).\n * The array (and each entry in dev) is wrapped in a read-only Proxy\n * that warns on `push`/`pop` and per-file mutation. Use\n * `ctx.req.files` if you genuinely need the mutable Express handle.\n */\n get files(): Express.Multer.File[] | undefined {\n // multer's `Request['files']` is a union — array (mode: 'array' /\n // 'any') or `{ [fieldname]: File[] }` (mode: 'fields'). The\n // `@FileUpload({ mode: 'array' })` decorator only sets the array\n // shape, but `Request['files']` is the broad union. Narrow back to\n // `File[]` here for the common-case getter; adopters using\n // `mode: 'fields'` can read `ctx.req.files` directly for the\n // dictionary shape.\n const f = this.req.files\n if (!Array.isArray(f)) return undefined\n return makeReadOnlyProxy<Express.Multer.File[]>(f, this.req, 'files')\n }\n\n // ── Metadata Store ──────────────────────────────────────────────────\n\n /**\n * Read a value from the per-request metadata store.\n *\n * When `ContextMeta` has been augmented with a matching key, the return\n * type is inferred automatically. For ad-hoc keys, pass a generic:\n * `ctx.get<MyType>('custom')`.\n */\n get<K extends string>(key: K): MetaValue<K> | undefined {\n return this.metadataReadOnly()?.get(key) as MetaValue<K> | undefined\n }\n\n /**\n * Write a value to the per-request metadata store. Throws when no\n * AsyncLocalStorage frame is active — see {@link metadataForWrite}\n * for why a silent fallback is the wrong default.\n */\n set<K extends string>(key: K, value: MetaValue<K>): void {\n this.metadataForWrite().set(key, value)\n }\n\n // ── Response Helpers ────────────────────────────────────────────────\n\n json(data: any, status = 200) {\n return this.res.status(status).json(data)\n }\n\n created(data: any) {\n return this.res.status(201).json(data)\n }\n\n noContent() {\n return this.res.status(204).end()\n }\n\n /**\n * @deprecated Prefer {@link ProblemSender.notFound | ctx.problem.notFound()},\n * which emits an RFC 9457 — Problem Details response. This helper stays\n * for backward compatibility and will not be removed.\n */\n notFound(message = 'Not Found') {\n return this.res.status(404).json({ message })\n }\n\n /**\n * @deprecated Prefer {@link ProblemSender.badRequest | ctx.problem.badRequest()},\n * which emits an RFC 9457 — Problem Details response. This helper stays\n * for backward compatibility and will not be removed.\n */\n badRequest(message: string) {\n return this.res.status(400).json({ message })\n }\n\n // ── RFC 9457 Problem Details ───────────────────────────────────────\n\n /**\n * RFC 9457 — Problem Details for HTTP APIs response helper.\n * https://datatracker.ietf.org/doc/html/rfc9457\n *\n * `ctx.problem(...)` sends a canonical problem-detail response with\n * `Content-Type: application/problem+json`. The convenience methods\n * (`ctx.problem.notFound`, `.badRequest`, etc.) pre-fill `status` +\n * `title` for the common codes.\n *\n * `type` defaults to `about:blank` (§3.1.1); `title` defaults to the\n * IANA reason phrase for `status` (§3.1.4). Extension members per §3.2\n * pass through unchanged.\n *\n * @example\n * ```ts\n * ctx.problem({\n * type: 'https://api.example.com/problems/out-of-credit',\n * status: 403,\n * detail: 'Your balance is 30, but that costs 50.',\n * instance: `/account/${ctx.user.id}/messages/${ctx.params.id}`,\n * balance: 30,\n * })\n *\n * ctx.problem.notFound({ detail: 'User abc not found' })\n * ctx.problem.validation(zodResult.error.issues)\n * ```\n *\n * @see {@link ProblemException} for the exception-throwing equivalent\n * (use it from services where `ctx` isn't in scope).\n */\n get problem(): ProblemSender {\n const send = (input: ProblemDetails): Response => this.sendProblem(input)\n send.badRequest = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 400, ...input })\n send.unauthorized = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 401, ...input })\n send.forbidden = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 403, ...input })\n send.notFound = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 404, ...input })\n send.conflict = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 409, ...input })\n send.unprocessable = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 422, ...input })\n send.tooManyRequests = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 429, ...input })\n send.internal = (input: ProblemInput = {}): Response =>\n this.sendProblem({ status: 500, ...input })\n send.validation = (issues: any[], input: ProblemInput = {}): Response => {\n const errors: ValidationError[] = (issues || []).map((issue: any) => ({\n field: issue.path?.join?.('.') ?? '',\n message: issue.message ?? '',\n code: issue.code,\n }))\n return this.sendProblem({\n status: 422,\n detail: issues?.[0]?.message,\n ...input,\n errors,\n })\n }\n return send as ProblemSender\n }\n\n private sendProblem(input: ProblemDetails): Response {\n const body = normalizeProblem(input)\n this.res.setHeader('Content-Type', 'application/problem+json')\n return this.res.status(body.status).json(body)\n }\n\n html(content: string, status = 200) {\n return this.res.status(status).type('html').send(content)\n }\n\n download(buffer: Buffer, filename: string, contentType = 'application/octet-stream') {\n this.res.setHeader('Content-Disposition', `attachment; filename=\"${filename}\"`)\n this.res.setHeader('Content-Type', contentType)\n return this.res.send(buffer)\n }\n\n /**\n * Render a template using the registered view engine (EJS, Pug, Handlebars, etc.).\n * Requires a ViewAdapter to be configured in bootstrap().\n *\n * @param template - Template name (without extension, relative to viewsDir)\n * @param data - Data to pass to the template\n *\n * @example\n * ```ts\n * ctx.render('dashboard', { user, title: 'Dashboard' })\n * ctx.render('emails/welcome', { name: 'Alice' })\n * ```\n */\n render(template: string, data: Record<string, any> = {}) {\n return this.res.render(template, data)\n }\n\n /**\n * Parse query params and return a standardized paginated response.\n * Calls `ctx.qs()` internally, then wraps your data with pagination meta.\n *\n * @param fetcher - Async function that receives ParsedQuery and returns `{ data, total }`\n * @param fieldConfig - Optional whitelist for filterable, sortable, searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * return ctx.paginate(\n * async (parsed) => {\n * const data = await db.select().from(users)\n * .where(query.where).limit(parsed.pagination.limit)\n * .offset(parsed.pagination.offset).all()\n * const total = await db.select({ count: count() }).from(users).get()\n * return { data, total: total?.count ?? 0 }\n * },\n * { filterable: ['name', 'role'], sortable: ['createdAt'] },\n * )\n * }\n * ```\n */\n async paginate<T, TConfig extends QueryFieldConfig | undefined = undefined>(\n fetcher: (parsed: TypedParsedQuery<TConfig>) => Promise<{ data: T[]; total: number }>,\n fieldConfig?: TConfig,\n ) {\n const parsed = this.qs<TConfig>(fieldConfig)\n const { data, total } = await fetcher(parsed)\n const { page, limit } = parsed.pagination\n const totalPages = Math.ceil(total / limit) || 1\n\n const response: PaginatedResponse<T> = {\n data,\n meta: {\n page,\n limit,\n total,\n totalPages,\n hasNext: page < totalPages,\n hasPrev: page > 1,\n },\n }\n\n return this.json(response)\n }\n\n // ── Server-Sent Events ──────────────────────────────────────────────\n\n /**\n * Start an SSE (Server-Sent Events) stream.\n * Sets the correct headers and returns helpers to send events.\n *\n * @example\n * ```ts\n * @Get('/events')\n * async stream(ctx: RequestContext) {\n * const sse = ctx.sse()\n *\n * const interval = setInterval(() => {\n * sse.send({ time: new Date().toISOString() }, 'tick')\n * }, 1000)\n *\n * sse.onClose(() => clearInterval(interval))\n * }\n * ```\n */\n sse() {\n this.res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n })\n this.res.flushHeaders()\n\n const closeCallbacks: Array<() => void> = []\n\n this.req.on('close', () => {\n for (const cb of closeCallbacks) cb()\n })\n\n return {\n /** Send an SSE event with optional event name and id */\n send: (data: any, event?: string, id?: string) => {\n if (id) this.res.write(`id: ${id}\\n`)\n if (event) this.res.write(`event: ${event}\\n`)\n this.res.write(`data: ${JSON.stringify(data)}\\n\\n`)\n },\n /** Send a comment (keeps connection alive) */\n comment: (text: string) => {\n this.res.write(`: ${text}\\n\\n`)\n },\n /** Register a callback when the client disconnects */\n onClose: (fn: () => void) => {\n closeCallbacks.push(fn)\n },\n /** End the SSE stream */\n close: () => {\n this.res.end()\n },\n }\n }\n}\n\n/**\n * Short alias for `RequestContext` parameterised by a route shape.\n *\n * Use with `kick typegen`'s generated `KickRoutes` namespace for\n * fully-typed `ctx.params`, `ctx.body`, and `ctx.query`. Metadata\n * typing (`ctx.get()`, `ctx.set()`, `ctx.user`) comes from\n * `ContextMeta` augmentation and works on all `RequestContext` instances.\n *\n * @example\n * ```ts\n * @Get('/:id')\n * getUser(ctx: Ctx<KickRoutes.UserController['getUser']>) {\n * ctx.params.id // typed from RouteShape\n * ctx.body // typed from RouteShape\n * ctx.user // typed from ContextMeta\n * ctx.get('tenant') // typed from ContextMeta\n * }\n * ```\n */\nexport type Ctx<TRoute extends RouteShape = RouteShape> = RequestContext<\n TRoute extends { body: infer B } ? B : any,\n TRoute extends { params: infer P } ? P : any,\n TRoute extends { query: infer Q } ? Q : any\n>\n\n/**\n * Global ambient registry of typed routes, populated by `kick typegen`.\n *\n * Each interface inside `KickRoutes` corresponds to a controller class;\n * each property is one route method on that controller. The values\n * conform to `RouteShape`.\n *\n * Empty by default — declarations come from `.kickjs/types/routes.d.ts`\n * generated alongside the rest of the typegen output.\n */\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-namespace\n namespace KickRoutes {}\n}\n"],"mappings":"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"}
|
package/dist/core/adapter.mjs
CHANGED
package/dist/core/app-module.mjs
CHANGED
package/dist/core/cache.mjs
CHANGED
package/dist/core/container.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.16.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,4 +8,4 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{n as tokenName,t as Container}from"../container-
|
|
11
|
+
import{n as tokenName,t as Container}from"../container-D1gfs6__.mjs";export{Container,tokenName};
|
package/dist/core/cron.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.16.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{c as pushClassMeta,t as getClassMeta}from"../metadata-
|
|
11
|
+
import{c as pushClassMeta,t as getClassMeta}from"../metadata-Cll3P0N0.mjs";const CRON_META=`kick/cron`;function Cron(expression,options){return(target,propertyKey)=>{pushClassMeta(CRON_META,target.constructor,{expression,handlerName:propertyKey,description:options?.description,timezone:options?.timezone,runOnInit:options?.runOnInit})}}function getCronJobs(target){return getClassMeta(CRON_META,target,[])}export{CRON_META,Cron,getCronJobs};
|
|
12
12
|
//# sourceMappingURL=cron.mjs.map
|
package/dist/core/decorators.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.16.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{t as Container}from"../container-
|
|
11
|
+
import{t as Container}from"../container-D1gfs6__.mjs";import{METADATA}from"./interfaces.mjs";import{c as pushClassMeta,d as setInMetaMap,f as setInMetaRecord,l as pushMethodMeta,p as setMethodMeta,u as setClassMeta}from"../metadata-Cll3P0N0.mjs";import"reflect-metadata";const pendingRegistrations=[],allRegistrations=new Map;let containerRef=null;function flushPending(container){containerRef=container;for(let{target,scope}of pendingRegistrations)container.has(target)||container.register(target,target,scope);pendingRegistrations.length=0}Container._onReady=flushPending,Container._onReset=container=>{containerRef=container;for(let[,{target,scope}]of allRegistrations)container.has(target)||container.register(target,target,scope)};function registerInContainer(target,scope){setClassMeta(METADATA.INJECTABLE,!0,target),setClassMeta(METADATA.SCOPE,scope,target);let name=target.name||String(target);allRegistrations.set(name,{target,scope}),containerRef?containerRef.has(target)||containerRef.register(target,target,scope):pendingRegistrations.push({target,scope})}function Injectable(options){return target=>{setClassMeta(METADATA.CLASS_KIND,`injectable`,target),registerInContainer(target,options?.scope??`singleton`)}}function Service(options){return target=>{setClassMeta(METADATA.CLASS_KIND,`service`,target),registerInContainer(target,options?.scope??`singleton`)}}function Component(options){return target=>{setClassMeta(METADATA.CLASS_KIND,`component`,target),registerInContainer(target,options?.scope??`singleton`)}}function Repository(options){return target=>{setClassMeta(METADATA.CLASS_KIND,`repository`,target),registerInContainer(target,options?.scope??`singleton`)}}function Controller(){return target=>{setClassMeta(METADATA.CLASS_KIND,`controller`,target),registerInContainer(target,`singleton`)}}function PostConstruct(){return(target,propertyKey)=>{setClassMeta(METADATA.POST_CONSTRUCT,propertyKey,target)}}function applyInjection(token,target,propertyKey,parameterIndex){if(typeof parameterIndex==`number`){setInMetaRecord(METADATA.INJECT,target,parameterIndex,token);return}propertyKey!=null&&setInMetaMap(METADATA.AUTOWIRED,target,propertyKey,token)}function Autowired(token){return((target,propertyKey,parameterIndex)=>{applyInjection(token,target,propertyKey,parameterIndex)})}function Inject(token){return((target,propertyKey,parameterIndex)=>{applyInjection(token,target,propertyKey,parameterIndex)})}function Value(envKey,defaultValue){return(target,propertyKey)=>{setInMetaMap(METADATA.VALUE,target,propertyKey,{envKey,defaultValue})}}function Asset(assetKey){return(target,propertyKey)=>{setInMetaMap(METADATA.ASSET,target,propertyKey,{assetKey})}}function createRouteDecorator(method){return(path,validation)=>(target,propertyKey)=>{pushClassMeta(METADATA.ROUTES,target.constructor,{method,path:path||`/`,handlerName:propertyKey,validation})}}const Get=createRouteDecorator(`GET`),Post=createRouteDecorator(`POST`),Put=createRouteDecorator(`PUT`),Delete=createRouteDecorator(`DELETE`),Patch=createRouteDecorator(`PATCH`);function normalizeApiQueryParamsConfig(config){return`columns`in config&&config.columns&&typeof config.columns==`object`?{filterable:Object.keys(config.columns),sortable:config.sortable?Object.keys(config.sortable):void 0,searchable:config.searchColumns?config.searchColumns.map(col=>col?.name??``).filter(Boolean):void 0}:config}function ApiQueryParams(config){return(target,propertyKey)=>{let normalized=normalizeApiQueryParamsConfig(config);setMethodMeta(METADATA.QUERY_PARAMS,normalized,target.constructor,propertyKey)}}function Middleware(...handlers){return(target,propertyKey)=>{propertyKey?pushMethodMeta(METADATA.METHOD_MIDDLEWARES,target.constructor,propertyKey,...handlers):pushClassMeta(METADATA.CLASS_MIDDLEWARES,target,...handlers)}}function FileUpload(config){return(target,propertyKey)=>{setMethodMeta(METADATA.FILE_UPLOAD,config,target.constructor,propertyKey)}}function attachBuilder(target){setClassMeta(METADATA.BUILDER,!0,target),target.builder=function(){let props={},proxy=new Proxy({},{get(_,key){return key===`build`?()=>Object.assign(new target,props):value=>(props[key]=value,proxy)}});return proxy}}function Builder(target){attachBuilder(target)}function withBuilder(ctor){return attachBuilder(ctor),ctor}export{ApiQueryParams,Asset,Autowired,Builder,Component,Controller,Delete,FileUpload,Get,Inject,Injectable,Middleware,Patch,Post,PostConstruct,Put,Repository,Service,Value,normalizeApiQueryParamsConfig,withBuilder};
|
|
12
12
|
//# sourceMappingURL=decorators.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"decorators.mjs","names":[],"sources":["../../src/core/decorators.ts"],"sourcesContent":["import 'reflect-metadata'\nimport { METADATA, Scope, type BuilderOf, type ServiceOptions } from './interfaces'\nimport { Container, type KickJsRegistry } from './container'\nimport {\n setClassMeta,\n setMethodMeta,\n pushClassMeta,\n pushMethodMeta,\n setInMetaMap,\n setInMetaRecord,\n} from './metadata'\n\n// ── Decorator Registration System ───────────────────────────────────────\n// Decorators execute at class-definition time (module load). The Container\n// may not exist yet when the first decorator fires. We queue registrations\n// and flush them on first Container.getInstance() call.\n//\n// `allRegistrations` is a persistent registry that survives Container.reset().\n// On reset (HMR), all previously decorated classes are re-registered on the\n// fresh container. This ensures @Service, @Controller, @Repository, etc.\n// all survive HMR without manual re-registration.\n\ntype PendingRegistration = { target: any; scope: Scope }\nconst pendingRegistrations: PendingRegistration[] = []\n// Keyed by class name to prevent memory leaks during HMR — new class with\n// the same name replaces the old entry instead of accumulating references.\nconst allRegistrations = new Map<string, { target: any; scope: Scope }>()\nlet containerRef: any = null\n\nfunction flushPending(container: any): void {\n containerRef = container\n for (const { target, scope } of pendingRegistrations) {\n if (!container.has(target)) {\n container.register(target, target, scope)\n }\n }\n pendingRegistrations.length = 0\n}\n\n// Wire up synchronously — Container._onReady is called on first getInstance()\nContainer._onReady = flushPending\n\n// On Container.reset(), update containerRef and replay ALL decorator\n// registrations on the fresh container. This handles HMR where the container\n// is wiped but not all decorated modules are re-evaluated.\nContainer._onReset = (container: any) => {\n containerRef = container\n for (const [, { target, scope }] of allRegistrations) {\n if (!container.has(target)) {\n container.register(target, target, scope)\n }\n }\n}\n\n// ── Class Decorators ────────────────────────────────────────────────────\n\nfunction registerInContainer(target: any, scope: Scope): void {\n setClassMeta(METADATA.INJECTABLE, true, target)\n setClassMeta(METADATA.SCOPE, scope, target)\n\n // Track in persistent registry — survives Container.reset() for HMR replay.\n // Keyed by name so HMR class re-creation replaces the old entry.\n const name = target.name || String(target)\n allRegistrations.set(name, { target, scope })\n\n if (containerRef) {\n // Container already initialized — register immediately\n if (!containerRef.has(target)) {\n containerRef.register(target, target, scope)\n }\n } else {\n // Container not ready yet — queue for later\n pendingRegistrations.push({ target, scope })\n }\n}\n\n/** Mark a class as injectable with lifecycle scope */\nexport function Injectable(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'injectable', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/** Mark a class as a service (semantic alias for Injectable) */\nexport function Service(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'service', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/** Mark a class as a generic managed component */\nexport function Component(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'component', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/** Mark a class as a repository */\nexport function Repository(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'repository', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/**\n * Mark a class as an HTTP controller and register it in the DI container.\n *\n * Route prefixes are defined by the module's `routes().path` — the single\n * source of truth for where routes mount. The legacy\n * `@Controller('/path')` parameter was removed in v4; if your code\n * still passes a string, see `docs/guide/migration-v3-to-v4.md` for\n * the rename map.\n */\nexport function Controller(): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'controller', target)\n registerInContainer(target, Scope.SINGLETON)\n }\n}\n\n// ── Method Decorators ───────────────────────────────────────────────────\n\n/** Mark a method as a lifecycle hook called after instantiation */\nexport function PostConstruct(): MethodDecorator {\n return (target, propertyKey) => {\n setClassMeta(METADATA.POST_CONSTRUCT, propertyKey, target)\n }\n}\n\n// ── Injection Decorators (property + constructor parameter) ─────────────\n\n/**\n * A decorator that's valid as either a property decorator OR a\n * constructor-parameter decorator. forinda/kick-js#235 §2 — `@Autowired`\n * and `@Inject` previously diverged at the type level (property-only\n * vs parameter-only) and adopters who picked the wrong name for the\n * position got a cryptic TS1240 error. Both names now accept either\n * position and route to the correct metadata bucket at runtime.\n */\nexport interface PropertyOrParameterDecorator {\n /** Property decorator form — `@Autowired(TOKEN) private foo!: Foo`. */\n (target: object, propertyKey: string | symbol): void\n /** Constructor-parameter form — `constructor(@Inject(TOKEN) foo: Foo) {}`. */\n (target: object, propertyKey: string | symbol | undefined, parameterIndex: number): void\n}\n\nfunction applyInjection(\n token: unknown,\n target: object,\n propertyKey: string | symbol | undefined,\n parameterIndex: number | undefined,\n): void {\n // Legacy parameter decorators receive 3 args (target, propertyKey,\n // parameterIndex); property decorators receive 2 (target, propertyKey).\n // The 3rd-arg-number-vs-undefined check is the canonical position\n // detection in TypeScript's legacy decorator runtime.\n if (typeof parameterIndex === 'number') {\n setInMetaRecord(METADATA.INJECT, target, parameterIndex, token)\n return\n }\n if (propertyKey != null) {\n // `target` for a property decorator on a non-static field is the\n // class prototype; the AUTOWIRED bucket is keyed off the prototype\n // identity at resolution time, so pass it through unchanged.\n setInMetaMap(METADATA.AUTOWIRED, target, propertyKey as string, token)\n }\n}\n\n/**\n * Inject a dependency. Works in two positions:\n *\n * - **Property decorator** — `@Autowired(TOKEN) private repo!: UserRepo`.\n * Resolved lazily from the container the first time the property is\n * read.\n * - **Constructor parameter decorator** — `constructor(@Autowired(TOKEN) repo: UserRepo) {}`.\n * Resolved at instantiation; injected into the ctor call.\n *\n * `@Inject` is the same function under another name — they share\n * runtime + types so the two names are interchangeable. Pick whichever\n * reads better at the call site.\n *\n * Like `@Inject`, the typed string-literal overload narrows the token\n * to a key of the augmented `KickJsRegistry`. After `kick typegen`\n * populates the registry, `@Autowired('kick/prisma/Client')` auto-\n * completes the key and typo'd literals become TS2345 errors.\n */\nexport function Autowired<K extends keyof KickJsRegistry & string>(\n token: K,\n): PropertyOrParameterDecorator\nexport function Autowired(token?: unknown): PropertyOrParameterDecorator\nexport function Autowired(token?: unknown): PropertyOrParameterDecorator {\n return ((\n target: object,\n propertyKey: string | symbol | undefined,\n parameterIndex?: number,\n ): void => {\n applyInjection(token, target, propertyKey, parameterIndex)\n }) as PropertyOrParameterDecorator\n}\n\n/**\n * Inject a dependency by token. Same shape as {@link Autowired} —\n * works as either a property decorator or a constructor-parameter\n * decorator. Pick whichever name reads better at the call site.\n *\n * ## Typed string-literal overload\n *\n * When called with a string literal that matches a key of the\n * augmented `KickJsRegistry`, TypeScript narrows the parameter type\n * to the registered type. Typo'd literals become compile errors\n * (the string isn't `keyof KickJsRegistry`). Class identities and\n * `InjectionToken<T>` keep working unchanged via the second overload.\n *\n * @example\n * ```ts\n * import { Service, Inject } from '@forinda/kickjs'\n * import type { PrismaClient } from '@prisma/client'\n *\n * `@Service()`\n * class UserRepo {\n * constructor(@Inject('kick/prisma/Client') private db: PrismaClient) {}\n * // OR property form, same decorator:\n * @Inject('kick/prisma/Client') private db2!: PrismaClient\n * }\n * ```\n *\n * After `kick typegen` runs, the literal autocompletes from the\n * registry and a typo (`@Inject('kick/prisma/Cleint')`) becomes a\n * TS2345 error.\n */\nexport function Inject<K extends keyof KickJsRegistry & string>(\n token: K,\n): PropertyOrParameterDecorator\nexport function Inject(token?: unknown): PropertyOrParameterDecorator\nexport function Inject(token?: unknown): PropertyOrParameterDecorator {\n return ((\n target: object,\n propertyKey: string | symbol | undefined,\n parameterIndex?: number,\n ): void => {\n applyInjection(token, target, propertyKey, parameterIndex)\n }) as PropertyOrParameterDecorator\n}\n\n/**\n * Global ambient registry of typed environment variables, populated by\n * `kick typegen` from the project's env schema (typically\n * `src/env.ts`'s `export default defineEnv(...)`).\n *\n * Empty by default — declarations come from `.kickjs/types/env.ts`\n * generated alongside the rest of the typegen output. Once populated,\n * `@Value('PORT')` autocompletes the key, type-checks the default\n * value, and `process.env.PORT` is also typed via the parallel\n * `NodeJS.ProcessEnv` augmentation in the same generated file.\n */\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n interface KickEnv {}\n}\n\n/** A string key known to the typed env registry (`never` until typegen runs) */\nexport type EnvKey = keyof KickEnv & string\n\n/**\n * Look up the type of a typed env entry.\n *\n * Decorators can't propagate the property type from the key, so users\n * still need to write the property's type — but `Env<K>` lets them\n * derive it from `KickEnv` instead of duplicating the schema:\n *\n * ```ts\n * @Service()\n * class MyService {\n * @Value('PORT')\n * private port!: Env<'PORT'> // → number, narrowed from KickEnv['PORT']\n * }\n * ```\n */\nexport type Env<K extends EnvKey = EnvKey> = KickEnv[K]\n\n/**\n * Constraint that allows any string when the typed env registry is\n * empty (no `kick typegen` has run yet, so existing code with raw\n * string keys keeps working) but locks the argument down to known\n * `EnvKey` literals once the registry is populated.\n *\n * Implementation: when `EnvKey` is `never`, the conditional resolves\n * to the user's `K`, accepting anything. Once typegen has populated\n * `KickEnv`, `EnvKey` becomes a real union and the conditional forces\n * `K` to be assignable to it.\n */\ntype ValueKey<K extends string> = [EnvKey] extends [never] ? K : K & EnvKey\n\n/**\n * Inject an environment variable value. Evaluated lazily so the env\n * is available at access time, not at decoration time.\n *\n * If no default is provided and the env var is missing, throws at access time\n * to catch misconfiguration early instead of returning undefined.\n *\n * Uses metadata + instance getter to work correctly with `useDefineForClassFields`.\n *\n * Type-safety:\n * - Without `kick typegen` run, any string key is accepted (back-compat).\n * - After typegen, `KickEnv` is populated and unknown keys become a tsc\n * error. The optional `defaultValue` is type-checked against the\n * schema's inferred type for that key.\n */\nexport function Value<K extends string>(\n envKey: ValueKey<K>,\n defaultValue?: K extends EnvKey ? KickEnv[K] : unknown,\n): PropertyDecorator {\n return (target, propertyKey) => {\n setInMetaMap(METADATA.VALUE, target, propertyKey as string, { envKey, defaultValue })\n }\n}\n\n// ── @Asset — typed asset path injection (assets-plan.md PR 3+) ─────────\n\nimport type { KickAssets } from './assets'\n\n/**\n * Flatten the nested `KickAssets` augmentation (typegen-emitted in\n * PR 4) into a string-literal union of dot/slash-joined paths.\n *\n * `interface KickAssets { mails: { welcome: () => string; orders: {\n * confirmation: () => string } } }` becomes\n * `'mails/welcome' | 'mails/orders/confirmation'`.\n *\n * Returns `never` when `KickAssets` is empty (no typegen has run yet)\n * so `AssetKeyArg` falls through to accept any string.\n */\ntype FlattenAssets<T, Prefix extends string = ''> = {\n [K in keyof T & string]: T[K] extends () => string\n ? `${Prefix}${K}`\n : T[K] extends Record<string, unknown>\n ? FlattenAssets<T[K], `${Prefix}${K}/`>\n : never\n}[keyof T & string]\n\n/** Flat union of every known `<namespace>/<key>` path. */\nexport type AssetKey = FlattenAssets<KickAssets>\n\n/**\n * Constraint mirroring `ValueKey<K>` — accepts any string when\n * `AssetKey` is `never` (back-compat for adopters who haven't run\n * `kick typegen` yet) but locks the argument to known literals once\n * the typegen-augmented `KickAssets` interface is populated.\n */\ntype AssetKeyArg<K extends string> = [AssetKey] extends [never] ? K : K & AssetKey\n\n/**\n * Inject the resolved file-system path for a typed asset. Mirrors\n * `@Value`'s lazy-getter pattern — the asset is resolved on every\n * property access, NOT at class instantiation, so:\n *\n * - Tests can swap fixtures + clear the cache without re-instantiating\n * the consuming class.\n * - The first access cost is paid once per process (the manifest cache\n * in `core/assets.ts` makes subsequent calls cheap).\n *\n * Type-safety:\n *\n * - Without typegen, any string key is accepted (back-compat — same\n * trick as `@Value`).\n * - After typegen, `KickAssets` is populated and `AssetKey` is a\n * union of every flattened `<namespace>/<key>` path. Unknown keys\n * become a TS error.\n *\n * @example\n * ```ts\n * import { Service, Asset } from '@forinda/kickjs'\n *\n * `@Service()`\n * class MailService {\n * `@Asset('mails/welcome')`\n * private welcomeTemplate!: string\n *\n * send(user: User) {\n * return ejs.renderFile(this.welcomeTemplate, { user })\n * }\n * }\n * ```\n */\nexport function Asset<K extends string>(assetKey: AssetKeyArg<K>): PropertyDecorator {\n return (target, propertyKey) => {\n setInMetaMap(METADATA.ASSET, target, propertyKey as string, { assetKey })\n }\n}\n\n// ── HTTP Route Decorators ───────────────────────────────────────────────\n\nexport interface RouteDefinition {\n method: string\n path: string\n handlerName: string\n validation?: {\n /** JSON Schema object for validating the request body */\n body?: any\n /** JSON Schema object for validating query parameters */\n query?: any\n /** JSON Schema object for validating URL params */\n params?: any\n /** Schema name in OpenAPI components/schemas for the request body. Auto-generated from handler name if omitted. */\n name?: string\n }\n}\n\nfunction createRouteDecorator(method: string) {\n return (path?: string, validation?: RouteDefinition['validation']): MethodDecorator => {\n return (target, propertyKey) => {\n pushClassMeta<RouteDefinition>(METADATA.ROUTES, target.constructor, {\n method,\n path: path || '/',\n handlerName: propertyKey as string,\n validation,\n })\n }\n }\n}\n\nexport const Get = createRouteDecorator('GET')\nexport const Post = createRouteDecorator('POST')\nexport const Put = createRouteDecorator('PUT')\nexport const Delete = createRouteDecorator('DELETE')\nexport const Patch = createRouteDecorator('PATCH')\n\n// ── Query Params Decorator ─────────────────────────────────────────────\n\nexport interface ApiQueryParamsConfig {\n /** Fields that can be used in filter queries (e.g., `?filter=status:eq:active`) */\n filterable?: string[]\n /** Fields that can be used in sort queries (e.g., `?sort=createdAt:desc`) */\n sortable?: string[]\n /** Fields that are searched with the `?q=` parameter */\n searchable?: string[]\n}\n\n/**\n * Column-object-based query params config (e.g., from DrizzleQueryParamsConfig).\n * `Object.keys()` is used to derive field names for OpenAPI docs.\n */\nexport interface ColumnApiQueryParamsConfig {\n columns: Record<string, any>\n sortable?: Record<string, any>\n searchColumns?: any[]\n [key: string]: any\n}\n\n/**\n * Normalize a query params config to the string-based ApiQueryParamsConfig.\n * Handles both string-based and column-object-based configs.\n */\nexport function normalizeApiQueryParamsConfig(\n config: ApiQueryParamsConfig | ColumnApiQueryParamsConfig,\n): ApiQueryParamsConfig {\n if ('columns' in config && config.columns && typeof config.columns === 'object') {\n return {\n filterable: Object.keys(config.columns),\n sortable: config.sortable ? Object.keys(config.sortable) : undefined,\n searchable: config.searchColumns\n ? config.searchColumns.map((col: any) => col?.name ?? '').filter(Boolean)\n : undefined,\n }\n }\n return config as ApiQueryParamsConfig\n}\n\n/**\n * Document the query parameters accepted by a GET endpoint.\n * Used by SwaggerAdapter to generate `filter`, `sort`, `page`, `limit`, and `q` params\n * in the OpenAPI spec, with descriptions listing the allowed fields.\n *\n * Accepts both string-based configs and column-object configs (e.g., DrizzleQueryParamsConfig).\n *\n * @example\n * ```ts\n * // String-based\n * @ApiQueryParams({\n * filterable: ['status', 'category', 'price'],\n * sortable: ['name', 'createdAt', 'price'],\n * searchable: ['name', 'description'],\n * })\n *\n * // Column-object-based (Drizzle)\n * @ApiQueryParams(TASK_QUERY_CONFIG)\n * ```\n */\nexport function ApiQueryParams(\n config: ApiQueryParamsConfig | ColumnApiQueryParamsConfig,\n): MethodDecorator {\n return (target, propertyKey) => {\n const normalized = normalizeApiQueryParamsConfig(config)\n setMethodMeta(METADATA.QUERY_PARAMS, normalized, target.constructor, propertyKey as string)\n }\n}\n\n// ── Middleware Decorators ───────────────────────────────────────────────\n\n/**\n * Middleware handler function.\n * Generic `TCtx` defaults to `any` — import `RequestContext` from\n * `@forinda/kickjs` for full type safety:\n *\n * ```ts\n * import type { MiddlewareHandler } from '@forinda/kickjs'\n * import type { RequestContext } from '@forinda/kickjs'\n *\n * const auth: MiddlewareHandler<RequestContext> = (ctx, next) => {\n * ctx.body // fully typed\n * next()\n * }\n * ```\n */\nexport type MiddlewareHandler<TCtx = any> = (ctx: TCtx, next: () => void) => void | Promise<void>\n\n/** Attach middleware handlers to a class or method */\nexport function Middleware(...handlers: MiddlewareHandler[]): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n pushMethodMeta(\n METADATA.METHOD_MIDDLEWARES,\n target.constructor,\n propertyKey as string,\n ...handlers,\n )\n } else {\n pushClassMeta(METADATA.CLASS_MIDDLEWARES, target, ...handlers)\n }\n }\n}\n\n// ── File Upload Decorator ───────────────────────────────────────────────\n\n/**\n * File filter — accepts short extensions ('jpg'), full MIME types ('image/jpeg'),\n * wildcards ('image/*'), or a function `(mimetype, filename) => boolean` for full control.\n */\nexport type FileTypeFilter = string[] | ((mimetype: string, filename: string) => boolean)\n\n/**\n * Shared upload options used by both the `@FileUpload` decorator and the\n * `upload.single()` / `upload.array()` / `upload.none()` middleware.\n */\nexport interface BaseUploadOptions {\n /** Max file size in bytes (default: 5MB) */\n maxSize?: number\n /**\n * Allowed file types:\n * - **string[]** — short extensions ('jpg'), full MIME types ('image/jpeg'), or wildcards ('image/*')\n * - **function** — `(mimetype, filename) => boolean` for full control\n */\n allowedTypes?: FileTypeFilter\n /** Extend the built-in extension-to-MIME map */\n customMimeMap?: Record<string, string>\n}\n\nexport interface FileUploadConfig extends BaseUploadOptions {\n mode: 'single' | 'array' | 'none'\n /** Form field name (default: 'file') */\n fieldName?: string\n /** Max files for array mode (default: 10) */\n maxCount?: number\n}\n\n/** Configure file upload handling for a controller method */\nexport function FileUpload(config: FileUploadConfig): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(METADATA.FILE_UPLOAD, config, target.constructor, propertyKey as string)\n }\n}\n\n// ── Builder Decorator ───────────────────────────────────────────────────\n\nfunction attachBuilder(target: any): void {\n setClassMeta(METADATA.BUILDER, true, target)\n target.builder = function () {\n const props: Record<string, any> = {}\n const proxy: any = new Proxy(\n {},\n {\n get(_, key) {\n if (key === 'build') {\n return () => Object.assign(new target(), props)\n }\n return (value: any) => {\n props[key as string] = value\n return proxy\n }\n },\n },\n )\n return proxy\n }\n}\n\n/**\n * Add a static `builder()` method for fluent construction.\n *\n * Legacy decorators cannot widen the class type, so by default the\n * caller sees the unmodified shape. Opt into typing by adding a single\n * `declare static` line:\n *\n * ```ts\n * @Builder\n * class UserDto {\n * name!: string\n * email!: string\n * declare static readonly builder: () => BuilderOf<UserDto>\n * }\n * ```\n *\n * `readonly` keeps SonarQube's `typescript:S1444` quiet — the runtime\n * assigns `target.builder` once at decoration time and never again.\n *\n * If you'd rather not write the `declare`, use `withBuilder()` instead —\n * same runtime, types inferred automatically.\n */\nexport function Builder(target: any): void {\n attachBuilder(target)\n}\n\n/**\n * Factory variant of `@Builder`. Wraps the class with the same runtime\n * and returns it intersected with `{ builder(): BuilderOf<T> }` so the\n * static is fully typed without any `declare` boilerplate.\n *\n * ```ts\n * class UserDtoBase {\n * name!: string\n * email!: string\n * }\n * export const UserDto = withBuilder(UserDtoBase)\n * export type UserDto = InstanceType<typeof UserDto>\n *\n * UserDto.builder().name('Alice').email('a@b.com').build() // fully typed\n * ```\n */\nexport function withBuilder<C extends new (...args: any[]) => any>(\n ctor: C,\n): C & { builder(): BuilderOf<InstanceType<C>> } {\n attachBuilder(ctor)\n return ctor as C & { builder(): BuilderOf<InstanceType<C>> }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"decorators.mjs","names":[],"sources":["../../src/core/decorators.ts"],"sourcesContent":["import 'reflect-metadata'\nimport { METADATA, Scope, type BuilderOf, type ServiceOptions } from './interfaces'\nimport { Container, type KickJsRegistry } from './container'\nimport {\n setClassMeta,\n setMethodMeta,\n pushClassMeta,\n pushMethodMeta,\n setInMetaMap,\n setInMetaRecord,\n} from './metadata'\n\n// ── Decorator Registration System ───────────────────────────────────────\n// Decorators execute at class-definition time (module load). The Container\n// may not exist yet when the first decorator fires. We queue registrations\n// and flush them on first Container.getInstance() call.\n//\n// `allRegistrations` is a persistent registry that survives Container.reset().\n// On reset (HMR), all previously decorated classes are re-registered on the\n// fresh container. This ensures @Service, @Controller, @Repository, etc.\n// all survive HMR without manual re-registration.\n\ntype PendingRegistration = { target: any; scope: Scope }\nconst pendingRegistrations: PendingRegistration[] = []\n// Keyed by class name to prevent memory leaks during HMR — new class with\n// the same name replaces the old entry instead of accumulating references.\nconst allRegistrations = new Map<string, { target: any; scope: Scope }>()\nlet containerRef: any = null\n\nfunction flushPending(container: any): void {\n containerRef = container\n for (const { target, scope } of pendingRegistrations) {\n if (!container.has(target)) {\n container.register(target, target, scope)\n }\n }\n pendingRegistrations.length = 0\n}\n\n// Wire up synchronously — Container._onReady is called on first getInstance()\nContainer._onReady = flushPending\n\n// On Container.reset(), update containerRef and replay ALL decorator\n// registrations on the fresh container. This handles HMR where the container\n// is wiped but not all decorated modules are re-evaluated.\nContainer._onReset = (container: any) => {\n containerRef = container\n for (const [, { target, scope }] of allRegistrations) {\n if (!container.has(target)) {\n container.register(target, target, scope)\n }\n }\n}\n\n// ── Class Decorators ────────────────────────────────────────────────────\n\nfunction registerInContainer(target: any, scope: Scope): void {\n setClassMeta(METADATA.INJECTABLE, true, target)\n setClassMeta(METADATA.SCOPE, scope, target)\n\n // Track in persistent registry — survives Container.reset() for HMR replay.\n // Keyed by name so HMR class re-creation replaces the old entry.\n const name = target.name || String(target)\n allRegistrations.set(name, { target, scope })\n\n if (containerRef) {\n // Container already initialized — register immediately\n if (!containerRef.has(target)) {\n containerRef.register(target, target, scope)\n }\n } else {\n // Container not ready yet — queue for later\n pendingRegistrations.push({ target, scope })\n }\n}\n\n/** Mark a class as injectable with lifecycle scope */\nexport function Injectable(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'injectable', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/** Mark a class as a service (semantic alias for Injectable) */\nexport function Service(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'service', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/** Mark a class as a generic managed component */\nexport function Component(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'component', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/** Mark a class as a repository */\nexport function Repository(options?: ServiceOptions): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'repository', target)\n registerInContainer(target, options?.scope ?? Scope.SINGLETON)\n }\n}\n\n/**\n * Mark a class as an HTTP controller and register it in the DI container.\n *\n * Route prefixes are defined by the module's `routes().path` — the single\n * source of truth for where routes mount. The legacy\n * `@Controller('/path')` parameter was removed in v4; if your code\n * still passes a string, see `docs/guide/migration-v3-to-v4.md` for\n * the rename map.\n */\nexport function Controller(): ClassDecorator {\n return (target: any) => {\n setClassMeta(METADATA.CLASS_KIND, 'controller', target)\n registerInContainer(target, Scope.SINGLETON)\n }\n}\n\n// ── Method Decorators ───────────────────────────────────────────────────\n\n/** Mark a method as a lifecycle hook called after instantiation */\nexport function PostConstruct(): MethodDecorator {\n return (target, propertyKey) => {\n setClassMeta(METADATA.POST_CONSTRUCT, propertyKey, target)\n }\n}\n\n// ── Injection Decorators (property + constructor parameter) ─────────────\n\n/**\n * A decorator that's valid as either a property decorator OR a\n * constructor-parameter decorator. forinda/kick-js#235 §2 — `@Autowired`\n * and `@Inject` previously diverged at the type level (property-only\n * vs parameter-only) and adopters who picked the wrong name for the\n * position got a cryptic TS1240 error. Both names now accept either\n * position and route to the correct metadata bucket at runtime.\n */\nexport interface PropertyOrParameterDecorator {\n /** Property decorator form — `@Autowired(TOKEN) private foo!: Foo`. */\n (target: object, propertyKey: string | symbol): void\n /** Constructor-parameter form — `constructor(@Inject(TOKEN) foo: Foo) {}`. */\n (target: object, propertyKey: string | symbol | undefined, parameterIndex: number): void\n}\n\nfunction applyInjection(\n token: unknown,\n target: object,\n propertyKey: string | symbol | undefined,\n parameterIndex: number | undefined,\n): void {\n // Legacy parameter decorators receive 3 args (target, propertyKey,\n // parameterIndex); property decorators receive 2 (target, propertyKey).\n // The 3rd-arg-number-vs-undefined check is the canonical position\n // detection in TypeScript's legacy decorator runtime.\n if (typeof parameterIndex === 'number') {\n setInMetaRecord(METADATA.INJECT, target, parameterIndex, token)\n return\n }\n if (propertyKey != null) {\n // `target` for a property decorator on a non-static field is the\n // class prototype; the AUTOWIRED bucket is keyed off the prototype\n // identity at resolution time, so pass it through unchanged.\n setInMetaMap(METADATA.AUTOWIRED, target, propertyKey as string, token)\n }\n}\n\n/**\n * Inject a dependency. Works in two positions:\n *\n * - **Property decorator** — `@Autowired(TOKEN) private repo!: UserRepo`.\n * Resolved lazily from the container the first time the property is\n * read.\n * - **Constructor parameter decorator** — `constructor(@Autowired(TOKEN) repo: UserRepo) {}`.\n * Resolved at instantiation; injected into the ctor call.\n *\n * `@Inject` is the same function under another name — they share\n * runtime + types so the two names are interchangeable. Pick whichever\n * reads better at the call site.\n *\n * Like `@Inject`, the typed string-literal overload narrows the token\n * to a key of the augmented `KickJsRegistry`. After `kick typegen`\n * populates the registry, `@Autowired('kick/prisma/Client')` auto-\n * completes the key and typo'd literals become TS2345 errors.\n */\nexport function Autowired<K extends keyof KickJsRegistry & string>(\n token: K,\n): PropertyOrParameterDecorator\nexport function Autowired(token?: unknown): PropertyOrParameterDecorator\nexport function Autowired(token?: unknown): PropertyOrParameterDecorator {\n return ((\n target: object,\n propertyKey: string | symbol | undefined,\n parameterIndex?: number,\n ): void => {\n applyInjection(token, target, propertyKey, parameterIndex)\n }) as PropertyOrParameterDecorator\n}\n\n/**\n * Inject a dependency by token. Same shape as {@link Autowired} —\n * works as either a property decorator or a constructor-parameter\n * decorator. Pick whichever name reads better at the call site.\n *\n * ## Typed string-literal overload\n *\n * When called with a string literal that matches a key of the\n * augmented `KickJsRegistry`, TypeScript narrows the parameter type\n * to the registered type. Typo'd literals become compile errors\n * (the string isn't `keyof KickJsRegistry`). Class identities and\n * `InjectionToken<T>` keep working unchanged via the second overload.\n *\n * @example\n * ```ts\n * import { Service, Inject } from '@forinda/kickjs'\n * import type { PrismaClient } from '@prisma/client'\n *\n * `@Service()`\n * class UserRepo {\n * constructor(@Inject('kick/prisma/Client') private db: PrismaClient) {}\n * // OR property form, same decorator:\n * @Inject('kick/prisma/Client') private db2!: PrismaClient\n * }\n * ```\n *\n * After `kick typegen` runs, the literal autocompletes from the\n * registry and a typo (`@Inject('kick/prisma/Cleint')`) becomes a\n * TS2345 error.\n */\nexport function Inject<K extends keyof KickJsRegistry & string>(\n token: K,\n): PropertyOrParameterDecorator\nexport function Inject(token?: unknown): PropertyOrParameterDecorator\nexport function Inject(token?: unknown): PropertyOrParameterDecorator {\n return ((\n target: object,\n propertyKey: string | symbol | undefined,\n parameterIndex?: number,\n ): void => {\n applyInjection(token, target, propertyKey, parameterIndex)\n }) as PropertyOrParameterDecorator\n}\n\n/**\n * Global ambient registry of typed environment variables, populated by\n * `kick typegen` from the project's env schema (typically\n * `src/env.ts`'s `export default defineEnv(...)`).\n *\n * Empty by default — declarations come from `.kickjs/types/env.ts`\n * generated alongside the rest of the typegen output. Once populated,\n * `@Value('PORT')` autocompletes the key, type-checks the default\n * value, and `process.env.PORT` is also typed via the parallel\n * `NodeJS.ProcessEnv` augmentation in the same generated file.\n */\ndeclare global {\n // eslint-disable-next-line @typescript-eslint/no-empty-object-type\n interface KickEnv {}\n}\n\n/** A string key known to the typed env registry (`never` until typegen runs) */\nexport type EnvKey = keyof KickEnv & string\n\n/**\n * Look up the type of a typed env entry.\n *\n * Decorators can't propagate the property type from the key, so users\n * still need to write the property's type — but `Env<K>` lets them\n * derive it from `KickEnv` instead of duplicating the schema:\n *\n * ```ts\n * @Service()\n * class MyService {\n * @Value('PORT')\n * private port!: Env<'PORT'> // → number, narrowed from KickEnv['PORT']\n * }\n * ```\n */\nexport type Env<K extends EnvKey = EnvKey> = KickEnv[K]\n\n/**\n * Constraint that allows any string when the typed env registry is\n * empty (no `kick typegen` has run yet, so existing code with raw\n * string keys keeps working) but locks the argument down to known\n * `EnvKey` literals once the registry is populated.\n *\n * Implementation: when `EnvKey` is `never`, the conditional resolves\n * to the user's `K`, accepting anything. Once typegen has populated\n * `KickEnv`, `EnvKey` becomes a real union and the conditional forces\n * `K` to be assignable to it.\n */\ntype ValueKey<K extends string> = [EnvKey] extends [never] ? K : K & EnvKey\n\n/**\n * Inject an environment variable value. Evaluated lazily so the env\n * is available at access time, not at decoration time.\n *\n * If no default is provided and the env var is missing, throws at access time\n * to catch misconfiguration early instead of returning undefined.\n *\n * Uses metadata + instance getter to work correctly with `useDefineForClassFields`.\n *\n * Type-safety:\n * - Without `kick typegen` run, any string key is accepted (back-compat).\n * - After typegen, `KickEnv` is populated and unknown keys become a tsc\n * error. The optional `defaultValue` is type-checked against the\n * schema's inferred type for that key.\n */\nexport function Value<K extends string>(\n envKey: ValueKey<K>,\n defaultValue?: K extends EnvKey ? KickEnv[K] : unknown,\n): PropertyDecorator {\n return (target, propertyKey) => {\n setInMetaMap(METADATA.VALUE, target, propertyKey as string, { envKey, defaultValue })\n }\n}\n\n// ── @Asset — typed asset path injection (assets-plan.md PR 3+) ─────────\n\nimport type { KickAssets } from './assets'\n\n/**\n * Flatten the nested `KickAssets` augmentation (typegen-emitted in\n * PR 4) into a string-literal union of dot/slash-joined paths.\n *\n * `interface KickAssets { mails: { welcome: () => string; orders: {\n * confirmation: () => string } } }` becomes\n * `'mails/welcome' | 'mails/orders/confirmation'`.\n *\n * Returns `never` when `KickAssets` is empty (no typegen has run yet)\n * so `AssetKeyArg` falls through to accept any string.\n */\ntype FlattenAssets<T, Prefix extends string = ''> = {\n [K in keyof T & string]: T[K] extends () => string\n ? `${Prefix}${K}`\n : T[K] extends Record<string, unknown>\n ? FlattenAssets<T[K], `${Prefix}${K}/`>\n : never\n}[keyof T & string]\n\n/** Flat union of every known `<namespace>/<key>` path. */\nexport type AssetKey = FlattenAssets<KickAssets>\n\n/**\n * Constraint mirroring `ValueKey<K>` — accepts any string when\n * `AssetKey` is `never` (back-compat for adopters who haven't run\n * `kick typegen` yet) but locks the argument to known literals once\n * the typegen-augmented `KickAssets` interface is populated.\n */\ntype AssetKeyArg<K extends string> = [AssetKey] extends [never] ? K : K & AssetKey\n\n/**\n * Inject the resolved file-system path for a typed asset. Mirrors\n * `@Value`'s lazy-getter pattern — the asset is resolved on every\n * property access, NOT at class instantiation, so:\n *\n * - Tests can swap fixtures + clear the cache without re-instantiating\n * the consuming class.\n * - The first access cost is paid once per process (the manifest cache\n * in `core/assets.ts` makes subsequent calls cheap).\n *\n * Type-safety:\n *\n * - Without typegen, any string key is accepted (back-compat — same\n * trick as `@Value`).\n * - After typegen, `KickAssets` is populated and `AssetKey` is a\n * union of every flattened `<namespace>/<key>` path. Unknown keys\n * become a TS error.\n *\n * @example\n * ```ts\n * import { Service, Asset } from '@forinda/kickjs'\n *\n * `@Service()`\n * class MailService {\n * `@Asset('mails/welcome')`\n * private welcomeTemplate!: string\n *\n * send(user: User) {\n * return ejs.renderFile(this.welcomeTemplate, { user })\n * }\n * }\n * ```\n */\nexport function Asset<K extends string>(assetKey: AssetKeyArg<K>): PropertyDecorator {\n return (target, propertyKey) => {\n setInMetaMap(METADATA.ASSET, target, propertyKey as string, { assetKey })\n }\n}\n\n// ── HTTP Route Decorators ───────────────────────────────────────────────\n\nexport interface RouteDefinition {\n method: string\n path: string\n handlerName: string\n validation?: {\n /** JSON Schema object for validating the request body */\n body?: any\n /** JSON Schema object for validating query parameters */\n query?: any\n /** JSON Schema object for validating URL params */\n params?: any\n /** Schema name in OpenAPI components/schemas for the request body. Auto-generated from handler name if omitted. */\n name?: string\n }\n}\n\nfunction createRouteDecorator(method: string) {\n return (path?: string, validation?: RouteDefinition['validation']): MethodDecorator => {\n return (target, propertyKey) => {\n pushClassMeta<RouteDefinition>(METADATA.ROUTES, target.constructor, {\n method,\n path: path || '/',\n handlerName: propertyKey as string,\n validation,\n })\n }\n }\n}\n\nexport const Get = createRouteDecorator('GET')\nexport const Post = createRouteDecorator('POST')\nexport const Put = createRouteDecorator('PUT')\nexport const Delete = createRouteDecorator('DELETE')\nexport const Patch = createRouteDecorator('PATCH')\n\n// ── Query Params Decorator ─────────────────────────────────────────────\n\nexport interface ApiQueryParamsConfig {\n /** Fields that can be used in filter queries (e.g., `?filter=status:eq:active`) */\n filterable?: string[]\n /** Fields that can be used in sort queries (e.g., `?sort=createdAt:desc`) */\n sortable?: string[]\n /** Fields that are searched with the `?q=` parameter */\n searchable?: string[]\n}\n\n/**\n * Column-object-based query params config (e.g., from DrizzleQueryParamsConfig).\n * `Object.keys()` is used to derive field names for OpenAPI docs.\n */\nexport interface ColumnApiQueryParamsConfig {\n columns: Record<string, any>\n sortable?: Record<string, any>\n searchColumns?: any[]\n [key: string]: any\n}\n\n/**\n * Normalize a query params config to the string-based ApiQueryParamsConfig.\n * Handles both string-based and column-object-based configs.\n */\nexport function normalizeApiQueryParamsConfig(\n config: ApiQueryParamsConfig | ColumnApiQueryParamsConfig,\n): ApiQueryParamsConfig {\n if ('columns' in config && config.columns && typeof config.columns === 'object') {\n return {\n filterable: Object.keys(config.columns),\n sortable: config.sortable ? Object.keys(config.sortable) : undefined,\n searchable: config.searchColumns\n ? config.searchColumns.map((col: any) => col?.name ?? '').filter(Boolean)\n : undefined,\n }\n }\n return config as ApiQueryParamsConfig\n}\n\n/**\n * Document the query parameters accepted by a GET endpoint.\n * Used by SwaggerAdapter to generate `filter`, `sort`, `page`, `limit`, and `q` params\n * in the OpenAPI spec, with descriptions listing the allowed fields.\n *\n * Accepts both string-based configs and column-object configs (e.g., DrizzleQueryParamsConfig).\n *\n * @example\n * ```ts\n * // String-based\n * @ApiQueryParams({\n * filterable: ['status', 'category', 'price'],\n * sortable: ['name', 'createdAt', 'price'],\n * searchable: ['name', 'description'],\n * })\n *\n * // Column-object-based (Drizzle)\n * @ApiQueryParams(TASK_QUERY_CONFIG)\n * ```\n */\nexport function ApiQueryParams(\n config: ApiQueryParamsConfig | ColumnApiQueryParamsConfig,\n): MethodDecorator {\n return (target, propertyKey) => {\n const normalized = normalizeApiQueryParamsConfig(config)\n setMethodMeta(METADATA.QUERY_PARAMS, normalized, target.constructor, propertyKey as string)\n }\n}\n\n// ── Middleware Decorators ───────────────────────────────────────────────\n\n/**\n * Middleware handler function.\n * Generic `TCtx` defaults to `any` — import `RequestContext` from\n * `@forinda/kickjs` for full type safety:\n *\n * ```ts\n * import type { MiddlewareHandler } from '@forinda/kickjs'\n * import type { RequestContext } from '@forinda/kickjs'\n *\n * const auth: MiddlewareHandler<RequestContext> = (ctx, next) => {\n * ctx.body // fully typed\n * next()\n * }\n * ```\n */\nexport type MiddlewareHandler<TCtx = any> = (ctx: TCtx, next: () => void) => void | Promise<void>\n\n/** Attach middleware handlers to a class or method */\nexport function Middleware(...handlers: MiddlewareHandler[]): ClassDecorator & MethodDecorator {\n return (target: any, propertyKey?: string | symbol) => {\n if (propertyKey) {\n pushMethodMeta(\n METADATA.METHOD_MIDDLEWARES,\n target.constructor,\n propertyKey as string,\n ...handlers,\n )\n } else {\n pushClassMeta(METADATA.CLASS_MIDDLEWARES, target, ...handlers)\n }\n }\n}\n\n// ── File Upload Decorator ───────────────────────────────────────────────\n\n/**\n * File filter — accepts short extensions ('jpg'), full MIME types ('image/jpeg'),\n * wildcards ('image/*'), or a function `(mimetype, filename) => boolean` for full control.\n */\nexport type FileTypeFilter = string[] | ((mimetype: string, filename: string) => boolean)\n\n/**\n * Shared upload options used by both the `@FileUpload` decorator and the\n * `upload.single()` / `upload.array()` / `upload.none()` middleware.\n */\nexport interface BaseUploadOptions {\n /** Max file size in bytes (default: 5MB) */\n maxSize?: number\n /**\n * Allowed file types:\n * - **string[]** — short extensions ('jpg'), full MIME types ('image/jpeg'), or wildcards ('image/*')\n * - **function** — `(mimetype, filename) => boolean` for full control\n */\n allowedTypes?: FileTypeFilter\n /** Extend the built-in extension-to-MIME map */\n customMimeMap?: Record<string, string>\n}\n\nexport interface FileUploadConfig extends BaseUploadOptions {\n mode: 'single' | 'array' | 'none'\n /** Form field name (default: 'file') */\n fieldName?: string\n /** Max files for array mode (default: 10) */\n maxCount?: number\n}\n\n/** Configure file upload handling for a controller method */\nexport function FileUpload(config: FileUploadConfig): MethodDecorator {\n return (target, propertyKey) => {\n setMethodMeta(METADATA.FILE_UPLOAD, config, target.constructor, propertyKey as string)\n }\n}\n\n// ── Builder Decorator ───────────────────────────────────────────────────\n\nfunction attachBuilder(target: any): void {\n setClassMeta(METADATA.BUILDER, true, target)\n target.builder = function () {\n const props: Record<string, any> = {}\n const proxy: any = new Proxy(\n {},\n {\n get(_, key) {\n if (key === 'build') {\n return () => Object.assign(new target(), props)\n }\n return (value: any) => {\n props[key as string] = value\n return proxy\n }\n },\n },\n )\n return proxy\n }\n}\n\n/**\n * Add a static `builder()` method for fluent construction.\n *\n * Legacy decorators cannot widen the class type, so by default the\n * caller sees the unmodified shape. Opt into typing by adding a single\n * `declare static` line:\n *\n * ```ts\n * @Builder\n * class UserDto {\n * name!: string\n * email!: string\n * declare static readonly builder: () => BuilderOf<UserDto>\n * }\n * ```\n *\n * `readonly` keeps SonarQube's `typescript:S1444` quiet — the runtime\n * assigns `target.builder` once at decoration time and never again.\n *\n * If you'd rather not write the `declare`, use `withBuilder()` instead —\n * same runtime, types inferred automatically.\n */\nexport function Builder(target: any): void {\n attachBuilder(target)\n}\n\n/**\n * Factory variant of `@Builder`. Wraps the class with the same runtime\n * and returns it intersected with `{ builder(): BuilderOf<T> }` so the\n * static is fully typed without any `declare` boilerplate.\n *\n * ```ts\n * class UserDtoBase {\n * name!: string\n * email!: string\n * }\n * export const UserDto = withBuilder(UserDtoBase)\n * export type UserDto = InstanceType<typeof UserDto>\n *\n * UserDto.builder().name('Alice').email('a@b.com').build() // fully typed\n * ```\n */\nexport function withBuilder<C extends new (...args: any[]) => any>(\n ctor: C,\n): C & { builder(): BuilderOf<InstanceType<C>> } {\n attachBuilder(ctor)\n return ctor as C & { builder(): BuilderOf<InstanceType<C>> }\n}\n"],"mappings":"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"}
|
package/dist/core/errors.mjs
CHANGED
package/dist/core/errors.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"errors.mjs","names":[],"sources":["../../src/core/errors.ts"],"sourcesContent":["/**\n * Standard HTTP status codes as a const enum.\n * Use instead of raw numbers for readability and type safety.\n *\n * @example\n * ```ts\n * throw new HttpException(HttpStatus.NOT_FOUND, 'User not found')\n * ctx.res.status(HttpStatus.CREATED).json(data)\n * ```\n */\nexport const HttpStatus = {\n // 2xx Success\n OK: 200,\n CREATED: 201,\n ACCEPTED: 202,\n NO_CONTENT: 204,\n\n // 3xx Redirection\n MOVED_PERMANENTLY: 301,\n FOUND: 302,\n NOT_MODIFIED: 304,\n TEMPORARY_REDIRECT: 307,\n PERMANENT_REDIRECT: 308,\n\n // 4xx Client Errors\n BAD_REQUEST: 400,\n UNAUTHORIZED: 401,\n PAYMENT_REQUIRED: 402,\n FORBIDDEN: 403,\n NOT_FOUND: 404,\n METHOD_NOT_ALLOWED: 405,\n NOT_ACCEPTABLE: 406,\n CONFLICT: 409,\n GONE: 410,\n UNPROCESSABLE_ENTITY: 422,\n TOO_MANY_REQUESTS: 429,\n\n // 5xx Server Errors\n INTERNAL_SERVER_ERROR: 500,\n NOT_IMPLEMENTED: 501,\n BAD_GATEWAY: 502,\n SERVICE_UNAVAILABLE: 503,\n GATEWAY_TIMEOUT: 504,\n} as const\n\nexport type HttpStatusCode = (typeof HttpStatus)[keyof typeof HttpStatus]\n\n/**\n * IANA HTTP status reason phrases for the codes the framework's typed\n * helpers cover. Used as the default `title` when a {@link ProblemException}\n * is constructed without one. Keep in sync with {@link HttpStatus}.\n */\nconst STATUS_REASONS: Record<number, string> = {\n 400: 'Bad Request',\n 401: 'Unauthorized',\n 402: 'Payment Required',\n 403: 'Forbidden',\n 404: 'Not Found',\n 405: 'Method Not Allowed',\n 406: 'Not Acceptable',\n 409: 'Conflict',\n 410: 'Gone',\n 422: 'Unprocessable Entity',\n 429: 'Too Many Requests',\n 500: 'Internal Server Error',\n 501: 'Not Implemented',\n 502: 'Bad Gateway',\n 503: 'Service Unavailable',\n 504: 'Gateway Timeout',\n}\n\n/** Validation error detail — shape used by `unprocessable()` and `fromZodError()`. */\nexport interface ValidationError {\n field: string\n message: string\n code?: string\n}\n\n/**\n * RFC 9457 — Problem Details for HTTP APIs.\n * https://datatracker.ietf.org/doc/html/rfc9457\n *\n * Canonical error shape for HTTP responses. Only `status` is required;\n * everything else has a sensible default. Extension members (any other\n * JSON-compatible field) are allowed per §3.2 — pass them on the same\n * object and the framework forwards them through.\n *\n * @example\n * ```ts\n * const problem: ProblemDetails = {\n * type: 'https://api.example.com/problems/out-of-credit',\n * status: 403,\n * title: 'You do not have enough credit',\n * detail: 'Your balance is 30, but that costs 50.',\n * instance: '/account/12345/messages/abc',\n * // extension members per §3.2\n * balance: 30,\n * }\n * ```\n */\nexport interface ProblemDetails {\n /**\n * URI reference identifying the problem type. Per §4.2.1, when\n * dereferenced it SHOULD return human-readable documentation. Defaults\n * to `about:blank` when omitted (RFC 9457 §3.1.1).\n */\n type?: string\n /**\n * Short human-readable summary, advisable to be the same regardless of\n * the occurrence (§3.1.4). Defaults to the IANA reason phrase for\n * `status` when omitted.\n */\n title?: string\n /** HTTP status code generated by the origin server. Required. */\n status: number\n /**\n * Human-readable explanation specific to this occurrence (§3.1.5).\n * Per the RFC's safety guidance, do not include sensitive information\n * (tokens, full SQL, stack traces) — those belong in logs.\n */\n detail?: string\n /**\n * URI reference identifying the specific occurrence (§3.1.6).\n * Typically the request path. Adopters set this explicitly — the\n * framework does not auto-populate it to avoid leaking unexpected\n * structure.\n */\n instance?: string\n /**\n * Extension members per §3.2 — any other JSON-compatible field.\n * Common examples: `balance`, `errors`, `traceId`, `accounts`.\n */\n [key: string]: unknown\n}\n\n/**\n * Typed HTTP exception with status code, free-form details, and optional\n * response headers. Provides static factory methods for common HTTP errors.\n *\n * `details` is intentionally typed as `unknown` — it serializes straight into\n * the response body's `errors` field, so any JSON-compatible shape works\n * (string, object, array of mixed values, ValidationError[]).\n *\n * `headers` are merged into the response by the global error handler so\n * spec-mandated headers (Retry-After, WWW-Authenticate, Allow, Location)\n * stay on the exception, not on ad-hoc try/catch chains.\n *\n * For RFC 9457 — Problem Details responses, throw {@link ProblemException}\n * instead — the global error handler emits `application/problem+json` for\n * those automatically.\n */\nexport class HttpException extends Error {\n constructor(\n public readonly status: number,\n message: string,\n public readonly details?: unknown,\n public readonly headers?: Record<string, string>,\n ) {\n super(message)\n this.name = 'HttpException'\n }\n\n /** Return a new exception with `headers` shallow-merged onto this one. */\n withHeaders(headers: Record<string, string>): HttpException {\n return new HttpException(this.status, this.message, this.details, {\n ...this.headers,\n ...headers,\n })\n }\n\n /** Create from a Zod error */\n static fromZodError(error: any, message?: string): HttpException {\n const firstIssue = error.issues?.[0]\n const details: ValidationError[] = (error.issues || []).map((issue: any) => ({\n field: issue.path?.join('.') || '',\n message: issue.message,\n code: issue.code,\n }))\n return new HttpException(\n HttpStatus.UNPROCESSABLE_ENTITY,\n message || firstIssue?.message || 'Validation failed',\n details,\n )\n }\n\n static badRequest(message = 'Bad Request') {\n return new HttpException(HttpStatus.BAD_REQUEST, message)\n }\n static unauthorized(message = 'Unauthorized', wwwAuthenticate?: string) {\n return new HttpException(\n HttpStatus.UNAUTHORIZED,\n message,\n undefined,\n wwwAuthenticate ? { 'WWW-Authenticate': wwwAuthenticate } : undefined,\n )\n }\n static forbidden(message = 'Forbidden') {\n return new HttpException(HttpStatus.FORBIDDEN, message)\n }\n static notFound(message = 'Not Found') {\n return new HttpException(HttpStatus.NOT_FOUND, message)\n }\n static methodNotAllowed(allowedMethods: string[], message = 'Method Not Allowed') {\n return new HttpException(HttpStatus.METHOD_NOT_ALLOWED, message, undefined, {\n Allow: allowedMethods.join(', '),\n })\n }\n static conflict(message = 'Conflict') {\n return new HttpException(HttpStatus.CONFLICT, message)\n }\n static unprocessable(message = 'Unprocessable Entity', details?: ValidationError[]) {\n return new HttpException(HttpStatus.UNPROCESSABLE_ENTITY, message, details)\n }\n static tooManyRequests(message = 'Too Many Requests', retryAfterSeconds?: number) {\n return new HttpException(\n HttpStatus.TOO_MANY_REQUESTS,\n message,\n undefined,\n retryAfterSeconds !== undefined ? { 'Retry-After': String(retryAfterSeconds) } : undefined,\n )\n }\n static serviceUnavailable(message = 'Service Unavailable', retryAfterSeconds?: number) {\n return new HttpException(\n HttpStatus.SERVICE_UNAVAILABLE,\n message,\n undefined,\n retryAfterSeconds !== undefined ? { 'Retry-After': String(retryAfterSeconds) } : undefined,\n )\n }\n static internal(message = 'Internal Server Error') {\n return new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, message)\n }\n}\n\n/**\n * Default `title` for an RFC 9457 problem with the given status — the IANA\n * reason phrase, or `'Error'` for codes outside the framework's table.\n * Exposed so {@link ProblemException} construction and {@link RequestContext}'s\n * `problem` helpers share a single source of truth.\n */\nexport function defaultProblemTitle(status: number): string {\n return STATUS_REASONS[status] ?? 'Error'\n}\n\n/**\n * Exception that carries an RFC 9457 — Problem Details payload. When\n * thrown, the framework error handler emits `application/problem+json`\n * with the canonical shape; `type` defaults to `about:blank`, `title`\n * to the IANA reason phrase for `status`.\n *\n * Extends {@link HttpException} so existing `instanceof HttpException`\n * catches still see it. Use `new ProblemException(...)` directly for\n * full control, or {@link Problems} for the pre-filled-status\n * convenience factories.\n *\n * Static factories deliberately live on {@link Problems} rather than\n * this class — putting them here would shadow `HttpException`'s\n * same-named statics with incompatible (object vs string) signatures,\n * which TypeScript correctly rejects.\n *\n * @example\n * ```ts\n * throw new ProblemException({\n * type: 'https://api.example.com/problems/out-of-credit',\n * status: 403,\n * title: 'You do not have enough credit',\n * detail: 'Your balance is 30, but that costs 50.',\n * instance: '/account/12345/messages/abc',\n * balance: 30, // extension\n * })\n *\n * // Or via the Problems convenience factory:\n * throw Problems.forbidden({\n * type: 'https://api.example.com/problems/out-of-credit',\n * detail: 'Your balance is 30, but that costs 50.',\n * balance: 30,\n * })\n * ```\n */\nexport class ProblemException extends HttpException {\n public readonly problem: ProblemDetails\n\n constructor(problem: ProblemDetails, headers?: Record<string, string>) {\n const title = problem.title ?? defaultProblemTitle(problem.status)\n const detail = problem.detail ?? title\n super(problem.status, detail, undefined, headers)\n this.name = 'ProblemException'\n this.problem = { ...problem, title }\n }\n\n /** Return a new exception with `headers` shallow-merged onto this one. */\n withHeaders(headers: Record<string, string>): ProblemException {\n return new ProblemException(this.problem, { ...this.headers, ...headers })\n }\n}\n\n/**\n * Convenience factories for {@link ProblemException}. Pre-fill `status`\n * (and `title` via {@link defaultProblemTitle}) for the common HTTP\n * codes — same shortcut set the `ctx.problem.*` response helpers cover.\n *\n * Lives in a separate namespace rather than as statics on\n * {@link ProblemException} to avoid shadowing {@link HttpException}'s\n * same-named statics, which would create a TypeScript variance\n * conflict (parent's `notFound(message?: string)` vs the child's\n * desired `notFound(input?: ProblemInput)`).\n *\n * @example\n * ```ts\n * throw Problems.notFound({ detail: 'User abc not found' })\n * throw Problems.tooManyRequests({}, 60) // sets Retry-After: 60\n * throw Problems.fromZodError(zodResult.error)\n * ```\n */\nexport const Problems = {\n /** 400 Bad Request with optional detail / instance / extensions. */\n badRequest(input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n return new ProblemException({ status: 400, ...input })\n },\n /** 401 Unauthorized. Pass a `WWW-Authenticate` challenge via the headers arg. */\n unauthorized(\n input: Omit<ProblemDetails, 'status'> = {},\n wwwAuthenticate?: string,\n ): ProblemException {\n return new ProblemException(\n { status: 401, ...input },\n wwwAuthenticate ? { 'WWW-Authenticate': wwwAuthenticate } : undefined,\n )\n },\n /** 403 Forbidden. */\n forbidden(input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n return new ProblemException({ status: 403, ...input })\n },\n /** 404 Not Found. */\n notFound(input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n return new ProblemException({ status: 404, ...input })\n },\n /** 405 Method Not Allowed. Pass the allowed methods so the Allow header is set. */\n methodNotAllowed(\n allowedMethods: string[],\n input: Omit<ProblemDetails, 'status'> = {},\n ): ProblemException {\n return new ProblemException({ status: 405, ...input }, { Allow: allowedMethods.join(', ') })\n },\n /** 409 Conflict. */\n conflict(input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n return new ProblemException({ status: 409, ...input })\n },\n /**\n * 422 Unprocessable Entity. Pass per-field issues via the `errors`\n * extension — keep the array shape per the RFC's recommended convention\n * (`{ field, message, code }`).\n */\n unprocessable(input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n return new ProblemException({ status: 422, ...input })\n },\n /** 429 Too Many Requests. Pass `retryAfterSeconds` to set the Retry-After header. */\n tooManyRequests(\n input: Omit<ProblemDetails, 'status'> = {},\n retryAfterSeconds?: number,\n ): ProblemException {\n return new ProblemException(\n { status: 429, ...input },\n retryAfterSeconds !== undefined ? { 'Retry-After': String(retryAfterSeconds) } : undefined,\n )\n },\n /** 503 Service Unavailable. */\n serviceUnavailable(\n input: Omit<ProblemDetails, 'status'> = {},\n retryAfterSeconds?: number,\n ): ProblemException {\n return new ProblemException(\n { status: 503, ...input },\n retryAfterSeconds !== undefined ? { 'Retry-After': String(retryAfterSeconds) } : undefined,\n )\n },\n /** 500 Internal Server Error. */\n internal(input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n return new ProblemException({ status: 500, ...input })\n },\n\n /**\n * Build from a Zod error — 422 with the issues serialized into the\n * `errors` extension per RFC 9457 §3.2's array convention.\n */\n fromZodError(error: any, input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n const issues: ValidationError[] = (error.issues || []).map((issue: any) => ({\n field: issue.path?.join('.') || '',\n message: issue.message,\n code: issue.code,\n }))\n return new ProblemException({\n status: 422,\n detail: error.issues?.[0]?.message,\n ...input,\n errors: issues,\n })\n },\n} as const\n\n/**\n * Normalize an RFC 9457 problem into the wire shape — fill in `type` →\n * `'about:blank'` and `title` → IANA reason phrase. The same function is\n * used by {@link RequestContext}'s `problem` helpers and the global\n * error handler so the response is identical regardless of entry point.\n *\n * Defaults are applied **after** the spread, so a caller passing\n * `{ type: undefined }` (e.g. a partial built up from optional fields)\n * still gets `'about:blank'` on the wire — `...input` would otherwise\n * re-overwrite the fallback with the explicit `undefined`.\n */\nexport function normalizeProblem(input: ProblemDetails): ProblemDetails {\n return {\n ...input,\n type: input.type ?? 'about:blank',\n title: input.title ?? defaultProblemTitle(input.status),\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"errors.mjs","names":[],"sources":["../../src/core/errors.ts"],"sourcesContent":["/**\n * Standard HTTP status codes as a const enum.\n * Use instead of raw numbers for readability and type safety.\n *\n * @example\n * ```ts\n * throw new HttpException(HttpStatus.NOT_FOUND, 'User not found')\n * ctx.res.status(HttpStatus.CREATED).json(data)\n * ```\n */\nexport const HttpStatus = {\n // 2xx Success\n OK: 200,\n CREATED: 201,\n ACCEPTED: 202,\n NO_CONTENT: 204,\n\n // 3xx Redirection\n MOVED_PERMANENTLY: 301,\n FOUND: 302,\n NOT_MODIFIED: 304,\n TEMPORARY_REDIRECT: 307,\n PERMANENT_REDIRECT: 308,\n\n // 4xx Client Errors\n BAD_REQUEST: 400,\n UNAUTHORIZED: 401,\n PAYMENT_REQUIRED: 402,\n FORBIDDEN: 403,\n NOT_FOUND: 404,\n METHOD_NOT_ALLOWED: 405,\n NOT_ACCEPTABLE: 406,\n CONFLICT: 409,\n GONE: 410,\n UNPROCESSABLE_ENTITY: 422,\n TOO_MANY_REQUESTS: 429,\n\n // 5xx Server Errors\n INTERNAL_SERVER_ERROR: 500,\n NOT_IMPLEMENTED: 501,\n BAD_GATEWAY: 502,\n SERVICE_UNAVAILABLE: 503,\n GATEWAY_TIMEOUT: 504,\n} as const\n\nexport type HttpStatusCode = (typeof HttpStatus)[keyof typeof HttpStatus]\n\n/**\n * IANA HTTP status reason phrases for the codes the framework's typed\n * helpers cover. Used as the default `title` when a {@link ProblemException}\n * is constructed without one. Keep in sync with {@link HttpStatus}.\n */\nconst STATUS_REASONS: Record<number, string> = {\n 400: 'Bad Request',\n 401: 'Unauthorized',\n 402: 'Payment Required',\n 403: 'Forbidden',\n 404: 'Not Found',\n 405: 'Method Not Allowed',\n 406: 'Not Acceptable',\n 409: 'Conflict',\n 410: 'Gone',\n 422: 'Unprocessable Entity',\n 429: 'Too Many Requests',\n 500: 'Internal Server Error',\n 501: 'Not Implemented',\n 502: 'Bad Gateway',\n 503: 'Service Unavailable',\n 504: 'Gateway Timeout',\n}\n\n/** Validation error detail — shape used by `unprocessable()` and `fromZodError()`. */\nexport interface ValidationError {\n field: string\n message: string\n code?: string\n}\n\n/**\n * RFC 9457 — Problem Details for HTTP APIs.\n * https://datatracker.ietf.org/doc/html/rfc9457\n *\n * Canonical error shape for HTTP responses. Only `status` is required;\n * everything else has a sensible default. Extension members (any other\n * JSON-compatible field) are allowed per §3.2 — pass them on the same\n * object and the framework forwards them through.\n *\n * @example\n * ```ts\n * const problem: ProblemDetails = {\n * type: 'https://api.example.com/problems/out-of-credit',\n * status: 403,\n * title: 'You do not have enough credit',\n * detail: 'Your balance is 30, but that costs 50.',\n * instance: '/account/12345/messages/abc',\n * // extension members per §3.2\n * balance: 30,\n * }\n * ```\n */\nexport interface ProblemDetails {\n /**\n * URI reference identifying the problem type. Per §4.2.1, when\n * dereferenced it SHOULD return human-readable documentation. Defaults\n * to `about:blank` when omitted (RFC 9457 §3.1.1).\n */\n type?: string\n /**\n * Short human-readable summary, advisable to be the same regardless of\n * the occurrence (§3.1.4). Defaults to the IANA reason phrase for\n * `status` when omitted.\n */\n title?: string\n /** HTTP status code generated by the origin server. Required. */\n status: number\n /**\n * Human-readable explanation specific to this occurrence (§3.1.5).\n * Per the RFC's safety guidance, do not include sensitive information\n * (tokens, full SQL, stack traces) — those belong in logs.\n */\n detail?: string\n /**\n * URI reference identifying the specific occurrence (§3.1.6).\n * Typically the request path. Adopters set this explicitly — the\n * framework does not auto-populate it to avoid leaking unexpected\n * structure.\n */\n instance?: string\n /**\n * Extension members per §3.2 — any other JSON-compatible field.\n * Common examples: `balance`, `errors`, `traceId`, `accounts`.\n */\n [key: string]: unknown\n}\n\n/**\n * Typed HTTP exception with status code, free-form details, and optional\n * response headers. Provides static factory methods for common HTTP errors.\n *\n * `details` is intentionally typed as `unknown` — it serializes straight into\n * the response body's `errors` field, so any JSON-compatible shape works\n * (string, object, array of mixed values, ValidationError[]).\n *\n * `headers` are merged into the response by the global error handler so\n * spec-mandated headers (Retry-After, WWW-Authenticate, Allow, Location)\n * stay on the exception, not on ad-hoc try/catch chains.\n *\n * For RFC 9457 — Problem Details responses, throw {@link ProblemException}\n * instead — the global error handler emits `application/problem+json` for\n * those automatically.\n */\nexport class HttpException extends Error {\n constructor(\n public readonly status: number,\n message: string,\n public readonly details?: unknown,\n public readonly headers?: Record<string, string>,\n ) {\n super(message)\n this.name = 'HttpException'\n }\n\n /** Return a new exception with `headers` shallow-merged onto this one. */\n withHeaders(headers: Record<string, string>): HttpException {\n return new HttpException(this.status, this.message, this.details, {\n ...this.headers,\n ...headers,\n })\n }\n\n /** Create from a Zod error */\n static fromZodError(error: any, message?: string): HttpException {\n const firstIssue = error.issues?.[0]\n const details: ValidationError[] = (error.issues || []).map((issue: any) => ({\n field: issue.path?.join('.') || '',\n message: issue.message,\n code: issue.code,\n }))\n return new HttpException(\n HttpStatus.UNPROCESSABLE_ENTITY,\n message || firstIssue?.message || 'Validation failed',\n details,\n )\n }\n\n static badRequest(message = 'Bad Request') {\n return new HttpException(HttpStatus.BAD_REQUEST, message)\n }\n static unauthorized(message = 'Unauthorized', wwwAuthenticate?: string) {\n return new HttpException(\n HttpStatus.UNAUTHORIZED,\n message,\n undefined,\n wwwAuthenticate ? { 'WWW-Authenticate': wwwAuthenticate } : undefined,\n )\n }\n static forbidden(message = 'Forbidden') {\n return new HttpException(HttpStatus.FORBIDDEN, message)\n }\n static notFound(message = 'Not Found') {\n return new HttpException(HttpStatus.NOT_FOUND, message)\n }\n static methodNotAllowed(allowedMethods: string[], message = 'Method Not Allowed') {\n return new HttpException(HttpStatus.METHOD_NOT_ALLOWED, message, undefined, {\n Allow: allowedMethods.join(', '),\n })\n }\n static conflict(message = 'Conflict') {\n return new HttpException(HttpStatus.CONFLICT, message)\n }\n static unprocessable(message = 'Unprocessable Entity', details?: ValidationError[]) {\n return new HttpException(HttpStatus.UNPROCESSABLE_ENTITY, message, details)\n }\n static tooManyRequests(message = 'Too Many Requests', retryAfterSeconds?: number) {\n return new HttpException(\n HttpStatus.TOO_MANY_REQUESTS,\n message,\n undefined,\n retryAfterSeconds !== undefined ? { 'Retry-After': String(retryAfterSeconds) } : undefined,\n )\n }\n static serviceUnavailable(message = 'Service Unavailable', retryAfterSeconds?: number) {\n return new HttpException(\n HttpStatus.SERVICE_UNAVAILABLE,\n message,\n undefined,\n retryAfterSeconds !== undefined ? { 'Retry-After': String(retryAfterSeconds) } : undefined,\n )\n }\n static internal(message = 'Internal Server Error') {\n return new HttpException(HttpStatus.INTERNAL_SERVER_ERROR, message)\n }\n}\n\n/**\n * Default `title` for an RFC 9457 problem with the given status — the IANA\n * reason phrase, or `'Error'` for codes outside the framework's table.\n * Exposed so {@link ProblemException} construction and {@link RequestContext}'s\n * `problem` helpers share a single source of truth.\n */\nexport function defaultProblemTitle(status: number): string {\n return STATUS_REASONS[status] ?? 'Error'\n}\n\n/**\n * Exception that carries an RFC 9457 — Problem Details payload. When\n * thrown, the framework error handler emits `application/problem+json`\n * with the canonical shape; `type` defaults to `about:blank`, `title`\n * to the IANA reason phrase for `status`.\n *\n * Extends {@link HttpException} so existing `instanceof HttpException`\n * catches still see it. Use `new ProblemException(...)` directly for\n * full control, or {@link Problems} for the pre-filled-status\n * convenience factories.\n *\n * Static factories deliberately live on {@link Problems} rather than\n * this class — putting them here would shadow `HttpException`'s\n * same-named statics with incompatible (object vs string) signatures,\n * which TypeScript correctly rejects.\n *\n * @example\n * ```ts\n * throw new ProblemException({\n * type: 'https://api.example.com/problems/out-of-credit',\n * status: 403,\n * title: 'You do not have enough credit',\n * detail: 'Your balance is 30, but that costs 50.',\n * instance: '/account/12345/messages/abc',\n * balance: 30, // extension\n * })\n *\n * // Or via the Problems convenience factory:\n * throw Problems.forbidden({\n * type: 'https://api.example.com/problems/out-of-credit',\n * detail: 'Your balance is 30, but that costs 50.',\n * balance: 30,\n * })\n * ```\n */\nexport class ProblemException extends HttpException {\n public readonly problem: ProblemDetails\n\n constructor(problem: ProblemDetails, headers?: Record<string, string>) {\n const title = problem.title ?? defaultProblemTitle(problem.status)\n const detail = problem.detail ?? title\n super(problem.status, detail, undefined, headers)\n this.name = 'ProblemException'\n this.problem = { ...problem, title }\n }\n\n /** Return a new exception with `headers` shallow-merged onto this one. */\n withHeaders(headers: Record<string, string>): ProblemException {\n return new ProblemException(this.problem, { ...this.headers, ...headers })\n }\n}\n\n/**\n * Convenience factories for {@link ProblemException}. Pre-fill `status`\n * (and `title` via {@link defaultProblemTitle}) for the common HTTP\n * codes — same shortcut set the `ctx.problem.*` response helpers cover.\n *\n * Lives in a separate namespace rather than as statics on\n * {@link ProblemException} to avoid shadowing {@link HttpException}'s\n * same-named statics, which would create a TypeScript variance\n * conflict (parent's `notFound(message?: string)` vs the child's\n * desired `notFound(input?: ProblemInput)`).\n *\n * @example\n * ```ts\n * throw Problems.notFound({ detail: 'User abc not found' })\n * throw Problems.tooManyRequests({}, 60) // sets Retry-After: 60\n * throw Problems.fromZodError(zodResult.error)\n * ```\n */\nexport const Problems = {\n /** 400 Bad Request with optional detail / instance / extensions. */\n badRequest(input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n return new ProblemException({ status: 400, ...input })\n },\n /** 401 Unauthorized. Pass a `WWW-Authenticate` challenge via the headers arg. */\n unauthorized(\n input: Omit<ProblemDetails, 'status'> = {},\n wwwAuthenticate?: string,\n ): ProblemException {\n return new ProblemException(\n { status: 401, ...input },\n wwwAuthenticate ? { 'WWW-Authenticate': wwwAuthenticate } : undefined,\n )\n },\n /** 403 Forbidden. */\n forbidden(input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n return new ProblemException({ status: 403, ...input })\n },\n /** 404 Not Found. */\n notFound(input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n return new ProblemException({ status: 404, ...input })\n },\n /** 405 Method Not Allowed. Pass the allowed methods so the Allow header is set. */\n methodNotAllowed(\n allowedMethods: string[],\n input: Omit<ProblemDetails, 'status'> = {},\n ): ProblemException {\n return new ProblemException({ status: 405, ...input }, { Allow: allowedMethods.join(', ') })\n },\n /** 409 Conflict. */\n conflict(input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n return new ProblemException({ status: 409, ...input })\n },\n /**\n * 422 Unprocessable Entity. Pass per-field issues via the `errors`\n * extension — keep the array shape per the RFC's recommended convention\n * (`{ field, message, code }`).\n */\n unprocessable(input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n return new ProblemException({ status: 422, ...input })\n },\n /** 429 Too Many Requests. Pass `retryAfterSeconds` to set the Retry-After header. */\n tooManyRequests(\n input: Omit<ProblemDetails, 'status'> = {},\n retryAfterSeconds?: number,\n ): ProblemException {\n return new ProblemException(\n { status: 429, ...input },\n retryAfterSeconds !== undefined ? { 'Retry-After': String(retryAfterSeconds) } : undefined,\n )\n },\n /** 503 Service Unavailable. */\n serviceUnavailable(\n input: Omit<ProblemDetails, 'status'> = {},\n retryAfterSeconds?: number,\n ): ProblemException {\n return new ProblemException(\n { status: 503, ...input },\n retryAfterSeconds !== undefined ? { 'Retry-After': String(retryAfterSeconds) } : undefined,\n )\n },\n /** 500 Internal Server Error. */\n internal(input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n return new ProblemException({ status: 500, ...input })\n },\n\n /**\n * Build from a Zod error — 422 with the issues serialized into the\n * `errors` extension per RFC 9457 §3.2's array convention.\n */\n fromZodError(error: any, input: Omit<ProblemDetails, 'status'> = {}): ProblemException {\n const issues: ValidationError[] = (error.issues || []).map((issue: any) => ({\n field: issue.path?.join('.') || '',\n message: issue.message,\n code: issue.code,\n }))\n return new ProblemException({\n status: 422,\n detail: error.issues?.[0]?.message,\n ...input,\n errors: issues,\n })\n },\n} as const\n\n/**\n * Normalize an RFC 9457 problem into the wire shape — fill in `type` →\n * `'about:blank'` and `title` → IANA reason phrase. The same function is\n * used by {@link RequestContext}'s `problem` helpers and the global\n * error handler so the response is identical regardless of entry point.\n *\n * Defaults are applied **after** the spread, so a caller passing\n * `{ type: undefined }` (e.g. a partial built up from optional fields)\n * still gets `'about:blank'` on the wire — `...input` would otherwise\n * re-overwrite the fallback with the explicit `undefined`.\n */\nexport function normalizeProblem(input: ProblemDetails): ProblemDetails {\n return {\n ...input,\n type: input.type ?? 'about:blank',\n title: input.title ?? defaultProblemTitle(input.status),\n }\n}\n"],"mappings":"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"}
|
package/dist/core/interfaces.mjs
CHANGED
package/dist/core/logger.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
|
|
2
|
-
import { i as createLogger, n as Logger, r as LoggerProvider, t as ConsoleLoggerProvider } from "../logger-
|
|
2
|
+
import { i as createLogger, n as Logger, r as LoggerProvider, t as ConsoleLoggerProvider } from "../logger-COlEkn_C.mjs";
|
|
3
3
|
export { ConsoleLoggerProvider, Logger, LoggerProvider, createLogger };
|
package/dist/core/logger.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.16.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
var ConsoleLoggerProvider=class ConsoleLoggerProvider{prefix;useColor;constructor(prefix){this.prefix=prefix??``,this.useColor=typeof process<`u`&&process.stdout?.isTTY===!0&&!process.env.NO_COLOR}fmt(level,color,msg){let tag=this.prefix?`[${this.prefix}]`:``;return this.useColor?`${color}${level}\x1b[0m ${tag?`\x1b[36m${tag}\x1b[0m `:``}${msg}`:`${level} ${tag?`${tag} `:``}${msg}`}info(msg,...args){console.log(this.fmt(`INFO`,`\x1B[32m`,msg),...args)}warn(msg,...args){console.warn(this.fmt(`WARN`,`\x1B[33m`,msg),...args)}error(msg,...args){console.error(this.fmt(`ERROR`,`\x1B[31m`,msg),...args)}debug(msg,...args){console.debug(this.fmt(`DEBUG`,`\x1B[90m`,msg),...args)}trace(msg,...args){console.trace(this.fmt(`TRACE`,`\x1B[90m`,msg),...args)}fatal(msg,...args){console.error(this.fmt(`FATAL`,`\x1B[1m\x1B[31m`,msg),...args)}child(bindings){return new ConsoleLoggerProvider(bindings.component)}};let activeProvider=new ConsoleLoggerProvider;const loggerCache=new Map;var Logger=class Logger{_provider;_providerVersion;_name;static _providerVersion=0;constructor(name){this._name=name,this._providerVersion=Logger._providerVersion,this._provider=name?activeProvider.child({component:name}):activeProvider}get provider(){return this._providerVersion!==Logger._providerVersion&&(this._providerVersion=Logger._providerVersion,this._provider=this._name?activeProvider.child({component:this._name}):activeProvider),this._provider}static setProvider(provider){activeProvider=provider,Logger._providerVersion++,loggerCache.clear()}static getProvider(){return activeProvider}static resetProvider(){activeProvider=new ConsoleLoggerProvider,Logger._providerVersion++,loggerCache.clear()}static for(name){let cached=loggerCache.get(name);return cached||(cached=new Logger(name),loggerCache.set(name,cached)),cached}child(name){return new Logger(name)}info(msg,...args){this.provider.info(msg,...args)}warn(msg,...args){this.provider.warn(msg,...args)}error(msgOrObj,msg,...args){typeof msgOrObj==`string`?this.provider.error(msgOrObj):msg?this.provider.error(msg,...args):this.provider.error(String(msgOrObj))}debug(msg,...args){this.provider.debug(msg,...args)}trace(msg,...args){this.provider.trace?this.provider.trace(msg,...args):this.provider.debug(msg,...args)}fatal(msg,...args){this.provider.fatal?this.provider.fatal(msg,...args):this.provider.error(msg,...args)}};function createLogger(name){return Logger.for(name)}export{ConsoleLoggerProvider,Logger,createLogger};
|
|
11
|
+
const LEVEL_RANK={trace:10,debug:20,info:30,warn:40,error:50,fatal:60,silent:100};function thresholdRank(){return LEVEL_RANK[(process.env.LOG_LEVEL??`info`).toLowerCase()]??LEVEL_RANK.info}var ConsoleLoggerProvider=class ConsoleLoggerProvider{prefix;useColor;constructor(prefix){this.prefix=prefix??``,this.useColor=typeof process<`u`&&process.stdout?.isTTY===!0&&!process.env.NO_COLOR}fmt(level,color,msg){let tag=this.prefix?`[${this.prefix}]`:``;return this.useColor?`${color}${level}\x1b[0m ${tag?`\x1b[36m${tag}\x1b[0m `:``}${msg}`:`${level} ${tag?`${tag} `:``}${msg}`}enabled(rank){return rank>=thresholdRank()}info(msg,...args){this.enabled(LEVEL_RANK.info)&&console.log(this.fmt(`INFO`,`\x1B[32m`,msg),...args)}warn(msg,...args){this.enabled(LEVEL_RANK.warn)&&console.warn(this.fmt(`WARN`,`\x1B[33m`,msg),...args)}error(msg,...args){this.enabled(LEVEL_RANK.error)&&console.error(this.fmt(`ERROR`,`\x1B[31m`,msg),...args)}debug(msg,...args){this.enabled(LEVEL_RANK.debug)&&console.debug(this.fmt(`DEBUG`,`\x1B[90m`,msg),...args)}trace(msg,...args){this.enabled(LEVEL_RANK.trace)&&console.trace(this.fmt(`TRACE`,`\x1B[90m`,msg),...args)}fatal(msg,...args){this.enabled(LEVEL_RANK.fatal)&&console.error(this.fmt(`FATAL`,`\x1B[1m\x1B[31m`,msg),...args)}child(bindings){return new ConsoleLoggerProvider(bindings.component)}};let activeProvider=new ConsoleLoggerProvider;const loggerCache=new Map;var Logger=class Logger{_provider;_providerVersion;_name;static _providerVersion=0;constructor(name){this._name=name,this._providerVersion=Logger._providerVersion,this._provider=name?activeProvider.child({component:name}):activeProvider}get provider(){return this._providerVersion!==Logger._providerVersion&&(this._providerVersion=Logger._providerVersion,this._provider=this._name?activeProvider.child({component:this._name}):activeProvider),this._provider}static setProvider(provider){activeProvider=provider,Logger._providerVersion++,loggerCache.clear()}static getProvider(){return activeProvider}static resetProvider(){activeProvider=new ConsoleLoggerProvider,Logger._providerVersion++,loggerCache.clear()}static for(name){let cached=loggerCache.get(name);return cached||(cached=new Logger(name),loggerCache.set(name,cached)),cached}child(name){return new Logger(name)}info(msg,...args){this.provider.info(msg,...args)}warn(msg,...args){this.provider.warn(msg,...args)}error(msgOrObj,msg,...args){typeof msgOrObj==`string`?this.provider.error(msgOrObj):msg?this.provider.error(msg,...args):this.provider.error(String(msgOrObj))}debug(msg,...args){this.provider.debug(msg,...args)}trace(msg,...args){this.provider.trace?this.provider.trace(msg,...args):this.provider.debug(msg,...args)}fatal(msg,...args){this.provider.fatal?this.provider.fatal(msg,...args):this.provider.error(msg,...args)}};function createLogger(name){return Logger.for(name)}export{ConsoleLoggerProvider,Logger,createLogger};
|
|
12
12
|
//# sourceMappingURL=logger.mjs.map
|