@forinda/kickjs-http 0.7.0 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/{chunk-E552HYEB.js → chunk-2OSVROBK.js} +2 -2
- package/dist/{chunk-Y5ZSC2FB.js → chunk-IUT42U72.js} +17 -1
- package/dist/chunk-IUT42U72.js.map +1 -0
- package/dist/context.d.ts +14 -0
- package/dist/context.js +1 -1
- package/dist/index.js +2 -2
- package/dist/middleware/views.d.ts +58 -0
- package/dist/middleware/views.js +40 -0
- package/dist/middleware/views.js.map +1 -0
- package/dist/router-builder.js +2 -2
- package/package.json +6 -2
- package/dist/chunk-Y5ZSC2FB.js.map +0 -1
- /package/dist/{chunk-E552HYEB.js.map → chunk-2OSVROBK.js.map} +0 -0
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
} from "./chunk-RPN7UFUO.js";
|
|
7
7
|
import {
|
|
8
8
|
RequestContext
|
|
9
|
-
} from "./chunk-
|
|
9
|
+
} from "./chunk-IUT42U72.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-2OSVROBK.js.map
|
|
@@ -107,6 +107,22 @@ var RequestContext = class {
|
|
|
107
107
|
return this.res.send(buffer);
|
|
108
108
|
}
|
|
109
109
|
/**
|
|
110
|
+
* Render a template using the registered view engine (EJS, Pug, Handlebars, etc.).
|
|
111
|
+
* Requires a ViewAdapter to be configured in bootstrap().
|
|
112
|
+
*
|
|
113
|
+
* @param template - Template name (without extension, relative to viewsDir)
|
|
114
|
+
* @param data - Data to pass to the template
|
|
115
|
+
*
|
|
116
|
+
* @example
|
|
117
|
+
* ```ts
|
|
118
|
+
* ctx.render('dashboard', { user, title: 'Dashboard' })
|
|
119
|
+
* ctx.render('emails/welcome', { name: 'Alice' })
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
render(template, data = {}) {
|
|
123
|
+
return this.res.render(template, data);
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
110
126
|
* Parse query params and return a standardized paginated response.
|
|
111
127
|
* Calls `ctx.qs()` internally, then wraps your data with pagination meta.
|
|
112
128
|
*
|
|
@@ -211,4 +227,4 @@ var RequestContext = class {
|
|
|
211
227
|
export {
|
|
212
228
|
RequestContext
|
|
213
229
|
};
|
|
214
|
-
//# sourceMappingURL=chunk-
|
|
230
|
+
//# sourceMappingURL=chunk-IUT42U72.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 * Render a template using the registered view engine (EJS, Pug, Handlebars, etc.).\n * Requires a ViewAdapter to be configured in bootstrap().\n *\n * @param template - Template name (without extension, relative to viewsDir)\n * @param data - Data to pass to the template\n *\n * @example\n * ```ts\n * ctx.render('dashboard', { user, title: 'Dashboard' })\n * ctx.render('emails/welcome', { name: 'Alice' })\n * ```\n */\n render(template: string, data: Record<string, any> = {}) {\n return this.res.render(template, data)\n }\n\n /**\n * Parse query params and return a standardized paginated response.\n * Calls `ctx.qs()` internally, then wraps your data with pagination meta.\n *\n * @param fetcher - Async function that receives ParsedQuery and returns `{ data, total }`\n * @param fieldConfig - Optional whitelist for filterable, sortable, searchable fields\n *\n * @example\n * ```ts\n * @Get('/')\n * async list(ctx: RequestContext) {\n * return ctx.paginate(\n * async (parsed) => {\n * const data = await db.select().from(users)\n * .where(query.where).limit(parsed.pagination.limit)\n * .offset(parsed.pagination.offset).all()\n * const total = await db.select({ count: count() }).from(users).get()\n * return { data, total: total?.count ?? 0 }\n * },\n * { filterable: ['name', 'role'], sortable: ['createdAt'] },\n * )\n * }\n * ```\n */\n async paginate<T>(\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 // ── Server-Sent Events ──────────────────────────────────────────────\n\n /**\n * Start an SSE (Server-Sent Events) stream.\n * Sets the correct headers and returns helpers to send events.\n *\n * @example\n * ```ts\n * @Get('/events')\n * async stream(ctx: RequestContext) {\n * const sse = ctx.sse()\n *\n * const interval = setInterval(() => {\n * sse.send({ time: new Date().toISOString() }, 'tick')\n * }, 1000)\n *\n * sse.onClose(() => clearInterval(interval))\n * }\n * ```\n */\n sse() {\n this.res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n })\n this.res.flushHeaders()\n\n const closeCallbacks: Array<() => void> = []\n\n this.req.on('close', () => {\n for (const cb of closeCallbacks) cb()\n })\n\n return {\n /** Send an SSE event with optional event name and id */\n send: (data: any, event?: string, id?: string) => {\n if (id) this.res.write(`id: ${id}\\n`)\n if (event) this.res.write(`event: ${event}\\n`)\n this.res.write(`data: ${JSON.stringify(data)}\\n\\n`)\n },\n /** Send a comment (keeps connection alive) */\n comment: (text: string) => {\n this.res.write(`: ${text}\\n\\n`)\n },\n /** Register a callback when the client disconnects */\n onClose: (fn: () => void) => {\n closeCallbacks.push(fn)\n },\n /** End the SSE stream */\n close: () => {\n this.res.end()\n },\n }\n }\n}\n"],"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;;;;;;;;;;;;;;EAeAI,OAAOC,UAAkBlB,OAA4B,CAAC,GAAG;AACvD,WAAO,KAAKlB,IAAImC,OAAOC,UAAUlB,IAAAA;EACnC;;;;;;;;;;;;;;;;;;;;;;;;;EA0BA,MAAMmB,SACJC,SACA7B,aACA;AACA,UAAM8B,SAAS,KAAK/B,GAAGC,WAAAA;AACvB,UAAM,EAAES,MAAMsB,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;MACrC7B;MACA8B,MAAM;QACJP;QACAC;QACAF;QACAI;QACAK,SAASR,OAAOG;QAChBM,SAAST,OAAO;MAClB;IACF;AAEA,WAAO,KAAKxB,KAAK8B,QAAAA;EACnB;;;;;;;;;;;;;;;;;;;;EAsBAI,MAAM;AACJ,SAAKnD,IAAIoD,UAAU,KAAK;MACtB,gBAAgB;MAChB,iBAAiB;MACjBC,YAAY;MACZ,qBAAqB;IACvB,CAAA;AACA,SAAKrD,IAAIsD,aAAY;AAErB,UAAMC,iBAAoC,CAAA;AAE1C,SAAKxD,IAAIyD,GAAG,SAAS,MAAA;AACnB,iBAAWC,MAAMF,eAAgBE,IAAAA;IACnC,CAAA;AAEA,WAAO;;MAEL5B,MAAM,wBAACX,MAAWwC,OAAgBC,OAAAA;AAChC,YAAIA,GAAI,MAAK3D,IAAI4D,MAAM,OAAOD,EAAAA;CAAM;AACpC,YAAID,MAAO,MAAK1D,IAAI4D,MAAM,UAAUF,KAAAA;CAAS;AAC7C,aAAK1D,IAAI4D,MAAM,SAASC,KAAKC,UAAU5C,IAAAA,CAAAA;;CAAW;MACpD,GAJM;;MAMN6C,SAAS,wBAACC,SAAAA;AACR,aAAKhE,IAAI4D,MAAM,KAAKI,IAAAA;;CAAU;MAChC,GAFS;;MAITC,SAAS,wBAACC,OAAAA;AACRX,uBAAeY,KAAKD,EAAAA;MACtB,GAFS;;MAITE,OAAO,6BAAA;AACL,aAAKpE,IAAIsB,IAAG;MACd,GAFO;IAGT;EACF;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","render","template","paginate","fetcher","parsed","total","page","limit","pagination","totalPages","Math","ceil","response","meta","hasNext","hasPrev","sse","writeHead","Connection","flushHeaders","closeCallbacks","on","cb","event","id","write","JSON","stringify","comment","text","onClose","fn","push","close"]}
|
package/dist/context.d.ts
CHANGED
|
@@ -52,6 +52,20 @@ 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
|
+
* Render a template using the registered view engine (EJS, Pug, Handlebars, etc.).
|
|
57
|
+
* Requires a ViewAdapter to be configured in bootstrap().
|
|
58
|
+
*
|
|
59
|
+
* @param template - Template name (without extension, relative to viewsDir)
|
|
60
|
+
* @param data - Data to pass to the template
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* ```ts
|
|
64
|
+
* ctx.render('dashboard', { user, title: 'Dashboard' })
|
|
65
|
+
* ctx.render('emails/welcome', { name: 'Alice' })
|
|
66
|
+
* ```
|
|
67
|
+
*/
|
|
68
|
+
render(template: string, data?: Record<string, any>): void;
|
|
55
69
|
/**
|
|
56
70
|
* Parse query params and return a standardized paginated response.
|
|
57
71
|
* Calls `ctx.qs()` internally, then wraps your data with pagination meta.
|
package/dist/context.js
CHANGED
package/dist/index.js
CHANGED
|
@@ -20,7 +20,7 @@ import {
|
|
|
20
20
|
import {
|
|
21
21
|
buildRoutes,
|
|
22
22
|
getControllerPath
|
|
23
|
-
} from "./chunk-
|
|
23
|
+
} from "./chunk-2OSVROBK.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-IUT42U72.js";
|
|
36
36
|
import {
|
|
37
37
|
FILTER_OPERATORS,
|
|
38
38
|
buildQueryParams,
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { AppAdapter, Container } from '@forinda/kickjs-core';
|
|
2
|
+
|
|
3
|
+
interface ViewAdapterOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Template engine — pass the engine module or a render function.
|
|
6
|
+
* Supported engines: ejs, pug, handlebars, nunjucks, or any
|
|
7
|
+
* Express-compatible engine with a __express property.
|
|
8
|
+
*
|
|
9
|
+
* @example
|
|
10
|
+
* ```ts
|
|
11
|
+
* import ejs from 'ejs'
|
|
12
|
+
* new ViewAdapter({ engine: ejs, ext: 'ejs' })
|
|
13
|
+
*
|
|
14
|
+
* import pug from 'pug'
|
|
15
|
+
* new ViewAdapter({ engine: pug, ext: 'pug' })
|
|
16
|
+
* ```
|
|
17
|
+
*/
|
|
18
|
+
engine: any;
|
|
19
|
+
/** File extension for templates (e.g., 'ejs', 'pug', 'hbs') */
|
|
20
|
+
ext: string;
|
|
21
|
+
/** Directory containing template files (default: 'src/views') */
|
|
22
|
+
viewsDir?: string;
|
|
23
|
+
/** Default layout template (optional — depends on engine) */
|
|
24
|
+
layout?: string;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* View/template adapter — pluggable template engine support for KickJS.
|
|
28
|
+
*
|
|
29
|
+
* Registers an Express view engine and sets the views directory.
|
|
30
|
+
* Use `ctx.render()` in controllers to render templates.
|
|
31
|
+
*
|
|
32
|
+
* @example
|
|
33
|
+
* ```ts
|
|
34
|
+
* import ejs from 'ejs'
|
|
35
|
+
* import { ViewAdapter } from '@forinda/kickjs-http/views'
|
|
36
|
+
*
|
|
37
|
+
* bootstrap({
|
|
38
|
+
* modules,
|
|
39
|
+
* adapters: [
|
|
40
|
+
* new ViewAdapter({ engine: ejs, ext: 'ejs', viewsDir: 'src/views' }),
|
|
41
|
+
* ],
|
|
42
|
+
* })
|
|
43
|
+
*
|
|
44
|
+
* // In a controller:
|
|
45
|
+
* @Get('/dashboard')
|
|
46
|
+
* async dashboard(ctx: RequestContext) {
|
|
47
|
+
* ctx.render('dashboard', { user: currentUser, title: 'Dashboard' })
|
|
48
|
+
* }
|
|
49
|
+
* ```
|
|
50
|
+
*/
|
|
51
|
+
declare class ViewAdapter implements AppAdapter {
|
|
52
|
+
private options;
|
|
53
|
+
name: string;
|
|
54
|
+
constructor(options: ViewAdapterOptions);
|
|
55
|
+
beforeMount(app: any, _container: Container): void;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export { ViewAdapter, type ViewAdapterOptions };
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import {
|
|
2
|
+
__name
|
|
3
|
+
} from "../chunk-WCQVDF3K.js";
|
|
4
|
+
|
|
5
|
+
// src/middleware/views.ts
|
|
6
|
+
import { resolve } from "path";
|
|
7
|
+
import { Logger } from "@forinda/kickjs-core";
|
|
8
|
+
var log = Logger.for("ViewEngine");
|
|
9
|
+
var ViewAdapter = class {
|
|
10
|
+
static {
|
|
11
|
+
__name(this, "ViewAdapter");
|
|
12
|
+
}
|
|
13
|
+
options;
|
|
14
|
+
name = "ViewAdapter";
|
|
15
|
+
constructor(options) {
|
|
16
|
+
this.options = options;
|
|
17
|
+
}
|
|
18
|
+
beforeMount(app, _container) {
|
|
19
|
+
const { engine, ext, viewsDir = "src/views" } = this.options;
|
|
20
|
+
if (engine.__express) {
|
|
21
|
+
app.engine(ext, engine.__express);
|
|
22
|
+
} else if (typeof engine.renderFile === "function") {
|
|
23
|
+
app.engine(ext, (path, options, callback) => {
|
|
24
|
+
engine.renderFile(path, options, callback);
|
|
25
|
+
});
|
|
26
|
+
} else if (typeof engine === "function") {
|
|
27
|
+
app.engine(ext, engine);
|
|
28
|
+
} else {
|
|
29
|
+
log.warn(`Engine for .${ext} does not have __express or renderFile. Trying as-is.`);
|
|
30
|
+
app.engine(ext, engine);
|
|
31
|
+
}
|
|
32
|
+
app.set("view engine", ext);
|
|
33
|
+
app.set("views", resolve(viewsDir));
|
|
34
|
+
log.info(`View engine: ${ext} (${resolve(viewsDir)})`);
|
|
35
|
+
}
|
|
36
|
+
};
|
|
37
|
+
export {
|
|
38
|
+
ViewAdapter
|
|
39
|
+
};
|
|
40
|
+
//# sourceMappingURL=views.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"sources":["../../src/middleware/views.ts"],"sourcesContent":["import { resolve } from 'node:path'\nimport { Logger, type AppAdapter, type Container } from '@forinda/kickjs-core'\n\nconst log = Logger.for('ViewEngine')\n\nexport interface ViewAdapterOptions {\n /**\n * Template engine — pass the engine module or a render function.\n * Supported engines: ejs, pug, handlebars, nunjucks, or any\n * Express-compatible engine with a __express property.\n *\n * @example\n * ```ts\n * import ejs from 'ejs'\n * new ViewAdapter({ engine: ejs, ext: 'ejs' })\n *\n * import pug from 'pug'\n * new ViewAdapter({ engine: pug, ext: 'pug' })\n * ```\n */\n engine: any\n\n /** File extension for templates (e.g., 'ejs', 'pug', 'hbs') */\n ext: string\n\n /** Directory containing template files (default: 'src/views') */\n viewsDir?: string\n\n /** Default layout template (optional — depends on engine) */\n layout?: string\n}\n\n/**\n * View/template adapter — pluggable template engine support for KickJS.\n *\n * Registers an Express view engine and sets the views directory.\n * Use `ctx.render()` in controllers to render templates.\n *\n * @example\n * ```ts\n * import ejs from 'ejs'\n * import { ViewAdapter } from '@forinda/kickjs-http/views'\n *\n * bootstrap({\n * modules,\n * adapters: [\n * new ViewAdapter({ engine: ejs, ext: 'ejs', viewsDir: 'src/views' }),\n * ],\n * })\n *\n * // In a controller:\n * @Get('/dashboard')\n * async dashboard(ctx: RequestContext) {\n * ctx.render('dashboard', { user: currentUser, title: 'Dashboard' })\n * }\n * ```\n */\nexport class ViewAdapter implements AppAdapter {\n name = 'ViewAdapter'\n\n constructor(private options: ViewAdapterOptions) {}\n\n beforeMount(app: any, _container: Container): void {\n const { engine, ext, viewsDir = 'src/views' } = this.options\n\n // Register the engine\n if (engine.__express) {\n // EJS, Pug — have __express method\n app.engine(ext, engine.__express)\n } else if (typeof engine.renderFile === 'function') {\n // Engines with renderFile (nunjucks-style)\n app.engine(ext, (path: string, options: any, callback: Function) => {\n engine.renderFile(path, options, callback)\n })\n } else if (typeof engine === 'function') {\n // Custom render function: (path, options, callback) => void\n app.engine(ext, engine)\n } else {\n log.warn(`Engine for .${ext} does not have __express or renderFile. Trying as-is.`)\n app.engine(ext, engine)\n }\n\n app.set('view engine', ext)\n app.set('views', resolve(viewsDir))\n\n log.info(`View engine: ${ext} (${resolve(viewsDir)})`)\n }\n}\n"],"mappings":";;;;;AAAA,SAASA,eAAe;AACxB,SAASC,cAA+C;AAExD,IAAMC,MAAMC,OAAOC,IAAI,YAAA;AAsDhB,IAAMC,cAAN,MAAMA;EAzDb,OAyDaA;;;;EACXC,OAAO;EAEP,YAAoBC,SAA6B;SAA7BA,UAAAA;EAA8B;EAElDC,YAAYC,KAAUC,YAA6B;AACjD,UAAM,EAAEC,QAAQC,KAAKC,WAAW,YAAW,IAAK,KAAKN;AAGrD,QAAII,OAAOG,WAAW;AAEpBL,UAAIE,OAAOC,KAAKD,OAAOG,SAAS;IAClC,WAAW,OAAOH,OAAOI,eAAe,YAAY;AAElDN,UAAIE,OAAOC,KAAK,CAACI,MAAcT,SAAcU,aAAAA;AAC3CN,eAAOI,WAAWC,MAAMT,SAASU,QAAAA;MACnC,CAAA;IACF,WAAW,OAAON,WAAW,YAAY;AAEvCF,UAAIE,OAAOC,KAAKD,MAAAA;IAClB,OAAO;AACLT,UAAIgB,KAAK,eAAeN,GAAAA,uDAA0D;AAClFH,UAAIE,OAAOC,KAAKD,MAAAA;IAClB;AAEAF,QAAIU,IAAI,eAAeP,GAAAA;AACvBH,QAAIU,IAAI,SAASC,QAAQP,QAAAA,CAAAA;AAEzBX,QAAImB,KAAK,gBAAgBT,GAAAA,KAAQQ,QAAQP,QAAAA,CAAAA,GAAY;EACvD;AACF;","names":["resolve","Logger","log","Logger","for","ViewAdapter","name","options","beforeMount","app","_container","engine","ext","viewsDir","__express","renderFile","path","callback","warn","set","resolve","info"]}
|
package/dist/router-builder.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import {
|
|
2
2
|
buildRoutes,
|
|
3
3
|
getControllerPath
|
|
4
|
-
} from "./chunk-
|
|
4
|
+
} from "./chunk-2OSVROBK.js";
|
|
5
5
|
import "./chunk-LEILPDMW.js";
|
|
6
6
|
import "./chunk-RPN7UFUO.js";
|
|
7
|
-
import "./chunk-
|
|
7
|
+
import "./chunk-IUT42U72.js";
|
|
8
8
|
import "./chunk-WYY34UWG.js";
|
|
9
9
|
import "./chunk-WCQVDF3K.js";
|
|
10
10
|
export {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@forinda/kickjs-http",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Express 5 integration, router builder, RequestContext, and middleware for KickJS",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"kickjs",
|
|
@@ -89,6 +89,10 @@
|
|
|
89
89
|
"./query": {
|
|
90
90
|
"import": "./dist/query/index.js",
|
|
91
91
|
"types": "./dist/query/index.d.ts"
|
|
92
|
+
},
|
|
93
|
+
"./views": {
|
|
94
|
+
"import": "./dist/middleware/views.js",
|
|
95
|
+
"types": "./dist/middleware/views.d.ts"
|
|
92
96
|
}
|
|
93
97
|
},
|
|
94
98
|
"files": [
|
|
@@ -98,7 +102,7 @@
|
|
|
98
102
|
"cookie-parser": "^1.4.7",
|
|
99
103
|
"multer": "^2.1.1",
|
|
100
104
|
"reflect-metadata": "^0.2.2",
|
|
101
|
-
"@forinda/kickjs-core": "0.
|
|
105
|
+
"@forinda/kickjs-core": "1.0.0"
|
|
102
106
|
},
|
|
103
107
|
"peerDependencies": {
|
|
104
108
|
"express": "^5.1.0"
|
|
@@ -1 +0,0 @@
|
|
|
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 // ── Server-Sent Events ──────────────────────────────────────────────\n\n /**\n * Start an SSE (Server-Sent Events) stream.\n * Sets the correct headers and returns helpers to send events.\n *\n * @example\n * ```ts\n * @Get('/events')\n * async stream(ctx: RequestContext) {\n * const sse = ctx.sse()\n *\n * const interval = setInterval(() => {\n * sse.send({ time: new Date().toISOString() }, 'tick')\n * }, 1000)\n *\n * sse.onClose(() => clearInterval(interval))\n * }\n * ```\n */\n sse() {\n this.res.writeHead(200, {\n 'Content-Type': 'text/event-stream',\n 'Cache-Control': 'no-cache',\n Connection: 'keep-alive',\n 'X-Accel-Buffering': 'no',\n })\n this.res.flushHeaders()\n\n const closeCallbacks: Array<() => void> = []\n\n this.req.on('close', () => {\n for (const cb of closeCallbacks) cb()\n })\n\n return {\n /** Send an SSE event with optional event name and id */\n send: (data: any, event?: string, id?: string) => {\n if (id) this.res.write(`id: ${id}\\n`)\n if (event) this.res.write(`event: ${event}\\n`)\n this.res.write(`data: ${JSON.stringify(data)}\\n\\n`)\n },\n /** Send a comment (keeps connection alive) */\n comment: (text: string) => {\n this.res.write(`: ${text}\\n\\n`)\n },\n /** Register a callback when the client disconnects */\n onClose: (fn: () => void) => {\n closeCallbacks.push(fn)\n },\n /** End the SSE stream */\n close: () => {\n this.res.end()\n },\n }\n }\n}\n"],"mappings":"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","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","sse","writeHead","Connection","flushHeaders","closeCallbacks","on","cb","event","id","write","JSON","stringify","comment","text","onClose","fn","push","close"]}
|
|
File without changes
|