@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.
Files changed (24) hide show
  1. package/README.md +489 -305
  2. package/dist/cjs/index.cjs +14 -14
  3. package/dist/esm/controller-registry.js +8 -8
  4. package/dist/esm/decorators/after-action/after-action-decorator.d.ts +2 -2
  5. package/dist/esm/decorators/after-action/after-action-decorator.js +4 -4
  6. package/dist/esm/decorators/after-action/after-action-decorator.spec.js +22 -25
  7. package/dist/esm/decorators/after-action/after-action-metadata.d.ts +1 -1
  8. package/dist/esm/decorators/after-action/after-action-reflector.spec.js +15 -15
  9. package/dist/esm/decorators/before-action/before-action-decorator.d.ts +2 -2
  10. package/dist/esm/decorators/before-action/before-action-decorator.js +4 -4
  11. package/dist/esm/decorators/before-action/before-action-decorator.spec.js +22 -25
  12. package/dist/esm/decorators/before-action/before-action-metadata.d.ts +1 -1
  13. package/dist/esm/decorators/before-action/before-action-reflector.spec.js +15 -15
  14. package/package.json +9 -9
  15. package/src/controller-registry.spec.ts +21 -21
  16. package/src/controller-registry.ts +8 -8
  17. package/src/decorators/after-action/after-action-decorator.spec.ts +22 -25
  18. package/src/decorators/after-action/after-action-decorator.ts +4 -4
  19. package/src/decorators/after-action/after-action-metadata.ts +1 -1
  20. package/src/decorators/after-action/after-action-reflector.spec.ts +15 -15
  21. package/src/decorators/before-action/before-action-decorator.spec.ts +22 -25
  22. package/src/decorators/before-action/before-action-decorator.ts +4 -4
  23. package/src/decorators/before-action/before-action-metadata.ts +1 -1
  24. 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 маршрутизатор на основе контроллеров для TypeScript.
