@thinkbun/middleware 1.0.0

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.md ADDED
@@ -0,0 +1,290 @@
1
+ # ThinkBun Middleware Collection
2
+
3
+ A comprehensive set of middleware for ThinkBun framework, following Koa middleware specification and supporting async/await syntax and onion model execution flow.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ bun add @thinkbun/middleware
9
+ ```
10
+
11
+ ## Available Middleware
12
+
13
+ ### 1. Error Handler
14
+
15
+ Global error handling middleware that captures and processes errors thrown by subsequent middleware, with support for different error information display in development and production environments.
16
+
17
+ #### Usage
18
+
19
+ ```typescript
20
+ import { errorHandler } from '@thinkbun/middleware';
21
+
22
+ // Basic usage
23
+ app.use(errorHandler());
24
+
25
+ // With custom options
26
+ app.use(errorHandler({
27
+ exposeStackTrace: true, // Show stack trace in error response (only recommended for development)
28
+ customHandler: (error, ctx) => {
29
+ // Custom error handling logic
30
+ return new Response(JSON.stringify({ error: error.message }), {
31
+ status: (error as any).status || 500,
32
+ headers: { 'Content-Type': 'application/json' }
33
+ });
34
+ },
35
+ logger: (error) => {
36
+ // Custom error logging
37
+ console.error('Error occurred:', error);
38
+ }
39
+ }));
40
+ ```
41
+
42
+ #### Options
43
+
44
+ | Option | Type | Default | Description |
45
+ | ------------------ | ------------------------------------------ | ------- | ----------------------------------------------- |
46
+ | `exposeStackTrace` | `boolean` | `false` | Whether to expose stack trace in error response |
47
+ | `customHandler` | `(error: Error, ctx: Context) => Response` | - | Custom error handling function |
48
+ | `logger` | `(error: Error, ctx: Context) => void` | - | Custom error logging function |
49
+
50
+ ### 2. Logger
51
+
52
+ Request logging middleware that records request and response information, with support for custom log formats, path filtering, and loggers.
53
+
54
+ #### Usage
55
+
56
+ ```typescript
57
+ import { logger } from '@thinkbun/middleware';
58
+
59
+ // Basic usage
60
+ app.use(logger());
61
+
62
+ // With custom options
63
+ app.use(logger({
64
+ format: (ctx) => `${ctx.method} ${ctx.path} ${ctx.header('user-agent')}`,
65
+ logger: (message) => {
66
+ // Custom logging
67
+ console.log('[LOG]', message);
68
+ },
69
+ ignorePaths: ['/health', '/metrics'] // Paths to exclude from logging
70
+ }));
71
+ ```
72
+
73
+ #### Options
74
+
75
+ | Option | Type | Default | Description |
76
+ | ------------- | --------------------------- | ------------- | -------------------------------------- |
77
+ | `format` | `(ctx: Context) => string` | - | Custom log format function |
78
+ | `logger` | `(message: string) => void` | `console.log` | Custom logger function |
79
+ | `ignorePaths` | `string[]` | `[]` | Array of paths to exclude from logging |
80
+
81
+ ### 3. Validator
82
+
83
+ Request parameter validation middleware that validates request body, query parameters, and route parameters using Ajv schema validation.
84
+
85
+ #### Usage
86
+
87
+ ```typescript
88
+ import { validator } from '@thinkbun/middleware';
89
+
90
+ app.use(validator({
91
+ rules: [
92
+ {
93
+ path: '/api/users',
94
+ method: 'POST',
95
+ schema: {
96
+ body: {
97
+ type: 'object',
98
+ properties: {
99
+ name: { type: 'string' },
100
+ email: { type: 'string', format: 'email' },
101
+ age: { type: 'number', minimum: 18 }
102
+ },
103
+ required: ['name', 'email']
104
+ },
105
+ query: {
106
+ type: 'object',
107
+ properties: {
108
+ active: { type: 'boolean' }
109
+ }
110
+ }
111
+ }
112
+ },
113
+ {
114
+ path: '/api/users/:id',
115
+ method: 'GET',
116
+ schema: {
117
+ params: {
118
+ type: 'object',
119
+ properties: {
120
+ id: { type: 'string', pattern: '^\\d+$' }
121
+ },
122
+ required: ['id']
123
+ }
124
+ }
125
+ }
126
+ ],
127
+ errorHandler: (errors, ctx) => {
128
+ // Custom validation error handling
129
+ ctx.throw(400, `Validation error: ${errors[0].message}`);
130
+ }
131
+ }));
132
+ ```
133
+
134
+ #### Options
135
+
136
+ | Option | Type | Default | Description |
137
+ | -------------- | --------------------------------------- | ------- | ----------------------------------------- |
138
+ | `rules` | `ValidationRule[]` | - | Array of validation rules |
139
+ | `errorHandler` | `(errors: any[], ctx: Context) => void` | - | Custom validation error handling function |
140
+
141
+ #### ValidationRule Interface
142
+
143
+ ```typescript
144
+ interface ValidationRule {
145
+ path: string; // Request path pattern to match
146
+ method: string; // HTTP method to match (GET, POST, etc.)
147
+ schema: {
148
+ body?: any; // Request body schema
149
+ query?: any; // Query parameters schema
150
+ params?: any; // Route parameters schema
151
+ };
152
+ }
153
+ ```
154
+
155
+ ### 4. CORS
156
+
157
+ CORS configuration middleware that handles Cross-Origin Resource Sharing, supporting flexible configuration options.
158
+
159
+ #### Usage
160
+
161
+ ```typescript
162
+ import { cors } from '@thinkbun/middleware';
163
+
164
+ // Basic usage (default allows all origins)
165
+ app.use(cors());
166
+
167
+ // With custom options
168
+ app.use(cors({
169
+ origin: ['https://example.com', 'https://sub.example.com'], // Allow specific origins
170
+ methods: ['GET', 'POST', 'PUT', 'DELETE'], // Allowed HTTP methods
171
+ allowedHeaders: ['Content-Type', 'Authorization'], // Allowed request headers
172
+ exposedHeaders: ['X-Custom-Header'], // Headers exposed to client
173
+ credentials: true, // Allow credentials
174
+ maxAge: 86400, // Preflight request cache time (seconds)
175
+ preflightContinue: true // Continue processing preflight requests
176
+ }));
177
+ ```
178
+
179
+ #### Options
180
+
181
+ | Option | Type | Default | Description |
182
+ | ------------------- | ----------------------------------------------------- | --------------------------------------------------- | -------------------------------------------------------------------------- |
183
+ | `origin` | `string \| string[] \| ((origin: string) => boolean)` | `'*'` | Allowed origins |
184
+ | `methods` | `string[]` | `['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']` | Allowed HTTP methods |
185
+ | `allowedHeaders` | `string[]` | - | Allowed request headers (defaults to Access-Control-Request-Headers value) |
186
+ | `exposedHeaders` | `string[]` | - | Headers exposed to client |
187
+ | `credentials` | `boolean` | `false` | Whether to allow credentials |
188
+ | `maxAge` | `number` | `86400` (24 hours) | Preflight request cache time (seconds) |
189
+ | `preflightContinue` | `boolean` | `true` | Whether to continue processing preflight requests |
190
+
191
+ ### 5. Static Serve
192
+
193
+ Static file serving middleware that serves static files from the specified directory, with support for request path prefixes, cache control, and more.
194
+
195
+ #### Usage
196
+
197
+ ```typescript
198
+ import { staticServe } from '@thinkbun/middleware';
199
+
200
+ // Basic usage
201
+ app.use(staticServe({ root: './public' }));
202
+
203
+ // With custom options
204
+ app.use(staticServe({
205
+ root: './public', // Static file directory
206
+ prefix: '/static', // Request path prefix (e.g., /static/css/style.css)
207
+ index: 'index.html', // Default index file
208
+ maxAge: 3600, // Cache control max-age (seconds)
209
+ gzip: true, // Enable gzip compression
210
+ brotli: true, // Enable Brotli compression
211
+ cacheControl: true // Enable cache control headers
212
+ }));
213
+ ```
214
+
215
+ #### Options
216
+
217
+ | Option | Type | Default | Description |
218
+ | -------------- | --------- | -------------- | -------------------------------- |
219
+ | `root` | `string` | - | Static file directory (required) |
220
+ | `prefix` | `string` | `''` | Request path prefix |
221
+ | `index` | `string` | `'index.html'` | Default index file |
222
+ | `maxAge` | `number` | `3600` | Cache control max-age (seconds) |
223
+ | `gzip` | `boolean` | `true` | Enable gzip compression |
224
+ | `brotli` | `boolean` | `true` | Enable Brotli compression |
225
+ | `cacheControl` | `boolean` | `true` | Enable cache control headers |
226
+
227
+ ## Using All Middleware
228
+
229
+ You can also import all middleware at once:
230
+
231
+ ```typescript
232
+ import { middlewares } from '@thinkbun/middleware';
233
+
234
+ // Use all middleware
235
+ app.use(middlewares.errorHandler());
236
+ app.use(middlewares.logger());
237
+ app.use(middlewares.cors());
238
+ app.use(middlewares.staticServe({ root: './public' }));
239
+ ```
240
+
241
+ ## Middleware Execution Flow
242
+
243
+ The middleware follows the Koa onion model, where each middleware can execute code before and after subsequent middleware:
244
+
245
+ ```typescript
246
+ app.use(async (ctx, next) => {
247
+ // Code executed before next middleware
248
+ console.log('Middleware 1: before');
249
+
250
+ const response = await next();
251
+
252
+ // Code executed after next middleware
253
+ console.log('Middleware 1: after');
254
+
255
+ return response;
256
+ });
257
+
258
+ app.use(async (ctx, next) => {
259
+ console.log('Middleware 2: before');
260
+
261
+ const response = await next();
262
+
263
+ console.log('Middleware 2: after');
264
+
265
+ return response;
266
+ });
267
+
268
+ // Output when handling a request:
269
+ // Middleware 1: before
270
+ // Middleware 2: before
271
+ // (Request handling)
272
+ // Middleware 2: after
273
+ // Middleware 1: after
274
+ ```
275
+
276
+ ## Best Practices
277
+
278
+ 1. **Order Matters**: Middleware execution follows the order in which they are added with `app.use()`. Place global middleware (like error handler, logger, CORS) before route-specific middleware.
279
+
280
+ 2. **Error Handler Placement**: Always place the error handler middleware first to ensure it can capture errors from all subsequent middleware.
281
+
282
+ 3. **Static Files**: Place the static file serving middleware before route handlers to improve performance (static files are served directly without going through route processing).
283
+
284
+ 4. **Validation Scope**: Use the validator middleware with specific paths and methods to avoid unnecessary validation overhead.
285
+
286
+ 5. **Environment-Specific Configuration**: Configure middleware differently for development and production environments (e.g., expose stack trace only in development).
287
+
288
+ ## License
289
+
290
+ MIT
package/package.json ADDED
@@ -0,0 +1,19 @@
1
+ {
2
+ "name": "@thinkbun/middleware",
3
+ "version": "1.0.0",
4
+ "module": "src/index.ts",
5
+ "type": "module",
6
+ "devDependencies": {
7
+ "@types/bun": "latest"
8
+ },
9
+ "peerDependencies": {
10
+ "typescript": "^5"
11
+ },
12
+ "dependencies": {
13
+ "picocolors": "^1.1.1",
14
+ "@thinkbun/core": "workspace:*"
15
+ },
16
+ "publishConfig": {
17
+ "access": "public"
18
+ }
19
+ }
package/src/index.ts ADDED
@@ -0,0 +1,32 @@
1
+ import { cors as _cors, type CorsOptions } from './middlewares/cors';
2
+ // 首先导入所有中间件
3
+ import { errorHandler as _errorHandler, type ErrorHandlerOptions } from './middlewares/errorHandler';
4
+ import { logger as _logger, type LoggerOptions } from './middlewares/logger';
5
+ import { staticServe as _staticServe, type StaticOptions } from './middlewares/static';
6
+ import { validator as _validator, type ValidatorOptions, type ValidationRule } from './middlewares/validator';
7
+
8
+ // 重新导出各个中间件
9
+ export { _errorHandler as errorHandler };
10
+ export type { ErrorHandlerOptions };
11
+ export { _logger as logger };
12
+ export type { LoggerOptions };
13
+ export { _validator as validator };
14
+ export type { ValidatorOptions, ValidationRule };
15
+ export { _cors as cors };
16
+ export type { CorsOptions };
17
+ export { _staticServe as staticServe };
18
+ export type { StaticOptions };
19
+
20
+ // 导出所有中间件的类型
21
+ export type { Middleware, Context } from '@thinkbun/core';
22
+
23
+ // 导出所有中间件的集合
24
+ export const middlewares = {
25
+ errorHandler: _errorHandler,
26
+ logger: _logger,
27
+ validator: _validator,
28
+ cors: _cors,
29
+ staticServe: _staticServe,
30
+ };
31
+
32
+ export default middlewares;
@@ -0,0 +1,182 @@
1
+ import type { Context, Middleware } from '@thinkbun/core';
2
+
3
+ export interface CorsOptions {
4
+ /**
5
+ * 允许的源
6
+ * @default '*'
7
+ * @example '*' | 'https://example.com' | ['https://example.com', 'https://sub.example.com'] | (origin: string) => boolean
8
+ */
9
+ origin?: string | string[] | ((origin: string) => boolean);
10
+ /**
11
+ * 允许的HTTP方法
12
+ * @default ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH']
13
+ */
14
+ methods?: string[];
15
+ /**
16
+ * 允许的请求头
17
+ * @default 请求头中的Access-Control-Request-Headers值
18
+ */
19
+ allowedHeaders?: string[];
20
+ /**
21
+ * 允许客户端访问的响应头
22
+ */
23
+ exposedHeaders?: string[];
24
+ /**
25
+ * 是否允许凭证
26
+ * @default false
27
+ */
28
+ credentials?: boolean;
29
+ /**
30
+ * 预检请求的缓存时间(秒)
31
+ * @default 86400 (24小时)
32
+ */
33
+ maxAge?: number;
34
+ /**
35
+ * 是否处理预检请求
36
+ * @default true
37
+ */
38
+ preflightContinue?: boolean;
39
+ }
40
+
41
+ export function cors(options: CorsOptions = {}): Middleware {
42
+ const {
43
+ origin = '*',
44
+ methods = ['GET', 'HEAD', 'PUT', 'POST', 'DELETE', 'PATCH'],
45
+ allowedHeaders,
46
+ exposedHeaders,
47
+ credentials = false,
48
+ maxAge = 86400,
49
+ preflightContinue = true,
50
+ } = options;
51
+
52
+ // 格式化methods为字符串
53
+ const methodsStr = methods.join(', ').toUpperCase();
54
+
55
+ // 检查origin是否允许
56
+ const checkOrigin = (originHeader: string | null): string | false => {
57
+ if (!originHeader) return false;
58
+
59
+ if (origin === '*') {
60
+ return origin;
61
+ }
62
+
63
+ if (typeof origin === 'string') {
64
+ return originHeader === origin ? origin : false;
65
+ }
66
+
67
+ if (Array.isArray(origin)) {
68
+ return origin.includes(originHeader) ? originHeader : false;
69
+ }
70
+
71
+ if (typeof origin === 'function') {
72
+ return origin(originHeader) ? originHeader : false;
73
+ }
74
+
75
+ return false;
76
+ };
77
+
78
+ return async (ctx: Context, next: () => Promise<Response | Context | void>): Promise<Response | Context | void> => {
79
+ // 处理OPTIONS预检请求
80
+ if (ctx.method === 'OPTIONS') {
81
+ const originHeader = ctx.header('Origin');
82
+ const allowedOrigin = checkOrigin(originHeader);
83
+
84
+ if (!allowedOrigin) {
85
+ // 不允许的源,直接返回403
86
+ return new Response('Forbidden', { status: 403 });
87
+ }
88
+
89
+ // 构建预检响应
90
+ const preflightResponse = new Response(null, {
91
+ status: 204,
92
+ headers: {
93
+ 'Access-Control-Allow-Origin': allowedOrigin,
94
+ 'Access-Control-Allow-Methods': methodsStr,
95
+ 'Access-Control-Allow-Headers':
96
+ allowedHeaders?.join(', ') || ctx.header('Access-Control-Request-Headers') || '',
97
+ 'Access-Control-Max-Age': maxAge.toString(),
98
+ ...(credentials && { 'Access-Control-Allow-Credentials': 'true' }),
99
+ },
100
+ });
101
+
102
+ if (!preflightContinue) {
103
+ return preflightResponse;
104
+ }
105
+
106
+ // 调用next()获取响应
107
+ const nextResponse = await next();
108
+
109
+ // 如果next()返回了Response对象,将CORS头合并到该对象
110
+ if (nextResponse instanceof Response) {
111
+ const headers = new Headers(nextResponse.headers);
112
+
113
+ // 复制preflightResponse的所有头到新响应
114
+ preflightResponse.headers.forEach((value, name) => {
115
+ headers.set(name, value);
116
+ });
117
+
118
+ return new Response(nextResponse.body, {
119
+ status: nextResponse.status,
120
+ statusText: nextResponse.statusText,
121
+ headers,
122
+ });
123
+ } else if (nextResponse === undefined) {
124
+ // 如果next()返回undefined,直接返回preflightResponse
125
+ return preflightResponse;
126
+ }
127
+
128
+ // 否则,将CORS头设置到上下文的responseHeaders
129
+ ctx.responseHeaders = preflightResponse.headers;
130
+ return nextResponse;
131
+ }
132
+
133
+ // 处理正常请求
134
+ const originHeader = ctx.header('Origin');
135
+ const allowedOrigin = checkOrigin(originHeader);
136
+
137
+ // 如果没有Origin头或不允许的源,直接继续执行
138
+ if (!originHeader || !allowedOrigin) {
139
+ return await next();
140
+ }
141
+
142
+ // 执行后续中间件
143
+ const response = await next();
144
+
145
+ // 设置CORS响应头
146
+ const setCorsHeaders = (res: Response) => {
147
+ const headers = new Headers(res.headers);
148
+ headers.set('Access-Control-Allow-Origin', allowedOrigin);
149
+
150
+ if (credentials) {
151
+ headers.set('Access-Control-Allow-Credentials', 'true');
152
+ }
153
+
154
+ if (exposedHeaders && exposedHeaders.length > 0) {
155
+ headers.set('Access-Control-Expose-Headers', exposedHeaders.join(', '));
156
+ }
157
+
158
+ return new Response(res.body, {
159
+ status: res.status,
160
+ statusText: res.statusText,
161
+ headers,
162
+ });
163
+ };
164
+
165
+ if (response instanceof Response) {
166
+ return setCorsHeaders(response);
167
+ } else {
168
+ // 如果返回的是上下文对象或undefined,设置响应头
169
+ ctx.responseHeaders.set('Access-Control-Allow-Origin', allowedOrigin);
170
+
171
+ if (credentials) {
172
+ ctx.responseHeaders.set('Access-Control-Allow-Credentials', 'true');
173
+ }
174
+
175
+ if (exposedHeaders && exposedHeaders.length > 0) {
176
+ ctx.responseHeaders.set('Access-Control-Expose-Headers', exposedHeaders.join(', '));
177
+ }
178
+ }
179
+
180
+ return response;
181
+ };
182
+ }
@@ -0,0 +1,88 @@
1
+ import type { Context, Middleware } from '@thinkbun/core';
2
+
3
+ export interface ErrorHandlerOptions {
4
+ /**
5
+ * 是否在响应中包含详细错误信息
6
+ * @default true(开发环境)/ false(生产环境)
7
+ */
8
+ exposeStackTrace?: boolean;
9
+ /**
10
+ * 自定义错误处理函数
11
+ * @param error 捕获到的错误
12
+ * @param ctx 请求上下文
13
+ * @returns 返回自定义的Response对象或undefined使用默认处理
14
+ */
15
+ customHandler?: (error: Error, ctx: Context) => Response | undefined;
16
+ /**
17
+ * 错误日志记录函数
18
+ * @param error 捕获到的错误
19
+ * @param ctx 请求上下文
20
+ */
21
+ logger?: (error: Error, ctx: Context) => void;
22
+ }
23
+
24
+ export function errorHandler(options: ErrorHandlerOptions = {}): Middleware {
25
+ const { exposeStackTrace = process.env.NODE_ENV !== 'production', customHandler, logger = console.error } = options;
26
+
27
+ return async (ctx: Context, next: () => Promise<Response | Context | void>): Promise<Response | Context | void> => {
28
+ try {
29
+ return await next();
30
+ } catch (error) {
31
+ // 记录错误日志
32
+ logger(error as Error, ctx);
33
+
34
+ // 检查是否有自定义错误处理
35
+ if (customHandler) {
36
+ const customResponse = customHandler(error as Error, ctx);
37
+ if (customResponse) {
38
+ return customResponse;
39
+ }
40
+ }
41
+
42
+ // 默认错误处理
43
+ const err = error as Error;
44
+ let status = 500;
45
+ let message = 'Internal Server Error';
46
+
47
+ // 根据错误类型设置状态码
48
+ if (err.name === 'BadRequestError') {
49
+ status = 400;
50
+ message = err.message || 'Bad Request';
51
+ } else if (err.name === 'UnauthorizedError') {
52
+ status = 401;
53
+ message = err.message || 'Unauthorized';
54
+ } else if (err.name === 'ForbiddenError') {
55
+ status = 403;
56
+ message = err.message || 'Forbidden';
57
+ } else if (err.name === 'NotFoundError') {
58
+ status = 404;
59
+ message = err.message || 'Not Found';
60
+ } else if (err.name === 'MethodNotAllowedError') {
61
+ status = 405;
62
+ message = err.message || 'Method Not Allowed';
63
+ } else if (err.name === 'ConflictError') {
64
+ status = 409;
65
+ message = err.message || 'Conflict';
66
+ } else if (err.name === 'UnprocessableEntityError') {
67
+ status = 422;
68
+ message = err.message || 'Unprocessable Entity';
69
+ }
70
+
71
+ // 构建错误响应
72
+ const errorResponse = {
73
+ error: message,
74
+ ...(exposeStackTrace && {
75
+ stack: err.stack,
76
+ name: err.name,
77
+ }),
78
+ };
79
+
80
+ return ctx.json(errorResponse, {
81
+ status,
82
+ headers: {
83
+ 'Content-Type': 'application/json',
84
+ },
85
+ });
86
+ }
87
+ };
88
+ }