@forinda/kickjs 6.2.0 → 6.3.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/README.md +15 -0
- package/dist/{adapter-DCfEFZMY.d.mts → adapter-stSpsUHs.d.mts} +33 -10
- package/dist/{adapter-DCfEFZMY.d.mts.map → adapter-stSpsUHs.d.mts.map} +1 -1
- package/dist/{application-DS7t40r-.mjs → application-CHYTI1Q_.mjs} +3 -3
- package/dist/{application-DS7t40r-.mjs.map → application-CHYTI1Q_.mjs.map} +1 -1
- package/dist/{application-p8RRz7p6.d.mts → application-C_KlrNoH.d.mts} +2 -2
- package/dist/{application-p8RRz7p6.d.mts.map → application-C_KlrNoH.d.mts.map} +1 -1
- package/dist/{bootstrap-JXdD0wVC.d.mts → bootstrap-BgffgFNX.d.mts} +2 -2
- package/dist/{bootstrap-JXdD0wVC.d.mts.map → bootstrap-BgffgFNX.d.mts.map} +1 -1
- package/dist/{bootstrap-DESuFm-t.mjs → bootstrap-sB0dlLu7.mjs} +3 -3
- package/dist/{bootstrap-DESuFm-t.mjs.map → bootstrap-sB0dlLu7.mjs.map} +1 -1
- package/dist/config/index.d.mts +1 -1
- package/dist/config/index.mjs +2 -2
- package/dist/{config-CqN6u5CP.mjs → config-DqO6gB-n.mjs} +3 -3
- package/dist/{config-CqN6u5CP.mjs.map → config-DqO6gB-n.mjs.map} +1 -1
- package/dist/{container-CkcVS_gA.mjs → container-C522k8AX.mjs} +3 -3
- package/dist/{container-CkcVS_gA.mjs.map → container-C522k8AX.mjs.map} +1 -1
- package/dist/{context-Cd-p7DfC.mjs → context-Cmnmn68a.mjs} +3 -3
- package/dist/{context-Cd-p7DfC.mjs.map → context-Cmnmn68a.mjs.map} +1 -1
- package/dist/{contributor-runner-BP9rOU_2.mjs → contributor-runner-C4qFEVIz.mjs} +2 -2
- package/dist/{contributor-runner-BP9rOU_2.mjs.map → contributor-runner-C4qFEVIz.mjs.map} +1 -1
- package/dist/core/adapter.d.mts +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.d.mts +1 -1
- 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/interfaces.mjs +1 -1
- package/dist/core/logger.mjs +1 -1
- package/dist/core/path.mjs +1 -1
- package/dist/core/plugin.d.mts +1 -1
- package/dist/core/plugin.mjs +1 -1
- package/dist/core/reactivity.mjs +1 -1
- package/dist/{core-BBnRIW_2.mjs → core-3S12kJ7Q.mjs} +3 -3
- package/dist/{core-BBnRIW_2.mjs.map → core-3S12kJ7Q.mjs.map} +1 -1
- package/dist/{csrf-CrChcN0K.mjs → csrf-CbB5oAPn.mjs} +2 -2
- package/dist/{csrf-CrChcN0K.mjs.map → csrf-CbB5oAPn.mjs.map} +1 -1
- package/dist/{decorators-DKjpujFp.d.mts → decorators-wEJBqXHg.d.mts} +9 -2
- package/dist/{decorators-DKjpujFp.d.mts.map → decorators-wEJBqXHg.d.mts.map} +1 -1
- package/dist/{disposables-msQG0AHt.mjs → disposables-BBTIbZkX.mjs} +2 -2
- package/dist/{disposables-msQG0AHt.mjs.map → disposables-BBTIbZkX.mjs.map} +1 -1
- package/dist/{env-CrvPEW3K.mjs → env-Cj5QLqnZ.mjs} +3 -3
- package/dist/{env-CrvPEW3K.mjs.map → env-Cj5QLqnZ.mjs.map} +1 -1
- package/dist/{handler-BMUwLMu_.mjs → handler-db1-nMd6.mjs} +3 -3
- package/dist/{handler-BMUwLMu_.mjs.map → handler-db1-nMd6.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.d.mts +2 -2
- package/dist/http/context.mjs +2 -2
- package/dist/http/middleware/cors.mjs +1 -1
- package/dist/http/middleware/csrf.mjs +2 -2
- package/dist/http/middleware/error-handler.mjs +2 -2
- package/dist/http/middleware/helmet.mjs +1 -1
- package/dist/http/middleware/rate-limit.mjs +2 -2
- package/dist/http/middleware/request-id.mjs +1 -1
- package/dist/http/middleware/request-logger.mjs +2 -2
- package/dist/http/middleware/session.mjs +2 -2
- package/dist/http/middleware/spa.d.mts +1 -1
- package/dist/http/middleware/spa.mjs +2 -2
- package/dist/http/middleware/upload.d.mts +1 -1
- package/dist/http/middleware/upload.mjs +2 -2
- package/dist/http/middleware/validate.mjs +1 -1
- package/dist/http/middleware/views.d.mts +1 -1
- package/dist/http/middleware/views.mjs +2 -2
- package/dist/http/query/index.mjs +2 -2
- package/dist/http/router-builder.d.mts +1 -1
- package/dist/http/router-builder.mjs +2 -2
- package/dist/http/runtimes/fastify.d.mts +1 -1
- package/dist/http/runtimes/fastify.mjs +2 -2
- package/dist/http/runtimes/h3-web.d.mts +1 -1
- package/dist/http/runtimes/h3-web.mjs +2 -2
- package/dist/http/runtimes/h3.d.mts +1 -1
- package/dist/http/runtimes/h3.mjs +2 -2
- package/dist/{index-ChK5rOgq.d.mts → index-D98NLfS5.d.mts} +2 -2
- package/dist/{index-ChK5rOgq.d.mts.map → index-D98NLfS5.d.mts.map} +1 -1
- package/dist/index.d.mts +11 -11
- package/dist/index.mjs +2 -2
- package/dist/{metadata-WlPWz6fi.mjs → metadata-W54KhZoU.mjs} +2 -2
- package/dist/{metadata-WlPWz6fi.mjs.map → metadata-W54KhZoU.mjs.map} +1 -1
- package/dist/{module-registry-DmTQjSdD.mjs → module-registry-BbYVBwD9.mjs} +2 -2
- package/dist/{module-registry-DmTQjSdD.mjs.map → module-registry-BbYVBwD9.mjs.map} +1 -1
- package/dist/{query-B2w_8bwe.mjs → query-B0ucxuU0.mjs} +2 -2
- package/dist/{query-B2w_8bwe.mjs.map → query-B0ucxuU0.mjs.map} +1 -1
- package/dist/{reply-Elm9mc8Z.d.mts → reply-2CvBP_Sl.d.mts} +2 -2
- package/dist/{reply-Elm9mc8Z.d.mts.map → reply-2CvBP_Sl.d.mts.map} +1 -1
- package/dist/{reply-Dh15A_0R.mjs → reply-DI4EqF0R.mjs} +3 -3
- package/dist/{reply-Dh15A_0R.mjs.map → reply-DI4EqF0R.mjs.map} +1 -1
- package/dist/{rolldown-runtime-C4pdyXoH.mjs → rolldown-runtime-Dv-xQPN8.mjs} +1 -1
- package/dist/{router-builder-BHTp3zL3.d.mts → router-builder-BK7yPm89.d.mts} +2 -2
- package/dist/{router-builder-BHTp3zL3.d.mts.map → router-builder-BK7yPm89.d.mts.map} +1 -1
- package/dist/{spa-D8I4YnpL.d.mts → spa-BwO1qOuA.d.mts} +2 -2
- package/dist/{spa-D8I4YnpL.d.mts.map → spa-BwO1qOuA.d.mts.map} +1 -1
- package/dist/{upload-CUyZ0zNt.d.mts → upload-CIZc803m.d.mts} +2 -2
- package/dist/{upload-CUyZ0zNt.d.mts.map → upload-CIZc803m.d.mts.map} +1 -1
- package/dist/{upload-config-B6vGWI0_.mjs → upload-config-D8gsl_ox.mjs} +2 -2
- package/dist/{upload-config-B6vGWI0_.mjs.map → upload-config-D8gsl_ox.mjs.map} +1 -1
- package/dist/{views-PFf-CBvN.d.mts → views-w0EPrhui.d.mts} +3 -3
- package/dist/{views-PFf-CBvN.d.mts.map → views-w0EPrhui.d.mts.map} +1 -1
- package/dist/web.d.mts +3 -3
- package/dist/web.d.mts.map +1 -1
- package/dist/web.mjs +2 -2
- package/dist/web.mjs.map +1 -1
- package/package.json +1 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"context-Cd-p7DfC.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 /** Set once {@link disposeRequestStore} has run — guards double-dispose. */\n disposed?: boolean\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, NextFunction } from 'express'\nimport type { ActiveRuntime, RuntimeResponse } from './runtime'\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 ParseQueryOptions,\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): RuntimeResponse\n /** 400 Bad Request — RFC 9457. */\n badRequest(input?: ProblemInput): RuntimeResponse\n /** 401 Unauthorized — RFC 9457. */\n unauthorized(input?: ProblemInput): RuntimeResponse\n /** 403 Forbidden — RFC 9457. */\n forbidden(input?: ProblemInput): RuntimeResponse\n /** 404 Not Found — RFC 9457. */\n notFound(input?: ProblemInput): RuntimeResponse\n /** 409 Conflict — RFC 9457. */\n conflict(input?: ProblemInput): RuntimeResponse\n /** 422 Unprocessable Entity — RFC 9457. */\n unprocessable(input?: ProblemInput): RuntimeResponse\n /** 429 Too Many Requests — RFC 9457. */\n tooManyRequests(input?: ProblemInput): RuntimeResponse\n /** 500 Internal Server Error — RFC 9457. */\n internal(input?: ProblemInput): RuntimeResponse\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): RuntimeResponse\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 /**\n * Engine-agnostic response driver the response helpers write through. Under\n * the Express runtime it IS `res` (Express's Response satisfies\n * {@link RuntimeResponse} structurally); other runtimes pass a thin wrapper\n * over their native reply as the optional fourth argument.\n */\n private readonly _response: RuntimeResponse\n\n constructor(\n public readonly req: ActiveRuntime['request'],\n public readonly res: ActiveRuntime['response'],\n public readonly next: NextFunction,\n responseDriver?: RuntimeResponse,\n ) {\n // Under the Express runtime `res` IS a RuntimeResponse (structural); other\n // runtimes pass an explicit driver wrapping their native reply.\n this._response = responseDriver ?? (res as RuntimeResponse)\n }\n\n /**\n * Memo of the most recent {@link qs} call this request. The query\n * object is immutable for a request's lifetime, so a repeat call with\n * the same `(fieldConfig, options)` references returns the cached\n * parse instead of re-walking the filter/sort strings.\n */\n private _qsMemo?: { config: unknown; options: unknown; result: unknown }\n\n /** Memoized {@link problem} sender — built lazily on first access. */\n private _problem?: ProblemSender\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._response.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 * @param options - Limits (`maxLimit`, `defaultLimit`, `maxSearchLength`) and an\n * `onReject` hook. By default a rejected/over-broad field is logged via\n * `console.warn` with the request id; pass an explicit `onReject` to override\n * (e.g. throw to return a 400), or `onReject: () => {}` to silence.\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 options?: ParseQueryOptions,\n ): TypedParsedQuery<TConfig> {\n // Cheap per-request memo — repeat calls with the same args (the\n // common controller pattern) skip re-parsing. `req.query` is fixed\n // for the request's lifetime, so caching is always sound.\n const memo = this._qsMemo\n if (memo && memo.config === fieldConfig && memo.options === options) {\n return memo.result as TypedParsedQuery<TConfig>\n }\n const effective: ParseQueryOptions = {\n ...options,\n onReject:\n options?.onReject ??\n ((rejection) => {\n const id = this.requestId ? `[${this.requestId}] ` : ''\n console.warn(\n `${id}query ${rejection.kind} '${rejection.field}' rejected (${rejection.reason})`,\n )\n }),\n }\n const result = parseQuery<TConfig>(\n this.req.query as Record<string, any>,\n fieldConfig,\n effective,\n )\n this._qsMemo = { config: fieldConfig, options, result }\n return result\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._response.status(status).json(data)\n }\n\n created(data: any) {\n return this._response.status(201).json(data)\n }\n\n noContent() {\n return this._response.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._response.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._response.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 // Memoized — building the sender allocates ~10 closures, so pay that only\n // on the first access per context instead of per `ctx.problem.*` call.\n if (this._problem) return this._problem\n const send = (input: ProblemDetails): RuntimeResponse => this.sendProblem(input)\n send.badRequest = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 400, ...input })\n send.unauthorized = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 401, ...input })\n send.forbidden = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 403, ...input })\n send.notFound = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 404, ...input })\n send.conflict = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 409, ...input })\n send.unprocessable = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 422, ...input })\n send.tooManyRequests = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 429, ...input })\n send.internal = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 500, ...input })\n send.validation = (issues: any[], input: ProblemInput = {}): RuntimeResponse => {\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 this._problem = send as ProblemSender\n return this._problem\n }\n\n private sendProblem(input: ProblemDetails): RuntimeResponse {\n const body = normalizeProblem(input)\n this._response.setHeader('Content-Type', 'application/problem+json')\n return this._response.status(body.status).json(body)\n }\n\n html(content: string, status = 200) {\n return this._response.status(status).type('html').send(content)\n }\n\n download(buffer: Buffer, filename: string, contentType = 'application/octet-stream') {\n this._response.setHeader('Content-Disposition', `attachment; filename=\"${filename}\"`)\n this._response.setHeader('Content-Type', contentType)\n return this._response.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._response.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._response.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._response.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._response.write(`id: ${id}\\n`)\n if (event) this._response.write(`event: ${event}\\n`)\n this._response.write(`data: ${JSON.stringify(data)}\\n\\n`)\n },\n /** Send a comment (keeps connection alive) */\n comment: (text: string) => {\n this._response.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._response.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 * Interfaces inside `KickRoutes` correspond to controller classes (each\n * property one route method, conforming to `RouteShape`) — plus the\n * reserved `KickRoutes.Api`: a flat `'METHOD /mounted/path'` map over the\n * same shapes, consumed by `@forinda/kickjs-client`. Don't name a\n * controller class `Api` — it would declaration-merge into that map.\n *\n * Empty by default — declarations come from `.kickjs/types/kick__routes.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-Cmnmn68a.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 /** Set once {@link disposeRequestStore} has run — guards double-dispose. */\n disposed?: boolean\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, NextFunction } from 'express'\nimport type { ActiveRuntime, RuntimeResponse } from './runtime'\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 ParseQueryOptions,\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): RuntimeResponse\n /** 400 Bad Request — RFC 9457. */\n badRequest(input?: ProblemInput): RuntimeResponse\n /** 401 Unauthorized — RFC 9457. */\n unauthorized(input?: ProblemInput): RuntimeResponse\n /** 403 Forbidden — RFC 9457. */\n forbidden(input?: ProblemInput): RuntimeResponse\n /** 404 Not Found — RFC 9457. */\n notFound(input?: ProblemInput): RuntimeResponse\n /** 409 Conflict — RFC 9457. */\n conflict(input?: ProblemInput): RuntimeResponse\n /** 422 Unprocessable Entity — RFC 9457. */\n unprocessable(input?: ProblemInput): RuntimeResponse\n /** 429 Too Many Requests — RFC 9457. */\n tooManyRequests(input?: ProblemInput): RuntimeResponse\n /** 500 Internal Server Error — RFC 9457. */\n internal(input?: ProblemInput): RuntimeResponse\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): RuntimeResponse\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 /**\n * Engine-agnostic response driver the response helpers write through. Under\n * the Express runtime it IS `res` (Express's Response satisfies\n * {@link RuntimeResponse} structurally); other runtimes pass a thin wrapper\n * over their native reply as the optional fourth argument.\n */\n private readonly _response: RuntimeResponse\n\n constructor(\n public readonly req: ActiveRuntime['request'],\n public readonly res: ActiveRuntime['response'],\n public readonly next: NextFunction,\n responseDriver?: RuntimeResponse,\n ) {\n // Under the Express runtime `res` IS a RuntimeResponse (structural); other\n // runtimes pass an explicit driver wrapping their native reply.\n this._response = responseDriver ?? (res as RuntimeResponse)\n }\n\n /**\n * Memo of the most recent {@link qs} call this request. The query\n * object is immutable for a request's lifetime, so a repeat call with\n * the same `(fieldConfig, options)` references returns the cached\n * parse instead of re-walking the filter/sort strings.\n */\n private _qsMemo?: { config: unknown; options: unknown; result: unknown }\n\n /** Memoized {@link problem} sender — built lazily on first access. */\n private _problem?: ProblemSender\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._response.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 * @param options - Limits (`maxLimit`, `defaultLimit`, `maxSearchLength`) and an\n * `onReject` hook. By default a rejected/over-broad field is logged via\n * `console.warn` with the request id; pass an explicit `onReject` to override\n * (e.g. throw to return a 400), or `onReject: () => {}` to silence.\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 options?: ParseQueryOptions,\n ): TypedParsedQuery<TConfig> {\n // Cheap per-request memo — repeat calls with the same args (the\n // common controller pattern) skip re-parsing. `req.query` is fixed\n // for the request's lifetime, so caching is always sound.\n const memo = this._qsMemo\n if (memo && memo.config === fieldConfig && memo.options === options) {\n return memo.result as TypedParsedQuery<TConfig>\n }\n const effective: ParseQueryOptions = {\n ...options,\n onReject:\n options?.onReject ??\n ((rejection) => {\n const id = this.requestId ? `[${this.requestId}] ` : ''\n console.warn(\n `${id}query ${rejection.kind} '${rejection.field}' rejected (${rejection.reason})`,\n )\n }),\n }\n const result = parseQuery<TConfig>(\n this.req.query as Record<string, any>,\n fieldConfig,\n effective,\n )\n this._qsMemo = { config: fieldConfig, options, result }\n return result\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._response.status(status).json(data)\n }\n\n created(data: any) {\n return this._response.status(201).json(data)\n }\n\n noContent() {\n return this._response.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._response.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._response.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 // Memoized — building the sender allocates ~10 closures, so pay that only\n // on the first access per context instead of per `ctx.problem.*` call.\n if (this._problem) return this._problem\n const send = (input: ProblemDetails): RuntimeResponse => this.sendProblem(input)\n send.badRequest = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 400, ...input })\n send.unauthorized = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 401, ...input })\n send.forbidden = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 403, ...input })\n send.notFound = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 404, ...input })\n send.conflict = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 409, ...input })\n send.unprocessable = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 422, ...input })\n send.tooManyRequests = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 429, ...input })\n send.internal = (input: ProblemInput = {}): RuntimeResponse =>\n this.sendProblem({ status: 500, ...input })\n send.validation = (issues: any[], input: ProblemInput = {}): RuntimeResponse => {\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 this._problem = send as ProblemSender\n return this._problem\n }\n\n private sendProblem(input: ProblemDetails): RuntimeResponse {\n const body = normalizeProblem(input)\n this._response.setHeader('Content-Type', 'application/problem+json')\n return this._response.status(body.status).json(body)\n }\n\n html(content: string, status = 200) {\n return this._response.status(status).type('html').send(content)\n }\n\n download(buffer: Buffer, filename: string, contentType = 'application/octet-stream') {\n this._response.setHeader('Content-Disposition', `attachment; filename=\"${filename}\"`)\n this._response.setHeader('Content-Type', contentType)\n return this._response.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._response.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 * Pass a payload type to get typed sends — and `return sse` from the\n * handler so `kick typegen` carries the event type into\n * `KickRoutes.Api`, where `@forinda/kickjs-client`'s `api.stream()`\n * picks it up (the returned handler is ignored at runtime — the\n * response is already streaming).\n *\n * @example\n * ```ts\n * interface Tick { time: string }\n *\n * @Get('/events')\n * async stream(ctx: RequestContext) {\n * const sse = ctx.sse<Tick>()\n *\n * const interval = setInterval(() => {\n * sse.send({ time: new Date().toISOString() }, 'tick')\n * }, 1000)\n *\n * sse.onClose(() => clearInterval(interval))\n * return sse // typed as SseHandler<Tick> → response inference\n * }\n * ```\n */\n sse<T = unknown>(): SseHandler<T> {\n this._response.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._response.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: T, event?: string, id?: string) => {\n if (id) this._response.write(`id: ${id}\\n`)\n if (event) this._response.write(`event: ${event}\\n`)\n this._response.write(`data: ${JSON.stringify(data)}\\n\\n`)\n },\n /** Send a comment (keeps connection alive) */\n comment: (text: string) => {\n this._response.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._response.end()\n },\n }\n }\n}\n\n/**\n * The handle {@link RequestContext.sse} returns. The optional phantom\n * `__sse` property carries the event payload type STRUCTURALLY — never set\n * at runtime — so `kick typegen`'s response inference (handler `return sse`)\n * flows it into `KickRoutes.Api`, and `@forinda/kickjs-client` detects SSE\n * routes without importing any server types.\n */\nexport interface SseHandler<T = unknown> {\n /** Phantom event-payload type marker — never assigned at runtime. */\n readonly __sse?: T\n /** Send an SSE event with optional event name and id. */\n send(data: T, event?: string, id?: string): void\n /** Send a comment line (keeps the connection alive through proxies). */\n comment(text: string): void\n /** Register a callback for client disconnect. */\n onClose(fn: () => void): void\n /** End the SSE stream. */\n close(): void\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 * Interfaces inside `KickRoutes` correspond to controller classes (each\n * property one route method, conforming to `RouteShape`) — plus the\n * reserved `KickRoutes.Api`: a flat `'METHOD /mounted/path'` map over the\n * same shapes, consumed by `@forinda/kickjs-client`. Don't name a\n * controller class `Api` — it would declaration-merge into that map.\n *\n * Empty by default — declarations come from `.kickjs/types/kick__routes.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,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v6.
|
|
2
|
+
* @forinda/kickjs v6.3.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -10,4 +10,4 @@
|
|
|
10
10
|
*/
|
|
11
11
|
var MissingContributorError=class extends Error{key;dependent;route;dependentDefinedAt;constructor(key,dependent,route,dependentDefinedAt){let where=route?` on route ${route}`:``,declaredAt=formatDeclaredAt(dependentDefinedAt);super(`Missing context contributor '${key}' required by '${dependent}'${where}${declaredAt}`),this.name=`MissingContributorError`,this.key=key,this.dependent=dependent,this.route=route,this.dependentDefinedAt=dependentDefinedAt}};function formatDeclaredAt(stack){if(!stack)return``;let lines=stack.split(`
|
|
12
12
|
`);for(let line of lines){let trimmed=line.trim();if(trimmed.startsWith(`at `)&&!trimmed.includes(`context-decorator.`)&&!trimmed.includes(`context-errors.`))return`\n declared at ${trimmed.replace(/^at /,``)}`}return``}var ContributorCycleError=class extends Error{cycle;route;constructor(cycle,route){let where=route?` on route ${route}`:``;super(`Cycle in context contributors${where}: ${cycle.join(` → `)}`),this.name=`ContributorCycleError`,this.cycle=Object.freeze([...cycle]),this.route=route}},DuplicateContributorError=class extends Error{key;sources;constructor(key,sources){super(`Duplicate context contributor for key '${key}' at the same precedence level. Sources: ${sources.join(`, `)}`),this.name=`DuplicateContributorError`,this.key=key,this.sources=Object.freeze([...sources])}};const PRECEDENCE_ORDER=[`method`,`class`,`module`,`adapter`,`global`];function buildPipeline(sources,options={}){let route=options.route,bySource=new Map;for(let src of PRECEDENCE_ORDER)bySource.set(src,[]);for(let entry of sources)bySource.get(entry.source).push(entry);let winners=new Map;for(let src of PRECEDENCE_ORDER){let rows=bySource.get(src),keysAtThisLevel=new Map;for(let row of rows){let key=row.registration.key,existing=keysAtThisLevel.get(key)??[];existing.push(row),keysAtThisLevel.set(key,existing)}for(let[key,rowsForKey]of keysAtThisLevel){if(rowsForKey.length>1)throw new DuplicateContributorError(key,rowsForKey.map((r,i)=>r.label??`${src}#${i}`));winners.has(key)||winners.set(key,rowsForKey[0])}}let survivors=[...winners.values()].map(r=>r.registration),allKeys=new Set(survivors.map(r=>r.key));for(let reg of survivors)for(let dep of reg.dependsOn)if(!allKeys.has(dep))throw new MissingContributorError(dep,reg.key,route,reg.definedAt);let sorted=topoSort(survivors,route);return Object.freeze({contributors:Object.freeze(sorted),keys:allKeys})}function topoSort(registrations,route){let indegree=new Map,dependents=new Map,byKey=new Map;for(let reg of registrations)indegree.set(reg.key,reg.dependsOn.length),byKey.set(reg.key,reg);for(let reg of registrations)for(let dep of reg.dependsOn){let list=dependents.get(dep)??[];list.push(reg),dependents.set(dep,list)}let queue=[];for(let reg of registrations)indegree.get(reg.key)===0&&queue.push(reg);let sorted=[];for(;queue.length>0;){let node=queue.shift();sorted.push(node);for(let dependent of dependents.get(node.key)??[]){let next=(indegree.get(dependent.key)??0)-1;indegree.set(dependent.key,next),next===0&&queue.push(dependent)}}if(sorted.length!==registrations.length)throw new ContributorCycleError(reconstructCycle(registrations.filter(r=>(indegree.get(r.key)??0)>0),byKey),route);return sorted}function reconstructCycle(residual,byKey){let residualKeys=new Set(residual.map(r=>r.key)),start=residual[0].key,path=[start],seen=new Set([start]),cursor=start;for(;;){let nextDep=byKey.get(cursor).dependsOn.find(d=>residualKeys.has(d));if(nextDep===void 0)break;if(seen.has(nextDep)){let cycleStart=path.indexOf(nextDep);return[...path.slice(cycleStart),nextDep]}path.push(nextDep),seen.add(nextDep),cursor=nextDep}return path}async function runContributors(options){let{pipeline,ctx,container}=options;for(let reg of pipeline.contributors)await runOne(reg,ctx,container)}async function runOne(reg,ctx,container){let value;try{let deps=resolveDeps(reg,container);value=await reg.resolve(ctx,deps)}catch(err){if(reg.optional)return;if(reg.onError){let replacement=await reg.onError(err,ctx);if(replacement===void 0)return;ctx.set(reg.key,replacement);return}throw err}ctx.set(reg.key,value)}function resolveDeps(reg,container){let out={};for(let[key,token]of Object.entries(reg.deps))out[key]=container.resolve(token);return out}export{MissingContributorError as a,DuplicateContributorError as i,buildPipeline as n,ContributorCycleError as r,runContributors as t};
|
|
13
|
-
//# sourceMappingURL=contributor-runner-
|
|
13
|
+
//# sourceMappingURL=contributor-runner-C4qFEVIz.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"contributor-runner-BP9rOU_2.mjs","names":[],"sources":["../src/core/context-errors.ts","../src/core/contributor-pipeline.ts","../src/core/contributor-runner.ts"],"sourcesContent":["/**\n * Errors thrown by the Context Contributor pipeline.\n *\n * All three are startup-time errors raised during pipeline build (topo-sort\n * + validation), not per-request — a misconfigured app fails to boot rather\n * than fails a request. Per the locked design in `architecture.md` §20.4,\n * the pipeline is validated once when each route is mounted and the\n * resolved order is cached on the route.\n */\n\n/**\n * A contributor declared `dependsOn: ['<key>']` but no other contributor\n * (at any precedence level) produces that key for the same route.\n *\n * @example\n * ```text\n * MissingContributorError: Missing context contributor 'tenant' required by 'project' on route GET /projects/:id\n * declared at src/contributors/load-project.ts:42:18\n * ```\n */\nexport class MissingContributorError extends Error {\n readonly key: string\n readonly dependent: string\n readonly route?: string\n readonly dependentDefinedAt?: string\n\n constructor(key: string, dependent: string, route?: string, dependentDefinedAt?: string) {\n const where = route ? ` on route ${route}` : ''\n const declaredAt = formatDeclaredAt(dependentDefinedAt)\n super(`Missing context contributor '${key}' required by '${dependent}'${where}${declaredAt}`)\n this.name = 'MissingContributorError'\n this.key = key\n this.dependent = dependent\n this.route = route\n this.dependentDefinedAt = dependentDefinedAt\n }\n}\n\n/**\n * Slice the first non-framework frame off a captured `Error.stack`\n * and format it as `\\n declared at <frame>` for inclusion in\n * boot-time error messages. Returns `''` when no useful frame is\n * available (hand-rolled registrations, missing stacks).\n */\nfunction formatDeclaredAt(stack: string | undefined): string {\n if (!stack) return ''\n const lines = stack.split('\\n')\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('at ')) continue\n // Skip frames inside this file or the decorator factory — the\n // adopter's call site is the next frame after those.\n if (trimmed.includes('context-decorator.')) continue\n if (trimmed.includes('context-errors.')) continue\n return `\\n declared at ${trimmed.replace(/^at /, '')}`\n }\n return ''\n}\n\n/**\n * Topo-sort detected a cycle in `dependsOn` declarations. The `cycle` field\n * lists the keys in the order they form the loop, with the first key\n * repeated at the end so the path reads naturally.\n *\n * @example\n * ```text\n * ContributorCycleError: Cycle in context contributors on route GET /projects/:id: tenant → project → tenant\n * ```\n */\nexport class ContributorCycleError extends Error {\n readonly cycle: readonly string[]\n readonly route?: string\n\n constructor(cycle: readonly string[], route?: string) {\n const where = route ? ` on route ${route}` : ''\n super(`Cycle in context contributors${where}: ${cycle.join(' → ')}`)\n this.name = 'ContributorCycleError'\n this.cycle = Object.freeze([...cycle])\n this.route = route\n }\n}\n\n/**\n * Two contributors at the same precedence level both produce the same key\n * for the same route. Precedence resolution (method > class > module >\n * adapter > global, per architecture.md §20.4) silently drops\n * lower-precedence duplicates; this error fires only when ties occur\n * within a single level — e.g., two method-level decorators declaring\n * `key: 'tenant'`.\n *\n * `sources` is best-effort identifying labels for each conflicting\n * registration (typically the decorator's call site or registering\n * module/adapter name).\n *\n * @example\n * ```text\n * DuplicateContributorError: Duplicate context contributor for key 'tenant' at the same precedence level. Sources: LoadTenantFromHeader, LoadTenantFromSubdomain\n * ```\n */\nexport class DuplicateContributorError extends Error {\n readonly key: string\n readonly sources: readonly string[]\n\n constructor(key: string, sources: readonly string[]) {\n super(\n `Duplicate context contributor for key '${key}' at the same precedence level. ` +\n `Sources: ${sources.join(', ')}`,\n )\n this.name = 'DuplicateContributorError'\n this.key = key\n this.sources = Object.freeze([...sources])\n }\n}\n","import type { AnyContributorRegistration } from './context-decorator'\nimport {\n ContributorCycleError,\n DuplicateContributorError,\n MissingContributorError,\n} from './context-errors'\n\n/**\n * Where a contributor was registered, ordered by precedence\n * (lower index wins). Mirrors `architecture.md` §20.4.\n *\n * Levels (highest → lowest precedence):\n * - `'method'` — `@LoadX` on a controller method.\n * - `'class'` — `@LoadX` on a controller class.\n * - `'module'` — returned by `AppModule.contributors?()`. Per-module scope.\n * - `'adapter'` — returned by `AppAdapter.contributors?()`. Cross-cutting, applies\n * to every module's routes.\n * - `'global'` — `ApplicationOptions.contributors`. App-wide default.\n */\nexport type ContributorSource = 'method' | 'class' | 'module' | 'adapter' | 'global'\n\n/** Precedence-ranked source list. Index = precedence; method (0) wins ties. */\nconst PRECEDENCE_ORDER: readonly ContributorSource[] = [\n 'method',\n 'class',\n 'module',\n 'adapter',\n 'global',\n]\n\n/**\n * Input row to {@link buildPipeline}. Pairs a registration with its origin\n * so dedup can apply the precedence rule and diagnostic errors can name\n * the offending site.\n *\n * `registration` is the type-erased {@link AnyContributorRegistration}\n * — the runner doesn't care about each registration's narrow `D` /\n * `Ctx` shape, and adopters frequently mix HTTP / transport-agnostic\n * contributors in the same `externalSources` / `contributors()`\n * array. The narrow generics from `defineContextDecorator` survive\n * at the *definition* site (where `resolve` is typed); they're erased\n * here so collections compose without `as any` casts.\n */\nexport interface SourcedRegistration {\n registration: AnyContributorRegistration\n source: ContributorSource\n /** Human label surfaced in DuplicateContributorError messages. */\n label?: string\n}\n\n/**\n * Resolved, validated, topo-sorted pipeline ready for execution.\n *\n * `contributors` is in execution order — every entry's `dependsOn`\n * keys appear earlier. `keys` is the set of all keys this pipeline\n * produces; the runner uses it for fast existence checks and the\n * builder uses it for missing-dep validation.\n */\nexport interface ContributorPipeline {\n readonly contributors: readonly AnyContributorRegistration[]\n readonly keys: ReadonlySet<string>\n}\n\nexport interface BuildPipelineOptions {\n /** Optional route identifier surfaced in diagnostic errors (e.g., 'GET /projects/:id'). */\n route?: string\n}\n\n/**\n * Build, validate, and topo-sort a contributor pipeline.\n *\n * Pipeline:\n *\n * 1. Group sources by precedence (method > class > module > adapter > global).\n * 2. Within each precedence level, two contributors with the same `key`\n * raise {@link DuplicateContributorError}.\n * 3. Across levels, the highest-precedence contributor wins each key\n * (lower-precedence duplicates are silently dropped — that is the\n * documented override mechanism, not an error).\n * 4. Every key referenced by `dependsOn` must be produced by the\n * surviving set, otherwise {@link MissingContributorError}.\n * 5. Kahn's algorithm topo-sorts the survivors. Any nodes left after\n * the queue drains form a cycle → {@link ContributorCycleError}\n * with a reconstructed path.\n *\n * Pure function — no DI, no I/O. Safe to call once at startup per\n * route and cache the result.\n */\nexport function buildPipeline(\n sources: readonly SourcedRegistration[],\n options: BuildPipelineOptions = {},\n): ContributorPipeline {\n const route = options.route\n\n // Step 1: group by source.\n const bySource = new Map<ContributorSource, SourcedRegistration[]>()\n for (const src of PRECEDENCE_ORDER) bySource.set(src, [])\n for (const entry of sources) bySource.get(entry.source)!.push(entry)\n\n // Step 2 + 3: dedup with precedence. We walk highest precedence first,\n // claim its keys, and drop lower-precedence rows that conflict.\n const winners = new Map<string, SourcedRegistration>()\n for (const src of PRECEDENCE_ORDER) {\n const rows = bySource.get(src)!\n const keysAtThisLevel = new Map<string, SourcedRegistration[]>()\n for (const row of rows) {\n const key = row.registration.key\n const existing = keysAtThisLevel.get(key) ?? []\n existing.push(row)\n keysAtThisLevel.set(key, existing)\n }\n for (const [key, rowsForKey] of keysAtThisLevel) {\n if (rowsForKey.length > 1) {\n const labels = rowsForKey.map((r, i) => r.label ?? `${src}#${i}`)\n throw new DuplicateContributorError(key, labels)\n }\n if (!winners.has(key)) winners.set(key, rowsForKey[0])\n }\n }\n\n const survivors = [...winners.values()].map((r) => r.registration)\n const allKeys = new Set(survivors.map((r) => r.key))\n\n // Step 4: dependsOn validation.\n for (const reg of survivors) {\n for (const dep of reg.dependsOn) {\n if (!allKeys.has(dep)) {\n throw new MissingContributorError(dep, reg.key, route, reg.definedAt)\n }\n }\n }\n\n // Step 5: Kahn topo-sort.\n const sorted = topoSort(survivors, route)\n\n return Object.freeze({\n contributors: Object.freeze(sorted),\n keys: allKeys,\n })\n}\n\n/**\n * Kahn's algorithm with deterministic ordering on ties.\n *\n * Nodes with no remaining incoming edges enter the queue in input\n * order. Cycles surface as nodes still present after the queue\n * drains; we reconstruct one cycle path for diagnostics.\n */\nfunction topoSort(\n registrations: readonly AnyContributorRegistration[],\n route: string | undefined,\n): AnyContributorRegistration[] {\n // indegree[key] = number of unresolved dependsOn entries for key\n const indegree = new Map<string, number>()\n // dependents[depKey] = list of registrations that depend on depKey\n const dependents = new Map<string, AnyContributorRegistration[]>()\n const byKey = new Map<string, AnyContributorRegistration>()\n\n for (const reg of registrations) {\n indegree.set(reg.key, reg.dependsOn.length)\n byKey.set(reg.key, reg)\n }\n for (const reg of registrations) {\n for (const dep of reg.dependsOn) {\n const list = dependents.get(dep) ?? []\n list.push(reg)\n dependents.set(dep, list)\n }\n }\n\n // Initial queue: registrations with zero indegree, preserving input order.\n const queue: AnyContributorRegistration[] = []\n for (const reg of registrations) {\n if (indegree.get(reg.key) === 0) queue.push(reg)\n }\n\n const sorted: AnyContributorRegistration[] = []\n while (queue.length > 0) {\n const node = queue.shift()!\n sorted.push(node)\n for (const dependent of dependents.get(node.key) ?? []) {\n const next = (indegree.get(dependent.key) ?? 0) - 1\n indegree.set(dependent.key, next)\n if (next === 0) queue.push(dependent)\n }\n }\n\n if (sorted.length !== registrations.length) {\n const remaining = registrations.filter((r) => (indegree.get(r.key) ?? 0) > 0)\n throw new ContributorCycleError(reconstructCycle(remaining, byKey), route)\n }\n\n return sorted\n}\n\n/**\n * Reconstruct one cycle path from the residual graph.\n *\n * Walks from any remaining node, following its first dependency that's\n * still in the residual set, until we revisit a node. The path returned\n * starts and ends with the same key so the message reads naturally:\n * `tenant → project → tenant`.\n */\nfunction reconstructCycle(\n residual: readonly AnyContributorRegistration[],\n byKey: ReadonlyMap<string, AnyContributorRegistration>,\n): string[] {\n const residualKeys = new Set(residual.map((r) => r.key))\n const start = residual[0].key\n const path: string[] = [start]\n const seen = new Set<string>([start])\n let cursor: string = start\n\n while (true) {\n const reg = byKey.get(cursor)!\n const nextDep = reg.dependsOn.find((d) => residualKeys.has(d))\n if (nextDep === undefined) break\n if (seen.has(nextDep)) {\n // Trim the prefix that isn't part of the cycle.\n const cycleStart = path.indexOf(nextDep)\n return [...path.slice(cycleStart), nextDep]\n }\n path.push(nextDep)\n seen.add(nextDep)\n cursor = nextDep\n }\n\n // Fallback — couldn't reconstruct a strict cycle, return what we walked.\n return path\n}\n","import type { Container } from './container'\nimport type { ContributorRegistration } from './context-decorator'\nimport type { ExecutionContext } from './execution-context'\nimport type { ContributorPipeline } from './contributor-pipeline'\n\nexport interface RunContributorsOptions {\n /** Pre-built, validated, topo-sorted pipeline. */\n pipeline: ContributorPipeline\n /** The execution context contributors read from and write into. */\n ctx: ExecutionContext\n /** DI container used to resolve declared `deps`. */\n container: Container\n}\n\n/**\n * Execute a built {@link ContributorPipeline} against an {@link ExecutionContext}.\n *\n * Sequential — one contributor at a time, in the topo order chosen at\n * pipeline-build time. Per `architecture.md` §20.10, V1 does not run\n * contributors in parallel even within the same topo level; that is a\n * V2 follow-up gated on profiling.\n *\n * ## Error matrix (architecture.md §20.9)\n *\n * | resolve outcome | optional | onError defined | runner behaviour |\n * |-----------------|----------|-----------------|--------------------------------------------------|\n * | throws | true | — | skip; `ctx.get(key)` remains undefined |\n * | throws | false | yes | call hook; returned value (if any) stored |\n * | throws | false | no | propagate the original error |\n * | hook throws | — | yes | propagate the new error |\n * | hook returns | — | yes | undefined/void → skip; value → `ctx.set(key, v)` |\n * | resolves | — | — | `ctx.set(key, value)` |\n *\n * Container.resolve() throwing while building `deps` is treated as a\n * resolve-side throw and flows through the same matrix — adopters who\n * need different behaviour for missing-DI vs. data-lookup failures can\n * wrap the container access inside their own `resolve()`.\n */\nexport async function runContributors(options: RunContributorsOptions): Promise<void> {\n const { pipeline, ctx, container } = options\n\n for (const reg of pipeline.contributors) {\n await runOne(reg, ctx, container)\n }\n}\n\nasync function runOne(\n reg: ContributorRegistration,\n ctx: ExecutionContext,\n container: Container,\n): Promise<void> {\n let value: unknown\n try {\n const deps = resolveDeps(reg, container)\n value = await reg.resolve(ctx, deps)\n } catch (err) {\n if (reg.optional) return\n if (reg.onError) {\n // Hook throws are unconditionally propagated — there is no\n // second-chance hook. Returning undefined/void from the hook\n // means \"skip\"; returning a value means \"use this instead\".\n const replacement = await reg.onError(err, ctx)\n if (replacement === undefined) return\n ctx.set(reg.key, replacement as never)\n return\n }\n throw err\n }\n ctx.set(reg.key, value as never)\n}\n\n/**\n * Resolve a contributor's declared dependencies through the container,\n * preserving the property names from the spec so `resolve(ctx, deps)`\n * receives exactly the shape the user wrote.\n *\n * Empty `deps` produces an empty object — `resolve()` always sees an\n * object, never `undefined`.\n */\nfunction resolveDeps(reg: ContributorRegistration, container: Container): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, token] of Object.entries(reg.deps)) {\n out[key] = container.resolve(token as never)\n }\n return out\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"contributor-runner-C4qFEVIz.mjs","names":[],"sources":["../src/core/context-errors.ts","../src/core/contributor-pipeline.ts","../src/core/contributor-runner.ts"],"sourcesContent":["/**\n * Errors thrown by the Context Contributor pipeline.\n *\n * All three are startup-time errors raised during pipeline build (topo-sort\n * + validation), not per-request — a misconfigured app fails to boot rather\n * than fails a request. Per the locked design in `architecture.md` §20.4,\n * the pipeline is validated once when each route is mounted and the\n * resolved order is cached on the route.\n */\n\n/**\n * A contributor declared `dependsOn: ['<key>']` but no other contributor\n * (at any precedence level) produces that key for the same route.\n *\n * @example\n * ```text\n * MissingContributorError: Missing context contributor 'tenant' required by 'project' on route GET /projects/:id\n * declared at src/contributors/load-project.ts:42:18\n * ```\n */\nexport class MissingContributorError extends Error {\n readonly key: string\n readonly dependent: string\n readonly route?: string\n readonly dependentDefinedAt?: string\n\n constructor(key: string, dependent: string, route?: string, dependentDefinedAt?: string) {\n const where = route ? ` on route ${route}` : ''\n const declaredAt = formatDeclaredAt(dependentDefinedAt)\n super(`Missing context contributor '${key}' required by '${dependent}'${where}${declaredAt}`)\n this.name = 'MissingContributorError'\n this.key = key\n this.dependent = dependent\n this.route = route\n this.dependentDefinedAt = dependentDefinedAt\n }\n}\n\n/**\n * Slice the first non-framework frame off a captured `Error.stack`\n * and format it as `\\n declared at <frame>` for inclusion in\n * boot-time error messages. Returns `''` when no useful frame is\n * available (hand-rolled registrations, missing stacks).\n */\nfunction formatDeclaredAt(stack: string | undefined): string {\n if (!stack) return ''\n const lines = stack.split('\\n')\n for (const line of lines) {\n const trimmed = line.trim()\n if (!trimmed.startsWith('at ')) continue\n // Skip frames inside this file or the decorator factory — the\n // adopter's call site is the next frame after those.\n if (trimmed.includes('context-decorator.')) continue\n if (trimmed.includes('context-errors.')) continue\n return `\\n declared at ${trimmed.replace(/^at /, '')}`\n }\n return ''\n}\n\n/**\n * Topo-sort detected a cycle in `dependsOn` declarations. The `cycle` field\n * lists the keys in the order they form the loop, with the first key\n * repeated at the end so the path reads naturally.\n *\n * @example\n * ```text\n * ContributorCycleError: Cycle in context contributors on route GET /projects/:id: tenant → project → tenant\n * ```\n */\nexport class ContributorCycleError extends Error {\n readonly cycle: readonly string[]\n readonly route?: string\n\n constructor(cycle: readonly string[], route?: string) {\n const where = route ? ` on route ${route}` : ''\n super(`Cycle in context contributors${where}: ${cycle.join(' → ')}`)\n this.name = 'ContributorCycleError'\n this.cycle = Object.freeze([...cycle])\n this.route = route\n }\n}\n\n/**\n * Two contributors at the same precedence level both produce the same key\n * for the same route. Precedence resolution (method > class > module >\n * adapter > global, per architecture.md §20.4) silently drops\n * lower-precedence duplicates; this error fires only when ties occur\n * within a single level — e.g., two method-level decorators declaring\n * `key: 'tenant'`.\n *\n * `sources` is best-effort identifying labels for each conflicting\n * registration (typically the decorator's call site or registering\n * module/adapter name).\n *\n * @example\n * ```text\n * DuplicateContributorError: Duplicate context contributor for key 'tenant' at the same precedence level. Sources: LoadTenantFromHeader, LoadTenantFromSubdomain\n * ```\n */\nexport class DuplicateContributorError extends Error {\n readonly key: string\n readonly sources: readonly string[]\n\n constructor(key: string, sources: readonly string[]) {\n super(\n `Duplicate context contributor for key '${key}' at the same precedence level. ` +\n `Sources: ${sources.join(', ')}`,\n )\n this.name = 'DuplicateContributorError'\n this.key = key\n this.sources = Object.freeze([...sources])\n }\n}\n","import type { AnyContributorRegistration } from './context-decorator'\nimport {\n ContributorCycleError,\n DuplicateContributorError,\n MissingContributorError,\n} from './context-errors'\n\n/**\n * Where a contributor was registered, ordered by precedence\n * (lower index wins). Mirrors `architecture.md` §20.4.\n *\n * Levels (highest → lowest precedence):\n * - `'method'` — `@LoadX` on a controller method.\n * - `'class'` — `@LoadX` on a controller class.\n * - `'module'` — returned by `AppModule.contributors?()`. Per-module scope.\n * - `'adapter'` — returned by `AppAdapter.contributors?()`. Cross-cutting, applies\n * to every module's routes.\n * - `'global'` — `ApplicationOptions.contributors`. App-wide default.\n */\nexport type ContributorSource = 'method' | 'class' | 'module' | 'adapter' | 'global'\n\n/** Precedence-ranked source list. Index = precedence; method (0) wins ties. */\nconst PRECEDENCE_ORDER: readonly ContributorSource[] = [\n 'method',\n 'class',\n 'module',\n 'adapter',\n 'global',\n]\n\n/**\n * Input row to {@link buildPipeline}. Pairs a registration with its origin\n * so dedup can apply the precedence rule and diagnostic errors can name\n * the offending site.\n *\n * `registration` is the type-erased {@link AnyContributorRegistration}\n * — the runner doesn't care about each registration's narrow `D` /\n * `Ctx` shape, and adopters frequently mix HTTP / transport-agnostic\n * contributors in the same `externalSources` / `contributors()`\n * array. The narrow generics from `defineContextDecorator` survive\n * at the *definition* site (where `resolve` is typed); they're erased\n * here so collections compose without `as any` casts.\n */\nexport interface SourcedRegistration {\n registration: AnyContributorRegistration\n source: ContributorSource\n /** Human label surfaced in DuplicateContributorError messages. */\n label?: string\n}\n\n/**\n * Resolved, validated, topo-sorted pipeline ready for execution.\n *\n * `contributors` is in execution order — every entry's `dependsOn`\n * keys appear earlier. `keys` is the set of all keys this pipeline\n * produces; the runner uses it for fast existence checks and the\n * builder uses it for missing-dep validation.\n */\nexport interface ContributorPipeline {\n readonly contributors: readonly AnyContributorRegistration[]\n readonly keys: ReadonlySet<string>\n}\n\nexport interface BuildPipelineOptions {\n /** Optional route identifier surfaced in diagnostic errors (e.g., 'GET /projects/:id'). */\n route?: string\n}\n\n/**\n * Build, validate, and topo-sort a contributor pipeline.\n *\n * Pipeline:\n *\n * 1. Group sources by precedence (method > class > module > adapter > global).\n * 2. Within each precedence level, two contributors with the same `key`\n * raise {@link DuplicateContributorError}.\n * 3. Across levels, the highest-precedence contributor wins each key\n * (lower-precedence duplicates are silently dropped — that is the\n * documented override mechanism, not an error).\n * 4. Every key referenced by `dependsOn` must be produced by the\n * surviving set, otherwise {@link MissingContributorError}.\n * 5. Kahn's algorithm topo-sorts the survivors. Any nodes left after\n * the queue drains form a cycle → {@link ContributorCycleError}\n * with a reconstructed path.\n *\n * Pure function — no DI, no I/O. Safe to call once at startup per\n * route and cache the result.\n */\nexport function buildPipeline(\n sources: readonly SourcedRegistration[],\n options: BuildPipelineOptions = {},\n): ContributorPipeline {\n const route = options.route\n\n // Step 1: group by source.\n const bySource = new Map<ContributorSource, SourcedRegistration[]>()\n for (const src of PRECEDENCE_ORDER) bySource.set(src, [])\n for (const entry of sources) bySource.get(entry.source)!.push(entry)\n\n // Step 2 + 3: dedup with precedence. We walk highest precedence first,\n // claim its keys, and drop lower-precedence rows that conflict.\n const winners = new Map<string, SourcedRegistration>()\n for (const src of PRECEDENCE_ORDER) {\n const rows = bySource.get(src)!\n const keysAtThisLevel = new Map<string, SourcedRegistration[]>()\n for (const row of rows) {\n const key = row.registration.key\n const existing = keysAtThisLevel.get(key) ?? []\n existing.push(row)\n keysAtThisLevel.set(key, existing)\n }\n for (const [key, rowsForKey] of keysAtThisLevel) {\n if (rowsForKey.length > 1) {\n const labels = rowsForKey.map((r, i) => r.label ?? `${src}#${i}`)\n throw new DuplicateContributorError(key, labels)\n }\n if (!winners.has(key)) winners.set(key, rowsForKey[0])\n }\n }\n\n const survivors = [...winners.values()].map((r) => r.registration)\n const allKeys = new Set(survivors.map((r) => r.key))\n\n // Step 4: dependsOn validation.\n for (const reg of survivors) {\n for (const dep of reg.dependsOn) {\n if (!allKeys.has(dep)) {\n throw new MissingContributorError(dep, reg.key, route, reg.definedAt)\n }\n }\n }\n\n // Step 5: Kahn topo-sort.\n const sorted = topoSort(survivors, route)\n\n return Object.freeze({\n contributors: Object.freeze(sorted),\n keys: allKeys,\n })\n}\n\n/**\n * Kahn's algorithm with deterministic ordering on ties.\n *\n * Nodes with no remaining incoming edges enter the queue in input\n * order. Cycles surface as nodes still present after the queue\n * drains; we reconstruct one cycle path for diagnostics.\n */\nfunction topoSort(\n registrations: readonly AnyContributorRegistration[],\n route: string | undefined,\n): AnyContributorRegistration[] {\n // indegree[key] = number of unresolved dependsOn entries for key\n const indegree = new Map<string, number>()\n // dependents[depKey] = list of registrations that depend on depKey\n const dependents = new Map<string, AnyContributorRegistration[]>()\n const byKey = new Map<string, AnyContributorRegistration>()\n\n for (const reg of registrations) {\n indegree.set(reg.key, reg.dependsOn.length)\n byKey.set(reg.key, reg)\n }\n for (const reg of registrations) {\n for (const dep of reg.dependsOn) {\n const list = dependents.get(dep) ?? []\n list.push(reg)\n dependents.set(dep, list)\n }\n }\n\n // Initial queue: registrations with zero indegree, preserving input order.\n const queue: AnyContributorRegistration[] = []\n for (const reg of registrations) {\n if (indegree.get(reg.key) === 0) queue.push(reg)\n }\n\n const sorted: AnyContributorRegistration[] = []\n while (queue.length > 0) {\n const node = queue.shift()!\n sorted.push(node)\n for (const dependent of dependents.get(node.key) ?? []) {\n const next = (indegree.get(dependent.key) ?? 0) - 1\n indegree.set(dependent.key, next)\n if (next === 0) queue.push(dependent)\n }\n }\n\n if (sorted.length !== registrations.length) {\n const remaining = registrations.filter((r) => (indegree.get(r.key) ?? 0) > 0)\n throw new ContributorCycleError(reconstructCycle(remaining, byKey), route)\n }\n\n return sorted\n}\n\n/**\n * Reconstruct one cycle path from the residual graph.\n *\n * Walks from any remaining node, following its first dependency that's\n * still in the residual set, until we revisit a node. The path returned\n * starts and ends with the same key so the message reads naturally:\n * `tenant → project → tenant`.\n */\nfunction reconstructCycle(\n residual: readonly AnyContributorRegistration[],\n byKey: ReadonlyMap<string, AnyContributorRegistration>,\n): string[] {\n const residualKeys = new Set(residual.map((r) => r.key))\n const start = residual[0].key\n const path: string[] = [start]\n const seen = new Set<string>([start])\n let cursor: string = start\n\n while (true) {\n const reg = byKey.get(cursor)!\n const nextDep = reg.dependsOn.find((d) => residualKeys.has(d))\n if (nextDep === undefined) break\n if (seen.has(nextDep)) {\n // Trim the prefix that isn't part of the cycle.\n const cycleStart = path.indexOf(nextDep)\n return [...path.slice(cycleStart), nextDep]\n }\n path.push(nextDep)\n seen.add(nextDep)\n cursor = nextDep\n }\n\n // Fallback — couldn't reconstruct a strict cycle, return what we walked.\n return path\n}\n","import type { Container } from './container'\nimport type { ContributorRegistration } from './context-decorator'\nimport type { ExecutionContext } from './execution-context'\nimport type { ContributorPipeline } from './contributor-pipeline'\n\nexport interface RunContributorsOptions {\n /** Pre-built, validated, topo-sorted pipeline. */\n pipeline: ContributorPipeline\n /** The execution context contributors read from and write into. */\n ctx: ExecutionContext\n /** DI container used to resolve declared `deps`. */\n container: Container\n}\n\n/**\n * Execute a built {@link ContributorPipeline} against an {@link ExecutionContext}.\n *\n * Sequential — one contributor at a time, in the topo order chosen at\n * pipeline-build time. Per `architecture.md` §20.10, V1 does not run\n * contributors in parallel even within the same topo level; that is a\n * V2 follow-up gated on profiling.\n *\n * ## Error matrix (architecture.md §20.9)\n *\n * | resolve outcome | optional | onError defined | runner behaviour |\n * |-----------------|----------|-----------------|--------------------------------------------------|\n * | throws | true | — | skip; `ctx.get(key)` remains undefined |\n * | throws | false | yes | call hook; returned value (if any) stored |\n * | throws | false | no | propagate the original error |\n * | hook throws | — | yes | propagate the new error |\n * | hook returns | — | yes | undefined/void → skip; value → `ctx.set(key, v)` |\n * | resolves | — | — | `ctx.set(key, value)` |\n *\n * Container.resolve() throwing while building `deps` is treated as a\n * resolve-side throw and flows through the same matrix — adopters who\n * need different behaviour for missing-DI vs. data-lookup failures can\n * wrap the container access inside their own `resolve()`.\n */\nexport async function runContributors(options: RunContributorsOptions): Promise<void> {\n const { pipeline, ctx, container } = options\n\n for (const reg of pipeline.contributors) {\n await runOne(reg, ctx, container)\n }\n}\n\nasync function runOne(\n reg: ContributorRegistration,\n ctx: ExecutionContext,\n container: Container,\n): Promise<void> {\n let value: unknown\n try {\n const deps = resolveDeps(reg, container)\n value = await reg.resolve(ctx, deps)\n } catch (err) {\n if (reg.optional) return\n if (reg.onError) {\n // Hook throws are unconditionally propagated — there is no\n // second-chance hook. Returning undefined/void from the hook\n // means \"skip\"; returning a value means \"use this instead\".\n const replacement = await reg.onError(err, ctx)\n if (replacement === undefined) return\n ctx.set(reg.key, replacement as never)\n return\n }\n throw err\n }\n ctx.set(reg.key, value as never)\n}\n\n/**\n * Resolve a contributor's declared dependencies through the container,\n * preserving the property names from the spec so `resolve(ctx, deps)`\n * receives exactly the shape the user wrote.\n *\n * Empty `deps` produces an empty object — `resolve()` always sees an\n * object, never `undefined`.\n */\nfunction resolveDeps(reg: ContributorRegistration, container: Container): Record<string, unknown> {\n const out: Record<string, unknown> = {}\n for (const [key, token] of Object.entries(reg.deps)) {\n out[key] = container.resolve(token as never)\n }\n return out\n}\n"],"mappings":"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"}
|
package/dist/core/adapter.d.mts
CHANGED
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
|
|
2
|
-
import { a as MiddlewarePath, i as AppAdapterClass, n as AdapterMiddleware, o as MiddlewarePhase, r as AppAdapter, t as AdapterContext } from "../adapter-
|
|
2
|
+
import { a as MiddlewarePath, i as AppAdapterClass, n as AdapterMiddleware, o as MiddlewarePhase, r as AppAdapter, t as AdapterContext } from "../adapter-stSpsUHs.mjs";
|
|
3
3
|
export { AdapterContext, AdapterMiddleware, AppAdapter, AppAdapterClass, MiddlewarePath, MiddlewarePhase };
|
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 v6.
|
|
2
|
+
* @forinda/kickjs v6.3.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-C522k8AX.mjs";export{Container,tokenName};
|
package/dist/core/cron.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v6.
|
|
2
|
+
* @forinda/kickjs v6.3.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-W54KhZoU.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
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
|
|
2
|
-
import { A as Service, C as Post, D as Put, E as PropertyOrParameterDecorator, M as normalizeApiQueryParamsConfig, N as withBuilder, O as Repository, S as Patch, T as PreDestroy, _ as Get, a as Autowired, b as Middleware, c as ColumnApiQueryParamsConfig, d as Delete, f as Env, g as FileUploadConfig, h as FileUpload, i as AssetKey, j as Value, k as RouteDefinition, l as Component, m as FileTypeFilter, n as ApiQueryParamsConfig, o as BaseUploadOptions, p as EnvKey, r as Asset, s as Builder, t as ApiQueryParams, u as Controller, v as Inject, w as PostConstruct, x as MiddlewareHandler, y as Injectable } from "../decorators-
|
|
2
|
+
import { A as Service, C as Post, D as Put, E as PropertyOrParameterDecorator, M as normalizeApiQueryParamsConfig, N as withBuilder, O as Repository, S as Patch, T as PreDestroy, _ as Get, a as Autowired, b as Middleware, c as ColumnApiQueryParamsConfig, d as Delete, f as Env, g as FileUploadConfig, h as FileUpload, i as AssetKey, j as Value, k as RouteDefinition, l as Component, m as FileTypeFilter, n as ApiQueryParamsConfig, o as BaseUploadOptions, p as EnvKey, r as Asset, s as Builder, t as ApiQueryParams, u as Controller, v as Inject, w as PostConstruct, x as MiddlewareHandler, y as Injectable } from "../decorators-wEJBqXHg.mjs";
|
|
3
3
|
export { ApiQueryParams, ApiQueryParamsConfig, Asset, AssetKey, Autowired, BaseUploadOptions, Builder, ColumnApiQueryParamsConfig, Component, Controller, Delete, Env, EnvKey, FileTypeFilter, FileUpload, FileUploadConfig, Get, Inject, Injectable, Middleware, MiddlewareHandler, Patch, Post, PostConstruct, PreDestroy, PropertyOrParameterDecorator, Put, Repository, RouteDefinition, Service, Value, normalizeApiQueryParamsConfig, withBuilder };
|
package/dist/core/decorators.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v6.
|
|
2
|
+
* @forinda/kickjs v6.3.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{METADATA}from"./interfaces.mjs";import{t as Container}from"../container-
|
|
11
|
+
import{METADATA}from"./interfaces.mjs";import{t as Container}from"../container-C522k8AX.mjs";import{c as pushClassMeta,d as setInMetaMap,f as setInMetaRecord,l as pushMethodMeta,p as setMethodMeta,u as setClassMeta}from"../metadata-W54KhZoU.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 PreDestroy(){return(target,propertyKey)=>{setClassMeta(METADATA.PRE_DESTROY,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,PreDestroy,Put,Repository,Service,Value,normalizeApiQueryParamsConfig,withBuilder};
|
|
12
12
|
//# sourceMappingURL=decorators.mjs.map
|