alado 2.2.1 → 2.2.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +485 -821
- package/dist/helpers/validate/built-in/index.d.ts +1 -0
- package/dist/helpers/validate/built-in/index.js +6 -0
- package/dist/helpers/validate/built-in/index.js.map +1 -0
- package/dist/helpers/validate/built-in/validator.d.ts +42 -0
- package/dist/helpers/validate/built-in/validator.js +131 -0
- package/dist/helpers/validate/built-in/validator.js.map +1 -0
- package/dist/helpers/validate/index.d.ts +1 -0
- package/dist/helpers/validate/index.js +1 -0
- package/dist/helpers/validate/index.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,185 +1,338 @@
|
|
|
1
|
-
Alado
|
|
2
|
-
for creating server applications written in Node.js and running over the HTTP protocol.
|
|
1
|
+
# Alado
|
|
3
2
|
|
|
4
|
-
|
|
5
|
-
it provides out-of-the-box such fully-functioning features as routing, CORS, automatic API documentation (Open API 3.0),
|
|
6
|
-
parsing query and path parameters, and the request body, file uploading, request authentication, etc.
|
|
3
|
+
> A fast, lightweight, zero-dependency Node.js HTTP framework with built-in routing, validation, authentication, file uploads, and automatic OpenAPI 3.0 documentation.
|
|
7
4
|
|
|
8
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/alado)
|
|
6
|
+
[](./LICENSE)
|
|
9
7
|
|
|
10
|
-
|
|
8
|
+
---
|
|
9
|
+
|
|
10
|
+
## Table of Contents
|
|
11
|
+
|
|
12
|
+
- [Why Alado](#why-alado)
|
|
13
|
+
- [Installation](#installation)
|
|
14
|
+
- [Quick Start](#quick-start)
|
|
15
|
+
- [Project Structure](#project-structure)
|
|
16
|
+
- [tsconfig Setup](#tsconfig-setup)
|
|
17
|
+
- [Application Setup](#application-setup)
|
|
18
|
+
- [Controllers](#controllers)
|
|
19
|
+
- [HTTP Method Decorators](#http-method-decorators)
|
|
20
|
+
- [defineRequest](#definerequest)
|
|
21
|
+
- [defineResponse](#defineresponse)
|
|
22
|
+
- [withAuth](#withauth)
|
|
23
|
+
- [DTOs](#dtos)
|
|
24
|
+
- [validateProperty](#validateproperty)
|
|
25
|
+
- [documentProperty](#documentproperty)
|
|
26
|
+
- [transformProperty](#transformproperty)
|
|
27
|
+
- [fileUploadProperty](#fileuploadproperty)
|
|
28
|
+
- [Authentication](#authentication)
|
|
29
|
+
- [File Uploads](#file-uploads)
|
|
30
|
+
- [OpenAPI / Swagger](#openapi--swagger)
|
|
31
|
+
- [API Reference](#api-reference)
|
|
11
32
|
|
|
12
|
-
|
|
13
|
-
[https://github.com/databikers/alado-decorators-api-example](https://github.com/databikers/alado-decorators-api-example)
|
|
33
|
+
---
|
|
14
34
|
|
|
15
|
-
##
|
|
35
|
+
## Why Alado
|
|
16
36
|
|
|
17
|
-
|
|
37
|
+
Alado sits in a unique position — it has the **simplicity of Express**, the **performance focus of Fastify**, and the **decorator-based structure of NestJS**, but with **zero external dependencies** and a much smaller surface area.
|
|
38
|
+
|
|
39
|
+
---
|
|
40
|
+
|
|
41
|
+
### Feature Comparison
|
|
42
|
+
|
|
43
|
+
| Feature | Alado | Express | Fastify | NestJS |
|
|
44
|
+
| --------------------------- | ---------- | --------------------- | --------------------- | ----------- |
|
|
45
|
+
| Zero dependencies | ✅ | ❌ | ❌ | ❌ |
|
|
46
|
+
| Decorator-based routing | ✅ | ❌ | ❌ | ✅ |
|
|
47
|
+
| Built-in request validation | ✅ | ❌ | ✅ (schemas) | ✅ (pipes) |
|
|
48
|
+
| Built-in auth pipeline | ✅ | ❌ | ❌ | ✅ (guards) |
|
|
49
|
+
| Auto OpenAPI 3.0 docs | ✅ | ❌ | ✅ (plugin) | ✅ (plugin) |
|
|
50
|
+
| Built-in file upload | ✅ | ❌ | ❌ | ❌ |
|
|
51
|
+
| Built-in body parsing | ✅ | ❌ (body-parser) | ✅ | ✅ |
|
|
52
|
+
| Built-in CORS | ✅ | ❌ (cors) | ❌ (plugin) | ✅ |
|
|
53
|
+
| TypeScript-first | ✅ | ⚠️ types via `@types` | ⚠️ types via `@types` | ✅ |
|
|
54
|
+
| Learning curve | 🟢 Low | 🟢 Low | 🟡 Medium | 🔴 High |
|
|
55
|
+
| Boilerplate | 🟢 Minimal | 🟡 Medium | 🟡 Medium | 🔴 Heavy |
|
|
56
|
+
| Ecosystem / plugins | 🟡 Small | 🟢 Massive | 🟢 Large | 🟢 Large |
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Installation
|
|
61
|
+
|
|
62
|
+
```bash
|
|
63
|
+
npm install alado
|
|
64
|
+
```
|
|
65
|
+
|
|
66
|
+
---
|
|
67
|
+
|
|
68
|
+
## Quick Start
|
|
69
|
+
|
|
70
|
+
A minimal API in under 30 lines:
|
|
18
71
|
|
|
19
72
|
```ts
|
|
20
|
-
|
|
21
|
-
post
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
73
|
+
// src/modules/user/user.controller.ts
|
|
74
|
+
import { post, defineRequest, defineResponse } from 'alado';
|
|
75
|
+
import { CredentialsDto } from './dto/credentials.dto';
|
|
76
|
+
|
|
77
|
+
export class UserController {
|
|
78
|
+
@post('/user', { tags: ['User'] })
|
|
79
|
+
@defineResponse({
|
|
80
|
+
statusCode: 201,
|
|
81
|
+
title: 'Created',
|
|
82
|
+
headers: { 'Content-Type': 'application/json' },
|
|
83
|
+
body: { id: '123', email: 'user@example.com' },
|
|
84
|
+
})
|
|
85
|
+
@defineRequest({ body: CredentialsDto })
|
|
86
|
+
async create(req) {
|
|
87
|
+
const user = { id: '123', ...req.body };
|
|
88
|
+
return {
|
|
89
|
+
statusCode: 201,
|
|
90
|
+
headers: { 'Content-Type': 'application/json' },
|
|
91
|
+
body: user,
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// src/main.ts
|
|
99
|
+
import { initializeApplication } from 'alado';
|
|
100
|
+
import { UserController } from './user/user.controller';
|
|
101
|
+
|
|
102
|
+
const app = initializeApplication({
|
|
103
|
+
serverOptions: { port: 3000 },
|
|
104
|
+
controllers: [UserController],
|
|
105
|
+
});
|
|
106
|
+
|
|
107
|
+
app.start(() => console.log('Running on http://localhost:3000'));
|
|
26
108
|
```
|
|
27
109
|
|
|
28
|
-
|
|
110
|
+
---
|
|
111
|
+
|
|
112
|
+
## Project Structure
|
|
113
|
+
|
|
114
|
+
Possible layout for a decorator-based Alado project:
|
|
29
115
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
116
|
+
```
|
|
117
|
+
src/
|
|
118
|
+
├── main.ts # App entry point
|
|
119
|
+
├── config/
|
|
120
|
+
│ └── server.options.ts # AladoServerOptions
|
|
121
|
+
├── auth/
|
|
122
|
+
│ └── bearer.auth.ts # RequestAuthentication config
|
|
123
|
+
└── modules/
|
|
124
|
+
└── user/
|
|
125
|
+
├── index.ts
|
|
126
|
+
├── user.controller.ts
|
|
127
|
+
└── dto/
|
|
128
|
+
├── credentials.dto.ts
|
|
129
|
+
├── user.dto.ts
|
|
130
|
+
├── user-files.dto.ts
|
|
131
|
+
└── id.dto.ts
|
|
132
|
+
```
|
|
37
133
|
|
|
38
134
|
---
|
|
39
135
|
|
|
40
|
-
|
|
136
|
+
## Application Setup
|
|
137
|
+
|
|
138
|
+
### `initializeApplication`
|
|
41
139
|
|
|
42
140
|
```ts
|
|
43
|
-
|
|
141
|
+
import { initializeApplication } from 'alado';
|
|
142
|
+
import { UserController } from '@user';
|
|
143
|
+
|
|
144
|
+
const app = initializeApplication({
|
|
145
|
+
serverOptions: {
|
|
146
|
+
port: 3000,
|
|
147
|
+
appId: 'my-api', // optional — used to scope multi-app setups
|
|
148
|
+
logger: {
|
|
149
|
+
log: console.log,
|
|
150
|
+
error: console.error,
|
|
151
|
+
},
|
|
152
|
+
},
|
|
153
|
+
controllers: [UserController],
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
app.start(() => console.log('Started'));
|
|
157
|
+
app.stop(() => console.log('Stopped'));
|
|
44
158
|
```
|
|
45
159
|
|
|
46
|
-
|
|
160
|
+
### Passing options to controller constructors
|
|
47
161
|
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
162
|
+
```ts
|
|
163
|
+
initializeApplication({
|
|
164
|
+
serverOptions: { port: 3000 },
|
|
165
|
+
controllers: [
|
|
166
|
+
{
|
|
167
|
+
controller: UserController,
|
|
168
|
+
options: [{ service: UserService }], // passed to new UserController(...)
|
|
169
|
+
},
|
|
170
|
+
],
|
|
171
|
+
});
|
|
172
|
+
```
|
|
54
173
|
|
|
55
|
-
|
|
174
|
+
### HTTPS
|
|
56
175
|
|
|
57
176
|
```ts
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
files?: Record<string, { stream: Readable; size: number; mimetype: string }>;
|
|
71
|
-
}
|
|
177
|
+
import { readFileSync } from 'fs';
|
|
178
|
+
|
|
179
|
+
const app = initializeApplication({
|
|
180
|
+
serverOptions: {
|
|
181
|
+
port: 443,
|
|
182
|
+
ssl: {
|
|
183
|
+
cert: readFileSync('/path/to/cert.pem'),
|
|
184
|
+
key: readFileSync('/path/to/key.pem'),
|
|
185
|
+
},
|
|
186
|
+
},
|
|
187
|
+
controllers: [UserController],
|
|
188
|
+
});
|
|
72
189
|
```
|
|
73
190
|
|
|
74
191
|
---
|
|
75
192
|
|
|
76
|
-
|
|
193
|
+
## Controllers
|
|
194
|
+
|
|
195
|
+
Controllers are plain TypeScript classes decorated with HTTP method decorators.
|
|
196
|
+
|
|
197
|
+
### HTTP Method Decorators
|
|
77
198
|
|
|
78
199
|
```ts
|
|
79
|
-
|
|
200
|
+
import { get, post, put, patch, del, head } from 'alado';
|
|
80
201
|
```
|
|
81
202
|
|
|
82
|
-
|
|
203
|
+
| Decorator | Method |
|
|
204
|
+
| ------------------------ | ------ |
|
|
205
|
+
| `@get(path, options?)` | GET |
|
|
206
|
+
| `@post(path, options?)` | POST |
|
|
207
|
+
| `@put(path, options?)` | PUT |
|
|
208
|
+
| `@patch(path, options?)` | PATCH |
|
|
209
|
+
| `@del(path, options?)` | DELETE |
|
|
210
|
+
| `@head(path, options?)` | HEAD |
|
|
211
|
+
|
|
212
|
+
**Options:**
|
|
83
213
|
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
214
|
+
```ts
|
|
215
|
+
interface HttpDecoratorOptions {
|
|
216
|
+
appId?: string; // scope to a specific app instance
|
|
217
|
+
title?: string; // endpoint name in OpenAPI
|
|
218
|
+
description?: string; // endpoint description in OpenAPI
|
|
219
|
+
tags?: string[]; // grouping tags in OpenAPI
|
|
220
|
+
isHidden?: boolean; // exclude from OpenAPI output
|
|
221
|
+
}
|
|
222
|
+
```
|
|
90
223
|
|
|
91
|
-
|
|
224
|
+
**Example:**
|
|
92
225
|
|
|
93
226
|
```ts
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
227
|
+
@get('/users', { tags: ['User'], title: 'List users' })
|
|
228
|
+
async list(req: Request) {
|
|
229
|
+
return {
|
|
230
|
+
statusCode: 200,
|
|
231
|
+
headers: { 'Content-Type': 'application/json' },
|
|
232
|
+
body: [],
|
|
233
|
+
};
|
|
100
234
|
}
|
|
101
235
|
```
|
|
102
236
|
|
|
103
237
|
---
|
|
104
238
|
|
|
105
|
-
### `
|
|
239
|
+
### `defineRequest`
|
|
240
|
+
|
|
241
|
+
Declares and validates the incoming request shape.
|
|
106
242
|
|
|
107
243
|
```ts
|
|
108
|
-
|
|
244
|
+
@defineRequest({
|
|
245
|
+
headers: AnyClass,
|
|
246
|
+
query: AnyClass,
|
|
247
|
+
path: AnyClass,
|
|
248
|
+
body: AnyClass,
|
|
249
|
+
files: AnyClass,
|
|
250
|
+
auth: AnyClass,
|
|
251
|
+
})
|
|
109
252
|
```
|
|
110
253
|
|
|
111
|
-
|
|
254
|
+
Each value is a DTO class whose properties carry `@validateProperty`, `@documentProperty`, etc.
|
|
112
255
|
|
|
113
|
-
|
|
114
|
-
- `inputProperty`: Field name in request used for auth input.
|
|
115
|
-
- `outputProperty`: Field name in request to store auth result.
|
|
116
|
-
- `handler`: Function to transform/verify authentication value.
|
|
117
|
-
- `handlerContext`: Optional context object passed to handler.
|
|
118
|
-
- `required`: Boolean indicating if authentication is mandatory.
|
|
119
|
-
- `error`: Error object to use when authentication fails.
|
|
120
|
-
|
|
121
|
-
#### `RequestAuthentication`
|
|
256
|
+
**Example:**
|
|
122
257
|
|
|
123
258
|
```ts
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
handlerContext?: any;
|
|
129
|
-
required: boolean;
|
|
130
|
-
error: AladoServerError;
|
|
259
|
+
@post('/user', { tags: ['User'] })
|
|
260
|
+
@defineRequest({ body: CredentialsDto })
|
|
261
|
+
async create(req: Request) {
|
|
262
|
+
console.log(req.body); // typed, validated CredentialsDto fields
|
|
131
263
|
}
|
|
132
264
|
```
|
|
133
265
|
|
|
134
|
-
|
|
135
|
-
import { RequestAuthentication } from 'alado';
|
|
136
|
-
import { DataStore } from '@data-store';
|
|
266
|
+
---
|
|
137
267
|
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
},
|
|
149
|
-
|
|
268
|
+
### `defineResponse`
|
|
269
|
+
|
|
270
|
+
Documents the expected response shape for OpenAPI output.
|
|
271
|
+
|
|
272
|
+
```ts
|
|
273
|
+
@defineResponse({
|
|
274
|
+
statusCode: 201,
|
|
275
|
+
title: 'Created',
|
|
276
|
+
entity: 'User',
|
|
277
|
+
description: 'User was created successfully',
|
|
278
|
+
headers: { 'Content-Type': 'application/json' },
|
|
279
|
+
body: exampleUserDto, // an instance of your DTO class
|
|
280
|
+
})
|
|
150
281
|
```
|
|
151
282
|
|
|
152
|
-
|
|
283
|
+
---
|
|
284
|
+
|
|
285
|
+
### `withAuth`
|
|
286
|
+
|
|
287
|
+
Attaches an authentication pipeline to a route.
|
|
153
288
|
|
|
154
289
|
```ts
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
290
|
+
@patch('/user/:id', { tags: ['User'] })
|
|
291
|
+
@withAuth(bearerAuth)
|
|
292
|
+
@defineRequest({ path: Id, body: UserDto })
|
|
293
|
+
async update(req: Request) {
|
|
294
|
+
console.log(req.auth.user); // injected by the auth handler
|
|
295
|
+
}
|
|
159
296
|
```
|
|
160
297
|
|
|
298
|
+
See [Authentication](#authentication) for full config details.
|
|
299
|
+
|
|
161
300
|
---
|
|
162
301
|
|
|
163
|
-
|
|
302
|
+
### Full controller example
|
|
164
303
|
|
|
165
304
|
```ts
|
|
305
|
+
import { get, post, patch, del, defineRequest, defineResponse, withAuth } from 'alado';
|
|
306
|
+
import { bearerAuth } from './auth/bearer.auth';
|
|
307
|
+
import { CredentialsDto, UserDto, Id } from './dto';
|
|
308
|
+
|
|
166
309
|
export class UserController {
|
|
310
|
+
@get('/users', { tags: ['User'] })
|
|
311
|
+
@defineResponse({
|
|
312
|
+
statusCode: 200,
|
|
313
|
+
headers: { 'Content-Type': 'application/json' },
|
|
314
|
+
body: [],
|
|
315
|
+
})
|
|
316
|
+
async list(req) {
|
|
317
|
+
return {
|
|
318
|
+
statusCode: 200,
|
|
319
|
+
headers: { 'Content-Type': 'application/json' },
|
|
320
|
+
body: [],
|
|
321
|
+
};
|
|
322
|
+
}
|
|
323
|
+
|
|
167
324
|
@post('/user', { tags: ['User'] })
|
|
168
325
|
@defineResponse({
|
|
169
326
|
statusCode: 201,
|
|
170
|
-
title: 'Created',
|
|
171
|
-
entity: 'User',
|
|
172
327
|
headers: { 'Content-Type': 'application/json' },
|
|
173
|
-
body:
|
|
328
|
+
body: {},
|
|
174
329
|
})
|
|
175
330
|
@defineRequest({ body: CredentialsDto })
|
|
176
|
-
|
|
177
|
-
const { body } = req;
|
|
178
|
-
const user = DataStore.signUp(body as CredentialsDto);
|
|
331
|
+
async create(req) {
|
|
179
332
|
return {
|
|
180
333
|
statusCode: 201,
|
|
181
334
|
headers: { 'Content-Type': 'application/json' },
|
|
182
|
-
body:
|
|
335
|
+
body: req.body,
|
|
183
336
|
};
|
|
184
337
|
}
|
|
185
338
|
|
|
@@ -187,70 +340,29 @@ export class UserController {
|
|
|
187
340
|
@withAuth(bearerAuth)
|
|
188
341
|
@defineResponse({
|
|
189
342
|
statusCode: 200,
|
|
190
|
-
title: 'OK',
|
|
191
|
-
enity: 'User',
|
|
192
343
|
headers: { 'Content-Type': 'application/json' },
|
|
193
|
-
body:
|
|
344
|
+
body: {},
|
|
345
|
+
})
|
|
346
|
+
@defineRequest({
|
|
347
|
+
path: Id,
|
|
348
|
+
body: UserDto,
|
|
194
349
|
})
|
|
195
|
-
|
|
196
|
-
public async update(req: Request) {
|
|
197
|
-
const isMyId = this.isMyId(req);
|
|
198
|
-
if (!isMyId) {
|
|
199
|
-
return {
|
|
200
|
-
statusCode: 403,
|
|
201
|
-
headers: { 'Content-Type': 'application/json' },
|
|
202
|
-
body: { message: 'Access denied' },
|
|
203
|
-
};
|
|
204
|
-
}
|
|
205
|
-
const { path, body } = req;
|
|
206
|
-
const user = DataStore.getUser(path.id);
|
|
207
|
-
if (user) {
|
|
208
|
-
DataStore.setUser(path.id, body as UserDto);
|
|
209
|
-
}
|
|
350
|
+
async update(req) {
|
|
210
351
|
return {
|
|
211
|
-
statusCode:
|
|
352
|
+
statusCode: 200,
|
|
212
353
|
headers: { 'Content-Type': 'application/json' },
|
|
213
|
-
body:
|
|
354
|
+
body: req.body,
|
|
214
355
|
};
|
|
215
356
|
}
|
|
216
357
|
|
|
217
|
-
@
|
|
358
|
+
@del('/user/:id', { tags: ['User'] })
|
|
218
359
|
@withAuth(bearerAuth)
|
|
219
|
-
@
|
|
220
|
-
|
|
221
|
-
title: 'OK',
|
|
222
|
-
enity: 'User',
|
|
223
|
-
headers: { 'Content-Type': 'application/json' },
|
|
224
|
-
body: exampleUserDto,
|
|
225
|
-
})
|
|
226
|
-
@defineRequest({ path: Id, files: UserFilesDto })
|
|
227
|
-
public setAvatar(req: Request) {
|
|
228
|
-
const isMyId = this.isMyId(req);
|
|
229
|
-
if (!isMyId) {
|
|
230
|
-
return {
|
|
231
|
-
statusCode: 403,
|
|
232
|
-
headers: { 'Content-Type': 'application/json' },
|
|
233
|
-
body: { message: 'Access denied' },
|
|
234
|
-
};
|
|
235
|
-
}
|
|
236
|
-
const { path, files } = req;
|
|
237
|
-
const user = DataStore.getUser(path.id);
|
|
238
|
-
if (!user) {
|
|
239
|
-
return {
|
|
240
|
-
statusCode: 404,
|
|
241
|
-
headers: { 'Content-Type': 'application/json' },
|
|
242
|
-
body: { message: 'Not Found' },
|
|
243
|
-
};
|
|
244
|
-
}
|
|
245
|
-
const { avatar } = files;
|
|
246
|
-
const writeStream = createWriteStream(`${process.cwd()}/uploads/user-${path.id}-avatar.png`, {
|
|
247
|
-
encoding: 'latin1',
|
|
248
|
-
});
|
|
249
|
-
avatar.stream.pipe(writeStream);
|
|
360
|
+
@defineRequest({ path: Id })
|
|
361
|
+
async remove(req) {
|
|
250
362
|
return {
|
|
251
|
-
statusCode:
|
|
252
|
-
headers: {
|
|
253
|
-
body:
|
|
363
|
+
statusCode: 204,
|
|
364
|
+
headers: {},
|
|
365
|
+
body: null,
|
|
254
366
|
};
|
|
255
367
|
}
|
|
256
368
|
}
|
|
@@ -258,93 +370,77 @@ export class UserController {
|
|
|
258
370
|
|
|
259
371
|
---
|
|
260
372
|
|
|
261
|
-
##
|
|
373
|
+
## DTOs
|
|
262
374
|
|
|
263
|
-
|
|
375
|
+
DTOs are plain TypeScript classes with decorated properties. They define how request data is validated, transformed, and documented.
|
|
264
376
|
|
|
265
|
-
|
|
266
|
-
fileUploadProperty(filePropertyDefinition: [FilePropertyDefinition](#filepropertydefinition))
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
#### Arguments
|
|
270
|
-
|
|
271
|
-
- **filePropertyDefinition**: Object describing file upload rules, including:
|
|
272
|
-
- `mimetypes`: Array of allowed MIME types.
|
|
273
|
-
- `maxSize`: Maximum file size (bytes).
|
|
274
|
-
- `required`: Whether the file must be provided.
|
|
275
|
-
- `mimetypeError`, `maxSizeError`, `requiredError`: Error objects describing validation failures.
|
|
377
|
+
### `validateProperty`
|
|
276
378
|
|
|
277
|
-
|
|
379
|
+
Validates an incoming property value. Runs before your handler.
|
|
278
380
|
|
|
279
381
|
```ts
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
382
|
+
import { validateProperty } from 'alado';
|
|
383
|
+
|
|
384
|
+
class CredentialsDto {
|
|
385
|
+
@validateProperty({
|
|
386
|
+
required: true,
|
|
387
|
+
handler: (value) => /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(value),
|
|
388
|
+
error: {
|
|
389
|
+
statusCode: 400,
|
|
390
|
+
message: 'Invalid email address',
|
|
391
|
+
},
|
|
392
|
+
})
|
|
393
|
+
email: string = 'user@example.com';
|
|
287
394
|
}
|
|
288
395
|
```
|
|
289
396
|
|
|
397
|
+
- `handler` receives the raw value and must return `boolean | Promise<boolean>`
|
|
398
|
+
- returning `false` → framework returns the configured `error` automatically
|
|
399
|
+
- `handler` is called with `this` bound to the full `Request` object — you can access `this.auth`, `this.query`, etc.
|
|
400
|
+
- **do not use arrow functions** if you need access to `this`
|
|
401
|
+
|
|
290
402
|
---
|
|
291
403
|
|
|
292
404
|
### `documentProperty`
|
|
293
405
|
|
|
294
|
-
|
|
295
|
-
documentProperty(propertyDocumentation: [PropertyDocumentation](#propertydocumentation))
|
|
296
|
-
```
|
|
297
|
-
|
|
298
|
-
#### Arguments
|
|
299
|
-
|
|
300
|
-
- **propertyDocumentation**: Documentation metadata for a property, including:
|
|
301
|
-
- `schema`: JSON Schema–like definition (`type`, `format`, validation structure).
|
|
302
|
-
- `description`: Human-readable explanation.
|
|
303
|
-
- `example`: Example value.
|
|
304
|
-
|
|
305
|
-
#### `PropertyDocumentation`
|
|
406
|
+
Adds OpenAPI/Swagger metadata to a property.
|
|
306
407
|
|
|
307
408
|
```ts
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
409
|
+
import { documentProperty } from 'alado';
|
|
410
|
+
|
|
411
|
+
class UserDto {
|
|
412
|
+
@documentProperty({
|
|
413
|
+
schema: {
|
|
414
|
+
type: 'string',
|
|
415
|
+
minLength: 2,
|
|
416
|
+
maxLength: 50,
|
|
417
|
+
},
|
|
418
|
+
description: 'The user display name',
|
|
419
|
+
example: 'John Doe',
|
|
420
|
+
})
|
|
421
|
+
name: string = 'John Doe';
|
|
312
422
|
}
|
|
313
423
|
```
|
|
314
424
|
|
|
315
|
-
|
|
425
|
+
Supports the full OpenAPI `PropertyDefinitionSchema`:
|
|
316
426
|
|
|
317
427
|
```ts
|
|
318
428
|
interface PropertyDefinitionSchema {
|
|
319
|
-
type?: string;
|
|
320
|
-
format?: string;
|
|
429
|
+
type?: string; // 'string' | 'number' | 'boolean' | 'object' | 'array'
|
|
430
|
+
format?: string; // 'email' | 'uuid' | 'date-time' | ...
|
|
321
431
|
default?: any;
|
|
322
|
-
$ref?: string;
|
|
323
432
|
nullable?: boolean;
|
|
324
|
-
readOnly?: boolean;
|
|
325
|
-
writeOnly?: boolean;
|
|
326
|
-
deprecated?: boolean;
|
|
327
433
|
enum?: any[];
|
|
328
|
-
oneOf?: Array<PropertyDefinitionSchema>;
|
|
329
|
-
anyOf?: Array<PropertyDefinitionSchema>;
|
|
330
|
-
allOf?: Array<PropertyDefinitionSchema>;
|
|
331
|
-
not?: Array<PropertyDefinitionSchema>;
|
|
332
|
-
properties?: Record<string, PropertyDefinitionSchema>;
|
|
333
|
-
additionalProperties?: PropertyDefinitionSchema;
|
|
334
|
-
minProperties?: number;
|
|
335
|
-
maxProperties?: number;
|
|
336
434
|
minimum?: number;
|
|
337
435
|
maximum?: number;
|
|
338
|
-
exclusiveMinimum?: number;
|
|
339
|
-
exclusiveMaximum?: number;
|
|
340
|
-
multipleOf?: number;
|
|
341
436
|
minLength?: number;
|
|
342
437
|
maxLength?: number;
|
|
343
438
|
pattern?: string;
|
|
344
|
-
items?: PropertyDefinitionSchema;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
439
|
+
items?: PropertyDefinitionSchema; // for arrays
|
|
440
|
+
properties?: Record<string, PropertyDefinitionSchema>; // for objects
|
|
441
|
+
oneOf?: PropertyDefinitionSchema[];
|
|
442
|
+
anyOf?: PropertyDefinitionSchema[];
|
|
443
|
+
allOf?: PropertyDefinitionSchema[];
|
|
348
444
|
}
|
|
349
445
|
```
|
|
350
446
|
|
|
@@ -352,350 +448,100 @@ interface PropertyDefinitionSchema {
|
|
|
352
448
|
|
|
353
449
|
### `transformProperty`
|
|
354
450
|
|
|
355
|
-
|
|
356
|
-
transformProperty(propertyTransformer: (value:any) => any | Promise<any>)
|
|
357
|
-
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
### `validateProperty`
|
|
451
|
+
Transforms a property value before it reaches your handler.
|
|
361
452
|
|
|
362
453
|
```ts
|
|
363
|
-
|
|
364
|
-
```
|
|
454
|
+
import { transformProperty } from 'alado';
|
|
365
455
|
|
|
366
|
-
|
|
456
|
+
class QueryDto {
|
|
457
|
+
@transformProperty((value) => parseInt(value.toString(), 10))
|
|
458
|
+
page: number = 1;
|
|
367
459
|
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
- `handler`: Function to validate the property value (sync or async).
|
|
371
|
-
- `error`: Error object returned when validation fails.
|
|
372
|
-
|
|
373
|
-
#### `PropertyValidation`
|
|
374
|
-
|
|
375
|
-
```ts
|
|
376
|
-
interface PropertyValidation {
|
|
377
|
-
required?: boolean;
|
|
378
|
-
handler: (value: any) => boolean | Promise<boolean>; // throws error when returns false
|
|
379
|
-
error: AladoServerError;
|
|
460
|
+
@transformProperty((value) => value.trim().toLowerCase())
|
|
461
|
+
email: string = '';
|
|
380
462
|
}
|
|
381
463
|
```
|
|
382
464
|
|
|
383
465
|
---
|
|
384
466
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
```typescript
|
|
388
|
-
import { validateProperty, documentProperty } from 'alado';
|
|
467
|
+
### `fileUploadProperty`
|
|
389
468
|
|
|
390
|
-
|
|
391
|
-
@validateProperty({
|
|
392
|
-
required: true,
|
|
393
|
-
handler: async (value: any) => idValidator(value),
|
|
394
|
-
error: {
|
|
395
|
-
statusCode: 400,
|
|
396
|
-
message: 'Invalid id',
|
|
397
|
-
},
|
|
398
|
-
})
|
|
399
|
-
@documentProperty({
|
|
400
|
-
schema: {
|
|
401
|
-
type: 'string',
|
|
402
|
-
},
|
|
403
|
-
example: '7ef5ed25-53b1-432f-96ec-8e35d830eb9c',
|
|
404
|
-
description: 'User Id',
|
|
405
|
-
})
|
|
406
|
-
id: string = '7ef5ed25-53b1-432f-96ec-8e35d830eb9c';
|
|
407
|
-
}
|
|
408
|
-
```
|
|
469
|
+
Marks a DTO property as a file upload field with MIME type and size validation.
|
|
409
470
|
|
|
410
|
-
```
|
|
471
|
+
```ts
|
|
411
472
|
import { fileUploadProperty } from 'alado';
|
|
412
473
|
|
|
413
474
|
export class UserFilesDto {
|
|
414
475
|
@fileUploadProperty({
|
|
415
|
-
mimetypes: [
|
|
416
|
-
|
|
476
|
+
mimetypes: [
|
|
477
|
+
'image/png',
|
|
478
|
+
'image/jpeg',
|
|
479
|
+
],
|
|
480
|
+
maxSize: 2 * 1024 * 1024, // 2MB in bytes
|
|
417
481
|
required: true,
|
|
418
482
|
maxSizeError: {
|
|
419
483
|
statusCode: 413,
|
|
420
|
-
message: '
|
|
484
|
+
message: 'File must not exceed 2MB',
|
|
421
485
|
},
|
|
422
486
|
mimetypeError: {
|
|
423
487
|
statusCode: 415,
|
|
424
|
-
message: '
|
|
488
|
+
message: 'Only PNG and JPEG images are allowed',
|
|
425
489
|
},
|
|
426
490
|
requiredError: {
|
|
427
491
|
statusCode: 400,
|
|
428
|
-
message: '
|
|
492
|
+
message: 'Avatar file is required',
|
|
429
493
|
},
|
|
430
494
|
})
|
|
431
|
-
avatar: string = '/path/to/
|
|
495
|
+
avatar: string = '/path/to/avatar.png';
|
|
432
496
|
}
|
|
433
497
|
```
|
|
434
498
|
|
|
435
|
-
|
|
436
|
-
import { validateProperty, documentProperty } from 'alado';
|
|
499
|
+
---
|
|
437
500
|
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
handler: async (value: any) => emailValidator(value),
|
|
442
|
-
error: {
|
|
443
|
-
statusCode: 400,
|
|
444
|
-
message: 'Invalid email',
|
|
445
|
-
},
|
|
446
|
-
})
|
|
447
|
-
@documentProperty({
|
|
448
|
-
schema: {
|
|
449
|
-
type: 'string',
|
|
450
|
-
},
|
|
451
|
-
example: 'example@example.com',
|
|
452
|
-
description: 'User email',
|
|
453
|
-
})
|
|
454
|
-
email: string = 'example@example.com';
|
|
501
|
+
### Combining decorators
|
|
502
|
+
|
|
503
|
+
Decorators stack — use them together for full validation + documentation:
|
|
455
504
|
|
|
505
|
+
```ts
|
|
506
|
+
export class Id {
|
|
456
507
|
@validateProperty({
|
|
457
508
|
required: true,
|
|
458
|
-
handler:
|
|
509
|
+
handler: (value) => /^[0-9a-f-]{36}$/.test(value),
|
|
459
510
|
error: {
|
|
460
511
|
statusCode: 400,
|
|
461
|
-
message: 'Invalid
|
|
512
|
+
message: 'Invalid UUID',
|
|
462
513
|
},
|
|
463
514
|
})
|
|
464
515
|
@documentProperty({
|
|
465
516
|
schema: {
|
|
466
517
|
type: 'string',
|
|
518
|
+
format: 'uuid',
|
|
467
519
|
},
|
|
468
|
-
example: '
|
|
469
|
-
description: '
|
|
520
|
+
example: '7ef5ed25-53b1-432f-96ec-8e35d830eb9c',
|
|
521
|
+
description: 'Resource identifier',
|
|
470
522
|
})
|
|
471
|
-
|
|
523
|
+
id: string = '7ef5ed25-53b1-432f-96ec-8e35d830eb9c';
|
|
472
524
|
}
|
|
473
|
-
|
|
474
|
-
export const credentialsDto = new CredentialsDto();
|
|
475
525
|
```
|
|
476
526
|
|
|
477
527
|
---
|
|
478
528
|
|
|
479
|
-
##
|
|
480
|
-
|
|
481
|
-
Finally, use the `initializeApplication` function to get an instance you would like to run:
|
|
529
|
+
## Authentication
|
|
482
530
|
|
|
483
|
-
|
|
484
|
-
import { initializeApplication } from 'alado';
|
|
485
|
-
import { aladoServerOptions } from '@config';
|
|
486
|
-
import { UserController } from '@user';
|
|
487
|
-
|
|
488
|
-
export const app = initializeApplication({
|
|
489
|
-
serverOptions: aladoServerOptions,
|
|
490
|
-
controllers: [UserController],
|
|
491
|
-
});
|
|
492
|
-
|
|
493
|
-
app.start(() => console.log('Application has been successfully started'));
|
|
494
|
-
```
|
|
495
|
-
|
|
496
|
-
There's ability to pass args to controller`s constructor:
|
|
497
|
-
|
|
498
|
-
```ts
|
|
499
|
-
import { initializeApplication } from 'alado';
|
|
500
|
-
import { aladoServerOptions } from '@config';
|
|
501
|
-
import { UserController } from '@user';
|
|
502
|
-
|
|
503
|
-
export const app = initializeApplication({
|
|
504
|
-
serverOptions: aladoServerOptions,
|
|
505
|
-
controllers: [{ controller: UserController, options: [{ service: SomeService }] }],
|
|
506
|
-
});
|
|
507
|
-
|
|
508
|
-
app.start(() => console.log('Application has been successfully started'));
|
|
509
|
-
```
|
|
510
|
-
|
|
511
|
-
#### Arguments
|
|
512
|
-
|
|
513
|
-
- **initializeApplicationOptions**: Object containing:
|
|
514
|
-
- **serverOptions** ([AladoServerOptions](#aladoserveroptions)): Configuration for starting the server (e.g., `appId`, port, logger).
|
|
515
|
-
- **controllers**: Array of controller classes to initialize at startup.
|
|
516
|
-
|
|
517
|
-
#### `InitializeApplicationOptions`
|
|
518
|
-
|
|
519
|
-
```ts
|
|
520
|
-
interface InitializeApplicationOptions {
|
|
521
|
-
serverOptions: AladoServerOptions;
|
|
522
|
-
controllers: AnyClass[];
|
|
523
|
-
}
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
#### `AladoServerOptions`
|
|
531
|
+
Define a `RequestAuthentication` config object and pass it to `@withAuth`.
|
|
527
532
|
|
|
528
533
|
```ts
|
|
529
|
-
|
|
530
|
-
appId?: string;
|
|
531
|
-
port?: number;
|
|
532
|
-
logger?: AladoServerLogger;
|
|
533
|
-
}
|
|
534
|
-
```
|
|
535
|
-
|
|
536
|
-
#### `AladoServerLogger`
|
|
537
|
-
|
|
538
|
-
```ts
|
|
539
|
-
interface AladoServerLogger {
|
|
540
|
-
log: (...args: any[]) => void;
|
|
541
|
-
error: (error: Error) => void;
|
|
542
|
-
}
|
|
543
|
-
```
|
|
544
|
-
|
|
545
|
-
There is an example of the API creating with Alado (version < 2.0.0):
|
|
546
|
-
|
|
547
|
-
The basis of use is a combination of interface simplicity and separation of the “routine” part of writing API
|
|
548
|
-
from business logic implementation - due to a declarative approach to describing all request parameters
|
|
549
|
-
in the form of static reusable structures.
|
|
550
|
-
|
|
551
|
-
[https://github.com/databikers/alado-api-example](https://github.com/databikers/alado-api-example)
|
|
552
|
-
|
|
553
|
-
1. Installation
|
|
554
|
-
|
|
555
|
-
```shell
|
|
556
|
-
npm i -s alado
|
|
557
|
-
```
|
|
558
|
-
|
|
559
|
-
2. Instancing
|
|
560
|
-
|
|
561
|
-
```typescript
|
|
562
|
-
import { AladoServer } from 'alado';
|
|
563
|
-
|
|
564
|
-
const app: AladoServer = new AladoServer({
|
|
565
|
-
port: 3000,
|
|
566
|
-
cors: {
|
|
567
|
-
enable: true,
|
|
568
|
-
allowedOrigin: '*',
|
|
569
|
-
allowedHeaders: ['Authorization'],
|
|
570
|
-
exposeHeaders: ['x-total-count'],
|
|
571
|
-
},
|
|
572
|
-
documentProperty: {
|
|
573
|
-
enable: true,
|
|
574
|
-
route: '/open-api',
|
|
575
|
-
info: {
|
|
576
|
-
title: 'My API title',
|
|
577
|
-
description: 'My API description',
|
|
578
|
-
version: '1.0.0',
|
|
579
|
-
},
|
|
580
|
-
},
|
|
581
|
-
verbose: true,
|
|
582
|
-
maxBodySizeBytes: 1024,
|
|
583
|
-
});
|
|
584
|
-
```
|
|
585
|
-
|
|
586
|
-
In case you want to run an HTTPs server (even though normally a Node.js app runs behind nginx/ingress etc.)
|
|
587
|
-
You can do it using the ssl parameter (look at SecureContextOptions in the "tls" module -
|
|
588
|
-
[Node.js TLS module](https://nodejs.org/api/tls.html#tlscreateserveroptions-secureconnectionlistener):
|
|
589
|
-
|
|
590
|
-
```typescript
|
|
591
|
-
import { readFileSync } from 'fs';
|
|
592
|
-
import { AladoServer } from 'alado';
|
|
593
|
-
|
|
594
|
-
const app = new AladoServer({
|
|
595
|
-
port: 3000,
|
|
596
|
-
ssl: {
|
|
597
|
-
cert: readFileSync('/path/to/cert'),
|
|
598
|
-
key: readFileSync('/path/to/key'),
|
|
599
|
-
},
|
|
600
|
-
// https://nodejs.org/api/http.html#httpcreateserveroptions-requestlistener
|
|
601
|
-
serverOptions: {
|
|
602
|
-
keepAliveTimeout: 5000,
|
|
603
|
-
},
|
|
604
|
-
cors: {
|
|
605
|
-
enable: true,
|
|
606
|
-
allowedOrigin: '*',
|
|
607
|
-
allowedHeaders: ['Authorization'],
|
|
608
|
-
exposeHeaders: ['x-total-count'],
|
|
609
|
-
allowedCredentials: true,
|
|
610
|
-
maxAge: 3600,
|
|
611
|
-
},
|
|
612
|
-
headers: {
|
|
613
|
-
a: 'b', //any additional header
|
|
614
|
-
},
|
|
615
|
-
openApiDoc: {
|
|
616
|
-
enable: true,
|
|
617
|
-
route: '/',
|
|
618
|
-
info: {
|
|
619
|
-
title: 'My API title',
|
|
620
|
-
description: 'My API description',
|
|
621
|
-
version: '1.0.0',
|
|
622
|
-
},
|
|
623
|
-
},
|
|
624
|
-
verbose: true,
|
|
625
|
-
maxBodySizeBytes: 1024,
|
|
626
|
-
});
|
|
627
|
-
```
|
|
628
|
-
|
|
629
|
-
3. Defining routes
|
|
630
|
-
|
|
631
|
-
Here is how you define routes in Alado:
|
|
632
|
-
|
|
633
|
-
```typescript
|
|
634
|
-
app.get('/user/:id', context, (request: Request): Response<UserDto> => {
|
|
635
|
-
const user = {
|
|
636
|
-
name: 'John Doe',
|
|
637
|
-
id: request.path.id,
|
|
638
|
-
};
|
|
639
|
-
return {
|
|
640
|
-
statusCode: 200,
|
|
641
|
-
headers: {
|
|
642
|
-
'Content-Type': 'application/json',
|
|
643
|
-
},
|
|
644
|
-
body: user,
|
|
645
|
-
};
|
|
646
|
-
});
|
|
647
|
-
|
|
648
|
-
app.start(() => console.log(`[API]: application has been launched successfully`));
|
|
649
|
-
```
|
|
650
|
-
|
|
651
|
-
The application provides "get", "post", "delete", "patch", "put" and "head" methods for defining routes.
|
|
652
|
-
These methods accept three required arguments:
|
|
653
|
-
|
|
654
|
-
- route or path: A string that may contain variables to specify the route.
|
|
655
|
-
- context: An aggregating object that fully describes the request.
|
|
656
|
-
- handler: A function that handles the request.
|
|
657
|
-
|
|
658
|
-
3.1 Route or path.
|
|
659
|
-
|
|
660
|
-
Path variables defining works in the same way as it works in other frameworks: You just define a part of your route (means part separated by slashes)
|
|
661
|
-
as ":id" and receive the passed value as request.path.id).
|
|
662
|
-
|
|
663
|
-
3.2 Context
|
|
664
|
-
|
|
665
|
-
Here's the contract of the request context:
|
|
666
|
-
|
|
667
|
-
```typescript
|
|
668
|
-
interface Context<T> {
|
|
669
|
-
title: string;
|
|
670
|
-
auth?: RequestAuthentication;
|
|
671
|
-
isHidden?: boolean; //Just exclude some route from Your API specification
|
|
672
|
-
options: ContextOptions;
|
|
673
|
-
request: ContextRequest;
|
|
674
|
-
response: Response<T>;
|
|
675
|
-
}
|
|
676
|
-
```
|
|
677
|
-
|
|
678
|
-
Unlike context.title, which is simply a unique string, the other properties may not be immediately clear.
|
|
679
|
-
However, once explained, they become quite obvious and extremely logical.
|
|
680
|
-
|
|
681
|
-
**Note that all parts of the context and its nested properties can be reused**
|
|
682
|
-
|
|
683
|
-
3.2.1 Context.auth
|
|
684
|
-
|
|
685
|
-
```typescript
|
|
534
|
+
// src/modules/user/auth/bearer.auth.ts
|
|
686
535
|
import { RequestAuthentication } from 'alado';
|
|
536
|
+
import { DataStore } from '@data-store';
|
|
687
537
|
|
|
688
|
-
const bearerAuth: RequestAuthentication = {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
return
|
|
694
|
-
name: 'John Doe',
|
|
695
|
-
id: 1,
|
|
696
|
-
};
|
|
538
|
+
export const bearerAuth: RequestAuthentication = {
|
|
539
|
+
required: true,
|
|
540
|
+
inputProperty: 'headers.x-api-key', // where to read the token from
|
|
541
|
+
outputProperty: 'auth.user', // where to write the result on req
|
|
542
|
+
handler(value: string) {
|
|
543
|
+
return DataStore.findUserByToken(value); // return user or null/undefined
|
|
697
544
|
},
|
|
698
|
-
required: true, // false You want "soft" checking
|
|
699
545
|
error: {
|
|
700
546
|
statusCode: 401,
|
|
701
547
|
message: 'Unauthorized',
|
|
@@ -703,43 +549,26 @@ const bearerAuth: RequestAuthentication = {
|
|
|
703
549
|
};
|
|
704
550
|
```
|
|
705
551
|
|
|
706
|
-
|
|
707
|
-
(or "api_token" query parameter if you provide "query.api_token" as an inputProperty,
|
|
708
|
-
"body.someThing" and "path.token" work also)
|
|
709
|
-
use the provided handler to get the user and set the received user to request.auth.user,
|
|
710
|
-
(and the mentioned "request" here is the request argument in your route handler)
|
|
711
|
-
Or just returns 401 Unauthorized.
|
|
712
|
-
This means that the application can authenticate using an "Authorization" header, or alternatively,
|
|
713
|
-
using an "api_token" query parameter (if specified as "query.api_token" in the inputProperty).
|
|
714
|
-
It can also authenticate using values like "body.someThing" or "path.token".
|
|
715
|
-
In the example the provided handler retrieves the user and assigns it to request.auth.user
|
|
716
|
-
(where request is the argument in your route handler).
|
|
717
|
-
If authentication fails, it returns a 401 Unauthorized response.
|
|
718
|
-
|
|
719
|
-
This also works:
|
|
720
|
-
|
|
721
|
-
```typescript
|
|
722
|
-
import { RequestAuthentication } from 'alado';
|
|
552
|
+
The `inputProperty` supports dot-notation and can point to any part of the request:
|
|
723
553
|
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
required: true, // false You want "soft" checking
|
|
554
|
+
| `inputProperty` | Reads from |
|
|
555
|
+
| ----------------------- | -------------------- |
|
|
556
|
+
| `headers.x-api-key` | Request header |
|
|
557
|
+
| `headers.authorization` | Authorization header |
|
|
558
|
+
| `query.api_token` | Query parameter |
|
|
559
|
+
| `body.token` | Request body field |
|
|
560
|
+
| `path.token` | Path parameter |
|
|
561
|
+
|
|
562
|
+
The resolved value from `handler` is set at `outputProperty` on the request object, making it available as `req.auth.user` in your handler.
|
|
563
|
+
|
|
564
|
+
**Optional auth** — set `required: false` for soft checking (handler runs but failure doesn't block the request):
|
|
565
|
+
|
|
566
|
+
```ts
|
|
567
|
+
export const optionalAuth: RequestAuthentication = {
|
|
568
|
+
required: false, // won't return 401 if token is missing or invalid
|
|
569
|
+
inputProperty: 'headers.authorization',
|
|
570
|
+
outputProperty: 'auth.user',
|
|
571
|
+
handler: (token) => UserService.findByToken(token),
|
|
743
572
|
error: {
|
|
744
573
|
statusCode: 401,
|
|
745
574
|
message: 'Unauthorized',
|
|
@@ -747,310 +576,145 @@ const bearerAuth: RequestAuthentication = {
|
|
|
747
576
|
};
|
|
748
577
|
```
|
|
749
578
|
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
```typescript
|
|
753
|
-
interface ContextOptions {
|
|
754
|
-
allowUnknownFields?: boolean;
|
|
755
|
-
documentProperty?: {
|
|
756
|
-
description?: string;
|
|
757
|
-
operationId: string;
|
|
758
|
-
tags?: string[];
|
|
759
|
-
};
|
|
760
|
-
}
|
|
761
|
-
```
|
|
762
|
-
|
|
763
|
-
Setting "allowUnknownFields" to false protects your application from receiving unexpected properties in the request body, query, and files. It does not affect request.path, as path variables are defined at the routing level, nor does it affect request headers. Conversely, you can receive { name: 'John Doe', role: 'Admin' } when only expecting name.
|
|
764
|
-
|
|
765
|
-
The "documentProperty" property is part of the Open API route definition and is optional.
|
|
579
|
+
---
|
|
766
580
|
|
|
767
|
-
|
|
581
|
+
## File Uploads
|
|
768
582
|
|
|
769
|
-
|
|
583
|
+
1. Define a files DTO:
|
|
770
584
|
|
|
771
|
-
```
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
query?: Record<string, PropertyDefinition>;
|
|
775
|
-
path?: Record<string, PropertyDefinition>;
|
|
776
|
-
body?: Record<string, PropertyDefinition>;
|
|
777
|
-
files?: Record<string, FilePropertyDefinition>;
|
|
778
|
-
auth?: Record<string, any>;
|
|
779
|
-
}
|
|
780
|
-
```
|
|
781
|
-
|
|
782
|
-
There's an example for request with path property only:
|
|
783
|
-
|
|
784
|
-
```typescript
|
|
785
|
-
const request: ContextRequest = {
|
|
786
|
-
path: {
|
|
787
|
-
id: {
|
|
788
|
-
documentProperty: {
|
|
789
|
-
schema: {
|
|
790
|
-
type: 'string',
|
|
791
|
-
},
|
|
792
|
-
example: '123456',
|
|
793
|
-
description: 'User id',
|
|
794
|
-
},
|
|
795
|
-
validation: {
|
|
796
|
-
required: true,
|
|
797
|
-
handler(value) {
|
|
798
|
-
return /^\d+$/.test(value);
|
|
799
|
-
},
|
|
800
|
-
transform(value) {
|
|
801
|
-
return parseInt(value, 10);
|
|
802
|
-
},
|
|
803
|
-
error: {
|
|
804
|
-
statusCode: 400,
|
|
805
|
-
message: '400.1.0',
|
|
806
|
-
},
|
|
807
|
-
},
|
|
808
|
-
},
|
|
809
|
-
},
|
|
810
|
-
};
|
|
811
|
-
```
|
|
585
|
+
```ts
|
|
586
|
+
// src/modules/user/dto/user-files.dto.ts
|
|
587
|
+
import { fileUploadProperty } from 'alado';
|
|
812
588
|
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
const request: ContextRequest = {
|
|
822
|
-
body: {
|
|
823
|
-
role: {
|
|
824
|
-
documentProperty: {
|
|
825
|
-
schema: {
|
|
826
|
-
type: 'string',
|
|
827
|
-
},
|
|
828
|
-
example: 'admin',
|
|
829
|
-
description: 'User role',
|
|
830
|
-
},
|
|
831
|
-
validation: {
|
|
832
|
-
required: true,
|
|
833
|
-
async handler(value) {
|
|
834
|
-
if (this.auth.user.role === 'admin') {
|
|
835
|
-
return true;
|
|
836
|
-
}
|
|
837
|
-
await userService.makeSomeChecking(this.query.scope);
|
|
838
|
-
return value === 'customer';
|
|
839
|
-
},
|
|
840
|
-
error: {
|
|
841
|
-
statusCode: 400,
|
|
842
|
-
message: '400.1.0',
|
|
843
|
-
},
|
|
844
|
-
},
|
|
589
|
+
export class UserFilesDto {
|
|
590
|
+
@fileUploadProperty({
|
|
591
|
+
mimetypes: ['image/png'],
|
|
592
|
+
maxSize: 1048576, // 1MB
|
|
593
|
+
required: true,
|
|
594
|
+
maxSizeError: {
|
|
595
|
+
statusCode: 413,
|
|
596
|
+
message: 'Max size is 1MB',
|
|
845
597
|
},
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
In the same way You can describe the request headers, query, body. Remember, if context.options.allowUnknownFields
|
|
851
|
-
is set to false, all non-described request properties (or non-described nested properties in the described request properties) never be accessible.
|
|
852
|
-
|
|
853
|
-
```typescript
|
|
854
|
-
const request: ContextRequest = {
|
|
855
|
-
query: {
|
|
856
|
-
sortBy: {
|
|
857
|
-
documentProperty: {
|
|
858
|
-
schema: {
|
|
859
|
-
type: 'string',
|
|
860
|
-
},
|
|
861
|
-
example: 'createdAt',
|
|
862
|
-
description: 'Sort by field',
|
|
863
|
-
},
|
|
864
|
-
validation: {
|
|
865
|
-
required: true,
|
|
866
|
-
handler(value) {
|
|
867
|
-
return [
|
|
868
|
-
'createdAt',
|
|
869
|
-
'updatedAt',
|
|
870
|
-
].includes(value);
|
|
871
|
-
},
|
|
872
|
-
error: {
|
|
873
|
-
statusCode: 400,
|
|
874
|
-
message: 'sortBy should be createdAt or updatedAt',
|
|
875
|
-
},
|
|
876
|
-
},
|
|
598
|
+
mimetypeError: {
|
|
599
|
+
statusCode: 415,
|
|
600
|
+
message: 'Only PNG allowed',
|
|
877
601
|
},
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
type: 'string',
|
|
882
|
-
},
|
|
883
|
-
example: 'asc',
|
|
884
|
-
description: 'Sort order',
|
|
885
|
-
},
|
|
886
|
-
validation: {
|
|
887
|
-
required: true,
|
|
888
|
-
handler(value) {
|
|
889
|
-
return [
|
|
890
|
-
'asc',
|
|
891
|
-
'desc',
|
|
892
|
-
].includes(value);
|
|
893
|
-
},
|
|
894
|
-
error: {
|
|
895
|
-
statusCode: 400,
|
|
896
|
-
message: 'sortOrder should be asc or desc',
|
|
897
|
-
},
|
|
898
|
-
},
|
|
602
|
+
requiredError: {
|
|
603
|
+
statusCode: 400,
|
|
604
|
+
message: 'File is required',
|
|
899
605
|
},
|
|
900
|
-
}
|
|
901
|
-
|
|
606
|
+
})
|
|
607
|
+
avatar: string = '';
|
|
608
|
+
}
|
|
902
609
|
```
|
|
903
610
|
|
|
904
|
-
|
|
905
|
-
|
|
906
|
-
```typescript
|
|
907
|
-
app.post('/user', context, (request: Request) => {
|
|
908
|
-
console.log(request.query);
|
|
611
|
+
2. Use in controller:
|
|
909
612
|
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
// sortOrder: 'desc'
|
|
913
|
-
// }
|
|
613
|
+
```ts
|
|
614
|
+
import { createWriteStream } from 'fs';
|
|
914
615
|
|
|
915
|
-
|
|
616
|
+
@post('/user/:id/avatar', { tags: ['User'] })
|
|
617
|
+
@withAuth(bearerAuth)
|
|
618
|
+
@defineRequest({ path: Id, files: UserFilesDto })
|
|
619
|
+
setAvatar(req) {
|
|
620
|
+
const { avatar } = req.files;
|
|
621
|
+
// avatar = { stream: Readable, size: number, mimetype: string }
|
|
916
622
|
|
|
917
|
-
|
|
623
|
+
const writeStream = createWriteStream(`./uploads/user-${req.path.id}.png`);
|
|
624
|
+
avatar.stream.pipe(writeStream);
|
|
918
625
|
|
|
919
626
|
return {
|
|
920
|
-
statusCode:
|
|
921
|
-
headers: {
|
|
922
|
-
|
|
923
|
-
},
|
|
924
|
-
body: [],
|
|
627
|
+
statusCode: 202,
|
|
628
|
+
headers: { 'Content-Type': 'application/json' },
|
|
629
|
+
body: { message: 'Upload accepted' },
|
|
925
630
|
};
|
|
926
|
-
}
|
|
631
|
+
}
|
|
927
632
|
```
|
|
928
633
|
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
},
|
|
947
|
-
requiredError: {
|
|
948
|
-
statusCode: 400,
|
|
949
|
-
message: 'The avatar file is required',
|
|
634
|
+
---
|
|
635
|
+
|
|
636
|
+
## OpenAPI / Swagger
|
|
637
|
+
|
|
638
|
+
Alado generates OpenAPI 3.0 documentation automatically from your decorators. No extra config needed — just enable it in server options:
|
|
639
|
+
|
|
640
|
+
```ts
|
|
641
|
+
const app = initializeApplication({
|
|
642
|
+
serverOptions: {
|
|
643
|
+
port: 3000,
|
|
644
|
+
openApiDoc: {
|
|
645
|
+
enable: true,
|
|
646
|
+
route: '/', // serve Swagger UI at this path
|
|
647
|
+
info: {
|
|
648
|
+
title: 'My API',
|
|
649
|
+
description: 'Auto-generated with Alado',
|
|
650
|
+
version: '1.0.0',
|
|
950
651
|
},
|
|
951
652
|
},
|
|
952
653
|
},
|
|
953
|
-
|
|
654
|
+
controllers: [UserController],
|
|
655
|
+
});
|
|
954
656
|
```
|
|
955
657
|
|
|
956
|
-
|
|
658
|
+
Open `http://localhost:3000/` to view the Swagger UI.
|
|
957
659
|
|
|
958
|
-
|
|
959
|
-
import { createWriteStream } from 'fs';
|
|
960
|
-
|
|
961
|
-
app.post('/user/:id/avatar', context, (request: Request) => {
|
|
962
|
-
console.log(request.files);
|
|
963
|
-
// {
|
|
964
|
-
// avatar: {
|
|
965
|
-
// stream: Readable,
|
|
966
|
-
// size: 65025,
|
|
967
|
-
// mimetype: 'image/png'
|
|
968
|
-
// }
|
|
969
|
-
// }
|
|
970
|
-
|
|
971
|
-
// You can play with request.files.avatar.stream as You want e.g. save it locally
|
|
660
|
+
Documentation is built from:
|
|
972
661
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
'Content-Type': 'application/json',
|
|
979
|
-
},
|
|
980
|
-
body: user,
|
|
981
|
-
};
|
|
982
|
-
});
|
|
983
|
-
```
|
|
662
|
+
- `@get`, `@post`, etc. → endpoint paths and methods
|
|
663
|
+
- `tags`, `title`, `description` in decorator options → grouping and descriptions
|
|
664
|
+
- `@defineRequest` + DTO `@documentProperty` → request schema
|
|
665
|
+
- `@defineResponse` → response schema
|
|
666
|
+
- `isHidden: true` → excludes endpoint from docs
|
|
984
667
|
|
|
985
|
-
|
|
668
|
+
---
|
|
986
669
|
|
|
987
|
-
|
|
670
|
+
## API Reference
|
|
988
671
|
|
|
989
|
-
|
|
990
|
-
interface Response<T> {
|
|
991
|
-
title?: string;
|
|
992
|
-
description?: string;
|
|
993
|
-
statusCode: number;
|
|
994
|
-
headers?: Record<string, string>;
|
|
995
|
-
body?: T;
|
|
996
|
-
}
|
|
997
|
-
```
|
|
672
|
+
### `initializeApplication(options)`
|
|
998
673
|
|
|
999
|
-
|
|
674
|
+
| Property | Type | Description |
|
|
675
|
+
| --------------- | ---------------------------------- | ------------------------------ |
|
|
676
|
+
| `serverOptions` | `AladoServerOptions` | Server configuration |
|
|
677
|
+
| `controllers` | `AnyClass[] \| ControllerConfig[]` | Controller classes to register |
|
|
1000
678
|
|
|
1001
|
-
|
|
679
|
+
### `AladoServerOptions`
|
|
1002
680
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
681
|
+
| Property | Type | Default | Description |
|
|
682
|
+
| -------- | ---------------------- | ------- | ----------------------------------- |
|
|
683
|
+
| `port` | `number` | `3000` | HTTP port |
|
|
684
|
+
| `appId` | `string` | — | App identifier for multi-app setups |
|
|
685
|
+
| `logger` | `AladoServerLogger` | — | Custom logger |
|
|
686
|
+
| `ssl` | `SecureContextOptions` | — | TLS/HTTPS options |
|
|
1006
687
|
|
|
1007
|
-
|
|
1008
|
-
the response body can be an object (for application/json content-type), text, or even a stream
|
|
688
|
+
### `Request`
|
|
1009
689
|
|
|
1010
|
-
|
|
1011
|
-
interface UserDto {
|
|
1012
|
-
id: number;
|
|
1013
|
-
name: string;
|
|
1014
|
-
}
|
|
690
|
+
The object received in every route handler:
|
|
1015
691
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
692
|
+
| Property | Type | Description |
|
|
693
|
+
| --------- | ---------------------------- | ---------------------------------- |
|
|
694
|
+
| `request` | `IncomingMessage` | Raw Node.js request |
|
|
695
|
+
| `ip` | `string` | Client IP address |
|
|
696
|
+
| `method` | `string` | HTTP method |
|
|
697
|
+
| `url` | `string` | Request URL |
|
|
698
|
+
| `headers` | `IncomingHttpHeaders` | Request headers |
|
|
699
|
+
| `path` | `Record<string, string>` | Path parameters |
|
|
700
|
+
| `query` | `Record<string, any>` | Query parameters |
|
|
701
|
+
| `body` | `Record<string, any>` | Request body (POST/PUT/PATCH only) |
|
|
702
|
+
| `rawBody` | `string` | Unparsed body string |
|
|
703
|
+
| `files` | `Record<string, FileResult>` | Uploaded files |
|
|
704
|
+
| `auth` | `Record<string, any>` | Auth handler output |
|
|
1027
705
|
|
|
1028
|
-
|
|
1029
|
-
const body: string = 'Not Found';
|
|
1030
|
-
return {
|
|
1031
|
-
statusCode: 404,
|
|
1032
|
-
headers: { 'Content-Type': 'text/plain' },
|
|
1033
|
-
body,
|
|
1034
|
-
};
|
|
1035
|
-
};
|
|
706
|
+
### `Response<T>`
|
|
1036
707
|
|
|
1037
|
-
|
|
1038
|
-
const body = createReadStream('/path/to/image.png');
|
|
1039
|
-
return {
|
|
1040
|
-
statusCode: 200,
|
|
1041
|
-
headers: { 'Content-Type': 'image/png' },
|
|
1042
|
-
body,
|
|
1043
|
-
};
|
|
1044
|
-
};
|
|
1045
|
-
```
|
|
708
|
+
The object your handler must return:
|
|
1046
709
|
|
|
1047
|
-
|
|
710
|
+
| Property | Type | Required | Description |
|
|
711
|
+
| ------------ | ------------------------ | -------- | ----------------------------------------------- |
|
|
712
|
+
| `statusCode` | `number` | required | HTTP status code |
|
|
713
|
+
| `headers` | `Record<string, string>` | — | Response headers |
|
|
714
|
+
| `body` | `T` | — | Response body — object, string, or `ReadStream` |
|
|
1048
715
|
|
|
1049
|
-
|
|
1050
|
-
app.start(() => console.log('Your awesome application has been successfully launched'));
|
|
716
|
+
---
|
|
1051
717
|
|
|
1052
|
-
|
|
1053
|
-
app.stop(() => console.log('Your awesome application has been successfully launched'));
|
|
1054
|
-
```
|
|
718
|
+
## License
|
|
1055
719
|
|
|
1056
|
-
|
|
720
|
+
MIT © [Databikers Limited](https://databikers.com)
|