alado 2.2.1 → 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 -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,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:
|
|
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
|
+
}
|
|
26
95
|
```
|
|
27
96
|
|
|
28
|
-
|
|
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
|
+
});
|
|
29
106
|
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
- `appId`: Identifier of the application instance.
|
|
33
|
-
- `title`: A descriptive name of the endpoint.
|
|
34
|
-
- `description`: Documentation text for the endpoint.
|
|
35
|
-
- `tags`: Array of tags for grouping in OpenAPI docs.
|
|
36
|
-
- `isHidden`: Boolean flag to hide the endpoint from OpenAPI output
|
|
107
|
+
app.start(() => console.log('Running on http://localhost:3000'));
|
|
108
|
+
```
|
|
37
109
|
|
|
38
110
|
---
|
|
39
111
|
|
|
40
|
-
|
|
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
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
---
|
|
135
|
+
|
|
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
|
+
```
|
|
156
|
+
|
|
157
|
+
---
|
|
158
|
+
|
|
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: 'Created',
|
|
171
|
-
entity: 'User',
|
|
172
350
|
headers: { 'Content-Type': 'application/json' },
|
|
173
|
-
body:
|
|
351
|
+
body: {},
|
|
174
352
|
})
|
|
175
353
|
@defineRequest({ body: CredentialsDto })
|
|
176
|
-
|
|
177
|
-
const { body } = req;
|
|
178
|
-
const user = DataStore.signUp(body as CredentialsDto);
|
|
354
|
+
async create(req) {
|
|
179
355
|
return {
|
|
180
356
|
statusCode: 201,
|
|
181
357
|
headers: { 'Content-Type': 'application/json' },
|
|
182
|
-
body:
|
|
358
|
+
body: req.body,
|
|
183
359
|
};
|
|
184
360
|
}
|
|
185
361
|
|
|
@@ -187,70 +363,29 @@ export class UserController {
|
|
|
187
363
|
@withAuth(bearerAuth)
|
|
188
364
|
@defineResponse({
|
|
189
365
|
statusCode: 200,
|
|
190
|
-
title: 'OK',
|
|
191
|
-
enity: 'User',
|
|
192
366
|
headers: { 'Content-Type': 'application/json' },
|
|
193
|
-
body:
|
|
367
|
+
body: {},
|
|
194
368
|
})
|
|
195
|
-
@defineRequest({
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
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
|
-
}
|
|
369
|
+
@defineRequest({
|
|
370
|
+
path: Id,
|
|
371
|
+
body: UserDto,
|
|
372
|
+
})
|
|
373
|
+
async update(req) {
|
|
210
374
|
return {
|
|
211
|
-
statusCode:
|
|
375
|
+
statusCode: 200,
|
|
212
376
|
headers: { 'Content-Type': 'application/json' },
|
|
213
|
-
body:
|
|
377
|
+
body: req.body,
|
|
214
378
|
};
|
|
215
379
|
}
|
|
216
380
|
|
|
217
|
-
@
|
|
381
|
+
@del('/user/:id', { tags: ['User'] })
|
|
218
382
|
@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);
|
|
383
|
+
@defineRequest({ path: Id })
|
|
384
|
+
async remove(req) {
|
|
250
385
|
return {
|
|
251
|
-
statusCode:
|
|
252
|
-
headers: {
|
|
253
|
-
body:
|
|
386
|
+
statusCode: 204,
|
|
387
|
+
headers: {},
|
|
388
|
+
body: null,
|
|
254
389
|
};
|
|
255
390
|
}
|
|
256
391
|
}
|
|
@@ -258,93 +393,77 @@ export class UserController {
|
|
|
258
393
|
|
|
259
394
|
---
|
|
260
395
|
|
|
261
|
-
##
|
|
396
|
+
## DTOs
|
|
262
397
|
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
```ts
|
|
266
|
-
fileUploadProperty(filePropertyDefinition: [FilePropertyDefinition](#filepropertydefinition))
|
|
267
|
-
```
|
|
268
|
-
|
|
269
|
-
#### Arguments
|
|
398
|
+
DTOs are plain TypeScript classes with decorated properties. They define how request data is validated, transformed, and documented.
|
|
270
399
|
|
|
271
|
-
|
|
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.
|
|
400
|
+
### `validateProperty`
|
|
276
401
|
|
|
277
|
-
|
|
402
|
+
Validates an incoming property value. Runs before your handler.
|
|
278
403
|
|
|
279
404
|
```ts
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
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';
|
|
287
417
|
}
|
|
288
418
|
```
|
|
289
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
|
+
|
|
290
425
|
---
|
|
291
426
|
|
|
292
427
|
### `documentProperty`
|
|
293
428
|
|
|
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`
|
|
429
|
+
Adds OpenAPI/Swagger metadata to a property.
|
|
306
430
|
|
|
307
431
|
```ts
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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';
|
|
312
445
|
}
|
|
313
446
|
```
|
|
314
447
|
|
|
315
|
-
|
|
448
|
+
Supports the full OpenAPI `PropertyDefinitionSchema`:
|
|
316
449
|
|
|
317
450
|
```ts
|
|
318
451
|
interface PropertyDefinitionSchema {
|
|
319
|
-
type?: string;
|
|
320
|
-
format?: string;
|
|
452
|
+
type?: string; // 'string' | 'number' | 'boolean' | 'object' | 'array'
|
|
453
|
+
format?: string; // 'email' | 'uuid' | 'date-time' | ...
|
|
321
454
|
default?: any;
|
|
322
|
-
$ref?: string;
|
|
323
455
|
nullable?: boolean;
|
|
324
|
-
readOnly?: boolean;
|
|
325
|
-
writeOnly?: boolean;
|
|
326
|
-
deprecated?: boolean;
|
|
327
456
|
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
457
|
minimum?: number;
|
|
337
458
|
maximum?: number;
|
|
338
|
-
exclusiveMinimum?: number;
|
|
339
|
-
exclusiveMaximum?: number;
|
|
340
|
-
multipleOf?: number;
|
|
341
459
|
minLength?: number;
|
|
342
460
|
maxLength?: number;
|
|
343
461
|
pattern?: string;
|
|
344
|
-
items?: PropertyDefinitionSchema;
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
462
|
+
items?: PropertyDefinitionSchema; // for arrays
|
|
463
|
+
properties?: Record<string, PropertyDefinitionSchema>; // for objects
|
|
464
|
+
oneOf?: PropertyDefinitionSchema[];
|
|
465
|
+
anyOf?: PropertyDefinitionSchema[];
|
|
466
|
+
allOf?: PropertyDefinitionSchema[];
|
|
348
467
|
}
|
|
349
468
|
```
|
|
350
469
|
|
|
@@ -352,350 +471,100 @@ interface PropertyDefinitionSchema {
|
|
|
352
471
|
|
|
353
472
|
### `transformProperty`
|
|
354
473
|
|
|
355
|
-
|
|
356
|
-
transformProperty(propertyTransformer: (value:any) => any | Promise<any>)
|
|
357
|
-
|
|
358
|
-
```
|
|
359
|
-
|
|
360
|
-
### `validateProperty`
|
|
474
|
+
Transforms a property value before it reaches your handler.
|
|
361
475
|
|
|
362
476
|
```ts
|
|
363
|
-
|
|
364
|
-
```
|
|
477
|
+
import { transformProperty } from 'alado';
|
|
365
478
|
|
|
366
|
-
|
|
479
|
+
class QueryDto {
|
|
480
|
+
@transformProperty((value) => parseInt(value.toString(), 10))
|
|
481
|
+
page: number = 1;
|
|
367
482
|
|
|
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;
|
|
483
|
+
@transformProperty((value) => value.trim().toLowerCase())
|
|
484
|
+
email: string = '';
|
|
380
485
|
}
|
|
381
486
|
```
|
|
382
487
|
|
|
383
488
|
---
|
|
384
489
|
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
```typescript
|
|
388
|
-
import { validateProperty, documentProperty } from 'alado';
|
|
490
|
+
### `fileUploadProperty`
|
|
389
491
|
|
|
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
|
-
```
|
|
492
|
+
Marks a DTO property as a file upload field with MIME type and size validation.
|
|
409
493
|
|
|
410
|
-
```
|
|
494
|
+
```ts
|
|
411
495
|
import { fileUploadProperty } from 'alado';
|
|
412
496
|
|
|
413
497
|
export class UserFilesDto {
|
|
414
498
|
@fileUploadProperty({
|
|
415
|
-
mimetypes: [
|
|
416
|
-
|
|
499
|
+
mimetypes: [
|
|
500
|
+
'image/png',
|
|
501
|
+
'image/jpeg',
|
|
502
|
+
],
|
|
503
|
+
maxSize: 2 * 1024 * 1024, // 2MB in bytes
|
|
417
504
|
required: true,
|
|
418
505
|
maxSizeError: {
|
|
419
506
|
statusCode: 413,
|
|
420
|
-
message: '
|
|
507
|
+
message: 'File must not exceed 2MB',
|
|
421
508
|
},
|
|
422
509
|
mimetypeError: {
|
|
423
510
|
statusCode: 415,
|
|
424
|
-
message: '
|
|
511
|
+
message: 'Only PNG and JPEG images are allowed',
|
|
425
512
|
},
|
|
426
513
|
requiredError: {
|
|
427
514
|
statusCode: 400,
|
|
428
|
-
message: '
|
|
515
|
+
message: 'Avatar file is required',
|
|
429
516
|
},
|
|
430
517
|
})
|
|
431
|
-
avatar: string = '/path/to/
|
|
518
|
+
avatar: string = '/path/to/avatar.png';
|
|
432
519
|
}
|
|
433
520
|
```
|
|
434
521
|
|
|
435
|
-
|
|
436
|
-
import { validateProperty, documentProperty } from 'alado';
|
|
522
|
+
---
|
|
437
523
|
|
|
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';
|
|
524
|
+
### Combining decorators
|
|
525
|
+
|
|
526
|
+
Decorators stack — use them together for full validation + documentation:
|
|
455
527
|
|
|
528
|
+
```ts
|
|
529
|
+
export class Id {
|
|
456
530
|
@validateProperty({
|
|
457
531
|
required: true,
|
|
458
|
-
handler:
|
|
532
|
+
handler: (value) => /^[0-9a-f-]{36}$/.test(value),
|
|
459
533
|
error: {
|
|
460
534
|
statusCode: 400,
|
|
461
|
-
message: 'Invalid
|
|
535
|
+
message: 'Invalid UUID',
|
|
462
536
|
},
|
|
463
537
|
})
|
|
464
538
|
@documentProperty({
|
|
465
539
|
schema: {
|
|
466
540
|
type: 'string',
|
|
541
|
+
format: 'uuid',
|
|
467
542
|
},
|
|
468
|
-
example: '
|
|
469
|
-
description: '
|
|
543
|
+
example: '7ef5ed25-53b1-432f-96ec-8e35d830eb9c',
|
|
544
|
+
description: 'Resource identifier',
|
|
470
545
|
})
|
|
471
|
-
|
|
546
|
+
id: string = '7ef5ed25-53b1-432f-96ec-8e35d830eb9c';
|
|
472
547
|
}
|
|
473
|
-
|
|
474
|
-
export const credentialsDto = new CredentialsDto();
|
|
475
548
|
```
|
|
476
549
|
|
|
477
550
|
---
|
|
478
551
|
|
|
479
|
-
##
|
|
480
|
-
|
|
481
|
-
Finally, use the `initializeApplication` function to get an instance you would like to run:
|
|
482
|
-
|
|
483
|
-
```ts
|
|
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.
|
|
552
|
+
## Authentication
|
|
516
553
|
|
|
517
|
-
|
|
554
|
+
Define a `RequestAuthentication` config object and pass it to `@withAuth`.
|
|
518
555
|
|
|
519
556
|
```ts
|
|
520
|
-
|
|
521
|
-
serverOptions: AladoServerOptions;
|
|
522
|
-
controllers: AnyClass[];
|
|
523
|
-
}
|
|
524
|
-
```
|
|
525
|
-
|
|
526
|
-
#### `AladoServerOptions`
|
|
527
|
-
|
|
528
|
-
```ts
|
|
529
|
-
interface AladoServerOptions {
|
|
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
|
|
557
|
+
// src/modules/user/auth/bearer.auth.ts
|
|
686
558
|
import { RequestAuthentication } from 'alado';
|
|
559
|
+
import { DataStore } from '@data-store';
|
|
687
560
|
|
|
688
|
-
const bearerAuth: RequestAuthentication = {
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
return
|
|
694
|
-
name: 'John Doe',
|
|
695
|
-
id: 1,
|
|
696
|
-
};
|
|
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
|
|
697
567
|
},
|
|
698
|
-
required: true, // false You want "soft" checking
|
|
699
568
|
error: {
|
|
700
569
|
statusCode: 401,
|
|
701
570
|
message: 'Unauthorized',
|
|
@@ -703,43 +572,26 @@ const bearerAuth: RequestAuthentication = {
|
|
|
703
572
|
};
|
|
704
573
|
```
|
|
705
574
|
|
|
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';
|
|
575
|
+
The `inputProperty` supports dot-notation and can point to any part of the request:
|
|
723
576
|
|
|
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
|
|
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),
|
|
743
595
|
error: {
|
|
744
596
|
statusCode: 401,
|
|
745
597
|
message: 'Unauthorized',
|
|
@@ -747,310 +599,145 @@ const bearerAuth: RequestAuthentication = {
|
|
|
747
599
|
};
|
|
748
600
|
```
|
|
749
601
|
|
|
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.
|
|
766
|
-
|
|
767
|
-
3.2.3 Context.request
|
|
602
|
+
---
|
|
768
603
|
|
|
769
|
-
|
|
604
|
+
## File Uploads
|
|
770
605
|
|
|
771
|
-
|
|
772
|
-
interface ContextRequest {
|
|
773
|
-
headers?: Record<string, PropertyDefinition>;
|
|
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
|
-
```
|
|
606
|
+
1. Define a files DTO:
|
|
781
607
|
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
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
|
-
```
|
|
608
|
+
```ts
|
|
609
|
+
// src/modules/user/dto/user-files.dto.ts
|
|
610
|
+
import { fileUploadProperty } from 'alado';
|
|
812
611
|
|
|
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
|
-
},
|
|
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',
|
|
845
620
|
},
|
|
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
|
-
},
|
|
621
|
+
mimetypeError: {
|
|
622
|
+
statusCode: 415,
|
|
623
|
+
message: 'Only PNG allowed',
|
|
877
624
|
},
|
|
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
|
-
},
|
|
625
|
+
requiredError: {
|
|
626
|
+
statusCode: 400,
|
|
627
|
+
message: 'File is required',
|
|
899
628
|
},
|
|
900
|
-
}
|
|
901
|
-
|
|
629
|
+
})
|
|
630
|
+
avatar: string = '';
|
|
631
|
+
}
|
|
902
632
|
```
|
|
903
633
|
|
|
904
|
-
|
|
634
|
+
2. Use in controller:
|
|
905
635
|
|
|
906
|
-
```
|
|
907
|
-
|
|
908
|
-
console.log(request.query);
|
|
909
|
-
|
|
910
|
-
// {
|
|
911
|
-
// sortBy: 'updatedAt',
|
|
912
|
-
// sortOrder: 'desc'
|
|
913
|
-
// }
|
|
636
|
+
```ts
|
|
637
|
+
import { createWriteStream } from 'fs';
|
|
914
638
|
|
|
915
|
-
|
|
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 }
|
|
916
645
|
|
|
917
|
-
|
|
646
|
+
const writeStream = createWriteStream(`./uploads/user-${req.path.id}.png`);
|
|
647
|
+
avatar.stream.pipe(writeStream);
|
|
918
648
|
|
|
919
649
|
return {
|
|
920
|
-
statusCode:
|
|
921
|
-
headers: {
|
|
922
|
-
|
|
923
|
-
},
|
|
924
|
-
body: [],
|
|
650
|
+
statusCode: 202,
|
|
651
|
+
headers: { 'Content-Type': 'application/json' },
|
|
652
|
+
body: { message: 'Upload accepted' },
|
|
925
653
|
};
|
|
926
|
-
}
|
|
654
|
+
}
|
|
927
655
|
```
|
|
928
656
|
|
|
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',
|
|
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',
|
|
950
674
|
},
|
|
951
675
|
},
|
|
952
676
|
},
|
|
953
|
-
|
|
677
|
+
controllers: [UserController],
|
|
678
|
+
});
|
|
954
679
|
```
|
|
955
680
|
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
```typescript
|
|
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
|
-
// }
|
|
681
|
+
Open `http://localhost:3000/` to view the Swagger UI.
|
|
970
682
|
|
|
971
|
-
|
|
683
|
+
Documentation is built from:
|
|
972
684
|
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
'Content-Type': 'application/json',
|
|
979
|
-
},
|
|
980
|
-
body: user,
|
|
981
|
-
};
|
|
982
|
-
});
|
|
983
|
-
```
|
|
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
|
|
984
690
|
|
|
985
|
-
|
|
691
|
+
---
|
|
986
692
|
|
|
987
|
-
|
|
693
|
+
## API Reference
|
|
988
694
|
|
|
989
|
-
|
|
990
|
-
interface Response<T> {
|
|
991
|
-
title?: string;
|
|
992
|
-
description?: string;
|
|
993
|
-
statusCode: number;
|
|
994
|
-
headers?: Record<string, string>;
|
|
995
|
-
body?: T;
|
|
996
|
-
}
|
|
997
|
-
```
|
|
695
|
+
### `initializeApplication(options)`
|
|
998
696
|
|
|
999
|
-
|
|
697
|
+
| Property | Type | Description |
|
|
698
|
+
| --------------- | ---------------------------------- | ------------------------------ |
|
|
699
|
+
| `serverOptions` | `AladoServerOptions` | Server configuration |
|
|
700
|
+
| `controllers` | `AnyClass[] \| ControllerConfig[]` | Controller classes to register |
|
|
1000
701
|
|
|
1001
|
-
|
|
702
|
+
### `AladoServerOptions`
|
|
1002
703
|
|
|
1003
|
-
|
|
1004
|
-
|
|
1005
|
-
|
|
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 |
|
|
1006
710
|
|
|
1007
|
-
|
|
1008
|
-
the response body can be an object (for application/json content-type), text, or even a stream
|
|
711
|
+
### `Request`
|
|
1009
712
|
|
|
1010
|
-
|
|
1011
|
-
interface UserDto {
|
|
1012
|
-
id: number;
|
|
1013
|
-
name: string;
|
|
1014
|
-
}
|
|
713
|
+
The object received in every route handler:
|
|
1015
714
|
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
|
|
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 |
|
|
1027
728
|
|
|
1028
|
-
|
|
1029
|
-
const body: string = 'Not Found';
|
|
1030
|
-
return {
|
|
1031
|
-
statusCode: 404,
|
|
1032
|
-
headers: { 'Content-Type': 'text/plain' },
|
|
1033
|
-
body,
|
|
1034
|
-
};
|
|
1035
|
-
};
|
|
729
|
+
### `Response<T>`
|
|
1036
730
|
|
|
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
|
-
```
|
|
731
|
+
The object your handler must return:
|
|
1046
732
|
|
|
1047
|
-
|
|
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` |
|
|
1048
738
|
|
|
1049
|
-
|
|
1050
|
-
app.start(() => console.log('Your awesome application has been successfully launched'));
|
|
739
|
+
---
|
|
1051
740
|
|
|
1052
|
-
|
|
1053
|
-
app.stop(() => console.log('Your awesome application has been successfully launched'));
|
|
1054
|
-
```
|
|
741
|
+
## License
|
|
1055
742
|
|
|
1056
|
-
|
|
743
|
+
MIT © [Databikers Limited](https://databikers.com)
|