ts-typed-api 0.2.17 → 0.2.19
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/definition.d.ts +6 -3
- 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/dist/openapi-self.d.ts +4 -0
- package/dist/openapi-self.js +20 -1
- package/dist/openapi.d.ts +2 -2
- package/dist/openapi.js +11 -1
- package/examples/advanced/definitions.ts +7 -0
- package/examples/simple/definitions.ts +8 -0
- package/package.json +1 -1
- package/src/definition.ts +6 -3
- 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/src/openapi-self.ts +27 -1
- package/src/openapi.ts +17 -4
- package/tests/middleware.test.ts +77 -0
- package/tests/openapi-spec.test.ts +101 -0
- package/tests/setup.ts +9 -0
package/dist/definition.d.ts
CHANGED
|
@@ -91,10 +91,13 @@ type RouteWithBody = {
|
|
|
91
91
|
fileUpload?: FileUploadConfig;
|
|
92
92
|
responses: Record<number, ZodTypeAny>;
|
|
93
93
|
};
|
|
94
|
-
export type RouteSchema = RouteWithoutBody | RouteWithBody
|
|
95
|
-
|
|
94
|
+
export type RouteSchema = (RouteWithoutBody | RouteWithBody) & {
|
|
95
|
+
description?: string;
|
|
96
|
+
};
|
|
97
|
+
export type ApiDefinitionSchema<TEndpoints extends Record<string, Record<string, RouteSchema>> = Record<string, Record<string, RouteSchema>>> = {
|
|
96
98
|
prefix?: string;
|
|
97
|
-
|
|
99
|
+
sectionDescriptions?: Partial<Record<keyof TEndpoints, string>>;
|
|
100
|
+
endpoints: TEndpoints;
|
|
98
101
|
};
|
|
99
102
|
export declare function CreateApiDefinition<T extends ApiDefinitionSchema>(definition: T): T;
|
|
100
103
|
export type ApiRouteKey<TDef extends ApiDefinitionSchema, TDomain extends keyof TDef['endpoints']> = keyof TDef['endpoints'][TDomain];
|
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/dist/openapi-self.d.ts
CHANGED
package/dist/openapi-self.js
CHANGED
|
@@ -417,10 +417,12 @@ function processRoute(route, fullPath, registry, domain, anonymousTypes = false)
|
|
|
417
417
|
const responses = createResponses(route.responses, registry, anonymousTypes);
|
|
418
418
|
const operation = {
|
|
419
419
|
summary: `${route.method} ${fullPath}`,
|
|
420
|
-
description: `${route.method} operation for ${fullPath}`,
|
|
421
420
|
responses,
|
|
422
421
|
tags: [domain]
|
|
423
422
|
};
|
|
423
|
+
if (route.description) {
|
|
424
|
+
operation.description = route.description;
|
|
425
|
+
}
|
|
424
426
|
if (parameters.length > 0) {
|
|
425
427
|
operation.parameters = parameters;
|
|
426
428
|
}
|
|
@@ -450,6 +452,7 @@ function generateOpenApiSpec(definitions, options = {}) {
|
|
|
450
452
|
const definitionsArray = Array.isArray(definitions) ? definitions : [definitions];
|
|
451
453
|
const anonymousTypes = options.anonymousTypes || false;
|
|
452
454
|
const allPaths = {};
|
|
455
|
+
const allTags = [];
|
|
453
456
|
// Process each definition
|
|
454
457
|
for (const definition of definitionsArray) {
|
|
455
458
|
const paths = processApiDefinition(definition, registry, anonymousTypes);
|
|
@@ -463,6 +466,18 @@ function generateOpenApiSpec(definitions, options = {}) {
|
|
|
463
466
|
allPaths[path] = pathItem;
|
|
464
467
|
}
|
|
465
468
|
}
|
|
469
|
+
// Collect tags from sectionDescriptions
|
|
470
|
+
if (definition.sectionDescriptions) {
|
|
471
|
+
for (const [sectionName, description] of Object.entries(definition.sectionDescriptions)) {
|
|
472
|
+
// Avoid duplicates
|
|
473
|
+
if (!allTags.find(tag => tag.name === sectionName)) {
|
|
474
|
+
allTags.push({
|
|
475
|
+
name: sectionName,
|
|
476
|
+
description: description
|
|
477
|
+
});
|
|
478
|
+
}
|
|
479
|
+
}
|
|
480
|
+
}
|
|
466
481
|
}
|
|
467
482
|
const spec = {
|
|
468
483
|
openapi: '3.0.0',
|
|
@@ -477,6 +492,10 @@ function generateOpenApiSpec(definitions, options = {}) {
|
|
|
477
492
|
if (options.servers && options.servers.length > 0) {
|
|
478
493
|
spec.servers = options.servers;
|
|
479
494
|
}
|
|
495
|
+
// Add tags if any were found
|
|
496
|
+
if (allTags.length > 0) {
|
|
497
|
+
spec.tags = allTags;
|
|
498
|
+
}
|
|
480
499
|
// Add components with schemas if any were registered and not using anonymous types
|
|
481
500
|
if (!anonymousTypes) {
|
|
482
501
|
const schemas = registry.getSchemas();
|
package/dist/openapi.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
export declare function generateOpenApiSpec(definitions: ApiDefinitionSchema | ApiDefinitionSchema[], options?: {
|
|
1
|
+
import { RouteSchema } from './definition';
|
|
2
|
+
export declare function generateOpenApiSpec<TEndpoints extends Record<string, Record<string, RouteSchema>>>(definitions: import('./definition').ApiDefinitionSchema<TEndpoints> | import('./definition').ApiDefinitionSchema<TEndpoints>[], options?: {
|
|
3
3
|
info?: {
|
|
4
4
|
title?: string;
|
|
5
5
|
version?: string;
|
package/dist/openapi.js
CHANGED
|
@@ -47,11 +47,20 @@ function generateOpenApiSpec(definitions, options = {}) {
|
|
|
47
47
|
},
|
|
48
48
|
};
|
|
49
49
|
}
|
|
50
|
+
// Collect all tags with descriptions for the OpenAPI spec
|
|
51
|
+
const allTags = [];
|
|
50
52
|
// Iterate over multiple API definitions to register routes
|
|
51
53
|
definitionArray.forEach((definition) => {
|
|
52
54
|
Object.keys(definition.endpoints).forEach(domainNameKey => {
|
|
53
55
|
// domainNameKey is a string, representing the domain like 'users', 'products'
|
|
54
56
|
const domain = definition.endpoints[domainNameKey];
|
|
57
|
+
// Add tag if not already present
|
|
58
|
+
if (!allTags.find(tag => tag.name === domainNameKey)) {
|
|
59
|
+
allTags.push({
|
|
60
|
+
name: domainNameKey,
|
|
61
|
+
description: definition.sectionDescriptions?.[domainNameKey]
|
|
62
|
+
});
|
|
63
|
+
}
|
|
55
64
|
Object.keys(domain).forEach(routeNameKey => {
|
|
56
65
|
// routeNameKey is a string, representing the route name like 'getUser', 'createProduct'
|
|
57
66
|
const route = domain[routeNameKey];
|
|
@@ -91,6 +100,7 @@ function generateOpenApiSpec(definitions, options = {}) {
|
|
|
91
100
|
}
|
|
92
101
|
const operation = {
|
|
93
102
|
summary: `${domainNameKey} - ${routeNameKey}`, // Use keys directly for summary
|
|
103
|
+
description: route.description, // Use route description if provided
|
|
94
104
|
tags: [domainNameKey], // Use domainNameKey for tags
|
|
95
105
|
parameters: parameters.length > 0 ? parameters : undefined,
|
|
96
106
|
requestBody: requestBody,
|
|
@@ -103,7 +113,6 @@ function generateOpenApiSpec(definitions, options = {}) {
|
|
|
103
113
|
method: route.method.toLowerCase(), // Ensure method is lowercase
|
|
104
114
|
path: openApiPath,
|
|
105
115
|
...operation,
|
|
106
|
-
// Add description or other OpenAPI fields if available in RouteSchema
|
|
107
116
|
});
|
|
108
117
|
});
|
|
109
118
|
});
|
|
@@ -118,6 +127,7 @@ function generateOpenApiSpec(definitions, options = {}) {
|
|
|
118
127
|
description: options.info?.description ?? 'Automatically generated OpenAPI specification',
|
|
119
128
|
},
|
|
120
129
|
servers: options.servers ?? [{ url: '/api' }], // Adjust as needed
|
|
130
|
+
tags: allTags.filter(tag => tag.description), // Only include tags that have descriptions
|
|
121
131
|
});
|
|
122
132
|
return openApiDocument;
|
|
123
133
|
}
|
|
@@ -20,11 +20,16 @@ const ProductSchema = z.object({
|
|
|
20
20
|
|
|
21
21
|
export const PublicApiDefinition = CreateApiDefinition({
|
|
22
22
|
prefix: '/api/v1/public',
|
|
23
|
+
sectionDescriptions: {
|
|
24
|
+
auth: 'Authentication endpoints for user login and logout',
|
|
25
|
+
products: 'Product management and listing endpoints'
|
|
26
|
+
},
|
|
23
27
|
endpoints: {
|
|
24
28
|
auth: {
|
|
25
29
|
login: {
|
|
26
30
|
method: 'POST',
|
|
27
31
|
path: '/login',
|
|
32
|
+
description: 'Authenticate a user with username and password, returns JWT token',
|
|
28
33
|
body: z.object({
|
|
29
34
|
username: z.string(),
|
|
30
35
|
password: z.string()
|
|
@@ -42,6 +47,7 @@ export const PublicApiDefinition = CreateApiDefinition({
|
|
|
42
47
|
logout: {
|
|
43
48
|
method: 'POST',
|
|
44
49
|
path: '/logout',
|
|
50
|
+
description: 'Log out the current user and invalidate their session',
|
|
45
51
|
responses: CreateResponses({
|
|
46
52
|
200: z.object({
|
|
47
53
|
message: z.string()
|
|
@@ -53,6 +59,7 @@ export const PublicApiDefinition = CreateApiDefinition({
|
|
|
53
59
|
list: {
|
|
54
60
|
method: 'GET',
|
|
55
61
|
path: '/products',
|
|
62
|
+
description: 'Retrieve a paginated list of products with optional filtering',
|
|
56
63
|
query: z.object({
|
|
57
64
|
page: z.number().int().min(1).optional().default(1),
|
|
58
65
|
limit: z.number().int().min(1).max(100).optional().default(10),
|
|
@@ -2,11 +2,16 @@ import { ZodSchema as z, CreateApiDefinition, CreateResponses } from '../../src'
|
|
|
2
2
|
|
|
3
3
|
export const PublicApiDefinition = CreateApiDefinition({
|
|
4
4
|
prefix: '/api/v1/public',
|
|
5
|
+
sectionDescriptions: {
|
|
6
|
+
status: 'Health check and status endpoints',
|
|
7
|
+
common: 'Common utility endpoints'
|
|
8
|
+
},
|
|
5
9
|
endpoints: {
|
|
6
10
|
status: {
|
|
7
11
|
probe1: {
|
|
8
12
|
method: 'GET',
|
|
9
13
|
path: '/status/probe1',
|
|
14
|
+
description: 'Advanced health check with query parameters',
|
|
10
15
|
query: z.object({
|
|
11
16
|
match: z.boolean()
|
|
12
17
|
}),
|
|
@@ -21,6 +26,7 @@ export const PublicApiDefinition = CreateApiDefinition({
|
|
|
21
26
|
probe2: {
|
|
22
27
|
method: 'GET',
|
|
23
28
|
path: '/status/probe2',
|
|
29
|
+
description: 'Simple health check endpoint',
|
|
24
30
|
responses: CreateResponses({
|
|
25
31
|
200: z.enum(["pong"]),
|
|
26
32
|
})
|
|
@@ -30,6 +36,7 @@ export const PublicApiDefinition = CreateApiDefinition({
|
|
|
30
36
|
ping: {
|
|
31
37
|
method: 'GET',
|
|
32
38
|
path: '/ping',
|
|
39
|
+
description: 'Basic ping endpoint to check if the service is alive',
|
|
33
40
|
query: z.object({
|
|
34
41
|
format: z.enum(["json", "html"]).optional()
|
|
35
42
|
}),
|
|
@@ -40,6 +47,7 @@ export const PublicApiDefinition = CreateApiDefinition({
|
|
|
40
47
|
customHeaders: {
|
|
41
48
|
method: 'GET',
|
|
42
49
|
path: '/custom-headers',
|
|
50
|
+
description: 'Returns information about custom headers',
|
|
43
51
|
responses: CreateResponses({
|
|
44
52
|
200: z.object({
|
|
45
53
|
message: z.string()
|
package/package.json
CHANGED
package/src/definition.ts
CHANGED
|
@@ -184,13 +184,16 @@ type RouteWithBody = {
|
|
|
184
184
|
};
|
|
185
185
|
|
|
186
186
|
// Union type for all route schemas
|
|
187
|
-
export type RouteSchema = RouteWithoutBody | RouteWithBody
|
|
187
|
+
export type RouteSchema = (RouteWithoutBody | RouteWithBody) & {
|
|
188
|
+
description?: string;
|
|
189
|
+
};
|
|
188
190
|
|
|
189
191
|
// Define the structure for the entire API definition object
|
|
190
192
|
// Now includes an optional prefix and endpoints record
|
|
191
|
-
export type ApiDefinitionSchema = {
|
|
193
|
+
export type ApiDefinitionSchema<TEndpoints extends Record<string, Record<string, RouteSchema>> = Record<string, Record<string, RouteSchema>>> = {
|
|
192
194
|
prefix?: string;
|
|
193
|
-
|
|
195
|
+
sectionDescriptions?: Partial<Record<keyof TEndpoints, string>>;
|
|
196
|
+
endpoints: TEndpoints;
|
|
194
197
|
};
|
|
195
198
|
|
|
196
199
|
// Helper function to ensure the definition conforms to ApiDefinitionSchema
|
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/src/openapi-self.ts
CHANGED
|
@@ -17,6 +17,10 @@ export interface OpenAPISpec {
|
|
|
17
17
|
components?: {
|
|
18
18
|
schemas?: Record<string, SchemaObject>;
|
|
19
19
|
};
|
|
20
|
+
tags?: Array<{
|
|
21
|
+
name: string;
|
|
22
|
+
description?: string;
|
|
23
|
+
}>;
|
|
20
24
|
}
|
|
21
25
|
|
|
22
26
|
export interface PathItem {
|
|
@@ -558,11 +562,14 @@ function processRoute(
|
|
|
558
562
|
|
|
559
563
|
const operation: Operation = {
|
|
560
564
|
summary: `${route.method} ${fullPath}`,
|
|
561
|
-
description: `${route.method} operation for ${fullPath}`,
|
|
562
565
|
responses,
|
|
563
566
|
tags: [domain]
|
|
564
567
|
};
|
|
565
568
|
|
|
569
|
+
if (route.description) {
|
|
570
|
+
operation.description = route.description;
|
|
571
|
+
}
|
|
572
|
+
|
|
566
573
|
if (parameters.length > 0) {
|
|
567
574
|
operation.parameters = parameters;
|
|
568
575
|
}
|
|
@@ -609,6 +616,7 @@ export function generateOpenApiSpec(
|
|
|
609
616
|
const anonymousTypes = options.anonymousTypes || false;
|
|
610
617
|
|
|
611
618
|
const allPaths: Record<string, PathItem> = {};
|
|
619
|
+
const allTags: Array<{ name: string; description?: string }> = [];
|
|
612
620
|
|
|
613
621
|
// Process each definition
|
|
614
622
|
for (const definition of definitionsArray) {
|
|
@@ -623,6 +631,19 @@ export function generateOpenApiSpec(
|
|
|
623
631
|
allPaths[path] = pathItem;
|
|
624
632
|
}
|
|
625
633
|
}
|
|
634
|
+
|
|
635
|
+
// Collect tags from sectionDescriptions
|
|
636
|
+
if (definition.sectionDescriptions) {
|
|
637
|
+
for (const [sectionName, description] of Object.entries(definition.sectionDescriptions)) {
|
|
638
|
+
// Avoid duplicates
|
|
639
|
+
if (!allTags.find(tag => tag.name === sectionName)) {
|
|
640
|
+
allTags.push({
|
|
641
|
+
name: sectionName,
|
|
642
|
+
description: description
|
|
643
|
+
});
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
626
647
|
}
|
|
627
648
|
|
|
628
649
|
const spec: OpenAPISpec = {
|
|
@@ -640,6 +661,11 @@ export function generateOpenApiSpec(
|
|
|
640
661
|
spec.servers = options.servers;
|
|
641
662
|
}
|
|
642
663
|
|
|
664
|
+
// Add tags if any were found
|
|
665
|
+
if (allTags.length > 0) {
|
|
666
|
+
spec.tags = allTags;
|
|
667
|
+
}
|
|
668
|
+
|
|
643
669
|
// Add components with schemas if any were registered and not using anonymous types
|
|
644
670
|
if (!anonymousTypes) {
|
|
645
671
|
const schemas = registry.getSchemas();
|
package/src/openapi.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RouteSchema } from './definition';
|
|
2
2
|
import { OpenAPIRegistry, OpenApiGeneratorV31, extendZodWithOpenApi } from '@asteasolutions/zod-to-openapi';
|
|
3
3
|
import { z, ZodTypeAny } from 'zod';
|
|
4
4
|
|
|
5
5
|
// Extend Zod with OpenAPI capabilities
|
|
6
6
|
extendZodWithOpenApi(z);
|
|
7
7
|
|
|
8
|
-
export function generateOpenApiSpec(
|
|
9
|
-
definitions: ApiDefinitionSchema | ApiDefinitionSchema[],
|
|
8
|
+
export function generateOpenApiSpec<TEndpoints extends Record<string, Record<string, RouteSchema>>>(
|
|
9
|
+
definitions: import('./definition').ApiDefinitionSchema<TEndpoints> | import('./definition').ApiDefinitionSchema<TEndpoints>[],
|
|
10
10
|
options: {
|
|
11
11
|
info?: {
|
|
12
12
|
title?: string;
|
|
@@ -59,11 +59,23 @@ export function generateOpenApiSpec(
|
|
|
59
59
|
};
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// Collect all tags with descriptions for the OpenAPI spec
|
|
63
|
+
const allTags: Array<{ name: string; description?: string }> = [];
|
|
64
|
+
|
|
62
65
|
// Iterate over multiple API definitions to register routes
|
|
63
66
|
definitionArray.forEach((definition) => {
|
|
64
67
|
Object.keys(definition.endpoints).forEach(domainNameKey => {
|
|
65
68
|
// domainNameKey is a string, representing the domain like 'users', 'products'
|
|
66
69
|
const domain = definition.endpoints[domainNameKey];
|
|
70
|
+
|
|
71
|
+
// Add tag if not already present
|
|
72
|
+
if (!allTags.find(tag => tag.name === domainNameKey)) {
|
|
73
|
+
allTags.push({
|
|
74
|
+
name: domainNameKey,
|
|
75
|
+
description: definition.sectionDescriptions?.[domainNameKey]
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
67
79
|
Object.keys(domain).forEach(routeNameKey => {
|
|
68
80
|
// routeNameKey is a string, representing the route name like 'getUser', 'createProduct'
|
|
69
81
|
const route: RouteSchema = domain[routeNameKey];
|
|
@@ -108,6 +120,7 @@ export function generateOpenApiSpec(
|
|
|
108
120
|
|
|
109
121
|
const operation = {
|
|
110
122
|
summary: `${domainNameKey} - ${routeNameKey}`, // Use keys directly for summary
|
|
123
|
+
description: route.description, // Use route description if provided
|
|
111
124
|
tags: [domainNameKey], // Use domainNameKey for tags
|
|
112
125
|
parameters: parameters.length > 0 ? parameters : undefined,
|
|
113
126
|
requestBody: requestBody,
|
|
@@ -122,7 +135,6 @@ export function generateOpenApiSpec(
|
|
|
122
135
|
method: route.method.toLowerCase() as any, // Ensure method is lowercase
|
|
123
136
|
path: openApiPath,
|
|
124
137
|
...operation,
|
|
125
|
-
// Add description or other OpenAPI fields if available in RouteSchema
|
|
126
138
|
});
|
|
127
139
|
});
|
|
128
140
|
});
|
|
@@ -138,6 +150,7 @@ export function generateOpenApiSpec(
|
|
|
138
150
|
description: options.info?.description ?? 'Automatically generated OpenAPI specification',
|
|
139
151
|
},
|
|
140
152
|
servers: options.servers ?? [{ url: '/api' }], // Adjust as needed
|
|
153
|
+
tags: allTags.filter(tag => tag.description), // Only include tags that have descriptions
|
|
141
154
|
});
|
|
142
155
|
|
|
143
156
|
return openApiDocument;
|
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
|
});
|
|
@@ -316,4 +316,105 @@ describe('OpenAPI Specification Generation', () => {
|
|
|
316
316
|
}
|
|
317
317
|
}
|
|
318
318
|
});
|
|
319
|
+
|
|
320
|
+
test('should include section descriptions in OpenAPI tags', () => {
|
|
321
|
+
const spec = generateOpenApiSpec(PublicApiDefinition);
|
|
322
|
+
|
|
323
|
+
expect(spec).toBeDefined();
|
|
324
|
+
expect(spec.tags).toBeDefined();
|
|
325
|
+
expect(Array.isArray(spec.tags)).toBe(true);
|
|
326
|
+
|
|
327
|
+
// Check that tags with descriptions are included
|
|
328
|
+
const statusTag = spec.tags?.find((tag: any) => tag.name === 'status');
|
|
329
|
+
const commonTag = spec.tags?.find((tag: any) => tag.name === 'common');
|
|
330
|
+
|
|
331
|
+
expect(statusTag).toBeDefined();
|
|
332
|
+
expect(statusTag?.description).toBe('Health check and status endpoints');
|
|
333
|
+
|
|
334
|
+
expect(commonTag).toBeDefined();
|
|
335
|
+
expect(commonTag?.description).toBe('Common utility endpoints');
|
|
336
|
+
});
|
|
337
|
+
|
|
338
|
+
test('should include route descriptions in operation descriptions', () => {
|
|
339
|
+
const spec = generateOpenApiSpec(PublicApiDefinition);
|
|
340
|
+
|
|
341
|
+
expect(spec).toBeDefined();
|
|
342
|
+
expect(spec.paths).toBeDefined();
|
|
343
|
+
|
|
344
|
+
// Check specific endpoints for descriptions
|
|
345
|
+
const probe1Path = spec.paths?.['/api/v1/public/status/probe1'];
|
|
346
|
+
const pingPath = spec.paths?.['/api/v1/public/ping'];
|
|
347
|
+
|
|
348
|
+
expect(probe1Path?.get?.description).toBe('Advanced health check with query parameters');
|
|
349
|
+
expect(pingPath?.get?.description).toBe('Basic ping endpoint to check if the service is alive');
|
|
350
|
+
});
|
|
351
|
+
|
|
352
|
+
test('should handle API definitions without descriptions', () => {
|
|
353
|
+
// Create a definition without descriptions
|
|
354
|
+
const NoDescriptionDefinition = CreateApiDefinition({
|
|
355
|
+
endpoints: {
|
|
356
|
+
test: {
|
|
357
|
+
simpleEndpoint: {
|
|
358
|
+
method: 'GET' as const,
|
|
359
|
+
path: '/test',
|
|
360
|
+
responses: {
|
|
361
|
+
200: z.object({ message: z.string() })
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
const spec = generateOpenApiSpec(NoDescriptionDefinition);
|
|
369
|
+
|
|
370
|
+
expect(spec).toBeDefined();
|
|
371
|
+
expect(spec.tags).toBeUndefined(); // No tags should be generated when no descriptions
|
|
372
|
+
|
|
373
|
+
const testPath = spec.paths?.['/test'];
|
|
374
|
+
expect(testPath?.get?.description).toBeUndefined(); // No description on operation
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
test('should handle partial section descriptions', () => {
|
|
378
|
+
// Create a definition with only some sections having descriptions
|
|
379
|
+
const PartialDescriptionDefinition = CreateApiDefinition({
|
|
380
|
+
sectionDescriptions: {
|
|
381
|
+
section1: 'Description for section 1'
|
|
382
|
+
// section2 intentionally omitted
|
|
383
|
+
},
|
|
384
|
+
endpoints: {
|
|
385
|
+
section1: {
|
|
386
|
+
endpoint1: {
|
|
387
|
+
method: 'GET' as const,
|
|
388
|
+
path: '/section1/endpoint1',
|
|
389
|
+
responses: {
|
|
390
|
+
200: z.object({ data: z.string() })
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
},
|
|
394
|
+
section2: {
|
|
395
|
+
endpoint2: {
|
|
396
|
+
method: 'GET' as const,
|
|
397
|
+
path: '/section2/endpoint2',
|
|
398
|
+
responses: {
|
|
399
|
+
200: z.object({ data: z.string() })
|
|
400
|
+
}
|
|
401
|
+
}
|
|
402
|
+
}
|
|
403
|
+
}
|
|
404
|
+
});
|
|
405
|
+
|
|
406
|
+
const spec = generateOpenApiSpec(PartialDescriptionDefinition);
|
|
407
|
+
|
|
408
|
+
expect(spec).toBeDefined();
|
|
409
|
+
expect(spec.tags).toBeDefined();
|
|
410
|
+
expect(spec.tags?.length).toBe(1); // Only one tag should be generated
|
|
411
|
+
|
|
412
|
+
const section1Tag = spec.tags?.find((tag: any) => tag.name === 'section1');
|
|
413
|
+
expect(section1Tag).toBeDefined();
|
|
414
|
+
expect(section1Tag?.description).toBe('Description for section 1');
|
|
415
|
+
|
|
416
|
+
// section2 should not have a tag since it has no description
|
|
417
|
+
const section2Tag = spec.tags?.find((tag: any) => tag.name === 'section2');
|
|
418
|
+
expect(section2Tag).toBeUndefined();
|
|
419
|
+
});
|
|
319
420
|
});
|
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
|