ts-typed-api 0.2.1 → 0.2.3
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/handler.js +1 -0
- package/dist/hono-cloudflare-workers.d.ts +6 -4
- package/dist/hono-cloudflare-workers.js +23 -2
- package/dist/hono-only.d.ts +2 -2
- package/dist/hono-only.js +2 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +2 -1
- package/dist/object-handlers.d.ts +7 -4
- package/dist/router.d.ts +5 -3
- package/dist/router.js +1 -1
- package/examples/simple/server.ts +1 -1
- package/examples/test-hono-server.ts +18 -25
- package/package.json +1 -1
- package/src/handler.ts +1 -0
- package/src/hono-cloudflare-workers.ts +58 -11
- package/src/hono-only.ts +2 -2
- package/src/index.ts +1 -1
- package/src/object-handlers.ts +30 -10
- package/src/router.ts +9 -5
- package/tests/middleware.test.ts +84 -0
- package/tests/setup.ts +173 -1
package/dist/handler.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Hono, Context } from 'hono';
|
|
1
|
+
import { Hono, Context, Env } from 'hono';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { ApiDefinitionSchema } from './definition';
|
|
4
4
|
import { TypedRequest, TypedResponse } from './router';
|
|
@@ -19,7 +19,7 @@ export declare const honoFileSchema: z.ZodObject<{
|
|
|
19
19
|
stream: z.ZodOptional<z.ZodAny>;
|
|
20
20
|
}, z.core.$strip>;
|
|
21
21
|
export type HonoFileType = z.infer<typeof honoFileSchema>;
|
|
22
|
-
export type HonoTypedContext<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteKey extends keyof TDef['endpoints'][TDomain]> = Context & {
|
|
22
|
+
export type HonoTypedContext<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteKey extends keyof TDef['endpoints'][TDomain], Ctx extends Record<string, any> = Record<string, any>> = Context & {
|
|
23
23
|
params: TypedRequest<TDef, TDomain, TRouteKey>['params'];
|
|
24
24
|
query: TypedRequest<TDef, TDomain, TRouteKey>['query'];
|
|
25
25
|
body: TypedRequest<TDef, TDomain, TRouteKey>['body'];
|
|
@@ -27,7 +27,9 @@ export type HonoTypedContext<TDef extends ApiDefinitionSchema, TDomain extends k
|
|
|
27
27
|
files?: HonoFile[] | {
|
|
28
28
|
[fieldname: string]: HonoFile[];
|
|
29
29
|
};
|
|
30
|
+
ctx?: Ctx;
|
|
30
31
|
respond: TypedResponse<TDef, TDomain, TRouteKey>['respond'];
|
|
31
32
|
};
|
|
32
|
-
export declare function registerHonoRouteHandlers<TDef extends ApiDefinitionSchema>(app: Hono, apiDefinition: TDef, routeHandlers: Array<SpecificRouteHandler<TDef>>, middlewares?: EndpointMiddleware<TDef>[]): void;
|
|
33
|
-
export declare function RegisterHonoHandlers<TDef extends ApiDefinitionSchema>(app: Hono, apiDefinition: TDef, objectHandlers: ObjectHandlers<TDef>, middlewares?: AnyMiddleware<TDef>[]): void;
|
|
33
|
+
export declare function registerHonoRouteHandlers<TDef extends ApiDefinitionSchema, TBindings extends Env = Env, TVariables extends Record<string, never> = Record<string, never>, TPath extends string = "/">(app: Hono<TBindings, TVariables, TPath>, apiDefinition: TDef, routeHandlers: Array<SpecificRouteHandler<TDef>>, middlewares?: EndpointMiddleware<TDef>[]): void;
|
|
34
|
+
export declare function RegisterHonoHandlers<TDef extends ApiDefinitionSchema, Ctx extends Record<string, any> = Record<string, any>, TBindings extends Env = Env, TVariables extends Record<string, never> = Record<string, never>, TPath extends string = "/">(app: Hono<TBindings, TVariables, TPath>, apiDefinition: TDef, objectHandlers: ObjectHandlers<TDef, Ctx>, middlewares?: AnyMiddleware<TDef>[]): void;
|
|
35
|
+
export declare function CreateTypedHonoHandlerWithContext<Ctx extends Record<string, any>>(): <TDef extends ApiDefinitionSchema>(app: Hono, apiDefinition: TDef, objectHandlers: ObjectHandlers<TDef, Ctx>, middlewares?: AnyMiddleware<TDef>[]) => void;
|
|
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.honoFileSchema = void 0;
|
|
4
4
|
exports.registerHonoRouteHandlers = registerHonoRouteHandlers;
|
|
5
5
|
exports.RegisterHonoHandlers = RegisterHonoHandlers;
|
|
6
|
+
exports.CreateTypedHonoHandlerWithContext = CreateTypedHonoHandlerWithContext;
|
|
6
7
|
const zod_1 = require("zod");
|
|
7
8
|
// Hono-compatible file schema for Workers environment
|
|
8
9
|
exports.honoFileSchema = zod_1.z.object({
|
|
@@ -268,6 +269,8 @@ function registerHonoRouteHandlers(app, apiDefinition, routeHandlers, middleware
|
|
|
268
269
|
c.params = parsedParams;
|
|
269
270
|
c.query = parsedQuery;
|
|
270
271
|
c.body = parsedBody;
|
|
272
|
+
// Get context from Hono's context system
|
|
273
|
+
c.ctx = c.get('ctx') || {};
|
|
271
274
|
// Add respond method to context
|
|
272
275
|
c.respond = (status, data) => {
|
|
273
276
|
const responseSchema = routeDefinition.responses[status];
|
|
@@ -318,6 +321,7 @@ function registerHonoRouteHandlers(app, apiDefinition, routeHandlers, middleware
|
|
|
318
321
|
body: parsedBody,
|
|
319
322
|
file: c.file,
|
|
320
323
|
files: c.files,
|
|
324
|
+
ctx: c.ctx,
|
|
321
325
|
headers: c.req.header(),
|
|
322
326
|
ip: c.req.header('CF-Connecting-IP') || '127.0.0.1',
|
|
323
327
|
method: c.req.method,
|
|
@@ -402,11 +406,23 @@ function registerHonoRouteHandlers(app, apiDefinition, routeHandlers, middleware
|
|
|
402
406
|
middlewares.forEach(middleware => {
|
|
403
407
|
const wrappedMiddleware = async (c, next) => {
|
|
404
408
|
try {
|
|
405
|
-
|
|
409
|
+
// Create Express-like req object for middleware compatibility
|
|
410
|
+
const fakeReq = {
|
|
411
|
+
headers: c.req.header(),
|
|
412
|
+
get ctx() { return c.get('ctx') || {}; },
|
|
413
|
+
set ctx(value) { c.set('ctx', value); },
|
|
414
|
+
method: c.req.method,
|
|
415
|
+
path: c.req.path,
|
|
416
|
+
originalUrl: c.req.url
|
|
417
|
+
};
|
|
418
|
+
// Create minimal res object (middleware shouldn't use it)
|
|
419
|
+
const fakeRes = {};
|
|
420
|
+
// Call Express-style middleware
|
|
421
|
+
await middleware(fakeReq, fakeRes, next, { domain: currentDomain, routeKey: currentRouteKey });
|
|
406
422
|
}
|
|
407
423
|
catch (error) {
|
|
408
424
|
console.error('Middleware error:', error);
|
|
409
|
-
|
|
425
|
+
await next();
|
|
410
426
|
}
|
|
411
427
|
};
|
|
412
428
|
middlewareWrappers.push(wrappedMiddleware);
|
|
@@ -472,3 +488,8 @@ function RegisterHonoHandlers(app, apiDefinition, objectHandlers, middlewares) {
|
|
|
472
488
|
}) || [];
|
|
473
489
|
registerHonoRouteHandlers(app, apiDefinition, handlerArray, endpointMiddlewares);
|
|
474
490
|
}
|
|
491
|
+
function CreateTypedHonoHandlerWithContext() {
|
|
492
|
+
return function (app, apiDefinition, objectHandlers, middlewares) {
|
|
493
|
+
return RegisterHonoHandlers(app, apiDefinition, objectHandlers, middlewares);
|
|
494
|
+
};
|
|
495
|
+
}
|
package/dist/hono-only.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
export { CreateApiDefinition, CreateResponses } from './definition';
|
|
2
2
|
export { z as ZodSchema } from 'zod';
|
|
3
|
-
export { EndpointMiddleware } from './object-handlers';
|
|
4
|
-
export { RegisterHonoHandlers } from './hono-cloudflare-workers';
|
|
3
|
+
export { EndpointMiddleware, EndpointMiddlewareCtx } from './object-handlers';
|
|
4
|
+
export { RegisterHonoHandlers, CreateTypedHonoHandlerWithContext } from './hono-cloudflare-workers';
|
|
5
5
|
export type { ApiDefinitionSchema, RouteSchema, UnifiedError, FileUploadConfig, HttpSuccessStatusCode, HttpClientErrorStatusCode, HttpServerErrorStatusCode, AllowedInputStatusCode, AllowedResponseStatusCode } from './definition';
|
|
6
6
|
export type { HonoFile, HonoFileType, HonoTypedContext } from './hono-cloudflare-workers';
|
package/dist/hono-only.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.RegisterHonoHandlers = exports.ZodSchema = exports.CreateResponses = exports.CreateApiDefinition = void 0;
|
|
3
|
+
exports.CreateTypedHonoHandlerWithContext = exports.RegisterHonoHandlers = exports.ZodSchema = exports.CreateResponses = exports.CreateApiDefinition = void 0;
|
|
4
4
|
// Hono-only exports - for Cloudflare Workers and other Hono environments
|
|
5
5
|
// Excludes Express dependencies like multer, busboy, etc.
|
|
6
6
|
var definition_1 = require("./definition");
|
|
@@ -11,3 +11,4 @@ Object.defineProperty(exports, "ZodSchema", { enumerable: true, get: function ()
|
|
|
11
11
|
// Hono adapter for Cloudflare Workers
|
|
12
12
|
var hono_cloudflare_workers_1 = require("./hono-cloudflare-workers");
|
|
13
13
|
Object.defineProperty(exports, "RegisterHonoHandlers", { enumerable: true, get: function () { return hono_cloudflare_workers_1.RegisterHonoHandlers; } });
|
|
14
|
+
Object.defineProperty(exports, "CreateTypedHonoHandlerWithContext", { enumerable: true, get: function () { return hono_cloudflare_workers_1.CreateTypedHonoHandlerWithContext; } });
|
package/dist/index.d.ts
CHANGED
|
@@ -5,4 +5,4 @@ export { CreateApiDefinition, CreateResponses, ApiDefinitionSchema } from './def
|
|
|
5
5
|
export { RegisterHandlers, EndpointMiddleware, UniversalEndpointMiddleware, SimpleMiddleware, EndpointInfo } from './object-handlers';
|
|
6
6
|
export { File as UploadedFile } from './router';
|
|
7
7
|
export { z as ZodSchema } from 'zod';
|
|
8
|
-
export { RegisterHonoHandlers, registerHonoRouteHandlers, HonoFile, HonoFileType, honoFileSchema, HonoTypedContext } from './hono-cloudflare-workers';
|
|
8
|
+
export { RegisterHonoHandlers, registerHonoRouteHandlers, HonoFile, HonoFileType, honoFileSchema, HonoTypedContext, CreateTypedHonoHandlerWithContext } from './hono-cloudflare-workers';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.honoFileSchema = exports.registerHonoRouteHandlers = exports.RegisterHonoHandlers = exports.ZodSchema = exports.RegisterHandlers = exports.CreateResponses = exports.CreateApiDefinition = exports.generateOpenApiSpec2 = exports.generateOpenApiSpec = exports.FetchHttpClientAdapter = exports.ApiClient = void 0;
|
|
3
|
+
exports.CreateTypedHonoHandlerWithContext = exports.honoFileSchema = exports.registerHonoRouteHandlers = exports.RegisterHonoHandlers = exports.ZodSchema = exports.RegisterHandlers = exports.CreateResponses = exports.CreateApiDefinition = exports.generateOpenApiSpec2 = exports.generateOpenApiSpec = exports.FetchHttpClientAdapter = exports.ApiClient = void 0;
|
|
4
4
|
var client_1 = require("./client");
|
|
5
5
|
Object.defineProperty(exports, "ApiClient", { enumerable: true, get: function () { return client_1.ApiClient; } });
|
|
6
6
|
Object.defineProperty(exports, "FetchHttpClientAdapter", { enumerable: true, get: function () { return client_1.FetchHttpClientAdapter; } });
|
|
@@ -20,3 +20,4 @@ var hono_cloudflare_workers_1 = require("./hono-cloudflare-workers");
|
|
|
20
20
|
Object.defineProperty(exports, "RegisterHonoHandlers", { enumerable: true, get: function () { return hono_cloudflare_workers_1.RegisterHonoHandlers; } });
|
|
21
21
|
Object.defineProperty(exports, "registerHonoRouteHandlers", { enumerable: true, get: function () { return hono_cloudflare_workers_1.registerHonoRouteHandlers; } });
|
|
22
22
|
Object.defineProperty(exports, "honoFileSchema", { enumerable: true, get: function () { return hono_cloudflare_workers_1.honoFileSchema; } });
|
|
23
|
+
Object.defineProperty(exports, "CreateTypedHonoHandlerWithContext", { enumerable: true, get: function () { return hono_cloudflare_workers_1.CreateTypedHonoHandlerWithContext; } });
|
|
@@ -13,13 +13,16 @@ export type UniversalEndpointMiddleware = (req: express.Request, res: express.Re
|
|
|
13
13
|
domain: string;
|
|
14
14
|
routeKey: string;
|
|
15
15
|
}) => void | Promise<void>;
|
|
16
|
+
export type EndpointMiddlewareCtx<Ctx extends Record<string, any> = Record<string, any>, TDef extends ApiDefinitionSchema = ApiDefinitionSchema> = ((req: express.Request & {
|
|
17
|
+
ctx?: Ctx;
|
|
18
|
+
}, res: express.Response, next: express.NextFunction, endpointInfo: EndpointInfo<TDef>) => void | Promise<void>) | ((c: any, next: any) => void | Promise<void>);
|
|
16
19
|
export type AnyMiddleware<TDef extends ApiDefinitionSchema = ApiDefinitionSchema> = EndpointMiddleware<TDef> | UniversalEndpointMiddleware | SimpleMiddleware;
|
|
17
|
-
type HandlerFunction<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteKey extends keyof TDef['endpoints'][TDomain]> = (req: TypedRequest<TDef, TDomain, TRouteKey>, res: TypedResponse<TDef, TDomain, TRouteKey>) => Promise<void> | void;
|
|
18
|
-
export type ObjectHandlers<TDef extends ApiDefinitionSchema> = {
|
|
20
|
+
type HandlerFunction<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteKey extends keyof TDef['endpoints'][TDomain], Ctx extends Record<string, any> = Record<string, any>> = (req: TypedRequest<TDef, TDomain, TRouteKey, any, any, any, any, Ctx>, res: TypedResponse<TDef, TDomain, TRouteKey>) => Promise<void> | void;
|
|
21
|
+
export type ObjectHandlers<TDef extends ApiDefinitionSchema, Ctx extends Record<string, any> = Record<string, any>> = {
|
|
19
22
|
[TDomain in keyof TDef['endpoints']]: {
|
|
20
|
-
[TRouteKey in keyof TDef['endpoints'][TDomain]]: HandlerFunction<TDef, TDomain, TRouteKey>;
|
|
23
|
+
[TRouteKey in keyof TDef['endpoints'][TDomain]]: HandlerFunction<TDef, TDomain, TRouteKey, Ctx>;
|
|
21
24
|
};
|
|
22
25
|
};
|
|
23
|
-
export declare function RegisterHandlers<TDef extends ApiDefinitionSchema>(app: express.Express, apiDefinition: TDef, objectHandlers: ObjectHandlers<TDef>, middlewares?: AnyMiddleware<TDef>[]): void;
|
|
26
|
+
export declare function RegisterHandlers<TDef extends ApiDefinitionSchema, Ctx extends Record<string, any> = Record<string, any>>(app: express.Express, apiDefinition: TDef, objectHandlers: ObjectHandlers<TDef, Ctx>, middlewares?: AnyMiddleware<TDef>[]): void;
|
|
24
27
|
export declare function makeObjectHandlerRegistrar<TDef extends ApiDefinitionSchema>(apiDefinition: TDef): (app: express.Express, objectHandlers: ObjectHandlers<TDef>, middlewares?: EndpointMiddleware<TDef>[]) => void;
|
|
25
28
|
export {};
|
package/dist/router.d.ts
CHANGED
|
@@ -3,11 +3,12 @@ import { ApiDefinitionSchema, // Changed from ApiDefinition
|
|
|
3
3
|
ApiBody, ApiParams, ApiQuery, InferDataFromUnifiedResponse } from './definition';
|
|
4
4
|
export type File = Express.Multer.File;
|
|
5
5
|
export type TypedRequest<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteKey extends keyof TDef['endpoints'][TDomain], // Using direct keyof for simplicity here
|
|
6
|
-
P extends ApiParams<TDef, TDomain, TRouteKey> = ApiParams<TDef, TDomain, TRouteKey>, ReqBody extends ApiBody<TDef, TDomain, TRouteKey> = ApiBody<TDef, TDomain, TRouteKey>, Q extends ApiQuery<TDef, TDomain, TRouteKey> = ApiQuery<TDef, TDomain, TRouteKey>, L extends Record<string, any> = Record<string, any>> = express.Request<P, any, ReqBody, Q, L> & {
|
|
6
|
+
P extends ApiParams<TDef, TDomain, TRouteKey> = ApiParams<TDef, TDomain, TRouteKey>, ReqBody extends ApiBody<TDef, TDomain, TRouteKey> = ApiBody<TDef, TDomain, TRouteKey>, Q extends ApiQuery<TDef, TDomain, TRouteKey> = ApiQuery<TDef, TDomain, TRouteKey>, L extends Record<string, any> = Record<string, any>, Ctx extends Record<string, any> = Record<string, any>> = express.Request<P, any, ReqBody, Q, L> & {
|
|
7
7
|
file?: File;
|
|
8
8
|
files?: File[] | {
|
|
9
9
|
[fieldname: string]: File[];
|
|
10
10
|
};
|
|
11
|
+
ctx?: Ctx;
|
|
11
12
|
};
|
|
12
13
|
type ResponseDataForStatus<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteName extends keyof TDef['endpoints'][TDomain], TStatus extends keyof TDef['endpoints'][TDomain][TRouteName]['responses'] & number> = InferDataFromUnifiedResponse<TDef['endpoints'][TDomain][TRouteName]['responses'][TStatus]>;
|
|
13
14
|
type RespondFunction<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteName extends keyof TDef['endpoints'][TDomain]> = <TStatusLocal extends keyof TDef['endpoints'][TDomain][TRouteName]['responses'] & number>(status: TStatusLocal, data: ResponseDataForStatus<TDef, TDomain, TRouteName, TStatusLocal>) => void;
|
|
@@ -15,10 +16,11 @@ export interface TypedResponse<TDef extends ApiDefinitionSchema, TDomain extends
|
|
|
15
16
|
respond: RespondFunction<TDef, TDomain, TRouteName>;
|
|
16
17
|
json: <B = any>(body: B) => this;
|
|
17
18
|
}
|
|
18
|
-
export declare function createRouteHandler<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteKey extends keyof TDef['endpoints'][TDomain]
|
|
19
|
+
export declare function createRouteHandler<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints'], TRouteKey extends keyof TDef['endpoints'][TDomain], // Using direct keyof for simplicity
|
|
20
|
+
Ctx extends Record<string, any> = Record<string, any>>(domain: TDomain, routeKey: TRouteKey, handler: (req: TypedRequest<TDef, TDomain, TRouteKey, any, any, any, any, Ctx>, res: TypedResponse<TDef, TDomain, TRouteKey>) => Promise<void> | void): {
|
|
19
21
|
domain: TDomain;
|
|
20
22
|
routeKey: TRouteKey;
|
|
21
|
-
handler: (req: TypedRequest<TDef, TDomain, TRouteKey>, res: TypedResponse<TDef, TDomain, TRouteKey>) => Promise<void> | void;
|
|
23
|
+
handler: (req: TypedRequest<TDef, TDomain, TRouteKey, any, any, any, any, Ctx>, res: TypedResponse<TDef, TDomain, TRouteKey>) => Promise<void> | void;
|
|
22
24
|
};
|
|
23
25
|
export declare function makeRouteHandlerCreator<TDef extends ApiDefinitionSchema>(): <TDomain extends keyof TDef["endpoints"], TRouteKey extends keyof TDef["endpoints"][TDomain]>(domain: TDomain, routeKey: TRouteKey, handler: (req: TypedRequest<TDef, TDomain, TRouteKey>, res: TypedResponse<TDef, TDomain, TRouteKey>) => Promise<void> | void) => ReturnType<typeof createRouteHandler<TDef, TDomain, TRouteKey>>;
|
|
24
26
|
export {};
|
package/dist/router.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
3
|
exports.createRouteHandler = createRouteHandler;
|
|
4
4
|
exports.makeRouteHandlerCreator = makeRouteHandlerCreator;
|
|
5
|
-
// Type-safe route handler creation function, now generic over TDef
|
|
5
|
+
// Type-safe route handler creation function, now generic over TDef and Ctx
|
|
6
6
|
// This function is called within a context where TDef is known (e.g. specific handlers file)
|
|
7
7
|
function createRouteHandler(domain, routeKey, handler) {
|
|
8
8
|
// The returned object includes enough type information (domain, routeKey)
|
|
@@ -18,7 +18,7 @@ const loggingMiddlewareTyped: EndpointMiddleware<typeof PrivateApiDefinition> =
|
|
|
18
18
|
};
|
|
19
19
|
|
|
20
20
|
// Universal auth middleware that doesn't use endpointInfo
|
|
21
|
-
const authMiddleware = async (req, res, next) => {
|
|
21
|
+
const authMiddleware: EndpointMiddleware = async (req, res, next) => {
|
|
22
22
|
// Example auth logic
|
|
23
23
|
const authHeader = req.headers.authorization;
|
|
24
24
|
if (!authHeader || !authHeader.startsWith('Bearer ')) {
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { Hono } from 'hono';
|
|
2
2
|
import http from 'http';
|
|
3
|
-
import { PublicApiDefinition as SimplePublicApiDefinition, PrivateApiDefinition as SimplePrivateApiDefinition } from './
|
|
4
|
-
import { RegisterHonoHandlers } from '
|
|
3
|
+
import { PublicApiDefinition as SimplePublicApiDefinition, PrivateApiDefinition as SimplePrivateApiDefinition } from './simple/definitions';
|
|
4
|
+
import { CreateTypedHonoHandlerWithContext, RegisterHonoHandlers } from '../src';
|
|
5
|
+
import { EndpointMiddlewareCtx } from '../src/object-handlers';
|
|
5
6
|
|
|
6
7
|
const HONO_PORT = 3004;
|
|
7
8
|
|
|
@@ -12,11 +13,18 @@ async function startHonoServer() {
|
|
|
12
13
|
|
|
13
14
|
console.log('Registering handlers...');
|
|
14
15
|
|
|
16
|
+
type Ctx = { foo: string, blah: () => string }
|
|
17
|
+
|
|
18
|
+
const loggingMiddleware: EndpointMiddlewareCtx<Ctx> = (req, res, next, endpointInfo) => {
|
|
19
|
+
console.log(`[${new Date().toISOString()}] ${req.method} ${req.path} - Endpoint: ${endpointInfo.domain}.${endpointInfo.routeKey}`);
|
|
20
|
+
next();
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const registerWithContext = CreateTypedHonoHandlerWithContext<Ctx>()
|
|
15
24
|
// Register public handlers using Hono
|
|
16
|
-
|
|
25
|
+
registerWithContext(app, SimplePublicApiDefinition, {
|
|
17
26
|
common: {
|
|
18
27
|
ping: async (req, res) => {
|
|
19
|
-
console.log('Handling ping request');
|
|
20
28
|
res.respond(200, "pong");
|
|
21
29
|
}
|
|
22
30
|
},
|
|
@@ -33,7 +41,7 @@ async function startHonoServer() {
|
|
|
33
41
|
res.respond(200, "pong");
|
|
34
42
|
}
|
|
35
43
|
}
|
|
36
|
-
});
|
|
44
|
+
}, [loggingMiddleware]);
|
|
37
45
|
|
|
38
46
|
// Register private handlers using Hono
|
|
39
47
|
RegisterHonoHandlers(app, SimplePrivateApiDefinition, {
|
|
@@ -52,19 +60,15 @@ async function startHonoServer() {
|
|
|
52
60
|
|
|
53
61
|
// Create a simple HTTP server wrapper for Hono
|
|
54
62
|
const honoServer = http.createServer(async (req: any, res: any) => {
|
|
55
|
-
console.log(`Incoming request: ${req.method} ${req.url}`);
|
|
56
|
-
|
|
57
63
|
try {
|
|
58
64
|
// Read the request body for non-GET/HEAD methods
|
|
59
65
|
let body: ReadableStream | undefined;
|
|
60
66
|
if (req.method !== 'GET' && req.method !== 'HEAD') {
|
|
61
|
-
console.log('Reading request body...');
|
|
62
67
|
const chunks: Buffer[] = [];
|
|
63
68
|
for await (const chunk of req) {
|
|
64
69
|
chunks.push(chunk);
|
|
65
70
|
}
|
|
66
71
|
const buffer = Buffer.concat(chunks);
|
|
67
|
-
console.log('Request body length:', buffer.length);
|
|
68
72
|
body = new ReadableStream({
|
|
69
73
|
start(controller) {
|
|
70
74
|
controller.enqueue(buffer);
|
|
@@ -73,18 +77,12 @@ async function startHonoServer() {
|
|
|
73
77
|
});
|
|
74
78
|
}
|
|
75
79
|
|
|
76
|
-
|
|
77
|
-
const request = new Request(`http://localhost:${HONO_PORT}${req.url}`, {
|
|
80
|
+
const response = await server(new Request(`http://localhost:${HONO_PORT}${req.url}`, {
|
|
78
81
|
method: req.method,
|
|
79
82
|
headers: req.headers,
|
|
80
|
-
body: body
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
console.log('Calling Hono app.fetch...');
|
|
84
|
-
const response = await server(request);
|
|
85
|
-
|
|
86
|
-
console.log('Response status:', response.status);
|
|
87
|
-
console.log('Response headers:', Object.fromEntries(response.headers.entries()));
|
|
83
|
+
body: body,
|
|
84
|
+
duplex: body ? 'half' : undefined
|
|
85
|
+
} as any));
|
|
88
86
|
|
|
89
87
|
res.statusCode = response.status;
|
|
90
88
|
for (const [key, value] of response.headers) {
|
|
@@ -92,10 +90,8 @@ async function startHonoServer() {
|
|
|
92
90
|
}
|
|
93
91
|
|
|
94
92
|
const responseBody = await response.text();
|
|
95
|
-
console.log('Response body:', responseBody);
|
|
96
93
|
res.end(responseBody);
|
|
97
94
|
} catch (error) {
|
|
98
|
-
console.error('Hono server error:', error);
|
|
99
95
|
res.statusCode = 500;
|
|
100
96
|
res.end('Internal Server Error');
|
|
101
97
|
}
|
|
@@ -115,10 +111,7 @@ async function startHonoServer() {
|
|
|
115
111
|
// Graceful shutdown
|
|
116
112
|
process.on('SIGINT', () => {
|
|
117
113
|
console.log('Shutting down server...');
|
|
118
|
-
|
|
119
|
-
console.log('Server closed');
|
|
120
|
-
process.exit(0);
|
|
121
|
-
});
|
|
114
|
+
process.exit(0);
|
|
122
115
|
});
|
|
123
116
|
}
|
|
124
117
|
|
package/package.json
CHANGED
package/src/handler.ts
CHANGED
|
@@ -281,6 +281,7 @@ export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
281
281
|
params: parsedParams,
|
|
282
282
|
query: parsedQuery,
|
|
283
283
|
body: parsedBody,
|
|
284
|
+
ctx: (expressReq as any).ctx,
|
|
284
285
|
headers: expressReq.headers,
|
|
285
286
|
cookies: expressReq.cookies,
|
|
286
287
|
ip: expressReq.ip,
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { Hono, Context, MiddlewareHandler } from 'hono';
|
|
1
|
+
import { Hono, Context, MiddlewareHandler, Env } from 'hono';
|
|
2
2
|
import { z } from 'zod';
|
|
3
3
|
import { ApiDefinitionSchema, RouteSchema, UnifiedError, FileUploadConfig } from './definition';
|
|
4
4
|
import { TypedRequest, TypedResponse } from './router';
|
|
@@ -29,7 +29,8 @@ export type HonoFileType = z.infer<typeof honoFileSchema>;
|
|
|
29
29
|
export type HonoTypedContext<
|
|
30
30
|
TDef extends ApiDefinitionSchema,
|
|
31
31
|
TDomain extends keyof TDef['endpoints'],
|
|
32
|
-
TRouteKey extends keyof TDef['endpoints'][TDomain]
|
|
32
|
+
TRouteKey extends keyof TDef['endpoints'][TDomain],
|
|
33
|
+
Ctx extends Record<string, any> = Record<string, any>
|
|
33
34
|
> = Context & {
|
|
34
35
|
// Add typed request properties
|
|
35
36
|
params: TypedRequest<TDef, TDomain, TRouteKey>['params'];
|
|
@@ -37,6 +38,8 @@ export type HonoTypedContext<
|
|
|
37
38
|
body: TypedRequest<TDef, TDomain, TRouteKey>['body'];
|
|
38
39
|
file?: HonoFile;
|
|
39
40
|
files?: HonoFile[] | { [fieldname: string]: HonoFile[] };
|
|
41
|
+
// Add typed context object
|
|
42
|
+
ctx?: Ctx;
|
|
40
43
|
|
|
41
44
|
// Add typed response method
|
|
42
45
|
respond: TypedResponse<TDef, TDomain, TRouteKey>['respond'];
|
|
@@ -265,8 +268,13 @@ function createHonoFileUploadMiddleware(config: FileUploadConfig): MiddlewareHan
|
|
|
265
268
|
}
|
|
266
269
|
|
|
267
270
|
// Register route handlers with Hono, now generic over TDef
|
|
268
|
-
export function registerHonoRouteHandlers<
|
|
269
|
-
|
|
271
|
+
export function registerHonoRouteHandlers<
|
|
272
|
+
TDef extends ApiDefinitionSchema,
|
|
273
|
+
TBindings extends Env = Env,
|
|
274
|
+
TVariables extends Record<string, never> = Record<string, never>,
|
|
275
|
+
TPath extends string = "/"
|
|
276
|
+
>(
|
|
277
|
+
app: Hono<TBindings, TVariables, TPath>,
|
|
270
278
|
apiDefinition: TDef,
|
|
271
279
|
routeHandlers: Array<SpecificRouteHandler<TDef>>,
|
|
272
280
|
middlewares?: EndpointMiddleware<TDef>[]
|
|
@@ -328,6 +336,9 @@ export function registerHonoRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
328
336
|
(c as any).query = parsedQuery;
|
|
329
337
|
(c as any).body = parsedBody;
|
|
330
338
|
|
|
339
|
+
// Get context from Hono's context system
|
|
340
|
+
(c as any).ctx = c.get('ctx') || {};
|
|
341
|
+
|
|
331
342
|
// Add respond method to context
|
|
332
343
|
(c as any).respond = (status: number, data: any) => {
|
|
333
344
|
const responseSchema = routeDefinition.responses[status];
|
|
@@ -387,6 +398,7 @@ export function registerHonoRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
387
398
|
body: parsedBody,
|
|
388
399
|
file: (c as any).file,
|
|
389
400
|
files: (c as any).files,
|
|
401
|
+
ctx: (c as any).ctx,
|
|
390
402
|
headers: c.req.header(),
|
|
391
403
|
ip: c.req.header('CF-Connecting-IP') || '127.0.0.1',
|
|
392
404
|
method: c.req.method,
|
|
@@ -475,10 +487,25 @@ export function registerHonoRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
475
487
|
middlewares.forEach(middleware => {
|
|
476
488
|
const wrappedMiddleware: MiddlewareHandler = async (c: any, next: any) => {
|
|
477
489
|
try {
|
|
478
|
-
|
|
490
|
+
// Create Express-like req object for middleware compatibility
|
|
491
|
+
const fakeReq = {
|
|
492
|
+
headers: c.req.header(),
|
|
493
|
+
get ctx() { return c.get('ctx') || {}; },
|
|
494
|
+
set ctx(value) { c.set('ctx', value); },
|
|
495
|
+
method: c.req.method,
|
|
496
|
+
path: c.req.path,
|
|
497
|
+
originalUrl: c.req.url
|
|
498
|
+
};
|
|
499
|
+
|
|
500
|
+
// Create minimal res object (middleware shouldn't use it)
|
|
501
|
+
const fakeRes = {};
|
|
502
|
+
|
|
503
|
+
// Call Express-style middleware
|
|
504
|
+
await middleware(fakeReq as any, fakeRes as any, next, { domain: currentDomain, routeKey: currentRouteKey });
|
|
505
|
+
|
|
479
506
|
} catch (error) {
|
|
480
507
|
console.error('Middleware error:', error);
|
|
481
|
-
|
|
508
|
+
await next();
|
|
482
509
|
}
|
|
483
510
|
};
|
|
484
511
|
middlewareWrappers.push(wrappedMiddleware);
|
|
@@ -502,8 +529,11 @@ export function registerHonoRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
502
529
|
}
|
|
503
530
|
|
|
504
531
|
// Transform object-based handlers to array format
|
|
505
|
-
function transformObjectHandlersToArray<
|
|
506
|
-
|
|
532
|
+
function transformObjectHandlersToArray<
|
|
533
|
+
TDef extends ApiDefinitionSchema,
|
|
534
|
+
Ctx extends Record<string, any> = Record<string, any>
|
|
535
|
+
>(
|
|
536
|
+
objectHandlers: ObjectHandlers<TDef, Ctx>
|
|
507
537
|
): Array<SpecificRouteHandler<TDef>> {
|
|
508
538
|
const handlerArray: Array<SpecificRouteHandler<TDef>> = [];
|
|
509
539
|
|
|
@@ -529,10 +559,16 @@ function transformObjectHandlersToArray<TDef extends ApiDefinitionSchema>(
|
|
|
529
559
|
}
|
|
530
560
|
|
|
531
561
|
// Main utility function that registers object-based handlers with Hono
|
|
532
|
-
export function RegisterHonoHandlers<
|
|
533
|
-
|
|
562
|
+
export function RegisterHonoHandlers<
|
|
563
|
+
TDef extends ApiDefinitionSchema,
|
|
564
|
+
Ctx extends Record<string, any> = Record<string, any>,
|
|
565
|
+
TBindings extends Env = Env,
|
|
566
|
+
TVariables extends Record<string, never> = Record<string, never>,
|
|
567
|
+
TPath extends string = "/"
|
|
568
|
+
>(
|
|
569
|
+
app: Hono<TBindings, TVariables, TPath>,
|
|
534
570
|
apiDefinition: TDef,
|
|
535
|
-
objectHandlers: ObjectHandlers<TDef>,
|
|
571
|
+
objectHandlers: ObjectHandlers<TDef, Ctx>,
|
|
536
572
|
middlewares?: AnyMiddleware<TDef>[]
|
|
537
573
|
): void {
|
|
538
574
|
const handlerArray = transformObjectHandlersToArray(objectHandlers);
|
|
@@ -550,3 +586,14 @@ export function RegisterHonoHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
550
586
|
|
|
551
587
|
registerHonoRouteHandlers(app, apiDefinition, handlerArray, endpointMiddlewares);
|
|
552
588
|
}
|
|
589
|
+
|
|
590
|
+
export function CreateTypedHonoHandlerWithContext<Ctx extends Record<string, any>>() {
|
|
591
|
+
return function <TDef extends ApiDefinitionSchema>(
|
|
592
|
+
app: Hono,
|
|
593
|
+
apiDefinition: TDef,
|
|
594
|
+
objectHandlers: ObjectHandlers<TDef, Ctx>,
|
|
595
|
+
middlewares?: AnyMiddleware<TDef>[]
|
|
596
|
+
) {
|
|
597
|
+
return RegisterHonoHandlers(app, apiDefinition, objectHandlers, middlewares);
|
|
598
|
+
};
|
|
599
|
+
}
|
package/src/hono-only.ts
CHANGED
|
@@ -2,10 +2,10 @@
|
|
|
2
2
|
// Excludes Express dependencies like multer, busboy, etc.
|
|
3
3
|
export { CreateApiDefinition, CreateResponses } from './definition';
|
|
4
4
|
export { z as ZodSchema } from 'zod';
|
|
5
|
-
export { EndpointMiddleware } from './object-handlers'
|
|
5
|
+
export { EndpointMiddleware, EndpointMiddlewareCtx } from './object-handlers'
|
|
6
6
|
|
|
7
7
|
// Hono adapter for Cloudflare Workers
|
|
8
|
-
export { RegisterHonoHandlers } from './hono-cloudflare-workers';
|
|
8
|
+
export { RegisterHonoHandlers, CreateTypedHonoHandlerWithContext } from './hono-cloudflare-workers';
|
|
9
9
|
|
|
10
10
|
// Re-export types that are needed for Hono development
|
|
11
11
|
export type {
|
package/src/index.ts
CHANGED
|
@@ -7,4 +7,4 @@ export { File as UploadedFile } from './router';
|
|
|
7
7
|
export { z as ZodSchema } from 'zod';
|
|
8
8
|
|
|
9
9
|
// Hono adapter for Cloudflare Workers
|
|
10
|
-
export { RegisterHonoHandlers, registerHonoRouteHandlers, HonoFile, HonoFileType, honoFileSchema, HonoTypedContext } from './hono-cloudflare-workers';
|
|
10
|
+
export { RegisterHonoHandlers, registerHonoRouteHandlers, HonoFile, HonoFileType, honoFileSchema, HonoTypedContext, CreateTypedHonoHandlerWithContext } from './hono-cloudflare-workers';
|
package/src/object-handlers.ts
CHANGED
|
@@ -36,6 +36,16 @@ export type UniversalEndpointMiddleware = (
|
|
|
36
36
|
}
|
|
37
37
|
) => void | Promise<void>;
|
|
38
38
|
|
|
39
|
+
// Unified middleware type that works for both Express and Hono with context typing
|
|
40
|
+
export type EndpointMiddlewareCtx<
|
|
41
|
+
Ctx extends Record<string, any> = Record<string, any>,
|
|
42
|
+
TDef extends ApiDefinitionSchema = ApiDefinitionSchema
|
|
43
|
+
> =
|
|
44
|
+
// Express version: (req, res, next, endpointInfo)
|
|
45
|
+
((req: express.Request & { ctx?: Ctx }, res: express.Response, next: express.NextFunction, endpointInfo: EndpointInfo<TDef>) => void | Promise<void>) |
|
|
46
|
+
// Hono version: (c, next) - context is passed via c.req.ctx -> c.ctx copying
|
|
47
|
+
((c: any, next: any) => void | Promise<void>);
|
|
48
|
+
|
|
39
49
|
// Union type that accepts endpoint-aware, universal, and simple middleware
|
|
40
50
|
export type AnyMiddleware<TDef extends ApiDefinitionSchema = ApiDefinitionSchema> =
|
|
41
51
|
| EndpointMiddleware<TDef>
|
|
@@ -46,23 +56,30 @@ export type AnyMiddleware<TDef extends ApiDefinitionSchema = ApiDefinitionSchema
|
|
|
46
56
|
type HandlerFunction<
|
|
47
57
|
TDef extends ApiDefinitionSchema,
|
|
48
58
|
TDomain extends keyof TDef['endpoints'],
|
|
49
|
-
TRouteKey extends keyof TDef['endpoints'][TDomain]
|
|
59
|
+
TRouteKey extends keyof TDef['endpoints'][TDomain],
|
|
60
|
+
Ctx extends Record<string, any> = Record<string, any>
|
|
50
61
|
> = (
|
|
51
|
-
req: TypedRequest<TDef, TDomain, TRouteKey>,
|
|
62
|
+
req: TypedRequest<TDef, TDomain, TRouteKey, any, any, any, any, Ctx>,
|
|
52
63
|
res: TypedResponse<TDef, TDomain, TRouteKey>
|
|
53
64
|
) => Promise<void> | void;
|
|
54
65
|
|
|
55
66
|
// Type for the object-based handler definition
|
|
56
67
|
// This ensures all domains and routes are required
|
|
57
|
-
export type ObjectHandlers<
|
|
58
|
-
|
|
59
|
-
|
|
68
|
+
export type ObjectHandlers<
|
|
69
|
+
TDef extends ApiDefinitionSchema,
|
|
70
|
+
Ctx extends Record<string, any> = Record<string, any>
|
|
71
|
+
> = {
|
|
72
|
+
[TDomain in keyof TDef['endpoints']]: {
|
|
73
|
+
[TRouteKey in keyof TDef['endpoints'][TDomain]]: HandlerFunction<TDef, TDomain, TRouteKey, Ctx>;
|
|
74
|
+
};
|
|
60
75
|
};
|
|
61
|
-
};
|
|
62
76
|
|
|
63
77
|
// Transform object-based handlers to array format
|
|
64
|
-
function transformObjectHandlersToArray<
|
|
65
|
-
|
|
78
|
+
function transformObjectHandlersToArray<
|
|
79
|
+
TDef extends ApiDefinitionSchema,
|
|
80
|
+
Ctx extends Record<string, any> = Record<string, any>
|
|
81
|
+
>(
|
|
82
|
+
objectHandlers: ObjectHandlers<TDef, Ctx>
|
|
66
83
|
): Array<SpecificRouteHandler<TDef>> {
|
|
67
84
|
const handlerArray: Array<SpecificRouteHandler<TDef>> = [];
|
|
68
85
|
|
|
@@ -91,10 +108,13 @@ function transformObjectHandlersToArray<TDef extends ApiDefinitionSchema>(
|
|
|
91
108
|
}
|
|
92
109
|
|
|
93
110
|
// Main utility function that registers object-based handlers
|
|
94
|
-
export function RegisterHandlers<
|
|
111
|
+
export function RegisterHandlers<
|
|
112
|
+
TDef extends ApiDefinitionSchema,
|
|
113
|
+
Ctx extends Record<string, any> = Record<string, any>
|
|
114
|
+
>(
|
|
95
115
|
app: express.Express,
|
|
96
116
|
apiDefinition: TDef,
|
|
97
|
-
objectHandlers: ObjectHandlers<TDef>,
|
|
117
|
+
objectHandlers: ObjectHandlers<TDef, Ctx>,
|
|
98
118
|
middlewares?: AnyMiddleware<TDef>[]
|
|
99
119
|
): void {
|
|
100
120
|
const handlerArray = transformObjectHandlersToArray(objectHandlers);
|
package/src/router.ts
CHANGED
|
@@ -19,11 +19,14 @@ export type TypedRequest<
|
|
|
19
19
|
P extends ApiParams<TDef, TDomain, TRouteKey> = ApiParams<TDef, TDomain, TRouteKey>,
|
|
20
20
|
ReqBody extends ApiBody<TDef, TDomain, TRouteKey> = ApiBody<TDef, TDomain, TRouteKey>,
|
|
21
21
|
Q extends ApiQuery<TDef, TDomain, TRouteKey> = ApiQuery<TDef, TDomain, TRouteKey>,
|
|
22
|
-
L extends Record<string, any> = Record<string, any
|
|
22
|
+
L extends Record<string, any> = Record<string, any>,
|
|
23
|
+
Ctx extends Record<string, any> = Record<string, any>
|
|
23
24
|
> = express.Request<P, any, ReqBody, Q, L> & {
|
|
24
25
|
// Add file upload support
|
|
25
26
|
file?: File;
|
|
26
27
|
files?: File[] | { [fieldname: string]: File[] };
|
|
28
|
+
// Add typed context object for carrying data between middlewares and handlers
|
|
29
|
+
ctx?: Ctx;
|
|
27
30
|
}
|
|
28
31
|
|
|
29
32
|
// --- Enhanced TypedResponse with res.respond, now generic over TDef ---
|
|
@@ -59,18 +62,19 @@ export interface TypedResponse<
|
|
|
59
62
|
json: <B = any>(body: B) => this; // Keep original json
|
|
60
63
|
}
|
|
61
64
|
|
|
62
|
-
// Type-safe route handler creation function, now generic over TDef
|
|
65
|
+
// Type-safe route handler creation function, now generic over TDef and Ctx
|
|
63
66
|
// This function is called within a context where TDef is known (e.g. specific handlers file)
|
|
64
67
|
export function createRouteHandler<
|
|
65
68
|
TDef extends ApiDefinitionSchema,
|
|
66
69
|
TDomain extends keyof TDef['endpoints'],
|
|
67
|
-
TRouteKey extends keyof TDef['endpoints'][TDomain] // Using direct keyof for simplicity
|
|
70
|
+
TRouteKey extends keyof TDef['endpoints'][TDomain], // Using direct keyof for simplicity
|
|
71
|
+
Ctx extends Record<string, any> = Record<string, any>
|
|
68
72
|
>(
|
|
69
73
|
domain: TDomain,
|
|
70
74
|
routeKey: TRouteKey,
|
|
71
75
|
handler: (
|
|
72
|
-
// Use the TDef generic for TypedRequest and TypedResponse
|
|
73
|
-
req: TypedRequest<TDef, TDomain, TRouteKey>,
|
|
76
|
+
// Use the TDef generic for TypedRequest and TypedResponse with Ctx
|
|
77
|
+
req: TypedRequest<TDef, TDomain, TRouteKey, any, any, any, any, Ctx>,
|
|
74
78
|
res: TypedResponse<TDef, TDomain, TRouteKey>
|
|
75
79
|
) => Promise<void> | void
|
|
76
80
|
) {
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
import { describe, test, expect } from '@jest/globals';
|
|
2
|
+
import { ApiClient } from '../src';
|
|
3
|
+
import { MiddlewareTestApiDefinition } from './setup';
|
|
4
|
+
|
|
5
|
+
// Test servers with middleware (will be defined in setup.ts)
|
|
6
|
+
export const MIDDLEWARE_EXPRESS_PORT = 3007;
|
|
7
|
+
export const MIDDLEWARE_HONO_PORT = 3008;
|
|
8
|
+
|
|
9
|
+
describe.each([
|
|
10
|
+
['Express', MIDDLEWARE_EXPRESS_PORT],
|
|
11
|
+
['Hono', MIDDLEWARE_HONO_PORT]
|
|
12
|
+
])('Middleware Tests - %s', (serverName, port) => {
|
|
13
|
+
const baseUrl = `http://localhost:${port}`;
|
|
14
|
+
const client = new ApiClient(baseUrl, MiddlewareTestApiDefinition);
|
|
15
|
+
|
|
16
|
+
describe('Basic Middleware Functionality', () => {
|
|
17
|
+
test('should execute middleware and allow request to proceed', async () => {
|
|
18
|
+
const result = await client.callApi('public', 'ping', {}, {
|
|
19
|
+
200: ({ data }) => {
|
|
20
|
+
expect(data.message).toBe('pong');
|
|
21
|
+
return data;
|
|
22
|
+
},
|
|
23
|
+
422: ({ error }) => {
|
|
24
|
+
throw new Error(`Validation error: ${JSON.stringify(error)}`);
|
|
25
|
+
}
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
expect(result.message).toBe('pong');
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe('Context Modification Middleware', () => {
|
|
33
|
+
test('should have access to context data set by middleware', async () => {
|
|
34
|
+
const result = await client.callApi('public', 'context', {}, {
|
|
35
|
+
200: ({ data }) => {
|
|
36
|
+
expect(data.message).toBe('context test');
|
|
37
|
+
expect(data.contextData).toBe('middleware-added-data');
|
|
38
|
+
return data;
|
|
39
|
+
},
|
|
40
|
+
422: ({ error }) => {
|
|
41
|
+
throw new Error(`Validation error: ${JSON.stringify(error)}`);
|
|
42
|
+
}
|
|
43
|
+
});
|
|
44
|
+
|
|
45
|
+
expect(result.contextData).toBe('middleware-added-data');
|
|
46
|
+
});
|
|
47
|
+
});
|
|
48
|
+
|
|
49
|
+
describe('Authentication Middleware', () => {
|
|
50
|
+
test('should allow access with valid auth header', async () => {
|
|
51
|
+
// This test assumes the middleware checks for an 'authorization' header
|
|
52
|
+
const result = await client.callApi('public', 'protected', { headers: { Authorization: 'Bearer valid-token' } }, {
|
|
53
|
+
200: ({ data }) => {
|
|
54
|
+
expect(data.message).toBe('protected content');
|
|
55
|
+
expect(data.user).toBe('testuser');
|
|
56
|
+
return data;
|
|
57
|
+
},
|
|
58
|
+
401: ({ data }) => {
|
|
59
|
+
throw new Error(`Authentication failed: ${data.error}`);
|
|
60
|
+
},
|
|
61
|
+
422: ({ error }) => {
|
|
62
|
+
throw new Error(`Validation error: ${JSON.stringify(error)}`);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
expect(result.user).toBe('testuser');
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test('should deny access without auth header', async () => {
|
|
70
|
+
await expect(
|
|
71
|
+
client.callApi('public', 'protected', {}, {
|
|
72
|
+
200: ({ data }) => data,
|
|
73
|
+
401: ({ data }) => {
|
|
74
|
+
expect(data.error).toBe('No authorization header');
|
|
75
|
+
throw new Error('Authentication failed as expected');
|
|
76
|
+
},
|
|
77
|
+
422: ({ error }) => {
|
|
78
|
+
throw new Error(`Validation error: ${JSON.stringify(error)}`);
|
|
79
|
+
}
|
|
80
|
+
})
|
|
81
|
+
).rejects.toThrow('Authentication failed as expected');
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
});
|
package/tests/setup.ts
CHANGED
|
@@ -5,8 +5,9 @@ import http from 'http';
|
|
|
5
5
|
import { Hono } from 'hono';
|
|
6
6
|
import { PublicApiDefinition as SimplePublicApiDefinition, PrivateApiDefinition as SimplePrivateApiDefinition } from '../examples/simple/definitions';
|
|
7
7
|
import { PublicApiDefinition as AdvancedPublicApiDefinition, PrivateApiDefinition as AdvancedPrivateApiDefinition } from '../examples/advanced/definitions';
|
|
8
|
-
import { RegisterHandlers, RegisterHonoHandlers, CreateApiDefinition, CreateResponses } from '../src';
|
|
8
|
+
import { RegisterHandlers, RegisterHonoHandlers, CreateApiDefinition, CreateResponses, CreateTypedHonoHandlerWithContext } from '../src';
|
|
9
9
|
import { z } from 'zod';
|
|
10
|
+
import { EndpointMiddlewareCtx } from '../src/object-handlers';
|
|
10
11
|
|
|
11
12
|
// Shared handler definitions for simple API
|
|
12
13
|
const simplePublicHandlers = {
|
|
@@ -202,6 +203,36 @@ const fileUploadHandlers = {
|
|
|
202
203
|
}
|
|
203
204
|
};
|
|
204
205
|
|
|
206
|
+
export const MiddlewareTestApiDefinition = CreateApiDefinition({
|
|
207
|
+
prefix: '/api/v1',
|
|
208
|
+
endpoints: {
|
|
209
|
+
public: {
|
|
210
|
+
ping: {
|
|
211
|
+
method: 'GET' as const,
|
|
212
|
+
path: '/ping',
|
|
213
|
+
responses: CreateResponses({
|
|
214
|
+
200: z.object({ message: z.string() })
|
|
215
|
+
})
|
|
216
|
+
},
|
|
217
|
+
protected: {
|
|
218
|
+
method: 'GET' as const,
|
|
219
|
+
path: '/protected',
|
|
220
|
+
responses: CreateResponses({
|
|
221
|
+
200: z.object({ message: z.string(), user: z.string() }),
|
|
222
|
+
401: z.object({ error: z.string() })
|
|
223
|
+
})
|
|
224
|
+
},
|
|
225
|
+
context: {
|
|
226
|
+
method: 'GET' as const,
|
|
227
|
+
path: '/context',
|
|
228
|
+
responses: CreateResponses({
|
|
229
|
+
200: z.object({ message: z.string(), contextData: z.string() })
|
|
230
|
+
})
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
});
|
|
235
|
+
|
|
205
236
|
// Global test server instances
|
|
206
237
|
export let simpleServer: Server;
|
|
207
238
|
export let advancedServer: Server;
|
|
@@ -209,6 +240,8 @@ export let fileUploadServer: Server;
|
|
|
209
240
|
export let honoServer: Server;
|
|
210
241
|
export let advancedHonoServer: Server;
|
|
211
242
|
export let fileUploadHonoServer: Server;
|
|
243
|
+
export let middlewareExpressServer: Server;
|
|
244
|
+
export let middlewareHonoServer: Server;
|
|
212
245
|
|
|
213
246
|
export const SIMPLE_PORT = 3001;
|
|
214
247
|
export const ADVANCED_PORT = 3002;
|
|
@@ -216,6 +249,8 @@ export const FILE_UPLOAD_PORT = 3003;
|
|
|
216
249
|
export const HONO_PORT = 3004;
|
|
217
250
|
export const ADVANCED_HONO_PORT = 3005;
|
|
218
251
|
export const FILE_UPLOAD_HONO_PORT = 3006;
|
|
252
|
+
export const MIDDLEWARE_EXPRESS_PORT = 3007;
|
|
253
|
+
export const MIDDLEWARE_HONO_PORT = 3008;
|
|
219
254
|
|
|
220
255
|
// Helper function to create HTTP server wrapper for Hono apps
|
|
221
256
|
function createHonoHttpServer(server: any, port: number, errorPrefix: string): Server {
|
|
@@ -267,6 +302,8 @@ beforeAll(async () => {
|
|
|
267
302
|
await startHonoServer();
|
|
268
303
|
await startAdvancedHonoServer();
|
|
269
304
|
await startFileUploadHonoServer();
|
|
305
|
+
await startMiddlewareExpressServer();
|
|
306
|
+
await startMiddlewareHonoServer();
|
|
270
307
|
});
|
|
271
308
|
|
|
272
309
|
afterAll(async () => {
|
|
@@ -289,6 +326,12 @@ afterAll(async () => {
|
|
|
289
326
|
if (fileUploadHonoServer) {
|
|
290
327
|
fileUploadHonoServer.close();
|
|
291
328
|
}
|
|
329
|
+
if (middlewareExpressServer) {
|
|
330
|
+
middlewareExpressServer.close();
|
|
331
|
+
}
|
|
332
|
+
if (middlewareHonoServer) {
|
|
333
|
+
middlewareHonoServer.close();
|
|
334
|
+
}
|
|
292
335
|
});
|
|
293
336
|
|
|
294
337
|
async function startSimpleServer(): Promise<void> {
|
|
@@ -399,3 +442,132 @@ async function startHonoServer(): Promise<void> {
|
|
|
399
442
|
});
|
|
400
443
|
});
|
|
401
444
|
}
|
|
445
|
+
|
|
446
|
+
type Ctx = { user: string }
|
|
447
|
+
|
|
448
|
+
// Middleware test servers
|
|
449
|
+
async function startMiddlewareExpressServer(): Promise<void> {
|
|
450
|
+
return new Promise((resolve) => {
|
|
451
|
+
const app = express();
|
|
452
|
+
app.use(express.json());
|
|
453
|
+
|
|
454
|
+
// Define middleware functions
|
|
455
|
+
const loggingMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction, endpointInfo: any) => {
|
|
456
|
+
console.log(`[Test] ${req.method} ${req.path} - Domain: ${endpointInfo.domain}, Route: ${endpointInfo.routeKey}`);
|
|
457
|
+
next();
|
|
458
|
+
};
|
|
459
|
+
|
|
460
|
+
const contextMiddleware = (req: express.Request, res: express.Response, next: express.NextFunction) => {
|
|
461
|
+
(req as any).ctx = { middlewareData: "middleware-added-data" };
|
|
462
|
+
next();
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
const authMiddleware: EndpointMiddlewareCtx<Ctx> = (req, res, next) => {
|
|
466
|
+
const authHeader = req.headers.authorization;
|
|
467
|
+
if (authHeader === 'Bearer valid-token') {
|
|
468
|
+
req.ctx = { user: 'testuser' };
|
|
469
|
+
}
|
|
470
|
+
next();
|
|
471
|
+
};
|
|
472
|
+
|
|
473
|
+
// Register handlers with middleware
|
|
474
|
+
RegisterHandlers(app, MiddlewareTestApiDefinition, {
|
|
475
|
+
public: {
|
|
476
|
+
ping: async (req: any, res: any) => {
|
|
477
|
+
res.respond(200, { message: "pong" });
|
|
478
|
+
},
|
|
479
|
+
protected: async (req, res) => {
|
|
480
|
+
// Check if user is authenticated via context
|
|
481
|
+
if (req.ctx && req.ctx.user) {
|
|
482
|
+
res.respond(200, {
|
|
483
|
+
message: "protected content",
|
|
484
|
+
user: req.ctx.user
|
|
485
|
+
});
|
|
486
|
+
} else {
|
|
487
|
+
res.respond(401, { error: "No authorization header" });
|
|
488
|
+
}
|
|
489
|
+
},
|
|
490
|
+
context: async (req: any, res: any) => {
|
|
491
|
+
res.respond(200, {
|
|
492
|
+
message: "context test",
|
|
493
|
+
contextData: req.ctx?.middlewareData || "default"
|
|
494
|
+
});
|
|
495
|
+
}
|
|
496
|
+
}
|
|
497
|
+
}, [
|
|
498
|
+
loggingMiddleware,
|
|
499
|
+
contextMiddleware,
|
|
500
|
+
authMiddleware
|
|
501
|
+
]);
|
|
502
|
+
|
|
503
|
+
middlewareExpressServer = app.listen(MIDDLEWARE_EXPRESS_PORT, () => {
|
|
504
|
+
resolve();
|
|
505
|
+
});
|
|
506
|
+
});
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
async function startMiddlewareHonoServer(): Promise<void> {
|
|
510
|
+
return new Promise((resolve) => {
|
|
511
|
+
const app = new Hono();
|
|
512
|
+
|
|
513
|
+
// Define middleware functions
|
|
514
|
+
const loggingMiddleware = async (req: any, res: any, next: any, endpointInfo: any) => {
|
|
515
|
+
console.log(`[Test Hono] ${req.method} ${req.path} - Domain: ${endpointInfo.domain}, Route: ${endpointInfo.routeKey}`);
|
|
516
|
+
await next();
|
|
517
|
+
};
|
|
518
|
+
|
|
519
|
+
const contextMiddleware = async (req: any, res: any, next: any) => {
|
|
520
|
+
req.ctx = { ...req.ctx, middlewareData: "middleware-added-data" };
|
|
521
|
+
await next();
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
const authMiddleware: EndpointMiddlewareCtx<Ctx> = async (req, res, next) => {
|
|
525
|
+
const authHeader = req.headers?.authorization;
|
|
526
|
+
if (authHeader === 'Bearer valid-token') {
|
|
527
|
+
req.ctx = { ...req.ctx, user: 'testuser' };
|
|
528
|
+
}
|
|
529
|
+
await next();
|
|
530
|
+
};
|
|
531
|
+
|
|
532
|
+
const hdnl = CreateTypedHonoHandlerWithContext<Ctx>()
|
|
533
|
+
// Register handlers with middleware
|
|
534
|
+
hdnl(app, MiddlewareTestApiDefinition, {
|
|
535
|
+
public: {
|
|
536
|
+
ping: async (req: any, res: any) => {
|
|
537
|
+
res.respond(200, { message: "pong" });
|
|
538
|
+
},
|
|
539
|
+
protected: async (req, res) => {
|
|
540
|
+
// Check if user is authenticated via context
|
|
541
|
+
if (req.ctx && req.ctx.user) {
|
|
542
|
+
res.respond(200, {
|
|
543
|
+
message: "protected content",
|
|
544
|
+
user: req.ctx.user
|
|
545
|
+
});
|
|
546
|
+
} else {
|
|
547
|
+
res.respond(401, { error: "No authorization header" });
|
|
548
|
+
}
|
|
549
|
+
},
|
|
550
|
+
context: async (req: any, res: any) => {
|
|
551
|
+
res.respond(200, {
|
|
552
|
+
message: "context test",
|
|
553
|
+
contextData: req.ctx?.middlewareData || "default"
|
|
554
|
+
});
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
}, [
|
|
558
|
+
loggingMiddleware,
|
|
559
|
+
contextMiddleware,
|
|
560
|
+
authMiddleware
|
|
561
|
+
]);
|
|
562
|
+
|
|
563
|
+
// Create HTTP server from Hono app
|
|
564
|
+
const server = app.fetch;
|
|
565
|
+
|
|
566
|
+
// Create a simple HTTP server wrapper for Hono
|
|
567
|
+
middlewareHonoServer = createHonoHttpServer(server, MIDDLEWARE_HONO_PORT, 'Middleware Hono server');
|
|
568
|
+
|
|
569
|
+
middlewareHonoServer.listen(MIDDLEWARE_HONO_PORT, () => {
|
|
570
|
+
resolve();
|
|
571
|
+
});
|
|
572
|
+
});
|
|
573
|
+
}
|