@forinda/kickjs 5.4.0 → 5.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{adapter-C1w_xt08.d.mts → adapter-0r2uRCD3.d.mts} +2 -2
- package/dist/{adapter-C1w_xt08.d.mts.map → adapter-0r2uRCD3.d.mts.map} +1 -1
- package/dist/app-module-CzlIIjO3.d.mts +92 -0
- package/dist/app-module-CzlIIjO3.d.mts.map +1 -0
- package/dist/{application-BTRI4aBd.d.mts → application-CaEYca9E.d.mts} +34 -7
- package/dist/application-CaEYca9E.d.mts.map +1 -0
- package/dist/application-YJZ2frWX.mjs +12 -0
- package/dist/application-YJZ2frWX.mjs.map +1 -0
- package/dist/{bootstrap-B4YrBipl.d.mts → bootstrap-Db8k2-21.d.mts} +2 -2
- package/dist/{bootstrap-B4YrBipl.d.mts.map → bootstrap-Db8k2-21.d.mts.map} +1 -1
- package/dist/{bootstrap-B7j_-dOg.mjs → bootstrap-UCZoqbbW.mjs} +3 -3
- package/dist/{bootstrap-B7j_-dOg.mjs.map → bootstrap-UCZoqbbW.mjs.map} +1 -1
- package/dist/config/index.mjs +2 -2
- package/dist/{config-Dk5RVTnD.mjs → config-D6VaNpwo.mjs} +3 -3
- package/dist/{config-Dk5RVTnD.mjs.map → config-D6VaNpwo.mjs.map} +1 -1
- package/dist/container-D0v17gF8.mjs +12 -0
- package/dist/{container-DArhTDQQ.mjs.map → container-D0v17gF8.mjs.map} +1 -1
- package/dist/context-CbX_T0XC.mjs +12 -0
- package/dist/context-CbX_T0XC.mjs.map +1 -0
- package/dist/{context-BAdAV-zO.d.mts → context-aocg9gO-.d.mts} +36 -1
- package/dist/context-aocg9gO-.d.mts.map +1 -0
- package/dist/{context-decorator-Bca_BoRK.d.mts → context-decorator-BgdDd-6X.d.mts} +19 -4
- package/dist/{context-decorator-Bca_BoRK.d.mts.map → context-decorator-BgdDd-6X.d.mts.map} +1 -1
- package/dist/core/adapter.d.mts +1 -1
- package/dist/core/adapter.mjs +1 -1
- package/dist/core/app-module.d.mts +2 -2
- package/dist/core/app-module.mjs +1 -1
- package/dist/core/cache.mjs +1 -1
- package/dist/core/container.mjs +2 -2
- package/dist/core/cron.mjs +2 -2
- package/dist/core/decorators.mjs +2 -2
- package/dist/core/decorators.mjs.map +1 -1
- package/dist/core/errors.mjs +1 -1
- package/dist/core/interfaces.mjs +1 -1
- package/dist/core/logger.mjs +1 -1
- package/dist/core/path.mjs +1 -1
- package/dist/core/path.mjs.map +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-DvTHNOay.mjs +13 -0
- package/dist/core-DvTHNOay.mjs.map +1 -0
- package/dist/{env-BL0xOTyi.mjs → env-yTMEMpV6.mjs} +3 -3
- package/dist/{env-BL0xOTyi.mjs.map → env-yTMEMpV6.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 +1 -1
- package/dist/http/context.mjs +2 -2
- package/dist/http/middleware/cors.mjs +1 -1
- package/dist/http/middleware/csrf.mjs +1 -1
- package/dist/http/middleware/error-handler.mjs +2 -2
- package/dist/http/middleware/helmet.mjs +1 -1
- package/dist/http/middleware/rate-limit.mjs +1 -1
- package/dist/http/middleware/request-id.mjs +1 -1
- package/dist/http/middleware/request-logger.mjs +2 -2
- package/dist/http/middleware/session.mjs +1 -1
- package/dist/http/middleware/session.mjs.map +1 -1
- package/dist/http/middleware/spa.d.mts +1 -1
- package/dist/http/middleware/spa.mjs +2 -2
- package/dist/http/middleware/upload.mjs +1 -1
- package/dist/http/middleware/upload.mjs.map +1 -1
- package/dist/http/middleware/validate.mjs +2 -2
- package/dist/http/middleware/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/router-builder.mjs.map +1 -1
- package/dist/{index-DvZfBNIf.d.mts → index-D-Wb23e6.d.mts} +264 -86
- package/dist/index-D-Wb23e6.d.mts.map +1 -0
- package/dist/index.d.mts +12 -12
- package/dist/index.mjs +2 -2
- package/dist/index.mjs.map +1 -1
- package/dist/{metadata-Dn6kNT82.mjs → metadata-IHiynOCv.mjs} +2 -2
- package/dist/{metadata-Dn6kNT82.mjs.map → metadata-IHiynOCv.mjs.map} +1 -1
- package/dist/{plugin-DF8Xfv9Z.d.mts → plugin-JMkG7soG.d.mts} +102 -8
- package/dist/plugin-JMkG7soG.d.mts.map +1 -0
- package/dist/{query-B7n5isk_.mjs → query-CQU-Hyiv.mjs} +2 -2
- package/dist/{query-B7n5isk_.mjs.map → query-CQU-Hyiv.mjs.map} +1 -1
- package/dist/{router-builder-C7sXk_FG.d.mts → router-builder-CaQ629tw.d.mts} +2 -2
- package/dist/{router-builder-C7sXk_FG.d.mts.map → router-builder-CaQ629tw.d.mts.map} +1 -1
- package/dist/{spa-e4FIS2RB.d.mts → spa-CreyQc4N.d.mts} +2 -2
- package/dist/{spa-e4FIS2RB.d.mts.map → spa-CreyQc4N.d.mts.map} +1 -1
- package/dist/{views-oKRSw0Op.d.mts → views-BMhrZsH9.d.mts} +4 -4
- package/dist/{views-oKRSw0Op.d.mts.map → views-BMhrZsH9.d.mts.map} +1 -1
- package/package.json +2 -2
- package/dist/app-module-D6zL_V5S.d.mts +0 -50
- package/dist/app-module-D6zL_V5S.d.mts.map +0 -1
- package/dist/application-BTRI4aBd.d.mts.map +0 -1
- package/dist/application-mir5CSWO.mjs +0 -12
- package/dist/application-mir5CSWO.mjs.map +0 -1
- package/dist/container-DArhTDQQ.mjs +0 -12
- package/dist/context-BAdAV-zO.d.mts.map +0 -1
- package/dist/context-CIoXqpAX.mjs +0 -12
- package/dist/context-CIoXqpAX.mjs.map +0 -1
- package/dist/core-BQZyB8Wv.mjs +0 -12
- package/dist/core-BQZyB8Wv.mjs.map +0 -1
- package/dist/index-DvZfBNIf.d.mts.map +0 -1
- package/dist/plugin-DF8Xfv9Z.d.mts.map +0 -1
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"env-
|
|
1
|
+
{"version":3,"file":"env-yTMEMpV6.mjs","names":[],"sources":["../src/config/env.ts"],"sourcesContent":["import { z } from 'zod'\nimport { Container } from '../core/container'\nimport type { EnvKey } from '../core/decorators'\n\n/**\n * Lazily load `.env` via dotenv if it is installed in the consumer app.\n * `dotenv` is an **optional peer dependency** of `@forinda/kickjs` —\n * apps that want `.env` file support install it themselves (the CLI\n * generators add it to every scaffolded `package.json`). Apps that\n * inject env via the shell, Docker, or a secret manager can skip it\n * entirely and `process.env` is read as-is.\n *\n * We call `dotenv.config()` directly rather than `require('dotenv/config')`\n * so that a re-evaluation of this module (e.g. Vite SSR full-reload after\n * editing `.env`) can re-read the file. `dotenv/config` is a side-effect\n * shim that CJS caches aggressively, so the second `require` is a no-op\n * and new keys never reach `process.env` — the root cause of the\n * \"DATABASE_URL undefined until restart\" bug on Windows dev.\n */\nfunction tryLoadDotenv(): void {\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n const dotenv = require('dotenv')\n // `quiet: true` suppresses dotenv 17's tip banner that goes to stdout.\n // Required for CLI tools whose stdout is parsed by callers (`kick explain\n // --json`, etc) — the banner would otherwise corrupt the JSON output.\n dotenv.config({ override: false, quiet: true })\n } catch {\n // dotenv not installed — fall through, env vars must come from\n // the environment directly. This is a valid deployment style.\n }\n}\ntryLoadDotenv()\n\n/**\n * Base environment schema with common server variables.\n * Users extend this with their own application-specific vars.\n */\nexport const baseEnvSchema = z.object({\n // Server\n PORT: z.coerce.number().default(3000),\n NODE_ENV: z.enum(['development', 'production', 'test']).default('development'),\n LOG_LEVEL: z.string().default('info'),\n})\n\n/** Cached env config to avoid re-parsing — keyed by schema reference */\nlet cachedEnv: any = null\nlet cachedSchema: any = null\n\n/**\n * Define a custom env schema. The base schema (`PORT`, `NODE_ENV`,\n * `LOG_LEVEL`) is always merged in, so users never need to remember to\n * extend it themselves — both styles below produce the same result and\n * both surface the base keys in `KickEnv`.\n *\n * Returns a Zod schema that validates `process.env`.\n *\n * @example\n * ```ts\n * // Style A — return a fresh object; base fields are merged automatically\n * export default defineEnv(() =>\n * z.object({\n * DATABASE_URL: z.string().url(),\n * JWT_SECRET: z.string().min(32),\n * })\n * )\n *\n * // Style B — explicitly extend `base` (still works, identical result)\n * export default defineEnv((base) =>\n * base.extend({\n * DATABASE_URL: z.string().url(),\n * })\n * )\n * ```\n *\n * User-defined keys override base keys when they collide, so a project\n * can re-type `PORT` or tighten `NODE_ENV` without losing the rest of\n * the base shape.\n */\nexport function defineEnv<T extends z.ZodRawShape>(\n extend: (base: typeof baseEnvSchema) => z.ZodObject<T>,\n): z.ZodObject<typeof baseEnvSchema.shape & T> {\n const userSchema = extend(baseEnvSchema)\n // Always merge the base in. `extend` lets the user's shape override\n // any colliding base keys, which preserves the escape hatch for\n // projects that want to re-type `PORT` etc.\n return baseEnvSchema.extend(userSchema.shape) as z.ZodObject<typeof baseEnvSchema.shape & T>\n}\n\n/**\n * Parse and validate `process.env` against a Zod schema. Caches result per schema.\n *\n * **No-arg behaviour is sticky.** The first time `loadEnv(schema)` is called\n * with an extended schema, the cache holds that schema. Subsequent\n * `loadEnv()` calls (no arg) will reuse the cached extended schema rather\n * than falling back to `baseEnvSchema`.\n *\n * Stickiness matters because `ConfigService` and the `@Value()` decorator\n * both read through the no-arg form internally. Without it, resolving\n * either of them after a `loadEnv(extendedSchema)` call would silently\n * downgrade the env to the base shape and drop every user-defined key.\n *\n * To force a re-parse with a different schema, pass the new schema explicitly.\n * To start fresh, call `resetEnvCache()` (full reset) or `reloadEnv()`\n * (re-parse against the same schema after a `.env` file change).\n *\n * **Typing:**\n * - With an explicit schema → returns `z.infer<typeof schema>`.\n * - With no schema and `KickEnv` populated by typegen → returns\n * `KickEnv` (the project-wide typed env shape).\n * - With no schema and `KickEnv` empty → returns the base `Env`.\n */\nexport function loadEnv<T extends z.ZodObject<any>>(schema: T): z.infer<T>\nexport function loadEnv(): [EnvKey] extends [never] ? Env : KickEnv\nexport function loadEnv<T extends z.ZodObject<any>>(schema?: T): any {\n // Sticky: when called with no arg, prefer the most recently cached\n // schema over re-parsing with the base schema.\n const s = schema ?? cachedSchema ?? baseEnvSchema\n // Re-parse if schema changed or no cache yet\n if (cachedEnv && cachedSchema === s) return cachedEnv\n cachedSchema = s\n cachedEnv = s.parse(process.env)\n\n // Register env resolver so @Value() reads validated values\n Container._envResolver = (key: string) => cachedEnv?.[key]\n\n return cachedEnv\n}\n\n/**\n * Get a single typed environment variable value.\n *\n * Three forms:\n * - **No-arg, KickEnv populated** (`kick typegen` has run) → key is\n * constrained to known `KickEnv` keys; return type inferred from\n * the schema.\n * - **With explicit schema** → key constrained to that schema's keys;\n * return type is `z.infer<typeof schema>[K]`.\n * - **No-arg, KickEnv empty** → loose `string` key, `any` return.\n *\n * @example\n * ```ts\n * // After `kick typegen` (KickEnv populated):\n * const port = getEnv('PORT') // typed as number\n * const url = getEnv('DATABASE_URL') // typed as string\n *\n * // With an inline schema (legacy / one-off):\n * const dbUrl = getEnv('DATABASE_URL', envSchema)\n * ```\n */\nexport function getEnv<K extends EnvKey>(key: K): KickEnv[K]\nexport function getEnv<T extends z.ZodObject<any>, K extends string & keyof z.infer<T>>(\n key: K,\n schema: T,\n): z.infer<T>[K]\nexport function getEnv(key: string, schema?: z.ZodObject<any>): any {\n const env = schema ? loadEnv(schema) : (loadEnv() as Record<string, any>)\n return env[key]\n}\n\n/**\n * Reload env from `process.env`. Re-reads `.env` via dotenv (if\n * installed), then re-parses the result against the **same** schema\n * the project registered at startup. Called during HMR rebuild and by\n * `envWatchPlugin` to pick up `.env` file changes mid-session.\n *\n * Crucially, this preserves the registered schema. Wiping it would\n * silently downgrade `ConfigService` and `@Value()` back to the base\n * schema on the next read, so every user-defined key would start\n * resolving as `undefined` after the first `.env` save — which was the\n * original \"config disappears in dev\" bug.\n *\n * If you need to fully forget the schema (e.g. tests that swap schemas\n * between cases), call {@link resetEnvCache} instead.\n */\nexport function reloadEnv(): void {\n // Re-read .env file into process.env if dotenv is installed.\n try {\n // eslint-disable-next-line @typescript-eslint/no-require-imports\n require('dotenv').config({ override: true, quiet: true })\n } catch {\n // dotenv not installed — nothing to reload, env comes from the\n // environment directly.\n }\n\n // Drop the parsed snapshot but keep the schema. Re-parse eagerly so\n // existing consumers (including ConfigService getters and @Value\n // resolvers that may already hold references to the env state) see\n // the new values on their very next read instead of having to\n // re-trigger any cache-population step.\n cachedEnv = null\n if (cachedSchema) {\n loadEnv(cachedSchema)\n }\n}\n\n/**\n * Fully reset the env cache, including the registered schema. Use this\n * in tests that need to swap schemas between cases — `reloadEnv()`\n * deliberately preserves the schema to avoid the \"user keys vanish on\n * `.env` reload\" footgun.\n */\nexport function resetEnvCache(): void {\n cachedEnv = null\n cachedSchema = null\n}\n\nexport type Env = z.infer<typeof baseEnvSchema>\n"],"mappings":";;;;;;;;;;uFAmBA,SAAS,eAAsB,CAC7B,GAAI,CAMF,UAJuB,SAIjB,CAAC,OAAO,CAAE,SAAU,GAAO,MAAO,GAAM,CAAC,MACzC,GAKV,eAAe,CAMf,MAAa,cAAgB,EAAE,OAAO,CAEpC,KAAM,EAAE,OAAO,QAAQ,CAAC,QAAQ,IAAK,CACrC,SAAU,EAAE,KAAK,CAAC,cAAe,aAAc,OAAO,CAAC,CAAC,QAAQ,cAAc,CAC9E,UAAW,EAAE,QAAQ,CAAC,QAAQ,OAAO,CACtC,CAAC,CAGF,IAAI,UAAiB,KACjB,aAAoB,KAgCxB,SAAgB,UACd,OAC6C,CAC7C,IAAM,WAAa,OAAO,cAAc,CAIxC,OAAO,cAAc,OAAO,WAAW,MAAM,CA4B/C,SAAgB,QAAoC,OAAiB,CAGnE,IAAM,EAAI,QAAU,cAAgB,cASpC,OAPI,WAAa,eAAiB,EAAU,WAC5C,aAAe,EACf,UAAY,EAAE,MAAM,QAAQ,IAAI,CAGhC,UAAU,aAAgB,KAAgB,YAAY,KAE/C,WA6BT,SAAgB,OAAO,IAAa,OAAgC,CAElE,OADY,OAAS,QAAQ,OAAO,CAAI,SAAS,EACtC,KAkBb,SAAgB,WAAkB,CAEhC,GAAI,CAEF,UAAQ,SAAS,CAAC,OAAO,CAAE,SAAU,GAAM,MAAO,GAAM,CAAC,MACnD,EAUR,UAAY,KACR,cACF,QAAQ,aAAa,CAUzB,SAAgB,eAAsB,CACpC,UAAY,KACZ,aAAe"}
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
|
|
2
|
-
import { n as ApplicationOptions, r as MiddlewareEntry, t as Application } from "../application-
|
|
2
|
+
import { n as ApplicationOptions, r as MiddlewareEntry, t as Application } from "../application-CaEYca9E.mjs";
|
|
3
3
|
export { Application, ApplicationOptions, MiddlewareEntry };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.6.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{t as Application}from"../application-
|
|
11
|
+
import{t as Application}from"../application-YJZ2frWX.mjs";export{Application};
|
package/dist/http/bootstrap.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.6.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{t as bootstrap}from"../bootstrap-
|
|
11
|
+
import{t as bootstrap}from"../bootstrap-UCZoqbbW.mjs";export{bootstrap};
|
package/dist/http/context.d.mts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
1
|
|
|
2
2
|
import { t as ContextMeta } from "../execution-context-DKnt61pY.mjs";
|
|
3
|
-
import { i as RouteShape, n as DeepReadonly, r as RequestContext, t as Ctx } from "../context-
|
|
3
|
+
import { i as RouteShape, n as DeepReadonly, r as RequestContext, t as Ctx } from "../context-aocg9gO-.mjs";
|
|
4
4
|
export { ContextMeta, Ctx, DeepReadonly, RequestContext, RouteShape };
|
package/dist/http/context.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.6.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{t as RequestContext}from"../context-
|
|
11
|
+
import{t as RequestContext}from"../context-CbX_T0XC.mjs";export{RequestContext};
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.6.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{createLogger}from"../../core/logger.mjs";import
|
|
11
|
+
import{createLogger}from"../../core/logger.mjs";import"../../core-DvTHNOay.mjs";import{HttpException}from"../../core/errors.mjs";const log=createLogger(`ErrorHandler`);function notFoundHandler(){return(_req,res,_next)=>{res.status(404).json({message:`Not Found`})}}function errorHandler(){let isProduction=process.env.NODE_ENV===`production`;return(err,req,res,_next)=>{if(res.headersSent){log.warn(`Error after headers sent: ${err?.message||`Unknown`}`);return}if(err?.name===`ZodError`){let firstIssue=err.issues?.[0];return res.status(422).json({message:firstIssue?.message||`Validation failed`,...isProduction?{}:{errors:err.issues}})}if(err instanceof HttpException){if(err.status>=500&&log.error(err,err.message),err.headers)for(let[k,v]of Object.entries(err.headers))res.setHeader(k,v);let exposeDetails=!isProduction&&err.details!==void 0;return res.status(err.status).json({message:err.message,...exposeDetails?{errors:err.details}:{}})}let status=err.status||err.statusCode||500;log.error(err,`${req.method} ${req.originalUrl} — ${err.message||`Unhandled error`}`);let message=status===500?`Internal Server Error`:err.message||`Error`;res.status(status).json({message})}}export{errorHandler,notFoundHandler};
|
|
12
12
|
//# sourceMappingURL=error-handler.mjs.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.6.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{createLogger}from"../../core/logger.mjs";import"../../core-
|
|
11
|
+
import{createLogger}from"../../core/logger.mjs";import"../../core-DvTHNOay.mjs";function requestLogger(options={}){let log=createLogger(options.name??`HTTP`),level=options.level??`info`,skip=options.skip??[];return(req,res,next)=>{if(skip.some(prefix=>req.path.startsWith(prefix)))return next();let start=Date.now();res.on(`finish`,()=>{let duration=Date.now()-start,requestId=req.requestId||req.headers[`x-request-id`]||`-`,status=res.statusCode;log[level](`${req.method} ${req.originalUrl} ${status} ${duration}ms ${requestId}`)}),next()}}export{requestLogger};
|
|
12
12
|
//# sourceMappingURL=request-logger.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"session.mjs","names":[],"sources":["../../../src/http/middleware/session.ts"],"sourcesContent":["import { randomUUID, createHmac, timingSafeEqual } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nexport interface SessionData {\n [key: string]: unknown\n}\n\nexport interface SessionStore {\n get(sid: string): Promise<SessionData | null>\n set(sid: string, data: SessionData, maxAge: number): Promise<void>\n destroy(sid: string): Promise<void>\n touch?(sid: string, maxAge: number): Promise<void>\n}\n\nexport interface Session {\n id: string\n data: SessionData\n regenerate(): Promise<void>\n destroy(): Promise<void>\n save(): Promise<void>\n}\n\nexport interface SessionOptions {\n /** Secret used to sign the session cookie (required) */\n secret: string\n /** Cookie name (default: 'kick.sid') */\n cookieName?: string\n /** Session max age in milliseconds (default: 86400000 = 24h) */\n maxAge?: number\n /** Reset maxAge on every response (default: false) */\n rolling?: boolean\n /** Save new sessions that have not been modified (default: true) */\n saveUninitialized?: boolean\n /** Cookie options */\n cookie?: {\n httpOnly?: boolean\n secure?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n path?: string\n domain?: string\n }\n /** Custom session store (default: in-memory store with TTL cleanup) */\n store?: SessionStore\n}\n\n// ── In-Memory Store ───────────────────────────────────────────────────\n\nclass MemoryStore implements SessionStore {\n private readonly sessions = new Map<string, { data: SessionData; expires: number }>()\n private readonly cleanupInterval: ReturnType<typeof setInterval>\n\n constructor() {\n // Purge expired sessions every 60 seconds\n this.cleanupInterval = setInterval(() => this.purge(), 60_000)\n this.cleanupInterval.unref()\n }\n\n async get(sid: string): Promise<SessionData | null> {\n const entry = this.sessions.get(sid)\n if (!entry) return null\n if (Date.now() > entry.expires) {\n this.sessions.delete(sid)\n return null\n }\n return entry.data\n }\n\n async set(sid: string, data: SessionData, maxAge: number): Promise<void> {\n this.sessions.set(sid, { data, expires: Date.now() + maxAge })\n }\n\n async destroy(sid: string): Promise<void> {\n this.sessions.delete(sid)\n }\n\n async touch(sid: string, maxAge: number): Promise<void> {\n const entry = this.sessions.get(sid)\n if (entry) {\n entry.expires = Date.now() + maxAge\n }\n }\n\n private purge() {\n const now = Date.now()\n for (const [sid, entry] of this.sessions) {\n if (now > entry.expires) {\n this.sessions.delete(sid)\n }\n }\n }\n}\n\n// ── Cookie Header Parsing ─────────────────────────────────────────────\n\nfunction parseCookieHeader(header: string): Record<string, string> {\n const out: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const eq = pair.indexOf('=')\n if (eq === -1) continue\n const key = pair.slice(0, eq).trim()\n if (!key) continue\n let value = pair.slice(eq + 1).trim()\n if (value.startsWith('\"') && value.endsWith('\"')) {\n value = value.slice(1, -1)\n }\n try {\n out[key] = decodeURIComponent(value)\n } catch {\n out[key] = value\n }\n }\n return out\n}\n\n// ── Cookie Signing ────────────────────────────────────────────────────\n\nfunction sign(value: string, secret: string): string {\n const signature = createHmac('sha256', secret).update(value).digest('base64url')\n return `s:${value}.${signature}`\n}\n\nfunction unsign(signed: string, secret: string): string | false {\n if (!signed.startsWith('s:')) return false\n const raw = signed.slice(2)\n const dotIndex = raw.lastIndexOf('.')\n if (dotIndex === -1) return false\n\n const value = raw.slice(0, dotIndex)\n const providedSig = raw.slice(dotIndex + 1)\n const expectedSig = createHmac('sha256', secret).update(value).digest('base64url')\n\n const a = Buffer.from(providedSig)\n const b = Buffer.from(expectedSig)\n\n if (a.length !== b.length || !timingSafeEqual(a, b)) {\n return false\n }\n\n return value\n}\n\n// ── Middleware Factory ────────────────────────────────────────────────\n\n/**\n * Session management middleware.\n *\n * Attaches a `req.session` object with `id`, `data`, `regenerate()`,\n * `destroy()`, and `save()` methods. Session IDs are signed with\n * HMAC-SHA256 to prevent cookie tampering.\n *\n * Reads the session cookie from `req.cookies` when an upstream cookie\n * parser is present; otherwise parses `req.headers.cookie` directly, so\n * no cookie parser is required upstream.\n *\n * @example\n * ```ts\n * import { session } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * session({ secret: process.env.SESSION_SECRET! }),\n * // ... other middleware\n * ],\n * })\n * ```\n */\nexport function session(options: SessionOptions) {\n const {\n secret,\n cookieName = 'kick.sid',\n maxAge = 86_400_000,\n rolling = false,\n saveUninitialized = true,\n store = new MemoryStore(),\n cookie: cookieOpts = {},\n } = options\n\n const cookieDefaults = {\n httpOnly: cookieOpts.httpOnly ?? true,\n secure: cookieOpts.secure ?? process.env.NODE_ENV === 'production',\n sameSite: cookieOpts.sameSite ?? ('lax' as const),\n path: cookieOpts.path ?? '/',\n ...(cookieOpts.domain ? { domain: cookieOpts.domain } : {}),\n maxAge,\n }\n\n return async (req: Request, res: Response, next: NextFunction) => {\n let cookies = (req as any).cookies as Record<string, string> | undefined\n if (!cookies) {\n const header = req.headers.cookie\n cookies = header ? parseCookieHeader(header) : {}\n }\n const signedCookie = cookies[cookieName]\n let sid: string | false = false\n let sessionData: SessionData | null = null\n let isNew = false\n\n // Attempt to recover existing session\n if (signedCookie) {\n sid = unsign(signedCookie, secret)\n if (sid) {\n sessionData = await store.get(sid)\n }\n }\n\n // Create new session if none found\n if (!sid || !sessionData) {\n sid = randomUUID()\n sessionData = {}\n isNew = true\n }\n\n let currentSid = sid\n let currentData = { ...sessionData }\n let destroyed = false\n\n const sessionObj: Session = {\n get id() {\n return currentSid\n },\n data: currentData,\n\n async regenerate() {\n await store.destroy(currentSid)\n currentSid = randomUUID()\n currentData = {}\n sessionObj.data = currentData\n await store.set(currentSid, currentData, maxAge)\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n },\n\n async destroy() {\n await store.destroy(currentSid)\n destroyed = true\n currentData = {}\n sessionObj.data = currentData\n res.clearCookie(cookieName, { path: cookieDefaults.path })\n },\n\n async save() {\n if (!destroyed) {\n await store.set(currentSid, currentData, maxAge)\n }\n },\n }\n\n ;(req as any).session = sessionObj\n\n // Set cookie for new sessions\n if (isNew) {\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n // Auto-save on response finish\n res.on('finish', async () => {\n if (destroyed) return\n if (isNew && !saveUninitialized && Object.keys(currentData).length === 0) return\n\n await store.set(currentSid, currentData, maxAge)\n\n if (rolling && !isNew) {\n if (store.touch) {\n await store.touch(currentSid, maxAge)\n }\n }\n })\n\n // Rolling: refresh cookie on every response\n if (rolling && !isNew) {\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n next()\n }\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"session.mjs","names":[],"sources":["../../../src/http/middleware/session.ts"],"sourcesContent":["import { randomUUID, createHmac, timingSafeEqual } from 'node:crypto'\nimport type { Request, Response, NextFunction } from 'express'\n\nexport interface SessionData {\n [key: string]: unknown\n}\n\nexport interface SessionStore {\n get(sid: string): Promise<SessionData | null>\n set(sid: string, data: SessionData, maxAge: number): Promise<void>\n destroy(sid: string): Promise<void>\n touch?(sid: string, maxAge: number): Promise<void>\n}\n\nexport interface Session {\n id: string\n data: SessionData\n regenerate(): Promise<void>\n destroy(): Promise<void>\n save(): Promise<void>\n}\n\nexport interface SessionOptions {\n /** Secret used to sign the session cookie (required) */\n secret: string\n /** Cookie name (default: 'kick.sid') */\n cookieName?: string\n /** Session max age in milliseconds (default: 86400000 = 24h) */\n maxAge?: number\n /** Reset maxAge on every response (default: false) */\n rolling?: boolean\n /** Save new sessions that have not been modified (default: true) */\n saveUninitialized?: boolean\n /** Cookie options */\n cookie?: {\n httpOnly?: boolean\n secure?: boolean\n sameSite?: 'strict' | 'lax' | 'none'\n path?: string\n domain?: string\n }\n /** Custom session store (default: in-memory store with TTL cleanup) */\n store?: SessionStore\n}\n\n// ── In-Memory Store ───────────────────────────────────────────────────\n\nclass MemoryStore implements SessionStore {\n private readonly sessions = new Map<string, { data: SessionData; expires: number }>()\n private readonly cleanupInterval: ReturnType<typeof setInterval>\n\n constructor() {\n // Purge expired sessions every 60 seconds\n this.cleanupInterval = setInterval(() => this.purge(), 60_000)\n this.cleanupInterval.unref()\n }\n\n async get(sid: string): Promise<SessionData | null> {\n const entry = this.sessions.get(sid)\n if (!entry) return null\n if (Date.now() > entry.expires) {\n this.sessions.delete(sid)\n return null\n }\n return entry.data\n }\n\n async set(sid: string, data: SessionData, maxAge: number): Promise<void> {\n this.sessions.set(sid, { data, expires: Date.now() + maxAge })\n }\n\n async destroy(sid: string): Promise<void> {\n this.sessions.delete(sid)\n }\n\n async touch(sid: string, maxAge: number): Promise<void> {\n const entry = this.sessions.get(sid)\n if (entry) {\n entry.expires = Date.now() + maxAge\n }\n }\n\n private purge() {\n const now = Date.now()\n for (const [sid, entry] of this.sessions) {\n if (now > entry.expires) {\n this.sessions.delete(sid)\n }\n }\n }\n}\n\n// ── Cookie Header Parsing ─────────────────────────────────────────────\n\nfunction parseCookieHeader(header: string): Record<string, string> {\n const out: Record<string, string> = {}\n for (const pair of header.split(';')) {\n const eq = pair.indexOf('=')\n if (eq === -1) continue\n const key = pair.slice(0, eq).trim()\n if (!key) continue\n let value = pair.slice(eq + 1).trim()\n if (value.startsWith('\"') && value.endsWith('\"')) {\n value = value.slice(1, -1)\n }\n try {\n out[key] = decodeURIComponent(value)\n } catch {\n out[key] = value\n }\n }\n return out\n}\n\n// ── Cookie Signing ────────────────────────────────────────────────────\n\nfunction sign(value: string, secret: string): string {\n const signature = createHmac('sha256', secret).update(value).digest('base64url')\n return `s:${value}.${signature}`\n}\n\nfunction unsign(signed: string, secret: string): string | false {\n if (!signed.startsWith('s:')) return false\n const raw = signed.slice(2)\n const dotIndex = raw.lastIndexOf('.')\n if (dotIndex === -1) return false\n\n const value = raw.slice(0, dotIndex)\n const providedSig = raw.slice(dotIndex + 1)\n const expectedSig = createHmac('sha256', secret).update(value).digest('base64url')\n\n const a = Buffer.from(providedSig)\n const b = Buffer.from(expectedSig)\n\n if (a.length !== b.length || !timingSafeEqual(a, b)) {\n return false\n }\n\n return value\n}\n\n// ── Middleware Factory ────────────────────────────────────────────────\n\n/**\n * Session management middleware.\n *\n * Attaches a `req.session` object with `id`, `data`, `regenerate()`,\n * `destroy()`, and `save()` methods. Session IDs are signed with\n * HMAC-SHA256 to prevent cookie tampering.\n *\n * Reads the session cookie from `req.cookies` when an upstream cookie\n * parser is present; otherwise parses `req.headers.cookie` directly, so\n * no cookie parser is required upstream.\n *\n * @example\n * ```ts\n * import { session } from '@forinda/kickjs'\n *\n * bootstrap({\n * modules,\n * middleware: [\n * session({ secret: process.env.SESSION_SECRET! }),\n * // ... other middleware\n * ],\n * })\n * ```\n */\nexport function session(options: SessionOptions) {\n const {\n secret,\n cookieName = 'kick.sid',\n maxAge = 86_400_000,\n rolling = false,\n saveUninitialized = true,\n store = new MemoryStore(),\n cookie: cookieOpts = {},\n } = options\n\n const cookieDefaults = {\n httpOnly: cookieOpts.httpOnly ?? true,\n secure: cookieOpts.secure ?? process.env.NODE_ENV === 'production',\n sameSite: cookieOpts.sameSite ?? ('lax' as const),\n path: cookieOpts.path ?? '/',\n ...(cookieOpts.domain ? { domain: cookieOpts.domain } : {}),\n maxAge,\n }\n\n return async (req: Request, res: Response, next: NextFunction) => {\n let cookies = (req as any).cookies as Record<string, string> | undefined\n if (!cookies) {\n const header = req.headers.cookie\n cookies = header ? parseCookieHeader(header) : {}\n }\n const signedCookie = cookies[cookieName]\n let sid: string | false = false\n let sessionData: SessionData | null = null\n let isNew = false\n\n // Attempt to recover existing session\n if (signedCookie) {\n sid = unsign(signedCookie, secret)\n if (sid) {\n sessionData = await store.get(sid)\n }\n }\n\n // Create new session if none found\n if (!sid || !sessionData) {\n sid = randomUUID()\n sessionData = {}\n isNew = true\n }\n\n let currentSid = sid\n let currentData = { ...sessionData }\n let destroyed = false\n\n const sessionObj: Session = {\n get id() {\n return currentSid\n },\n data: currentData,\n\n async regenerate() {\n await store.destroy(currentSid)\n currentSid = randomUUID()\n currentData = {}\n sessionObj.data = currentData\n await store.set(currentSid, currentData, maxAge)\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n },\n\n async destroy() {\n await store.destroy(currentSid)\n destroyed = true\n currentData = {}\n sessionObj.data = currentData\n res.clearCookie(cookieName, { path: cookieDefaults.path })\n },\n\n async save() {\n if (!destroyed) {\n await store.set(currentSid, currentData, maxAge)\n }\n },\n }\n\n ;(req as any).session = sessionObj\n\n // Set cookie for new sessions\n if (isNew) {\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n // Auto-save on response finish\n res.on('finish', async () => {\n if (destroyed) return\n if (isNew && !saveUninitialized && Object.keys(currentData).length === 0) return\n\n await store.set(currentSid, currentData, maxAge)\n\n if (rolling && !isNew) {\n if (store.touch) {\n await store.touch(currentSid, maxAge)\n }\n }\n })\n\n // Rolling: refresh cookie on every response\n if (rolling && !isNew) {\n res.cookie(cookieName, sign(currentSid, secret), cookieDefaults)\n }\n\n next()\n }\n}\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.6.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{h as __require}from"../../container-
|
|
11
|
+
import{h as __require}from"../../container-D0v17gF8.mjs";import{Logger}from"../../core/logger.mjs";import"../../core-DvTHNOay.mjs";import{existsSync,readFileSync}from"node:fs";import{join,resolve}from"node:path";const log=Logger.for(`SpaAdapter`);var SpaAdapter=class{name=`SpaAdapter`;clientDir;apiPrefixes;excludePaths;cacheControl;indexCacheControl;indexHtml=null;constructor(options={}){this.options=options,this.clientDir=resolve(options.clientDir??`dist/client`),this.excludePaths=options.exclude??[],this.cacheControl=options.cacheControl??`public, max-age=31536000, immutable`,this.indexCacheControl=options.indexCacheControl??`no-cache`;let prefix=options.apiPrefix??`/api`;this.apiPrefixes=Array.isArray(prefix)?prefix:[prefix]}beforeMount({app}){if(!existsSync(this.clientDir)){log.warn(`SPA client directory not found: ${this.clientDir}`),log.warn(`Build your frontend first, or set clientDir to the correct path.`);return}let indexPath=join(this.clientDir,`index.html`);if(existsSync(indexPath))this.indexHtml=readFileSync(indexPath,`utf-8`);else{log.warn(`index.html not found in ${this.clientDir}`);return}try{let express=__require(`express`),staticOpts={};this.cacheControl&&(staticOpts.setHeaders=(res,filePath)=>{filePath.endsWith(`.html`)?res.setHeader(`Cache-Control`,this.indexCacheControl):res.setHeader(`Cache-Control`,this.cacheControl)}),app.use(express.static(this.clientDir,staticOpts))}catch{log.error(`express.static not available — SPA static file serving disabled`);return}log.debug(`Serving SPA from ${this.clientDir}`)}beforeStart({app}){this.indexHtml&&app.use((req,res,next)=>{for(let prefix of this.apiPrefixes)if(req.path.startsWith(prefix))return next();for(let path of this.excludePaths)if(req.path.startsWith(path))return next();if(req.path.includes(`.`)||req.method!==`GET`)return next();res.setHeader(`Content-Type`,`text/html`),res.setHeader(`Cache-Control`,this.indexCacheControl),res.send(this.indexHtml)})}};export{SpaAdapter};
|
|
12
12
|
//# sourceMappingURL=spa.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"upload.mjs","names":[],"sources":["../../../src/http/middleware/upload.ts"],"sourcesContent":["import { unlink } from 'node:fs/promises'\nimport type { Request, Response, NextFunction, RequestHandler } from 'express'\nimport multer, { type Options as MulterOptions } from 'multer'\nimport type { BaseUploadOptions, FileUploadConfig } from '../../core'\n\n/**\n * Maps short file extensions to their MIME types.\n * Users can pass `['jpg', 'png', 'pdf']` instead of full MIME strings.\n */\nconst MIME_MAP: Record<string, string> = {\n // Images\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n tiff: 'image/tiff',\n tif: 'image/tiff',\n avif: 'image/avif',\n // Documents\n pdf: 'application/pdf',\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n ppt: 'application/vnd.ms-powerpoint',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n odt: 'application/vnd.oasis.opendocument.text',\n ods: 'application/vnd.oasis.opendocument.spreadsheet',\n odp: 'application/vnd.oasis.opendocument.presentation',\n rtf: 'application/rtf',\n txt: 'text/plain',\n csv: 'text/csv',\n // Archives\n zip: 'application/zip',\n gz: 'application/gzip',\n tar: 'application/x-tar',\n rar: 'application/vnd.rar',\n '7z': 'application/x-7z-compressed',\n // Audio\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n flac: 'audio/flac',\n aac: 'audio/aac',\n // Video\n mp4: 'video/mp4',\n webm: 'video/webm',\n avi: 'video/x-msvideo',\n mov: 'video/quicktime',\n mkv: 'video/x-matroska',\n // Other\n json: 'application/json',\n xml: 'application/xml',\n html: 'text/html',\n}\n\n/**\n * Resolves a list of file type identifiers to MIME type strings.\n * Accepts short extensions (`'jpg'`, `'pdf'`) or full MIME types (`'image/jpeg'`).\n *\n * @example\n * ```ts\n * resolveMimeTypes(['jpg', 'png', 'application/pdf'])\n * // → ['image/jpeg', 'image/png', 'application/pdf']\n * ```\n */\nexport function resolveMimeTypes(types: string[]): string[] {\n return types.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return MIME_MAP[lower] ?? t\n })\n}\n\n/**\n * Upload options for the middleware.\n * Extends BaseUploadOptions from core (shared with @FileUpload decorator)\n * and adds Multer-specific storage options.\n */\nexport interface UploadOptions extends BaseUploadOptions {\n /** Multer storage config (default: memory storage) */\n storage?: MulterOptions['storage']\n /** Multer dest for disk storage shorthand */\n dest?: string\n}\n\nfunction createMulter(options: UploadOptions) {\n const mimeMap = options.customMimeMap ? { ...MIME_MAP, ...options.customMimeMap } : MIME_MAP\n\n const limits: MulterOptions['limits'] = {\n fileSize: options.maxSize ?? 5 * 1024 * 1024,\n }\n\n let fileFilter: MulterOptions['fileFilter'] | undefined\n\n if (typeof options.allowedTypes === 'function') {\n // Custom filter function\n const filterFn = options.allowedTypes\n fileFilter = (_req, file, cb) => {\n if (filterFn(file.mimetype, file.originalname)) {\n cb(null, true)\n } else {\n cb(new Error(`File type ${file.mimetype} is not allowed`))\n }\n }\n } else if (Array.isArray(options.allowedTypes)) {\n // String array — resolve short extensions using the (possibly extended) MIME map\n const resolvedTypes = options.allowedTypes.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return mimeMap[lower] ?? t\n })\n fileFilter = (_req, file, cb) => {\n const allowed = resolvedTypes.some((type) => {\n if (type.endsWith('/*')) {\n return file.mimetype.startsWith(type.replace('/*', '/'))\n }\n return file.mimetype === type\n })\n if (allowed) {\n cb(null, true)\n } else {\n cb(new Error(`File type ${file.mimetype} is not allowed`))\n }\n }\n }\n\n const multerOptions: MulterOptions = {\n limits,\n ...(fileFilter ? { fileFilter } : {}),\n ...(options.storage ? { storage: options.storage } : {}),\n ...(options.dest ? { dest: options.dest } : {}),\n }\n\n return multer(multerOptions)\n}\n\n/**\n * Single file upload middleware. Attaches the file to `req.file`.\n *\n * @example\n * ```ts\n * @Post('/avatar')\n * @Middleware(upload.single('avatar', { maxSize: 2 * 1024 * 1024, allowedTypes: ['jpg', 'png'] }))\n * async uploadAvatar(ctx: RequestContext) {\n * ctx.json({ filename: ctx.file.originalname })\n * }\n * ```\n */\nfunction single(fieldName: string, options: UploadOptions = {}): RequestHandler {\n const m = createMulter(options)\n return m.single(fieldName) as RequestHandler\n}\n\n/**\n * Multiple file upload middleware. Attaches files to `req.files`.\n */\nfunction array(fieldName: string, maxCount = 10, options: UploadOptions = {}): RequestHandler {\n const m = createMulter(options)\n return m.array(fieldName, maxCount) as RequestHandler\n}\n\n/**\n * No file upload — just parse multipart form data without file fields.\n */\nfunction none(options: UploadOptions = {}): RequestHandler {\n const m = createMulter(options)\n return m.none() as RequestHandler\n}\n\n/**\n * Removes temporary files from disk after the response is sent.\n * Attach this as Express middleware after the upload middleware.\n * Works with both `req.file` (single) and `req.files` (array).\n *\n * @example\n * ```ts\n * @Post('/process')\n * @Middleware(upload.single('document', { dest: '/tmp/uploads' }), cleanupFiles())\n * async processDocument(ctx: RequestContext) {\n * ctx.json({ ok: true })\n * }\n * ```\n */\nexport function cleanupFiles() {\n return (req: Request, res: Response, next: NextFunction) => {\n res.on('finish', async () => {\n const files: any[] = []\n\n if ((req as any).file?.path) {\n files.push((req as any).file)\n }\n if (Array.isArray((req as any).files)) {\n for (const f of (req as any).files) {\n if (f?.path) files.push(f)\n }\n }\n\n for (const file of files) {\n try {\n await unlink(file.path)\n } catch {\n // File may already be moved/deleted by the handler — ignore\n }\n }\n })\n\n next()\n }\n}\n\n/**\n * Build upload middleware from a @FileUpload decorator config.\n * Used internally by the router builder when it detects FILE_UPLOAD metadata.\n * Accepts the same FileUploadConfig interface used by the @FileUpload decorator.\n */\nexport function buildUploadMiddleware(config: FileUploadConfig): RequestHandler {\n const options: UploadOptions = {}\n if (config.maxSize) options.maxSize = config.maxSize\n if (config.allowedTypes) options.allowedTypes = config.allowedTypes\n if (config.customMimeMap) options.customMimeMap = config.customMimeMap\n\n const fieldName = config.fieldName ?? 'file'\n\n switch (config.mode) {\n case 'single':\n return single(fieldName, options)\n case 'array':\n return array(fieldName, config.maxCount ?? 10, options)\n case 'none':\n return none(options)\n }\n}\n\n/** Upload middleware factory with `.single()`, `.array()`, `.none()` methods */\nexport const upload = { single, array, none }\n"],"mappings":";;;;;;;;;;gEASA,MAAM,SAAmC,CAEvC,IAAK,aACL,KAAM,aACN,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,gBACL,IAAK,YACL,IAAK,eACL,KAAM,aACN,IAAK,aACL,KAAM,aAEN,IAAK,kBACL,IAAK,qBACL,KAAM,0EACN,IAAK,2BACL,KAAM,oEACN,IAAK,gCACL,KAAM,4EACN,IAAK,0CACL,IAAK,iDACL,IAAK,kDACL,IAAK,kBACL,IAAK,aACL,IAAK,WAEL,IAAK,kBACL,GAAI,mBACJ,IAAK,oBACL,IAAK,sBACL,KAAM,8BAEN,IAAK,aACL,IAAK,YACL,IAAK,YACL,KAAM,aACN,IAAK,YAEL,IAAK,YACL,KAAM,aACN,IAAK,kBACL,IAAK,kBACL,IAAK,mBAEL,KAAM,mBACN,IAAK,kBACL,KAAM,YACP,CAYD,SAAgB,iBAAiB,MAA2B,CAC1D,OAAO,MAAM,IAAK,GAET,SADO,EAAE,aAAa,CAAC,QAAQ,MAAO,
|
|
1
|
+
{"version":3,"file":"upload.mjs","names":[],"sources":["../../../src/http/middleware/upload.ts"],"sourcesContent":["import { unlink } from 'node:fs/promises'\nimport type { Request, Response, NextFunction, RequestHandler } from 'express'\nimport multer, { type Options as MulterOptions } from 'multer'\nimport type { BaseUploadOptions, FileUploadConfig } from '../../core'\n\n/**\n * Maps short file extensions to their MIME types.\n * Users can pass `['jpg', 'png', 'pdf']` instead of full MIME strings.\n */\nconst MIME_MAP: Record<string, string> = {\n // Images\n jpg: 'image/jpeg',\n jpeg: 'image/jpeg',\n png: 'image/png',\n gif: 'image/gif',\n webp: 'image/webp',\n svg: 'image/svg+xml',\n bmp: 'image/bmp',\n ico: 'image/x-icon',\n tiff: 'image/tiff',\n tif: 'image/tiff',\n avif: 'image/avif',\n // Documents\n pdf: 'application/pdf',\n doc: 'application/msword',\n docx: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document',\n xls: 'application/vnd.ms-excel',\n xlsx: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',\n ppt: 'application/vnd.ms-powerpoint',\n pptx: 'application/vnd.openxmlformats-officedocument.presentationml.presentation',\n odt: 'application/vnd.oasis.opendocument.text',\n ods: 'application/vnd.oasis.opendocument.spreadsheet',\n odp: 'application/vnd.oasis.opendocument.presentation',\n rtf: 'application/rtf',\n txt: 'text/plain',\n csv: 'text/csv',\n // Archives\n zip: 'application/zip',\n gz: 'application/gzip',\n tar: 'application/x-tar',\n rar: 'application/vnd.rar',\n '7z': 'application/x-7z-compressed',\n // Audio\n mp3: 'audio/mpeg',\n wav: 'audio/wav',\n ogg: 'audio/ogg',\n flac: 'audio/flac',\n aac: 'audio/aac',\n // Video\n mp4: 'video/mp4',\n webm: 'video/webm',\n avi: 'video/x-msvideo',\n mov: 'video/quicktime',\n mkv: 'video/x-matroska',\n // Other\n json: 'application/json',\n xml: 'application/xml',\n html: 'text/html',\n}\n\n/**\n * Resolves a list of file type identifiers to MIME type strings.\n * Accepts short extensions (`'jpg'`, `'pdf'`) or full MIME types (`'image/jpeg'`).\n *\n * @example\n * ```ts\n * resolveMimeTypes(['jpg', 'png', 'application/pdf'])\n * // → ['image/jpeg', 'image/png', 'application/pdf']\n * ```\n */\nexport function resolveMimeTypes(types: string[]): string[] {\n return types.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return MIME_MAP[lower] ?? t\n })\n}\n\n/**\n * Upload options for the middleware.\n * Extends BaseUploadOptions from core (shared with @FileUpload decorator)\n * and adds Multer-specific storage options.\n */\nexport interface UploadOptions extends BaseUploadOptions {\n /** Multer storage config (default: memory storage) */\n storage?: MulterOptions['storage']\n /** Multer dest for disk storage shorthand */\n dest?: string\n}\n\nfunction createMulter(options: UploadOptions) {\n const mimeMap = options.customMimeMap ? { ...MIME_MAP, ...options.customMimeMap } : MIME_MAP\n\n const limits: MulterOptions['limits'] = {\n fileSize: options.maxSize ?? 5 * 1024 * 1024,\n }\n\n let fileFilter: MulterOptions['fileFilter'] | undefined\n\n if (typeof options.allowedTypes === 'function') {\n // Custom filter function\n const filterFn = options.allowedTypes\n fileFilter = (_req, file, cb) => {\n if (filterFn(file.mimetype, file.originalname)) {\n cb(null, true)\n } else {\n cb(new Error(`File type ${file.mimetype} is not allowed`))\n }\n }\n } else if (Array.isArray(options.allowedTypes)) {\n // String array — resolve short extensions using the (possibly extended) MIME map\n const resolvedTypes = options.allowedTypes.map((t) => {\n const lower = t.toLowerCase().replace(/^\\./, '')\n return mimeMap[lower] ?? t\n })\n fileFilter = (_req, file, cb) => {\n const allowed = resolvedTypes.some((type) => {\n if (type.endsWith('/*')) {\n return file.mimetype.startsWith(type.replace('/*', '/'))\n }\n return file.mimetype === type\n })\n if (allowed) {\n cb(null, true)\n } else {\n cb(new Error(`File type ${file.mimetype} is not allowed`))\n }\n }\n }\n\n const multerOptions: MulterOptions = {\n limits,\n ...(fileFilter ? { fileFilter } : {}),\n ...(options.storage ? { storage: options.storage } : {}),\n ...(options.dest ? { dest: options.dest } : {}),\n }\n\n return multer(multerOptions)\n}\n\n/**\n * Single file upload middleware. Attaches the file to `req.file`.\n *\n * @example\n * ```ts\n * @Post('/avatar')\n * @Middleware(upload.single('avatar', { maxSize: 2 * 1024 * 1024, allowedTypes: ['jpg', 'png'] }))\n * async uploadAvatar(ctx: RequestContext) {\n * ctx.json({ filename: ctx.file.originalname })\n * }\n * ```\n */\nfunction single(fieldName: string, options: UploadOptions = {}): RequestHandler {\n const m = createMulter(options)\n return m.single(fieldName) as RequestHandler\n}\n\n/**\n * Multiple file upload middleware. Attaches files to `req.files`.\n */\nfunction array(fieldName: string, maxCount = 10, options: UploadOptions = {}): RequestHandler {\n const m = createMulter(options)\n return m.array(fieldName, maxCount) as RequestHandler\n}\n\n/**\n * No file upload — just parse multipart form data without file fields.\n */\nfunction none(options: UploadOptions = {}): RequestHandler {\n const m = createMulter(options)\n return m.none() as RequestHandler\n}\n\n/**\n * Removes temporary files from disk after the response is sent.\n * Attach this as Express middleware after the upload middleware.\n * Works with both `req.file` (single) and `req.files` (array).\n *\n * @example\n * ```ts\n * @Post('/process')\n * @Middleware(upload.single('document', { dest: '/tmp/uploads' }), cleanupFiles())\n * async processDocument(ctx: RequestContext) {\n * ctx.json({ ok: true })\n * }\n * ```\n */\nexport function cleanupFiles() {\n return (req: Request, res: Response, next: NextFunction) => {\n res.on('finish', async () => {\n const files: any[] = []\n\n if ((req as any).file?.path) {\n files.push((req as any).file)\n }\n if (Array.isArray((req as any).files)) {\n for (const f of (req as any).files) {\n if (f?.path) files.push(f)\n }\n }\n\n for (const file of files) {\n try {\n await unlink(file.path)\n } catch {\n // File may already be moved/deleted by the handler — ignore\n }\n }\n })\n\n next()\n }\n}\n\n/**\n * Build upload middleware from a @FileUpload decorator config.\n * Used internally by the router builder when it detects FILE_UPLOAD metadata.\n * Accepts the same FileUploadConfig interface used by the @FileUpload decorator.\n */\nexport function buildUploadMiddleware(config: FileUploadConfig): RequestHandler {\n const options: UploadOptions = {}\n if (config.maxSize) options.maxSize = config.maxSize\n if (config.allowedTypes) options.allowedTypes = config.allowedTypes\n if (config.customMimeMap) options.customMimeMap = config.customMimeMap\n\n const fieldName = config.fieldName ?? 'file'\n\n switch (config.mode) {\n case 'single':\n return single(fieldName, options)\n case 'array':\n return array(fieldName, config.maxCount ?? 10, options)\n case 'none':\n return none(options)\n }\n}\n\n/** Upload middleware factory with `.single()`, `.array()`, `.none()` methods */\nexport const upload = { single, array, none }\n"],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.6.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{HttpException,HttpStatus}from"../../core/errors.mjs";
|
|
11
|
+
import"../../core-DvTHNOay.mjs";import{HttpException,HttpStatus}from"../../core/errors.mjs";function toValidationException(error,message,useFirstIssueMessage){let issues=error.issues||[],details=issues.map(i=>({field:i.path?.join(`.`)??``,message:i.message})),finalMessage=useFirstIssueMessage?issues[0]?.message??message:message;return new HttpException(HttpStatus.UNPROCESSABLE_ENTITY,finalMessage,details)}function assignReqProperty(req,key,value){Object.defineProperty(req,key,{value,writable:!0,configurable:!0,enumerable:!0})}function validate(schema){return(req,_res,next)=>{try{if(schema.body){let result=schema.body.safeParse(req.body);if(!result.success)return next(toValidationException(result.error,`Validation failed`,!0));req.body=result.data}if(schema.query){let result=schema.query.safeParse(req.query);if(!result.success)return next(toValidationException(result.error,`Invalid query parameters`,!1));assignReqProperty(req,`query`,result.data)}if(schema.params){let result=schema.params.safeParse(req.params);if(!result.success)return next(toValidationException(result.error,`Invalid path parameters`,!1));assignReqProperty(req,`params`,result.data)}next()}catch(err){next(err)}}}export{validate};
|
|
12
12
|
//# sourceMappingURL=validate.mjs.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.6.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{Logger}from"../../core/logger.mjs";import{n as defineAdapter}from"../../core-
|
|
11
|
+
import{Logger}from"../../core/logger.mjs";import{n as defineAdapter}from"../../core-DvTHNOay.mjs";import{resolve}from"node:path";const log=Logger.for(`ViewEngine`),ViewAdapter=defineAdapter({name:`ViewAdapter`,build:options=>({beforeMount({app}){let{engine,ext,viewsDir=`src/views`}=options;engine.__express?app.engine(ext,engine.__express):typeof engine.renderFile==`function`?app.engine(ext,(path,opts,callback)=>{engine.renderFile(path,opts,callback)}):(typeof engine==`function`||log.warn(`Engine for .${ext} does not have __express or renderFile. Trying as-is.`),app.engine(ext,engine)),app.set(`view engine`,ext),app.set(`views`,resolve(viewsDir)),log.debug(`View engine: ${ext} (${resolve(viewsDir)})`)}})});export{ViewAdapter};
|
|
12
12
|
//# sourceMappingURL=views.mjs.map
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.6.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{a as parseSearchQuery,i as parseQuery,n as parseFilters,o as parseSort,r as parsePagination,s as FILTER_OPERATORS,t as buildQueryParams}from"../../query-
|
|
11
|
+
import{a as parseSearchQuery,i as parseQuery,n as parseFilters,o as parseSort,r as parsePagination,s as FILTER_OPERATORS,t as buildQueryParams}from"../../query-CQU-Hyiv.mjs";export{FILTER_OPERATORS,buildQueryParams,parseFilters,parsePagination,parseQuery,parseSearchQuery,parseSort};
|
|
@@ -1,3 +1,3 @@
|
|
|
1
1
|
|
|
2
|
-
import { n as _setExternalContributorSources, r as buildRoutes, t as BuildRoutesOptions } from "../router-builder-
|
|
2
|
+
import { n as _setExternalContributorSources, r as buildRoutes, t as BuildRoutesOptions } from "../router-builder-CaQ629tw.mjs";
|
|
3
3
|
export { BuildRoutesOptions, _setExternalContributorSources, buildRoutes };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* @forinda/kickjs v5.
|
|
2
|
+
* @forinda/kickjs v5.6.0
|
|
3
3
|
*
|
|
4
4
|
* Copyright (c) Felix Orinda
|
|
5
5
|
*
|
|
@@ -8,5 +8,5 @@
|
|
|
8
8
|
*
|
|
9
9
|
* @license MIT
|
|
10
10
|
*/
|
|
11
|
-
import{t as Container}from"../container-
|
|
11
|
+
import{t as Container}from"../container-D0v17gF8.mjs";import{METADATA}from"../core/interfaces.mjs";import{a as getMethodMeta,o as getMethodMetaOrUndefined,t as getClassMeta}from"../metadata-IHiynOCv.mjs";import{c as runContributors,l as buildPipeline}from"../core-DvTHNOay.mjs";import{t as RequestContext}from"../context-CbX_T0XC.mjs";import{validate}from"./middleware/validate.mjs";import{buildUploadMiddleware}from"./middleware/upload.mjs";import{Router}from"express";let _externalContributorSources=[];function _setExternalContributorSources(sources){_externalContributorSources=sources}function buildRoutes(controllerClass,options={}){let router=Router(),container=Container.getInstance(),externalSources=options.externalSources??_externalContributorSources,routes=getClassMeta(METADATA.ROUTES,controllerClass,[]),classMiddlewares=getClassMeta(METADATA.CLASS_MIDDLEWARES,controllerClass,[]),classContributors=getClassMeta(METADATA.CLASS_CONTRIBUTORS,controllerClass,[]);for(let route of routes){let method=route.method.toLowerCase(),fullPath=route.path||`/`,methodMiddlewares=getMethodMeta(METADATA.METHOD_MIDDLEWARES,controllerClass,route.handlerName,[]),handlers=[];route.validation&&handlers.push(validate(route.validation));let fileUploadConfig=getMethodMetaOrUndefined(METADATA.FILE_UPLOAD,controllerClass,route.handlerName);fileUploadConfig&&handlers.push(buildUploadMiddleware(fileUploadConfig));for(let mw of[...classMiddlewares,...methodMiddlewares])handlers.push((req,res,next)=>{let ctx=new RequestContext(req,res,next);Promise.resolve(mw(ctx,next)).catch(next)});let methodContributors=getMethodMeta(METADATA.METHOD_CONTRIBUTORS,controllerClass,route.handlerName,[]);if(classContributors.length>0||methodContributors.length>0||externalSources.length>0){let pipeline=buildPipeline([...methodContributors.map((registration,i)=>({source:`method`,registration,label:`${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`})),...classContributors.map((registration,i)=>({source:`class`,registration,label:`${controllerClass.name}.@class#${i}(${registration.key})`})),...externalSources],{route:`${route.method.toUpperCase()} ${fullPath}`});handlers.push(async(req,res,next)=>{let ctx=new RequestContext(req,res,next);try{await runContributors({pipeline,ctx,container}),next()}catch(err){next(err)}})}handlers.push(async(req,res,next)=>{let ctx=new RequestContext(req,res,next);try{await container.resolve(controllerClass)[route.handlerName](ctx)}catch(err){next(err)}}),router[method](fullPath,...handlers)}return router}export{_setExternalContributorSources,buildRoutes};
|
|
12
12
|
//# sourceMappingURL=router-builder.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"router-builder.mjs","names":[],"sources":["../../src/http/router-builder.ts"],"sourcesContent":["import { Router, type Request, type Response, type NextFunction } from 'express'\nimport {\n buildPipeline,\n Container,\n METADATA,\n runContributors,\n type ContributorRegistration,\n type FileUploadConfig,\n type MiddlewareHandler,\n type RouteDefinition,\n type SourcedRegistration,\n} from '../core'\nimport { getClassMeta, getMethodMeta, getMethodMetaOrUndefined } from '../core/metadata'\nimport { RequestContext } from './context'\nimport { validate } from './middleware/validate'\nimport { buildUploadMiddleware } from './middleware/upload'\n\n/**\n * Per-module SourcedRegistration[] threaded through the route-mount loop by\n * Application.setup(). Carries module + adapter + global contributors so\n * buildRoutes() can merge them with class + method ones into a single pipeline.\n *\n * Module setup is sequential, so this slot is race-free. Cleared in a finally\n * block after each module mounts. Outside of Application setup the slot is\n * empty — direct buildRoutes() callers (mostly tests) see only class + method\n * contributors unless they pass `externalSources` explicitly.\n *\n * Same idiom as `Container._requestStoreProvider` and `Logger._contextProvider`:\n * an internal escape hatch for cross-module wiring without inversion-of-control\n * gymnastics.\n *\n * @internal\n */\nlet _externalContributorSources: readonly SourcedRegistration[] = []\n\n/** @internal — set by Application.setup() before each `mod.routes()` call. */\nexport function _setExternalContributorSources(sources: readonly SourcedRegistration[]): void {\n _externalContributorSources = sources\n}\n\nexport interface BuildRoutesOptions {\n /**\n * Extra contributors to merge into the per-route pipeline at their declared\n * precedence levels. Pass explicitly when calling buildRoutes outside the\n * Application route-mount loop (typically in tests). When omitted, falls\n * back to the slot set by Application.setup().\n */\n externalSources?: readonly SourcedRegistration[]\n}\n\n/**\n * Build an Express Router from a controller class decorated with @Get, @Post, etc.\n * Resolves the controller from the DI container, wraps handlers in RequestContext,\n * and applies class-level and method-level middleware.\n *\n * Routes are registered using only the method-level decorator paths (e.g. @Get('/me') → '/me').\n * The @Controller path is NOT baked into the router — it serves as metadata only\n * (used by Swagger and other adapters for introspection).\n * The module's routes().path is the single source of truth for the mount prefix,\n * which avoids path doubling when both the module and controller specify the same path.\n */\nexport function buildRoutes(controllerClass: any, options: BuildRoutesOptions = {}): Router {\n const router = Router()\n const container = Container.getInstance()\n const externalSources = options.externalSources ?? _externalContributorSources\n const routes: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n\n // Class-level middleware\n const classMiddlewares: MiddlewareHandler[] = getClassMeta<MiddlewareHandler[]>(\n METADATA.CLASS_MIDDLEWARES,\n controllerClass,\n [],\n )\n\n // Class-level Context Contributors (#107) — applied to every method on the\n // controller. Method-level contributors are added per-route below.\n const classContributors: ContributorRegistration[] = getClassMeta<ContributorRegistration[]>(\n METADATA.CLASS_CONTRIBUTORS,\n controllerClass,\n [],\n )\n\n for (const route of routes) {\n const method = route.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch'\n const fullPath = route.path || '/'\n\n // Method-level middleware\n const methodMiddlewares: MiddlewareHandler[] = getMethodMeta<MiddlewareHandler[]>(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass,\n route.handlerName,\n [],\n )\n\n // Build handler chain\n const handlers: any[] = []\n\n // Validation middleware (shared with standalone validate() export)\n if (route.validation) {\n handlers.push(validate(route.validation))\n }\n\n // @FileUpload decorator — auto-attach upload middleware from metadata\n const fileUploadConfig = getMethodMetaOrUndefined<FileUploadConfig>(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n if (fileUploadConfig) {\n handlers.push(buildUploadMiddleware(fileUploadConfig))\n }\n\n // Class + method middleware (wrapped as Express middleware with error catching)\n for (const mw of [...classMiddlewares, ...methodMiddlewares]) {\n handlers.push((req: Request, res: Response, next: NextFunction) => {\n const ctx = new RequestContext(req, res, next)\n Promise.resolve(mw(ctx, next)).catch(next)\n })\n }\n\n // Context Contributor pipeline (#107) — class + method contributors,\n // built once at mount time so cycle/missing-dep failures abort boot.\n const methodContributors: ContributorRegistration[] = getMethodMeta<ContributorRegistration[]>(\n METADATA.METHOD_CONTRIBUTORS,\n controllerClass,\n route.handlerName,\n [],\n )\n\n if (\n classContributors.length > 0 ||\n methodContributors.length > 0 ||\n externalSources.length > 0\n ) {\n // Labels are surfaced in DuplicateContributorError messages — include\n // the registration's key + the original index so a same-key collision\n // at one precedence level points at the conflicting decorator slot,\n // not just the host method/class.\n const sources: SourcedRegistration[] = [\n ...methodContributors.map(\n (registration, i): SourcedRegistration => ({\n source: 'method',\n registration,\n label: `${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`,\n }),\n ),\n ...classContributors.map(\n (registration, i): SourcedRegistration => ({\n source: 'class',\n registration,\n label: `${controllerClass.name}.@class#${i}(${registration.key})`,\n }),\n ),\n ...externalSources,\n ]\n const pipeline = buildPipeline(sources, {\n route: `${route.method.toUpperCase()} ${fullPath}`,\n })\n\n handlers.push(async (req: Request, res: Response, next: NextFunction) => {\n const ctx = new RequestContext(req, res, next)\n try {\n await runContributors({ pipeline, ctx, container })\n next()\n } catch (err) {\n next(err)\n }\n })\n }\n\n // Main handler — resolve controller per-request to respect DI scoping\n handlers.push(async (req: Request, res: Response, next: NextFunction) => {\n const ctx = new RequestContext(req, res, next)\n try {\n const controller = container.resolve(controllerClass)\n await controller[route.handlerName](ctx)\n } catch (err: any) {\n next(err)\n }\n })\n ;(router as any)[method](fullPath, ...handlers)\n }\n\n return router\n}\n"],"mappings":";;;;;;;;;;sdAiCA,IAAI,4BAA8D,EAAE,CAGpE,SAAgB,+BAA+B,QAA+C,CAC5F,4BAA8B,QAwBhC,SAAgB,YAAY,gBAAsB,QAA8B,EAAE,CAAU,CAC1F,IAAM,OAAS,QAAQ,CACjB,UAAY,UAAU,aAAa,CACnC,gBAAkB,QAAQ,iBAAmB,4BAC7C,OAA4B,aAChC,SAAS,OACT,gBACA,EAAE,CACH,CAGK,iBAAwC,aAC5C,SAAS,kBACT,gBACA,EAAE,CACH,CAIK,kBAA+C,aACnD,SAAS,mBACT,gBACA,EAAE,CACH,CAED,IAAK,IAAM,SAAS,OAAQ,CAC1B,IAAM,OAAS,MAAM,OAAO,aAAa,CACnC,SAAW,MAAM,MAAQ,IAGzB,kBAAyC,cAC7C,SAAS,mBACT,gBACA,MAAM,YACN,EAAE,CACH,CAGK,SAAkB,EAAE,CAGtB,MAAM,YACR,SAAS,KAAK,SAAS,MAAM,WAAW,CAAC,CAI3C,IAAM,iBAAmB,yBACvB,SAAS,YACT,gBACA,MAAM,YACP,CACG,kBACF,SAAS,KAAK,sBAAsB,iBAAiB,CAAC,CAIxD,IAAK,IAAM,KAAM,CAAC,GAAG,iBAAkB,GAAG,kBAAkB,CAC1D,SAAS,MAAM,IAAc,IAAe,OAAuB,CACjE,IAAM,IAAM,IAAI,eAAe,IAAK,IAAK,KAAK,CAC9C,QAAQ,QAAQ,GAAG,IAAK,KAAK,CAAC,CAAC,MAAM,KAAK,EAC1C,CAKJ,IAAM,mBAAgD,cACpD,SAAS,oBACT,gBACA,MAAM,YACN,EAAE,CACH,CAED,GACE,kBAAkB,OAAS,GAC3B,mBAAmB,OAAS,GAC5B,gBAAgB,OAAS,EACzB,CAsBA,IAAM,SAAW,
|
|
1
|
+
{"version":3,"file":"router-builder.mjs","names":[],"sources":["../../src/http/router-builder.ts"],"sourcesContent":["import { Router, type Request, type Response, type NextFunction } from 'express'\nimport {\n buildPipeline,\n Container,\n METADATA,\n runContributors,\n type ContributorRegistration,\n type FileUploadConfig,\n type MiddlewareHandler,\n type RouteDefinition,\n type SourcedRegistration,\n} from '../core'\nimport { getClassMeta, getMethodMeta, getMethodMetaOrUndefined } from '../core/metadata'\nimport { RequestContext } from './context'\nimport { validate } from './middleware/validate'\nimport { buildUploadMiddleware } from './middleware/upload'\n\n/**\n * Per-module SourcedRegistration[] threaded through the route-mount loop by\n * Application.setup(). Carries module + adapter + global contributors so\n * buildRoutes() can merge them with class + method ones into a single pipeline.\n *\n * Module setup is sequential, so this slot is race-free. Cleared in a finally\n * block after each module mounts. Outside of Application setup the slot is\n * empty — direct buildRoutes() callers (mostly tests) see only class + method\n * contributors unless they pass `externalSources` explicitly.\n *\n * Same idiom as `Container._requestStoreProvider` and `Logger._contextProvider`:\n * an internal escape hatch for cross-module wiring without inversion-of-control\n * gymnastics.\n *\n * @internal\n */\nlet _externalContributorSources: readonly SourcedRegistration[] = []\n\n/** @internal — set by Application.setup() before each `mod.routes()` call. */\nexport function _setExternalContributorSources(sources: readonly SourcedRegistration[]): void {\n _externalContributorSources = sources\n}\n\nexport interface BuildRoutesOptions {\n /**\n * Extra contributors to merge into the per-route pipeline at their declared\n * precedence levels. Pass explicitly when calling buildRoutes outside the\n * Application route-mount loop (typically in tests). When omitted, falls\n * back to the slot set by Application.setup().\n */\n externalSources?: readonly SourcedRegistration[]\n}\n\n/**\n * Build an Express Router from a controller class decorated with @Get, @Post, etc.\n * Resolves the controller from the DI container, wraps handlers in RequestContext,\n * and applies class-level and method-level middleware.\n *\n * Routes are registered using only the method-level decorator paths (e.g. @Get('/me') → '/me').\n * The @Controller path is NOT baked into the router — it serves as metadata only\n * (used by Swagger and other adapters for introspection).\n * The module's routes().path is the single source of truth for the mount prefix,\n * which avoids path doubling when both the module and controller specify the same path.\n */\nexport function buildRoutes(controllerClass: any, options: BuildRoutesOptions = {}): Router {\n const router = Router()\n const container = Container.getInstance()\n const externalSources = options.externalSources ?? _externalContributorSources\n const routes: RouteDefinition[] = getClassMeta<RouteDefinition[]>(\n METADATA.ROUTES,\n controllerClass,\n [],\n )\n\n // Class-level middleware\n const classMiddlewares: MiddlewareHandler[] = getClassMeta<MiddlewareHandler[]>(\n METADATA.CLASS_MIDDLEWARES,\n controllerClass,\n [],\n )\n\n // Class-level Context Contributors (#107) — applied to every method on the\n // controller. Method-level contributors are added per-route below.\n const classContributors: ContributorRegistration[] = getClassMeta<ContributorRegistration[]>(\n METADATA.CLASS_CONTRIBUTORS,\n controllerClass,\n [],\n )\n\n for (const route of routes) {\n const method = route.method.toLowerCase() as 'get' | 'post' | 'put' | 'delete' | 'patch'\n const fullPath = route.path || '/'\n\n // Method-level middleware\n const methodMiddlewares: MiddlewareHandler[] = getMethodMeta<MiddlewareHandler[]>(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass,\n route.handlerName,\n [],\n )\n\n // Build handler chain\n const handlers: any[] = []\n\n // Validation middleware (shared with standalone validate() export)\n if (route.validation) {\n handlers.push(validate(route.validation))\n }\n\n // @FileUpload decorator — auto-attach upload middleware from metadata\n const fileUploadConfig = getMethodMetaOrUndefined<FileUploadConfig>(\n METADATA.FILE_UPLOAD,\n controllerClass,\n route.handlerName,\n )\n if (fileUploadConfig) {\n handlers.push(buildUploadMiddleware(fileUploadConfig))\n }\n\n // Class + method middleware (wrapped as Express middleware with error catching)\n for (const mw of [...classMiddlewares, ...methodMiddlewares]) {\n handlers.push((req: Request, res: Response, next: NextFunction) => {\n const ctx = new RequestContext(req, res, next)\n Promise.resolve(mw(ctx, next)).catch(next)\n })\n }\n\n // Context Contributor pipeline (#107) — class + method contributors,\n // built once at mount time so cycle/missing-dep failures abort boot.\n const methodContributors: ContributorRegistration[] = getMethodMeta<ContributorRegistration[]>(\n METADATA.METHOD_CONTRIBUTORS,\n controllerClass,\n route.handlerName,\n [],\n )\n\n if (\n classContributors.length > 0 ||\n methodContributors.length > 0 ||\n externalSources.length > 0\n ) {\n // Labels are surfaced in DuplicateContributorError messages — include\n // the registration's key + the original index so a same-key collision\n // at one precedence level points at the conflicting decorator slot,\n // not just the host method/class.\n const sources: SourcedRegistration[] = [\n ...methodContributors.map(\n (registration, i): SourcedRegistration => ({\n source: 'method',\n registration,\n label: `${controllerClass.name}.${String(route.handlerName)}#${i}(${registration.key})`,\n }),\n ),\n ...classContributors.map(\n (registration, i): SourcedRegistration => ({\n source: 'class',\n registration,\n label: `${controllerClass.name}.@class#${i}(${registration.key})`,\n }),\n ),\n ...externalSources,\n ]\n const pipeline = buildPipeline(sources, {\n route: `${route.method.toUpperCase()} ${fullPath}`,\n })\n\n handlers.push(async (req: Request, res: Response, next: NextFunction) => {\n const ctx = new RequestContext(req, res, next)\n try {\n await runContributors({ pipeline, ctx, container })\n next()\n } catch (err) {\n next(err)\n }\n })\n }\n\n // Main handler — resolve controller per-request to respect DI scoping\n handlers.push(async (req: Request, res: Response, next: NextFunction) => {\n const ctx = new RequestContext(req, res, next)\n try {\n const controller = container.resolve(controllerClass)\n await controller[route.handlerName](ctx)\n } catch (err: any) {\n next(err)\n }\n })\n ;(router as any)[method](fullPath, ...handlers)\n }\n\n return router\n}\n"],"mappings":";;;;;;;;;;sdAiCA,IAAI,4BAA8D,EAAE,CAGpE,SAAgB,+BAA+B,QAA+C,CAC5F,4BAA8B,QAwBhC,SAAgB,YAAY,gBAAsB,QAA8B,EAAE,CAAU,CAC1F,IAAM,OAAS,QAAQ,CACjB,UAAY,UAAU,aAAa,CACnC,gBAAkB,QAAQ,iBAAmB,4BAC7C,OAA4B,aAChC,SAAS,OACT,gBACA,EAAE,CACH,CAGK,iBAAwC,aAC5C,SAAS,kBACT,gBACA,EAAE,CACH,CAIK,kBAA+C,aACnD,SAAS,mBACT,gBACA,EAAE,CACH,CAED,IAAK,IAAM,SAAS,OAAQ,CAC1B,IAAM,OAAS,MAAM,OAAO,aAAa,CACnC,SAAW,MAAM,MAAQ,IAGzB,kBAAyC,cAC7C,SAAS,mBACT,gBACA,MAAM,YACN,EAAE,CACH,CAGK,SAAkB,EAAE,CAGtB,MAAM,YACR,SAAS,KAAK,SAAS,MAAM,WAAW,CAAC,CAI3C,IAAM,iBAAmB,yBACvB,SAAS,YACT,gBACA,MAAM,YACP,CACG,kBACF,SAAS,KAAK,sBAAsB,iBAAiB,CAAC,CAIxD,IAAK,IAAM,KAAM,CAAC,GAAG,iBAAkB,GAAG,kBAAkB,CAC1D,SAAS,MAAM,IAAc,IAAe,OAAuB,CACjE,IAAM,IAAM,IAAI,eAAe,IAAK,IAAK,KAAK,CAC9C,QAAQ,QAAQ,GAAG,IAAK,KAAK,CAAC,CAAC,MAAM,KAAK,EAC1C,CAKJ,IAAM,mBAAgD,cACpD,SAAS,oBACT,gBACA,MAAM,YACN,EAAE,CACH,CAED,GACE,kBAAkB,OAAS,GAC3B,mBAAmB,OAAS,GAC5B,gBAAgB,OAAS,EACzB,CAsBA,IAAM,SAAW,cAAc,CAhB7B,GAAG,mBAAmB,KACnB,aAAc,KAA4B,CACzC,OAAQ,SACR,aACA,MAAO,GAAG,gBAAgB,KAAK,GAAG,OAAO,MAAM,YAAY,CAAC,GAAG,EAAE,GAAG,aAAa,IAAI,GACtF,EACF,CACD,GAAG,kBAAkB,KAClB,aAAc,KAA4B,CACzC,OAAQ,QACR,aACA,MAAO,GAAG,gBAAgB,KAAK,UAAU,EAAE,GAAG,aAAa,IAAI,GAChE,EACF,CACD,GAAG,gBAEiC,CAAE,CACtC,MAAO,GAAG,MAAM,OAAO,aAAa,CAAC,GAAG,WACzC,CAAC,CAEF,SAAS,KAAK,MAAO,IAAc,IAAe,OAAuB,CACvE,IAAM,IAAM,IAAI,eAAe,IAAK,IAAK,KAAK,CAC9C,GAAI,CACF,MAAM,gBAAgB,CAAE,SAAU,IAAK,UAAW,CAAC,CACnD,MAAM,OACC,IAAK,CACZ,KAAK,IAAI,GAEX,CAIJ,SAAS,KAAK,MAAO,IAAc,IAAe,OAAuB,CACvE,IAAM,IAAM,IAAI,eAAe,IAAK,IAAK,KAAK,CAC9C,GAAI,CAEF,MADmB,UAAU,QAAQ,gBACrB,CAAC,MAAM,aAAa,IAAI,OACjC,IAAU,CACjB,KAAK,IAAI,GAEX,CACA,OAAe,QAAQ,SAAU,GAAG,SAAS,CAGjD,OAAO"}
|