@shadow-library/fastify 0.0.6 → 0.0.8
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 +451 -14
- package/cjs/classes/default-error-handler.js +5 -4
- package/cjs/decorators/http-input.decorator.d.ts +2 -1
- package/cjs/decorators/http-input.decorator.js +4 -5
- package/cjs/decorators/index.d.ts +1 -0
- package/cjs/decorators/index.js +1 -0
- package/cjs/decorators/middleware.decorator.js +2 -2
- package/cjs/decorators/sensitive.decorator.d.ts +4 -0
- package/cjs/decorators/sensitive.decorator.js +15 -0
- package/cjs/interfaces/middleware.interface.d.ts +9 -6
- package/cjs/interfaces/route-handler.interface.d.ts +6 -2
- package/cjs/interfaces/server-metadata.interface.d.ts +5 -4
- package/cjs/module/error-response.dto.d.ts +1 -1
- package/cjs/module/error-response.dto.js +1 -1
- package/cjs/module/fastify-module.interface.d.ts +6 -1
- package/cjs/module/fastify-router.d.ts +25 -2
- package/cjs/module/fastify-router.js +89 -20
- package/cjs/module/fastify.module.js +23 -6
- package/cjs/module/fastify.utils.js +20 -7
- package/cjs/server.error.d.ts +2 -0
- package/cjs/server.error.js +9 -7
- package/cjs/services/context.service.d.ts +16 -5
- package/cjs/services/context.service.js +51 -12
- package/esm/classes/default-error-handler.js +6 -5
- package/esm/decorators/http-input.decorator.d.ts +2 -1
- package/esm/decorators/http-input.decorator.js +2 -3
- package/esm/decorators/index.d.ts +1 -0
- package/esm/decorators/index.js +1 -0
- package/esm/decorators/middleware.decorator.js +1 -1
- package/esm/decorators/sensitive.decorator.d.ts +4 -0
- package/esm/decorators/sensitive.decorator.js +12 -0
- package/esm/interfaces/middleware.interface.d.ts +9 -6
- package/esm/interfaces/route-handler.interface.d.ts +6 -2
- package/esm/interfaces/server-metadata.interface.d.ts +5 -4
- package/esm/module/error-response.dto.d.ts +1 -1
- package/esm/module/error-response.dto.js +1 -1
- package/esm/module/fastify-module.interface.d.ts +6 -1
- package/esm/module/fastify-router.d.ts +25 -2
- package/esm/module/fastify-router.js +87 -18
- package/esm/module/fastify.module.js +24 -7
- package/esm/module/fastify.utils.js +19 -6
- package/esm/server.error.d.ts +2 -0
- package/esm/server.error.js +9 -7
- package/esm/services/context.service.d.ts +16 -5
- package/esm/services/context.service.js +50 -11
- package/package.json +6 -4
package/README.md
CHANGED
|
@@ -1,24 +1,461 @@
|
|
|
1
|
-
#
|
|
1
|
+
# @shadow-library/fastify
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
and URL parameter validation using AJV for speed, response formatting with fast-json-stringify, and a unified approach to authentication and authorization.
|
|
5
|
-
The package uses decorators to define controller classes and HTTP methods, and includes a render decorator for templating engine support.
|
|
3
|
+
A powerful TypeScript-first Fastify wrapper featuring decorator-based routing, automatic validation, response serialization, and comprehensive middleware support.
|
|
6
4
|
|
|
7
5
|
## Features
|
|
8
6
|
|
|
9
|
-
**
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
**Authentication
|
|
14
|
-
**
|
|
7
|
+
- 🚀 **High Performance**: Built on top of Fastify, one of the fastest Node.js web frameworks
|
|
8
|
+
- 🎯 **Decorator-Based**: Clean, intuitive API using TypeScript decorators
|
|
9
|
+
- ✅ **Automatic Validation**: Fast validation for body, query, and URL parameters using AJV
|
|
10
|
+
- 📝 **Response Serialization**: Consistent response formatting with fast-json-stringify
|
|
11
|
+
- 🔒 **Authentication & Authorization**: Built-in support for guards and middleware
|
|
12
|
+
- 🛡️ **Error Handling**: Comprehensive error handling with custom error types
|
|
13
|
+
- 🔄 **Middleware Support**: Flexible middleware system with lifecycle hooks
|
|
14
|
+
- 📊 **Type Safety**: Full TypeScript support with schema generation
|
|
15
|
+
- 🎨 **Templating Ready**: Built-in support for templating engines
|
|
15
16
|
|
|
16
|
-
##
|
|
17
|
+
## Installation
|
|
17
18
|
|
|
18
|
-
```
|
|
19
|
-
|
|
19
|
+
```bash
|
|
20
|
+
# npm
|
|
21
|
+
npm install @shadow-library/fastify
|
|
22
|
+
|
|
23
|
+
# yarn
|
|
24
|
+
yarn add @shadow-library/fastify
|
|
25
|
+
|
|
26
|
+
# pnpm
|
|
27
|
+
pnpm add @shadow-library/fastify
|
|
28
|
+
|
|
29
|
+
# bun
|
|
30
|
+
bun add @shadow-library/fastify
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
## Quick Start
|
|
34
|
+
|
|
35
|
+
### 1. Create a Controller
|
|
36
|
+
|
|
37
|
+
```typescript
|
|
38
|
+
import { HttpController, Get, Post, Body, RespondFor } from '@shadow-library/fastify';
|
|
39
|
+
import { Schema, Field } from '@shadow-library/class-schema';
|
|
40
|
+
|
|
41
|
+
@Schema()
|
|
42
|
+
class CreateUserDto {
|
|
43
|
+
@Field(() => String, { minLength: 2, maxLength: 50 })
|
|
44
|
+
name: string;
|
|
45
|
+
|
|
46
|
+
@Field(() => String, { format: 'email' })
|
|
47
|
+
email: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@Schema()
|
|
51
|
+
class UserResponse {
|
|
52
|
+
@Field(() => Number)
|
|
53
|
+
id: number;
|
|
54
|
+
|
|
55
|
+
@Field(() => String)
|
|
56
|
+
name: string;
|
|
57
|
+
|
|
58
|
+
@Field(() => String)
|
|
59
|
+
email: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
@HttpController('/api/users')
|
|
63
|
+
export class UserController {
|
|
64
|
+
@Get()
|
|
65
|
+
@RespondFor(200, [UserResponse])
|
|
66
|
+
async getUsers(): Promise<UserResponse[]> {
|
|
67
|
+
return [{ id: 1, name: 'John Doe', email: 'john@example.com' }];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
@Post()
|
|
71
|
+
@RespondFor(201, UserResponse)
|
|
72
|
+
async createUser(@Body() userData: CreateUserDto): Promise<UserResponse> {
|
|
73
|
+
// Your business logic here
|
|
74
|
+
return { id: 2, ...userData };
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
### 2. Create a Module
|
|
80
|
+
|
|
81
|
+
```typescript
|
|
82
|
+
import { FastifyModule } from '@shadow-library/fastify';
|
|
83
|
+
import { UserController } from './user.controller';
|
|
84
|
+
|
|
85
|
+
export const AppModule = FastifyModule.forRoot({
|
|
86
|
+
controllers: [UserController],
|
|
87
|
+
port: 3000,
|
|
88
|
+
host: '0.0.0.0',
|
|
89
|
+
});
|
|
90
|
+
```
|
|
91
|
+
|
|
92
|
+
### 3. Bootstrap Your Application
|
|
93
|
+
|
|
94
|
+
```typescript
|
|
95
|
+
import { ShadowFactory } from '@shadow-library/app';
|
|
96
|
+
import { Logger } from '@shadow-library/common';
|
|
97
|
+
import { AppModule } from './app.module';
|
|
98
|
+
|
|
99
|
+
Logger.addDefaultTransports();
|
|
100
|
+
|
|
101
|
+
async function bootstrap() {
|
|
102
|
+
const app = await ShadowFactory.create(AppModule);
|
|
103
|
+
await app.start();
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
bootstrap();
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
## API Reference
|
|
110
|
+
|
|
111
|
+
### Decorators
|
|
112
|
+
|
|
113
|
+
#### Route Decorators
|
|
114
|
+
|
|
115
|
+
```typescript
|
|
116
|
+
@Get(path?: string) // GET requests
|
|
117
|
+
@Post(path?: string) // POST requests
|
|
118
|
+
@Put(path?: string) // PUT requests
|
|
119
|
+
@Patch(path?: string) // PATCH requests
|
|
120
|
+
@Delete(path?: string) // DELETE requests
|
|
121
|
+
@Options(path?: string) // OPTIONS requests
|
|
122
|
+
@Head(path?: string) // HEAD requests
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
#### Parameter Decorators
|
|
126
|
+
|
|
127
|
+
```typescript
|
|
128
|
+
@Body(schema?: JSONSchema) // Request body
|
|
129
|
+
@Params(schema?: JSONSchema) // URL parameters
|
|
130
|
+
@Query(schema?: JSONSchema) // Query parameters
|
|
131
|
+
@Request() / @Req() // Raw Fastify request
|
|
132
|
+
@Response() / @Res() // Raw Fastify response
|
|
133
|
+
```
|
|
134
|
+
|
|
135
|
+
#### Response Decorators
|
|
136
|
+
|
|
137
|
+
```typescript
|
|
138
|
+
@RespondFor(statusCode: number, schema: Class | JSONSchema)
|
|
139
|
+
@HttpStatus(statusCode: number)
|
|
140
|
+
```
|
|
141
|
+
|
|
142
|
+
#### Controller Decorator
|
|
143
|
+
|
|
144
|
+
```typescript
|
|
145
|
+
@HttpController(prefix?: string)
|
|
20
146
|
```
|
|
21
147
|
|
|
148
|
+
### Example: Advanced Usage with Authentication
|
|
149
|
+
|
|
150
|
+
```typescript
|
|
151
|
+
import { HttpController, Get, Post, Middleware, Body, Params } from '@shadow-library/fastify';
|
|
152
|
+
|
|
153
|
+
// Custom Authentication Guard
|
|
154
|
+
@Middleware({ type: 'preHandler', weight: 100 })
|
|
155
|
+
class AuthGuard {
|
|
156
|
+
use(request: HttpRequest, reply: HttpResponse, done: HttpCallback) {
|
|
157
|
+
const token = request.headers.authorization?.replace('Bearer ', '');
|
|
158
|
+
if (!token) {
|
|
159
|
+
return reply.status(401).send({ error: 'Unauthorized' });
|
|
160
|
+
}
|
|
161
|
+
// Validate token logic here
|
|
162
|
+
done();
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
@HttpController('/api/protected')
|
|
167
|
+
export class ProtectedController {
|
|
168
|
+
@Get('/profile')
|
|
169
|
+
@RespondFor(200, UserResponse)
|
|
170
|
+
async getProfile(@Request() req): Promise<UserResponse> {
|
|
171
|
+
// Access authenticated user from request
|
|
172
|
+
return req.user;
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
### Error Handling
|
|
178
|
+
|
|
179
|
+
```typescript
|
|
180
|
+
import { ServerError, ServerErrorCode } from '@shadow-library/fastify';
|
|
181
|
+
|
|
182
|
+
@HttpController('/api')
|
|
183
|
+
export class ExampleController {
|
|
184
|
+
@Get('/error')
|
|
185
|
+
throwError() {
|
|
186
|
+
// Throws a predefined server error
|
|
187
|
+
throw new ServerError(ServerErrorCode.S008);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
@Get('/custom-error')
|
|
191
|
+
throwCustomError() {
|
|
192
|
+
// Throws a custom error
|
|
193
|
+
throw new Error('Something went wrong');
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
### Child Routes and Route Resolution
|
|
199
|
+
|
|
200
|
+
```typescript
|
|
201
|
+
@HttpController('/api')
|
|
202
|
+
export class RoutesController {
|
|
203
|
+
constructor(@Inject(Router) private readonly fastifyRouter: FastifyRouter) {}
|
|
204
|
+
|
|
205
|
+
@Get('/unified')
|
|
206
|
+
async unifiedRoute(@Query() query: Record<string, any>) {
|
|
207
|
+
const results = [];
|
|
208
|
+
for (const route of query.routes?.split(',') ?? []) {
|
|
209
|
+
const result = await this.fastifyRouter.resolveChildRoute(route);
|
|
210
|
+
results.push(result);
|
|
211
|
+
}
|
|
212
|
+
return { results };
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
```
|
|
216
|
+
|
|
217
|
+
## Configuration
|
|
218
|
+
|
|
219
|
+
### Module Configuration
|
|
220
|
+
|
|
221
|
+
```typescript
|
|
222
|
+
const AppModule = FastifyModule.forRoot({
|
|
223
|
+
// Basic server configuration
|
|
224
|
+
host: 'localhost',
|
|
225
|
+
port: 8080,
|
|
226
|
+
|
|
227
|
+
// Controllers to register
|
|
228
|
+
controllers: [UserController, AuthController],
|
|
229
|
+
|
|
230
|
+
// Additional providers
|
|
231
|
+
providers: [UserService, AuthService],
|
|
232
|
+
|
|
233
|
+
// Error handling
|
|
234
|
+
errorHandler: new CustomErrorHandler(),
|
|
235
|
+
|
|
236
|
+
// Security
|
|
237
|
+
maskSensitiveData: true,
|
|
238
|
+
|
|
239
|
+
// Request ID generation
|
|
240
|
+
requestIdLogLabel: 'rid',
|
|
241
|
+
genReqId: () => uuid(),
|
|
242
|
+
|
|
243
|
+
// Router options
|
|
244
|
+
routerOptions: {
|
|
245
|
+
ignoreTrailingSlash: true,
|
|
246
|
+
ignoreDuplicateSlashes: true,
|
|
247
|
+
},
|
|
248
|
+
|
|
249
|
+
// Response schemas for error handling
|
|
250
|
+
responseSchema: {
|
|
251
|
+
'4xx': ErrorResponseSchema,
|
|
252
|
+
'5xx': ErrorResponseSchema,
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
// Extend Fastify instance before registering controllers
|
|
256
|
+
fastifyFactory: async fastify => {
|
|
257
|
+
// Register plugins, add hooks, or configure Fastify
|
|
258
|
+
await fastify.register(require('@fastify/cors'), {
|
|
259
|
+
origin: true,
|
|
260
|
+
});
|
|
261
|
+
|
|
262
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
263
|
+
console.log(`Incoming request: ${request.method} ${request.url}`);
|
|
264
|
+
});
|
|
265
|
+
|
|
266
|
+
return fastify;
|
|
267
|
+
},
|
|
268
|
+
});
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
### Async Configuration
|
|
272
|
+
|
|
273
|
+
```typescript
|
|
274
|
+
const AppModule = FastifyModule.forRootAsync({
|
|
275
|
+
useFactory: async (configService: ConfigService) => ({
|
|
276
|
+
host: configService.get('HOST'),
|
|
277
|
+
port: configService.get('PORT'),
|
|
278
|
+
controllers: [UserController],
|
|
279
|
+
}),
|
|
280
|
+
inject: [ConfigService],
|
|
281
|
+
});
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### Extending Fastify Instance
|
|
285
|
+
|
|
286
|
+
Use the `fastifyFactory` option to customize the Fastify instance before controllers are registered. This is perfect for adding plugins, global hooks, or custom configurations:
|
|
287
|
+
|
|
288
|
+
```typescript
|
|
289
|
+
import cors from '@fastify/cors';
|
|
290
|
+
import helmet from '@fastify/helmet';
|
|
291
|
+
import rateLimit from '@fastify/rate-limit';
|
|
292
|
+
|
|
293
|
+
const AppModule = FastifyModule.forRoot({
|
|
294
|
+
controllers: [UserController],
|
|
295
|
+
fastifyFactory: async fastify => {
|
|
296
|
+
// Register security plugins
|
|
297
|
+
await fastify.register(helmet, {
|
|
298
|
+
contentSecurityPolicy: {
|
|
299
|
+
directives: {
|
|
300
|
+
defaultSrc: ["'self'"],
|
|
301
|
+
styleSrc: ["'self'", "'unsafe-inline'"],
|
|
302
|
+
},
|
|
303
|
+
},
|
|
304
|
+
});
|
|
305
|
+
|
|
306
|
+
// Add CORS support
|
|
307
|
+
await fastify.register(cors, {
|
|
308
|
+
origin: (origin, callback) => {
|
|
309
|
+
const allowedOrigins = ['http://localhost:3000', 'https://myapp.com'];
|
|
310
|
+
if (!origin || allowedOrigins.includes(origin)) {
|
|
311
|
+
callback(null, true);
|
|
312
|
+
} else {
|
|
313
|
+
callback(new Error('Not allowed by CORS'), false);
|
|
314
|
+
}
|
|
315
|
+
},
|
|
316
|
+
credentials: true,
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
// Add rate limiting
|
|
320
|
+
await fastify.register(rateLimit, {
|
|
321
|
+
max: 100,
|
|
322
|
+
timeWindow: '1 minute',
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
// Add global hooks
|
|
326
|
+
fastify.addHook('onRequest', async (request, reply) => {
|
|
327
|
+
console.log(`[${new Date().toISOString()}] ${request.method} ${request.url}`);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
fastify.addHook('onResponse', async (request, reply) => {
|
|
331
|
+
const responseTime = reply.elapsedTime;
|
|
332
|
+
console.log(`Response sent in ${responseTime}ms`);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
// Add custom context or decorators
|
|
336
|
+
fastify.decorate('config', {
|
|
337
|
+
apiVersion: 'v1',
|
|
338
|
+
environment: process.env.NODE_ENV,
|
|
339
|
+
});
|
|
340
|
+
|
|
341
|
+
// Register custom content type parsers
|
|
342
|
+
fastify.addContentTypeParser('text/plain', { parseAs: 'string' }, (req, body, done) => {
|
|
343
|
+
done(null, body);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
return fastify;
|
|
347
|
+
},
|
|
348
|
+
});
|
|
349
|
+
```
|
|
350
|
+
|
|
351
|
+
#### Common Use Cases for fastifyFactory:
|
|
352
|
+
|
|
353
|
+
- **Security**: Add helmet, CORS, rate limiting
|
|
354
|
+
- **Logging**: Custom request/response logging hooks
|
|
355
|
+
- **Authentication**: Register authentication plugins
|
|
356
|
+
- **File Upload**: Configure multipart/file upload handling
|
|
357
|
+
- **Custom Parsers**: Add support for custom content types
|
|
358
|
+
- **Swagger/OpenAPI**: Register documentation plugins
|
|
359
|
+
- **Database**: Add database connection decorators
|
|
360
|
+
- **Caching**: Configure caching plugins
|
|
361
|
+
|
|
362
|
+
## Middleware
|
|
363
|
+
|
|
364
|
+
Create custom middleware by implementing the `Middleware` decorator:
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
@Middleware({ type: 'preHandler', weight: 50 })
|
|
368
|
+
export class LoggingMiddleware {
|
|
369
|
+
use(request: HttpRequest, reply: HttpResponse, done: HttpCallback) {
|
|
370
|
+
console.log(`${request.method} ${request.url}`);
|
|
371
|
+
done();
|
|
372
|
+
}
|
|
373
|
+
}
|
|
374
|
+
|
|
375
|
+
// Or generate middleware dynamically
|
|
376
|
+
@Middleware({ type: 'preValidation', weight: 75 })
|
|
377
|
+
export class ValidationMiddleware {
|
|
378
|
+
generate(route: RouteMetadata) {
|
|
379
|
+
return (request: HttpRequest, reply: HttpResponse, done: HttpCallback) => {
|
|
380
|
+
// Custom validation logic based on route metadata
|
|
381
|
+
done();
|
|
382
|
+
};
|
|
383
|
+
}
|
|
384
|
+
}
|
|
385
|
+
```
|
|
386
|
+
|
|
387
|
+
## Validation
|
|
388
|
+
|
|
389
|
+
The package uses `@shadow-library/class-schema` for automatic validation:
|
|
390
|
+
|
|
391
|
+
```typescript
|
|
392
|
+
@Schema()
|
|
393
|
+
class CreateProductDto {
|
|
394
|
+
@Field(() => String, {
|
|
395
|
+
minLength: 1,
|
|
396
|
+
maxLength: 100,
|
|
397
|
+
description: 'Product name',
|
|
398
|
+
})
|
|
399
|
+
name: string;
|
|
400
|
+
|
|
401
|
+
@Field(() => Number, {
|
|
402
|
+
minimum: 0,
|
|
403
|
+
description: 'Product price in cents',
|
|
404
|
+
})
|
|
405
|
+
price: number;
|
|
406
|
+
|
|
407
|
+
@Field(() => [String], {
|
|
408
|
+
maxItems: 10,
|
|
409
|
+
description: 'Product tags',
|
|
410
|
+
})
|
|
411
|
+
tags?: string[];
|
|
412
|
+
}
|
|
413
|
+
```
|
|
414
|
+
|
|
415
|
+
## Response Serialization
|
|
416
|
+
|
|
417
|
+
Define response schemas for automatic serialization and documentation:
|
|
418
|
+
|
|
419
|
+
```typescript
|
|
420
|
+
@Schema()
|
|
421
|
+
class ProductResponse {
|
|
422
|
+
@Field(() => Number)
|
|
423
|
+
id: number;
|
|
424
|
+
|
|
425
|
+
@Field(() => String)
|
|
426
|
+
name: string;
|
|
427
|
+
|
|
428
|
+
@Field(() => Number)
|
|
429
|
+
price: number;
|
|
430
|
+
|
|
431
|
+
@Field(() => Date)
|
|
432
|
+
createdAt: Date;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
@HttpController('/products')
|
|
436
|
+
export class ProductController {
|
|
437
|
+
@Get('/:id')
|
|
438
|
+
@RespondFor(200, ProductResponse)
|
|
439
|
+
@RespondFor(404, ErrorResponse)
|
|
440
|
+
async getProduct(@Params() params: { id: number }): Promise<ProductResponse> {
|
|
441
|
+
// Only the fields defined in ProductResponse will be serialized
|
|
442
|
+
return this.productService.findById(params.id);
|
|
443
|
+
}
|
|
444
|
+
}
|
|
445
|
+
```
|
|
446
|
+
|
|
447
|
+
## Examples
|
|
448
|
+
|
|
449
|
+
Check out the [examples](./examples) directory for complete working examples:
|
|
450
|
+
|
|
451
|
+
- **hello-world**: Basic HTTP controller with GET/POST routes
|
|
452
|
+
- **user-auth**: Advanced example with authentication guards
|
|
453
|
+
- **child-routes**: Route resolution and unified endpoints
|
|
454
|
+
|
|
455
|
+
## Contributing
|
|
456
|
+
|
|
457
|
+
Contributions are welcome! Please read the [Contributing Guide](./CONTRIBUTING.md) for details.
|
|
458
|
+
|
|
22
459
|
## License
|
|
23
460
|
|
|
24
|
-
This package is licensed under the MIT License. See the
|
|
461
|
+
This package is licensed under the MIT License. See the [LICENSE](./LICENSE) file for more information.
|
|
@@ -6,28 +6,29 @@ const constants_1 = require("../constants.js");
|
|
|
6
6
|
const server_error_1 = require("../server.error.js");
|
|
7
7
|
const unexpectedError = new server_error_1.ServerError(server_error_1.ServerErrorCode.S001);
|
|
8
8
|
const validationError = new server_error_1.ServerError(server_error_1.ServerErrorCode.S003);
|
|
9
|
+
const invalidRequestError = new server_error_1.ServerError(server_error_1.ServerErrorCode.S006);
|
|
9
10
|
class DefaultErrorHandler {
|
|
10
11
|
logger = common_1.Logger.getLogger(constants_1.NAMESPACE, 'DefaultErrorHandler');
|
|
11
12
|
parseFastifyError(err) {
|
|
12
13
|
if (err.statusCode === 500)
|
|
13
14
|
return { statusCode: 500, error: unexpectedError.toObject() };
|
|
14
|
-
return { statusCode: err.statusCode, error: {
|
|
15
|
+
return { statusCode: err.statusCode, error: { ...invalidRequestError.toObject(), message: err.message } };
|
|
15
16
|
}
|
|
16
17
|
handle(err, _req, res) {
|
|
17
18
|
this.logger.warn('Handling error', err);
|
|
18
19
|
if (err.cause)
|
|
19
|
-
this.logger.warn('Caused by
|
|
20
|
+
this.logger.warn('Caused by', err.cause);
|
|
20
21
|
if (err instanceof server_error_1.ServerError)
|
|
21
22
|
return res.status(err.getStatusCode()).send(err.toObject());
|
|
22
23
|
else if (err instanceof common_1.ValidationError)
|
|
23
|
-
return res.status(
|
|
24
|
+
return res.status(validationError.getStatusCode()).send({ ...err.toObject(), ...validationError.toObject() });
|
|
24
25
|
else if (err instanceof common_1.AppError)
|
|
25
26
|
return res.status(500).send(err.toObject());
|
|
26
27
|
else if (err.name === 'FastifyError') {
|
|
27
28
|
const { statusCode, error } = this.parseFastifyError(err);
|
|
28
29
|
return res.status(statusCode).send(error);
|
|
29
30
|
}
|
|
30
|
-
this.logger.error('
|
|
31
|
+
this.logger.error('Unhandled error has occurred', err);
|
|
31
32
|
return res.status(500).send(unexpectedError.toObject());
|
|
32
33
|
}
|
|
33
34
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { JSONSchema } from '@shadow-library/class-schema';
|
|
2
|
+
import { Class } from 'type-fest';
|
|
2
3
|
export declare enum RouteInputType {
|
|
3
4
|
BODY = "body",
|
|
4
5
|
PARAMS = "params",
|
|
@@ -6,7 +7,7 @@ export declare enum RouteInputType {
|
|
|
6
7
|
REQUEST = "request",
|
|
7
8
|
RESPONSE = "response"
|
|
8
9
|
}
|
|
9
|
-
export type RouteInputSchemas = Partial<Record<'body' | 'params' | 'query', JSONSchema
|
|
10
|
+
export type RouteInputSchemas = Partial<Record<'body' | 'params' | 'query', JSONSchema | Class<unknown>>>;
|
|
10
11
|
export declare function HttpInput(type: RouteInputType, schema?: JSONSchema): ParameterDecorator;
|
|
11
12
|
export declare const Body: (schema?: JSONSchema) => ParameterDecorator;
|
|
12
13
|
export declare const Params: (schema?: JSONSchema) => ParameterDecorator;
|
|
@@ -5,9 +5,8 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.Res = exports.Response = exports.Req = exports.Request = exports.Query = exports.Params = exports.Body = exports.RouteInputType = void 0;
|
|
7
7
|
exports.HttpInput = HttpInput;
|
|
8
|
-
const
|
|
8
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
9
9
|
const app_1 = require("@shadow-library/app");
|
|
10
|
-
const class_schema_1 = require("@shadow-library/class-schema");
|
|
11
10
|
const constants_1 = require("../constants.js");
|
|
12
11
|
var RouteInputType;
|
|
13
12
|
(function (RouteInputType) {
|
|
@@ -19,16 +18,16 @@ var RouteInputType;
|
|
|
19
18
|
})(RouteInputType || (exports.RouteInputType = RouteInputType = {}));
|
|
20
19
|
function HttpInput(type, schema) {
|
|
21
20
|
return (target, propertyKey, index) => {
|
|
22
|
-
(0,
|
|
21
|
+
(0, node_assert_1.default)(propertyKey, 'Cannot apply decorator to a constructor parameter');
|
|
23
22
|
const inputs = Reflect.getMetadata(constants_1.HTTP_CONTROLLER_INPUTS, target, propertyKey) ?? [];
|
|
24
23
|
Reflect.defineMetadata(constants_1.HTTP_CONTROLLER_INPUTS, inputs, target, propertyKey);
|
|
25
24
|
inputs[index] = type;
|
|
26
25
|
if (!schema) {
|
|
27
26
|
const paramTypes = Reflect.getMetadata(constants_1.PARAMTYPES_METADATA, target, propertyKey);
|
|
28
|
-
schema =
|
|
27
|
+
schema = paramTypes[index];
|
|
29
28
|
}
|
|
30
29
|
const descriptor = Reflect.getOwnPropertyDescriptor(target, propertyKey);
|
|
31
|
-
(0,
|
|
30
|
+
(0, node_assert_1.default)(descriptor, 'Cannot apply decorator to a non-method');
|
|
32
31
|
(0, app_1.Route)({ schemas: { [type]: schema } })(target, propertyKey, descriptor);
|
|
33
32
|
};
|
|
34
33
|
}
|
package/cjs/decorators/index.js
CHANGED
|
@@ -19,3 +19,4 @@ __exportStar(require("./http-input.decorator.js"), exports);
|
|
|
19
19
|
__exportStar(require("./http-output.decorator.js"), exports);
|
|
20
20
|
__exportStar(require("./http-route.decorator.js"), exports);
|
|
21
21
|
__exportStar(require("./middleware.decorator.js"), exports);
|
|
22
|
+
__exportStar(require("./sensitive.decorator.js"), exports);
|
|
@@ -4,7 +4,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
6
|
exports.Middleware = Middleware;
|
|
7
|
-
const
|
|
7
|
+
const node_assert_1 = __importDefault(require("node:assert"));
|
|
8
8
|
const app_1 = require("@shadow-library/app");
|
|
9
9
|
const constants_1 = require("../constants.js");
|
|
10
10
|
const propertyKeys = ['generate', 'use'];
|
|
@@ -15,7 +15,7 @@ function Middleware(options = {}) {
|
|
|
15
15
|
options.weight = 0;
|
|
16
16
|
return target => {
|
|
17
17
|
const key = propertyKeys.find(key => key in target.prototype);
|
|
18
|
-
(0,
|
|
18
|
+
(0, node_assert_1.default)(key, `Cannot apply @Middleware to a class without a 'generate()' or 'use()' method`);
|
|
19
19
|
(0, app_1.Controller)({ ...options, [constants_1.HTTP_CONTROLLER_TYPE]: 'middleware', generates: key === 'generate' })(target);
|
|
20
20
|
};
|
|
21
21
|
}
|
|
@@ -0,0 +1,4 @@
|
|
|
1
|
+
import { MaskOptions } from '@shadow-library/common';
|
|
2
|
+
export type SensitiveDataType = 'secret' | 'email' | 'number' | 'words';
|
|
3
|
+
export declare function Sensitive(type?: SensitiveDataType): PropertyDecorator;
|
|
4
|
+
export declare function Sensitive(maskOptions: MaskOptions): PropertyDecorator;
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.Sensitive = Sensitive;
|
|
4
|
+
const class_schema_1 = require("@shadow-library/class-schema");
|
|
5
|
+
function Sensitive(typeOrMaskOptions = 'secret') {
|
|
6
|
+
return (target, propertyKey) => {
|
|
7
|
+
const options = { sensitive: true };
|
|
8
|
+
if (typeof typeOrMaskOptions === 'string')
|
|
9
|
+
options.type = typeOrMaskOptions;
|
|
10
|
+
else
|
|
11
|
+
options.maskOptions = typeOrMaskOptions;
|
|
12
|
+
const decorator = (0, class_schema_1.FieldMetadata)({ 'x-fastify': options });
|
|
13
|
+
decorator(target, propertyKey);
|
|
14
|
+
};
|
|
15
|
+
}
|
|
@@ -1,9 +1,12 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import { HttpRequest, HttpResponse } from './route-handler.interface.js';
|
|
3
|
-
export type MiddlewareHandler = (request: HttpRequest, response: HttpResponse) => Promise<any>;
|
|
1
|
+
import { RouteMetadata } from '@shadow-library/app';
|
|
2
|
+
import { HttpCallback, HttpRequest, HttpResponse, RouteHandler } from './route-handler.interface.js';
|
|
4
3
|
export interface MiddlewareGenerator {
|
|
5
|
-
|
|
4
|
+
cacheKey?: (metadata: RouteMetadata) => string;
|
|
5
|
+
generate(metadata: RouteMetadata): RouteHandler | undefined | Promise<RouteHandler | undefined>;
|
|
6
6
|
}
|
|
7
|
-
export interface
|
|
8
|
-
use(request: HttpRequest, response: HttpResponse): Promise<
|
|
7
|
+
export interface AsyncHttpMiddleware {
|
|
8
|
+
use(request: HttpRequest, response: HttpResponse): Promise<unknown>;
|
|
9
|
+
}
|
|
10
|
+
export interface CallbackHttpMiddleware {
|
|
11
|
+
use(request: HttpRequest, response: HttpResponse, done: HttpCallback): void;
|
|
9
12
|
}
|
|
@@ -1,4 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { SyncValue } from '@shadow-library/common';
|
|
2
|
+
import { DoneFuncWithErrOrRes, FastifyReply, FastifyRequest } from 'fastify';
|
|
2
3
|
export type HttpRequest = FastifyRequest;
|
|
3
4
|
export type HttpResponse = FastifyReply;
|
|
4
|
-
export type
|
|
5
|
+
export type HttpCallback = DoneFuncWithErrOrRes;
|
|
6
|
+
export type CallbackRouteHandler = (request: HttpRequest, response: HttpResponse, done: HttpCallback) => SyncValue<unknown>;
|
|
7
|
+
export type AsyncRouteHandler = (request: HttpRequest, response: HttpResponse) => Promise<unknown>;
|
|
8
|
+
export type RouteHandler<T extends (...args: any[]) => any = any> = ReturnType<T> extends Promise<unknown> ? AsyncRouteHandler : CallbackRouteHandler;
|
|
@@ -1,24 +1,25 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { RouteMetadata } from '@shadow-library/app';
|
|
2
2
|
import { JSONSchema } from '@shadow-library/class-schema';
|
|
3
3
|
import { RouteShorthandOptions } from 'fastify';
|
|
4
4
|
import { HTTP_CONTROLLER_TYPE } from '../constants.js';
|
|
5
5
|
import { HttpMethod, RouteInputSchemas } from '../decorators/index.js';
|
|
6
6
|
declare module '@shadow-library/app' {
|
|
7
|
-
interface
|
|
7
|
+
interface RouteMetadata extends Omit<RouteShorthandOptions, 'config'> {
|
|
8
8
|
method?: HttpMethod;
|
|
9
9
|
path?: string;
|
|
10
10
|
schemas?: RouteInputSchemas & {
|
|
11
11
|
response?: Record<number | string, JSONSchema>;
|
|
12
12
|
};
|
|
13
13
|
rawBody?: boolean;
|
|
14
|
+
silentValidation?: boolean;
|
|
14
15
|
status?: number;
|
|
15
16
|
headers?: Record<string, string | (() => string)>;
|
|
16
17
|
redirect?: string;
|
|
17
18
|
render?: string | true;
|
|
18
19
|
}
|
|
19
|
-
interface
|
|
20
|
+
interface ControllerMetadata {
|
|
20
21
|
[HTTP_CONTROLLER_TYPE]?: 'router' | 'middleware';
|
|
21
22
|
path?: string;
|
|
22
23
|
}
|
|
23
24
|
}
|
|
24
|
-
export type ServerMetadata =
|
|
25
|
+
export type ServerMetadata = RouteMetadata;
|