@forinda/kickjs-http 0.4.3 → 0.5.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/application.d.ts +1 -1
- package/dist/application.js +1 -1
- package/dist/bootstrap.js +2 -2
- package/dist/{chunk-OWLI3SBW.js → chunk-D76WCWAW.js} +2 -2
- package/dist/{chunk-LQ6RSWMX.js → chunk-GDMORQ2P.js} +44 -2
- package/dist/chunk-GDMORQ2P.js.map +1 -0
- package/dist/{chunk-4G2S7T4R.js → chunk-OKB76LY2.js} +20 -26
- package/dist/chunk-OKB76LY2.js.map +1 -0
- package/dist/{chunk-VFVMIFNZ.js → chunk-S7DJ5HF3.js} +2 -2
- package/dist/{chunk-VXX2Y3TA.js → chunk-WYY34UWG.js} +1 -1
- package/dist/chunk-WYY34UWG.js.map +1 -0
- package/dist/{chunk-DUQ7SN7N.js → chunk-YYT24FTH.js} +24 -6
- package/dist/chunk-YYT24FTH.js.map +1 -0
- package/dist/context.d.ts +29 -1
- package/dist/context.js +2 -2
- package/dist/devtools.d.ts +3 -1
- package/dist/devtools.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +6 -6
- package/dist/query/index.d.ts +2 -2
- package/dist/query/index.js +1 -1
- package/dist/router-builder.js +3 -3
- package/dist/{types-Doz6f3AB.d.ts → types-DsbCdE8f.d.ts} +13 -1
- package/package.json +2 -2
- package/dist/chunk-4G2S7T4R.js.map +0 -1
- package/dist/chunk-DUQ7SN7N.js.map +0 -1
- package/dist/chunk-LQ6RSWMX.js.map +0 -1
- package/dist/chunk-VXX2Y3TA.js.map +0 -1
- /package/dist/{chunk-OWLI3SBW.js.map → chunk-D76WCWAW.js.map} +0 -0
- /package/dist/{chunk-VFVMIFNZ.js.map → chunk-S7DJ5HF3.js.map} +0 -0
package/dist/application.d.ts
CHANGED
|
@@ -73,7 +73,7 @@ declare class Application {
|
|
|
73
73
|
* 10. Adapter beforeStart hooks
|
|
74
74
|
*/
|
|
75
75
|
setup(): void;
|
|
76
|
-
/** Start the HTTP server
|
|
76
|
+
/** Start the HTTP server — fails fast if port is in use */
|
|
77
77
|
start(): void;
|
|
78
78
|
/** HMR rebuild: swap Express handler without restarting the server */
|
|
79
79
|
rebuild(): void;
|
package/dist/application.js
CHANGED
package/dist/bootstrap.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
Application
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-OKB76LY2.js";
|
|
4
4
|
import {
|
|
5
5
|
__name,
|
|
6
6
|
__require
|
|
@@ -57,4 +57,4 @@ __name(bootstrap, "bootstrap");
|
|
|
57
57
|
export {
|
|
58
58
|
bootstrap
|
|
59
59
|
};
|
|
60
|
-
//# sourceMappingURL=chunk-
|
|
60
|
+
//# sourceMappingURL=chunk-D76WCWAW.js.map
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import {
|
|
2
2
|
parseQuery
|
|
3
|
-
} from "./chunk-
|
|
3
|
+
} from "./chunk-WYY34UWG.js";
|
|
4
4
|
import {
|
|
5
5
|
__name
|
|
6
6
|
} from "./chunk-WCQVDF3K.js";
|
|
@@ -106,9 +106,51 @@ var RequestContext = class {
|
|
|
106
106
|
this.res.setHeader("Content-Type", contentType);
|
|
107
107
|
return this.res.send(buffer);
|
|
108
108
|
}
|
|
109
|
+
/**
|
|
110
|
+
* Parse query params and return a standardized paginated response.
|
|
111
|
+
* Calls `ctx.qs()` internally, then wraps your data with pagination meta.
|
|
112
|
+
*
|
|
113
|
+
* @param fetcher - Async function that receives ParsedQuery and returns `{ data, total }`
|
|
114
|
+
* @param fieldConfig - Optional whitelist for filterable, sortable, searchable fields
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* @Get('/')
|
|
119
|
+
* async list(ctx: RequestContext) {
|
|
120
|
+
* return ctx.paginate(
|
|
121
|
+
* async (parsed) => {
|
|
122
|
+
* const data = await db.select().from(users)
|
|
123
|
+
* .where(query.where).limit(parsed.pagination.limit)
|
|
124
|
+
* .offset(parsed.pagination.offset).all()
|
|
125
|
+
* const total = await db.select({ count: count() }).from(users).get()
|
|
126
|
+
* return { data, total: total?.count ?? 0 }
|
|
127
|
+
* },
|
|
128
|
+
* { filterable: ['name', 'role'], sortable: ['createdAt'] },
|
|
129
|
+
* )
|
|
130
|
+
* }
|
|
131
|
+
* ```
|
|
132
|
+
*/
|
|
133
|
+
async paginate(fetcher, fieldConfig) {
|
|
134
|
+
const parsed = this.qs(fieldConfig);
|
|
135
|
+
const { data, total } = await fetcher(parsed);
|
|
136
|
+
const { page, limit } = parsed.pagination;
|
|
137
|
+
const totalPages = Math.ceil(total / limit) || 1;
|
|
138
|
+
const response = {
|
|
139
|
+
data,
|
|
140
|
+
meta: {
|
|
141
|
+
page,
|
|
142
|
+
limit,
|
|
143
|
+
total,
|
|
144
|
+
totalPages,
|
|
145
|
+
hasNext: page < totalPages,
|
|
146
|
+
hasPrev: page > 1
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
return this.json(response);
|
|
150
|
+
}
|
|
109
151
|
};
|
|
110
152
|
|
|
111
153
|
export {
|
|
112
154
|
RequestContext
|
|
113
155
|
};
|
|
114
|
-
//# sourceMappingURL=chunk-
|
|
156
|
+
//# sourceMappingURL=chunk-GDMORQ2P.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/context.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport {\n parseQuery,\n type ParsedQuery,\n type QueryFieldConfig,\n type PaginatedResponse,\n} from './query'\n\n/**\n * Unified request/response abstraction passed to every controller method.\n * Shields handlers from raw Express objects and provides convenience methods.\n */\nexport class RequestContext<TBody = any, TParams = any, TQuery = any> {\n private metadata = new Map<string, any>()\n\n constructor(\n public readonly req: Request,\n public readonly res: Response,\n public readonly next: NextFunction,\n ) {}\n\n // ── Request Data ────────────────────────────────────────────────────\n\n get body(): TBody {\n return this.req.body as TBody\n }\n\n get params(): TParams {\n return this.req.params as TParams\n }\n\n get query(): TQuery {\n return this.req.query as TQuery\n }\n\n get headers() {\n return 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 /** Session data (requires session middleware) */\n get session(): any {\n return (this.req as any).session\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 * @param fieldConfig - Optional whitelist for filterable, sortable, and searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * const parsed = ctx.qs({\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt', 'title'],\n * })\n * const q = drizzleAdapter.build(parsed, { columns })\n * // ... use q.where, q.orderBy, q.limit, q.offset\n * }\n * ```\n */\n qs(fieldConfig?: QueryFieldConfig): ParsedQuery {\n return parseQuery(this.req.query as Record<string, any>, fieldConfig)\n }\n\n // ── File Uploads ────────────────────────────────────────────────────\n\n /** Single uploaded file (requires @FileUpload({ mode: 'single' })) */\n get file(): any {\n return (this.req as any).file\n }\n\n /** Array of uploaded files (requires @FileUpload({ mode: 'array' })) */\n get files(): any[] | undefined {\n return (this.req as any).files\n }\n\n // ── Metadata Store ──────────────────────────────────────────────────\n\n get<T = any>(key: string): T | undefined {\n return this.metadata.get(key) as T | undefined\n }\n\n set(key: string, value: any): void {\n this.metadata.set(key, value)\n }\n\n // ── Response Helpers ────────────────────────────────────────────────\n\n json(data: any, status = 200) {\n return this.res.status(status).json(data)\n }\n\n created(data: any) {\n return this.res.status(201).json(data)\n }\n\n noContent() {\n return this.res.status(204).end()\n }\n\n notFound(message = 'Not Found') {\n return this.res.status(404).json({ message })\n }\n\n badRequest(message: string) {\n return this.res.status(400).json({ message })\n }\n\n html(content: string, status = 200) {\n return this.res.status(status).type('html').send(content)\n }\n\n download(buffer: Buffer, filename: string, contentType = 'application/octet-stream') {\n this.res.setHeader('Content-Disposition', `attachment; filename=\"${filename}\"`)\n this.res.setHeader('Content-Type', contentType)\n return this.res.send(buffer)\n }\n\n /**\n * 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>(\n fetcher: (parsed: ParsedQuery) => Promise<{ data: T[]; total: number }>,\n fieldConfig?: QueryFieldConfig,\n ) {\n const parsed = this.qs(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"],"mappings":";;;;;;;;AAYO,IAAMA,iBAAN,MAAMA;EAXb,OAWaA;;;;;;EACHC,WAAW,oBAAIC,IAAAA;EAEvB,YACkBC,KACAC,KACAC,MAChB;SAHgBF,MAAAA;SACAC,MAAAA;SACAC,OAAAA;EACf;;EAIH,IAAIC,OAAc;AAChB,WAAO,KAAKH,IAAIG;EAClB;EAEA,IAAIC,SAAkB;AACpB,WAAO,KAAKJ,IAAII;EAClB;EAEA,IAAIC,QAAgB;AAClB,WAAO,KAAKL,IAAIK;EAClB;EAEA,IAAIC,UAAU;AACZ,WAAO,KAAKN,IAAIM;EAClB;EAEA,IAAIC,YAAgC;AAClC,WAAQ,KAAKP,IAAYO,aAAc,KAAKP,IAAIM,QAAQ,cAAA;EAC1D;;EAGA,IAAIE,UAAe;AACjB,WAAQ,KAAKR,IAAYQ;EAC3B;;;;;;;;;;;;;;;;;;;;;EAuBAC,GAAGC,aAA6C;AAC9C,WAAOC,WAAW,KAAKX,IAAIK,OAA8BK,WAAAA;EAC3D;;;EAKA,IAAIE,OAAY;AACd,WAAQ,KAAKZ,IAAYY;EAC3B;;EAGA,IAAIC,QAA2B;AAC7B,WAAQ,KAAKb,IAAYa;EAC3B;;EAIAC,IAAaC,KAA4B;AACvC,WAAO,KAAKjB,SAASgB,IAAIC,GAAAA;EAC3B;EAEAC,IAAID,KAAaE,OAAkB;AACjC,SAAKnB,SAASkB,IAAID,KAAKE,KAAAA;EACzB;;EAIAC,KAAKC,MAAWC,SAAS,KAAK;AAC5B,WAAO,KAAKnB,IAAImB,OAAOA,MAAAA,EAAQF,KAAKC,IAAAA;EACtC;EAEAE,QAAQF,MAAW;AACjB,WAAO,KAAKlB,IAAImB,OAAO,GAAA,EAAKF,KAAKC,IAAAA;EACnC;EAEAG,YAAY;AACV,WAAO,KAAKrB,IAAImB,OAAO,GAAA,EAAKG,IAAG;EACjC;EAEAC,SAASC,UAAU,aAAa;AAC9B,WAAO,KAAKxB,IAAImB,OAAO,GAAA,EAAKF,KAAK;MAAEO;IAAQ,CAAA;EAC7C;EAEAC,WAAWD,SAAiB;AAC1B,WAAO,KAAKxB,IAAImB,OAAO,GAAA,EAAKF,KAAK;MAAEO;IAAQ,CAAA;EAC7C;EAEAE,KAAKC,SAAiBR,SAAS,KAAK;AAClC,WAAO,KAAKnB,IAAImB,OAAOA,MAAAA,EAAQS,KAAK,MAAA,EAAQC,KAAKF,OAAAA;EACnD;EAEAG,SAASC,QAAgBC,UAAkBC,cAAc,4BAA4B;AACnF,SAAKjC,IAAIkC,UAAU,uBAAuB,yBAAyBF,QAAAA,GAAW;AAC9E,SAAKhC,IAAIkC,UAAU,gBAAgBD,WAAAA;AACnC,WAAO,KAAKjC,IAAI6B,KAAKE,MAAAA;EACvB;;;;;;;;;;;;;;;;;;;;;;;;;EA0BA,MAAMI,SACJC,SACA3B,aACA;AACA,UAAM4B,SAAS,KAAK7B,GAAGC,WAAAA;AACvB,UAAM,EAAES,MAAMoB,MAAK,IAAK,MAAMF,QAAQC,MAAAA;AACtC,UAAM,EAAEE,MAAMC,MAAK,IAAKH,OAAOI;AAC/B,UAAMC,aAAaC,KAAKC,KAAKN,QAAQE,KAAAA,KAAU;AAE/C,UAAMK,WAAiC;MACrC3B;MACA4B,MAAM;QACJP;QACAC;QACAF;QACAI;QACAK,SAASR,OAAOG;QAChBM,SAAST,OAAO;MAClB;IACF;AAEA,WAAO,KAAKtB,KAAK4B,QAAAA;EACnB;AACF;","names":["RequestContext","metadata","Map","req","res","next","body","params","query","headers","requestId","session","qs","fieldConfig","parseQuery","file","files","get","key","set","value","json","data","status","created","noContent","end","notFound","message","badRequest","html","content","type","send","download","buffer","filename","contentType","setHeader","paginate","fetcher","parsed","total","page","limit","pagination","totalPages","Math","ceil","response","meta","hasNext","hasPrev"]}
|
|
@@ -94,33 +94,27 @@ var Application = class {
|
|
|
94
94
|
adapter.beforeStart?.(this.app, this.container);
|
|
95
95
|
}
|
|
96
96
|
}
|
|
97
|
-
/** Start the HTTP server
|
|
97
|
+
/** Start the HTTP server — fails fast if port is in use */
|
|
98
98
|
start() {
|
|
99
99
|
this.setup();
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
for (const adapter of this.adapters) {
|
|
119
|
-
adapter.afterStart?.(this.httpServer, this.container);
|
|
120
|
-
}
|
|
121
|
-
});
|
|
122
|
-
}, "tryListen");
|
|
123
|
-
tryListen(basePort, 0);
|
|
100
|
+
const port = this.options.port ?? parseInt(process.env.PORT || "3000", 10);
|
|
101
|
+
this.httpServer = http.createServer(this.app);
|
|
102
|
+
this.httpServer.on("error", (err) => {
|
|
103
|
+
if (err.code === "EADDRINUSE") {
|
|
104
|
+
log.error(`Port ${port} is already in use. Kill the existing process or use a different port:
|
|
105
|
+
PORT=${port + 1} kick dev
|
|
106
|
+
lsof -i :${port} # find what's using it
|
|
107
|
+
kill <PID> # stop it`);
|
|
108
|
+
process.exit(1);
|
|
109
|
+
}
|
|
110
|
+
throw err;
|
|
111
|
+
});
|
|
112
|
+
this.httpServer.listen(port, () => {
|
|
113
|
+
log.info(`Server running on http://localhost:${port}`);
|
|
114
|
+
for (const adapter of this.adapters) {
|
|
115
|
+
adapter.afterStart?.(this.httpServer, this.container);
|
|
116
|
+
}
|
|
117
|
+
});
|
|
124
118
|
}
|
|
125
119
|
/** HMR rebuild: swap Express handler without restarting the server */
|
|
126
120
|
rebuild() {
|
|
@@ -193,4 +187,4 @@ var Application = class {
|
|
|
193
187
|
export {
|
|
194
188
|
Application
|
|
195
189
|
};
|
|
196
|
-
//# sourceMappingURL=chunk-
|
|
190
|
+
//# sourceMappingURL=chunk-OKB76LY2.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/application.ts"],"sourcesContent":["import http from 'node:http'\nimport express, { type Express, type RequestHandler } from 'express'\nimport {\n Container,\n createLogger,\n type AppModuleClass,\n type AppAdapter,\n type AdapterMiddleware,\n} from '@forinda/kickjs-core'\nimport { buildRoutes } from './router-builder'\nimport { requestId } from './middleware/request-id'\nimport { notFoundHandler, errorHandler } from './middleware/error-handler'\n\nconst log = createLogger('Application')\n\n/**\n * A middleware entry in the declarative pipeline.\n * Can be a bare handler or an object with path scoping.\n */\nexport type MiddlewareEntry = RequestHandler | { path: string; handler: RequestHandler }\n\nexport interface ApplicationOptions {\n /** Feature modules to load */\n modules: AppModuleClass[]\n /** Adapters that hook into the lifecycle (DB, Redis, Swagger, etc.) */\n adapters?: AppAdapter[]\n /** Server port (falls back to PORT env var, then 3000) */\n port?: number\n /** Global API prefix (default: '/api') */\n apiPrefix?: string\n /** Default API version (default: 1) — routes become /{prefix}/v{version}/{path} */\n defaultVersion?: number\n\n /**\n * Global middleware pipeline. Declared in order.\n * Replaces the hardcoded middleware stack — you control exactly what runs.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * middleware: [\n * helmet(),\n * cors(),\n * compression(),\n * morgan('dev'),\n * express.json({ limit: '1mb' }),\n * ],\n * })\n * ```\n *\n * If omitted, a sensible default is applied:\n * requestId(), express.json({ limit: '100kb' })\n */\n middleware?: MiddlewareEntry[]\n\n /** Express `trust proxy` setting */\n trustProxy?: boolean | number | string | ((ip: string, hopIndex: number) => boolean)\n /** Maximum JSON body size (only used when middleware is not provided) */\n jsonLimit?: string | number\n}\n\n/**\n * The main application class. Wires together Express, the DI container,\n * feature modules, adapters, and the middleware pipeline.\n */\nexport class Application {\n private app: Express\n private container: Container\n private httpServer: http.Server | null = null\n private adapters: AppAdapter[]\n\n constructor(private readonly options: ApplicationOptions) {\n this.app = express()\n this.container = Container.getInstance()\n this.adapters = options.adapters ?? []\n }\n\n /**\n * Full setup pipeline:\n * 1. Adapter beforeMount hooks (early routes — docs, health)\n * 2. Adapter middleware (phase: beforeGlobal)\n * 3. Global middleware (user-declared or defaults)\n * 4. Adapter middleware (phase: afterGlobal)\n * 5. Module registration + DI bootstrap\n * 6. Adapter middleware (phase: beforeRoutes)\n * 7. Module route mounting\n * 8. Adapter middleware (phase: afterRoutes)\n * 9. Error handlers (notFound + global)\n * 10. Adapter beforeStart hooks\n */\n setup(): void {\n log.info('Bootstrapping application...')\n\n // Collect adapter middleware by phase\n const adapterMw = this.collectAdapterMiddleware()\n\n // ── 1. Adapter beforeMount hooks ──────────────────────────────────\n for (const adapter of this.adapters) {\n adapter.beforeMount?.(this.app, this.container)\n }\n\n // ── 2. Hardened defaults ──────────────────────────────────────────\n this.app.disable('x-powered-by')\n this.app.set('trust proxy', this.options.trustProxy ?? false)\n\n // ── 3. Adapter middleware: beforeGlobal ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeGlobal)\n\n // ── 4. Global middleware ─────────────────────────────────────────\n if (this.options.middleware) {\n // User-declared pipeline — full control\n for (const entry of this.options.middleware) {\n this.mountMiddlewareEntry(entry)\n }\n } else {\n // Sensible defaults when no middleware declared\n this.app.use(requestId())\n this.app.use(express.json({ limit: this.options.jsonLimit ?? '100kb' }))\n }\n\n // ── 5. Adapter middleware: afterGlobal ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterGlobal)\n\n // ── 6. Module registration + DI bootstrap ────────────────────────\n const modules = this.options.modules.map((ModuleClass) => {\n const mod = new ModuleClass()\n mod.register(this.container)\n return mod\n })\n this.container.bootstrap()\n\n // ── 7. Adapter middleware: beforeRoutes ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeRoutes)\n\n // ── 8. Mount module routes with versioning ───────────────────────\n const apiPrefix = this.options.apiPrefix ?? '/api'\n const defaultVersion = this.options.defaultVersion ?? 1\n\n for (const mod of modules) {\n const result = mod.routes()\n const routeSets = Array.isArray(result) ? result : [result]\n\n for (const route of routeSets) {\n const version = route.version ?? defaultVersion\n const mountPath = `${apiPrefix}/v${version}${route.path}`\n this.app.use(mountPath, route.router)\n\n // Notify adapters (e.g. SwaggerAdapter for OpenAPI spec generation)\n if (route.controller) {\n for (const adapter of this.adapters) {\n adapter.onRouteMount?.(route.controller, mountPath)\n }\n }\n }\n }\n\n // ── 9. Adapter middleware: afterRoutes ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterRoutes)\n\n // ── 10. Error handlers ───────────────────────────────────────────\n this.app.use(notFoundHandler())\n this.app.use(errorHandler())\n\n // ── 11. Adapter beforeStart hooks ────────────────────────────────\n for (const adapter of this.adapters) {\n adapter.beforeStart?.(this.app, this.container)\n }\n }\n\n /** Start the HTTP server — fails fast if port is in use */\n start(): void {\n this.setup()\n\n const port = this.options.port ?? parseInt(process.env.PORT || '3000', 10)\n this.httpServer = http.createServer(this.app)\n\n this.httpServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE') {\n log.error(\n `Port ${port} is already in use. Kill the existing process or use a different port:\\n` +\n ` PORT=${port + 1} kick dev\\n` +\n ` lsof -i :${port} # find what's using it\\n` +\n ` kill <PID> # stop it`,\n )\n process.exit(1)\n }\n throw err\n })\n\n this.httpServer.listen(port, () => {\n log.info(`Server running on http://localhost:${port}`)\n\n for (const adapter of this.adapters) {\n adapter.afterStart?.(this.httpServer!, this.container)\n }\n })\n }\n\n /** HMR rebuild: swap Express handler without restarting the server */\n rebuild(): void {\n // Reset the DI container so singletons are re-created with fresh code\n Container.reset()\n this.container = Container.getInstance()\n\n this.app = express()\n this.setup()\n\n if (this.httpServer) {\n this.httpServer.removeAllListeners('request')\n this.httpServer.on('request', this.app)\n log.info('HMR: Express app rebuilt and swapped')\n }\n }\n\n /** Graceful shutdown — runs all adapter shutdowns in parallel, resilient to failures */\n async shutdown(): Promise<void> {\n log.info('Shutting down...')\n\n // Run all adapter shutdowns concurrently — don't let one failure block the rest\n const results = await Promise.allSettled(\n this.adapters.map((adapter) => Promise.resolve(adapter.shutdown?.())),\n )\n for (const result of results) {\n if (result.status === 'rejected') {\n log.error({ err: result.reason }, 'Adapter shutdown failed')\n }\n }\n\n if (this.httpServer) {\n await new Promise<void>((resolve) => this.httpServer!.close(() => resolve()))\n }\n }\n\n getExpressApp(): Express {\n return this.app\n }\n\n getHttpServer(): http.Server | null {\n return this.httpServer\n }\n\n // ── Internal helpers ────────────────────────────────────────────────\n\n private collectAdapterMiddleware() {\n const result = {\n beforeGlobal: [] as AdapterMiddleware[],\n afterGlobal: [] as AdapterMiddleware[],\n beforeRoutes: [] as AdapterMiddleware[],\n afterRoutes: [] as AdapterMiddleware[],\n }\n\n for (const adapter of this.adapters) {\n const entries = adapter.middleware?.() ?? []\n for (const entry of entries) {\n const phase = entry.phase ?? 'afterGlobal'\n result[phase].push(entry)\n }\n }\n\n return result\n }\n\n private mountMiddlewareList(entries: AdapterMiddleware[]): void {\n for (const entry of entries) {\n if (entry.path) {\n this.app.use(entry.path, entry.handler)\n } else {\n this.app.use(entry.handler)\n }\n }\n }\n\n private mountMiddlewareEntry(entry: MiddlewareEntry): void {\n if (typeof entry === 'function') {\n this.app.use(entry)\n } else {\n this.app.use(entry.path, entry.handler)\n }\n }\n}\n"],"mappings":"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","names":["http","express","Container","createLogger","log","createLogger","Application","app","container","httpServer","adapters","options","express","Container","getInstance","setup","info","adapterMw","collectAdapterMiddleware","adapter","beforeMount","disable","set","trustProxy","mountMiddlewareList","beforeGlobal","middleware","entry","mountMiddlewareEntry","use","requestId","json","limit","jsonLimit","afterGlobal","modules","map","ModuleClass","mod","register","bootstrap","beforeRoutes","apiPrefix","defaultVersion","result","routes","routeSets","Array","isArray","route","version","mountPath","path","router","controller","onRouteMount","afterRoutes","notFoundHandler","errorHandler","beforeStart","start","port","parseInt","process","env","PORT","http","createServer","on","err","code","error","exit","listen","afterStart","rebuild","reset","removeAllListeners","shutdown","results","Promise","allSettled","resolve","status","reason","close","getExpressApp","getHttpServer","entries","phase","push","handler"]}
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "./chunk-RPN7UFUO.js";
|
|
7
7
|
import {
|
|
8
8
|
RequestContext
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-GDMORQ2P.js";
|
|
10
10
|
import {
|
|
11
11
|
__name
|
|
12
12
|
} from "./chunk-WCQVDF3K.js";
|
|
@@ -66,4 +66,4 @@ export {
|
|
|
66
66
|
getControllerPath,
|
|
67
67
|
buildRoutes
|
|
68
68
|
};
|
|
69
|
-
//# sourceMappingURL=chunk-
|
|
69
|
+
//# sourceMappingURL=chunk-S7DJ5HF3.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/query/types.ts","../src/query/parse-query.ts"],"sourcesContent":["/** Supported filter operators for query string parsing */\nexport type FilterOperator =\n | 'eq'\n | 'neq'\n | 'gt'\n | 'gte'\n | 'lt'\n | 'lte'\n | 'between'\n | 'in'\n | 'contains'\n | 'starts'\n | 'ends'\n\nexport const FILTER_OPERATORS = new Set<string>([\n 'eq',\n 'neq',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n 'between',\n 'in',\n 'contains',\n 'starts',\n 'ends',\n])\n\nexport interface FilterItem {\n field: string\n operator: FilterOperator\n value: string\n}\n\nexport interface SortItem {\n field: string\n direction: 'asc' | 'desc'\n}\n\nexport interface PaginationParams {\n page: number\n limit: number\n offset: number\n}\n\n/**\n * The result of parsing a query string. ORM-agnostic — pass this to\n * a query builder adapter (Drizzle, Prisma, Sequelize, etc.) to produce\n * database-specific query objects.\n */\nexport interface ParsedQuery {\n filters: FilterItem[]\n sort: SortItem[]\n pagination: PaginationParams\n search: string\n}\n\n/**\n * Restrict which fields can be filtered, sorted, or searched.\n * Fields not in the allow-list are silently ignored.\n */\nexport interface QueryFieldConfig {\n filterable?: string[]\n sortable?: string[]\n searchable?: string[]\n}\n\n/** Standardized paginated response shape */\nexport interface PaginatedResponse<T = any> {\n data: T[]\n meta: {\n page: number\n limit: number\n total: number\n totalPages: number\n hasNext: boolean\n hasPrev: boolean\n }\n}\n\n/**\n * Interface for ORM-specific query builder adapters.\n * Implement this to translate a `ParsedQuery` into your ORM's query format.\n *\n * @example\n * ```ts\n * // Drizzle adapter\n * class DrizzleQueryAdapter implements QueryBuilderAdapter<DrizzleQueryResult> {\n * build(parsed: ParsedQuery, config: DrizzleConfig): DrizzleQueryResult {\n * // Convert filters → Drizzle SQL conditions\n * // Convert sort → Drizzle orderBy\n * return { where, orderBy, limit, offset }\n * }\n * }\n *\n * // Prisma adapter\n * class PrismaQueryAdapter implements QueryBuilderAdapter<PrismaQueryResult> {\n * build(parsed: ParsedQuery, config: PrismaConfig): PrismaQueryResult {\n * return { where, orderBy, skip, take }\n * }\n * }\n * ```\n */\nexport interface QueryBuilderAdapter<TResult = any, TConfig = any> {\n /** Human-readable name for debugging */\n readonly name: string\n\n /**\n * Convert a ParsedQuery into an ORM-specific query object.\n * @param parsed - The ORM-agnostic parsed query\n * @param config - ORM-specific configuration (column maps, search columns, etc.)\n */\n build(parsed: ParsedQuery, config: TConfig): TResult\n}\n","import {\n FILTER_OPERATORS,\n type FilterItem,\n type FilterOperator,\n type SortItem,\n type PaginationParams,\n type ParsedQuery,\n type QueryFieldConfig,\n} from './types'\n\nconst MAX_SEARCH_LENGTH = 200\nconst DEFAULT_PAGE = 1\nconst DEFAULT_LIMIT = 20\nconst MAX_LIMIT = 100\n\n// ── Individual parsers ──────────────────────────────────────────────────\n\n/** Parse filter strings like \"status:eq:active\" into structured objects */\nexport function parseFilters(\n filterParam: string | string[] | undefined,\n allowedFields?: string[],\n): FilterItem[] {\n if (!filterParam) return []\n\n const items = Array.isArray(filterParam) ? filterParam : [filterParam]\n const results: FilterItem[] = []\n const allowed = allowedFields ? new Set(allowedFields) : null\n\n for (const item of items) {\n // Split on first two colons only — value may contain colons (e.g. timestamps)\n const firstColon = item.indexOf(':')\n if (firstColon === -1) continue\n\n const secondColon = item.indexOf(':', firstColon + 1)\n if (secondColon === -1) continue\n\n const field = item.slice(0, firstColon)\n const operator = item.slice(firstColon + 1, secondColon)\n const value = item.slice(secondColon + 1)\n\n if (!field || !value) continue\n if (!FILTER_OPERATORS.has(operator)) continue\n if (allowed && !allowed.has(field)) continue\n\n results.push({ field, operator: operator as FilterOperator, value })\n }\n\n return results\n}\n\n/** Parse sort strings like \"firstName:asc\" into structured objects */\nexport function parseSort(\n sortParam: string | string[] | undefined,\n allowedFields?: string[],\n): SortItem[] {\n if (!sortParam) return []\n\n const items = Array.isArray(sortParam) ? sortParam : [sortParam]\n const results: SortItem[] = []\n const allowed = allowedFields ? new Set(allowedFields) : null\n\n for (const item of items) {\n // Split on last colon so field names with colons work\n const lastColon = item.lastIndexOf(':')\n if (lastColon === -1) continue\n\n const field = item.slice(0, lastColon)\n const dir = item.slice(lastColon + 1).toLowerCase()\n\n if (!field) continue\n if (dir !== 'asc' && dir !== 'desc') continue\n if (allowed && !allowed.has(field)) continue\n\n results.push({ field, direction: dir })\n }\n\n return results\n}\n\n/** Parse page/limit into pagination with computed offset */\nexport function parsePagination(params: {\n page?: string | number\n limit?: string | number\n}): PaginationParams {\n let page =\n typeof params.page === 'string' ? parseInt(params.page, 10) : (params.page ?? DEFAULT_PAGE)\n let limit =\n typeof params.limit === 'string' ? parseInt(params.limit, 10) : (params.limit ?? DEFAULT_LIMIT)\n\n if (isNaN(page) || page < 1) page = DEFAULT_PAGE\n if (isNaN(limit) || limit < 1) limit = DEFAULT_LIMIT\n if (limit > MAX_LIMIT) limit = MAX_LIMIT\n\n return { page, limit, offset: (page - 1) * limit }\n}\n\n/** Sanitize and truncate search query */\nexport function parseSearchQuery(q: string | undefined): string {\n if (!q) return ''\n return q.trim().slice(0, MAX_SEARCH_LENGTH)\n}\n\n// ── Combined parser ─────────────────────────────────────────────────────\n\n/**\n * Parse a raw Express query object into a structured, ORM-agnostic ParsedQuery.\n *\n * @param query - Raw query string object from `req.query` or Zod-validated object\n * @param fieldConfig - Optional field restrictions (whitelist filterable/sortable/searchable)\n *\n * @example\n * ```ts\n * // In a controller\n * @Get('/')\n * async list(ctx: RequestContext) {\n * const parsed = parseQuery(ctx.query, {\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt', 'title'],\n * })\n * // Pass to your ORM query builder adapter\n * const q = drizzleAdapter.build(parsed, { columns, searchColumns })\n * }\n * ```\n *\n * Query string format:\n * - Filters: `?filter=field:operator:value` (repeatable)\n * - Sort: `?sort=field:asc|desc` (repeatable)\n * - Pagination: `?page=1&limit=20`\n * - Search: `?q=search+term`\n *\n * Filter operators: eq, neq, gt, gte, lt, lte, between, in, contains, starts, ends\n */\nexport function parseQuery(\n query: Record<string, any>,\n fieldConfig?: QueryFieldConfig,\n): ParsedQuery {\n return {\n filters: parseFilters(query.filter, fieldConfig?.filterable),\n sort: parseSort(query.sort, fieldConfig?.sortable),\n pagination: parsePagination({ page: query.page, limit: query.limit }),\n search: parseSearchQuery(query.q),\n }\n}\n\n// ── Query URL builder (for client-side or testing) ──────────────────────\n\n/** Convert ParsedQuery back into query string parameters */\nexport function buildQueryParams(\n parsed: Partial<ParsedQuery>,\n): Record<string, string | string[] | number> {\n const params: Record<string, string | string[] | number> = {}\n\n if (parsed.filters?.length) {\n params.filter = parsed.filters.map((f) => `${f.field}:${f.operator}:${f.value}`)\n }\n\n if (parsed.sort?.length) {\n params.sort = parsed.sort.map((s) => `${s.field}:${s.direction}`)\n }\n\n if (parsed.pagination) {\n params.page = parsed.pagination.page\n params.limit = parsed.pagination.limit\n }\n\n if (parsed.search) {\n params.q = parsed.search\n }\n\n return params\n}\n"],"mappings":";;;;;AAcO,IAAMA,mBAAmB,oBAAIC,IAAY;EAC9C;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;EACA;CACD;;;AChBD,IAAMC,oBAAoB;AAC1B,IAAMC,eAAe;AACrB,IAAMC,gBAAgB;AACtB,IAAMC,YAAY;AAKX,SAASC,aACdC,aACAC,eAAwB;AAExB,MAAI,CAACD,YAAa,QAAO,CAAA;AAEzB,QAAME,QAAQC,MAAMC,QAAQJ,WAAAA,IAAeA,cAAc;IAACA;;AAC1D,QAAMK,UAAwB,CAAA;AAC9B,QAAMC,UAAUL,gBAAgB,IAAIM,IAAIN,aAAAA,IAAiB;AAEzD,aAAWO,QAAQN,OAAO;AAExB,UAAMO,aAAaD,KAAKE,QAAQ,GAAA;AAChC,QAAID,eAAe,GAAI;AAEvB,UAAME,cAAcH,KAAKE,QAAQ,KAAKD,aAAa,CAAA;AACnD,QAAIE,gBAAgB,GAAI;AAExB,UAAMC,QAAQJ,KAAKK,MAAM,GAAGJ,UAAAA;AAC5B,UAAMK,WAAWN,KAAKK,MAAMJ,aAAa,GAAGE,WAAAA;AAC5C,UAAMI,QAAQP,KAAKK,MAAMF,cAAc,CAAA;AAEvC,QAAI,CAACC,SAAS,CAACG,MAAO;AACtB,QAAI,CAACC,iBAAiBC,IAAIH,QAAAA,EAAW;AACrC,QAAIR,WAAW,CAACA,QAAQW,IAAIL,KAAAA,EAAQ;AAEpCP,YAAQa,KAAK;MAAEN;MAAOE;MAAsCC;IAAM,CAAA;EACpE;AAEA,SAAOV;AACT;AA9BgBN;AAiCT,SAASoB,UACdC,WACAnB,eAAwB;AAExB,MAAI,CAACmB,UAAW,QAAO,CAAA;AAEvB,QAAMlB,QAAQC,MAAMC,QAAQgB,SAAAA,IAAaA,YAAY;IAACA;;AACtD,QAAMf,UAAsB,CAAA;AAC5B,QAAMC,UAAUL,gBAAgB,IAAIM,IAAIN,aAAAA,IAAiB;AAEzD,aAAWO,QAAQN,OAAO;AAExB,UAAMmB,YAAYb,KAAKc,YAAY,GAAA;AACnC,QAAID,cAAc,GAAI;AAEtB,UAAMT,QAAQJ,KAAKK,MAAM,GAAGQ,SAAAA;AAC5B,UAAME,MAAMf,KAAKK,MAAMQ,YAAY,CAAA,EAAGG,YAAW;AAEjD,QAAI,CAACZ,MAAO;AACZ,QAAIW,QAAQ,SAASA,QAAQ,OAAQ;AACrC,QAAIjB,WAAW,CAACA,QAAQW,IAAIL,KAAAA,EAAQ;AAEpCP,YAAQa,KAAK;MAAEN;MAAOa,WAAWF;IAAI,CAAA;EACvC;AAEA,SAAOlB;AACT;AA1BgBc;AA6BT,SAASO,gBAAgBC,QAG/B;AACC,MAAIC,OACF,OAAOD,OAAOC,SAAS,WAAWC,SAASF,OAAOC,MAAM,EAAA,IAAOD,OAAOC,QAAQhC;AAChF,MAAIkC,QACF,OAAOH,OAAOG,UAAU,WAAWD,SAASF,OAAOG,OAAO,EAAA,IAAOH,OAAOG,SAASjC;AAEnF,MAAIkC,MAAMH,IAAAA,KAASA,OAAO,EAAGA,QAAOhC;AACpC,MAAImC,MAAMD,KAAAA,KAAUA,QAAQ,EAAGA,SAAQjC;AACvC,MAAIiC,QAAQhC,UAAWgC,SAAQhC;AAE/B,SAAO;IAAE8B;IAAME;IAAOE,SAASJ,OAAO,KAAKE;EAAM;AACnD;AAdgBJ;AAiBT,SAASO,iBAAiBC,GAAqB;AACpD,MAAI,CAACA,EAAG,QAAO;AACf,SAAOA,EAAEC,KAAI,EAAGtB,MAAM,GAAGlB,iBAAAA;AAC3B;AAHgBsC;AAmCT,SAASG,WACdC,OACAC,aAA8B;AAE9B,SAAO;IACLC,SAASxC,aAAasC,MAAMG,QAAQF,aAAaG,UAAAA;IACjDC,MAAMvB,UAAUkB,MAAMK,MAAMJ,aAAaK,QAAAA;IACzCC,YAAYlB,gBAAgB;MAAEE,MAAMS,MAAMT;MAAME,OAAOO,MAAMP;IAAM,CAAA;IACnEe,QAAQZ,iBAAiBI,MAAMH,CAAC;EAClC;AACF;AAVgBE;AAeT,SAASU,iBACdC,QAA4B;AAE5B,QAAMpB,SAAqD,CAAC;AAE5D,MAAIoB,OAAOR,SAASS,QAAQ;AAC1BrB,WAAOa,SAASO,OAAOR,QAAQU,IAAI,CAACC,MAAM,GAAGA,EAAEtC,KAAK,IAAIsC,EAAEpC,QAAQ,IAAIoC,EAAEnC,KAAK,EAAE;EACjF;AAEA,MAAIgC,OAAOL,MAAMM,QAAQ;AACvBrB,WAAOe,OAAOK,OAAOL,KAAKO,IAAI,CAACE,MAAM,GAAGA,EAAEvC,KAAK,IAAIuC,EAAE1B,SAAS,EAAE;EAClE;AAEA,MAAIsB,OAAOH,YAAY;AACrBjB,WAAOC,OAAOmB,OAAOH,WAAWhB;AAChCD,WAAOG,QAAQiB,OAAOH,WAAWd;EACnC;AAEA,MAAIiB,OAAOF,QAAQ;AACjBlB,WAAOO,IAAIa,OAAOF;EACpB;AAEA,SAAOlB;AACT;AAvBgBmB;","names":["FILTER_OPERATORS","Set","MAX_SEARCH_LENGTH","DEFAULT_PAGE","DEFAULT_LIMIT","MAX_LIMIT","parseFilters","filterParam","allowedFields","items","Array","isArray","results","allowed","Set","item","firstColon","indexOf","secondColon","field","slice","operator","value","FILTER_OPERATORS","has","push","parseSort","sortParam","lastColon","lastIndexOf","dir","toLowerCase","direction","parsePagination","params","page","parseInt","limit","isNaN","offset","parseSearchQuery","q","trim","parseQuery","query","fieldConfig","filters","filter","filterable","sort","sortable","pagination","search","buildQueryParams","parsed","length","map","f","s"]}
|
|
@@ -36,6 +36,7 @@ var DevToolsAdapter = class {
|
|
|
36
36
|
container = null;
|
|
37
37
|
adapterStatuses = {};
|
|
38
38
|
stopErrorWatch = null;
|
|
39
|
+
peerAdapters = [];
|
|
39
40
|
constructor(options = {}) {
|
|
40
41
|
this.basePath = options.basePath ?? "/_debug";
|
|
41
42
|
this.enabled = options.enabled ?? process.env.NODE_ENV !== "production";
|
|
@@ -45,6 +46,7 @@ var DevToolsAdapter = class {
|
|
|
45
46
|
"NODE_ENV"
|
|
46
47
|
];
|
|
47
48
|
this.errorRateThreshold = options.errorRateThreshold ?? 0.5;
|
|
49
|
+
this.peerAdapters = options.adapters ?? [];
|
|
48
50
|
this.requestCount = ref(0);
|
|
49
51
|
this.errorCount = ref(0);
|
|
50
52
|
this.clientErrorCount = ref(0);
|
|
@@ -73,6 +75,8 @@ var DevToolsAdapter = class {
|
|
|
73
75
|
if (!this.enabled) return;
|
|
74
76
|
this.container = container;
|
|
75
77
|
this.startedAt.value = Date.now();
|
|
78
|
+
this.routes = [];
|
|
79
|
+
this.adapterStatuses[this.name] = "running";
|
|
76
80
|
const router = Router();
|
|
77
81
|
router.get("/routes", (_req, res) => {
|
|
78
82
|
res.json({
|
|
@@ -108,6 +112,7 @@ var DevToolsAdapter = class {
|
|
|
108
112
|
});
|
|
109
113
|
});
|
|
110
114
|
router.get("/state", (_req, res) => {
|
|
115
|
+
const wsAdapter = this.peerAdapters.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
|
|
111
116
|
res.json({
|
|
112
117
|
reactive: {
|
|
113
118
|
requestCount: this.requestCount.value,
|
|
@@ -119,7 +124,24 @@ var DevToolsAdapter = class {
|
|
|
119
124
|
},
|
|
120
125
|
routes: this.routes.length,
|
|
121
126
|
container: this.container?.getRegistrations().length ?? 0,
|
|
122
|
-
routeLatency: this.routeLatency
|
|
127
|
+
routeLatency: this.routeLatency,
|
|
128
|
+
...wsAdapter ? {
|
|
129
|
+
ws: wsAdapter.getStats()
|
|
130
|
+
} : {}
|
|
131
|
+
});
|
|
132
|
+
});
|
|
133
|
+
router.get("/ws", (_req, res) => {
|
|
134
|
+
const wsAdapter = this.peerAdapters.find((a) => a.name === "WsAdapter" && typeof a.getStats === "function");
|
|
135
|
+
if (!wsAdapter) {
|
|
136
|
+
res.json({
|
|
137
|
+
enabled: false,
|
|
138
|
+
message: "WsAdapter not found"
|
|
139
|
+
});
|
|
140
|
+
return;
|
|
141
|
+
}
|
|
142
|
+
res.json({
|
|
143
|
+
enabled: true,
|
|
144
|
+
...wsAdapter.getStats()
|
|
123
145
|
});
|
|
124
146
|
});
|
|
125
147
|
if (this.exposeConfig) {
|
|
@@ -188,10 +210,6 @@ var DevToolsAdapter = class {
|
|
|
188
210
|
});
|
|
189
211
|
}
|
|
190
212
|
}
|
|
191
|
-
beforeStart(_app, _container) {
|
|
192
|
-
if (!this.enabled) return;
|
|
193
|
-
this.adapterStatuses[this.name] = "running";
|
|
194
|
-
}
|
|
195
213
|
afterStart(_server, _container) {
|
|
196
214
|
if (!this.enabled) return;
|
|
197
215
|
log.info(`DevTools ready \u2014 ${this.routes.length} routes tracked, ${this.container?.getRegistrations().length ?? 0} DI bindings`);
|
|
@@ -205,4 +223,4 @@ var DevToolsAdapter = class {
|
|
|
205
223
|
export {
|
|
206
224
|
DevToolsAdapter
|
|
207
225
|
};
|
|
208
|
-
//# sourceMappingURL=chunk-
|
|
226
|
+
//# sourceMappingURL=chunk-YYT24FTH.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../src/devtools.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { Router } from 'express'\nimport {\n type AppAdapter,\n type AdapterMiddleware,\n type Container,\n METADATA,\n ref,\n computed,\n reactive,\n watch,\n createLogger,\n type Ref,\n type ComputedRef,\n} from '@forinda/kickjs-core'\n\nconst log = createLogger('DevTools')\n\n/** Route metadata collected during mount */\ninterface RouteInfo {\n method: string\n path: string\n controller: string\n handler: string\n middleware: string[]\n}\n\n/** Per-route latency stats */\ninterface RouteStats {\n count: number\n totalMs: number\n minMs: number\n maxMs: number\n}\n\nexport interface DevToolsOptions {\n /** Base path for debug endpoints (default: '/_debug') */\n basePath?: string\n /** Only enable when this is true (default: process.env.NODE_ENV !== 'production') */\n enabled?: boolean\n /** Include environment variables (sanitized) at /_debug/config (default: false) */\n exposeConfig?: boolean\n /** Env var prefixes to expose (default: ['APP_', 'NODE_ENV']). Others are redacted. */\n configPrefixes?: string[]\n /** Callback when error rate exceeds threshold */\n onErrorRateExceeded?: (rate: number) => void\n /** Error rate threshold (default: 0.5 = 50%) */\n errorRateThreshold?: number\n /** Other adapters to discover stats from (e.g., WsAdapter) */\n adapters?: any[]\n}\n\n/**\n * DevToolsAdapter — Vue-style reactive introspection for KickJS applications.\n *\n * Exposes debug endpoints powered by reactive state (ref, computed, watch):\n * - `GET /_debug/routes` — all registered routes with middleware\n * - `GET /_debug/container` — DI registry with scopes and instantiation status\n * - `GET /_debug/metrics` — live request/error counts, error rate, uptime\n * - `GET /_debug/health` — deep health check with adapter status\n * - `GET /_debug/config` — sanitized environment variables (opt-in)\n * - `GET /_debug/state` — full reactive state snapshot\n *\n * @example\n * ```ts\n * import { DevToolsAdapter } from '@forinda/kickjs-http/devtools'\n *\n * bootstrap({\n * modules: [UserModule],\n * adapters: [\n * new DevToolsAdapter({\n * enabled: process.env.NODE_ENV !== 'production',\n * exposeConfig: true,\n * configPrefixes: ['APP_', 'DATABASE_'],\n * }),\n * ],\n * })\n * ```\n */\nexport class DevToolsAdapter implements AppAdapter {\n readonly name = 'DevToolsAdapter'\n\n private basePath: string\n private enabled: boolean\n private exposeConfig: boolean\n private configPrefixes: string[]\n private errorRateThreshold: number\n\n // ── Reactive State ───────────────────────────────────────────────────\n /** Total requests received */\n readonly requestCount: Ref<number>\n /** Total responses with status >= 500 */\n readonly errorCount: Ref<number>\n /** Total responses with status >= 400 and < 500 */\n readonly clientErrorCount: Ref<number>\n /** Server start time */\n readonly startedAt: Ref<number>\n /** Computed error rate (server errors / total requests) */\n readonly errorRate: ComputedRef<number>\n /** Computed uptime in seconds */\n readonly uptimeSeconds: ComputedRef<number>\n /** Per-route latency tracking */\n readonly routeLatency: Record<string, RouteStats>\n\n // ── Internal State ───────────────────────────────────────────────────\n private routes: RouteInfo[] = []\n private container: Container | null = null\n private adapterStatuses: Record<string, string> = {}\n private stopErrorWatch: (() => void) | null = null\n private peerAdapters: any[] = []\n\n constructor(options: DevToolsOptions = {}) {\n this.basePath = options.basePath ?? '/_debug'\n this.enabled = options.enabled ?? process.env.NODE_ENV !== 'production'\n this.exposeConfig = options.exposeConfig ?? false\n this.configPrefixes = options.configPrefixes ?? ['APP_', 'NODE_ENV']\n this.errorRateThreshold = options.errorRateThreshold ?? 0.5\n this.peerAdapters = options.adapters ?? []\n\n // Initialize reactive state\n this.requestCount = ref(0)\n this.errorCount = ref(0)\n this.clientErrorCount = ref(0)\n this.startedAt = ref(Date.now())\n this.routeLatency = reactive({})\n\n this.errorRate = computed(() =>\n this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0,\n )\n\n this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1000))\n\n // Watch error rate — log warnings when elevated\n if (options.onErrorRateExceeded) {\n const callback = options.onErrorRateExceeded\n const threshold = this.errorRateThreshold\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > threshold) {\n callback(rate)\n }\n })\n } else {\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > this.errorRateThreshold) {\n log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`)\n }\n })\n }\n }\n\n // ── Adapter Lifecycle ────────────────────────────────────────────────\n\n beforeMount(app: any, container: Container): void {\n if (!this.enabled) return\n\n this.container = container\n this.startedAt.value = Date.now()\n // Clear routes on rebuild/restart to prevent HMR duplication\n this.routes = []\n this.adapterStatuses[this.name] = 'running'\n\n const router = Router()\n\n router.get('/routes', (_req: Request, res: Response) => {\n res.json({ routes: this.routes })\n })\n\n router.get('/container', (_req: Request, res: Response) => {\n const registrations = this.container?.getRegistrations() ?? []\n res.json({ registrations, count: registrations.length })\n })\n\n router.get('/metrics', (_req: Request, res: Response) => {\n res.json({\n requests: this.requestCount.value,\n serverErrors: this.errorCount.value,\n clientErrors: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n routeLatency: this.routeLatency,\n })\n })\n\n router.get('/health', (_req: Request, res: Response) => {\n const healthy = this.errorRate.value < this.errorRateThreshold\n const status = healthy ? 'healthy' : 'degraded'\n\n res.status(healthy ? 200 : 503).json({\n status,\n errorRate: this.errorRate.value,\n uptime: this.uptimeSeconds.value,\n adapters: this.adapterStatuses,\n })\n })\n\n router.get('/state', (_req: Request, res: Response) => {\n const wsAdapter = this.peerAdapters.find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n res.json({\n reactive: {\n requestCount: this.requestCount.value,\n errorCount: this.errorCount.value,\n clientErrorCount: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n },\n routes: this.routes.length,\n container: this.container?.getRegistrations().length ?? 0,\n routeLatency: this.routeLatency,\n ...(wsAdapter ? { ws: wsAdapter.getStats() } : {}),\n })\n })\n\n router.get('/ws', (_req: Request, res: Response) => {\n const wsAdapter = this.peerAdapters.find(\n (a) => a.name === 'WsAdapter' && typeof a.getStats === 'function',\n )\n if (!wsAdapter) {\n res.json({ enabled: false, message: 'WsAdapter not found' })\n return\n }\n res.json({ enabled: true, ...wsAdapter.getStats() })\n })\n\n if (this.exposeConfig) {\n router.get('/config', (_req: Request, res: Response) => {\n const config: Record<string, string> = {}\n for (const [key, value] of Object.entries(process.env)) {\n if (value === undefined) continue\n const allowed = this.configPrefixes.some((prefix) => key.startsWith(prefix))\n config[key] = allowed ? value : '[REDACTED]'\n }\n res.json({ config })\n })\n }\n\n app.use(this.basePath, router)\n log.info(`DevTools mounted at ${this.basePath}`)\n }\n\n middleware(): AdapterMiddleware[] {\n if (!this.enabled) return []\n\n return [\n {\n handler: (req: Request, res: Response, next: NextFunction) => {\n const start = Date.now()\n this.requestCount.value++\n\n res.on('finish', () => {\n if (res.statusCode >= 500) this.errorCount.value++\n else if (res.statusCode >= 400) this.clientErrorCount.value++\n\n // Track per-route latency\n const routeKey = `${req.method} ${req.route?.path ?? req.path}`\n const elapsed = Date.now() - start\n\n if (!this.routeLatency[routeKey]) {\n this.routeLatency[routeKey] = {\n count: 0,\n totalMs: 0,\n minMs: Infinity,\n maxMs: 0,\n }\n }\n const stats = this.routeLatency[routeKey]\n stats.count++\n stats.totalMs += elapsed\n stats.minMs = Math.min(stats.minMs, elapsed)\n stats.maxMs = Math.max(stats.maxMs, elapsed)\n })\n\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n onRouteMount(controllerClass: any, mountPath: string): void {\n if (!this.enabled) return\n\n const routes: Array<{ method: string; path: string; handlerName: string }> =\n Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? []\n\n const classMiddleware: any[] =\n Reflect.getMetadata(METADATA.CLASS_MIDDLEWARES, controllerClass) ?? []\n\n for (const route of routes) {\n const methodMiddleware: any[] =\n Reflect.getMetadata(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass.prototype,\n route.handlerName,\n ) ?? []\n\n this.routes.push({\n method: route.method.toUpperCase(),\n path: `${mountPath}${route.path === '/' ? '' : route.path}`,\n controller: controllerClass.name,\n handler: route.handlerName,\n middleware: [\n ...classMiddleware.map((m: any) => m.name || 'anonymous'),\n ...methodMiddleware.map((m: any) => m.name || 'anonymous'),\n ],\n })\n }\n }\n\n afterStart(_server: any, _container: Container): void {\n if (!this.enabled) return\n log.info(\n `DevTools ready — ${this.routes.length} routes tracked, ` +\n `${this.container?.getRegistrations().length ?? 0} DI bindings`,\n )\n }\n\n shutdown(): void {\n this.stopErrorWatch?.()\n this.adapterStatuses[this.name] = 'stopped'\n }\n}\n"],"mappings":"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","names":["Router","METADATA","ref","computed","reactive","watch","createLogger","log","createLogger","DevToolsAdapter","name","basePath","enabled","exposeConfig","configPrefixes","errorRateThreshold","requestCount","errorCount","clientErrorCount","startedAt","errorRate","uptimeSeconds","routeLatency","routes","container","adapterStatuses","stopErrorWatch","peerAdapters","options","process","env","NODE_ENV","adapters","ref","Date","now","reactive","computed","value","Math","floor","onErrorRateExceeded","callback","threshold","watch","rate","warn","toFixed","beforeMount","app","router","Router","get","_req","res","json","registrations","getRegistrations","count","length","requests","serverErrors","clientErrors","toISOString","healthy","status","uptime","wsAdapter","find","a","getStats","ws","message","config","key","Object","entries","undefined","allowed","some","prefix","startsWith","use","info","middleware","handler","req","next","start","on","statusCode","routeKey","method","route","path","elapsed","totalMs","minMs","Infinity","maxMs","stats","min","max","phase","onRouteMount","controllerClass","mountPath","Reflect","getMetadata","METADATA","ROUTES","classMiddleware","CLASS_MIDDLEWARES","methodMiddleware","METHOD_MIDDLEWARES","prototype","handlerName","push","toUpperCase","controller","map","m","afterStart","_server","_container","shutdown"]}
|
package/dist/context.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import * as http from 'http';
|
|
2
2
|
import { Request, Response, NextFunction } from 'express';
|
|
3
|
-
import {
|
|
3
|
+
import { e as QueryFieldConfig, d as ParsedQuery } from './types-DsbCdE8f.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Unified request/response abstraction passed to every controller method.
|
|
@@ -52,6 +52,34 @@ declare class RequestContext<TBody = any, TParams = any, TQuery = any> {
|
|
|
52
52
|
badRequest(message: string): Response<any, Record<string, any>>;
|
|
53
53
|
html(content: string, status?: number): Response<any, Record<string, any>>;
|
|
54
54
|
download(buffer: Buffer, filename: string, contentType?: string): Response<any, Record<string, any>>;
|
|
55
|
+
/**
|
|
56
|
+
* Parse query params and return a standardized paginated response.
|
|
57
|
+
* Calls `ctx.qs()` internally, then wraps your data with pagination meta.
|
|
58
|
+
*
|
|
59
|
+
* @param fetcher - Async function that receives ParsedQuery and returns `{ data, total }`
|
|
60
|
+
* @param fieldConfig - Optional whitelist for filterable, sortable, searchable fields
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* @Get('/')
|
|
65
|
+
* async list(ctx: RequestContext) {
|
|
66
|
+
* return ctx.paginate(
|
|
67
|
+
* async (parsed) => {
|
|
68
|
+
* const data = await db.select().from(users)
|
|
69
|
+
* .where(query.where).limit(parsed.pagination.limit)
|
|
70
|
+
* .offset(parsed.pagination.offset).all()
|
|
71
|
+
* const total = await db.select({ count: count() }).from(users).get()
|
|
72
|
+
* return { data, total: total?.count ?? 0 }
|
|
73
|
+
* },
|
|
74
|
+
* { filterable: ['name', 'role'], sortable: ['createdAt'] },
|
|
75
|
+
* )
|
|
76
|
+
* }
|
|
77
|
+
* ```
|
|
78
|
+
*/
|
|
79
|
+
paginate<T>(fetcher: (parsed: ParsedQuery) => Promise<{
|
|
80
|
+
data: T[];
|
|
81
|
+
total: number;
|
|
82
|
+
}>, fieldConfig?: QueryFieldConfig): Promise<Response<any, Record<string, any>>>;
|
|
55
83
|
}
|
|
56
84
|
|
|
57
85
|
export { RequestContext };
|
package/dist/context.js
CHANGED
package/dist/devtools.d.ts
CHANGED
|
@@ -20,6 +20,8 @@ interface DevToolsOptions {
|
|
|
20
20
|
onErrorRateExceeded?: (rate: number) => void;
|
|
21
21
|
/** Error rate threshold (default: 0.5 = 50%) */
|
|
22
22
|
errorRateThreshold?: number;
|
|
23
|
+
/** Other adapters to discover stats from (e.g., WsAdapter) */
|
|
24
|
+
adapters?: any[];
|
|
23
25
|
}
|
|
24
26
|
/**
|
|
25
27
|
* DevToolsAdapter — Vue-style reactive introspection for KickJS applications.
|
|
@@ -73,11 +75,11 @@ declare class DevToolsAdapter implements AppAdapter {
|
|
|
73
75
|
private container;
|
|
74
76
|
private adapterStatuses;
|
|
75
77
|
private stopErrorWatch;
|
|
78
|
+
private peerAdapters;
|
|
76
79
|
constructor(options?: DevToolsOptions);
|
|
77
80
|
beforeMount(app: any, container: Container): void;
|
|
78
81
|
middleware(): AdapterMiddleware[];
|
|
79
82
|
onRouteMount(controllerClass: any, mountPath: string): void;
|
|
80
|
-
beforeStart(_app: any, _container: Container): void;
|
|
81
83
|
afterStart(_server: any, _container: Container): void;
|
|
82
84
|
shutdown(): void;
|
|
83
85
|
}
|
package/dist/devtools.js
CHANGED
package/dist/index.d.ts
CHANGED
|
@@ -11,7 +11,7 @@ export { Session, SessionData, SessionOptions, SessionStore, session } from './m
|
|
|
11
11
|
export { UploadOptions, buildUploadMiddleware, cleanupFiles, resolveMimeTypes, upload } from './middleware/upload.js';
|
|
12
12
|
export { DevToolsAdapter, DevToolsOptions } from './devtools.js';
|
|
13
13
|
export { buildQueryParams, parseFilters, parsePagination, parseQuery, parseSearchQuery, parseSort } from './query/index.js';
|
|
14
|
-
export { F as FILTER_OPERATORS, a as FilterItem, b as FilterOperator, P as
|
|
14
|
+
export { F as FILTER_OPERATORS, a as FilterItem, b as FilterOperator, P as PaginatedResponse, c as PaginationParams, d as ParsedQuery, Q as QueryBuilderAdapter, e as QueryFieldConfig, S as SortItem } from './types-DsbCdE8f.js';
|
|
15
15
|
import 'node:http';
|
|
16
16
|
import 'express';
|
|
17
17
|
import '@forinda/kickjs-core';
|
package/dist/index.js
CHANGED
|
@@ -6,21 +6,21 @@ import {
|
|
|
6
6
|
} from "./chunk-NQJNMKW5.js";
|
|
7
7
|
import {
|
|
8
8
|
bootstrap
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-D76WCWAW.js";
|
|
10
10
|
import {
|
|
11
11
|
Application
|
|
12
|
-
} from "./chunk-
|
|
12
|
+
} from "./chunk-OKB76LY2.js";
|
|
13
13
|
import {
|
|
14
14
|
REQUEST_ID_HEADER,
|
|
15
15
|
requestId
|
|
16
16
|
} from "./chunk-35NUARK7.js";
|
|
17
17
|
import {
|
|
18
18
|
DevToolsAdapter
|
|
19
|
-
} from "./chunk-
|
|
19
|
+
} from "./chunk-YYT24FTH.js";
|
|
20
20
|
import {
|
|
21
21
|
buildRoutes,
|
|
22
22
|
getControllerPath
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-S7DJ5HF3.js";
|
|
24
24
|
import {
|
|
25
25
|
buildUploadMiddleware,
|
|
26
26
|
cleanupFiles,
|
|
@@ -32,7 +32,7 @@ import {
|
|
|
32
32
|
} from "./chunk-RPN7UFUO.js";
|
|
33
33
|
import {
|
|
34
34
|
RequestContext
|
|
35
|
-
} from "./chunk-
|
|
35
|
+
} from "./chunk-GDMORQ2P.js";
|
|
36
36
|
import {
|
|
37
37
|
FILTER_OPERATORS,
|
|
38
38
|
buildQueryParams,
|
|
@@ -41,7 +41,7 @@ import {
|
|
|
41
41
|
parseQuery,
|
|
42
42
|
parseSearchQuery,
|
|
43
43
|
parseSort
|
|
44
|
-
} from "./chunk-
|
|
44
|
+
} from "./chunk-WYY34UWG.js";
|
|
45
45
|
import {
|
|
46
46
|
csrf
|
|
47
47
|
} from "./chunk-I32MVBEG.js";
|
package/dist/query/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export { F as FILTER_OPERATORS, b as FilterOperator, Q as QueryBuilderAdapter } from '../types-
|
|
1
|
+
import { d as ParsedQuery, a as FilterItem, c as PaginationParams, e as QueryFieldConfig, S as SortItem } from '../types-DsbCdE8f.js';
|
|
2
|
+
export { F as FILTER_OPERATORS, b as FilterOperator, P as PaginatedResponse, Q as QueryBuilderAdapter } from '../types-DsbCdE8f.js';
|
|
3
3
|
|
|
4
4
|
/** Parse filter strings like "status:eq:active" into structured objects */
|
|
5
5
|
declare function parseFilters(filterParam: string | string[] | undefined, allowedFields?: string[]): FilterItem[];
|
package/dist/query/index.js
CHANGED
package/dist/router-builder.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildRoutes,
|
|
3
3
|
getControllerPath
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-S7DJ5HF3.js";
|
|
5
5
|
import "./chunk-LEILPDMW.js";
|
|
6
6
|
import "./chunk-RPN7UFUO.js";
|
|
7
|
-
import "./chunk-
|
|
8
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-GDMORQ2P.js";
|
|
8
|
+
import "./chunk-WYY34UWG.js";
|
|
9
9
|
import "./chunk-WCQVDF3K.js";
|
|
10
10
|
export {
|
|
11
11
|
buildRoutes,
|
|
@@ -35,6 +35,18 @@ interface QueryFieldConfig {
|
|
|
35
35
|
sortable?: string[];
|
|
36
36
|
searchable?: string[];
|
|
37
37
|
}
|
|
38
|
+
/** Standardized paginated response shape */
|
|
39
|
+
interface PaginatedResponse<T = any> {
|
|
40
|
+
data: T[];
|
|
41
|
+
meta: {
|
|
42
|
+
page: number;
|
|
43
|
+
limit: number;
|
|
44
|
+
total: number;
|
|
45
|
+
totalPages: number;
|
|
46
|
+
hasNext: boolean;
|
|
47
|
+
hasPrev: boolean;
|
|
48
|
+
};
|
|
49
|
+
}
|
|
38
50
|
/**
|
|
39
51
|
* Interface for ORM-specific query builder adapters.
|
|
40
52
|
* Implement this to translate a `ParsedQuery` into your ORM's query format.
|
|
@@ -69,4 +81,4 @@ interface QueryBuilderAdapter<TResult = any, TConfig = any> {
|
|
|
69
81
|
build(parsed: ParsedQuery, config: TConfig): TResult;
|
|
70
82
|
}
|
|
71
83
|
|
|
72
|
-
export { FILTER_OPERATORS as F, type
|
|
84
|
+
export { FILTER_OPERATORS as F, type PaginatedResponse as P, type QueryBuilderAdapter as Q, type SortItem as S, type FilterItem as a, type FilterOperator as b, type PaginationParams as c, type ParsedQuery as d, type QueryFieldConfig as e };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.5.2",
|
|
4
4
|
"description": "Express 5 integration, router builder, RequestContext, and middleware for KickJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -98,7 +98,7 @@
|
|
|
98
98
|
"cookie-parser": "^1.4.7",
|
|
99
99
|
"multer": "^2.1.1",
|
|
100
100
|
"reflect-metadata": "^0.2.2",
|
|
101
|
-
"@forinda/kickjs-core": "0.
|
|
101
|
+
"@forinda/kickjs-core": "0.5.2"
|
|
102
102
|
},
|
|
103
103
|
"peerDependencies": {
|
|
104
104
|
"express": "^5.1.0"
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/application.ts"],"sourcesContent":["import http from 'node:http'\nimport express, { type Express, type RequestHandler } from 'express'\nimport {\n Container,\n createLogger,\n type AppModuleClass,\n type AppAdapter,\n type AdapterMiddleware,\n} from '@forinda/kickjs-core'\nimport { buildRoutes } from './router-builder'\nimport { requestId } from './middleware/request-id'\nimport { notFoundHandler, errorHandler } from './middleware/error-handler'\n\nconst log = createLogger('Application')\n\n/**\n * A middleware entry in the declarative pipeline.\n * Can be a bare handler or an object with path scoping.\n */\nexport type MiddlewareEntry = RequestHandler | { path: string; handler: RequestHandler }\n\nexport interface ApplicationOptions {\n /** Feature modules to load */\n modules: AppModuleClass[]\n /** Adapters that hook into the lifecycle (DB, Redis, Swagger, etc.) */\n adapters?: AppAdapter[]\n /** Server port (falls back to PORT env var, then 3000) */\n port?: number\n /** Global API prefix (default: '/api') */\n apiPrefix?: string\n /** Default API version (default: 1) — routes become /{prefix}/v{version}/{path} */\n defaultVersion?: number\n\n /**\n * Global middleware pipeline. Declared in order.\n * Replaces the hardcoded middleware stack — you control exactly what runs.\n *\n * @example\n * ```ts\n * bootstrap({\n * modules,\n * middleware: [\n * helmet(),\n * cors(),\n * compression(),\n * morgan('dev'),\n * express.json({ limit: '1mb' }),\n * ],\n * })\n * ```\n *\n * If omitted, a sensible default is applied:\n * requestId(), express.json({ limit: '100kb' })\n */\n middleware?: MiddlewareEntry[]\n\n /** Express `trust proxy` setting */\n trustProxy?: boolean | number | string | ((ip: string, hopIndex: number) => boolean)\n /** Maximum JSON body size (only used when middleware is not provided) */\n jsonLimit?: string | number\n}\n\n/**\n * The main application class. Wires together Express, the DI container,\n * feature modules, adapters, and the middleware pipeline.\n */\nexport class Application {\n private app: Express\n private container: Container\n private httpServer: http.Server | null = null\n private adapters: AppAdapter[]\n\n constructor(private readonly options: ApplicationOptions) {\n this.app = express()\n this.container = Container.getInstance()\n this.adapters = options.adapters ?? []\n }\n\n /**\n * Full setup pipeline:\n * 1. Adapter beforeMount hooks (early routes — docs, health)\n * 2. Adapter middleware (phase: beforeGlobal)\n * 3. Global middleware (user-declared or defaults)\n * 4. Adapter middleware (phase: afterGlobal)\n * 5. Module registration + DI bootstrap\n * 6. Adapter middleware (phase: beforeRoutes)\n * 7. Module route mounting\n * 8. Adapter middleware (phase: afterRoutes)\n * 9. Error handlers (notFound + global)\n * 10. Adapter beforeStart hooks\n */\n setup(): void {\n log.info('Bootstrapping application...')\n\n // Collect adapter middleware by phase\n const adapterMw = this.collectAdapterMiddleware()\n\n // ── 1. Adapter beforeMount hooks ──────────────────────────────────\n for (const adapter of this.adapters) {\n adapter.beforeMount?.(this.app, this.container)\n }\n\n // ── 2. Hardened defaults ──────────────────────────────────────────\n this.app.disable('x-powered-by')\n this.app.set('trust proxy', this.options.trustProxy ?? false)\n\n // ── 3. Adapter middleware: beforeGlobal ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeGlobal)\n\n // ── 4. Global middleware ─────────────────────────────────────────\n if (this.options.middleware) {\n // User-declared pipeline — full control\n for (const entry of this.options.middleware) {\n this.mountMiddlewareEntry(entry)\n }\n } else {\n // Sensible defaults when no middleware declared\n this.app.use(requestId())\n this.app.use(express.json({ limit: this.options.jsonLimit ?? '100kb' }))\n }\n\n // ── 5. Adapter middleware: afterGlobal ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterGlobal)\n\n // ── 6. Module registration + DI bootstrap ────────────────────────\n const modules = this.options.modules.map((ModuleClass) => {\n const mod = new ModuleClass()\n mod.register(this.container)\n return mod\n })\n this.container.bootstrap()\n\n // ── 7. Adapter middleware: beforeRoutes ───────────────────────────\n this.mountMiddlewareList(adapterMw.beforeRoutes)\n\n // ── 8. Mount module routes with versioning ───────────────────────\n const apiPrefix = this.options.apiPrefix ?? '/api'\n const defaultVersion = this.options.defaultVersion ?? 1\n\n for (const mod of modules) {\n const result = mod.routes()\n const routeSets = Array.isArray(result) ? result : [result]\n\n for (const route of routeSets) {\n const version = route.version ?? defaultVersion\n const mountPath = `${apiPrefix}/v${version}${route.path}`\n this.app.use(mountPath, route.router)\n\n // Notify adapters (e.g. SwaggerAdapter for OpenAPI spec generation)\n if (route.controller) {\n for (const adapter of this.adapters) {\n adapter.onRouteMount?.(route.controller, mountPath)\n }\n }\n }\n }\n\n // ── 9. Adapter middleware: afterRoutes ────────────────────────────\n this.mountMiddlewareList(adapterMw.afterRoutes)\n\n // ── 10. Error handlers ───────────────────────────────────────────\n this.app.use(notFoundHandler())\n this.app.use(errorHandler())\n\n // ── 11. Adapter beforeStart hooks ────────────────────────────────\n for (const adapter of this.adapters) {\n adapter.beforeStart?.(this.app, this.container)\n }\n }\n\n /** Start the HTTP server, retrying up to 3 times on port conflict */\n start(): void {\n this.setup()\n\n const basePort = this.options.port ?? parseInt(process.env.PORT || '3000', 10)\n const maxRetries = 3\n\n const tryListen = (port: number, attempt: number) => {\n this.httpServer = http.createServer(this.app)\n\n this.httpServer.on('error', (err: NodeJS.ErrnoException) => {\n if (err.code === 'EADDRINUSE' && attempt < maxRetries) {\n const nextPort = port + 1\n log.warn(`Port ${port} in use, trying ${nextPort}... (${attempt + 1}/${maxRetries})`)\n tryListen(nextPort, attempt + 1)\n } else {\n throw err\n }\n })\n\n this.httpServer.listen(port, () => {\n if (port !== basePort) {\n log.warn(`Port ${basePort} was in use, using ${port} instead`)\n }\n log.info(`Server running on http://localhost:${port}`)\n\n for (const adapter of this.adapters) {\n adapter.afterStart?.(this.httpServer!, this.container)\n }\n })\n }\n\n tryListen(basePort, 0)\n }\n\n /** HMR rebuild: swap Express handler without restarting the server */\n rebuild(): void {\n // Reset the DI container so singletons are re-created with fresh code\n Container.reset()\n this.container = Container.getInstance()\n\n this.app = express()\n this.setup()\n\n if (this.httpServer) {\n this.httpServer.removeAllListeners('request')\n this.httpServer.on('request', this.app)\n log.info('HMR: Express app rebuilt and swapped')\n }\n }\n\n /** Graceful shutdown — runs all adapter shutdowns in parallel, resilient to failures */\n async shutdown(): Promise<void> {\n log.info('Shutting down...')\n\n // Run all adapter shutdowns concurrently — don't let one failure block the rest\n const results = await Promise.allSettled(\n this.adapters.map((adapter) => Promise.resolve(adapter.shutdown?.())),\n )\n for (const result of results) {\n if (result.status === 'rejected') {\n log.error({ err: result.reason }, 'Adapter shutdown failed')\n }\n }\n\n if (this.httpServer) {\n await new Promise<void>((resolve) => this.httpServer!.close(() => resolve()))\n }\n }\n\n getExpressApp(): Express {\n return this.app\n }\n\n getHttpServer(): http.Server | null {\n return this.httpServer\n }\n\n // ── Internal helpers ────────────────────────────────────────────────\n\n private collectAdapterMiddleware() {\n const result = {\n beforeGlobal: [] as AdapterMiddleware[],\n afterGlobal: [] as AdapterMiddleware[],\n beforeRoutes: [] as AdapterMiddleware[],\n afterRoutes: [] as AdapterMiddleware[],\n }\n\n for (const adapter of this.adapters) {\n const entries = adapter.middleware?.() ?? []\n for (const entry of entries) {\n const phase = entry.phase ?? 'afterGlobal'\n result[phase].push(entry)\n }\n }\n\n return result\n }\n\n private mountMiddlewareList(entries: AdapterMiddleware[]): void {\n for (const entry of entries) {\n if (entry.path) {\n this.app.use(entry.path, entry.handler)\n } else {\n this.app.use(entry.handler)\n }\n }\n }\n\n private mountMiddlewareEntry(entry: MiddlewareEntry): void {\n if (typeof entry === 'function') {\n this.app.use(entry)\n } else {\n this.app.use(entry.path, entry.handler)\n }\n }\n}\n"],"mappings":"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","names":["http","express","Container","createLogger","log","createLogger","Application","app","container","httpServer","adapters","options","express","Container","getInstance","setup","info","adapterMw","collectAdapterMiddleware","adapter","beforeMount","disable","set","trustProxy","mountMiddlewareList","beforeGlobal","middleware","entry","mountMiddlewareEntry","use","requestId","json","limit","jsonLimit","afterGlobal","modules","map","ModuleClass","mod","register","bootstrap","beforeRoutes","apiPrefix","defaultVersion","result","routes","routeSets","Array","isArray","route","version","mountPath","path","router","controller","onRouteMount","afterRoutes","notFoundHandler","errorHandler","beforeStart","start","basePort","port","parseInt","process","env","PORT","maxRetries","tryListen","attempt","http","createServer","on","err","code","nextPort","warn","listen","afterStart","rebuild","reset","removeAllListeners","shutdown","results","Promise","allSettled","resolve","status","error","reason","close","getExpressApp","getHttpServer","entries","phase","push","handler"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/devtools.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { Router } from 'express'\nimport {\n type AppAdapter,\n type AdapterMiddleware,\n type Container,\n METADATA,\n ref,\n computed,\n reactive,\n watch,\n createLogger,\n type Ref,\n type ComputedRef,\n} from '@forinda/kickjs-core'\n\nconst log = createLogger('DevTools')\n\n/** Route metadata collected during mount */\ninterface RouteInfo {\n method: string\n path: string\n controller: string\n handler: string\n middleware: string[]\n}\n\n/** Per-route latency stats */\ninterface RouteStats {\n count: number\n totalMs: number\n minMs: number\n maxMs: number\n}\n\nexport interface DevToolsOptions {\n /** Base path for debug endpoints (default: '/_debug') */\n basePath?: string\n /** Only enable when this is true (default: process.env.NODE_ENV !== 'production') */\n enabled?: boolean\n /** Include environment variables (sanitized) at /_debug/config (default: false) */\n exposeConfig?: boolean\n /** Env var prefixes to expose (default: ['APP_', 'NODE_ENV']). Others are redacted. */\n configPrefixes?: string[]\n /** Callback when error rate exceeds threshold */\n onErrorRateExceeded?: (rate: number) => void\n /** Error rate threshold (default: 0.5 = 50%) */\n errorRateThreshold?: number\n}\n\n/**\n * DevToolsAdapter — Vue-style reactive introspection for KickJS applications.\n *\n * Exposes debug endpoints powered by reactive state (ref, computed, watch):\n * - `GET /_debug/routes` — all registered routes with middleware\n * - `GET /_debug/container` — DI registry with scopes and instantiation status\n * - `GET /_debug/metrics` — live request/error counts, error rate, uptime\n * - `GET /_debug/health` — deep health check with adapter status\n * - `GET /_debug/config` — sanitized environment variables (opt-in)\n * - `GET /_debug/state` — full reactive state snapshot\n *\n * @example\n * ```ts\n * import { DevToolsAdapter } from '@forinda/kickjs-http/devtools'\n *\n * bootstrap({\n * modules: [UserModule],\n * adapters: [\n * new DevToolsAdapter({\n * enabled: process.env.NODE_ENV !== 'production',\n * exposeConfig: true,\n * configPrefixes: ['APP_', 'DATABASE_'],\n * }),\n * ],\n * })\n * ```\n */\nexport class DevToolsAdapter implements AppAdapter {\n readonly name = 'DevToolsAdapter'\n\n private basePath: string\n private enabled: boolean\n private exposeConfig: boolean\n private configPrefixes: string[]\n private errorRateThreshold: number\n\n // ── Reactive State ───────────────────────────────────────────────────\n /** Total requests received */\n readonly requestCount: Ref<number>\n /** Total responses with status >= 500 */\n readonly errorCount: Ref<number>\n /** Total responses with status >= 400 and < 500 */\n readonly clientErrorCount: Ref<number>\n /** Server start time */\n readonly startedAt: Ref<number>\n /** Computed error rate (server errors / total requests) */\n readonly errorRate: ComputedRef<number>\n /** Computed uptime in seconds */\n readonly uptimeSeconds: ComputedRef<number>\n /** Per-route latency tracking */\n readonly routeLatency: Record<string, RouteStats>\n\n // ── Internal State ───────────────────────────────────────────────────\n private routes: RouteInfo[] = []\n private container: Container | null = null\n private adapterStatuses: Record<string, string> = {}\n private stopErrorWatch: (() => void) | null = null\n\n constructor(options: DevToolsOptions = {}) {\n this.basePath = options.basePath ?? '/_debug'\n this.enabled = options.enabled ?? process.env.NODE_ENV !== 'production'\n this.exposeConfig = options.exposeConfig ?? false\n this.configPrefixes = options.configPrefixes ?? ['APP_', 'NODE_ENV']\n this.errorRateThreshold = options.errorRateThreshold ?? 0.5\n\n // Initialize reactive state\n this.requestCount = ref(0)\n this.errorCount = ref(0)\n this.clientErrorCount = ref(0)\n this.startedAt = ref(Date.now())\n this.routeLatency = reactive({})\n\n this.errorRate = computed(() =>\n this.requestCount.value > 0 ? this.errorCount.value / this.requestCount.value : 0,\n )\n\n this.uptimeSeconds = computed(() => Math.floor((Date.now() - this.startedAt.value) / 1000))\n\n // Watch error rate — log warnings when elevated\n if (options.onErrorRateExceeded) {\n const callback = options.onErrorRateExceeded\n const threshold = this.errorRateThreshold\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > threshold) {\n callback(rate)\n }\n })\n } else {\n this.stopErrorWatch = watch(this.errorRate, (rate) => {\n if (rate > this.errorRateThreshold) {\n log.warn(`Error rate elevated: ${(rate * 100).toFixed(1)}%`)\n }\n })\n }\n }\n\n // ── Adapter Lifecycle ────────────────────────────────────────────────\n\n beforeMount(app: any, container: Container): void {\n if (!this.enabled) return\n\n this.container = container\n this.startedAt.value = Date.now()\n\n const router = Router()\n\n router.get('/routes', (_req: Request, res: Response) => {\n res.json({ routes: this.routes })\n })\n\n router.get('/container', (_req: Request, res: Response) => {\n const registrations = this.container?.getRegistrations() ?? []\n res.json({ registrations, count: registrations.length })\n })\n\n router.get('/metrics', (_req: Request, res: Response) => {\n res.json({\n requests: this.requestCount.value,\n serverErrors: this.errorCount.value,\n clientErrors: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n routeLatency: this.routeLatency,\n })\n })\n\n router.get('/health', (_req: Request, res: Response) => {\n const healthy = this.errorRate.value < this.errorRateThreshold\n const status = healthy ? 'healthy' : 'degraded'\n\n res.status(healthy ? 200 : 503).json({\n status,\n errorRate: this.errorRate.value,\n uptime: this.uptimeSeconds.value,\n adapters: this.adapterStatuses,\n })\n })\n\n router.get('/state', (_req: Request, res: Response) => {\n res.json({\n reactive: {\n requestCount: this.requestCount.value,\n errorCount: this.errorCount.value,\n clientErrorCount: this.clientErrorCount.value,\n errorRate: this.errorRate.value,\n uptimeSeconds: this.uptimeSeconds.value,\n startedAt: new Date(this.startedAt.value).toISOString(),\n },\n routes: this.routes.length,\n container: this.container?.getRegistrations().length ?? 0,\n routeLatency: this.routeLatency,\n })\n })\n\n if (this.exposeConfig) {\n router.get('/config', (_req: Request, res: Response) => {\n const config: Record<string, string> = {}\n for (const [key, value] of Object.entries(process.env)) {\n if (value === undefined) continue\n const allowed = this.configPrefixes.some((prefix) => key.startsWith(prefix))\n config[key] = allowed ? value : '[REDACTED]'\n }\n res.json({ config })\n })\n }\n\n app.use(this.basePath, router)\n log.info(`DevTools mounted at ${this.basePath}`)\n }\n\n middleware(): AdapterMiddleware[] {\n if (!this.enabled) return []\n\n return [\n {\n handler: (req: Request, res: Response, next: NextFunction) => {\n const start = Date.now()\n this.requestCount.value++\n\n res.on('finish', () => {\n if (res.statusCode >= 500) this.errorCount.value++\n else if (res.statusCode >= 400) this.clientErrorCount.value++\n\n // Track per-route latency\n const routeKey = `${req.method} ${req.route?.path ?? req.path}`\n const elapsed = Date.now() - start\n\n if (!this.routeLatency[routeKey]) {\n this.routeLatency[routeKey] = {\n count: 0,\n totalMs: 0,\n minMs: Infinity,\n maxMs: 0,\n }\n }\n const stats = this.routeLatency[routeKey]\n stats.count++\n stats.totalMs += elapsed\n stats.minMs = Math.min(stats.minMs, elapsed)\n stats.maxMs = Math.max(stats.maxMs, elapsed)\n })\n\n next()\n },\n phase: 'beforeGlobal',\n },\n ]\n }\n\n onRouteMount(controllerClass: any, mountPath: string): void {\n if (!this.enabled) return\n\n const routes: Array<{ method: string; path: string; handlerName: string }> =\n Reflect.getMetadata(METADATA.ROUTES, controllerClass) ?? []\n\n const classMiddleware: any[] =\n Reflect.getMetadata(METADATA.CLASS_MIDDLEWARES, controllerClass) ?? []\n\n for (const route of routes) {\n const methodMiddleware: any[] =\n Reflect.getMetadata(\n METADATA.METHOD_MIDDLEWARES,\n controllerClass.prototype,\n route.handlerName,\n ) ?? []\n\n this.routes.push({\n method: route.method.toUpperCase(),\n path: `${mountPath}${route.path === '/' ? '' : route.path}`,\n controller: controllerClass.name,\n handler: route.handlerName,\n middleware: [\n ...classMiddleware.map((m: any) => m.name || 'anonymous'),\n ...methodMiddleware.map((m: any) => m.name || 'anonymous'),\n ],\n })\n }\n }\n\n beforeStart(_app: any, _container: Container): void {\n if (!this.enabled) return\n this.adapterStatuses[this.name] = 'running'\n }\n\n afterStart(_server: any, _container: Container): void {\n if (!this.enabled) return\n log.info(\n `DevTools ready — ${this.routes.length} routes tracked, ` +\n `${this.container?.getRegistrations().length ?? 0} DI bindings`,\n )\n }\n\n shutdown(): void {\n this.stopErrorWatch?.()\n this.adapterStatuses[this.name] = 'stopped'\n }\n}\n"],"mappings":";;;;;AACA,SAASA,cAAc;AACvB,SAIEC,UACAC,KACAC,UACAC,UACAC,OACAC,oBAGK;AAEP,IAAMC,MAAMC,aAAa,UAAA;AA6DlB,IAAMC,kBAAN,MAAMA;EA5Eb,OA4EaA;;;EACFC,OAAO;EAERC;EACAC;EACAC;EACAC;EACAC;;;EAICC;;EAEAC;;EAEAC;;EAEAC;;EAEAC;;EAEAC;;EAEAC;;EAGDC,SAAsB,CAAA;EACtBC,YAA8B;EAC9BC,kBAA0C,CAAC;EAC3CC,iBAAsC;EAE9C,YAAYC,UAA2B,CAAC,GAAG;AACzC,SAAKhB,WAAWgB,QAAQhB,YAAY;AACpC,SAAKC,UAAUe,QAAQf,WAAWgB,QAAQC,IAAIC,aAAa;AAC3D,SAAKjB,eAAec,QAAQd,gBAAgB;AAC5C,SAAKC,iBAAiBa,QAAQb,kBAAkB;MAAC;MAAQ;;AACzD,SAAKC,qBAAqBY,QAAQZ,sBAAsB;AAGxD,SAAKC,eAAee,IAAI,CAAA;AACxB,SAAKd,aAAac,IAAI,CAAA;AACtB,SAAKb,mBAAmBa,IAAI,CAAA;AAC5B,SAAKZ,YAAYY,IAAIC,KAAKC,IAAG,CAAA;AAC7B,SAAKX,eAAeY,SAAS,CAAC,CAAA;AAE9B,SAAKd,YAAYe,SAAS,MACxB,KAAKnB,aAAaoB,QAAQ,IAAI,KAAKnB,WAAWmB,QAAQ,KAAKpB,aAAaoB,QAAQ,CAAA;AAGlF,SAAKf,gBAAgBc,SAAS,MAAME,KAAKC,OAAON,KAAKC,IAAG,IAAK,KAAKd,UAAUiB,SAAS,GAAA,CAAA;AAGrF,QAAIT,QAAQY,qBAAqB;AAC/B,YAAMC,WAAWb,QAAQY;AACzB,YAAME,YAAY,KAAK1B;AACvB,WAAKW,iBAAiBgB,MAAM,KAAKtB,WAAW,CAACuB,SAAAA;AAC3C,YAAIA,OAAOF,WAAW;AACpBD,mBAASG,IAAAA;QACX;MACF,CAAA;IACF,OAAO;AACL,WAAKjB,iBAAiBgB,MAAM,KAAKtB,WAAW,CAACuB,SAAAA;AAC3C,YAAIA,OAAO,KAAK5B,oBAAoB;AAClCR,cAAIqC,KAAK,yBAAyBD,OAAO,KAAKE,QAAQ,CAAA,CAAA,GAAK;QAC7D;MACF,CAAA;IACF;EACF;;EAIAC,YAAYC,KAAUvB,WAA4B;AAChD,QAAI,CAAC,KAAKZ,QAAS;AAEnB,SAAKY,YAAYA;AACjB,SAAKL,UAAUiB,QAAQJ,KAAKC,IAAG;AAE/B,UAAMe,SAASC,OAAAA;AAEfD,WAAOE,IAAI,WAAW,CAACC,MAAeC,QAAAA;AACpCA,UAAIC,KAAK;QAAE9B,QAAQ,KAAKA;MAAO,CAAA;IACjC,CAAA;AAEAyB,WAAOE,IAAI,cAAc,CAACC,MAAeC,QAAAA;AACvC,YAAME,gBAAgB,KAAK9B,WAAW+B,iBAAAA,KAAsB,CAAA;AAC5DH,UAAIC,KAAK;QAAEC;QAAeE,OAAOF,cAAcG;MAAO,CAAA;IACxD,CAAA;AAEAT,WAAOE,IAAI,YAAY,CAACC,MAAeC,QAAAA;AACrCA,UAAIC,KAAK;QACPK,UAAU,KAAK1C,aAAaoB;QAC5BuB,cAAc,KAAK1C,WAAWmB;QAC9BwB,cAAc,KAAK1C,iBAAiBkB;QACpChB,WAAW,KAAKA,UAAUgB;QAC1Bf,eAAe,KAAKA,cAAce;QAClCjB,WAAW,IAAIa,KAAK,KAAKb,UAAUiB,KAAK,EAAEyB,YAAW;QACrDvC,cAAc,KAAKA;MACrB,CAAA;IACF,CAAA;AAEA0B,WAAOE,IAAI,WAAW,CAACC,MAAeC,QAAAA;AACpC,YAAMU,UAAU,KAAK1C,UAAUgB,QAAQ,KAAKrB;AAC5C,YAAMgD,SAASD,UAAU,YAAY;AAErCV,UAAIW,OAAOD,UAAU,MAAM,GAAA,EAAKT,KAAK;QACnCU;QACA3C,WAAW,KAAKA,UAAUgB;QAC1B4B,QAAQ,KAAK3C,cAAce;QAC3B6B,UAAU,KAAKxC;MACjB,CAAA;IACF,CAAA;AAEAuB,WAAOE,IAAI,UAAU,CAACC,MAAeC,QAAAA;AACnCA,UAAIC,KAAK;QACPnB,UAAU;UACRlB,cAAc,KAAKA,aAAaoB;UAChCnB,YAAY,KAAKA,WAAWmB;UAC5BlB,kBAAkB,KAAKA,iBAAiBkB;UACxChB,WAAW,KAAKA,UAAUgB;UAC1Bf,eAAe,KAAKA,cAAce;UAClCjB,WAAW,IAAIa,KAAK,KAAKb,UAAUiB,KAAK,EAAEyB,YAAW;QACvD;QACAtC,QAAQ,KAAKA,OAAOkC;QACpBjC,WAAW,KAAKA,WAAW+B,iBAAAA,EAAmBE,UAAU;QACxDnC,cAAc,KAAKA;MACrB,CAAA;IACF,CAAA;AAEA,QAAI,KAAKT,cAAc;AACrBmC,aAAOE,IAAI,WAAW,CAACC,MAAeC,QAAAA;AACpC,cAAMc,SAAiC,CAAC;AACxC,mBAAW,CAACC,KAAK/B,KAAAA,KAAUgC,OAAOC,QAAQzC,QAAQC,GAAG,GAAG;AACtD,cAAIO,UAAUkC,OAAW;AACzB,gBAAMC,UAAU,KAAKzD,eAAe0D,KAAK,CAACC,WAAWN,IAAIO,WAAWD,MAAAA,CAAAA;AACpEP,iBAAOC,GAAAA,IAAOI,UAAUnC,QAAQ;QAClC;AACAgB,YAAIC,KAAK;UAAEa;QAAO,CAAA;MACpB,CAAA;IACF;AAEAnB,QAAI4B,IAAI,KAAKhE,UAAUqC,MAAAA;AACvBzC,QAAIqE,KAAK,uBAAuB,KAAKjE,QAAQ,EAAE;EACjD;EAEAkE,aAAkC;AAChC,QAAI,CAAC,KAAKjE,QAAS,QAAO,CAAA;AAE1B,WAAO;MACL;QACEkE,SAAS,wBAACC,KAAc3B,KAAe4B,SAAAA;AACrC,gBAAMC,QAAQjD,KAAKC,IAAG;AACtB,eAAKjB,aAAaoB;AAElBgB,cAAI8B,GAAG,UAAU,MAAA;AACf,gBAAI9B,IAAI+B,cAAc,IAAK,MAAKlE,WAAWmB;qBAClCgB,IAAI+B,cAAc,IAAK,MAAKjE,iBAAiBkB;AAGtD,kBAAMgD,WAAW,GAAGL,IAAIM,MAAM,IAAIN,IAAIO,OAAOC,QAAQR,IAAIQ,IAAI;AAC7D,kBAAMC,UAAUxD,KAAKC,IAAG,IAAKgD;AAE7B,gBAAI,CAAC,KAAK3D,aAAa8D,QAAAA,GAAW;AAChC,mBAAK9D,aAAa8D,QAAAA,IAAY;gBAC5B5B,OAAO;gBACPiC,SAAS;gBACTC,OAAOC;gBACPC,OAAO;cACT;YACF;AACA,kBAAMC,QAAQ,KAAKvE,aAAa8D,QAAAA;AAChCS,kBAAMrC;AACNqC,kBAAMJ,WAAWD;AACjBK,kBAAMH,QAAQrD,KAAKyD,IAAID,MAAMH,OAAOF,OAAAA;AACpCK,kBAAMD,QAAQvD,KAAK0D,IAAIF,MAAMD,OAAOJ,OAAAA;UACtC,CAAA;AAEAR,eAAAA;QACF,GA5BS;QA6BTgB,OAAO;MACT;;EAEJ;EAEAC,aAAaC,iBAAsBC,WAAyB;AAC1D,QAAI,CAAC,KAAKvF,QAAS;AAEnB,UAAMW,SACJ6E,QAAQC,YAAYC,SAASC,QAAQL,eAAAA,KAAoB,CAAA;AAE3D,UAAMM,kBACJJ,QAAQC,YAAYC,SAASG,mBAAmBP,eAAAA,KAAoB,CAAA;AAEtE,eAAWZ,SAAS/D,QAAQ;AAC1B,YAAMmF,mBACJN,QAAQC,YACNC,SAASK,oBACTT,gBAAgBU,WAChBtB,MAAMuB,WAAW,KACd,CAAA;AAEP,WAAKtF,OAAOuF,KAAK;QACfzB,QAAQC,MAAMD,OAAO0B,YAAW;QAChCxB,MAAM,GAAGY,SAAAA,GAAYb,MAAMC,SAAS,MAAM,KAAKD,MAAMC,IAAI;QACzDyB,YAAYd,gBAAgBxF;QAC5BoE,SAASQ,MAAMuB;QACfhC,YAAY;aACP2B,gBAAgBS,IAAI,CAACC,MAAWA,EAAExG,QAAQ,WAAA;aAC1CgG,iBAAiBO,IAAI,CAACC,MAAWA,EAAExG,QAAQ,WAAA;;MAElD,CAAA;IACF;EACF;EAEAyG,YAAYC,MAAWC,YAA6B;AAClD,QAAI,CAAC,KAAKzG,QAAS;AACnB,SAAKa,gBAAgB,KAAKf,IAAI,IAAI;EACpC;EAEA4G,WAAWC,SAAcF,YAA6B;AACpD,QAAI,CAAC,KAAKzG,QAAS;AACnBL,QAAIqE,KACF,yBAAoB,KAAKrD,OAAOkC,MAAM,oBACjC,KAAKjC,WAAW+B,iBAAAA,EAAmBE,UAAU,CAAA,cAAe;EAErE;EAEA+D,WAAiB;AACf,SAAK9F,iBAAc;AACnB,SAAKD,gBAAgB,KAAKf,IAAI,IAAI;EACpC;AACF;","names":["Router","METADATA","ref","computed","reactive","watch","createLogger","log","createLogger","DevToolsAdapter","name","basePath","enabled","exposeConfig","configPrefixes","errorRateThreshold","requestCount","errorCount","clientErrorCount","startedAt","errorRate","uptimeSeconds","routeLatency","routes","container","adapterStatuses","stopErrorWatch","options","process","env","NODE_ENV","ref","Date","now","reactive","computed","value","Math","floor","onErrorRateExceeded","callback","threshold","watch","rate","warn","toFixed","beforeMount","app","router","Router","get","_req","res","json","registrations","getRegistrations","count","length","requests","serverErrors","clientErrors","toISOString","healthy","status","uptime","adapters","config","key","Object","entries","undefined","allowed","some","prefix","startsWith","use","info","middleware","handler","req","next","start","on","statusCode","routeKey","method","route","path","elapsed","totalMs","minMs","Infinity","maxMs","stats","min","max","phase","onRouteMount","controllerClass","mountPath","Reflect","getMetadata","METADATA","ROUTES","classMiddleware","CLASS_MIDDLEWARES","methodMiddleware","METHOD_MIDDLEWARES","prototype","handlerName","push","toUpperCase","controller","map","m","beforeStart","_app","_container","afterStart","_server","shutdown"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/context.ts"],"sourcesContent":["import type { Request, Response, NextFunction } from 'express'\nimport { parseQuery, type ParsedQuery, type QueryFieldConfig } from './query'\n\n/**\n * Unified request/response abstraction passed to every controller method.\n * Shields handlers from raw Express objects and provides convenience methods.\n */\nexport class RequestContext<TBody = any, TParams = any, TQuery = any> {\n private metadata = new Map<string, any>()\n\n constructor(\n public readonly req: Request,\n public readonly res: Response,\n public readonly next: NextFunction,\n ) {}\n\n // ── Request Data ────────────────────────────────────────────────────\n\n get body(): TBody {\n return this.req.body as TBody\n }\n\n get params(): TParams {\n return this.req.params as TParams\n }\n\n get query(): TQuery {\n return this.req.query as TQuery\n }\n\n get headers() {\n return 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 /** Session data (requires session middleware) */\n get session(): any {\n return (this.req as any).session\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 * @param fieldConfig - Optional whitelist for filterable, sortable, and searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * const parsed = ctx.qs({\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt', 'title'],\n * })\n * const q = drizzleAdapter.build(parsed, { columns })\n * // ... use q.where, q.orderBy, q.limit, q.offset\n * }\n * ```\n */\n qs(fieldConfig?: QueryFieldConfig): ParsedQuery {\n return parseQuery(this.req.query as Record<string, any>, fieldConfig)\n }\n\n // ── File Uploads ────────────────────────────────────────────────────\n\n /** Single uploaded file (requires @FileUpload({ mode: 'single' })) */\n get file(): any {\n return (this.req as any).file\n }\n\n /** Array of uploaded files (requires @FileUpload({ mode: 'array' })) */\n get files(): any[] | undefined {\n return (this.req as any).files\n }\n\n // ── Metadata Store ──────────────────────────────────────────────────\n\n get<T = any>(key: string): T | undefined {\n return this.metadata.get(key) as T | undefined\n }\n\n set(key: string, value: any): void {\n this.metadata.set(key, value)\n }\n\n // ── Response Helpers ────────────────────────────────────────────────\n\n json(data: any, status = 200) {\n return this.res.status(status).json(data)\n }\n\n created(data: any) {\n return this.res.status(201).json(data)\n }\n\n noContent() {\n return this.res.status(204).end()\n }\n\n notFound(message = 'Not Found') {\n return this.res.status(404).json({ message })\n }\n\n badRequest(message: string) {\n return this.res.status(400).json({ message })\n }\n\n html(content: string, status = 200) {\n return this.res.status(status).type('html').send(content)\n }\n\n download(buffer: Buffer, filename: string, contentType = 'application/octet-stream') {\n this.res.setHeader('Content-Disposition', `attachment; filename=\"${filename}\"`)\n this.res.setHeader('Content-Type', contentType)\n return this.res.send(buffer)\n }\n}\n"],"mappings":";;;;;;;;AAOO,IAAMA,iBAAN,MAAMA;EANb,OAMaA;;;;;;EACHC,WAAW,oBAAIC,IAAAA;EAEvB,YACkBC,KACAC,KACAC,MAChB;SAHgBF,MAAAA;SACAC,MAAAA;SACAC,OAAAA;EACf;;EAIH,IAAIC,OAAc;AAChB,WAAO,KAAKH,IAAIG;EAClB;EAEA,IAAIC,SAAkB;AACpB,WAAO,KAAKJ,IAAII;EAClB;EAEA,IAAIC,QAAgB;AAClB,WAAO,KAAKL,IAAIK;EAClB;EAEA,IAAIC,UAAU;AACZ,WAAO,KAAKN,IAAIM;EAClB;EAEA,IAAIC,YAAgC;AAClC,WAAQ,KAAKP,IAAYO,aAAc,KAAKP,IAAIM,QAAQ,cAAA;EAC1D;;EAGA,IAAIE,UAAe;AACjB,WAAQ,KAAKR,IAAYQ;EAC3B;;;;;;;;;;;;;;;;;;;;;EAuBAC,GAAGC,aAA6C;AAC9C,WAAOC,WAAW,KAAKX,IAAIK,OAA8BK,WAAAA;EAC3D;;;EAKA,IAAIE,OAAY;AACd,WAAQ,KAAKZ,IAAYY;EAC3B;;EAGA,IAAIC,QAA2B;AAC7B,WAAQ,KAAKb,IAAYa;EAC3B;;EAIAC,IAAaC,KAA4B;AACvC,WAAO,KAAKjB,SAASgB,IAAIC,GAAAA;EAC3B;EAEAC,IAAID,KAAaE,OAAkB;AACjC,SAAKnB,SAASkB,IAAID,KAAKE,KAAAA;EACzB;;EAIAC,KAAKC,MAAWC,SAAS,KAAK;AAC5B,WAAO,KAAKnB,IAAImB,OAAOA,MAAAA,EAAQF,KAAKC,IAAAA;EACtC;EAEAE,QAAQF,MAAW;AACjB,WAAO,KAAKlB,IAAImB,OAAO,GAAA,EAAKF,KAAKC,IAAAA;EACnC;EAEAG,YAAY;AACV,WAAO,KAAKrB,IAAImB,OAAO,GAAA,EAAKG,IAAG;EACjC;EAEAC,SAASC,UAAU,aAAa;AAC9B,WAAO,KAAKxB,IAAImB,OAAO,GAAA,EAAKF,KAAK;MAAEO;IAAQ,CAAA;EAC7C;EAEAC,WAAWD,SAAiB;AAC1B,WAAO,KAAKxB,IAAImB,OAAO,GAAA,EAAKF,KAAK;MAAEO;IAAQ,CAAA;EAC7C;EAEAE,KAAKC,SAAiBR,SAAS,KAAK;AAClC,WAAO,KAAKnB,IAAImB,OAAOA,MAAAA,EAAQS,KAAK,MAAA,EAAQC,KAAKF,OAAAA;EACnD;EAEAG,SAASC,QAAgBC,UAAkBC,cAAc,4BAA4B;AACnF,SAAKjC,IAAIkC,UAAU,uBAAuB,yBAAyBF,QAAAA,GAAW;AAC9E,SAAKhC,IAAIkC,UAAU,gBAAgBD,WAAAA;AACnC,WAAO,KAAKjC,IAAI6B,KAAKE,MAAAA;EACvB;AACF;","names":["RequestContext","metadata","Map","req","res","next","body","params","query","headers","requestId","session","qs","fieldConfig","parseQuery","file","files","get","key","set","value","json","data","status","created","noContent","end","notFound","message","badRequest","html","content","type","send","download","buffer","filename","contentType","setHeader"]}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"sources":["../src/query/types.ts","../src/query/parse-query.ts"],"sourcesContent":["/** Supported filter operators for query string parsing */\nexport type FilterOperator =\n | 'eq'\n | 'neq'\n | 'gt'\n | 'gte'\n | 'lt'\n | 'lte'\n | 'between'\n | 'in'\n | 'contains'\n | 'starts'\n | 'ends'\n\nexport const FILTER_OPERATORS = new Set<string>([\n 'eq',\n 'neq',\n 'gt',\n 'gte',\n 'lt',\n 'lte',\n 'between',\n 'in',\n 'contains',\n 'starts',\n 'ends',\n])\n\nexport interface FilterItem {\n field: string\n operator: FilterOperator\n value: string\n}\n\nexport interface SortItem {\n field: string\n direction: 'asc' | 'desc'\n}\n\nexport interface PaginationParams {\n page: number\n limit: number\n offset: number\n}\n\n/**\n * The result of parsing a query string. ORM-agnostic — pass this to\n * a query builder adapter (Drizzle, Prisma, Sequelize, etc.) to produce\n * database-specific query objects.\n */\nexport interface ParsedQuery {\n filters: FilterItem[]\n sort: SortItem[]\n pagination: PaginationParams\n search: string\n}\n\n/**\n * Restrict which fields can be filtered, sorted, or searched.\n * Fields not in the allow-list are silently ignored.\n */\nexport interface QueryFieldConfig {\n filterable?: string[]\n sortable?: string[]\n searchable?: string[]\n}\n\n/**\n * Interface for ORM-specific query builder adapters.\n * Implement this to translate a `ParsedQuery` into your ORM's query format.\n *\n * @example\n * ```ts\n * // Drizzle adapter\n * class DrizzleQueryAdapter implements QueryBuilderAdapter<DrizzleQueryResult> {\n * build(parsed: ParsedQuery, config: DrizzleConfig): DrizzleQueryResult {\n * // Convert filters → Drizzle SQL conditions\n * // Convert sort → Drizzle orderBy\n * return { where, orderBy, limit, offset }\n * }\n * }\n *\n * // Prisma adapter\n * class PrismaQueryAdapter implements QueryBuilderAdapter<PrismaQueryResult> {\n * build(parsed: ParsedQuery, config: PrismaConfig): PrismaQueryResult {\n * return { where, orderBy, skip, take }\n * }\n * }\n * ```\n */\nexport interface QueryBuilderAdapter<TResult = any, TConfig = any> {\n /** Human-readable name for debugging */\n readonly name: string\n\n /**\n * Convert a ParsedQuery into an ORM-specific query object.\n * @param parsed - The ORM-agnostic parsed query\n * @param config - ORM-specific configuration (column maps, search columns, etc.)\n */\n build(parsed: ParsedQuery, config: TConfig): TResult\n}\n","import {\n FILTER_OPERATORS,\n type FilterItem,\n type FilterOperator,\n type SortItem,\n type PaginationParams,\n type ParsedQuery,\n type QueryFieldConfig,\n} from './types'\n\nconst MAX_SEARCH_LENGTH = 200\nconst DEFAULT_PAGE = 1\nconst DEFAULT_LIMIT = 20\nconst MAX_LIMIT = 100\n\n// ── Individual parsers ──────────────────────────────────────────────────\n\n/** Parse filter strings like \"status:eq:active\" into structured objects */\nexport function parseFilters(\n filterParam: string | string[] | undefined,\n allowedFields?: string[],\n): FilterItem[] {\n if (!filterParam) return []\n\n const items = Array.isArray(filterParam) ? filterParam : [filterParam]\n const results: FilterItem[] = []\n const allowed = allowedFields ? new Set(allowedFields) : null\n\n for (const item of items) {\n // Split on first two colons only — value may contain colons (e.g. timestamps)\n const firstColon = item.indexOf(':')\n if (firstColon === -1) continue\n\n const secondColon = item.indexOf(':', firstColon + 1)\n if (secondColon === -1) continue\n\n const field = item.slice(0, firstColon)\n const operator = item.slice(firstColon + 1, secondColon)\n const value = item.slice(secondColon + 1)\n\n if (!field || !value) continue\n if (!FILTER_OPERATORS.has(operator)) continue\n if (allowed && !allowed.has(field)) continue\n\n results.push({ field, operator: operator as FilterOperator, value })\n }\n\n return results\n}\n\n/** Parse sort strings like \"firstName:asc\" into structured objects */\nexport function parseSort(\n sortParam: string | string[] | undefined,\n allowedFields?: string[],\n): SortItem[] {\n if (!sortParam) return []\n\n const items = Array.isArray(sortParam) ? sortParam : [sortParam]\n const results: SortItem[] = []\n const allowed = allowedFields ? new Set(allowedFields) : null\n\n for (const item of items) {\n // Split on last colon so field names with colons work\n const lastColon = item.lastIndexOf(':')\n if (lastColon === -1) continue\n\n const field = item.slice(0, lastColon)\n const dir = item.slice(lastColon + 1).toLowerCase()\n\n if (!field) continue\n if (dir !== 'asc' && dir !== 'desc') continue\n if (allowed && !allowed.has(field)) continue\n\n results.push({ field, direction: dir })\n }\n\n return results\n}\n\n/** Parse page/limit into pagination with computed offset */\nexport function parsePagination(params: {\n page?: string | number\n limit?: string | number\n}): PaginationParams {\n let page =\n typeof params.page === 'string' ? parseInt(params.page, 10) : (params.page ?? DEFAULT_PAGE)\n let limit =\n typeof params.limit === 'string' ? parseInt(params.limit, 10) : (params.limit ?? DEFAULT_LIMIT)\n\n if (isNaN(page) || page < 1) page = DEFAULT_PAGE\n if (isNaN(limit) || limit < 1) limit = DEFAULT_LIMIT\n if (limit > MAX_LIMIT) limit = MAX_LIMIT\n\n return { page, limit, offset: (page - 1) * limit }\n}\n\n/** Sanitize and truncate search query */\nexport function parseSearchQuery(q: string | undefined): string {\n if (!q) return ''\n return q.trim().slice(0, MAX_SEARCH_LENGTH)\n}\n\n// ── Combined parser ─────────────────────────────────────────────────────\n\n/**\n * Parse a raw Express query object into a structured, ORM-agnostic ParsedQuery.\n *\n * @param query - Raw query string object from `req.query` or Zod-validated object\n * @param fieldConfig - Optional field restrictions (whitelist filterable/sortable/searchable)\n *\n * @example\n * ```ts\n * // In a controller\n * @Get('/')\n * async list(ctx: RequestContext) {\n * const parsed = parseQuery(ctx.query, {\n * filterable: ['status', 'priority'],\n * sortable: ['createdAt', 'title'],\n * })\n * // Pass to your ORM query builder adapter\n * const q = drizzleAdapter.build(parsed, { columns, searchColumns })\n * }\n * ```\n *\n * Query string format:\n * - Filters: `?filter=field:operator:value` (repeatable)\n * - Sort: `?sort=field:asc|desc` (repeatable)\n * - Pagination: `?page=1&limit=20`\n * - Search: `?q=search+term`\n *\n * Filter operators: eq, neq, gt, gte, lt, lte, between, in, contains, starts, ends\n */\nexport function parseQuery(\n query: Record<string, any>,\n fieldConfig?: QueryFieldConfig,\n): ParsedQuery {\n return {\n filters: parseFilters(query.filter, fieldConfig?.filterable),\n sort: parseSort(query.sort, fieldConfig?.sortable),\n pagination: parsePagination({ page: query.page, limit: query.limit }),\n search: parseSearchQuery(query.q),\n }\n}\n\n// ── Query URL builder (for client-side or testing) ──────────────────────\n\n/** Convert ParsedQuery back into query string parameters */\nexport function buildQueryParams(\n parsed: Partial<ParsedQuery>,\n): Record<string, string | string[] | number> {\n const params: Record<string, string | string[] | number> = {}\n\n if (parsed.filters?.length) {\n params.filter = parsed.filters.map((f) => `${f.field}:${f.operator}:${f.value}`)\n }\n\n if (parsed.sort?.length) {\n params.sort = parsed.sort.map((s) => `${s.field}:${s.direction}`)\n }\n\n if (parsed.pagination) {\n params.page = parsed.pagination.page\n params.limit = parsed.pagination.limit\n }\n\n if (parsed.search) {\n params.q = parsed.search\n }\n\n return params\n}\n"],"mappings":"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","names":["FILTER_OPERATORS","Set","MAX_SEARCH_LENGTH","DEFAULT_PAGE","DEFAULT_LIMIT","MAX_LIMIT","parseFilters","filterParam","allowedFields","items","Array","isArray","results","allowed","Set","item","firstColon","indexOf","secondColon","field","slice","operator","value","FILTER_OPERATORS","has","push","parseSort","sortParam","lastColon","lastIndexOf","dir","toLowerCase","direction","parsePagination","params","page","parseInt","limit","isNaN","offset","parseSearchQuery","q","trim","parseQuery","query","fieldConfig","filters","filter","filterable","sort","sortable","pagination","search","buildQueryParams","parsed","length","map","f","s"]}
|
|
File without changes
|
|
File without changes
|