@dangao/bun-server 1.7.1 → 1.8.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 +129 -21
- package/dist/di/decorators.d.ts +37 -0
- package/dist/di/decorators.d.ts.map +1 -1
- package/dist/di/index.d.ts +1 -1
- package/dist/di/index.d.ts.map +1 -1
- package/dist/di/module-registry.d.ts +17 -0
- package/dist/di/module-registry.d.ts.map +1 -1
- package/dist/events/decorators.d.ts +52 -0
- package/dist/events/decorators.d.ts.map +1 -0
- package/dist/events/event-module.d.ts +97 -0
- package/dist/events/event-module.d.ts.map +1 -0
- package/dist/events/index.d.ts +5 -0
- package/dist/events/index.d.ts.map +1 -0
- package/dist/events/service.d.ts +76 -0
- package/dist/events/service.d.ts.map +1 -0
- package/dist/events/types.d.ts +184 -0
- package/dist/events/types.d.ts.map +1 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1511 -11
- package/dist/security/filter.d.ts +23 -0
- package/dist/security/filter.d.ts.map +1 -1
- package/dist/security/guards/builtin/auth-guard.d.ts +44 -0
- package/dist/security/guards/builtin/auth-guard.d.ts.map +1 -0
- package/dist/security/guards/builtin/index.d.ts +3 -0
- package/dist/security/guards/builtin/index.d.ts.map +1 -0
- package/dist/security/guards/builtin/roles-guard.d.ts +66 -0
- package/dist/security/guards/builtin/roles-guard.d.ts.map +1 -0
- package/dist/security/guards/decorators.d.ts +50 -0
- package/dist/security/guards/decorators.d.ts.map +1 -0
- package/dist/security/guards/execution-context.d.ts +56 -0
- package/dist/security/guards/execution-context.d.ts.map +1 -0
- package/dist/security/guards/guard-registry.d.ts +67 -0
- package/dist/security/guards/guard-registry.d.ts.map +1 -0
- package/dist/security/guards/index.d.ts +7 -0
- package/dist/security/guards/index.d.ts.map +1 -0
- package/dist/security/guards/reflector.d.ts +57 -0
- package/dist/security/guards/reflector.d.ts.map +1 -0
- package/dist/security/guards/types.d.ts +126 -0
- package/dist/security/guards/types.d.ts.map +1 -0
- package/dist/security/index.d.ts +1 -0
- package/dist/security/index.d.ts.map +1 -1
- package/dist/security/security-module.d.ts +20 -0
- package/dist/security/security-module.d.ts.map +1 -1
- package/dist/validation/class-validator.d.ts +108 -0
- package/dist/validation/class-validator.d.ts.map +1 -0
- package/dist/validation/custom-validator.d.ts +130 -0
- package/dist/validation/custom-validator.d.ts.map +1 -0
- package/dist/validation/errors.d.ts +22 -2
- package/dist/validation/errors.d.ts.map +1 -1
- package/dist/validation/index.d.ts +7 -1
- package/dist/validation/index.d.ts.map +1 -1
- package/dist/validation/rules/array.d.ts +33 -0
- package/dist/validation/rules/array.d.ts.map +1 -0
- package/dist/validation/rules/common.d.ts +90 -0
- package/dist/validation/rules/common.d.ts.map +1 -0
- package/dist/validation/rules/conditional.d.ts +30 -0
- package/dist/validation/rules/conditional.d.ts.map +1 -0
- package/dist/validation/rules/index.d.ts +5 -0
- package/dist/validation/rules/index.d.ts.map +1 -0
- package/dist/validation/rules/object.d.ts +30 -0
- package/dist/validation/rules/object.d.ts.map +1 -0
- package/dist/validation/types.d.ts +52 -1
- package/dist/validation/types.d.ts.map +1 -1
- package/docs/events.md +494 -0
- package/docs/guards.md +376 -0
- package/docs/guide.md +309 -1
- package/docs/request-lifecycle.md +444 -0
- package/docs/validation.md +407 -0
- package/docs/zh/events.md +494 -0
- package/docs/zh/guards.md +376 -0
- package/docs/zh/guide.md +309 -1
- package/docs/zh/request-lifecycle.md +444 -0
- package/docs/zh/validation.md +407 -0
- package/package.json +1 -1
- package/src/di/decorators.ts +46 -0
- package/src/di/index.ts +10 -1
- package/src/di/module-registry.ts +39 -0
- package/src/events/decorators.ts +103 -0
- package/src/events/event-module.ts +272 -0
- package/src/events/index.ts +32 -0
- package/src/events/service.ts +352 -0
- package/src/events/types.ts +223 -0
- package/src/index.ts +133 -1
- package/src/security/filter.ts +88 -8
- package/src/security/guards/builtin/auth-guard.ts +68 -0
- package/src/security/guards/builtin/index.ts +3 -0
- package/src/security/guards/builtin/roles-guard.ts +165 -0
- package/src/security/guards/decorators.ts +124 -0
- package/src/security/guards/execution-context.ts +152 -0
- package/src/security/guards/guard-registry.ts +164 -0
- package/src/security/guards/index.ts +7 -0
- package/src/security/guards/reflector.ts +99 -0
- package/src/security/guards/types.ts +144 -0
- package/src/security/index.ts +1 -0
- package/src/security/security-module.ts +72 -2
- package/src/validation/class-validator.ts +322 -0
- package/src/validation/custom-validator.ts +289 -0
- package/src/validation/errors.ts +50 -2
- package/src/validation/index.ts +103 -1
- package/src/validation/rules/array.ts +118 -0
- package/src/validation/rules/common.ts +286 -0
- package/src/validation/rules/conditional.ts +52 -0
- package/src/validation/rules/index.ts +51 -0
- package/src/validation/rules/object.ts +86 -0
- package/src/validation/types.ts +61 -1
- package/tests/di/global-module.test.ts +487 -0
- package/tests/events/event-decorators.test.ts +173 -0
- package/tests/events/event-emitter.test.ts +373 -0
- package/tests/events/event-module.test.ts +373 -0
- package/tests/security/guards/guards-integration.test.ts +371 -0
- package/tests/security/guards/guards.test.ts +775 -0
- package/tests/security/security-module.test.ts +2 -2
- package/tests/validation/class-validator.test.ts +349 -0
- package/tests/validation/custom-validator.test.ts +335 -0
- package/tests/validation/rules.test.ts +543 -0
package/docs/guards.md
ADDED
|
@@ -0,0 +1,376 @@
|
|
|
1
|
+
# Guards
|
|
2
|
+
|
|
3
|
+
Guards are a powerful mechanism for controlling access to routes in your application. They execute before the route handler and determine whether a request should be processed.
|
|
4
|
+
|
|
5
|
+
## Overview
|
|
6
|
+
|
|
7
|
+
Guards implement the `CanActivate` interface and return a boolean value indicating whether the request should be allowed to proceed. Unlike middleware, guards have access to the `ExecutionContext`, which provides rich information about the current request context.
|
|
8
|
+
|
|
9
|
+
### When to Use Guards
|
|
10
|
+
|
|
11
|
+
- **Authentication**: Verify if a user is authenticated
|
|
12
|
+
- **Authorization**: Check if a user has the required roles or permissions
|
|
13
|
+
- **Rate limiting**: Implement custom rate limiting logic
|
|
14
|
+
- **Feature flags**: Enable/disable features based on conditions
|
|
15
|
+
- **Request validation**: Validate request metadata before processing
|
|
16
|
+
|
|
17
|
+
## Basic Usage
|
|
18
|
+
|
|
19
|
+
### Creating a Guard
|
|
20
|
+
|
|
21
|
+
```typescript
|
|
22
|
+
import { Injectable } from '@dangao/bun-server';
|
|
23
|
+
import type { CanActivate, ExecutionContext } from '@dangao/bun-server';
|
|
24
|
+
|
|
25
|
+
@Injectable()
|
|
26
|
+
class AuthGuard implements CanActivate {
|
|
27
|
+
canActivate(context: ExecutionContext): boolean | Promise<boolean> {
|
|
28
|
+
const request = context.switchToHttp().getRequest();
|
|
29
|
+
const token = request.getHeader('authorization');
|
|
30
|
+
|
|
31
|
+
// Validate token and return true/false
|
|
32
|
+
return !!token;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### Applying Guards
|
|
38
|
+
|
|
39
|
+
Guards can be applied at different levels:
|
|
40
|
+
|
|
41
|
+
#### Controller Level
|
|
42
|
+
|
|
43
|
+
```typescript
|
|
44
|
+
import { Controller, GET, UseGuards } from '@dangao/bun-server';
|
|
45
|
+
|
|
46
|
+
@Controller('/api/users')
|
|
47
|
+
@UseGuards(AuthGuard)
|
|
48
|
+
class UserController {
|
|
49
|
+
@GET('/')
|
|
50
|
+
getUsers() {
|
|
51
|
+
return { users: [] };
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
#### Method Level
|
|
57
|
+
|
|
58
|
+
```typescript
|
|
59
|
+
@Controller('/api')
|
|
60
|
+
class ApiController {
|
|
61
|
+
@GET('/public')
|
|
62
|
+
publicEndpoint() {
|
|
63
|
+
return { message: 'Public' };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
@GET('/private')
|
|
67
|
+
@UseGuards(AuthGuard)
|
|
68
|
+
privateEndpoint() {
|
|
69
|
+
return { message: 'Private' };
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
#### Global Guards
|
|
75
|
+
|
|
76
|
+
```typescript
|
|
77
|
+
import { SecurityModule } from '@dangao/bun-server';
|
|
78
|
+
|
|
79
|
+
SecurityModule.forRoot({
|
|
80
|
+
jwt: { secret: 'your-secret' },
|
|
81
|
+
globalGuards: [AuthGuard, RolesGuard],
|
|
82
|
+
});
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
## Execution Order
|
|
86
|
+
|
|
87
|
+
Guards execute in the following order:
|
|
88
|
+
|
|
89
|
+
1. **Global guards** (in registration order)
|
|
90
|
+
2. **Controller guards** (in decoration order)
|
|
91
|
+
3. **Method guards** (in decoration order)
|
|
92
|
+
|
|
93
|
+
```
|
|
94
|
+
HTTP Request
|
|
95
|
+
↓
|
|
96
|
+
Middleware Pipeline
|
|
97
|
+
↓
|
|
98
|
+
Security Filter (Token extraction & Authentication)
|
|
99
|
+
↓
|
|
100
|
+
Guards (Global → Controller → Method)
|
|
101
|
+
↓
|
|
102
|
+
Interceptors (Pre)
|
|
103
|
+
↓
|
|
104
|
+
Route Handler
|
|
105
|
+
↓
|
|
106
|
+
Interceptors (Post)
|
|
107
|
+
↓
|
|
108
|
+
HTTP Response
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
## Built-in Guards
|
|
112
|
+
|
|
113
|
+
### AuthGuard
|
|
114
|
+
|
|
115
|
+
Checks if the user is authenticated:
|
|
116
|
+
|
|
117
|
+
```typescript
|
|
118
|
+
import { Controller, GET, UseGuards, AuthGuard } from '@dangao/bun-server';
|
|
119
|
+
|
|
120
|
+
@Controller('/api/profile')
|
|
121
|
+
@UseGuards(AuthGuard)
|
|
122
|
+
class ProfileController {
|
|
123
|
+
@GET('/')
|
|
124
|
+
getProfile() {
|
|
125
|
+
// Only authenticated users can access
|
|
126
|
+
return { profile: {} };
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
```
|
|
130
|
+
|
|
131
|
+
### OptionalAuthGuard
|
|
132
|
+
|
|
133
|
+
Allows access without authentication but validates token if present:
|
|
134
|
+
|
|
135
|
+
```typescript
|
|
136
|
+
import { Controller, GET, UseGuards, OptionalAuthGuard } from '@dangao/bun-server';
|
|
137
|
+
|
|
138
|
+
@Controller('/api/posts')
|
|
139
|
+
class PostController {
|
|
140
|
+
@GET('/')
|
|
141
|
+
@UseGuards(OptionalAuthGuard)
|
|
142
|
+
getPosts() {
|
|
143
|
+
// Access allowed with or without authentication
|
|
144
|
+
return { posts: [] };
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### RolesGuard
|
|
150
|
+
|
|
151
|
+
Checks if the user has required roles:
|
|
152
|
+
|
|
153
|
+
```typescript
|
|
154
|
+
import { Controller, GET, UseGuards, AuthGuard, RolesGuard, Roles } from '@dangao/bun-server';
|
|
155
|
+
|
|
156
|
+
@Controller('/api/admin')
|
|
157
|
+
@UseGuards(AuthGuard, RolesGuard)
|
|
158
|
+
class AdminController {
|
|
159
|
+
@GET('/dashboard')
|
|
160
|
+
@Roles('admin')
|
|
161
|
+
dashboard() {
|
|
162
|
+
return { message: 'Admin Dashboard' };
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
@GET('/super')
|
|
166
|
+
@Roles('admin', 'superadmin') // Either role grants access
|
|
167
|
+
superAdmin() {
|
|
168
|
+
return { message: 'Super Admin' };
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
## Custom Guards
|
|
174
|
+
|
|
175
|
+
### Basic Custom Guard
|
|
176
|
+
|
|
177
|
+
```typescript
|
|
178
|
+
import { Injectable } from '@dangao/bun-server';
|
|
179
|
+
import type { CanActivate, ExecutionContext } from '@dangao/bun-server';
|
|
180
|
+
|
|
181
|
+
@Injectable()
|
|
182
|
+
class ApiKeyGuard implements CanActivate {
|
|
183
|
+
private readonly validApiKeys = ['key1', 'key2'];
|
|
184
|
+
|
|
185
|
+
canActivate(context: ExecutionContext): boolean {
|
|
186
|
+
const request = context.switchToHttp().getRequest();
|
|
187
|
+
const apiKey = request.getHeader('x-api-key');
|
|
188
|
+
|
|
189
|
+
return this.validApiKeys.includes(apiKey || '');
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
```
|
|
193
|
+
|
|
194
|
+
### Async Guard
|
|
195
|
+
|
|
196
|
+
```typescript
|
|
197
|
+
@Injectable()
|
|
198
|
+
class SubscriptionGuard implements CanActivate {
|
|
199
|
+
constructor(private readonly subscriptionService: SubscriptionService) {}
|
|
200
|
+
|
|
201
|
+
async canActivate(context: ExecutionContext): Promise<boolean> {
|
|
202
|
+
const request = context.switchToHttp().getRequest();
|
|
203
|
+
const userId = request.auth?.user?.id;
|
|
204
|
+
|
|
205
|
+
if (!userId) return false;
|
|
206
|
+
|
|
207
|
+
const subscription = await this.subscriptionService.getSubscription(userId);
|
|
208
|
+
return subscription?.isActive ?? false;
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
```
|
|
212
|
+
|
|
213
|
+
### Guard with Metadata Access
|
|
214
|
+
|
|
215
|
+
```typescript
|
|
216
|
+
@Injectable()
|
|
217
|
+
class FeatureFlagGuard implements CanActivate {
|
|
218
|
+
constructor(private readonly reflector: Reflector) {}
|
|
219
|
+
|
|
220
|
+
canActivate(context: ExecutionContext): boolean {
|
|
221
|
+
const feature = this.reflector.getAllAndOverride<string>(
|
|
222
|
+
'feature',
|
|
223
|
+
context.getClass(),
|
|
224
|
+
context.getMethodName(),
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
if (!feature) return true;
|
|
228
|
+
|
|
229
|
+
return this.isFeatureEnabled(feature);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
private isFeatureEnabled(feature: string): boolean {
|
|
233
|
+
// Check feature flag status
|
|
234
|
+
return true;
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
```
|
|
238
|
+
|
|
239
|
+
## ExecutionContext
|
|
240
|
+
|
|
241
|
+
The `ExecutionContext` provides access to:
|
|
242
|
+
|
|
243
|
+
### HTTP Context
|
|
244
|
+
|
|
245
|
+
```typescript
|
|
246
|
+
const httpHost = context.switchToHttp();
|
|
247
|
+
const request = httpHost.getRequest(); // Context object
|
|
248
|
+
const response = httpHost.getResponse(); // ResponseBuilder (if available)
|
|
249
|
+
```
|
|
250
|
+
|
|
251
|
+
### Controller and Method Info
|
|
252
|
+
|
|
253
|
+
```typescript
|
|
254
|
+
const controllerClass = context.getClass(); // Controller class
|
|
255
|
+
const handler = context.getHandler(); // Method function
|
|
256
|
+
const methodName = context.getMethodName(); // Method name string
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Metadata Access
|
|
260
|
+
|
|
261
|
+
```typescript
|
|
262
|
+
const metadata = context.getMetadata<string[]>('roles');
|
|
263
|
+
// First checks method, then class
|
|
264
|
+
```
|
|
265
|
+
|
|
266
|
+
## Reflector Utility
|
|
267
|
+
|
|
268
|
+
The `Reflector` class helps with metadata retrieval:
|
|
269
|
+
|
|
270
|
+
```typescript
|
|
271
|
+
import { Reflector, REFLECTOR_TOKEN } from '@dangao/bun-server';
|
|
272
|
+
|
|
273
|
+
@Injectable()
|
|
274
|
+
class MyGuard implements CanActivate {
|
|
275
|
+
constructor(@Inject(REFLECTOR_TOKEN) private readonly reflector: Reflector) {}
|
|
276
|
+
|
|
277
|
+
canActivate(context: ExecutionContext): boolean {
|
|
278
|
+
// Get metadata with method priority
|
|
279
|
+
const roles = this.reflector.getAllAndOverride<string[]>(
|
|
280
|
+
ROLES_METADATA_KEY,
|
|
281
|
+
context.getClass(),
|
|
282
|
+
context.getMethodName(),
|
|
283
|
+
);
|
|
284
|
+
|
|
285
|
+
// Merge class and method metadata
|
|
286
|
+
const permissions = this.reflector.getAllAndMerge<string[]>(
|
|
287
|
+
'permissions',
|
|
288
|
+
context.getClass(),
|
|
289
|
+
context.getMethodName(),
|
|
290
|
+
);
|
|
291
|
+
|
|
292
|
+
return true;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
```
|
|
296
|
+
|
|
297
|
+
## Custom Roles Guard Factory
|
|
298
|
+
|
|
299
|
+
Create customized role guards:
|
|
300
|
+
|
|
301
|
+
```typescript
|
|
302
|
+
import { createRolesGuard } from '@dangao/bun-server';
|
|
303
|
+
|
|
304
|
+
// Require ALL roles instead of ANY
|
|
305
|
+
const AllRolesGuard = createRolesGuard({ matchAll: true });
|
|
306
|
+
|
|
307
|
+
// Custom role extraction
|
|
308
|
+
const CustomRolesGuard = createRolesGuard({
|
|
309
|
+
getRoles: (context) => {
|
|
310
|
+
const request = context.switchToHttp().getRequest();
|
|
311
|
+
return request.auth?.user?.permissions || [];
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
## Error Handling
|
|
317
|
+
|
|
318
|
+
Guards can throw exceptions to reject requests:
|
|
319
|
+
|
|
320
|
+
```typescript
|
|
321
|
+
import { ForbiddenException, UnauthorizedException } from '@dangao/bun-server';
|
|
322
|
+
|
|
323
|
+
@Injectable()
|
|
324
|
+
class StrictAuthGuard implements CanActivate {
|
|
325
|
+
canActivate(context: ExecutionContext): boolean {
|
|
326
|
+
const request = context.switchToHttp().getRequest();
|
|
327
|
+
|
|
328
|
+
if (!request.auth?.isAuthenticated) {
|
|
329
|
+
throw new UnauthorizedException('Authentication required');
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
if (!this.hasPermission(request.auth.user)) {
|
|
333
|
+
throw new ForbiddenException('Insufficient permissions');
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
return true;
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
```
|
|
340
|
+
|
|
341
|
+
## Best Practices
|
|
342
|
+
|
|
343
|
+
1. **Keep guards focused**: Each guard should handle one concern
|
|
344
|
+
2. **Use dependency injection**: Inject services for complex logic
|
|
345
|
+
3. **Prefer throwing exceptions**: Provides better error messages than returning false
|
|
346
|
+
4. **Order matters**: Apply AuthGuard before RolesGuard
|
|
347
|
+
5. **Use built-in guards**: Leverage AuthGuard and RolesGuard when possible
|
|
348
|
+
6. **Test your guards**: Write unit tests for guard logic
|
|
349
|
+
|
|
350
|
+
## Integration with SecurityModule
|
|
351
|
+
|
|
352
|
+
Guards work seamlessly with the SecurityModule:
|
|
353
|
+
|
|
354
|
+
```typescript
|
|
355
|
+
import { Application, SecurityModule, AuthGuard, RolesGuard } from '@dangao/bun-server';
|
|
356
|
+
|
|
357
|
+
const app = new Application();
|
|
358
|
+
|
|
359
|
+
app.registerModule(
|
|
360
|
+
SecurityModule.forRoot({
|
|
361
|
+
jwt: {
|
|
362
|
+
secret: 'your-jwt-secret',
|
|
363
|
+
accessTokenExpiresIn: 3600,
|
|
364
|
+
},
|
|
365
|
+
excludePaths: ['/public', '/health'],
|
|
366
|
+
globalGuards: [AuthGuard], // Applied to all routes
|
|
367
|
+
}),
|
|
368
|
+
);
|
|
369
|
+
```
|
|
370
|
+
|
|
371
|
+
## See Also
|
|
372
|
+
|
|
373
|
+
- [Request Lifecycle](./request-lifecycle.md)
|
|
374
|
+
- [Security Module](./guide.md#security-module)
|
|
375
|
+
- [Custom Decorators](./custom-decorators.md)
|
|
376
|
+
|
package/docs/guide.md
CHANGED
|
@@ -2,6 +2,16 @@
|
|
|
2
2
|
|
|
3
3
|
Covers key steps for building Bun Server applications from scratch.
|
|
4
4
|
|
|
5
|
+
## Request Lifecycle Overview
|
|
6
|
+
|
|
7
|
+
Before diving into implementation details, it's helpful to understand how Bun Server processes requests:
|
|
8
|
+
|
|
9
|
+
```
|
|
10
|
+
HTTP Request → Middleware → Security → Router → Interceptors(Pre) → Validation → Handler → Interceptors(Post) → Exception Filter → HTTP Response
|
|
11
|
+
```
|
|
12
|
+
|
|
13
|
+
For detailed lifecycle documentation, see [Request Lifecycle](./request-lifecycle.md).
|
|
14
|
+
|
|
5
15
|
## 1. Initialize Application
|
|
6
16
|
|
|
7
17
|
```ts
|
|
@@ -620,7 +630,305 @@ class UserController {
|
|
|
620
630
|
}
|
|
621
631
|
```
|
|
622
632
|
|
|
623
|
-
## 16.
|
|
633
|
+
## 16. Guards
|
|
634
|
+
|
|
635
|
+
Guards provide fine-grained access control for your routes. They execute after middleware and before interceptors, deciding whether a request should proceed.
|
|
636
|
+
|
|
637
|
+
### Built-in Guards
|
|
638
|
+
|
|
639
|
+
```ts
|
|
640
|
+
import {
|
|
641
|
+
AuthGuard,
|
|
642
|
+
Controller,
|
|
643
|
+
GET,
|
|
644
|
+
Roles,
|
|
645
|
+
RolesGuard,
|
|
646
|
+
UseGuards,
|
|
647
|
+
} from "@dangao/bun-server";
|
|
648
|
+
|
|
649
|
+
@Controller("/api/admin")
|
|
650
|
+
@UseGuards(AuthGuard, RolesGuard)
|
|
651
|
+
class AdminController {
|
|
652
|
+
@GET("/dashboard")
|
|
653
|
+
@Roles("admin")
|
|
654
|
+
public dashboard() {
|
|
655
|
+
return { message: "Admin Dashboard" };
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
@GET("/users")
|
|
659
|
+
@Roles("admin", "moderator") // Either role grants access
|
|
660
|
+
public listUsers() {
|
|
661
|
+
return { users: [] };
|
|
662
|
+
}
|
|
663
|
+
}
|
|
664
|
+
```
|
|
665
|
+
|
|
666
|
+
### Custom Guards
|
|
667
|
+
|
|
668
|
+
```ts
|
|
669
|
+
import { Injectable } from "@dangao/bun-server";
|
|
670
|
+
import type { CanActivate, ExecutionContext } from "@dangao/bun-server";
|
|
671
|
+
|
|
672
|
+
@Injectable()
|
|
673
|
+
class ApiKeyGuard implements CanActivate {
|
|
674
|
+
canActivate(context: ExecutionContext): boolean {
|
|
675
|
+
const request = context.switchToHttp().getRequest();
|
|
676
|
+
const apiKey = request.getHeader("x-api-key");
|
|
677
|
+
return apiKey === "valid-api-key";
|
|
678
|
+
}
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
@Controller("/api/external")
|
|
682
|
+
@UseGuards(ApiKeyGuard)
|
|
683
|
+
class ExternalApiController {
|
|
684
|
+
@GET("/data")
|
|
685
|
+
public getData() {
|
|
686
|
+
return { data: [] };
|
|
687
|
+
}
|
|
688
|
+
}
|
|
689
|
+
```
|
|
690
|
+
|
|
691
|
+
### Global Guards
|
|
692
|
+
|
|
693
|
+
```ts
|
|
694
|
+
SecurityModule.forRoot({
|
|
695
|
+
jwt: { secret: "your-secret" },
|
|
696
|
+
globalGuards: [AuthGuard], // Applied to all routes
|
|
697
|
+
});
|
|
698
|
+
```
|
|
699
|
+
|
|
700
|
+
For detailed documentation, see [Guards](./guards.md).
|
|
701
|
+
|
|
702
|
+
## 17. Event System
|
|
703
|
+
|
|
704
|
+
The Event Module provides a powerful event-driven architecture for building loosely coupled applications.
|
|
705
|
+
|
|
706
|
+
### Basic Usage
|
|
707
|
+
|
|
708
|
+
```ts
|
|
709
|
+
import {
|
|
710
|
+
EventModule,
|
|
711
|
+
Injectable,
|
|
712
|
+
Inject,
|
|
713
|
+
OnEvent,
|
|
714
|
+
EVENT_EMITTER_TOKEN,
|
|
715
|
+
} from "@dangao/bun-server";
|
|
716
|
+
import type { EventEmitter } from "@dangao/bun-server";
|
|
717
|
+
|
|
718
|
+
// Define events
|
|
719
|
+
const USER_CREATED = Symbol("user.created");
|
|
720
|
+
|
|
721
|
+
interface UserCreatedEvent {
|
|
722
|
+
userId: string;
|
|
723
|
+
email: string;
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Service that publishes events
|
|
727
|
+
@Injectable()
|
|
728
|
+
class UserService {
|
|
729
|
+
public constructor(
|
|
730
|
+
@Inject(EVENT_EMITTER_TOKEN) private readonly eventEmitter: EventEmitter,
|
|
731
|
+
) {}
|
|
732
|
+
|
|
733
|
+
public async createUser(email: string) {
|
|
734
|
+
const userId = "user-123";
|
|
735
|
+
|
|
736
|
+
// Publish event
|
|
737
|
+
this.eventEmitter.emit<UserCreatedEvent>(USER_CREATED, {
|
|
738
|
+
userId,
|
|
739
|
+
email,
|
|
740
|
+
});
|
|
741
|
+
|
|
742
|
+
return { userId, email };
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
// Service that listens to events
|
|
747
|
+
@Injectable()
|
|
748
|
+
class NotificationService {
|
|
749
|
+
@OnEvent(USER_CREATED)
|
|
750
|
+
public handleUserCreated(payload: UserCreatedEvent) {
|
|
751
|
+
console.log(`Welcome email sent to ${payload.email}`);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
@OnEvent(USER_CREATED, { async: true, priority: 10 })
|
|
755
|
+
public async trackUserCreation(payload: UserCreatedEvent) {
|
|
756
|
+
await this.analytics.track("user_created", payload);
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
### Module Configuration
|
|
762
|
+
|
|
763
|
+
```ts
|
|
764
|
+
EventModule.forRoot({
|
|
765
|
+
wildcard: true, // Enable wildcard matching
|
|
766
|
+
maxListeners: 20, // Max listeners per event
|
|
767
|
+
onError: (error, event, payload) => {
|
|
768
|
+
console.error(`Error in event ${String(event)}:`, error);
|
|
769
|
+
},
|
|
770
|
+
});
|
|
771
|
+
|
|
772
|
+
// Register listener classes
|
|
773
|
+
EventModule.registerListeners([NotificationService, AnalyticsService]);
|
|
774
|
+
|
|
775
|
+
@Module({
|
|
776
|
+
imports: [EventModule],
|
|
777
|
+
providers: [UserService, NotificationService, AnalyticsService],
|
|
778
|
+
})
|
|
779
|
+
class AppModule {}
|
|
780
|
+
|
|
781
|
+
const app = new Application();
|
|
782
|
+
app.registerModule(AppModule);
|
|
783
|
+
|
|
784
|
+
// Initialize event listeners after module registration
|
|
785
|
+
EventModule.initializeListeners(app.getContainer());
|
|
786
|
+
```
|
|
787
|
+
|
|
788
|
+
### Wildcard Events
|
|
789
|
+
|
|
790
|
+
```ts
|
|
791
|
+
// Match any user event: user.created, user.updated, user.deleted
|
|
792
|
+
@OnEvent("user.*")
|
|
793
|
+
handleAnyUserEvent(payload: unknown) {}
|
|
794
|
+
|
|
795
|
+
// Match nested events: order.created, order.item.added, order.payment.completed
|
|
796
|
+
@OnEvent("order.**")
|
|
797
|
+
handleAllOrderEvents(payload: unknown) {}
|
|
798
|
+
```
|
|
799
|
+
|
|
800
|
+
### Async Event Publishing
|
|
801
|
+
|
|
802
|
+
```ts
|
|
803
|
+
// Fire and forget (async listeners are triggered but not awaited)
|
|
804
|
+
this.eventEmitter.emit("order.created", orderData);
|
|
805
|
+
|
|
806
|
+
// Wait for all listeners to complete
|
|
807
|
+
await this.eventEmitter.emitAsync("order.created", orderData);
|
|
808
|
+
```
|
|
809
|
+
|
|
810
|
+
For detailed documentation, see [Event System](./events.md).
|
|
811
|
+
|
|
812
|
+
## 18. Global Modules
|
|
813
|
+
|
|
814
|
+
Global modules allow you to share providers across all modules without explicit imports. This is useful for commonly used services like configuration, logging, or caching.
|
|
815
|
+
|
|
816
|
+
### Creating a Global Module
|
|
817
|
+
|
|
818
|
+
Use the `@Global()` decorator to mark a module as global:
|
|
819
|
+
|
|
820
|
+
```ts
|
|
821
|
+
import { Global, Injectable, Module } from "@dangao/bun-server";
|
|
822
|
+
|
|
823
|
+
const CONFIG_TOKEN = Symbol("config");
|
|
824
|
+
|
|
825
|
+
@Injectable()
|
|
826
|
+
class ConfigService {
|
|
827
|
+
public get(key: string): string {
|
|
828
|
+
return `config:${key}`;
|
|
829
|
+
}
|
|
830
|
+
}
|
|
831
|
+
|
|
832
|
+
@Global()
|
|
833
|
+
@Module({
|
|
834
|
+
providers: [
|
|
835
|
+
{
|
|
836
|
+
provide: CONFIG_TOKEN,
|
|
837
|
+
useClass: ConfigService,
|
|
838
|
+
},
|
|
839
|
+
],
|
|
840
|
+
exports: [CONFIG_TOKEN],
|
|
841
|
+
})
|
|
842
|
+
class GlobalConfigModule {}
|
|
843
|
+
```
|
|
844
|
+
|
|
845
|
+
### Using Global Module Exports
|
|
846
|
+
|
|
847
|
+
Other modules can use the exported providers without importing the global module:
|
|
848
|
+
|
|
849
|
+
```ts
|
|
850
|
+
@Injectable()
|
|
851
|
+
class UserService {
|
|
852
|
+
public constructor(
|
|
853
|
+
@Inject(CONFIG_TOKEN) private readonly config: ConfigService,
|
|
854
|
+
) {}
|
|
855
|
+
|
|
856
|
+
public getAppName(): string {
|
|
857
|
+
return this.config.get("app.name");
|
|
858
|
+
}
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
// UserModule does NOT need to import GlobalConfigModule
|
|
862
|
+
@Module({
|
|
863
|
+
providers: [UserService],
|
|
864
|
+
})
|
|
865
|
+
class UserModule {}
|
|
866
|
+
```
|
|
867
|
+
|
|
868
|
+
### Registering Global Modules
|
|
869
|
+
|
|
870
|
+
Global modules must be registered with the application, typically in your root module:
|
|
871
|
+
|
|
872
|
+
```ts
|
|
873
|
+
@Module({
|
|
874
|
+
imports: [
|
|
875
|
+
GlobalConfigModule, // Register the global module once
|
|
876
|
+
GlobalLoggerModule,
|
|
877
|
+
UserModule, // UserModule can use ConfigService without importing it
|
|
878
|
+
ProductModule,
|
|
879
|
+
],
|
|
880
|
+
})
|
|
881
|
+
class AppModule {}
|
|
882
|
+
|
|
883
|
+
const app = new Application();
|
|
884
|
+
app.registerModule(AppModule);
|
|
885
|
+
```
|
|
886
|
+
|
|
887
|
+
### Key Points
|
|
888
|
+
|
|
889
|
+
- **Single Registration**: Global modules only need to be registered once (usually in the root module)
|
|
890
|
+
- **Automatic Availability**: Exports from global modules are available to all other modules
|
|
891
|
+
- **Singleton Sharing**: Global module providers maintain singleton behavior across the application
|
|
892
|
+
- **No Import Required**: Other modules don't need to add global modules to their `imports` array
|
|
893
|
+
|
|
894
|
+
### Use Cases
|
|
895
|
+
|
|
896
|
+
Global modules are ideal for:
|
|
897
|
+
|
|
898
|
+
- **Configuration Services**: App-wide configuration access
|
|
899
|
+
- **Logging Services**: Centralized logging
|
|
900
|
+
- **Cache Services**: Shared caching layer
|
|
901
|
+
- **Database Connections**: Shared database access
|
|
902
|
+
- **Event Emitters**: Application-wide event bus
|
|
903
|
+
|
|
904
|
+
### Example: Multiple Global Modules
|
|
905
|
+
|
|
906
|
+
```ts
|
|
907
|
+
@Global()
|
|
908
|
+
@Module({
|
|
909
|
+
providers: [{ provide: LOGGER_TOKEN, useClass: LoggerService }],
|
|
910
|
+
exports: [LOGGER_TOKEN],
|
|
911
|
+
})
|
|
912
|
+
class GlobalLoggerModule {}
|
|
913
|
+
|
|
914
|
+
@Global()
|
|
915
|
+
@Module({
|
|
916
|
+
providers: [{ provide: CACHE_TOKEN, useClass: CacheService }],
|
|
917
|
+
exports: [CACHE_TOKEN],
|
|
918
|
+
})
|
|
919
|
+
class GlobalCacheModule {}
|
|
920
|
+
|
|
921
|
+
// App service can use both without explicit imports
|
|
922
|
+
@Injectable()
|
|
923
|
+
class AppService {
|
|
924
|
+
public constructor(
|
|
925
|
+
@Inject(LOGGER_TOKEN) private readonly logger: LoggerService,
|
|
926
|
+
@Inject(CACHE_TOKEN) private readonly cache: CacheService,
|
|
927
|
+
) {}
|
|
928
|
+
}
|
|
929
|
+
```
|
|
930
|
+
|
|
931
|
+
## 19. Testing Recommendations
|
|
624
932
|
|
|
625
933
|
- Use `tests/utils/test-port.ts` to get auto-incrementing ports, avoiding local
|
|
626
934
|
conflicts.
|