ts-typed-api 0.1.21 → 0.1.23
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/README_HONO_ADAPTER.md +136 -0
- package/dist/hono-cloudflare-workers.d.ts +33 -0
- package/dist/hono-cloudflare-workers.js +474 -0
- package/dist/index.d.ts +1 -0
- package/dist/index.js +6 -1
- package/dist/openapi-self.js +39 -2
- package/examples/hono-cloudflare-worker-example.ts +66 -0
- package/examples/test-hono-server.ts +125 -0
- package/package.json +3 -2
- package/src/hono-cloudflare-workers.ts +552 -0
- package/src/index.ts +3 -0
- package/src/openapi-self.ts +42 -2
- package/tests/advanced-api.test.ts +11 -4
- package/tests/openapi-spec.test.ts +130 -0
- package/tests/setup.ts +324 -176
- package/tests/simple-api.test.ts +33 -3
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
# Hono Cloudflare Workers Adapter
|
|
2
|
+
|
|
3
|
+
This adapter allows you to use `ts-typed-api` with [Hono](https://hono.dev/) framework in Cloudflare Workers, while maintaining the same API definitions and handler functions as the Express version.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
npm install hono
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
## Usage
|
|
12
|
+
|
|
13
|
+
### Basic Setup
|
|
14
|
+
|
|
15
|
+
```typescript
|
|
16
|
+
import { Hono } from 'hono';
|
|
17
|
+
import { RegisterHonoHandlers } from 'ts-typed-api';
|
|
18
|
+
import { ApiDefinition } from './definitions';
|
|
19
|
+
|
|
20
|
+
const app = new Hono();
|
|
21
|
+
|
|
22
|
+
// Use the same API definitions and handlers as Express
|
|
23
|
+
RegisterHonoHandlers(app, ApiDefinition, {
|
|
24
|
+
domain: {
|
|
25
|
+
endpoint: async (req, res) => {
|
|
26
|
+
res.respond(200, { message: 'Hello from Cloudflare Workers!' });
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
// Export for Cloudflare Workers
|
|
32
|
+
export default app;
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
### File Upload Support
|
|
36
|
+
|
|
37
|
+
The adapter supports file uploads using Hono's `parseBody()` method, which is compatible with Cloudflare Workers:
|
|
38
|
+
|
|
39
|
+
```typescript
|
|
40
|
+
import { CreateApiDefinition, CreateResponses } from 'ts-typed-api';
|
|
41
|
+
|
|
42
|
+
const ApiDefinition = CreateApiDefinition({
|
|
43
|
+
endpoints: {
|
|
44
|
+
upload: {
|
|
45
|
+
file: {
|
|
46
|
+
method: 'POST',
|
|
47
|
+
path: '/upload',
|
|
48
|
+
fileUpload: {
|
|
49
|
+
single: {
|
|
50
|
+
fieldName: 'file',
|
|
51
|
+
maxSize: 10 * 1024 * 1024, // 10MB
|
|
52
|
+
allowedMimeTypes: ['image/jpeg', 'image/png']
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
responses: CreateResponses({
|
|
56
|
+
200: z.object({ filename: z.string() })
|
|
57
|
+
})
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Handler works the same as Express
|
|
64
|
+
RegisterHonoHandlers(app, ApiDefinition, {
|
|
65
|
+
upload: {
|
|
66
|
+
file: async (req, res) => {
|
|
67
|
+
const file = req.file; // File object with buffer, mimetype, etc.
|
|
68
|
+
// Process file...
|
|
69
|
+
res.respond(200, { filename: file.originalname });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
});
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
### Middleware Support
|
|
76
|
+
|
|
77
|
+
```typescript
|
|
78
|
+
const authMiddleware = async (req, res, next, endpointInfo) => {
|
|
79
|
+
const authHeader = req.headers.get('authorization');
|
|
80
|
+
if (!authHeader?.startsWith('Bearer ')) {
|
|
81
|
+
return res.status(401).json({
|
|
82
|
+
error: [{ field: "authorization", type: "general", message: "Unauthorized" }]
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
await next();
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
RegisterHonoHandlers(app, ApiDefinition, handlers, [authMiddleware]);
|
|
89
|
+
```
|
|
90
|
+
|
|
91
|
+
## Key Differences from Express
|
|
92
|
+
|
|
93
|
+
1. **File Handling**: Uses `Uint8Array` instead of `Buffer` for file contents
|
|
94
|
+
2. **Headers**: Use `req.headers.get()` instead of `req.headers[]`
|
|
95
|
+
3. **Response Methods**: Hono uses `c.json()` instead of Express `res.json()`
|
|
96
|
+
4. **Middleware**: Adapted to work with Hono's middleware system
|
|
97
|
+
|
|
98
|
+
## Cloudflare Workers Deployment
|
|
99
|
+
|
|
100
|
+
```javascript
|
|
101
|
+
// wrangler.toml
|
|
102
|
+
name = "my-api"
|
|
103
|
+
main = "src/index.ts"
|
|
104
|
+
compatibility_date = "2023-01-01"
|
|
105
|
+
|
|
106
|
+
// src/index.ts
|
|
107
|
+
import app from './app';
|
|
108
|
+
|
|
109
|
+
export default {
|
|
110
|
+
fetch: app.fetch
|
|
111
|
+
};
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## API Compatibility
|
|
115
|
+
|
|
116
|
+
The `RegisterHonoHandlers` function has the same signature as `RegisterHandlers`:
|
|
117
|
+
|
|
118
|
+
```typescript
|
|
119
|
+
RegisterHonoHandlers<TDef extends ApiDefinitionSchema>(
|
|
120
|
+
app: Hono,
|
|
121
|
+
apiDefinition: TDef,
|
|
122
|
+
objectHandlers: ObjectHandlers<TDef>,
|
|
123
|
+
middlewares?: AnyMiddleware<TDef>[]
|
|
124
|
+
): void
|
|
125
|
+
```
|
|
126
|
+
|
|
127
|
+
This means you can switch between Express and Hono by simply changing the import and app initialization, while keeping your API definitions and handlers unchanged.
|
|
128
|
+
|
|
129
|
+
## Supported File Upload Configurations
|
|
130
|
+
|
|
131
|
+
- `single`: Single file upload
|
|
132
|
+
- `array`: Multiple files with same field name
|
|
133
|
+
- `fields`: Multiple files with different field names
|
|
134
|
+
- `any`: Accept any files
|
|
135
|
+
|
|
136
|
+
All configurations work the same as in the Express version but use Workers-compatible APIs.
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Hono, Context } from 'hono';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { ApiDefinitionSchema } from './definition';
|
|
4
|
+
import { TypedRequest, TypedResponse } from './router';
|
|
5
|
+
import { SpecificRouteHandler } from './handler';
|
|
6
|
+
import { ObjectHandlers, AnyMiddleware, EndpointMiddleware } from './object-handlers';
|
|
7
|
+
export type HonoFile = File;
|
|
8
|
+
export declare const honoFileSchema: z.ZodObject<{
|
|
9
|
+
fieldname: z.ZodString;
|
|
10
|
+
originalname: z.ZodString;
|
|
11
|
+
encoding: z.ZodString;
|
|
12
|
+
mimetype: z.ZodString;
|
|
13
|
+
size: z.ZodNumber;
|
|
14
|
+
buffer: z.ZodOptional<z.ZodCustom<Uint8Array<ArrayBuffer>, Uint8Array<ArrayBuffer>>>;
|
|
15
|
+
file: z.ZodOptional<z.ZodCustom<import("buffer").File, import("buffer").File>>;
|
|
16
|
+
destination: z.ZodOptional<z.ZodString>;
|
|
17
|
+
filename: z.ZodOptional<z.ZodString>;
|
|
18
|
+
path: z.ZodOptional<z.ZodString>;
|
|
19
|
+
stream: z.ZodOptional<z.ZodAny>;
|
|
20
|
+
}, z.core.$strip>;
|
|
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 & {
|
|
23
|
+
params: TypedRequest<TDef, TDomain, TRouteKey>['params'];
|
|
24
|
+
query: TypedRequest<TDef, TDomain, TRouteKey>['query'];
|
|
25
|
+
body: TypedRequest<TDef, TDomain, TRouteKey>['body'];
|
|
26
|
+
file?: HonoFile;
|
|
27
|
+
files?: HonoFile[] | {
|
|
28
|
+
[fieldname: string]: HonoFile[];
|
|
29
|
+
};
|
|
30
|
+
respond: TypedResponse<TDef, TDomain, TRouteKey>['respond'];
|
|
31
|
+
};
|
|
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;
|
|
@@ -0,0 +1,474 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.honoFileSchema = void 0;
|
|
4
|
+
exports.registerHonoRouteHandlers = registerHonoRouteHandlers;
|
|
5
|
+
exports.RegisterHonoHandlers = RegisterHonoHandlers;
|
|
6
|
+
const zod_1 = require("zod");
|
|
7
|
+
// Hono-compatible file schema for Workers environment
|
|
8
|
+
exports.honoFileSchema = zod_1.z.object({
|
|
9
|
+
fieldname: zod_1.z.string(),
|
|
10
|
+
originalname: zod_1.z.string(),
|
|
11
|
+
encoding: zod_1.z.string(),
|
|
12
|
+
mimetype: zod_1.z.string(),
|
|
13
|
+
size: zod_1.z.number(),
|
|
14
|
+
buffer: zod_1.z.instanceof(Uint8Array).optional(), // Workers use Uint8Array instead of Buffer
|
|
15
|
+
file: zod_1.z.instanceof(File).optional(), // Direct File object access
|
|
16
|
+
destination: zod_1.z.string().optional(),
|
|
17
|
+
filename: zod_1.z.string().optional(),
|
|
18
|
+
path: zod_1.z.string().optional(),
|
|
19
|
+
stream: zod_1.z.any().optional(),
|
|
20
|
+
});
|
|
21
|
+
// Helper function to preprocess query parameters for type coercion
|
|
22
|
+
function preprocessQueryParams(query, querySchema) {
|
|
23
|
+
if (!querySchema || !query)
|
|
24
|
+
return query;
|
|
25
|
+
const processedQuery = { ...query };
|
|
26
|
+
if (querySchema instanceof zod_1.z.ZodObject) {
|
|
27
|
+
const shape = querySchema.shape;
|
|
28
|
+
for (const [key, value] of Object.entries(processedQuery)) {
|
|
29
|
+
if (typeof value === 'string' && shape[key]) {
|
|
30
|
+
const fieldSchema = shape[key];
|
|
31
|
+
let innerSchema = fieldSchema;
|
|
32
|
+
if (fieldSchema instanceof zod_1.z.ZodOptional) {
|
|
33
|
+
innerSchema = fieldSchema._def.innerType;
|
|
34
|
+
}
|
|
35
|
+
if (fieldSchema instanceof zod_1.z.ZodDefault) {
|
|
36
|
+
innerSchema = fieldSchema._def.innerType;
|
|
37
|
+
}
|
|
38
|
+
while (innerSchema instanceof zod_1.z.ZodOptional || innerSchema instanceof zod_1.z.ZodDefault) {
|
|
39
|
+
if (innerSchema instanceof zod_1.z.ZodOptional) {
|
|
40
|
+
innerSchema = innerSchema._def.innerType;
|
|
41
|
+
}
|
|
42
|
+
else if (innerSchema instanceof zod_1.z.ZodDefault) {
|
|
43
|
+
innerSchema = innerSchema._def.innerType;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
if (innerSchema instanceof zod_1.z.ZodNumber) {
|
|
47
|
+
const numValue = Number(value);
|
|
48
|
+
if (!isNaN(numValue)) {
|
|
49
|
+
processedQuery[key] = numValue;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
else if (innerSchema instanceof zod_1.z.ZodBoolean) {
|
|
53
|
+
if (value === 'true') {
|
|
54
|
+
processedQuery[key] = true;
|
|
55
|
+
}
|
|
56
|
+
else if (value === 'false') {
|
|
57
|
+
processedQuery[key] = false;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return processedQuery;
|
|
64
|
+
}
|
|
65
|
+
// Helper function to create file upload middleware for Hono/Workers
|
|
66
|
+
function createHonoFileUploadMiddleware(config) {
|
|
67
|
+
return async (c, next) => {
|
|
68
|
+
try {
|
|
69
|
+
if (config.single) {
|
|
70
|
+
const formData = await c.req.parseBody({ all: false });
|
|
71
|
+
const file = formData[config.single.fieldName];
|
|
72
|
+
if (file instanceof File) {
|
|
73
|
+
// Validate file
|
|
74
|
+
if (config.single.maxSize && file.size > config.single.maxSize) {
|
|
75
|
+
return c.json({
|
|
76
|
+
data: null,
|
|
77
|
+
error: [{ field: 'file', message: `File size exceeds ${config.single.maxSize} bytes`, type: 'body' }]
|
|
78
|
+
}, 422);
|
|
79
|
+
}
|
|
80
|
+
if (config.single.allowedMimeTypes && !config.single.allowedMimeTypes.includes(file.type)) {
|
|
81
|
+
return c.json({
|
|
82
|
+
data: null,
|
|
83
|
+
error: [{ field: 'file', message: `File type ${file.type} not allowed`, type: 'body' }]
|
|
84
|
+
}, 422);
|
|
85
|
+
}
|
|
86
|
+
// Attach file to context
|
|
87
|
+
c.file = {
|
|
88
|
+
fieldname: config.single.fieldName,
|
|
89
|
+
originalname: file.name,
|
|
90
|
+
encoding: '7bit',
|
|
91
|
+
mimetype: file.type,
|
|
92
|
+
size: file.size,
|
|
93
|
+
buffer: new Uint8Array(await file.arrayBuffer()),
|
|
94
|
+
file: file
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
else if (config.array) {
|
|
99
|
+
const formData = await c.req.parseBody({ all: true });
|
|
100
|
+
const files = formData[config.array.fieldName];
|
|
101
|
+
if (Array.isArray(files)) {
|
|
102
|
+
// Validate files
|
|
103
|
+
if (config.array.maxCount && files.length > config.array.maxCount) {
|
|
104
|
+
return c.json({
|
|
105
|
+
data: null,
|
|
106
|
+
error: [{ field: 'file', message: `Maximum ${config.array.maxCount} files allowed`, type: 'body' }]
|
|
107
|
+
}, 422);
|
|
108
|
+
}
|
|
109
|
+
const processedFiles = [];
|
|
110
|
+
for (const file of files) {
|
|
111
|
+
if (file instanceof File) {
|
|
112
|
+
if (config.array.maxSize && file.size > config.array.maxSize) {
|
|
113
|
+
return c.json({
|
|
114
|
+
data: null,
|
|
115
|
+
error: [{ field: 'file', message: `File size exceeds ${config.array.maxSize} bytes`, type: 'body' }]
|
|
116
|
+
}, 422);
|
|
117
|
+
}
|
|
118
|
+
if (config.array.allowedMimeTypes && !config.array.allowedMimeTypes.includes(file.type)) {
|
|
119
|
+
return c.json({
|
|
120
|
+
data: null,
|
|
121
|
+
error: [{ field: 'file', message: `File type ${file.type} not allowed`, type: 'body' }]
|
|
122
|
+
}, 422);
|
|
123
|
+
}
|
|
124
|
+
processedFiles.push({
|
|
125
|
+
fieldname: config.array.fieldName,
|
|
126
|
+
originalname: file.name,
|
|
127
|
+
encoding: '7bit',
|
|
128
|
+
mimetype: file.type,
|
|
129
|
+
size: file.size,
|
|
130
|
+
buffer: new Uint8Array(await file.arrayBuffer()),
|
|
131
|
+
file: file
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
c.files = processedFiles;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
else if (config.fields) {
|
|
139
|
+
const formData = await c.req.parseBody({ all: true });
|
|
140
|
+
const filesMap = {};
|
|
141
|
+
for (const fieldConfig of config.fields) {
|
|
142
|
+
const files = formData[fieldConfig.fieldName];
|
|
143
|
+
if (Array.isArray(files)) {
|
|
144
|
+
if (fieldConfig.maxCount && files.length > fieldConfig.maxCount) {
|
|
145
|
+
return c.json({
|
|
146
|
+
data: null,
|
|
147
|
+
error: [{ field: fieldConfig.fieldName, message: `Maximum ${fieldConfig.maxCount} files allowed`, type: 'body' }]
|
|
148
|
+
}, 422);
|
|
149
|
+
}
|
|
150
|
+
const processedFiles = [];
|
|
151
|
+
for (const file of files) {
|
|
152
|
+
if (file instanceof File) {
|
|
153
|
+
if (fieldConfig.maxSize && file.size > fieldConfig.maxSize) {
|
|
154
|
+
return c.json({
|
|
155
|
+
data: null,
|
|
156
|
+
error: [{ field: fieldConfig.fieldName, message: `File size exceeds ${fieldConfig.maxSize} bytes`, type: 'body' }]
|
|
157
|
+
}, 422);
|
|
158
|
+
}
|
|
159
|
+
if (fieldConfig.allowedMimeTypes && !fieldConfig.allowedMimeTypes.includes(file.type)) {
|
|
160
|
+
return c.json({
|
|
161
|
+
data: null,
|
|
162
|
+
error: [{ field: fieldConfig.fieldName, message: `File type ${file.type} not allowed`, type: 'body' }]
|
|
163
|
+
}, 422);
|
|
164
|
+
}
|
|
165
|
+
processedFiles.push({
|
|
166
|
+
fieldname: fieldConfig.fieldName,
|
|
167
|
+
originalname: file.name,
|
|
168
|
+
encoding: '7bit',
|
|
169
|
+
mimetype: file.type,
|
|
170
|
+
size: file.size,
|
|
171
|
+
buffer: new Uint8Array(await file.arrayBuffer()),
|
|
172
|
+
file: file
|
|
173
|
+
});
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
filesMap[fieldConfig.fieldName] = processedFiles;
|
|
177
|
+
}
|
|
178
|
+
}
|
|
179
|
+
c.files = filesMap;
|
|
180
|
+
}
|
|
181
|
+
else if (config.any) {
|
|
182
|
+
const formData = await c.req.parseBody({ all: true });
|
|
183
|
+
// Process all files
|
|
184
|
+
const allFiles = [];
|
|
185
|
+
for (const [key, value] of Object.entries(formData)) {
|
|
186
|
+
if (value instanceof File) {
|
|
187
|
+
if (config.any.maxSize && value.size > config.any.maxSize) {
|
|
188
|
+
return c.json({
|
|
189
|
+
data: null,
|
|
190
|
+
error: [{ field: key, message: `File size exceeds ${config.any.maxSize} bytes`, type: 'body' }]
|
|
191
|
+
}, 422);
|
|
192
|
+
}
|
|
193
|
+
if (config.any.allowedMimeTypes && !config.any.allowedMimeTypes.includes(value.type)) {
|
|
194
|
+
return c.json({
|
|
195
|
+
data: null,
|
|
196
|
+
error: [{ field: key, message: `File type ${value.type} not allowed`, type: 'body' }]
|
|
197
|
+
}, 422);
|
|
198
|
+
}
|
|
199
|
+
allFiles.push({
|
|
200
|
+
fieldname: key,
|
|
201
|
+
originalname: value.name,
|
|
202
|
+
encoding: '7bit',
|
|
203
|
+
mimetype: value.type,
|
|
204
|
+
size: value.size,
|
|
205
|
+
buffer: new Uint8Array(await value.arrayBuffer()),
|
|
206
|
+
file: value
|
|
207
|
+
});
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
c.files = allFiles;
|
|
211
|
+
}
|
|
212
|
+
await next();
|
|
213
|
+
}
|
|
214
|
+
catch (error) {
|
|
215
|
+
console.error('File upload middleware error:', error);
|
|
216
|
+
return c.json({
|
|
217
|
+
data: null,
|
|
218
|
+
error: [{ field: 'file', message: 'File upload processing failed', type: 'body' }]
|
|
219
|
+
}, 422);
|
|
220
|
+
}
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
// Register route handlers with Hono, now generic over TDef
|
|
224
|
+
function registerHonoRouteHandlers(app, apiDefinition, routeHandlers, middlewares) {
|
|
225
|
+
routeHandlers.forEach((specificHandlerIterationItem) => {
|
|
226
|
+
const { domain, routeKey, handler } = specificHandlerIterationItem;
|
|
227
|
+
const currentDomain = domain;
|
|
228
|
+
const currentRouteKey = routeKey;
|
|
229
|
+
const routeDefinition = apiDefinition.endpoints[currentDomain][currentRouteKey];
|
|
230
|
+
if (!routeDefinition) {
|
|
231
|
+
console.error(`Route definition not found for domain "${String(currentDomain)}" and routeKey "${String(currentRouteKey)}"`);
|
|
232
|
+
return;
|
|
233
|
+
}
|
|
234
|
+
const { path, method } = routeDefinition;
|
|
235
|
+
// Apply prefix from API definition if it exists
|
|
236
|
+
const fullPath = apiDefinition.prefix
|
|
237
|
+
? `${apiDefinition.prefix.startsWith('/') ? apiDefinition.prefix : `/${apiDefinition.prefix}`}${path}`.replace(/\/+/g, '/')
|
|
238
|
+
: path;
|
|
239
|
+
const honoMiddleware = async (c) => {
|
|
240
|
+
try {
|
|
241
|
+
// Parse and validate request
|
|
242
|
+
const parsedParams = ('params' in routeDefinition && routeDefinition.params)
|
|
243
|
+
? routeDefinition.params.parse(c.req.param())
|
|
244
|
+
: c.req.param();
|
|
245
|
+
const preprocessedQuery = ('query' in routeDefinition && routeDefinition.query)
|
|
246
|
+
? preprocessQueryParams(c.req.query(), routeDefinition.query)
|
|
247
|
+
: c.req.query();
|
|
248
|
+
const parsedQuery = ('query' in routeDefinition && routeDefinition.query)
|
|
249
|
+
? routeDefinition.query.parse(preprocessedQuery)
|
|
250
|
+
: preprocessedQuery;
|
|
251
|
+
let parsedBody = undefined;
|
|
252
|
+
if (method === 'POST' || method === 'PUT' || method === 'DELETE' || method === 'PATCH') {
|
|
253
|
+
if ('body' in routeDefinition && routeDefinition.body) {
|
|
254
|
+
// For JSON requests
|
|
255
|
+
if (c.req.header('content-type')?.includes('application/json')) {
|
|
256
|
+
parsedBody = routeDefinition.body.parse(await c.req.json());
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// For form data or other body types
|
|
260
|
+
parsedBody = routeDefinition.body.parse(await c.req.parseBody());
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
parsedBody = await c.req.parseBody();
|
|
265
|
+
}
|
|
266
|
+
}
|
|
267
|
+
// Attach parsed data to context
|
|
268
|
+
c.params = parsedParams;
|
|
269
|
+
c.query = parsedQuery;
|
|
270
|
+
c.body = parsedBody;
|
|
271
|
+
// Add respond method to context
|
|
272
|
+
c.respond = (status, data) => {
|
|
273
|
+
const responseSchema = routeDefinition.responses[status];
|
|
274
|
+
if (!responseSchema) {
|
|
275
|
+
console.error(`No response schema defined for status ${status} in route ${String(currentDomain)}/${String(currentRouteKey)}`);
|
|
276
|
+
c.__response = c.json({
|
|
277
|
+
data: null,
|
|
278
|
+
error: [{ field: "general", type: "general", message: "Internal server error: Undefined response schema for status." }]
|
|
279
|
+
}, 500);
|
|
280
|
+
return;
|
|
281
|
+
}
|
|
282
|
+
let responseBody;
|
|
283
|
+
if (status === 422) {
|
|
284
|
+
responseBody = {
|
|
285
|
+
data: null,
|
|
286
|
+
error: data
|
|
287
|
+
};
|
|
288
|
+
}
|
|
289
|
+
else {
|
|
290
|
+
// Always use unified response format since CreateResponses wraps all schemas
|
|
291
|
+
responseBody = {
|
|
292
|
+
data: data,
|
|
293
|
+
error: null
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
const validationResult = responseSchema.safeParse(responseBody);
|
|
297
|
+
if (validationResult.success) {
|
|
298
|
+
// Handle 204 responses specially - they must not have a body
|
|
299
|
+
if (status === 204) {
|
|
300
|
+
c.__response = new Response(null, { status: status });
|
|
301
|
+
}
|
|
302
|
+
else {
|
|
303
|
+
c.__response = c.json(validationResult.data, status);
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
else {
|
|
307
|
+
console.error(`FATAL: Constructed response body failed Zod validation for status ${status} in route ${String(currentDomain)}/${String(currentRouteKey)}.`, validationResult.error.issues, 'Expected schema shape:', responseSchema._def?.shape, 'Provided data:', data, 'Constructed response body:', responseBody);
|
|
308
|
+
c.__response = c.json({
|
|
309
|
+
data: null,
|
|
310
|
+
error: [{ field: "general", type: "general", message: "Internal server error: Constructed response failed validation." }]
|
|
311
|
+
}, 500);
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
// Create Express-like req/res objects for handler compatibility
|
|
315
|
+
const fakeReq = {
|
|
316
|
+
params: parsedParams,
|
|
317
|
+
query: parsedQuery,
|
|
318
|
+
body: parsedBody,
|
|
319
|
+
file: c.file,
|
|
320
|
+
files: c.files,
|
|
321
|
+
headers: c.req.header(),
|
|
322
|
+
ip: c.req.header('CF-Connecting-IP') || '127.0.0.1',
|
|
323
|
+
method: c.req.method,
|
|
324
|
+
path: c.req.path,
|
|
325
|
+
originalUrl: c.req.url
|
|
326
|
+
};
|
|
327
|
+
const fakeRes = {
|
|
328
|
+
respond: c.respond
|
|
329
|
+
};
|
|
330
|
+
const specificHandlerFn = handler;
|
|
331
|
+
await specificHandlerFn(fakeReq, fakeRes);
|
|
332
|
+
// Return the response created by the handler
|
|
333
|
+
if (c.__response) {
|
|
334
|
+
return c.__response;
|
|
335
|
+
}
|
|
336
|
+
else {
|
|
337
|
+
console.error('No response was set by the handler');
|
|
338
|
+
return c.json({
|
|
339
|
+
data: null,
|
|
340
|
+
error: [{ field: "general", type: "general", message: "Internal server error: No response set by handler." }]
|
|
341
|
+
}, 500);
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
catch (error) {
|
|
345
|
+
if (error instanceof zod_1.z.ZodError) {
|
|
346
|
+
const mappedErrors = error.issues.map(err => {
|
|
347
|
+
let errorType = 'general';
|
|
348
|
+
const pathZero = String(err.path[0]);
|
|
349
|
+
if (pathZero === 'params')
|
|
350
|
+
errorType = 'param';
|
|
351
|
+
else if (pathZero === 'query')
|
|
352
|
+
errorType = 'query';
|
|
353
|
+
else if (pathZero === 'body')
|
|
354
|
+
errorType = 'body';
|
|
355
|
+
return {
|
|
356
|
+
field: err.path.join('.') || 'request',
|
|
357
|
+
message: err.message,
|
|
358
|
+
type: errorType,
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
const errorResponseBody = { data: null, error: mappedErrors };
|
|
362
|
+
const schema422 = routeDefinition.responses[422];
|
|
363
|
+
if (schema422) {
|
|
364
|
+
const validationResult = schema422.safeParse(errorResponseBody);
|
|
365
|
+
if (validationResult.success) {
|
|
366
|
+
return c.json(validationResult.data, 422);
|
|
367
|
+
}
|
|
368
|
+
else {
|
|
369
|
+
console.error("FATAL: Constructed 422 error response failed its own schema validation.", validationResult.error.issues);
|
|
370
|
+
return c.json({ error: [{ field: "general", type: "general", message: "Internal server error constructing validation error response." }] }, 500);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
else {
|
|
374
|
+
console.error("Error: 422 schema not found for route, sending raw Zod errors.");
|
|
375
|
+
return c.json({ error: mappedErrors }, 422);
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
else if (error instanceof Error) {
|
|
379
|
+
console.error(`Error in ${method} ${path}:`, error.message);
|
|
380
|
+
return c.json({ error: [{ field: "general", type: "general", message: 'Internal server error' }] }, 500);
|
|
381
|
+
}
|
|
382
|
+
else {
|
|
383
|
+
console.error(`Unknown error in ${method} ${path}:`, error);
|
|
384
|
+
return c.json({ error: [{ field: "general", type: "general", message: 'An unknown error occurred' }] }, 500);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
};
|
|
388
|
+
// Create middleware wrappers
|
|
389
|
+
const middlewareWrappers = [];
|
|
390
|
+
// Add file upload middleware if configured
|
|
391
|
+
if (routeDefinition.fileUpload) {
|
|
392
|
+
try {
|
|
393
|
+
const fileUploadMiddleware = createHonoFileUploadMiddleware(routeDefinition.fileUpload);
|
|
394
|
+
middlewareWrappers.push(fileUploadMiddleware);
|
|
395
|
+
}
|
|
396
|
+
catch (error) {
|
|
397
|
+
console.error(`Error creating file upload middleware for ${currentDomain}.${currentRouteKey}:`, error);
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
if (middlewares && middlewares.length > 0) {
|
|
402
|
+
middlewares.forEach(middleware => {
|
|
403
|
+
const wrappedMiddleware = async (c, next) => {
|
|
404
|
+
try {
|
|
405
|
+
await middleware(c.req, c.res, next, { domain: currentDomain, routeKey: currentRouteKey });
|
|
406
|
+
}
|
|
407
|
+
catch (error) {
|
|
408
|
+
console.error('Middleware error:', error);
|
|
409
|
+
return next();
|
|
410
|
+
}
|
|
411
|
+
};
|
|
412
|
+
middlewareWrappers.push(wrappedMiddleware);
|
|
413
|
+
});
|
|
414
|
+
}
|
|
415
|
+
// Register route with middlewares
|
|
416
|
+
const allHandlers = [...middlewareWrappers, honoMiddleware];
|
|
417
|
+
// Register with Hono
|
|
418
|
+
switch (method.toUpperCase()) {
|
|
419
|
+
case 'GET':
|
|
420
|
+
app.get(fullPath, ...allHandlers);
|
|
421
|
+
break;
|
|
422
|
+
case 'POST':
|
|
423
|
+
app.post(fullPath, ...allHandlers);
|
|
424
|
+
break;
|
|
425
|
+
case 'PATCH':
|
|
426
|
+
app.patch(fullPath, ...allHandlers);
|
|
427
|
+
break;
|
|
428
|
+
case 'PUT':
|
|
429
|
+
app.put(fullPath, ...allHandlers);
|
|
430
|
+
break;
|
|
431
|
+
case 'DELETE':
|
|
432
|
+
app.delete(fullPath, ...allHandlers);
|
|
433
|
+
break;
|
|
434
|
+
default:
|
|
435
|
+
console.warn(`Unsupported HTTP method: ${method} for path ${fullPath}`);
|
|
436
|
+
}
|
|
437
|
+
});
|
|
438
|
+
}
|
|
439
|
+
// Transform object-based handlers to array format
|
|
440
|
+
function transformObjectHandlersToArray(objectHandlers) {
|
|
441
|
+
const handlerArray = [];
|
|
442
|
+
for (const domain in objectHandlers) {
|
|
443
|
+
if (Object.prototype.hasOwnProperty.call(objectHandlers, domain)) {
|
|
444
|
+
const domainHandlers = objectHandlers[domain];
|
|
445
|
+
for (const routeKey in domainHandlers) {
|
|
446
|
+
if (Object.prototype.hasOwnProperty.call(domainHandlers, routeKey)) {
|
|
447
|
+
const handler = domainHandlers[routeKey];
|
|
448
|
+
handlerArray.push({
|
|
449
|
+
domain,
|
|
450
|
+
routeKey,
|
|
451
|
+
handler
|
|
452
|
+
});
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
return handlerArray;
|
|
458
|
+
}
|
|
459
|
+
// Main utility function that registers object-based handlers with Hono
|
|
460
|
+
function RegisterHonoHandlers(app, apiDefinition, objectHandlers, middlewares) {
|
|
461
|
+
const handlerArray = transformObjectHandlersToArray(objectHandlers);
|
|
462
|
+
// Convert AnyMiddleware to EndpointMiddleware by checking function arity
|
|
463
|
+
const endpointMiddlewares = middlewares?.map(middleware => {
|
|
464
|
+
if (middleware.length === 4) {
|
|
465
|
+
return middleware;
|
|
466
|
+
}
|
|
467
|
+
else {
|
|
468
|
+
return ((req, res, next) => {
|
|
469
|
+
return middleware(req, res, next);
|
|
470
|
+
});
|
|
471
|
+
}
|
|
472
|
+
}) || [];
|
|
473
|
+
registerHonoRouteHandlers(app, apiDefinition, handlerArray, endpointMiddlewares);
|
|
474
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -5,3 +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';
|
package/dist/index.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.ZodSchema = exports.RegisterHandlers = exports.CreateResponses = exports.CreateApiDefinition = exports.generateOpenApiSpec2 = exports.generateOpenApiSpec = exports.FetchHttpClientAdapter = exports.ApiClient = void 0;
|
|
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;
|
|
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; } });
|
|
@@ -15,3 +15,8 @@ var object_handlers_1 = require("./object-handlers");
|
|
|
15
15
|
Object.defineProperty(exports, "RegisterHandlers", { enumerable: true, get: function () { return object_handlers_1.RegisterHandlers; } });
|
|
16
16
|
var zod_1 = require("zod");
|
|
17
17
|
Object.defineProperty(exports, "ZodSchema", { enumerable: true, get: function () { return zod_1.z; } });
|
|
18
|
+
// Hono adapter for Cloudflare Workers
|
|
19
|
+
var hono_cloudflare_workers_1 = require("./hono-cloudflare-workers");
|
|
20
|
+
Object.defineProperty(exports, "RegisterHonoHandlers", { enumerable: true, get: function () { return hono_cloudflare_workers_1.RegisterHonoHandlers; } });
|
|
21
|
+
Object.defineProperty(exports, "registerHonoRouteHandlers", { enumerable: true, get: function () { return hono_cloudflare_workers_1.registerHonoRouteHandlers; } });
|
|
22
|
+
Object.defineProperty(exports, "honoFileSchema", { enumerable: true, get: function () { return hono_cloudflare_workers_1.honoFileSchema; } });
|