@e22m4u/ts-rest-router 0.5.1 → 0.5.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +489 -305
- package/dist/cjs/index.cjs +14 -14
- package/dist/esm/controller-registry.js +8 -8
- package/dist/esm/decorators/after-action/after-action-decorator.d.ts +2 -2
- package/dist/esm/decorators/after-action/after-action-decorator.js +4 -4
- package/dist/esm/decorators/after-action/after-action-decorator.spec.js +22 -25
- package/dist/esm/decorators/after-action/after-action-metadata.d.ts +1 -1
- package/dist/esm/decorators/after-action/after-action-reflector.spec.js +15 -15
- package/dist/esm/decorators/before-action/before-action-decorator.d.ts +2 -2
- package/dist/esm/decorators/before-action/before-action-decorator.js +4 -4
- package/dist/esm/decorators/before-action/before-action-decorator.spec.js +22 -25
- package/dist/esm/decorators/before-action/before-action-metadata.d.ts +1 -1
- package/dist/esm/decorators/before-action/before-action-reflector.spec.js +15 -15
- package/package.json +9 -9
- package/src/controller-registry.spec.ts +21 -21
- package/src/controller-registry.ts +8 -8
- package/src/decorators/after-action/after-action-decorator.spec.ts +22 -25
- package/src/decorators/after-action/after-action-decorator.ts +4 -4
- package/src/decorators/after-action/after-action-metadata.ts +1 -1
- package/src/decorators/after-action/after-action-reflector.spec.ts +15 -15
- package/src/decorators/before-action/before-action-decorator.spec.ts +22 -25
- package/src/decorators/before-action/before-action-decorator.ts +4 -4
- package/src/decorators/before-action/before-action-metadata.ts +1 -1
- package/src/decorators/before-action/before-action-reflector.spec.ts +15 -15
package/README.md
CHANGED
@@ -1,25 +1,56 @@
|
|
1
1
|
# @e22m4u/ts-rest-router
|
2
2
|
|
3
|
-
REST
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
3
|
+
**REST-маршрутизатор на основе контроллеров и TypeScript декораторов.**
|
4
|
+
|
5
|
+
Модуль `@e22m4u/ts-rest-router` позволяет создавать структурированное
|
6
|
+
и масштабируемое REST API. В его основе лежит декларативный подход
|
7
|
+
с использованием TypeScript декораторов для определения маршрутов,
|
8
|
+
обработки входящих данных и управления жизненным циклом запроса.
|
9
|
+
|
10
|
+
### Особенности
|
11
|
+
|
12
|
+
- **Декларативная маршрутизация**
|
13
|
+
Определение маршрутов непосредственно над методами контроллера с помощью
|
14
|
+
декораторов (`@getAction`, `@postAction` и т.д.).
|
15
|
+
- **Типобезопасная обработка данных**
|
16
|
+
Автоматическое извлечение, преобразование и валидация данных из `body`,
|
17
|
+
`query`, `params`, `headers` и `cookie` с привязкой к типизированным
|
18
|
+
аргументам методов.
|
19
|
+
- **Встроенная валидация**
|
20
|
+
Использование схем данных из
|
21
|
+
[@e22m4u/ts-data-schema](https://www.npmjs.com/package/@e22m4u/ts-data-schema)
|
22
|
+
для описания сложных правил проверки.
|
23
|
+
- **Хуки (`@beforeAction`, `@afterAction`)**
|
24
|
+
Поддержка хуков для выполнения сквозной логики (например, аутентификация
|
25
|
+
или логирование) на уровне контроллера и отдельных методов.
|
26
|
+
- **Изоляция запросов**
|
27
|
+
Обработка каждого запроса в отдельном DI-контейнере, что гарантирует
|
28
|
+
отсутствие конфликтов состояний и повышает надежность приложения.
|
29
|
+
- **Производительность и гибкая архитектура**
|
30
|
+
Основан на
|
31
|
+
[@e22m4u/js-trie-router](https://www.npmjs.com/package/@e22m4u/js-trie-router)
|
32
|
+
для маршрутизации на базе *префиксного дерева* и
|
33
|
+
[@e22m4u/js-service](https://www.npmjs.com/package/@e22m4u/js-service)
|
34
|
+
для внедрения зависимостей.
|
12
35
|
|
13
36
|
## Содержание
|
14
37
|
|
15
38
|
- [Установка](#установка)
|
16
|
-
|
17
|
-
- [
|
18
|
-
- [
|
19
|
-
- [
|
20
|
-
- [
|
39
|
+
- [Быстрый старт: Пример сервера](#быстрый-старт-пример-сервера)
|
40
|
+
- [Обработка данных запроса](#обработка-данных-запроса)
|
41
|
+
- [URL-параметры (`@requestParam`)](#url-параметры-requestparam)
|
42
|
+
- [Query-параметры (`@requestQuery`)](#query-параметры-requestquery)
|
43
|
+
- [Тело запроса (`@requestBody`, `@requestField`)](#тело-запроса-requestbody-requestfield)
|
44
|
+
- [Заголовки и Cookies](#заголовки-и-cookies)
|
45
|
+
- [Контекст запроса (`@requestContext`)](#контекст-запроса-requestcontext)
|
46
|
+
- [Валидация и схемы данных](#валидация-и-схемы-данных)
|
47
|
+
- [Хуки (`@beforeAction`, `@afterAction`)](#хуки-beforeaction-afteraction)
|
48
|
+
- [Архитектура: Жизненный цикл контроллера и DI](#архитектура-жизненный-цикл-контроллера-и-di)
|
49
|
+
- [Производительность: Префиксное дерево](#производительность-префиксное-дерево)
|
50
|
+
- [Полный список декораторов](#полный-список-декораторов)
|
21
51
|
- [Отладка](#отладка)
|
22
52
|
- [Тесты](#тесты)
|
53
|
+
- [Лицензия](#лицензия)
|
23
54
|
|
24
55
|
## Установка
|
25
56
|
|
@@ -27,10 +58,10 @@ REST маршрутизатор на основе контроллеров дл
|
|
27
58
|
npm install @e22m4u/ts-rest-router
|
28
59
|
```
|
29
60
|
|
30
|
-
|
61
|
+
**Поддержка декораторов**
|
31
62
|
|
32
|
-
Для включения поддержки декораторов, добавьте указанные
|
33
|
-
|
63
|
+
Для включения поддержки декораторов, добавьте указанные ниже опции в файл
|
64
|
+
`tsconfig.json` вашего проекта.
|
34
65
|
|
35
66
|
```json
|
36
67
|
{
|
@@ -39,375 +70,436 @@ npm install @e22m4u/ts-rest-router
|
|
39
70
|
}
|
40
71
|
```
|
41
72
|
|
42
|
-
##
|
73
|
+
## Быстрый старт: Пример сервера
|
74
|
+
|
75
|
+
Пример создания простого сервера для управления списком пользователей.
|
43
76
|
|
44
|
-
|
77
|
+
**`user.controller.ts`**
|
45
78
|
|
46
79
|
```ts
|
47
|
-
import {
|
48
|
-
|
49
|
-
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
80
|
+
import {
|
81
|
+
restController,
|
82
|
+
getAction,
|
83
|
+
postAction,
|
84
|
+
requestBody,
|
85
|
+
DataType,
|
86
|
+
} from '@e22m4u/ts-rest-router';
|
87
|
+
|
88
|
+
// Временное хранилище данных
|
89
|
+
const users = [
|
90
|
+
{id: 1, name: 'John Doe'},
|
91
|
+
];
|
92
|
+
|
93
|
+
// 1. Декоратор @restController определяет класс как контроллер
|
94
|
+
// и устанавливает базовый путь для всех его маршрутов.
|
55
95
|
@restController('users')
|
56
|
-
class UserController {
|
57
|
-
//
|
58
|
-
//
|
59
|
-
@
|
60
|
-
|
61
|
-
//
|
62
|
-
|
63
|
-
|
64
|
-
|
96
|
+
export class UserController {
|
97
|
+
// 2. Декоратор @getAction создает маршрут для GET-запросов.
|
98
|
+
// Полный путь: GET /users
|
99
|
+
@getAction()
|
100
|
+
getAllUsers() {
|
101
|
+
// Результат автоматически сериализуется в JSON
|
102
|
+
return users;
|
103
|
+
}
|
104
|
+
|
105
|
+
// 3. Декоратор @postAction создает маршрут для POST-запросов.
|
106
|
+
// Полный путь: POST /users
|
107
|
+
@postAction()
|
108
|
+
createUser(
|
109
|
+
// 4. Декоратор @requestBody извлекает тело запроса,
|
110
|
+
// проверяет его по схеме и передает в аргумент `newUser`.
|
111
|
+
@requestBody({
|
112
|
+
type: DataType.OBJECT,
|
113
|
+
properties: {
|
114
|
+
name: {
|
115
|
+
type: DataType.STRING,
|
116
|
+
required: true
|
117
|
+
},
|
118
|
+
},
|
119
|
+
})
|
120
|
+
newUser: {name: string},
|
65
121
|
) {
|
66
|
-
|
67
|
-
|
68
|
-
//
|
69
|
-
return
|
70
|
-
id: 1,
|
71
|
-
firstName: 'John',
|
72
|
-
lastName: 'Doe',
|
73
|
-
};
|
122
|
+
const user = {id: users.length + 1, ...newUser};
|
123
|
+
users.push(user);
|
124
|
+
// Возвращаемый объект будет отправлен клиенту как JSON.
|
125
|
+
return user;
|
74
126
|
}
|
75
127
|
}
|
76
128
|
```
|
77
129
|
|
78
|
-
|
130
|
+
**`index.ts`**
|
79
131
|
|
80
132
|
```ts
|
81
133
|
import http from 'http';
|
134
|
+
import {UserController} from './user.controller';
|
82
135
|
import {RestRouter} from '@e22m4u/ts-rest-router';
|
83
136
|
|
84
|
-
|
85
|
-
|
86
|
-
router
|
87
|
-
|
137
|
+
async function bootstrap() {
|
138
|
+
// Создание экземпляра роутера
|
139
|
+
const router = new RestRouter();
|
140
|
+
// Регистрация контроллера
|
141
|
+
router.addController(UserController);
|
142
|
+
|
143
|
+
// Создание HTTP-сервера с обработчиком запросов из роутера
|
144
|
+
const server = http.createServer(router.requestListener);
|
145
|
+
|
146
|
+
// Запуск сервера
|
147
|
+
server.listen(3000, () => {
|
148
|
+
console.log('Server is running on http://localhost:3000');
|
149
|
+
console.log('Try GET http://localhost:3000/users');
|
150
|
+
console.log(
|
151
|
+
'Try POST http://localhost:3000/users with body {"name": "Jane Doe"}'
|
152
|
+
);
|
153
|
+
});
|
154
|
+
}
|
88
155
|
|
89
|
-
|
90
|
-
|
91
|
-
|
156
|
+
bootstrap();
|
157
|
+
```
|
158
|
+
|
159
|
+
## Обработка данных запроса
|
160
|
+
|
161
|
+
Модуль предоставляет удобные и типобезопасные механизмы для доступа
|
162
|
+
к данным входящего запроса.
|
92
163
|
|
93
|
-
|
94
|
-
|
95
|
-
|
96
|
-
|
164
|
+
### URL-параметры (`@requestParam`)
|
165
|
+
|
166
|
+
Используются для извлечения динамических частей URL (например, `:id`).
|
167
|
+
|
168
|
+
```ts
|
169
|
+
import {getAction, requestParam, DataType} from '@e22m4u/ts-rest-router';
|
170
|
+
|
171
|
+
@restController('articles')
|
172
|
+
class ArticleController {
|
173
|
+
// Маршрут: GET /articles/42
|
174
|
+
@getAction(':id')
|
175
|
+
getArticleById(
|
176
|
+
// Извлечение параметра 'id' с проверкой на соответствие
|
177
|
+
// типу "number"
|
178
|
+
@requestParam('id', DataType.NUMBER) id: number,
|
179
|
+
) {
|
180
|
+
// Если id не является числом, фреймворк вернет ошибку
|
181
|
+
// 400 Bad Request
|
182
|
+
return {articleId: id, content: '...'};
|
183
|
+
}
|
184
|
+
}
|
97
185
|
```
|
98
186
|
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
имеет параметр `schemaOrType`, в котором определяется тип ожидаемого значения
|
104
|
-
или схема для проверки данных.
|
105
|
-
|
106
|
-
- `@requestParam(name: string, schemaOrType?: DataSchema | DataType)`
|
107
|
-
*- извлечение URL параметра по названию;*
|
108
|
-
- `@requestQuery(name: string, schemaOrType?: DataSchema | DataType)`
|
109
|
-
*- извлечение query параметра по названию;*
|
110
|
-
- `@requestBody(schemaOrType?: DataSchema | DataType)`
|
111
|
-
*- извлечение тела запроса;*
|
112
|
-
- `@requestField(name: string, schemaOrType?: DataSchema | DataType)`
|
113
|
-
*- извлечение свойства из тела запроса;*
|
114
|
-
- `@requestHeader(name: string, schemaOrType?: DataSchema | DataType)`
|
115
|
-
*- извлечение заголовка запроса по названию;*
|
116
|
-
- `@requestCookie(name: string, schemaOrType?: DataSchema | DataType)`
|
117
|
-
*- извлечение cookie запроса по названию;*
|
118
|
-
|
119
|
-
Проверка входящих данных выполняется встроенным модулем
|
120
|
-
[@e22m4u/ts-data-schema](https://www.npmjs.com/package/@e22m4u/ts-data-schema)
|
121
|
-
(не требует установки). Ниже приводятся константы для определения допустимых
|
122
|
-
типов извлекаемого значения.
|
187
|
+
**Декораторы:**
|
188
|
+
|
189
|
+
- `@requestParam(name, schema)` - извлечение одного параметра;
|
190
|
+
- `@requestParams(schema)` - извлечение всех URL-параметров в виде объекта;
|
123
191
|
|
124
|
-
|
125
|
-
- `DataType.STRING` - строковые значения
|
126
|
-
- `DataType.NUMBER` - числовые значения
|
127
|
-
- `DataType.BOOLEAN` - логические значения
|
128
|
-
- `DataType.ARRAY` - массивы
|
129
|
-
- `DataType.OBJECT` - объекты (не экземпляры)
|
192
|
+
### Query-параметры (`@requestQuery`)
|
130
193
|
|
131
|
-
|
132
|
-
|
133
|
-
элементы массива, функции-валидаторы и другие ограничения входящих данных.
|
194
|
+
Применяются для извлечения параметров из строки запроса
|
195
|
+
(например, `?sort=desc`).
|
134
196
|
|
135
197
|
```ts
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
|
141
|
-
|
142
|
-
|
198
|
+
import {getAction, requestQuery, DataType} from '@e22m4u/ts-rest-router';
|
199
|
+
|
200
|
+
@restController('products')
|
201
|
+
class ProductController {
|
202
|
+
// Маршрут: GET /products/search?q=phone&limit=10
|
203
|
+
@getAction('search')
|
204
|
+
searchProducts(
|
205
|
+
@requestQuery('q', {
|
206
|
+
type: DataType.STRING,
|
207
|
+
required: true,
|
208
|
+
})
|
209
|
+
searchTerm: string,
|
210
|
+
@requestQuery('limit', {
|
211
|
+
type: DataType.NUMBER,
|
212
|
+
default: 20,
|
213
|
+
})
|
214
|
+
limit: number,
|
215
|
+
) {
|
216
|
+
// searchTerm будет 'phone', limit будет 10.
|
217
|
+
// При отсутствии 'q' будет ошибка. При отсутствии 'limit'
|
218
|
+
// будет использовано значение по умолчанию 20.
|
219
|
+
return {results: [], query: {searchTerm, limit}};
|
220
|
+
}
|
143
221
|
}
|
144
222
|
```
|
145
223
|
|
146
|
-
|
224
|
+
**Декораторы:**
|
225
|
+
|
226
|
+
- `@requestQuery(name, schema)` - извлечение одного query-параметра;
|
227
|
+
- `@requestQueries(schema)` - извлечение всех query-параметров в виде объекта;
|
228
|
+
|
229
|
+
### Тело запроса (`@requestBody`, `@requestField`)
|
230
|
+
|
231
|
+
Для работы с данными, отправленными в теле POST, PUT, PATCH запросов.
|
147
232
|
|
148
233
|
```ts
|
149
|
-
import {
|
150
|
-
|
151
|
-
|
152
|
-
|
153
|
-
|
234
|
+
import {
|
235
|
+
postAction,
|
236
|
+
requestBody,
|
237
|
+
requestField,
|
238
|
+
DataType,
|
239
|
+
} from '@e22m4u/ts-rest-router';
|
154
240
|
|
155
241
|
@restController('users')
|
156
242
|
class UserController {
|
157
|
-
|
158
|
-
|
159
|
-
|
160
|
-
|
243
|
+
// Пример с @requestBody: получение и валидация всего тела запроса
|
244
|
+
@postAction()
|
245
|
+
createUser(
|
246
|
+
@requestBody({
|
247
|
+
type: DataType.OBJECT,
|
161
248
|
properties: {
|
162
|
-
|
163
|
-
type: DataType.STRING,
|
164
|
-
required: true,
|
165
|
-
|
249
|
+
username: {
|
250
|
+
type: DataType.STRING,
|
251
|
+
required: true,
|
252
|
+
},
|
253
|
+
email: {
|
254
|
+
type: DataType.STRING,
|
255
|
+
required: true,
|
166
256
|
},
|
167
|
-
age: { // схема свойства "age"
|
168
|
-
type: DataType.NUMBER, // свойство должно являться числом
|
169
|
-
}
|
170
257
|
},
|
171
258
|
})
|
172
|
-
body: {
|
259
|
+
body: {username: string; email: string},
|
173
260
|
) {
|
174
|
-
return {
|
175
|
-
|
176
|
-
|
177
|
-
|
178
|
-
|
261
|
+
return {id: 1, ...body};
|
262
|
+
}
|
263
|
+
|
264
|
+
// Пример с @requestField: получение только одного поля из тела
|
265
|
+
@postAction('login')
|
266
|
+
login(
|
267
|
+
@requestField('username', DataType.STRING) username: string,
|
268
|
+
@requestField('password', DataType.STRING) password: string,
|
269
|
+
) {
|
270
|
+
// ... логика аутентификации
|
271
|
+
return {token: 'jwt-token'};
|
179
272
|
}
|
180
273
|
}
|
181
274
|
```
|
182
275
|
|
183
|
-
|
276
|
+
**Декораторы:**
|
184
277
|
|
185
|
-
|
278
|
+
- `@requestBody(schema)` - извлечение тела запроса;
|
279
|
+
- `@requestField(name, schema)` - извлечение отдельного поля из тела запроса;
|
186
280
|
|
187
|
-
|
188
|
-
- `@restAction` - базовый декоратор для методов;
|
189
|
-
- `@getAction` - метод GET;
|
190
|
-
- `@postAction` - метод POST;
|
191
|
-
- `@putAction` - метод PUT;
|
192
|
-
- `@patchAction` - метод PATCH;
|
193
|
-
- `@deleteAction` - метод DELETE;
|
281
|
+
### Заголовки и Cookies
|
194
282
|
|
195
|
-
|
283
|
+
Работа с заголовками и cookies осуществляется аналогичным образом:
|
196
284
|
|
197
|
-
- `@
|
198
|
-
- `@
|
285
|
+
- `@requestHeader(name, schema)` / `@requestHeaders(schema)`
|
286
|
+
- `@requestCookie(name, schema)` / `@requestCookies(schema)`
|
199
287
|
|
200
|
-
|
288
|
+
### Контекст запроса (`@requestContext`)
|
201
289
|
|
202
|
-
|
203
|
-
|
204
|
-
- `@requestQuery` - определенный query параметр;
|
205
|
-
- `@requestQueries` - все query параметры как объект;
|
206
|
-
- `@requestBody` - тело запроса;
|
207
|
-
- `@requestField` - поле в теле запроса;
|
208
|
-
- `@requestHeader` - определенный заголовок запроса;
|
209
|
-
- `@requestHeaders` - все заголовки запроса как объект;
|
210
|
-
- `@requestCookie` - определенный cookie запроса;
|
211
|
-
- `@requestCookies` - все cookies запроса как объект;
|
212
|
-
- `@requestContext` - доступ к контексту запроса;
|
213
|
-
- `@requestContainer` - сервис-контейнер запроса;
|
214
|
-
- `@requestData` - доступ к данным запроса;
|
215
|
-
- `@httpRequest` - экземпляр `IncomingMessage`;
|
216
|
-
- `@httpResponse` - экземпляр `ServerResponse`;
|
290
|
+
Для доступа к "сырым" объектам запроса/ответа Node.js или другим частям
|
291
|
+
контекста используются следующие декораторы:
|
217
292
|
|
218
|
-
|
219
|
-
|
220
|
-
|
293
|
+
- `@requestContext()` - инъекция всего объекта `RequestContext`;
|
294
|
+
- `@requestContext('req')` - инъекция нативного `IncomingMessage`;
|
295
|
+
- `@requestContext('res')` - инъекция нативного `ServerResponse`;
|
296
|
+
- `@requestContext('container')` - инъекция DI-контейнера запроса;
|
297
|
+
- **Алиасы:** `@httpRequest()`, `@httpResponse()`, `@requestContainer()`;
|
221
298
|
|
222
299
|
```ts
|
223
|
-
@
|
224
|
-
|
225
|
-
// ...
|
226
|
-
}
|
227
|
-
```
|
228
|
-
|
229
|
-
Определение базового пути.
|
300
|
+
import {getAction, requestContext} from '@e22m4u/ts-rest-router';
|
301
|
+
import {RequestContext} from '@e22m4u/js-trie-router';
|
230
302
|
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
303
|
+
@restController('system')
|
304
|
+
class SystemController {
|
305
|
+
@getAction('ip')
|
306
|
+
getClientIp(
|
307
|
+
@requestContext()
|
308
|
+
ctx: RequestContext,
|
309
|
+
) {
|
310
|
+
// ctx.req - это нативный объект IncomingMessage
|
311
|
+
const ip = ctx.req.socket.remoteAddress;
|
312
|
+
return {ip};
|
313
|
+
}
|
235
314
|
}
|
236
315
|
```
|
237
316
|
|
238
|
-
|
317
|
+
## Валидация и схемы данных
|
239
318
|
|
240
|
-
|
241
|
-
@
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
class UserController {
|
247
|
-
// ...
|
248
|
-
}
|
249
|
-
```
|
319
|
+
Модуль интегрирован с
|
320
|
+
[@e22m4u/ts-data-schema](https://www.npmjs.com/package/@e22m4u/ts-data-schema)
|
321
|
+
для гибкой проверки данных. Это дает возможность определять типы данных
|
322
|
+
и сложные правила.
|
323
|
+
|
324
|
+
**Базовые типы `DataType`:**
|
250
325
|
|
251
|
-
|
326
|
+
- `DataType.ANY`
|
327
|
+
- `DataType.STRING`
|
328
|
+
- `DataType.NUMBER`
|
329
|
+
- `DataType.BOOLEAN`
|
330
|
+
- `DataType.ARRAY`
|
331
|
+
- `DataType.OBJECT`
|
252
332
|
|
253
|
-
|
333
|
+
Для более сложных проверок используется объект `DataSchema`:
|
254
334
|
|
255
335
|
```ts
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
263
|
-
};
|
264
|
-
}
|
336
|
+
type DataSchema = {
|
337
|
+
type: DataType; // Обязательное поле
|
338
|
+
required?: boolean; // Значение не может быть null или undefined
|
339
|
+
default?: unknown; // Значение по умолчанию, если `required: false`
|
340
|
+
items?: DataSchema; // Схема для элементов массива (для type: DataType.ARRAY)
|
341
|
+
properties?: {[key: string]: DataSchema}; // Схема для свойств объекта
|
342
|
+
validate?: (value: any) => boolean | string; // Пользовательская функция
|
265
343
|
}
|
266
344
|
```
|
267
345
|
|
268
|
-
|
346
|
+
**Пример сложной валидации:**
|
269
347
|
|
270
348
|
```ts
|
271
|
-
@
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
349
|
+
import {postAction, requestBody, DataType} from '@e22m4u/ts-rest-router';
|
350
|
+
|
351
|
+
@restController('orders')
|
352
|
+
class OrderController {
|
353
|
+
@postAction()
|
354
|
+
createOrder(
|
355
|
+
@requestBody({
|
356
|
+
type: DataType.OBJECT,
|
357
|
+
properties: {
|
358
|
+
userId: {
|
359
|
+
type: DataType.NUMBER,
|
360
|
+
required: true,
|
361
|
+
},
|
362
|
+
products: {
|
363
|
+
type: DataType.ARRAY,
|
364
|
+
required: true,
|
365
|
+
// Описание схемы для каждого элемента массива:
|
366
|
+
items: {
|
367
|
+
type: DataType.OBJECT,
|
368
|
+
properties: {
|
369
|
+
id: {
|
370
|
+
type: DataType.NUMBER,
|
371
|
+
required: true
|
372
|
+
},
|
373
|
+
quantity: {
|
374
|
+
type: DataType.NUMBER,
|
375
|
+
required: true,
|
376
|
+
// Валидатор: количество должно быть больше 0
|
377
|
+
validate: (qty) => qty > 0 || 'Quantity must be positive',
|
378
|
+
},
|
379
|
+
},
|
380
|
+
},
|
381
|
+
},
|
382
|
+
},
|
383
|
+
})
|
384
|
+
orderData: { /* ... */ },
|
385
|
+
) {
|
386
|
+
// ...
|
282
387
|
}
|
283
388
|
}
|
284
389
|
```
|
285
390
|
|
286
|
-
|
391
|
+
## Хуки (`@beforeAction`, `@afterAction`)
|
392
|
+
|
393
|
+
Хуки - это функции, выполняющиеся до (`@beforeAction`) или после
|
394
|
+
(`@afterAction`) основного метода контроллера. Они предназначены для
|
395
|
+
сквозной логики, такой как аутентификация, логирование или модификация
|
396
|
+
ответа.
|
287
397
|
|
288
|
-
|
398
|
+
Применение хуков возможно как ко всему контроллеру, так и к отдельному
|
399
|
+
методу.
|
289
400
|
|
290
401
|
```ts
|
291
402
|
import {RequestContext} from '@e22m4u/js-trie-router';
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
console.log(ctx.res); // ServerResponse
|
302
|
-
console.log(ctx.params); // {id: 10}
|
303
|
-
console.log(ctx.query); // {include: 'city'}
|
304
|
-
console.log(ctx.headers); // {cookie: 'foo=bar; baz=qux;'}
|
305
|
-
console.log(ctx.cookie); // {foo: 'bar', baz: 'qux'}
|
306
|
-
console.log(ctx.method); // "GET"
|
307
|
-
console.log(ctx.path); // "/users/10?include=city"
|
308
|
-
console.log(ctx.pathname); // "/users/10"
|
309
|
-
// ...
|
403
|
+
import createError from 'http-errors';
|
404
|
+
|
405
|
+
// Хук для проверки аутентификации
|
406
|
+
async function authHook(ctx: RequestContext) {
|
407
|
+
const token = ctx.headers.authorization;
|
408
|
+
if (!token /* || !isValidToken(token) */) {
|
409
|
+
// Выброс ошибки прерывает выполнение и отправляет клиенту
|
410
|
+
// соответствующий HTTP-статус.
|
411
|
+
throw createError(401, 'Unauthorized');
|
310
412
|
}
|
311
413
|
}
|
312
|
-
```
|
313
414
|
|
314
|
-
|
415
|
+
// Хук для логирования и модификации ответа
|
416
|
+
async function loggerHook(ctx: RequestContext, data: any) {
|
417
|
+
console.log(`Response for ${ctx.pathname}:`, data);
|
418
|
+
// Хуки @afterAction могут модифицировать ответ
|
419
|
+
return {...data, timestamp: new Date()};
|
420
|
+
}
|
315
421
|
|
316
|
-
|
317
|
-
|
318
|
-
|
319
|
-
|
320
|
-
@
|
321
|
-
|
322
|
-
|
323
|
-
|
324
|
-
|
325
|
-
|
326
|
-
|
327
|
-
|
328
|
-
) {
|
329
|
-
console.log(req); // IncomingMessage
|
330
|
-
console.log(res); // ServerResponse
|
422
|
+
@beforeAction(authHook) // Применение ко всем методам контроллера
|
423
|
+
@restController('profile')
|
424
|
+
class ProfileController {
|
425
|
+
@getAction('me')
|
426
|
+
@afterAction(loggerHook) // Применение только к этому методу
|
427
|
+
getMyProfile() {
|
428
|
+
return {id: 1, name: 'Current User'};
|
429
|
+
}
|
430
|
+
|
431
|
+
@getAction('settings')
|
432
|
+
getMySettings() {
|
433
|
+
return {theme: 'dark'};
|
331
434
|
}
|
332
435
|
}
|
333
436
|
```
|
334
437
|
|
335
|
-
|
336
|
-
|
337
|
-
- `container: ServiceContainer` экземпляр [сервис-контейнера](https://npmjs.com/package/@e22m4u/js-service)
|
338
|
-
- `req: IncomingMessage` нативный поток входящего запроса
|
339
|
-
- `res: ServerResponse` нативный поток ответа сервера
|
340
|
-
- `params: ParsedParams` объект ключ-значение с параметрами пути
|
341
|
-
- `query: ParsedQuery` объект ключ-значение с параметрами строки запроса
|
342
|
-
- `headers: ParsedHeaders` объект ключ-значение с заголовками запроса
|
343
|
-
- `cookie: ParsedCookie` объект ключ-значение разобранного заголовка `cookie`
|
344
|
-
- `method: string` метод запроса в верхнем регистре, например `GET`, `POST` и т.д.
|
345
|
-
- `path: string` путь включающий строку запроса, например `/myPath?foo=bar`
|
346
|
-
- `pathname: string` путь запроса, например `/myMath`
|
347
|
-
- `body: unknown` тело запроса
|
438
|
+
## Архитектура: Жизненный цикл контроллера и DI
|
348
439
|
|
349
|
-
|
440
|
+
Понимание архитектурных принципов `ts-rest-router` является ключом
|
441
|
+
к созданию надежных и масштабируемых приложений. Модуль построен на
|
442
|
+
базе библиотеки `@e22m4u/js-service`, реализующей паттерн
|
443
|
+
Service Locator / Dependency Injection.
|
350
444
|
|
351
|
-
|
352
|
-
контроллеров, основанное на библиотеке
|
353
|
-
[@e22m4u/js-service](https://www.npmjs.com/package/@e22m4u/js-service).
|
445
|
+
#### Принцип №1: Изоляция запросов
|
354
446
|
|
355
|
-
|
447
|
+
Для **каждого** входящего HTTP-запроса создается **новый, изолированный
|
448
|
+
экземпляр контроллера**. Этот фундаментальный принцип гарантирует, что
|
449
|
+
состояние одного запроса (например, данные аутентифицированного
|
450
|
+
пользователя) никогда не будет разделено с другим, одновременно
|
451
|
+
обрабатываемым запросом. Это устраняет целый класс потенциальных
|
452
|
+
ошибок, связанных с состоянием гонки.
|
356
453
|
|
357
|
-
|
358
|
-
экземпляр контроллера**. Это гарантирует, что состояние одного запроса
|
359
|
-
(например, данные пользователя или временные вычисления) никогда не "протечет"
|
360
|
-
в другой, одновременно обрабатываемый запрос. Такой подход устраняет целый
|
361
|
-
класс потенциальных ошибок, связанных с состоянием гонки (race conditions).
|
454
|
+
#### Принцип №2: Request-Scoped Service Container
|
362
455
|
|
363
|
-
|
456
|
+
Каждый экземпляр контроллера создается с помощью своего собственного
|
457
|
+
DI-контейнера, который существует только в рамках одного запроса. Чтобы
|
458
|
+
контроллер мог взаимодействовать с другими сервисами (например, с сервисом
|
459
|
+
для работы с базой данных), его класс должен наследоваться от базового
|
460
|
+
класса `Service`. Это дает доступ к методу `this.getService()` для
|
461
|
+
получения зависимостей.
|
364
462
|
|
365
|
-
|
366
|
-
"живет" только в рамках одного запроса. Чтобы контроллер мог взаимодействовать
|
367
|
-
с другими сервисами, он должен наследоваться от базового класса `Service`.
|
368
|
-
Это дает ему доступ к методу `this.getService()` для получения зависимостей.
|
463
|
+
**Практический пример с сервисом аутентификации:**
|
369
464
|
|
370
|
-
|
371
|
-
пользователя в Middleware и сделать информацию о нем доступной в контроллере.
|
465
|
+
**Шаг 1: Создание хука для подготовки сервиса**
|
372
466
|
|
373
|
-
|
374
|
-
|
375
|
-
|
376
|
-
В данном примере мы вручную создаем экземпляр `AuthService`, выполняем
|
377
|
-
аутентификацию, а затем регистрируем этот экземпляр в контейнере запроса
|
378
|
-
с помощью метода `.set()`.
|
467
|
+
Хуки - идеальное место для подготовки сервисов, специфичных для запроса.
|
468
|
+
В этом примере хук `@beforeAction` создает экземпляр `AuthService`,
|
469
|
+
выполняет аутентификацию и регистрирует его в контейнере запроса.
|
379
470
|
|
380
471
|
```ts
|
381
|
-
// src/auth.
|
382
|
-
import {AuthService} from './auth.service
|
472
|
+
// src/auth.hook.ts
|
473
|
+
import {AuthService} from './auth.service';
|
383
474
|
import {RequestContext} from '@e22m4u/js-trie-router';
|
384
475
|
|
385
|
-
export async function
|
386
|
-
// Получение контейнера текущего запроса
|
476
|
+
export async function authHook(context: RequestContext) {
|
387
477
|
const requestContainer = context.container;
|
388
|
-
// Создание
|
389
|
-
const authService = new AuthService();
|
478
|
+
// Создание сервиса с передачей ему контейнера запроса
|
479
|
+
const authService = new AuthService(requestContainer);
|
390
480
|
// Регистрация созданного экземпляра в контейнере.
|
391
481
|
// Теперь любой другой сервис в рамках этого запроса
|
392
482
|
// сможет получить этот конкретный экземпляр.
|
393
483
|
requestContainer.set(AuthService, authService);
|
394
|
-
// Выполнение логики аутентификации
|
484
|
+
// Выполнение логики аутентификации
|
395
485
|
await authService.authenticate(context.headers.authorization);
|
396
486
|
}
|
397
487
|
```
|
398
488
|
|
399
|
-
|
489
|
+
**Шаг 2: Создание `AuthService`**
|
400
490
|
|
401
|
-
|
402
|
-
(`
|
491
|
+
`AuthService` наследуется от `Service`, что позволяет ему запрашивать другие
|
492
|
+
зависимости (например, `this.getService(DatabaseService)`).
|
403
493
|
|
404
494
|
```ts
|
405
495
|
// src/auth.service.ts
|
406
|
-
|
407
|
-
|
408
|
-
|
496
|
+
import {Service} from '@e22m4u/js-service';
|
497
|
+
|
498
|
+
export class AuthService extends Service {
|
499
|
+
public currentUser?: {id: number; name: string;};
|
500
|
+
|
409
501
|
async authenticate(token?: string) {
|
410
|
-
//
|
502
|
+
// Логика проверки токена и поиска пользователя в БД...
|
411
503
|
if (token === 'valid-token') {
|
412
504
|
this.currentUser = { id: 1, name: 'John Doe' };
|
413
505
|
}
|
@@ -415,35 +507,31 @@ export class AuthService {
|
|
415
507
|
}
|
416
508
|
```
|
417
509
|
|
418
|
-
|
419
|
-
использовать `this.getService()` внутри), то его конструктор нужно было бы
|
420
|
-
вызывать с передачей контейнера: `new AuthService(requestContainer)`.*
|
421
|
-
|
422
|
-
**3. Использование сервиса в контроллере**
|
510
|
+
**Шаг 3: Использование сервиса в контроллере**
|
423
511
|
|
424
|
-
Контроллер, унаследованный от `Service`, теперь может получить
|
425
|
-
к предварительно настроенному экземпляру `AuthService`
|
426
|
-
|
427
|
-
вызов `this.getService(AuthService)` вернет именно тот экземпляр, который
|
428
|
-
был создан и настроен на предыдущем шаге.
|
512
|
+
Контроллер, унаследованный от `Service`, теперь может получить
|
513
|
+
доступ к предварительно настроенному экземпляру `AuthService`
|
514
|
+
через `this.getService()`.
|
429
515
|
|
430
516
|
```ts
|
431
517
|
// src/profile.controller.ts
|
432
|
-
import {
|
518
|
+
import {authHook} from './auth.hook';
|
519
|
+
import createError from 'http-errors';
|
433
520
|
import {Service} from '@e22m4u/js-service';
|
434
|
-
import {AuthService} from './auth.service
|
435
|
-
import {
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
521
|
+
import {AuthService} from './auth.service';
|
522
|
+
import {
|
523
|
+
getAction,
|
524
|
+
restController,
|
525
|
+
beforeAction,
|
526
|
+
} from '@e22m4u/ts-rest-router';
|
527
|
+
|
528
|
+
@beforeAction(authHook)
|
440
529
|
@restController('profile')
|
441
|
-
@beforeAction(authMiddleware) // применение middleware ко всем методам контроллера
|
442
530
|
export class ProfileController extends Service {
|
443
531
|
@getAction('me')
|
444
532
|
getProfile() {
|
445
533
|
// Получение request-scoped экземпляра AuthService,
|
446
|
-
// который был создан и зарегистрирован в
|
534
|
+
// который был создан и зарегистрирован в хуке.
|
447
535
|
const authService = this.getService(AuthService);
|
448
536
|
|
449
537
|
if (!authService.currentUser)
|
@@ -454,24 +542,120 @@ export class ProfileController extends Service {
|
|
454
542
|
}
|
455
543
|
```
|
456
544
|
|
457
|
-
Таким образом,
|
545
|
+
Таким образом, DI-контейнер запроса выступает в роли моста между хуками
|
458
546
|
и контроллером, позволяя безопасно передавать состояние, изолированное
|
459
547
|
в рамках одного HTTP-запроса.
|
460
548
|
|
549
|
+
## Производительность: Префиксное дерево
|
550
|
+
|
551
|
+
В основе данного модуля лежит
|
552
|
+
[@e22m4u/js-trie-router](https://www.npmjs.com/package/@e22m4u/js-trie-router),
|
553
|
+
который использует структуру данных
|
554
|
+
**префиксного дерева** ([Trie](https://en.wikipedia.org/wiki/Trie))
|
555
|
+
для хранения маршрутов и их сопоставления. Такое архитектурное решение
|
556
|
+
обеспечивает высокую производительность, особенно в приложениях
|
557
|
+
с большим количеством маршрутов.
|
558
|
+
|
559
|
+
#### Как это работает?
|
560
|
+
|
561
|
+
Вместо хранения маршрутов в виде плоского списка, префиксное дерево
|
562
|
+
организует их в древовидную структуру. Каждый маршрут, например
|
563
|
+
`GET /users/:id/posts`, разбивается на сегменты (`GET`, `users`, `:id`,
|
564
|
+
`posts`), где каждый сегмент становится узлом в дереве.
|
565
|
+
|
566
|
+
Когда поступает новый запрос, например `GET /users/123/posts`,
|
567
|
+
маршрутизатор не перебирает все существующие маршруты. Вместо этого
|
568
|
+
он последовательно спускается по дереву:
|
569
|
+
|
570
|
+
1. Находит корневой узел для метода `GET`.
|
571
|
+
2. От него переходит к дочернему узлу `users`.
|
572
|
+
3. Далее, не найдя статического узла `123`, он ищет динамический
|
573
|
+
узел (`:id`) и сопоставляет его, сохраняя `123` как значение
|
574
|
+
параметра `id`.
|
575
|
+
4. Наконец, он переходит к узлу `posts` и находит совпадение.
|
576
|
+
|
577
|
+
#### Преимущества для производительности
|
578
|
+
|
579
|
+
1. **Эффективный поиск (O(k))**
|
580
|
+
Самое главное преимущество — скорость поиска. Вместо того чтобы
|
581
|
+
перебирать массив из `N` маршрутов и проверять каждый с помощью
|
582
|
+
регулярного выражения (сложность `O(N)`), префиксное дерево
|
583
|
+
находит совпадение за время, пропорциональное количеству сегментов
|
584
|
+
`k` в URL-пути (сложность `O(k)`). Это означает, что производительность
|
585
|
+
поиска **не падает** с ростом общего числа маршрутов в приложении.
|
586
|
+
|
587
|
+
2. **Быстрая обработка 404 (ранний выход)**
|
588
|
+
Если приходит запрос на несуществующий путь, например
|
589
|
+
`/users/123/comments`, поиск по дереву прекратится сразу после
|
590
|
+
того, как маршрутизатор не найдет дочерний узел `comments`
|
591
|
+
у узла `:id`. Ему не нужно проверять остальные сотни маршрутов,
|
592
|
+
чтобы убедиться, что совпадений нет. Это делает обработку
|
593
|
+
ненайденных маршрутов (404) почти мгновенной.
|
594
|
+
|
595
|
+
3. **Оптимизация для статических и динамических маршрутов**
|
596
|
+
При поиске маршрутизатор всегда отдает приоритет статическим
|
597
|
+
сегментам перед динамическими. Маршрут `/users/me` всегда будет
|
598
|
+
найден раньше и быстрее, чем `/users/:id` при запросе `/users/me`,
|
599
|
+
поскольку не требуется проверка на соответствие шаблону.
|
600
|
+
|
601
|
+
Этот подход делает `ts-rest-router` производительным решением для крупных
|
602
|
+
приложений с большим количеством маршрутов.
|
603
|
+
|
604
|
+
## Полный список декораторов
|
605
|
+
|
606
|
+
#### Контроллер и методы:
|
607
|
+
|
608
|
+
- `@restController(options)` - определение класса как контроллера;
|
609
|
+
- `@getAction(path, options)` - метод GET;
|
610
|
+
- `@postAction(path, options)` - метод POST;
|
611
|
+
- `@putAction(path, options)` - метод PUT;
|
612
|
+
- `@patchAction(path, options)` - метод PATCH;
|
613
|
+
- `@deleteAction(path, options)` - метод DELETE;
|
614
|
+
- `@restAction(options)` - базовый декоратор для определения метода;
|
615
|
+
|
616
|
+
#### Хуки:
|
617
|
+
|
618
|
+
- `@beforeAction(hook)` - выполнение перед методом;
|
619
|
+
- `@afterAction(hook)` - выполнение после метода;
|
620
|
+
|
621
|
+
#### Параметры запроса:
|
622
|
+
|
623
|
+
- `@requestParam(name, schema)` - URL-параметр;
|
624
|
+
- `@requestParams(schema)` - все URL-параметры;
|
625
|
+
- `@requestQuery(name, schema)` - query-параметр;
|
626
|
+
- `@requestQueries(schema)` - все query-параметры;
|
627
|
+
- `@requestBody(schema)` - тело запроса;
|
628
|
+
- `@requestField(name, schema)` - поле из тела запроса;
|
629
|
+
- `@requestHeader(name, schema)` - заголовок запроса;
|
630
|
+
- `@requestHeaders(schema)` - все заголовки;
|
631
|
+
- `@requestCookie(name, schema)` - cookie;
|
632
|
+
- `@requestCookies(schema)` - все cookies;
|
633
|
+
- `@requestData(options)` - базовый декоратор для доступа к данным;
|
634
|
+
|
635
|
+
#### Контекст:
|
636
|
+
|
637
|
+
- `@requestContext(property)` - доступ к `RequestContext` или его свойствам;
|
638
|
+
- `@requestContainer()` - DI-контейнер запроса;
|
639
|
+
- `@httpRequest()` - экземпляр `IncomingMessage`;
|
640
|
+
- `@httpResponse()` - экземпляр `ServerResponse`;
|
641
|
+
|
461
642
|
## Отладка
|
462
643
|
|
463
|
-
|
644
|
+
Включение вывода отладочных логов в консоль осуществляется
|
645
|
+
установкой переменной окружения `DEBUG`:
|
464
646
|
|
465
647
|
```bash
|
466
|
-
DEBUG=tsRestRouter* npm
|
648
|
+
DEBUG=tsRestRouter* npm start
|
467
649
|
```
|
468
650
|
|
469
651
|
## Тесты
|
470
652
|
|
653
|
+
Запуск тестов выполняется командой:
|
654
|
+
|
471
655
|
```bash
|
472
656
|
npm run test
|
473
657
|
```
|
474
658
|
|
475
659
|
## Лицензия
|
476
660
|
|
477
|
-
MIT
|
661
|
+
MIT
|