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 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 method to res for middleware compatibility
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 method for middleware compatibility
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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ts-typed-api",
3
- "version": "0.2.17",
3
+ "version": "0.2.18",
4
4
  "description": "A lightweight, type-safe RPC library for TypeScript with Zod validation",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
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 method to res for middleware compatibility
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 method for middleware compatibility
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
 
@@ -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
@@ -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