@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.
@@ -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
+