@avleon/core 0.0.46 → 0.0.48
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/License +21 -21
- package/README.md +666 -666
- package/dist/chunk-9hOWP6kD.cjs +64 -0
- package/dist/chunk-DORXReHP.js +37 -0
- package/dist/index-BxIMWhgy.d.ts +1284 -0
- package/dist/index-DPn7qtzq.d.cts +1283 -0
- package/dist/index.cjs +3194 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.js +3022 -79
- package/dist/index.js.map +1 -0
- package/dist/lib-Bk8hUm06.cjs +7847 -0
- package/dist/lib-Bk8hUm06.cjs.map +1 -0
- package/dist/lib-CvDxBMkR.js +7843 -0
- package/dist/lib-CvDxBMkR.js.map +1 -0
- package/package.json +41 -103
- package/dist/application.test.d.ts +0 -1
- package/dist/application.test.js +0 -15
- package/dist/authentication.d.ts +0 -13
- package/dist/authentication.js +0 -16
- package/dist/cache.d.ts +0 -12
- package/dist/cache.js +0 -78
- package/dist/cache.test.d.ts +0 -1
- package/dist/cache.test.js +0 -36
- package/dist/collection.d.ts +0 -43
- package/dist/collection.js +0 -231
- package/dist/collection.test.d.ts +0 -1
- package/dist/collection.test.js +0 -59
- package/dist/config.d.ts +0 -18
- package/dist/config.js +0 -58
- package/dist/config.test.d.ts +0 -1
- package/dist/config.test.js +0 -40
- package/dist/constants.d.ts +0 -1
- package/dist/constants.js +0 -4
- package/dist/container.d.ts +0 -30
- package/dist/container.js +0 -55
- package/dist/controller.d.ts +0 -50
- package/dist/controller.js +0 -71
- package/dist/controller.test.d.ts +0 -1
- package/dist/controller.test.js +0 -97
- package/dist/core/application.d.ts +0 -74
- package/dist/core/application.js +0 -424
- package/dist/core/router.d.ts +0 -44
- package/dist/core/router.js +0 -520
- package/dist/core/testing.d.ts +0 -21
- package/dist/core/testing.js +0 -104
- package/dist/core/types.d.ts +0 -67
- package/dist/core/types.js +0 -2
- package/dist/decorators.d.ts +0 -15
- package/dist/decorators.js +0 -41
- package/dist/environment-variables.d.ts +0 -49
- package/dist/environment-variables.js +0 -130
- package/dist/environment-variables.test.d.ts +0 -1
- package/dist/environment-variables.test.js +0 -70
- package/dist/event-dispatcher.d.ts +0 -22
- package/dist/event-dispatcher.js +0 -97
- package/dist/event-subscriber.d.ts +0 -14
- package/dist/event-subscriber.js +0 -87
- package/dist/exceptions/http-exceptions.d.ts +0 -50
- package/dist/exceptions/http-exceptions.js +0 -85
- package/dist/exceptions/index.d.ts +0 -1
- package/dist/exceptions/index.js +0 -17
- package/dist/exceptions/system-exception.d.ts +0 -22
- package/dist/exceptions/system-exception.js +0 -26
- package/dist/file-storage.d.ts +0 -69
- package/dist/file-storage.js +0 -323
- package/dist/file-storage.test.d.ts +0 -1
- package/dist/file-storage.test.js +0 -117
- package/dist/helpers.d.ts +0 -11
- package/dist/helpers.js +0 -27
- package/dist/helpers.test.d.ts +0 -1
- package/dist/helpers.test.js +0 -95
- package/dist/index.d.ts +0 -57
- package/dist/interfaces/avleon-application.d.ts +0 -75
- package/dist/interfaces/avleon-application.js +0 -2
- package/dist/kenx-provider.d.ts +0 -7
- package/dist/kenx-provider.js +0 -44
- package/dist/kenx-provider.test.d.ts +0 -1
- package/dist/kenx-provider.test.js +0 -36
- package/dist/logger.d.ts +0 -12
- package/dist/logger.js +0 -87
- package/dist/logger.test.d.ts +0 -1
- package/dist/logger.test.js +0 -42
- package/dist/map-types.d.ts +0 -17
- package/dist/map-types.js +0 -89
- package/dist/middleware.d.ts +0 -34
- package/dist/middleware.js +0 -73
- package/dist/middleware.test.d.ts +0 -1
- package/dist/middleware.test.js +0 -121
- package/dist/multipart.d.ts +0 -17
- package/dist/multipart.js +0 -70
- package/dist/multipart.test.d.ts +0 -1
- package/dist/multipart.test.js +0 -87
- package/dist/openapi.d.ts +0 -410
- package/dist/openapi.js +0 -59
- package/dist/openapi.test.d.ts +0 -1
- package/dist/openapi.test.js +0 -111
- package/dist/params.d.ts +0 -17
- package/dist/params.js +0 -64
- package/dist/params.test.d.ts +0 -1
- package/dist/params.test.js +0 -83
- package/dist/queue.d.ts +0 -29
- package/dist/queue.js +0 -84
- package/dist/response.d.ts +0 -16
- package/dist/response.js +0 -56
- package/dist/results.d.ts +0 -20
- package/dist/results.js +0 -32
- package/dist/route-methods.d.ts +0 -25
- package/dist/route-methods.js +0 -60
- package/dist/route-methods.test.d.ts +0 -1
- package/dist/route-methods.test.js +0 -129
- package/dist/swagger-schema.d.ts +0 -37
- package/dist/swagger-schema.js +0 -454
- package/dist/swagger-schema.test.d.ts +0 -1
- package/dist/swagger-schema.test.js +0 -125
- package/dist/types/app-builder.interface.d.ts +0 -15
- package/dist/types/app-builder.interface.js +0 -8
- package/dist/types/application.interface.d.ts +0 -8
- package/dist/types/application.interface.js +0 -2
- package/dist/utils/common-utils.d.ts +0 -17
- package/dist/utils/common-utils.js +0 -108
- package/dist/utils/di-utils.d.ts +0 -1
- package/dist/utils/di-utils.js +0 -22
- package/dist/utils/hash.d.ts +0 -2
- package/dist/utils/hash.js +0 -11
- package/dist/utils/index.d.ts +0 -2
- package/dist/utils/index.js +0 -18
- package/dist/utils/object-utils.d.ts +0 -11
- package/dist/utils/object-utils.js +0 -198
- package/dist/utils/optional-require.d.ts +0 -8
- package/dist/utils/optional-require.js +0 -70
- package/dist/utils/validation-utils.d.ts +0 -13
- package/dist/utils/validation-utils.js +0 -119
- package/dist/validation.d.ts +0 -39
- package/dist/validation.js +0 -108
- package/dist/validation.test.d.ts +0 -1
- package/dist/validation.test.js +0 -61
- package/dist/validator-extend.d.ts +0 -7
- package/dist/validator-extend.js +0 -28
- package/dist/websocket.d.ts +0 -10
- package/dist/websocket.js +0 -21
- package/dist/websocket.test.d.ts +0 -1
- package/dist/websocket.test.js +0 -27
package/README.md
CHANGED
|
@@ -1,667 +1,667 @@
|
|
|
1
|
-
# Avleon
|
|
2
|
-
|
|
3
|
-

|
|
4
|
-

|
|
5
|
-

