@avleon/core 0.0.45 → 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/README.md +355 -369
- 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 -83
- 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 +67 -116
- package/dist/application.d.ts +0 -47
- package/dist/application.js +0 -50
- 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 -111
- 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 -23
- package/dist/event-dispatcher.js +0 -100
- 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 -104
- package/dist/helpers.d.ts +0 -44
- package/dist/helpers.js +0 -419
- package/dist/helpers.test.d.ts +0 -1
- package/dist/helpers.test.js +0 -95
- package/dist/icore.d.ts +0 -226
- package/dist/icore.js +0 -968
- package/dist/icore.test.d.ts +0 -1
- package/dist/icore.test.js +0 -14
- package/dist/index.d.ts +0 -55
- package/dist/interfaces/avleon-application.d.ts +0 -27
- package/dist/interfaces/avleon-application.js +0 -1
- 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 -27
- package/dist/middleware.js +0 -64
- 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 -343
- package/dist/openapi.js +0 -27
- 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 -49
- package/dist/route-methods.test.d.ts +0 -1
- package/dist/route-methods.test.js +0 -129
- package/dist/swagger-schema.d.ts +0 -43
- package/dist/swagger-schema.js +0 -452
- package/dist/swagger-schema.test.d.ts +0 -1
- package/dist/swagger-schema.test.js +0 -105
- package/dist/testing.d.ts +0 -55
- package/dist/testing.js +0 -196
- 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/hash.d.ts +0 -4
- package/dist/utils/hash.js +0 -15
- package/dist/utils/index.d.ts +0 -2
- package/dist/utils/index.js +0 -18
- package/dist/utils/optional-require.d.ts +0 -8
- package/dist/utils/optional-require.js +0 -70
- package/dist/validation.d.ts +0 -39
- package/dist/validation.js +0 -111
- 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 -7
- package/dist/websocket.js +0 -20
- package/dist/websocket.test.d.ts +0 -1
- package/dist/websocket.test.js +0 -27
package/README.md
CHANGED
|
@@ -1,63 +1,60 @@
|
|
|
1
|
-
# Avleon
|
|
1
|
+
# Avleon
|
|
2
2
|
|
|
3
|
-

|
|
4
|
-
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

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