ts-typed-api 0.2.17 → 0.2.18
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 +32 -3
- package/dist/hono-cloudflare-workers.js +30 -1
- package/dist/index.d.ts +1 -1
- package/dist/object-handlers.d.ts +1 -0
- package/package.json +1 -1
- package/src/handler.ts +33 -3
- package/src/hono-cloudflare-workers.ts +30 -1
- package/src/index.ts +1 -1
- package/src/object-handlers.ts +1 -0
- package/tests/middleware.test.ts +77 -0
- package/tests/setup.ts +9 -0
package/dist/handler.js
CHANGED
|
@@ -56,8 +56,19 @@ function preprocessQueryParams(query, querySchema) {
|
|
|
56
56
|
return processedQuery;
|
|
57
57
|
}
|
|
58
58
|
// Helper function to create respond method for middleware compatibility
|
|
59
|
-
function createRespondFunction(routeDefinition, responseSetter) {
|
|
59
|
+
function createRespondFunction(routeDefinition, responseSetter, middlewareRes) {
|
|
60
60
|
return (status, data) => {
|
|
61
|
+
// Call any registered response callbacks
|
|
62
|
+
if (middlewareRes && middlewareRes._responseCallbacks) {
|
|
63
|
+
middlewareRes._responseCallbacks.forEach((callback) => {
|
|
64
|
+
try {
|
|
65
|
+
callback(status, data);
|
|
66
|
+
}
|
|
67
|
+
catch (error) {
|
|
68
|
+
console.error('Error in response callback:', error);
|
|
69
|
+
}
|
|
70
|
+
});
|
|
71
|
+
}
|
|
61
72
|
const responseSchema = routeDefinition.responses[status];
|
|
62
73
|
if (!responseSchema) {
|
|
63
74
|
console.error(`No response schema defined for status ${status}`);
|
|
@@ -305,6 +316,17 @@ middlewares) {
|
|
|
305
316
|
// Augment expressRes with the .respond and .setHeader methods, using TDef
|
|
306
317
|
const typedExpressRes = expressRes;
|
|
307
318
|
typedExpressRes.respond = (status, dataForResponse) => {
|
|
319
|
+
// Call any registered response callbacks from middleware
|
|
320
|
+
if (expressRes._responseCallbacks) {
|
|
321
|
+
expressRes._responseCallbacks.forEach((callback) => {
|
|
322
|
+
try {
|
|
323
|
+
callback(status, dataForResponse);
|
|
324
|
+
}
|
|
325
|
+
catch (error) {
|
|
326
|
+
console.error('Error in response callback:', error);
|
|
327
|
+
}
|
|
328
|
+
});
|
|
329
|
+
}
|
|
308
330
|
// Use the passed apiDefinition object
|
|
309
331
|
const routeSchemaForHandler = apiDefinition.endpoints[currentDomain][currentRouteKey];
|
|
310
332
|
const responseSchemaForStatus = routeSchemaForHandler.responses[status];
|
|
@@ -422,11 +444,18 @@ middlewares) {
|
|
|
422
444
|
middlewares.forEach(middleware => {
|
|
423
445
|
const wrappedMiddleware = async (req, res, next) => {
|
|
424
446
|
try {
|
|
425
|
-
// Add respond
|
|
447
|
+
// Add respond and onFinish methods to res for middleware compatibility
|
|
426
448
|
const middlewareRes = res;
|
|
427
449
|
middlewareRes.respond = createRespondFunction(routeDefinition, (status, data) => {
|
|
428
450
|
res.status(status).json(data);
|
|
429
|
-
});
|
|
451
|
+
}, middlewareRes);
|
|
452
|
+
middlewareRes.onResponse = (callback) => {
|
|
453
|
+
// Store callback on the underlying express response so it's accessible from TypedResponse
|
|
454
|
+
if (!res._responseCallbacks) {
|
|
455
|
+
res._responseCallbacks = [];
|
|
456
|
+
}
|
|
457
|
+
res._responseCallbacks.push(callback);
|
|
458
|
+
};
|
|
430
459
|
await middleware(req, middlewareRes, next, { domain: currentDomain, routeKey: currentRouteKey });
|
|
431
460
|
}
|
|
432
461
|
catch (error) {
|
|
@@ -273,6 +273,17 @@ function registerHonoRouteHandlers(app, apiDefinition, routeHandlers, middleware
|
|
|
273
273
|
c.ctx = c.get('ctx') || {};
|
|
274
274
|
// Add respond method to context
|
|
275
275
|
c.respond = (status, data) => {
|
|
276
|
+
// Call any registered response callbacks from middleware
|
|
277
|
+
if (c._responseCallbacks) {
|
|
278
|
+
c._responseCallbacks.forEach((callback) => {
|
|
279
|
+
try {
|
|
280
|
+
callback(status, data);
|
|
281
|
+
}
|
|
282
|
+
catch (error) {
|
|
283
|
+
console.error('Error in response callback:', error);
|
|
284
|
+
}
|
|
285
|
+
});
|
|
286
|
+
}
|
|
276
287
|
const responseSchema = routeDefinition.responses[status];
|
|
277
288
|
if (!responseSchema) {
|
|
278
289
|
console.error(`No response schema defined for status ${status} in route ${String(currentDomain)}/${String(currentRouteKey)}`);
|
|
@@ -426,9 +437,20 @@ function registerHonoRouteHandlers(app, apiDefinition, routeHandlers, middleware
|
|
|
426
437
|
path: c.req.path,
|
|
427
438
|
originalUrl: c.req.url
|
|
428
439
|
};
|
|
429
|
-
// Create minimal res object with respond
|
|
440
|
+
// Create minimal res object with respond and onResponse methods for middleware compatibility
|
|
430
441
|
const fakeRes = {
|
|
431
442
|
respond: (status, data) => {
|
|
443
|
+
// Call any registered response callbacks
|
|
444
|
+
if (c._responseCallbacks) {
|
|
445
|
+
c._responseCallbacks.forEach((callback) => {
|
|
446
|
+
try {
|
|
447
|
+
callback(status, data);
|
|
448
|
+
}
|
|
449
|
+
catch (error) {
|
|
450
|
+
console.error('Error in response callback:', error);
|
|
451
|
+
}
|
|
452
|
+
});
|
|
453
|
+
}
|
|
432
454
|
const responseSchema = routeDefinition.responses[status];
|
|
433
455
|
if (!responseSchema) {
|
|
434
456
|
console.error(`No response schema defined for status ${status}`);
|
|
@@ -482,6 +504,13 @@ function registerHonoRouteHandlers(app, apiDefinition, routeHandlers, middleware
|
|
|
482
504
|
},
|
|
483
505
|
end: () => {
|
|
484
506
|
// Perhaps do nothing or set response
|
|
507
|
+
},
|
|
508
|
+
onResponse: (callback) => {
|
|
509
|
+
// Store callback to be called when respond() is invoked
|
|
510
|
+
if (!c._responseCallbacks) {
|
|
511
|
+
c._responseCallbacks = [];
|
|
512
|
+
}
|
|
513
|
+
c._responseCallbacks.push(callback);
|
|
485
514
|
}
|
|
486
515
|
};
|
|
487
516
|
// Call Express-style middleware
|
package/dist/index.d.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { ApiClient, FetchHttpClientAdapter } from './client';
|
|
|
2
2
|
export { generateOpenApiSpec } from './openapi';
|
|
3
3
|
export { generateOpenApiSpec as generateOpenApiSpec2 } from './openapi-self';
|
|
4
4
|
export { CreateApiDefinition, CreateResponses, ApiDefinitionSchema } from './definition';
|
|
5
|
-
export { RegisterHandlers, EndpointMiddleware, UniversalEndpointMiddleware, SimpleMiddleware, EndpointInfo } from './object-handlers';
|
|
5
|
+
export { RegisterHandlers, EndpointMiddleware, UniversalEndpointMiddleware, SimpleMiddleware, EndpointInfo, MiddlewareResponse } from './object-handlers';
|
|
6
6
|
export { File as UploadedFile } from './router';
|
|
7
7
|
export { z as ZodSchema } from 'zod';
|
|
8
8
|
export { RegisterHonoHandlers, registerHonoRouteHandlers, HonoFile, HonoFileType, honoFileSchema, HonoTypedContext, CreateTypedHonoHandlerWithContext } from './hono-cloudflare-workers';
|
|
@@ -19,6 +19,7 @@ export interface MiddlewareResponse {
|
|
|
19
19
|
json(data: any): void;
|
|
20
20
|
setHeader(name: string, value: string): void;
|
|
21
21
|
end(): void;
|
|
22
|
+
onResponse(callback: (status: number, data: any) => void): void;
|
|
22
23
|
}
|
|
23
24
|
export type EndpointMiddlewareCtx<Ctx extends Record<string, any> = Record<string, any>, TDef extends ApiDefinitionSchema = ApiDefinitionSchema> = ((req: express.Request & {
|
|
24
25
|
ctx?: Ctx;
|
package/package.json
CHANGED
package/src/handler.ts
CHANGED
|
@@ -83,9 +83,21 @@ function preprocessQueryParams(query: any, querySchema?: z.ZodTypeAny): any {
|
|
|
83
83
|
// Helper function to create respond method for middleware compatibility
|
|
84
84
|
function createRespondFunction(
|
|
85
85
|
routeDefinition: RouteSchema,
|
|
86
|
-
responseSetter: (status: number, data: any) => void
|
|
86
|
+
responseSetter: (status: number, data: any) => void,
|
|
87
|
+
middlewareRes?: any
|
|
87
88
|
) {
|
|
88
89
|
return (status: number, data: any) => {
|
|
90
|
+
// Call any registered response callbacks
|
|
91
|
+
if (middlewareRes && middlewareRes._responseCallbacks) {
|
|
92
|
+
middlewareRes._responseCallbacks.forEach((callback: (status: number, data: any) => void) => {
|
|
93
|
+
try {
|
|
94
|
+
callback(status, data);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
console.error('Error in response callback:', error);
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
|
|
89
101
|
const responseSchema = routeDefinition.responses[status];
|
|
90
102
|
|
|
91
103
|
if (!responseSchema) {
|
|
@@ -359,6 +371,17 @@ export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
359
371
|
const typedExpressRes = expressRes as TypedResponse<TDef, typeof currentDomain, typeof currentRouteKey>;
|
|
360
372
|
|
|
361
373
|
typedExpressRes.respond = (status, dataForResponse) => {
|
|
374
|
+
// Call any registered response callbacks from middleware
|
|
375
|
+
if ((expressRes as any)._responseCallbacks) {
|
|
376
|
+
(expressRes as any)._responseCallbacks.forEach((callback: (status: number, data: any) => void) => {
|
|
377
|
+
try {
|
|
378
|
+
callback(status, dataForResponse);
|
|
379
|
+
} catch (error) {
|
|
380
|
+
console.error('Error in response callback:', error);
|
|
381
|
+
}
|
|
382
|
+
});
|
|
383
|
+
}
|
|
384
|
+
|
|
362
385
|
// Use the passed apiDefinition object
|
|
363
386
|
const routeSchemaForHandler = apiDefinition.endpoints[currentDomain][currentRouteKey] as RouteSchema;
|
|
364
387
|
const responseSchemaForStatus = routeSchemaForHandler.responses[status as number];
|
|
@@ -487,11 +510,18 @@ export function registerRouteHandlers<TDef extends ApiDefinitionSchema>(
|
|
|
487
510
|
middlewares.forEach(middleware => {
|
|
488
511
|
const wrappedMiddleware: express.RequestHandler = async (req, res, next) => {
|
|
489
512
|
try {
|
|
490
|
-
// Add respond
|
|
513
|
+
// Add respond and onFinish methods to res for middleware compatibility
|
|
491
514
|
const middlewareRes = res as any;
|
|
492
515
|
middlewareRes.respond = createRespondFunction(routeDefinition, (status, data) => {
|
|
493
516
|
res.status(status).json(data);
|
|
494
|
-
});
|
|
517
|
+
}, middlewareRes);
|
|
518
|
+
middlewareRes.onResponse = (callback: (status: number, data: any) => void) => {
|
|
519
|
+
// Store callback on the underlying express response so it's accessible from TypedResponse
|
|
520
|
+
if (!(res as any)._responseCallbacks) {
|
|
521
|
+
(res as any)._responseCallbacks = [];
|
|
522
|
+
}
|
|
523
|
+
(res as any)._responseCallbacks.push(callback);
|
|
524
|
+
};
|
|
495
525
|
await middleware(req, middlewareRes as MiddlewareResponse, next, { domain: currentDomain, routeKey: currentRouteKey } as any);
|
|
496
526
|
} catch (error) {
|
|
497
527
|
next(error);
|
|
@@ -341,6 +341,17 @@ export function registerHonoRouteHandlers<
|
|
|
341
341
|
|
|
342
342
|
// Add respond method to context
|
|
343
343
|
(c as any).respond = (status: number, data: any) => {
|
|
344
|
+
// Call any registered response callbacks from middleware
|
|
345
|
+
if ((c as any)._responseCallbacks) {
|
|
346
|
+
(c as any)._responseCallbacks.forEach((callback: (status: number, data: any) => void) => {
|
|
347
|
+
try {
|
|
348
|
+
callback(status, data);
|
|
349
|
+
} catch (error) {
|
|
350
|
+
console.error('Error in response callback:', error);
|
|
351
|
+
}
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
|
|
344
355
|
const responseSchema = routeDefinition.responses[status];
|
|
345
356
|
|
|
346
357
|
if (!responseSchema) {
|
|
@@ -508,9 +519,20 @@ export function registerHonoRouteHandlers<
|
|
|
508
519
|
originalUrl: c.req.url
|
|
509
520
|
};
|
|
510
521
|
|
|
511
|
-
// Create minimal res object with respond
|
|
522
|
+
// Create minimal res object with respond and onResponse methods for middleware compatibility
|
|
512
523
|
const fakeRes = {
|
|
513
524
|
respond: (status: number, data: any) => {
|
|
525
|
+
// Call any registered response callbacks
|
|
526
|
+
if ((c as any)._responseCallbacks) {
|
|
527
|
+
(c as any)._responseCallbacks.forEach((callback: (status: number, data: any) => void) => {
|
|
528
|
+
try {
|
|
529
|
+
callback(status, data);
|
|
530
|
+
} catch (error) {
|
|
531
|
+
console.error('Error in response callback:', error);
|
|
532
|
+
}
|
|
533
|
+
});
|
|
534
|
+
}
|
|
535
|
+
|
|
514
536
|
const responseSchema = routeDefinition.responses[status];
|
|
515
537
|
|
|
516
538
|
if (!responseSchema) {
|
|
@@ -571,6 +593,13 @@ export function registerHonoRouteHandlers<
|
|
|
571
593
|
},
|
|
572
594
|
end: () => {
|
|
573
595
|
// Perhaps do nothing or set response
|
|
596
|
+
},
|
|
597
|
+
onResponse: (callback: (status: number, data: any) => void) => {
|
|
598
|
+
// Store callback to be called when respond() is invoked
|
|
599
|
+
if (!(c as any)._responseCallbacks) {
|
|
600
|
+
(c as any)._responseCallbacks = [];
|
|
601
|
+
}
|
|
602
|
+
(c as any)._responseCallbacks.push(callback);
|
|
574
603
|
}
|
|
575
604
|
};
|
|
576
605
|
|
package/src/index.ts
CHANGED
|
@@ -2,7 +2,7 @@ export { ApiClient, FetchHttpClientAdapter } from './client';
|
|
|
2
2
|
export { generateOpenApiSpec } from './openapi'
|
|
3
3
|
export { generateOpenApiSpec as generateOpenApiSpec2 } from './openapi-self'
|
|
4
4
|
export { CreateApiDefinition, CreateResponses, ApiDefinitionSchema } from './definition';
|
|
5
|
-
export { RegisterHandlers, EndpointMiddleware, UniversalEndpointMiddleware, SimpleMiddleware, EndpointInfo } from './object-handlers';
|
|
5
|
+
export { RegisterHandlers, EndpointMiddleware, UniversalEndpointMiddleware, SimpleMiddleware, EndpointInfo, MiddlewareResponse } from './object-handlers';
|
|
6
6
|
export { File as UploadedFile } from './router';
|
|
7
7
|
export { z as ZodSchema } from 'zod';
|
|
8
8
|
|
package/src/object-handlers.ts
CHANGED
|
@@ -43,6 +43,7 @@ export interface MiddlewareResponse {
|
|
|
43
43
|
json(data: any): void;
|
|
44
44
|
setHeader(name: string, value: string): void;
|
|
45
45
|
end(): void;
|
|
46
|
+
onResponse(callback: (status: number, data: any) => void): void;
|
|
46
47
|
}
|
|
47
48
|
|
|
48
49
|
// Unified middleware type that works for both Express and Hono with context typing
|
package/tests/middleware.test.ts
CHANGED
|
@@ -105,4 +105,81 @@ describe.each([
|
|
|
105
105
|
).rejects.toThrow('Forbidden as expected');
|
|
106
106
|
});
|
|
107
107
|
});
|
|
108
|
+
|
|
109
|
+
describe('Response Logging Middleware', () => {
|
|
110
|
+
let consoleSpy: jest.SpyInstance;
|
|
111
|
+
|
|
112
|
+
beforeEach(() => {
|
|
113
|
+
consoleSpy = jest.spyOn(console, 'log').mockImplementation();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
afterEach(() => {
|
|
117
|
+
consoleSpy.mockRestore();
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
test('should log response status without breaking functionality', async () => {
|
|
121
|
+
// The response logging middleware should not interfere with normal operation
|
|
122
|
+
const result = await client.callApi('public', 'ping', {}, {
|
|
123
|
+
200: ({ data }) => {
|
|
124
|
+
expect(data.message).toBe('pong');
|
|
125
|
+
return data;
|
|
126
|
+
},
|
|
127
|
+
422: ({ error }) => {
|
|
128
|
+
throw new Error(`Validation error: ${JSON.stringify(error)}`);
|
|
129
|
+
}
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
expect(result.message).toBe('pong');
|
|
133
|
+
|
|
134
|
+
// Assert that console.log was called with the expected message
|
|
135
|
+
expect(consoleSpy).toHaveBeenCalledWith('[TIMING] public.ping responded with 200');
|
|
136
|
+
expect(consoleSpy).toHaveBeenCalledWith('[Test] GET /api/v1/ping - Domain: public, Route: ping');
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
test('should log response status for protected routes', async () => {
|
|
140
|
+
const result = await client.callApi('public', 'protected', { headers: { Authorization: 'Bearer valid-token' } }, {
|
|
141
|
+
200: ({ data }) => {
|
|
142
|
+
expect(data.message).toBe('protected content');
|
|
143
|
+
expect(data.user).toBe('testuser');
|
|
144
|
+
return data;
|
|
145
|
+
},
|
|
146
|
+
401: ({ data }) => {
|
|
147
|
+
throw new Error(`Authentication failed: ${data.error}`);
|
|
148
|
+
},
|
|
149
|
+
403: ({ data }) => {
|
|
150
|
+
throw new Error(`Forbidden: ${data.error}`);
|
|
151
|
+
},
|
|
152
|
+
422: ({ error }) => {
|
|
153
|
+
throw new Error(`Validation error: ${JSON.stringify(error)}`);
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
|
|
157
|
+
expect(result.user).toBe('testuser');
|
|
158
|
+
|
|
159
|
+
// Assert that console.log was called with the expected messages
|
|
160
|
+
expect(consoleSpy).toHaveBeenCalledWith('[TIMING] public.protected responded with 200');
|
|
161
|
+
expect(consoleSpy).toHaveBeenCalledWith('[Test] GET /api/v1/protected - Domain: public, Route: protected');
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
test('should log error status codes for auth failures', async () => {
|
|
165
|
+
await expect(
|
|
166
|
+
client.callApi('public', 'protected', {}, {
|
|
167
|
+
200: ({ data }) => data,
|
|
168
|
+
401: ({ data }) => {
|
|
169
|
+
expect(data.error).toBe('No authorization header');
|
|
170
|
+
throw new Error('Authentication failed as expected');
|
|
171
|
+
},
|
|
172
|
+
403: ({ data }) => {
|
|
173
|
+
throw new Error(`Unexpected forbidden: ${data.error}`);
|
|
174
|
+
},
|
|
175
|
+
422: ({ error }) => {
|
|
176
|
+
throw new Error(`Validation error: ${JSON.stringify(error)}`);
|
|
177
|
+
}
|
|
178
|
+
})
|
|
179
|
+
).rejects.toThrow('Authentication failed as expected');
|
|
180
|
+
|
|
181
|
+
// Assert that console.log was called with the error status
|
|
182
|
+
expect(consoleSpy).toHaveBeenCalledWith('[TIMING] public.protected responded with 401');
|
|
183
|
+
});
|
|
184
|
+
});
|
|
108
185
|
});
|
package/tests/setup.ts
CHANGED
|
@@ -481,6 +481,14 @@ const middlewareTestHandlers = {
|
|
|
481
481
|
// Generic middleware setup function
|
|
482
482
|
function setupMiddlewareApp(app: any, isHono: boolean) {
|
|
483
483
|
// Define middleware functions
|
|
484
|
+
const timingMiddleware: EndpointMiddlewareCtx<Ctx> = async (req, res, next, endpointInfo) => {
|
|
485
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
486
|
+
res.onResponse((status, _data) => {
|
|
487
|
+
console.log(`[TIMING] ${endpointInfo.domain}.${endpointInfo.routeKey} responded with ${status}`);
|
|
488
|
+
});
|
|
489
|
+
await next();
|
|
490
|
+
};
|
|
491
|
+
|
|
484
492
|
const loggingMiddleware: EndpointMiddlewareCtx<Ctx> = async (req, res, next, endpointInfo) => {
|
|
485
493
|
console.log(`[Test] ${req.method} ${req.path} - Domain: ${endpointInfo.domain}, Route: ${endpointInfo.routeKey}`);
|
|
486
494
|
await next();
|
|
@@ -509,6 +517,7 @@ function setupMiddlewareApp(app: any, isHono: boolean) {
|
|
|
509
517
|
}
|
|
510
518
|
|
|
511
519
|
const middlewares = [
|
|
520
|
+
timingMiddleware,
|
|
512
521
|
loggingMiddleware,
|
|
513
522
|
contextMiddleware,
|
|
514
523
|
authMiddleware
|