4
-
5
- #### Основные возможности
6
-
7
- - Декларативное определение маршрутов через декораторы.
8
- - Типизированные параметры запросов (body, query, params).
9
- - Поддержка middleware до и после обработки запроса.
10
- - Валидация входящих данных.
11
- - Поддержка всех HTTP методов (GET, POST, PUT, PATCH и DELETE).
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
- ниже опции в файл `tsconfig.json` вашего проекта.
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 {DataType} from '@e22m4u/ts-rest-router';
48
- import {getAction} from '@e22m4u/ts-rest-router';
49
- import {postAction} from '@e22m4u/ts-rest-router';
50
- import {requestField} from '@e22m4u/ts-rest-router';
51
- import {restController} from '@e22m4u/ts-rest-router';
52
-
53
- // объявление контроллера
54
- // и базового пути /users
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
- // объявление метода POST /users/login
58
- // (использует базовый путь контроллера)
59
- @postAction('login')
60
- async login(
61
- // инъекция значений указанных полей
62
- // извлекаемых из тела запроса
63
- @requestField('username') username?: string,
64
- @requestField('password') password?: string,
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
- // "Content-Type: application/json"
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
- const router = new RestRouter();
86
- router.registerController(UserController);
87
- router.registerController(ProductController);
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
- const server = new http.Server();
91
- server.on('request', router.requestListener);
156
+ bootstrap();
157
+ ```
158
+
159
+ ## Обработка данных запроса
160
+
161
+ Модуль предоставляет удобные и типобезопасные механизмы для доступа
162
+ к данным входящего запроса.
92
163
 
93
- // запуск сервера
94
- server.listen('8080', '0.0.0.0', () => {
95
- console.log(`Server is running on http://localhost:8080`);
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
- - `DataType.ANY` - принимает любой тип
125
- - `DataType.STRING` - строковые значения
126
- - `DataType.NUMBER` - числовые значения
127
- - `DataType.BOOLEAN` - логические значения
128
- - `DataType.ARRAY` - массивы
129
- - `DataType.OBJECT` - объекты (не экземпляры)
192
+ ### Query-параметры (`@requestQuery`)
130
193
 
131
- Для определения дополнительных условий, используется объект `DataSchema`,
132
- с помощью которого можно определить структуру ожидаемого объекта, допустимые
133
- элементы массива, функции-валидаторы и другие ограничения входящих данных.
194
+ Применяются для извлечения параметров из строки запроса
195
+ (например, `?sort=desc`).
134
196
 
135
197
  ```ts
136
- type DataSchema = {
137
- type: DataType;
138
- items?: DataSchema;
139
- properties?: {[key: string]: DataSchema};
140
- required?: boolean;
141
- validate?: CallableValidator | CallableValidator[];
142
- default?: unknown;
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
- Пример проверки передаваемого объекта методом POST:
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 {DataType} from '@e22m4u/ts-rest-router';
150
- import {getAction} from '@e22m4u/ts-rest-router';
151
- import {postAction} from '@e22m4u/ts-rest-router';
152
- import {requestField} from '@e22m4u/ts-rest-router';
153
- import {restController} from '@e22m4u/ts-rest-router';
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
- @postAction() // POST /users
158
- async create(
159
- @requestBody({ // декоратор тела запроса
160
- type: DataType.OBJECT, // в теле запроса ожидается объект
243
+ // Пример с @requestBody: получение и валидация всего тела запроса
244
+ @postAction()
245
+ createUser(
246
+ @requestBody({
247
+ type: DataType.OBJECT,
161
248
  properties: {
162
- name: { // схема свойства "name"
163
- type: DataType.STRING, // свойство должно содержать строку
164
- required: true, // свойство не может содержать undefined или null
165
- validate: v => v.length > 2, // проверка длины строки
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: {name: string, age?: number},
259
+ body: {username: string; email: string},
173
260
  ) {
174
- return {
175
- id: 1,
176
- name: body.name,
177
- age: body.age,
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
- - `@restController` - определяет класс как контроллер;
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
- - `@beforeAction` - middleware перед обработкой запроса;
198
- - `@afterAction` - middleware после обработки запроса;
285
+ - `@requestHeader(name, schema)` / `@requestHeaders(schema)`
286
+ - `@requestCookie(name, schema)` / `@requestCookies(schema)`
199
287
 
200
- Параметры запроса:
288
+ ### Контекст запроса (`@requestContext`)
201
289
 
202
- - `@requestParam` - определенный URL параметр;
203
- - `@requestParams` - все параметры URL как объект;
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
- #### `@restController(options?: ControllerOptions)`
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
- @restController()
224
- class UserController {
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
- ```ts
232
- @restController('users')
233
- class UserController {
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
- ```ts
241
- @restController({
242
- path: 'api', // базовый путь
243
- before: [authMiddleware], // middleware до обработки запроса
244
- after: [loggerMiddleware], // middleware после обработки запроса
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
- #### `@getAction(path: string, options?: ActionOptions)`
326
+ - `DataType.ANY`
327
+ - `DataType.STRING`
328
+ - `DataType.NUMBER`
329
+ - `DataType.BOOLEAN`
330
+ - `DataType.ARRAY`
331
+ - `DataType.OBJECT`
252
332
 
253
- Определение метода GET.
333
+ Для более сложных проверок используется объект `DataSchema`:
254
334
 
255
335
  ```ts
256
- @restController('users')
257
- class UserController {
258
- @getAction('whoAmI') // маршрут GET /users/whoAmI
259
- async whoAmI() {
260
- return { // если метод возвращает объект,
261
- name: 'John', // то результат будет представлен
262
- surname: 'Doe', // как "Content-Type: application/json"
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
- @restController('users')
272
- class UserController {
273
- @getAction('whoAmI', { // маршрут GET /users/whoAmI
274
- before: [authMiddleware], // middleware до обработки запроса
275
- after: [loggerMiddleware], // middleware после обработки запроса
276
- })
277
- async whoAmI() {
278
- return {
279
- name: 'John',
280
- surname: 'Doe',
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
- #### `@requestContext(propertyName?: string)`
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
- @restController('users')
294
- class UserController {
295
- @getAction(':id')
296
- findById(
297
- @requestContext() // инъекция контекста запроса
298
- ctx: RequestContext, // в качестве аргумента
299
- ) {
300
- console.log(ctx.req); // IncomingMessage
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
- ```ts
317
- import {ServerResponse} from 'http';
318
- import {IncomingMessage} from 'http';
319
-
320
- @restController('/users') // путь контроллера
321
- class UserController { // класс контроллера
322
- @getAction('/:id') // маршрут GET /users/:id
323
- findById(
324
- @requestContext('req') // декоратор контекста запроса
325
- req: IncomingMessage, // включающий свойство "req"
326
- @requestContext('res') // декоратор контекста запроса
327
- res: ServerResponse, // включающий свойство "res"
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
- Для каждого входящего HTTP-запроса фреймворк создает **новый, изолированный
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
- **1. Создание Middleware для подготовки сервиса**
374
-
375
- Middleware - идеальное место для подготовки сервисов, специфичных для запроса.
376
- В данном примере мы вручную создаем экземпляр `AuthService`, выполняем
377
- аутентификацию, а затем регистрируем этот экземпляр в контейнере запроса
378
- с помощью метода `.set()`.
467
+ Хуки - идеальное место для подготовки сервисов, специфичных для запроса.
468
+ В этом примере хук `@beforeAction` создает экземпляр `AuthService`,
469
+ выполняет аутентификацию и регистрирует его в контейнере запроса.
379
470
 
380
471
  ```ts
381
- // src/auth.middleware.ts
382
- import {AuthService} from './auth.service.ts';
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 authMiddleware(context: RequestContext) {
386
- // Получение контейнера текущего запроса
476
+ export async function authHook(context: RequestContext) {
387
477
  const requestContainer = context.container;
388
- // Создание нового экземпляра AuthService
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
- **2. Создание `AuthService`**
489
+ **Шаг 2: Создание `AuthService`**
400
490
 
401
- В этом примере `AuthService` - это простой класс. Он хранит состояние
402
- (`currentUser`), которое будет установлено в middleware.
491
+ `AuthService` наследуется от `Service`, что позволяет ему запрашивать другие
492
+ зависимости (например, `this.getService(DatabaseService)`).
403
493
 
404
494
  ```ts
405
495
  // src/auth.service.ts
406
- export class AuthService {
407
- public currentUser?: { id: number; name: string; };
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
- *Примечание: если бы `AuthService` сам наследовал `Service` (чтобы, например,
419
- использовать `this.getService()` внутри), то его конструктор нужно было бы
420
- вызывать с передачей контейнера: `new AuthService(requestContainer)`.*
421
-
422
- **3. Использование сервиса в контроллере**
510
+ **Шаг 3: Использование сервиса в контроллере**
423
511
 
424
- Контроллер, унаследованный от `Service`, теперь может получить доступ
425
- к предварительно настроенному экземпляру `AuthService` через `this.getService()`.
426
- Поскольку middleware уже выполнил `.set(AuthService, authService)`,
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 {createError} from 'http-errors';
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.ts';
435
- import {getAction} from '@e22m4u/ts-rest-router';
436
- import {beforeAction} from '@e22m4u/ts-rest-router';
437
- import {authMiddleware} from './auth.middleware.ts';
438
- import {restController} from '@e22m4u/ts-rest-router';
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
- // который был создан и зарегистрирован в middleware.
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
- Таким образом, контейнер запроса выступает в роли моста между middleware
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
- Установка переменной `DEBUG` включает вывод логов.
644
+ Включение вывода отладочных логов в консоль осуществляется
645
+ установкой переменной окружения `DEBUG`:
464
646
 
465
647
  ```bash
466
- DEBUG=tsRestRouter* npm run test
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