@dangao/bun-server 1.1.2 → 1.1.3
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/docs/api.md +602 -0
- package/docs/best-practices.md +12 -0
- package/docs/custom-decorators.md +440 -0
- package/docs/deployment.md +447 -0
- package/docs/error-handling.md +462 -0
- package/docs/extensions.md +569 -0
- package/docs/guide.md +634 -0
- package/docs/migration.md +10 -0
- package/docs/performance.md +452 -0
- package/docs/troubleshooting.md +286 -0
- package/docs/zh/api.md +168 -0
- package/docs/zh/best-practices.md +38 -0
- package/docs/zh/custom-decorators.md +466 -0
- package/docs/zh/deployment.md +445 -0
- package/docs/zh/error-handling.md +456 -0
- package/docs/zh/extensions.md +584 -0
- package/docs/zh/guide.md +361 -0
- package/docs/zh/migration.md +86 -0
- package/docs/zh/performance.md +451 -0
- package/docs/zh/troubleshooting.md +279 -0
- package/package.json +4 -3
|
@@ -0,0 +1,440 @@
|
|
|
1
|
+
# Custom Decorators Guide
|
|
2
|
+
|
|
3
|
+
This guide explains how to create custom decorators and interceptors in Bun Server Framework.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Overview](#overview)
|
|
8
|
+
- [Creating Custom Decorators](#creating-custom-decorators)
|
|
9
|
+
- [Creating Interceptors](#creating-interceptors)
|
|
10
|
+
- [Using BaseInterceptor](#using-baseinterceptor)
|
|
11
|
+
- [Accessing Container and Context](#accessing-container-and-context)
|
|
12
|
+
- [Metadata System](#metadata-system)
|
|
13
|
+
- [Examples](#examples)
|
|
14
|
+
|
|
15
|
+
## Overview
|
|
16
|
+
|
|
17
|
+
Bun Server Framework provides a powerful interceptor mechanism that allows you to create custom decorators and interceptors for AOP (Aspect-Oriented Programming). This enables you to add cross-cutting concerns like caching, logging, permission checking, and more.
|
|
18
|
+
|
|
19
|
+
### Key Concepts
|
|
20
|
+
|
|
21
|
+
- **Decorator**: A TypeScript decorator that adds metadata to methods
|
|
22
|
+
- **Interceptor**: A class that intercepts method execution and can modify behavior
|
|
23
|
+
- **Metadata Key**: A Symbol used to link decorators with interceptors
|
|
24
|
+
- **Interceptor Registry**: A central registry that manages all interceptors
|
|
25
|
+
|
|
26
|
+
## Creating Custom Decorators
|
|
27
|
+
|
|
28
|
+
### Basic Decorator Pattern
|
|
29
|
+
|
|
30
|
+
A custom decorator is a function that returns a `MethodDecorator`. It uses `reflect-metadata` to store metadata on the method.
|
|
31
|
+
|
|
32
|
+
```typescript
|
|
33
|
+
import 'reflect-metadata';
|
|
34
|
+
|
|
35
|
+
// 1. Define a metadata key (use Symbol for uniqueness)
|
|
36
|
+
export const MY_METADATA_KEY = Symbol('@my-app:my-decorator');
|
|
37
|
+
|
|
38
|
+
// 2. Define metadata type
|
|
39
|
+
export interface MyDecoratorOptions {
|
|
40
|
+
option1: string;
|
|
41
|
+
option2?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
// 3. Create the decorator function
|
|
45
|
+
export function MyDecorator(options: MyDecoratorOptions): MethodDecorator {
|
|
46
|
+
return (target, propertyKey, descriptor) => {
|
|
47
|
+
// Store metadata on the method
|
|
48
|
+
Reflect.defineMetadata(MY_METADATA_KEY, options, target, propertyKey);
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
### Decorator Naming Conventions
|
|
54
|
+
|
|
55
|
+
- Use PascalCase for decorator names: `@Cache()`, `@Permission()`, `@Log()`
|
|
56
|
+
- Use descriptive names that indicate the decorator's purpose
|
|
57
|
+
- Metadata keys should follow the pattern: `Symbol('@namespace:feature')`
|
|
58
|
+
|
|
59
|
+
### Decorator Function Signature
|
|
60
|
+
|
|
61
|
+
```typescript
|
|
62
|
+
function MyDecorator(options?: MyOptions): MethodDecorator {
|
|
63
|
+
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
|
|
64
|
+
// Implementation
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
## Creating Interceptors
|
|
70
|
+
|
|
71
|
+
### Implementing the Interceptor Interface
|
|
72
|
+
|
|
73
|
+
An interceptor must implement the `Interceptor` interface:
|
|
74
|
+
|
|
75
|
+
```typescript
|
|
76
|
+
import type { Interceptor } from '@dangao/bun-server';
|
|
77
|
+
import type { Container } from '@dangao/bun-server';
|
|
78
|
+
import type { Context } from '@dangao/bun-server';
|
|
79
|
+
|
|
80
|
+
class MyInterceptor implements Interceptor {
|
|
81
|
+
public async execute<T>(
|
|
82
|
+
target: unknown,
|
|
83
|
+
propertyKey: string | symbol,
|
|
84
|
+
originalMethod: (...args: unknown[]) => T | Promise<T>,
|
|
85
|
+
args: unknown[],
|
|
86
|
+
container: Container,
|
|
87
|
+
context?: Context,
|
|
88
|
+
): Promise<T> {
|
|
89
|
+
// Pre-processing logic
|
|
90
|
+
console.log(`Executing ${String(propertyKey)}`);
|
|
91
|
+
|
|
92
|
+
// Execute the original method
|
|
93
|
+
const result = await Promise.resolve(originalMethod.apply(target, args));
|
|
94
|
+
|
|
95
|
+
// Post-processing logic
|
|
96
|
+
console.log(`Completed ${String(propertyKey)}`);
|
|
97
|
+
|
|
98
|
+
return result;
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Registering Interceptors
|
|
104
|
+
|
|
105
|
+
Interceptors must be registered with the `InterceptorRegistry`:
|
|
106
|
+
|
|
107
|
+
```typescript
|
|
108
|
+
import { Application } from '@dangao/bun-server';
|
|
109
|
+
import { INTERCEPTOR_REGISTRY_TOKEN } from '@dangao/bun-server';
|
|
110
|
+
import type { InterceptorRegistry } from '@dangao/bun-server';
|
|
111
|
+
|
|
112
|
+
const app = new Application({ port: 3000 });
|
|
113
|
+
const registry = app.getContainer().resolve<InterceptorRegistry>(INTERCEPTOR_REGISTRY_TOKEN);
|
|
114
|
+
|
|
115
|
+
// Register interceptor with metadata key and priority
|
|
116
|
+
registry.register(MY_METADATA_KEY, new MyInterceptor(), 100);
|
|
117
|
+
```
|
|
118
|
+
|
|
119
|
+
### Priority System
|
|
120
|
+
|
|
121
|
+
Interceptors are executed in order of priority (lower numbers execute first):
|
|
122
|
+
|
|
123
|
+
- Priority 0-50: System interceptors (e.g., transactions)
|
|
124
|
+
- Priority 51-100: Framework interceptors
|
|
125
|
+
- Priority 101+: Application interceptors
|
|
126
|
+
|
|
127
|
+
## Using BaseInterceptor
|
|
128
|
+
|
|
129
|
+
`BaseInterceptor` provides a convenient base class with hooks for common operations:
|
|
130
|
+
|
|
131
|
+
```typescript
|
|
132
|
+
import { BaseInterceptor } from '@dangao/bun-server';
|
|
133
|
+
import type { Container } from '@dangao/bun-server';
|
|
134
|
+
import type { Context } from '@dangao/bun-server';
|
|
135
|
+
|
|
136
|
+
class MyInterceptor extends BaseInterceptor {
|
|
137
|
+
public async execute<T>(
|
|
138
|
+
target: unknown,
|
|
139
|
+
propertyKey: string | symbol,
|
|
140
|
+
originalMethod: (...args: unknown[]) => T | Promise<T>,
|
|
141
|
+
args: unknown[],
|
|
142
|
+
container: Container,
|
|
143
|
+
context?: Context,
|
|
144
|
+
): Promise<T> {
|
|
145
|
+
try {
|
|
146
|
+
// Pre-processing
|
|
147
|
+
await this.before(target, propertyKey, args, container, context);
|
|
148
|
+
|
|
149
|
+
// Execute original method
|
|
150
|
+
const result = await Promise.resolve(originalMethod.apply(target, args));
|
|
151
|
+
|
|
152
|
+
// Post-processing
|
|
153
|
+
return await this.after(target, propertyKey, result, container, context) as T;
|
|
154
|
+
} catch (error) {
|
|
155
|
+
// Error handling
|
|
156
|
+
return await this.onError(target, propertyKey, error, container, context);
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
protected async before(
|
|
161
|
+
target: unknown,
|
|
162
|
+
propertyKey: string | symbol,
|
|
163
|
+
args: unknown[],
|
|
164
|
+
container: Container,
|
|
165
|
+
context?: Context,
|
|
166
|
+
): Promise<void> {
|
|
167
|
+
// Override to add pre-processing logic
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
protected async after<T>(
|
|
171
|
+
target: unknown,
|
|
172
|
+
propertyKey: string | symbol,
|
|
173
|
+
result: T,
|
|
174
|
+
container: Container,
|
|
175
|
+
context?: Context,
|
|
176
|
+
): Promise<T> {
|
|
177
|
+
// Override to add post-processing logic
|
|
178
|
+
return result;
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
protected async onError(
|
|
182
|
+
target: unknown,
|
|
183
|
+
propertyKey: string | symbol,
|
|
184
|
+
error: unknown,
|
|
185
|
+
container: Container,
|
|
186
|
+
context?: Context,
|
|
187
|
+
): Promise<never> {
|
|
188
|
+
// Override to customize error handling
|
|
189
|
+
throw error;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Helper Methods
|
|
195
|
+
|
|
196
|
+
`BaseInterceptor` provides several helper methods:
|
|
197
|
+
|
|
198
|
+
```typescript
|
|
199
|
+
// Get metadata from method
|
|
200
|
+
const metadata = this.getMetadata<MyOptions>(target, propertyKey, MY_METADATA_KEY);
|
|
201
|
+
|
|
202
|
+
// Resolve service from container
|
|
203
|
+
const service = this.resolveService<MyService>(container, MyService);
|
|
204
|
+
|
|
205
|
+
// Access context
|
|
206
|
+
const header = this.getHeader(context!, 'Authorization');
|
|
207
|
+
const query = this.getQuery(context!, 'page');
|
|
208
|
+
const param = this.getParam(context!, 'id');
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
## Accessing Container and Context
|
|
212
|
+
|
|
213
|
+
### Resolving Services from Container
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
class MyInterceptor extends BaseInterceptor {
|
|
217
|
+
public async execute<T>(...): Promise<T> {
|
|
218
|
+
// Resolve service using helper method
|
|
219
|
+
const userService = this.resolveService<UserService>(container, UserService);
|
|
220
|
+
|
|
221
|
+
// Or resolve directly
|
|
222
|
+
const config = container.resolve<ConfigService>(CONFIG_SERVICE_TOKEN);
|
|
223
|
+
|
|
224
|
+
// Use the service
|
|
225
|
+
const user = await userService.find(userId);
|
|
226
|
+
// ...
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
```
|
|
230
|
+
|
|
231
|
+
### Accessing Request Context
|
|
232
|
+
|
|
233
|
+
```typescript
|
|
234
|
+
class MyInterceptor extends BaseInterceptor {
|
|
235
|
+
public async execute<T>(...): Promise<T> {
|
|
236
|
+
if (context) {
|
|
237
|
+
// Get request headers
|
|
238
|
+
const authHeader = this.getHeader(context, 'Authorization');
|
|
239
|
+
const contentType = this.getHeader(context, 'Content-Type');
|
|
240
|
+
|
|
241
|
+
// Get query parameters
|
|
242
|
+
const page = this.getQuery(context, 'page');
|
|
243
|
+
const limit = this.getQuery(context, 'limit');
|
|
244
|
+
|
|
245
|
+
// Get path parameters
|
|
246
|
+
const userId = this.getParam(context, 'id');
|
|
247
|
+
|
|
248
|
+
// Get request body
|
|
249
|
+
const body = await context.getBody();
|
|
250
|
+
|
|
251
|
+
// Set response headers
|
|
252
|
+
context.setHeader('X-Custom-Header', 'value');
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
```
|
|
257
|
+
|
|
258
|
+
## Metadata System
|
|
259
|
+
|
|
260
|
+
### Storing Metadata
|
|
261
|
+
|
|
262
|
+
```typescript
|
|
263
|
+
import 'reflect-metadata';
|
|
264
|
+
|
|
265
|
+
const METADATA_KEY = Symbol('my-metadata');
|
|
266
|
+
|
|
267
|
+
// Store metadata
|
|
268
|
+
Reflect.defineMetadata(METADATA_KEY, { value: 'data' }, target, propertyKey);
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Reading Metadata
|
|
272
|
+
|
|
273
|
+
**Important**: Decorators store metadata on the prototype (class), not on instances. When reading metadata from an interceptor, you need to handle this correctly.
|
|
274
|
+
|
|
275
|
+
**Option 1: Use `BaseInterceptor.getMetadata()` (Recommended)**
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
class MyInterceptor extends BaseInterceptor {
|
|
279
|
+
public async execute<T>(...): Promise<T> {
|
|
280
|
+
// getMetadata() automatically handles prototype chain lookup
|
|
281
|
+
const metadata = this.getMetadata<MyOptions>(target, propertyKey, METADATA_KEY);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
```
|
|
285
|
+
|
|
286
|
+
**Option 2: Manual prototype chain lookup**
|
|
287
|
+
|
|
288
|
+
If you're not using `BaseInterceptor`, you need to manually check the prototype chain:
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
// Read metadata (handles both instance and prototype)
|
|
292
|
+
let metadata: MyOptions | undefined;
|
|
293
|
+
if (typeof target === 'object' && target !== null) {
|
|
294
|
+
// First try direct lookup (if target is prototype)
|
|
295
|
+
metadata = Reflect.getMetadata(METADATA_KEY, target, propertyKey);
|
|
296
|
+
|
|
297
|
+
// If not found and target is an instance, check prototype
|
|
298
|
+
if (metadata === undefined) {
|
|
299
|
+
const prototype = Object.getPrototypeOf(target);
|
|
300
|
+
if (prototype && prototype !== Object.prototype) {
|
|
301
|
+
metadata = Reflect.getMetadata(METADATA_KEY, prototype, propertyKey);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// Also check constructor prototype as fallback
|
|
305
|
+
if (metadata === undefined) {
|
|
306
|
+
const constructor = (target as any).constructor;
|
|
307
|
+
if (constructor && typeof constructor === 'function' && constructor.prototype) {
|
|
308
|
+
metadata = Reflect.getMetadata(METADATA_KEY, constructor.prototype, propertyKey);
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
|
|
314
|
+
// Check if metadata exists
|
|
315
|
+
const exists = metadata !== undefined;
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
### Metadata in Interceptors
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
class MyInterceptor extends BaseInterceptor {
|
|
322
|
+
public async execute<T>(...): Promise<T> {
|
|
323
|
+
// Get metadata using helper method
|
|
324
|
+
// Note: getMetadata() signature is (target, propertyKey, metadataKey)
|
|
325
|
+
const options = this.getMetadata<MyOptions>(target, propertyKey, MY_METADATA_KEY);
|
|
326
|
+
|
|
327
|
+
if (options) {
|
|
328
|
+
// Use options
|
|
329
|
+
console.log(`Option value: ${options.value}`);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
```
|
|
334
|
+
|
|
335
|
+
## Examples
|
|
336
|
+
|
|
337
|
+
### Example 1: Simple Logging Interceptor
|
|
338
|
+
|
|
339
|
+
```typescript
|
|
340
|
+
import 'reflect-metadata';
|
|
341
|
+
import { BaseInterceptor } from '@dangao/bun-server';
|
|
342
|
+
import type { Container } from '@dangao/bun-server';
|
|
343
|
+
import type { Context } from '@dangao/bun-server';
|
|
344
|
+
|
|
345
|
+
const LOG_METADATA_KEY = Symbol('@my-app:log');
|
|
346
|
+
|
|
347
|
+
export function Log(message?: string): MethodDecorator {
|
|
348
|
+
return (target, propertyKey) => {
|
|
349
|
+
Reflect.defineMetadata(LOG_METADATA_KEY, { message }, target, propertyKey);
|
|
350
|
+
};
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
export class LogInterceptor extends BaseInterceptor {
|
|
354
|
+
public async execute<T>(...): Promise<T> {
|
|
355
|
+
const metadata = this.getMetadata<{ message?: string }>(target, propertyKey, LOG_METADATA_KEY);
|
|
356
|
+
const logMessage = metadata?.message || `Executing ${String(propertyKey)}`;
|
|
357
|
+
|
|
358
|
+
console.log(`[LOG] ${logMessage} - Start`);
|
|
359
|
+
const start = Date.now();
|
|
360
|
+
|
|
361
|
+
try {
|
|
362
|
+
const result = await Promise.resolve(originalMethod.apply(target, args));
|
|
363
|
+
const duration = Date.now() - start;
|
|
364
|
+
console.log(`[LOG] ${logMessage} - Completed in ${duration}ms`);
|
|
365
|
+
return result;
|
|
366
|
+
} catch (error) {
|
|
367
|
+
const duration = Date.now() - start;
|
|
368
|
+
console.error(`[LOG] ${logMessage} - Failed after ${duration}ms`, error);
|
|
369
|
+
throw error;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
```
|
|
374
|
+
|
|
375
|
+
### Example 2: Rate Limiting Interceptor
|
|
376
|
+
|
|
377
|
+
```typescript
|
|
378
|
+
import 'reflect-metadata';
|
|
379
|
+
import { BaseInterceptor, HttpException } from '@dangao/bun-server';
|
|
380
|
+
import type { Container } from '@dangao/bun-server';
|
|
381
|
+
import type { Context } from '@dangao/bun-server';
|
|
382
|
+
|
|
383
|
+
const RATE_LIMIT_METADATA_KEY = Symbol('@my-app:rate-limit');
|
|
384
|
+
|
|
385
|
+
export interface RateLimitOptions {
|
|
386
|
+
maxRequests: number;
|
|
387
|
+
windowMs: number;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
export function RateLimit(options: RateLimitOptions): MethodDecorator {
|
|
391
|
+
return (target, propertyKey) => {
|
|
392
|
+
Reflect.defineMetadata(RATE_LIMIT_METADATA_KEY, options, target, propertyKey);
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export class RateLimitInterceptor extends BaseInterceptor {
|
|
397
|
+
private readonly requests = new Map<string, number[]>();
|
|
398
|
+
|
|
399
|
+
public async execute<T>(...): Promise<T> {
|
|
400
|
+
const options = this.getMetadata<RateLimitOptions>(target, propertyKey, RATE_LIMIT_METADATA_KEY);
|
|
401
|
+
if (!options || !context) {
|
|
402
|
+
return await Promise.resolve(originalMethod.apply(target, args));
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const clientId = this.getHeader(context, 'X-Client-Id') || context.request.headers.get('X-Forwarded-For') || 'unknown';
|
|
406
|
+
const now = Date.now();
|
|
407
|
+
const windowStart = now - options.windowMs;
|
|
408
|
+
|
|
409
|
+
// Clean old requests
|
|
410
|
+
const requests = this.requests.get(clientId) || [];
|
|
411
|
+
const recentRequests = requests.filter(time => time > windowStart);
|
|
412
|
+
|
|
413
|
+
if (recentRequests.length >= options.maxRequests) {
|
|
414
|
+
throw new HttpException(429, 'Too Many Requests');
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
recentRequests.push(now);
|
|
418
|
+
this.requests.set(clientId, recentRequests);
|
|
419
|
+
|
|
420
|
+
return await Promise.resolve(originalMethod.apply(target, args));
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
```
|
|
424
|
+
|
|
425
|
+
## Best Practices
|
|
426
|
+
|
|
427
|
+
1. **Use Symbols for Metadata Keys**: Ensures uniqueness and avoids conflicts
|
|
428
|
+
2. **Follow Naming Conventions**: Use consistent naming patterns
|
|
429
|
+
3. **Document Your Decorators**: Provide clear documentation for users
|
|
430
|
+
4. **Handle Errors Gracefully**: Always handle errors in interceptors
|
|
431
|
+
5. **Consider Performance**: Minimize overhead in interceptor execution
|
|
432
|
+
6. **Use BaseInterceptor**: Leverage the base class for common patterns
|
|
433
|
+
7. **Test Your Interceptors**: Write comprehensive tests
|
|
434
|
+
|
|
435
|
+
## See Also
|
|
436
|
+
|
|
437
|
+
- [API Documentation](./api.md)
|
|
438
|
+
- [Examples](../examples/)
|
|
439
|
+
- [Built-in Interceptors](../packages/bun-server/src/interceptor/builtin/)
|
|
440
|
+
|