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