|
|
6
|
-
|
|
7
|
-
> **🚧 This project is in active development. APIs may change between versions.**
|
|
8
|
-
|
|
9
|
-
Avleon is a TypeScript-first web framework built on top of [Fastify](https://fastify.dev), designed for building scalable, maintainable REST APIs with minimal boilerplate. It provides decorator-based routing, built-in dependency injection, automatic OpenAPI documentation, and first-class validation support.
|
|
10
|
-
|
|
11
|
-
---
|
|
12
|
-
|
|
13
|
-
## Table of Contents
|
|
14
|
-
|
|
15
|
-
- [Features](#features)
|
|
16
|
-
- [Installation](#installation)
|
|
17
|
-
- [Quick Start](#quick-start)
|
|
18
|
-
- [Core Concepts](#core-concepts)
|
|
19
|
-
- [Application](#application)
|
|
20
|
-
- [Controllers](#controllers)
|
|
21
|
-
- [Route Methods](#route-methods)
|
|
22
|
-
- [Parameter Decorators](#parameter-decorators)
|
|
23
|
-
- [Error Handling](#error-handling)
|
|
24
|
-
- [Middleware](#middleware)
|
|
25
|
-
- [Authorization](#authorization)
|
|
26
|
-
- [Validation](#validation)
|
|
27
|
-
- [OpenAPI Documentation](#openapi-documentation)
|
|
28
|
-
- [Advanced Features](#advanced-features)
|
|
29
|
-
- [Database — Knex](#database--knex)
|
|
30
|
-
- [Database — TypeORM](#database--typeorm)
|
|
31
|
-
- [File Uploads](#file-uploads)
|
|
32
|
-
- [Static Files](#static-files)
|
|
33
|
-
- [WebSocket (Socket.IO)](#websocket-socketio)
|
|
34
|
-
- [Route Mapping (Functional Style)](#route-mapping-functional-style)
|
|
35
|
-
- [Testing](#testing)
|
|
36
|
-
- [License](#license)
|
|
37
|
-
|
|
38
|
-
---
|
|
39
|
-
|
|
40
|
-
## Features
|
|
41
|
-
|
|
42
|
-
- 🎯 **Decorator-based routing** — define controllers and routes with TypeScript decorators
|
|
43
|
-
- 💉 **Dependency injection** — powered by [TypeDI](https://github.com/typestack/typedi)
|
|
44
|
-
- 📄 **OpenAPI / Swagger** — automatic docs with Swagger UI or [Scalar](https://scalar.com)
|
|
45
|
-
- ✅ **Validation** — request validation via [class-validator](https://github.com/typestack/class-validator)
|
|
46
|
-
- 🔒 **Authorization** — flexible middleware-based auth system
|
|
47
|
-
- 📁 **File uploads** — multipart form support out of the box
|
|
48
|
-
- 🗄️ **Database** — TypeORM and Knex integrations
|
|
49
|
-
- 🔌 **WebSocket** — Socket.IO integration
|
|
50
|
-
- 🧪 **Testing** — built-in test utilities
|
|
51
|
-
|
|
52
|
-
---
|
|
53
|
-
|
|
54
|
-
## Installation
|
|
55
|
-
|
|
56
|
-
Scaffold a new project using the CLI:
|
|
57
|
-
|
|
58
|
-
```bash
|
|
59
|
-
npx @avleon/cli new myapp
|
|
60
|
-
# or
|
|
61
|
-
yarn dlx @avleon/cli new myapp
|
|
62
|
-
# or
|
|
63
|
-
pnpm dlx @avleon/cli new myapp
|
|
64
|
-
```
|
|
65
|
-
|
|
66
|
-
Or install manually:
|
|
67
|
-
|
|
68
|
-
```bash
|
|
69
|
-
npm install @avleon/core reflect-metadata class-validator class-transformer
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
---
|
|
73
|
-
|
|
74
|
-
## Quick Start
|
|
75
|
-
|
|
76
|
-
### Minimal (functional style)
|
|
77
|
-
|
|
78
|
-
```typescript
|
|
79
|
-
import { Avleon } from '@avleon/core';
|
|
80
|
-
|
|
81
|
-
const app = Avleon.createApplication();
|
|
82
|
-
|
|
83
|
-
app.mapGet('/', () => ({ message: 'Hello, Avleon!' }));
|
|
84
|
-
|
|
85
|
-
app.run(4000);
|
|
86
|
-
```
|
|
87
|
-
|
|
88
|
-
### Controller style
|
|
89
|
-
|
|
90
|
-
```typescript
|
|
91
|
-
import { Avleon, ApiController, Get } from '@avleon/core';
|
|
92
|
-
|
|
93
|
-
@ApiController('/')
|
|
94
|
-
class HelloController {
|
|
95
|
-
@Get()
|
|
96
|
-
sayHello() {
|
|
97
|
-
return { message: 'Hello, Avleon!' };
|
|
98
|
-
}
|
|
99
|
-
}
|
|
100
|
-
|
|
101
|
-
const app = Avleon.createApplication();
|
|
102
|
-
app.useControllers([HelloController]);
|
|
103
|
-
app.run(4000);
|
|
104
|
-
```
|
|
105
|
-
|
|
106
|
-
---
|
|
107
|
-
|
|
108
|
-
## Core Concepts
|
|
109
|
-
|
|
110
|
-
### Application
|
|
111
|
-
|
|
112
|
-
```typescript
|
|
113
|
-
import { Avleon } from '@avleon/core';
|
|
114
|
-
|
|
115
|
-
const app = Avleon.createApplication();
|
|
116
|
-
|
|
117
|
-
app.useCors({ origin: '*' });
|
|
118
|
-
app.useControllers([UserController]);
|
|
119
|
-
// Auto-discover controllers from a directory:
|
|
120
|
-
// app.useControllers({ auto: true, path: 'src/controllers' });
|
|
121
|
-
|
|
122
|
-
app.run(4000);
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
---
|
|
126
|
-
|
|
127
|
-
### Controllers
|
|
128
|
-
|
|
129
|
-
```typescript
|
|
130
|
-
import { ApiController, Get, Post, Put, Delete } from '@avleon/core';
|
|
131
|
-
|
|
132
|
-
@ApiController('/users')
|
|
133
|
-
class UserController {
|
|
134
|
-
@Get('/')
|
|
135
|
-
getAll() { ... }
|
|
136
|
-
|
|
137
|
-
@Post('/')
|
|
138
|
-
create() { ... }
|
|
139
|
-
|
|
140
|
-
@Put('/:id')
|
|
141
|
-
update() { ... }
|
|
142
|
-
|
|
143
|
-
@Delete('/:id')
|
|
144
|
-
remove() { ... }
|
|
145
|
-
}
|
|
146
|
-
```
|
|
147
|
-
|
|
148
|
-
---
|
|
149
|
-
|
|
150
|
-
### Route Methods
|
|
151
|
-
|
|
152
|
-
| Decorator | HTTP Method |
|
|
153
|
-
|-----------|-------------|
|
|
154
|
-
| `@Get(path?)` | GET |
|
|
155
|
-
| `@Post(path?)` | POST |
|
|
156
|
-
| `@Put(path?)` | PUT |
|
|
157
|
-
| `@Patch(path?)` | PATCH |
|
|
158
|
-
| `@Delete(path?)` | DELETE |
|
|
159
|
-
|
|
160
|
-
---
|
|
161
|
-
|
|
162
|
-
### Parameter Decorators
|
|
163
|
-
|
|
164
|
-
```typescript
|
|
165
|
-
@Get('/:id')
|
|
166
|
-
async getUser(
|
|
167
|
-
@Param('id') id: string,
|
|
168
|
-
@Query('include') include: string,
|
|
169
|
-
@Query() query: UserQuery, // maps full query to a DTO
|
|
170
|
-
@Body() body: CreateUserDto,
|
|
171
|
-
@Header('authorization') token: string,
|
|
172
|
-
@AuthUser() user: CurrentUser,
|
|
173
|
-
) {
|
|
174
|
-
// ...
|
|
175
|
-
}
|
|
176
|
-
```
|
|
177
|
-
|
|
178
|
-
| Decorator | Source |
|
|
179
|
-
|-----------|--------|
|
|
180
|
-
| `@Param(key?)` | Route path params |
|
|
181
|
-
| `@Query(key?)` | Query string |
|
|
182
|
-
| `@Body()` | Request body |
|
|
183
|
-
| `@Header(key?)` | Request headers |
|
|
184
|
-
| `@AuthUser()` | Current authenticated user |
|
|
185
|
-
|
|
186
|
-
---
|
|
187
|
-
|
|
188
|
-
### Error Handling
|
|
189
|
-
|
|
190
|
-
```typescript
|
|
191
|
-
import { HttpExceptions, HttpResponse } from '@avleon/core';
|
|
192
|
-
|
|
193
|
-
@Get('/:id')
|
|
194
|
-
async getUser(@Param('id') id: string) {
|
|
195
|
-
const user = await this.userService.findById(id);
|
|
196
|
-
|
|
197
|
-
if (!user) {
|
|
198
|
-
throw HttpExceptions.NotFound('User not found');
|
|
199
|
-
}
|
|
200
|
-
|
|
201
|
-
return HttpResponse.Ok(user);
|
|
202
|
-
}
|
|
203
|
-
```
|
|
204
|
-
|
|
205
|
-
Available exceptions: `NotFound`, `BadRequest`, `Unauthorized`, `Forbidden`, `InternalServerError`.
|
|
206
|
-
|
|
207
|
-
---
|
|
208
|
-
|
|
209
|
-
### Middleware
|
|
210
|
-
|
|
211
|
-
```typescript
|
|
212
|
-
import { Middleware, AppMiddleware, IRequest, UseMiddleware } from '@avleon/core';
|
|
213
|
-
|
|
214
|
-
@Middleware
|
|
215
|
-
class LoggingMiddleware extends AppMiddleware {
|
|
216
|
-
async invoke(req: IRequest) {
|
|
217
|
-
console.log(`${req.method} ${req.url}`);
|
|
218
|
-
return req;
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
// Apply to entire controller
|
|
223
|
-
@UseMiddleware(LoggingMiddleware)
|
|
224
|
-
@ApiController('/users')
|
|
225
|
-
class UserController { ... }
|
|
226
|
-
|
|
227
|
-
// Or apply to a specific route
|
|
228
|
-
@ApiController('/users')
|
|
229
|
-
class UserController {
|
|
230
|
-
@UseMiddleware(LoggingMiddleware)
|
|
231
|
-
@Get('/')
|
|
232
|
-
getAll() { ... }
|
|
233
|
-
}
|
|
234
|
-
```
|
|
235
|
-
|
|
236
|
-
---
|
|
237
|
-
|
|
238
|
-
### Authorization
|
|
239
|
-
|
|
240
|
-
**1 — Define your authorization class:**
|
|
241
|
-
|
|
242
|
-
```typescript
|
|
243
|
-
import { CanAuthorize, AuthorizeMiddleware, IRequest } from '@avleon/core';
|
|
244
|
-
|
|
245
|
-
@CanAuthorize
|
|
246
|
-
class JwtAuthorization extends AuthorizeMiddleware {
|
|
247
|
-
async authorize(req: IRequest, options?: any) {
|
|
248
|
-
const token = req.headers['authorization']?.split(' ')[1];
|
|
249
|
-
if (!token) throw HttpExceptions.Unauthorized('Missing token');
|
|
250
|
-
req.user = verifyToken(token); // attach user to request
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
```
|
|
254
|
-
|
|
255
|
-
**2 — Register with the app:**
|
|
256
|
-
|
|
257
|
-
```typescript
|
|
258
|
-
app.useAuthorization(JwtAuthorization);
|
|
259
|
-
```
|
|
260
|
-
|
|
261
|
-
**3 — Protect controllers or routes:**
|
|
262
|
-
|
|
263
|
-
```typescript
|
|
264
|
-
// Protect entire controller
|
|
265
|
-
@Authorized()
|
|
266
|
-
@ApiController('/admin')
|
|
267
|
-
class AdminController {
|
|
268
|
-
@Get('/')
|
|
269
|
-
dashboard(@AuthUser() user: User) {
|
|
270
|
-
return user;
|
|
271
|
-
}
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
// Protect specific route with roles
|
|
275
|
-
@ApiController('/admin')
|
|
276
|
-
class AdminController {
|
|
277
|
-
@Authorized({ roles: ['admin'] })
|
|
278
|
-
@Get('/stats')
|
|
279
|
-
stats() { ... }
|
|
280
|
-
}
|
|
281
|
-
```
|
|
282
|
-
|
|
283
|
-
---
|
|
284
|
-
|
|
285
|
-
### Validation
|
|
286
|
-
|
|
287
|
-
Validation is powered by `class-validator`. Decorate your DTOs and Avleon validates automatically:
|
|
288
|
-
|
|
289
|
-
```typescript
|
|
290
|
-
import { IsString, IsEmail, IsInt, Min, Max, IsOptional } from 'class-validator';
|
|
291
|
-
|
|
292
|
-
class CreateUserDto {
|
|
293
|
-
@IsString()
|
|
294
|
-
@IsNotEmpty()
|
|
295
|
-
name: string;
|
|
296
|
-
|
|
297
|
-
@IsEmail()
|
|
298
|
-
email: string;
|
|
299
|
-
|
|
300
|
-
@IsInt()
|
|
301
|
-
@Min(0)
|
|
302
|
-
@Max(120)
|
|
303
|
-
age: number;
|
|
304
|
-
|
|
305
|
-
@IsOptional()
|
|
306
|
-
@IsString()
|
|
307
|
-
role?: string;
|
|
308
|
-
}
|
|
309
|
-
|
|
310
|
-
@Post('/')
|
|
311
|
-
async createUser(@Body() body: CreateUserDto) {
|
|
312
|
-
return this.userService.create(body);
|
|
313
|
-
}
|
|
314
|
-
```
|
|
315
|
-
|
|
316
|
-
---
|
|
317
|
-
|
|
318
|
-
### OpenAPI Documentation
|
|
319
|
-
|
|
320
|
-
**Inline config:**
|
|
321
|
-
|
|
322
|
-
```typescript
|
|
323
|
-
app.useOpenApi({
|
|
324
|
-
info: {
|
|
325
|
-
title: 'User API',
|
|
326
|
-
version: '1.0.0',
|
|
327
|
-
description: 'API for managing users',
|
|
328
|
-
},
|
|
329
|
-
servers: [{ url: 'http://localhost:4000', description: 'Dev server' }],
|
|
330
|
-
components: {
|
|
331
|
-
securitySchemes: {
|
|
332
|
-
bearerAuth: {
|
|
333
|
-
type: 'http',
|
|
334
|
-
scheme: 'bearer',
|
|
335
|
-
bearerFormat: 'JWT',
|
|
336
|
-
},
|
|
337
|
-
},
|
|
338
|
-
},
|
|
339
|
-
});
|
|
340
|
-
```
|
|
341
|
-
|
|
342
|
-
**Config class:**
|
|
343
|
-
|
|
344
|
-
```typescript
|
|
345
|
-
import { AppConfig, IConfig, Environment } from '@avleon/core';
|
|
346
|
-
|
|
347
|
-
@AppConfig
|
|
348
|
-
export class OpenApiConfig implements IConfig {
|
|
349
|
-
config(env: Environment) {
|
|
350
|
-
return {
|
|
351
|
-
info: { title: 'My API', version: '1.0.0' },
|
|
352
|
-
routePrefix: '/docs',
|
|
353
|
-
provider: 'scalar', // or 'default' for Swagger UI
|
|
354
|
-
};
|
|
355
|
-
}
|
|
356
|
-
}
|
|
357
|
-
|
|
358
|
-
// In app.ts
|
|
359
|
-
if (app.isDevelopment()) {
|
|
360
|
-
app.useOpenApi(OpenApiConfig);
|
|
361
|
-
}
|
|
362
|
-
```
|
|
363
|
-
|
|
364
|
-
**Route-level docs with `@OpenApi`:**
|
|
365
|
-
|
|
366
|
-
```typescript
|
|
367
|
-
import { OpenApi, OpenApiProperty, OpenApiSchema } from '@avleon/core';
|
|
368
|
-
|
|
369
|
-
@OpenApiSchema()
|
|
370
|
-
export class UserQuery {
|
|
371
|
-
@OpenApiProperty({ type: 'string', example: 'john', required: false })
|
|
372
|
-
@IsOptional()
|
|
373
|
-
search?: string;
|
|
374
|
-
|
|
375
|
-
@OpenApiProperty({ type: 'integer', example: 1, required: false })
|
|
376
|
-
@IsOptional()
|
|
377
|
-
page?: number;
|
|
378
|
-
}
|
|
379
|
-
|
|
380
|
-
@OpenApi({
|
|
381
|
-
summary: 'Get all users',
|
|
382
|
-
tags: ['users'],
|
|
383
|
-
security: [{ bearerAuth: [] }],
|
|
384
|
-
response: {
|
|
385
|
-
200: {
|
|
386
|
-
description: 'List of users',
|
|
387
|
-
type: 'object',
|
|
388
|
-
properties: {
|
|
389
|
-
data: { type: 'array' },
|
|
390
|
-
total: { type: 'integer', example: 100 },
|
|
391
|
-
},
|
|
392
|
-
},
|
|
393
|
-
401: { description: 'Unauthorized' },
|
|
394
|
-
},
|
|
395
|
-
})
|
|
396
|
-
@Get('/')
|
|
397
|
-
getAll(@Query() query: UserQuery) { ... }
|
|
398
|
-
```
|
|
399
|
-
|
|
400
|
-
---
|
|
401
|
-
|
|
402
|
-
## Advanced Features
|
|
403
|
-
|
|
404
|
-
### Database — Knex
|
|
405
|
-
|
|
406
|
-
```typescript
|
|
407
|
-
app.useKnex({
|
|
408
|
-
client: 'mysql',
|
|
409
|
-
connection: {
|
|
410
|
-
host: '127.0.0.1',
|
|
411
|
-
port: 3306,
|
|
412
|
-
user: 'root',
|
|
413
|
-
password: 'password',
|
|
414
|
-
database: 'myapp',
|
|
415
|
-
},
|
|
416
|
-
});
|
|
417
|
-
```
|
|
418
|
-
|
|
419
|
-
Using a config class:
|
|
420
|
-
|
|
421
|
-
```typescript
|
|
422
|
-
@AppConfig
|
|
423
|
-
export class KnexConfig implements IConfig {
|
|
424
|
-
config(env: Environment) {
|
|
425
|
-
return {
|
|
426
|
-
client: 'mysql',
|
|
427
|
-
connection: {
|
|
428
|
-
host: env.get('DB_HOST') || '127.0.0.1',
|
|
429
|
-
port: env.get('DB_PORT') || 3306,
|
|
430
|
-
user: env.get('DB_USER') || 'root',
|
|
431
|
-
password: env.get('DB_PASS') || 'password',
|
|
432
|
-
database: env.get('DB_NAME') || 'myapp',
|
|
433
|
-
},
|
|
434
|
-
};
|
|
435
|
-
}
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
app.useKnex(KnexConfig);
|
|
439
|
-
```
|
|
440
|
-
|
|
441
|
-
Using in a service:
|
|
442
|
-
|
|
443
|
-
```typescript
|
|
444
|
-
import { DB, AppService } from '@avleon/core';
|
|
445
|
-
|
|
446
|
-
@AppService
|
|
447
|
-
export class UsersService {
|
|
448
|
-
constructor(private readonly db: DB) {}
|
|
449
|
-
|
|
450
|
-
async findAll() {
|
|
451
|
-
return this.db.client.select('*').from('users');
|
|
452
|
-
}
|
|
453
|
-
}
|
|
454
|
-
```
|
|
455
|
-
|
|
456
|
-
---
|
|
457
|
-
|
|
458
|
-
### Database — TypeORM
|
|
459
|
-
|
|
460
|
-
```typescript
|
|
461
|
-
app.useDataSource({
|
|
462
|
-
type: 'postgres',
|
|
463
|
-
host: 'localhost',
|
|
464
|
-
port: 5432,
|
|
465
|
-
username: 'postgres',
|
|
466
|
-
password: 'password',
|
|
467
|
-
database: 'avleon',
|
|
468
|
-
entities: [User],
|
|
469
|
-
synchronize: true,
|
|
470
|
-
});
|
|
471
|
-
```
|
|
472
|
-
|
|
473
|
-
Using a config class:
|
|
474
|
-
|
|
475
|
-
```typescript
|
|
476
|
-
@AppConfig
|
|
477
|
-
export class DataSourceConfig implements IConfig {
|
|
478
|
-
config(env: Environment) {
|
|
479
|
-
return {
|
|
480
|
-
type: 'postgres',
|
|
481
|
-
host: env.get('DB_HOST') || 'localhost',
|
|
482
|
-
port: Number(env.get('DB_PORT')) || 5432,
|
|
483
|
-
username: env.get('DB_USER') || 'postgres',
|
|
484
|
-
password: env.get('DB_PASS') || 'password',
|
|
485
|
-
database: env.get('DB_NAME') || 'avleon',
|
|
486
|
-
entities: [User],
|
|
487
|
-
synchronize: true,
|
|
488
|
-
};
|
|
489
|
-
}
|
|
490
|
-
}
|
|
491
|
-
|
|
492
|
-
app.useDataSource(DataSourceConfig);
|
|
493
|
-
```
|
|
494
|
-
|
|
495
|
-
Using in a service:
|
|
496
|
-
|
|
497
|
-
```typescript
|
|
498
|
-
import { AppService, InjectRepository } from '@avleon/core';
|
|
499
|
-
import { Repository } from 'typeorm';
|
|
500
|
-
import { User } from './user.entity';
|
|
501
|
-
|
|
502
|
-
@AppService
|
|
503
|
-
export class UserService {
|
|
504
|
-
constructor(
|
|
505
|
-
@InjectRepository(User)
|
|
506
|
-
private readonly userRepo: Repository<User>,
|
|
507
|
-
) {}
|
|
508
|
-
|
|
509
|
-
async findAll() {
|
|
510
|
-
return this.userRepo.find();
|
|
511
|
-
}
|
|
512
|
-
}
|
|
513
|
-
```
|
|
514
|
-
|
|
515
|
-
---
|
|
516
|
-
|
|
517
|
-
### File Uploads
|
|
518
|
-
|
|
519
|
-
```typescript
|
|
520
|
-
// Configure multipart support
|
|
521
|
-
app.useMultipart({
|
|
522
|
-
destination: path.join(process.cwd(), 'public/uploads'),
|
|
523
|
-
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
|
|
524
|
-
});
|
|
525
|
-
```
|
|
526
|
-
|
|
527
|
-
```typescript
|
|
528
|
-
import { FileStorage, UploadFile, MultipartFile } from '@avleon/core';
|
|
529
|
-
|
|
530
|
-
@ApiController('/files')
|
|
531
|
-
class FileController {
|
|
532
|
-
constructor(private readonly fileStorage: FileStorage) {}
|
|
533
|
-
|
|
534
|
-
@OpenApi({
|
|
535
|
-
description: 'Upload a single file',
|
|
536
|
-
body: {
|
|
537
|
-
type: 'object',
|
|
538
|
-
properties: {
|
|
539
|
-
file: { type: 'string', format: 'binary' },
|
|
540
|
-
},
|
|
541
|
-
required: ['file'],
|
|
542
|
-
},
|
|
543
|
-
})
|
|
544
|
-
@Post('/upload')
|
|
545
|
-
async upload(@UploadFile('file') file: MultipartFile) {
|
|
546
|
-
const result = await this.fileStorage.save(file);
|
|
547
|
-
// optionally rename: this.fileStorage.save(file, { as: 'newname.jpg' })
|
|
548
|
-
return result;
|
|
549
|
-
// { uploadPath: '/uploads/...', staticPath: '/static/...' }
|
|
550
|
-
}
|
|
551
|
-
}
|
|
552
|
-
```
|
|
553
|
-
|
|
554
|
-
---
|
|
555
|
-
|
|
556
|
-
### Static Files
|
|
557
|
-
|
|
558
|
-
```typescript
|
|
559
|
-
import path from 'path';
|
|
560
|
-
|
|
561
|
-
app.useStaticFiles({
|
|
562
|
-
path: path.join(process.cwd(), 'public'),
|
|
563
|
-
prefix: '/static/',
|
|
564
|
-
});
|
|
565
|
-
```
|
|
566
|
-
|
|
567
|
-
---
|
|
568
|
-
|
|
569
|
-
### WebSocket (Socket.IO)
|
|
570
|
-
|
|
571
|
-
```typescript
|
|
572
|
-
app.useSocketIo({ cors: { origin: '*' } });
|
|
573
|
-
```
|
|
574
|
-
|
|
575
|
-
Dispatch events from services:
|
|
576
|
-
|
|
577
|
-
```typescript
|
|
578
|
-
import { AppService, EventDispatcher } from '@avleon/core';
|
|
579
|
-
|
|
580
|
-
@AppService
|
|
581
|
-
export class UserService {
|
|
582
|
-
constructor(private readonly dispatcher: EventDispatcher) {}
|
|
583
|
-
|
|
584
|
-
async create(data: any) {
|
|
585
|
-
const user = await this.save(data);
|
|
586
|
-
await this.dispatcher.dispatch('users:created', { userId: user.id });
|
|
587
|
-
return user;
|
|
588
|
-
}
|
|
589
|
-
}
|
|
590
|
-
```
|
|
591
|
-
|
|
592
|
-
---
|
|
593
|
-
|
|
594
|
-
## Route Mapping (Functional Style)
|
|
595
|
-
|
|
596
|
-
For simple routes without a controller class:
|
|
597
|
-
|
|
598
|
-
```typescript
|
|
599
|
-
app.mapGet('/users', async (req, res) => {
|
|
600
|
-
return { users: [] };
|
|
601
|
-
});
|
|
602
|
-
|
|
603
|
-
app.mapPost('/users', async (req, res) => {
|
|
604
|
-
return { success: true };
|
|
605
|
-
});
|
|
606
|
-
|
|
607
|
-
app.mapPut('/users/:id', async (req, res) => {
|
|
608
|
-
return { success: true };
|
|
609
|
-
});
|
|
610
|
-
|
|
611
|
-
app.mapDelete('/users/:id', async (req, res) => {
|
|
612
|
-
return { success: true };
|
|
613
|
-
});
|
|
614
|
-
```
|
|
615
|
-
|
|
616
|
-
Add middleware and OpenAPI docs to functional routes:
|
|
617
|
-
|
|
618
|
-
```typescript
|
|
619
|
-
app
|
|
620
|
-
.mapGet('/users', async (req, res) => {
|
|
621
|
-
return { users: [] };
|
|
622
|
-
})
|
|
623
|
-
.useMiddleware([AuthMiddleware])
|
|
624
|
-
.useOpenApi({
|
|
625
|
-
summary: 'Get all users',
|
|
626
|
-
tags: ['users'],
|
|
627
|
-
security: [{ bearerAuth: [] }],
|
|
628
|
-
response: {
|
|
629
|
-
200: {
|
|
630
|
-
description: 'List of users',
|
|
631
|
-
type: 'array',
|
|
632
|
-
},
|
|
633
|
-
},
|
|
634
|
-
});
|
|
635
|
-
```
|
|
636
|
-
|
|
637
|
-
---
|
|
638
|
-
|
|
639
|
-
## Testing
|
|
640
|
-
|
|
641
|
-
```typescript
|
|
642
|
-
import { AvleonTest } from '@avleon/core';
|
|
643
|
-
import { UserController } from './user.controller';
|
|
644
|
-
|
|
645
|
-
describe('UserController', () => {
|
|
646
|
-
let controller: UserController;
|
|
647
|
-
|
|
648
|
-
beforeAll(() => {
|
|
649
|
-
controller = AvleonTest.getController(UserController);
|
|
650
|
-
});
|
|
651
|
-
|
|
652
|
-
it('should be defined', () => {
|
|
653
|
-
expect(controller).toBeDefined();
|
|
654
|
-
});
|
|
655
|
-
|
|
656
|
-
it('should return users', async () => {
|
|
657
|
-
const result = await controller.getAll();
|
|
658
|
-
expect(Array.isArray(result)).toBe(true);
|
|
659
|
-
});
|
|
660
|
-
});
|
|
661
|
-
```
|
|
662
|
-
|
|
663
|
-
---
|
|
664
|
-
|
|
665
|
-
## License
|
|
666
|
-
|
|
1
|
+
# Avleon
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
> **🚧 This project is in active development. APIs may change between versions.**
|
|
8
|
+
|
|
9
|
+
Avleon is a TypeScript-first web framework built on top of [Fastify](https://fastify.dev), designed for building scalable, maintainable REST APIs with minimal boilerplate. It provides decorator-based routing, built-in dependency injection, automatic OpenAPI documentation, and first-class validation support.
|
|
10
|
+
|
|
11
|
+
---
|
|
12
|
+
|
|
13
|
+
## Table of Contents
|
|
14
|
+
|
|
15
|
+
- [Features](#features)
|
|
16
|
+
- [Installation](#installation)
|
|
17
|
+
- [Quick Start](#quick-start)
|
|
18
|
+
- [Core Concepts](#core-concepts)
|
|
19
|
+
- [Application](#application)
|
|
20
|
+
- [Controllers](#controllers)
|
|
21
|
+
- [Route Methods](#route-methods)
|
|
22
|
+
- [Parameter Decorators](#parameter-decorators)
|
|
23
|
+
- [Error Handling](#error-handling)
|
|
24
|
+
- [Middleware](#middleware)
|
|
25
|
+
- [Authorization](#authorization)
|
|
26
|
+
- [Validation](#validation)
|
|
27
|
+
- [OpenAPI Documentation](#openapi-documentation)
|
|
28
|
+
- [Advanced Features](#advanced-features)
|
|
29
|
+
- [Database — Knex](#database--knex)
|
|
30
|
+
- [Database — TypeORM](#database--typeorm)
|
|
31
|
+
- [File Uploads](#file-uploads)
|
|
32
|
+
- [Static Files](#static-files)
|
|
33
|
+
- [WebSocket (Socket.IO)](#websocket-socketio)
|
|
34
|
+
- [Route Mapping (Functional Style)](#route-mapping-functional-style)
|
|
35
|
+
- [Testing](#testing)
|
|
36
|
+
- [License](#license)
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Features
|
|
41
|
+
|
|
42
|
+
- 🎯 **Decorator-based routing** — define controllers and routes with TypeScript decorators
|
|
43
|
+
- 💉 **Dependency injection** — powered by [TypeDI](https://github.com/typestack/typedi)
|
|
44
|
+
- 📄 **OpenAPI / Swagger** — automatic docs with Swagger UI or [Scalar](https://scalar.com)
|
|
45
|
+
- ✅ **Validation** — request validation via [class-validator](https://github.com/typestack/class-validator)
|
|
46
|
+
- 🔒 **Authorization** — flexible middleware-based auth system
|
|
47
|
+
- 📁 **File uploads** — multipart form support out of the box
|
|
48
|
+
- 🗄️ **Database** — TypeORM and Knex integrations
|
|
49
|
+
- 🔌 **WebSocket** — Socket.IO integration
|
|
50
|
+
- 🧪 **Testing** — built-in test utilities
|
|
51
|
+
|
|
52
|
+
---
|
|
53
|
+
|
|
54
|
+
## Installation
|
|
55
|
+
|
|
56
|
+
Scaffold a new project using the CLI:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
npx @avleon/cli new myapp
|
|
60
|
+
# or
|
|
61
|
+
yarn dlx @avleon/cli new myapp
|
|
62
|
+
# or
|
|
63
|
+
pnpm dlx @avleon/cli new myapp
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
Or install manually:
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
npm install @avleon/core reflect-metadata class-validator class-transformer
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
---
|
|
73
|
+
|
|
74
|
+
## Quick Start
|
|
75
|
+
|
|
76
|
+
### Minimal (functional style)
|
|
77
|
+
|
|
78
|
+
```typescript
|
|
79
|
+
import { Avleon } from '@avleon/core';
|
|
80
|
+
|
|
81
|
+
const app = Avleon.createApplication();
|
|
82
|
+
|
|
83
|
+
app.mapGet('/', () => ({ message: 'Hello, Avleon!' }));
|
|
84
|
+
|
|
85
|
+
app.run(4000);
|
|
86
|
+
```
|
|
87
|
+
|
|
88
|
+
### Controller style
|
|
89
|
+
|
|
90
|
+
```typescript
|
|
91
|
+
import { Avleon, ApiController, Get } from '@avleon/core';
|
|
92
|
+
|
|
93
|
+
@ApiController('/')
|
|
94
|
+
class HelloController {
|
|
95
|
+
@Get()
|
|
96
|
+
sayHello() {
|
|
97
|
+
return { message: 'Hello, Avleon!' };
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const app = Avleon.createApplication();
|
|
102
|
+
app.useControllers([HelloController]);
|
|
103
|
+
app.run(4000);
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
---
|
|
107
|
+
|
|
108
|
+
## Core Concepts
|
|
109
|
+
|
|
110
|
+
### Application
|
|
111
|
+
|
|
112
|
+
```typescript
|
|
113
|
+
import { Avleon } from '@avleon/core';
|
|
114
|
+
|
|
115
|
+
const app = Avleon.createApplication();
|
|
116
|
+
|
|
117
|
+
app.useCors({ origin: '*' });
|
|
118
|
+
app.useControllers([UserController]);
|
|
119
|
+
// Auto-discover controllers from a directory:
|
|
120
|
+
// app.useControllers({ auto: true, path: 'src/controllers' });
|
|
121
|
+
|
|
122
|
+
app.run(4000);
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
|
|
127
|
+
### Controllers
|
|
128
|
+
|
|
129
|
+
```typescript
|
|
130
|
+
import { ApiController, Get, Post, Put, Delete } from '@avleon/core';
|
|
131
|
+
|
|
132
|
+
@ApiController('/users')
|
|
133
|
+
class UserController {
|
|
134
|
+
@Get('/')
|
|
135
|
+
getAll() { ... }
|
|
136
|
+
|
|
137
|
+
@Post('/')
|
|
138
|
+
create() { ... }
|
|
139
|
+
|
|
140
|
+
@Put('/:id')
|
|
141
|
+
update() { ... }
|
|
142
|
+
|
|
143
|
+
@Delete('/:id')
|
|
144
|
+
remove() { ... }
|
|
145
|
+
}
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
---
|
|
149
|
+
|
|
150
|
+
### Route Methods
|
|
151
|
+
|
|
152
|
+
| Decorator | HTTP Method |
|
|
153
|
+
|-----------|-------------|
|
|
154
|
+
| `@Get(path?)` | GET |
|
|
155
|
+
| `@Post(path?)` | POST |
|
|
156
|
+
| `@Put(path?)` | PUT |
|
|
157
|
+
| `@Patch(path?)` | PATCH |
|
|
158
|
+
| `@Delete(path?)` | DELETE |
|
|
159
|
+
|
|
160
|
+
---
|
|
161
|
+
|
|
162
|
+
### Parameter Decorators
|
|
163
|
+
|
|
164
|
+
```typescript
|
|
165
|
+
@Get('/:id')
|
|
166
|
+
async getUser(
|
|
167
|
+
@Param('id') id: string,
|
|
168
|
+
@Query('include') include: string,
|
|
169
|
+
@Query() query: UserQuery, // maps full query to a DTO
|
|
170
|
+
@Body() body: CreateUserDto,
|
|
171
|
+
@Header('authorization') token: string,
|
|
172
|
+
@AuthUser() user: CurrentUser,
|
|
173
|
+
) {
|
|
174
|
+
// ...
|
|
175
|
+
}
|
|
176
|
+
```
|
|
177
|
+
|
|
178
|
+
| Decorator | Source |
|
|
179
|
+
|-----------|--------|
|
|
180
|
+
| `@Param(key?)` | Route path params |
|
|
181
|
+
| `@Query(key?)` | Query string |
|
|
182
|
+
| `@Body()` | Request body |
|
|
183
|
+
| `@Header(key?)` | Request headers |
|
|
184
|
+
| `@AuthUser()` | Current authenticated user |
|
|
185
|
+
|
|
186
|
+
---
|
|
187
|
+
|
|
188
|
+
### Error Handling
|
|
189
|
+
|
|
190
|
+
```typescript
|
|
191
|
+
import { HttpExceptions, HttpResponse } from '@avleon/core';
|
|
192
|
+
|
|
193
|
+
@Get('/:id')
|
|
194
|
+
async getUser(@Param('id') id: string) {
|
|
195
|
+
const user = await this.userService.findById(id);
|
|
196
|
+
|
|
197
|
+
if (!user) {
|
|
198
|
+
throw HttpExceptions.NotFound('User not found');
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return HttpResponse.Ok(user);
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
Available exceptions: `NotFound`, `BadRequest`, `Unauthorized`, `Forbidden`, `InternalServerError`.
|
|
206
|
+
|
|
207
|
+
---
|
|
208
|
+
|
|
209
|
+
### Middleware
|
|
210
|
+
|
|
211
|
+
```typescript
|
|
212
|
+
import { Middleware, AppMiddleware, IRequest, UseMiddleware } from '@avleon/core';
|
|
213
|
+
|
|
214
|
+
@Middleware
|
|
215
|
+
class LoggingMiddleware extends AppMiddleware {
|
|
216
|
+
async invoke(req: IRequest) {
|
|
217
|
+
console.log(`${req.method} ${req.url}`);
|
|
218
|
+
return req;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
// Apply to entire controller
|
|
223
|
+
@UseMiddleware(LoggingMiddleware)
|
|
224
|
+
@ApiController('/users')
|
|
225
|
+
class UserController { ... }
|
|
226
|
+
|
|
227
|
+
// Or apply to a specific route
|
|
228
|
+
@ApiController('/users')
|
|
229
|
+
class UserController {
|
|
230
|
+
@UseMiddleware(LoggingMiddleware)
|
|
231
|
+
@Get('/')
|
|
232
|
+
getAll() { ... }
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
---
|
|
237
|
+
|
|
238
|
+
### Authorization
|
|
239
|
+
|
|
240
|
+
**1 — Define your authorization class:**
|
|
241
|
+
|
|
242
|
+
```typescript
|
|
243
|
+
import { CanAuthorize, AuthorizeMiddleware, IRequest } from '@avleon/core';
|
|
244
|
+
|
|
245
|
+
@CanAuthorize
|
|
246
|
+
class JwtAuthorization extends AuthorizeMiddleware {
|
|
247
|
+
async authorize(req: IRequest, options?: any) {
|
|
248
|
+
const token = req.headers['authorization']?.split(' ')[1];
|
|
249
|
+
if (!token) throw HttpExceptions.Unauthorized('Missing token');
|
|
250
|
+
req.user = verifyToken(token); // attach user to request
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
```
|
|
254
|
+
|
|
255
|
+
**2 — Register with the app:**
|
|
256
|
+
|
|
257
|
+
```typescript
|
|
258
|
+
app.useAuthorization(JwtAuthorization);
|
|
259
|
+
```
|
|
260
|
+
|
|
261
|
+
**3 — Protect controllers or routes:**
|
|
262
|
+
|
|
263
|
+
```typescript
|
|
264
|
+
// Protect entire controller
|
|
265
|
+
@Authorized()
|
|
266
|
+
@ApiController('/admin')
|
|
267
|
+
class AdminController {
|
|
268
|
+
@Get('/')
|
|
269
|
+
dashboard(@AuthUser() user: User) {
|
|
270
|
+
return user;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
// Protect specific route with roles
|
|
275
|
+
@ApiController('/admin')
|
|
276
|
+
class AdminController {
|
|
277
|
+
@Authorized({ roles: ['admin'] })
|
|
278
|
+
@Get('/stats')
|
|
279
|
+
stats() { ... }
|
|
280
|
+
}
|
|
281
|
+
```
|
|
282
|
+
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
### Validation
|
|
286
|
+
|
|
287
|
+
Validation is powered by `class-validator`. Decorate your DTOs and Avleon validates automatically:
|
|
288
|
+
|
|
289
|
+
```typescript
|
|
290
|
+
import { IsString, IsEmail, IsInt, Min, Max, IsOptional } from 'class-validator';
|
|
291
|
+
|
|
292
|
+
class CreateUserDto {
|
|
293
|
+
@IsString()
|
|
294
|
+
@IsNotEmpty()
|
|
295
|
+
name: string;
|
|
296
|
+
|
|
297
|
+
@IsEmail()
|
|
298
|
+
email: string;
|
|
299
|
+
|
|
300
|
+
@IsInt()
|
|
301
|
+
@Min(0)
|
|
302
|
+
@Max(120)
|
|
303
|
+
age: number;
|
|
304
|
+
|
|
305
|
+
@IsOptional()
|
|
306
|
+
@IsString()
|
|
307
|
+
role?: string;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
@Post('/')
|
|
311
|
+
async createUser(@Body() body: CreateUserDto) {
|
|
312
|
+
return this.userService.create(body);
|
|
313
|
+
}
|
|
314
|
+
```
|
|
315
|
+
|
|
316
|
+
---
|
|
317
|
+
|
|
318
|
+
### OpenAPI Documentation
|
|
319
|
+
|
|
320
|
+
**Inline config:**
|
|
321
|
+
|
|
322
|
+
```typescript
|
|
323
|
+
app.useOpenApi({
|
|
324
|
+
info: {
|
|
325
|
+
title: 'User API',
|
|
326
|
+
version: '1.0.0',
|
|
327
|
+
description: 'API for managing users',
|
|
328
|
+
},
|
|
329
|
+
servers: [{ url: 'http://localhost:4000', description: 'Dev server' }],
|
|
330
|
+
components: {
|
|
331
|
+
securitySchemes: {
|
|
332
|
+
bearerAuth: {
|
|
333
|
+
type: 'http',
|
|
334
|
+
scheme: 'bearer',
|
|
335
|
+
bearerFormat: 'JWT',
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
},
|
|
339
|
+
});
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
**Config class:**
|
|
343
|
+
|
|
344
|
+
```typescript
|
|
345
|
+
import { AppConfig, IConfig, Environment } from '@avleon/core';
|
|
346
|
+
|
|
347
|
+
@AppConfig
|
|
348
|
+
export class OpenApiConfig implements IConfig {
|
|
349
|
+
config(env: Environment) {
|
|
350
|
+
return {
|
|
351
|
+
info: { title: 'My API', version: '1.0.0' },
|
|
352
|
+
routePrefix: '/docs',
|
|
353
|
+
provider: 'scalar', // or 'default' for Swagger UI
|
|
354
|
+
};
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
// In app.ts
|
|
359
|
+
if (app.isDevelopment()) {
|
|
360
|
+
app.useOpenApi(OpenApiConfig);
|
|
361
|
+
}
|
|
362
|
+
```
|
|
363
|
+
|
|
364
|
+
**Route-level docs with `@OpenApi`:**
|
|
365
|
+
|
|
366
|
+
```typescript
|
|
367
|
+
import { OpenApi, OpenApiProperty, OpenApiSchema } from '@avleon/core';
|
|
368
|
+
|
|
369
|
+
@OpenApiSchema()
|
|
370
|
+
export class UserQuery {
|
|
371
|
+
@OpenApiProperty({ type: 'string', example: 'john', required: false })
|
|
372
|
+
@IsOptional()
|
|
373
|
+
search?: string;
|
|
374
|
+
|
|
375
|
+
@OpenApiProperty({ type: 'integer', example: 1, required: false })
|
|
376
|
+
@IsOptional()
|
|
377
|
+
page?: number;
|
|
378
|
+
}
|
|
379
|
+
|
|
380
|
+
@OpenApi({
|
|
381
|
+
summary: 'Get all users',
|
|
382
|
+
tags: ['users'],
|
|
383
|
+
security: [{ bearerAuth: [] }],
|
|
384
|
+
response: {
|
|
385
|
+
200: {
|
|
386
|
+
description: 'List of users',
|
|
387
|
+
type: 'object',
|
|
388
|
+
properties: {
|
|
389
|
+
data: { type: 'array' },
|
|
390
|
+
total: { type: 'integer', example: 100 },
|
|
391
|
+
},
|
|
392
|
+
},
|
|
393
|
+
401: { description: 'Unauthorized' },
|
|
394
|
+
},
|
|
395
|
+
})
|
|
396
|
+
@Get('/')
|
|
397
|
+
getAll(@Query() query: UserQuery) { ... }
|
|
398
|
+
```
|
|
399
|
+
|
|
400
|
+
---
|
|
401
|
+
|
|
402
|
+
## Advanced Features
|
|
403
|
+
|
|
404
|
+
### Database — Knex
|
|
405
|
+
|
|
406
|
+
```typescript
|
|
407
|
+
app.useKnex({
|
|
408
|
+
client: 'mysql',
|
|
409
|
+
connection: {
|
|
410
|
+
host: '127.0.0.1',
|
|
411
|
+
port: 3306,
|
|
412
|
+
user: 'root',
|
|
413
|
+
password: 'password',
|
|
414
|
+
database: 'myapp',
|
|
415
|
+
},
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Using a config class:
|
|
420
|
+
|
|
421
|
+
```typescript
|
|
422
|
+
@AppConfig
|
|
423
|
+
export class KnexConfig implements IConfig {
|
|
424
|
+
config(env: Environment) {
|
|
425
|
+
return {
|
|
426
|
+
client: 'mysql',
|
|
427
|
+
connection: {
|
|
428
|
+
host: env.get('DB_HOST') || '127.0.0.1',
|
|
429
|
+
port: env.get('DB_PORT') || 3306,
|
|
430
|
+
user: env.get('DB_USER') || 'root',
|
|
431
|
+
password: env.get('DB_PASS') || 'password',
|
|
432
|
+
database: env.get('DB_NAME') || 'myapp',
|
|
433
|
+
},
|
|
434
|
+
};
|
|
435
|
+
}
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
app.useKnex(KnexConfig);
|
|
439
|
+
```
|
|
440
|
+
|
|
441
|
+
Using in a service:
|
|
442
|
+
|
|
443
|
+
```typescript
|
|
444
|
+
import { DB, AppService } from '@avleon/core';
|
|
445
|
+
|
|
446
|
+
@AppService
|
|
447
|
+
export class UsersService {
|
|
448
|
+
constructor(private readonly db: DB) {}
|
|
449
|
+
|
|
450
|
+
async findAll() {
|
|
451
|
+
return this.db.client.select('*').from('users');
|
|
452
|
+
}
|
|
453
|
+
}
|
|
454
|
+
```
|
|
455
|
+
|
|
456
|
+
---
|
|
457
|
+
|
|
458
|
+
### Database — TypeORM
|
|
459
|
+
|
|
460
|
+
```typescript
|
|
461
|
+
app.useDataSource({
|
|
462
|
+
type: 'postgres',
|
|
463
|
+
host: 'localhost',
|
|
464
|
+
port: 5432,
|
|
465
|
+
username: 'postgres',
|
|
466
|
+
password: 'password',
|
|
467
|
+
database: 'avleon',
|
|
468
|
+
entities: [User],
|
|
469
|
+
synchronize: true,
|
|
470
|
+
});
|
|
471
|
+
```
|
|
472
|
+
|
|
473
|
+
Using a config class:
|
|
474
|
+
|
|
475
|
+
```typescript
|
|
476
|
+
@AppConfig
|
|
477
|
+
export class DataSourceConfig implements IConfig {
|
|
478
|
+
config(env: Environment) {
|
|
479
|
+
return {
|
|
480
|
+
type: 'postgres',
|
|
481
|
+
host: env.get('DB_HOST') || 'localhost',
|
|
482
|
+
port: Number(env.get('DB_PORT')) || 5432,
|
|
483
|
+
username: env.get('DB_USER') || 'postgres',
|
|
484
|
+
password: env.get('DB_PASS') || 'password',
|
|
485
|
+
database: env.get('DB_NAME') || 'avleon',
|
|
486
|
+
entities: [User],
|
|
487
|
+
synchronize: true,
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
app.useDataSource(DataSourceConfig);
|
|
493
|
+
```
|
|
494
|
+
|
|
495
|
+
Using in a service:
|
|
496
|
+
|
|
497
|
+
```typescript
|
|
498
|
+
import { AppService, InjectRepository } from '@avleon/core';
|
|
499
|
+
import { Repository } from 'typeorm';
|
|
500
|
+
import { User } from './user.entity';
|
|
501
|
+
|
|
502
|
+
@AppService
|
|
503
|
+
export class UserService {
|
|
504
|
+
constructor(
|
|
505
|
+
@InjectRepository(User)
|
|
506
|
+
private readonly userRepo: Repository<User>,
|
|
507
|
+
) {}
|
|
508
|
+
|
|
509
|
+
async findAll() {
|
|
510
|
+
return this.userRepo.find();
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
```
|
|
514
|
+
|
|
515
|
+
---
|
|
516
|
+
|
|
517
|
+
### File Uploads
|
|
518
|
+
|
|
519
|
+
```typescript
|
|
520
|
+
// Configure multipart support
|
|
521
|
+
app.useMultipart({
|
|
522
|
+
destination: path.join(process.cwd(), 'public/uploads'),
|
|
523
|
+
limits: { fileSize: 5 * 1024 * 1024 }, // 5MB
|
|
524
|
+
});
|
|
525
|
+
```
|
|
526
|
+
|
|
527
|
+
```typescript
|
|
528
|
+
import { FileStorage, UploadFile, MultipartFile } from '@avleon/core';
|
|
529
|
+
|
|
530
|
+
@ApiController('/files')
|
|
531
|
+
class FileController {
|
|
532
|
+
constructor(private readonly fileStorage: FileStorage) {}
|
|
533
|
+
|
|
534
|
+
@OpenApi({
|
|
535
|
+
description: 'Upload a single file',
|
|
536
|
+
body: {
|
|
537
|
+
type: 'object',
|
|
538
|
+
properties: {
|
|
539
|
+
file: { type: 'string', format: 'binary' },
|
|
540
|
+
},
|
|
541
|
+
required: ['file'],
|
|
542
|
+
},
|
|
543
|
+
})
|
|
544
|
+
@Post('/upload')
|
|
545
|
+
async upload(@UploadFile('file') file: MultipartFile) {
|
|
546
|
+
const result = await this.fileStorage.save(file);
|
|
547
|
+
// optionally rename: this.fileStorage.save(file, { as: 'newname.jpg' })
|
|
548
|
+
return result;
|
|
549
|
+
// { uploadPath: '/uploads/...', staticPath: '/static/...' }
|
|
550
|
+
}
|
|
551
|
+
}
|
|
552
|
+
```
|
|
553
|
+
|
|
554
|
+
---
|
|
555
|
+
|
|
556
|
+
### Static Files
|
|
557
|
+
|
|
558
|
+
```typescript
|
|
559
|
+
import path from 'path';
|
|
560
|
+
|
|
561
|
+
app.useStaticFiles({
|
|
562
|
+
path: path.join(process.cwd(), 'public'),
|
|
563
|
+
prefix: '/static/',
|
|
564
|
+
});
|
|
565
|
+
```
|
|
566
|
+
|
|
567
|
+
---
|
|
568
|
+
|
|
569
|
+
### WebSocket (Socket.IO)
|
|
570
|
+
|
|
571
|
+
```typescript
|
|
572
|
+
app.useSocketIo({ cors: { origin: '*' } });
|
|
573
|
+
```
|
|
574
|
+
|
|
575
|
+
Dispatch events from services:
|
|
576
|
+
|
|
577
|
+
```typescript
|
|
578
|
+
import { AppService, EventDispatcher } from '@avleon/core';
|
|
579
|
+
|
|
580
|
+
@AppService
|
|
581
|
+
export class UserService {
|
|
582
|
+
constructor(private readonly dispatcher: EventDispatcher) {}
|
|
583
|
+
|
|
584
|
+
async create(data: any) {
|
|
585
|
+
const user = await this.save(data);
|
|
586
|
+
await this.dispatcher.dispatch('users:created', { userId: user.id });
|
|
587
|
+
return user;
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
```
|
|
591
|
+
|
|
592
|
+
---
|
|
593
|
+
|
|
594
|
+
## Route Mapping (Functional Style)
|
|
595
|
+
|
|
596
|
+
For simple routes without a controller class:
|
|
597
|
+
|
|
598
|
+
```typescript
|
|
599
|
+
app.mapGet('/users', async (req, res) => {
|
|
600
|
+
return { users: [] };
|
|
601
|
+
});
|
|
602
|
+
|
|
603
|
+
app.mapPost('/users', async (req, res) => {
|
|
604
|
+
return { success: true };
|
|
605
|
+
});
|
|
606
|
+
|
|
607
|
+
app.mapPut('/users/:id', async (req, res) => {
|
|
608
|
+
return { success: true };
|
|
609
|
+
});
|
|
610
|
+
|
|
611
|
+
app.mapDelete('/users/:id', async (req, res) => {
|
|
612
|
+
return { success: true };
|
|
613
|
+
});
|
|
614
|
+
```
|
|
615
|
+
|
|
616
|
+
Add middleware and OpenAPI docs to functional routes:
|
|
617
|
+
|
|
618
|
+
```typescript
|
|
619
|
+
app
|
|
620
|
+
.mapGet('/users', async (req, res) => {
|
|
621
|
+
return { users: [] };
|
|
622
|
+
})
|
|
623
|
+
.useMiddleware([AuthMiddleware])
|
|
624
|
+
.useOpenApi({
|
|
625
|
+
summary: 'Get all users',
|
|
626
|
+
tags: ['users'],
|
|
627
|
+
security: [{ bearerAuth: [] }],
|
|
628
|
+
response: {
|
|
629
|
+
200: {
|
|
630
|
+
description: 'List of users',
|
|
631
|
+
type: 'array',
|
|
632
|
+
},
|
|
633
|
+
},
|
|
634
|
+
});
|
|
635
|
+
```
|
|
636
|
+
|
|
637
|
+
---
|
|
638
|
+
|
|
639
|
+
## Testing
|
|
640
|
+
|
|
641
|
+
```typescript
|
|
642
|
+
import { AvleonTest } from '@avleon/core';
|
|
643
|
+
import { UserController } from './user.controller';
|
|
644
|
+
|
|
645
|
+
describe('UserController', () => {
|
|
646
|
+
let controller: UserController;
|
|
647
|
+
|
|
648
|
+
beforeAll(() => {
|
|
649
|
+
controller = AvleonTest.getController(UserController);
|
|
650
|
+
});
|
|
651
|
+
|
|
652
|
+
it('should be defined', () => {
|
|
653
|
+
expect(controller).toBeDefined();
|
|
654
|
+
});
|
|
655
|
+
|
|
656
|
+
it('should return users', async () => {
|
|
657
|
+
const result = await controller.getAll();
|
|
658
|
+
expect(Array.isArray(result)).toBe(true);
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
```
|
|
662
|
+
|
|
663
|
+
---
|
|
664
|
+
|
|
665
|
+
## License
|
|
666
|
+
|
|
667
667
|
ISC © [Tareq Hossain](https://github.com/xtareq)
|