@e22m4u/ts-rest-router 0.6.7 → 0.6.8

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 CHANGED
@@ -1,53 +1,31 @@
1
1
  # @e22m4u/ts-rest-router
2
2
 
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/js-data-schema](https://www.npmjs.com/package/@e22m4u/js-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
- для внедрения зависимостей.
3
+ ![npm version](https://badge.fury.io/js/@e22m4u%2Fts-rest-router.svg)
4
+ ![license](https://img.shields.io/badge/license-mit-blue.svg)
5
+
6
+ REST-маршрутизатор на основе контроллеров и TypeScript декораторов.
7
+
8
+ Данный модуль позволяет создавать структурированное и масштабируемое REST API.
9
+ В его основе лежит декларативный подход с использованием TypeScript декораторов
10
+ для определения маршрутов, обработки входящих данных и управления жизненным
11
+ циклом запроса.
35
12
 
36
13
  ## Содержание
37
14
 
38
15
  - [Установка](#установка)
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
- - [Полный список декораторов](#полный-список-декораторов)
16
+ - [Базовый пример](#базовый-пример)
17
+ - [Данные запроса](#данные-запроса)
18
+ - [URL-параметры](#url-параметры)
19
+ - [Query-параметры](#query-параметры)
20
+ - [Тело запроса](#тело-запроса)
21
+ - [Заголовки](#заголовки)
22
+ - [Cookies](#cookies)
23
+ - [Контекст запроса](#контекст-запроса)
24
+ - [Валидация данных](#валидация-данных)
25
+ - [Хуки](#хуки)
26
+ - [Архитектура](#архитектура)
27
+ - [Производительность](#производительность)
28
+ - [Декораторы](#декораторы)
51
29
  - [Отладка](#отладка)
52
30
  - [Тесты](#тесты)
53
31
  - [Лицензия](#лицензия)
@@ -70,7 +48,7 @@ npm install @e22m4u/ts-rest-router
70
48
  }
71
49
  ```
72
50
 
73
- ## Быстрый старт: Пример сервера
51
+ ## Базовый пример
74
52
 
75
53
  Пример создания простого сервера для управления списком пользователей.
76
54
 
@@ -78,34 +56,34 @@ npm install @e22m4u/ts-rest-router
78
56
 
79
57
  ```ts
80
58
  import {
81
- restController,
59
+ DataType,
82
60
  getAction,
83
61
  postAction,
84
62
  requestBody,
85
- DataType,
63
+ restController,
86
64
  } from '@e22m4u/ts-rest-router';
87
65
 
88
- // Временное хранилище данных
66
+ // временное хранилище данных
89
67
  const users = [{id: 1, name: 'John Doe'}];
90
68
 
91
- // 1. Декоратор @restController определяет класс как контроллер
92
- // и устанавливает базовый путь для всех его маршрутов.
69
+ // декоратор @restController определяет класс как контроллер
70
+ // и устанавливает базовый путь для всех его маршрутов
93
71
  @restController('users')
94
72
  export class UserController {
95
- // 2. Декоратор @getAction создает маршрут для GET-запросов.
96
- // Полный путь: GET /users
73
+ // декоратор @getAction создает маршрут для GET-запросов,
74
+ // полный путь: GET /users
97
75
  @getAction()
98
76
  getAllUsers() {
99
- // Результат автоматически сериализуется в JSON
77
+ // результат автоматически сериализуется в JSON
100
78
  return users;
101
79
  }
102
80
 
103
- // 3. Декоратор @postAction создает маршрут для POST-запросов.
104
- // Полный путь: POST /users
81
+ // декоратор @postAction создает маршрут для POST-запросов,
82
+ // полный путь: POST /users
105
83
  @postAction()
106
84
  createUser(
107
- // 4. Декоратор @requestBody извлекает тело запроса,
108
- // проверяет его по схеме и передает в аргумент `newUser`.
85
+ // декоратор @requestBody извлекает тело запроса,
86
+ // проверяет его по схеме и передает в аргумент `newUser`
109
87
  @requestBody({
110
88
  type: DataType.OBJECT,
111
89
  properties: {
@@ -119,7 +97,7 @@ export class UserController {
119
97
  ) {
120
98
  const user = {id: users.length + 1, ...newUser};
121
99
  users.push(user);
122
- // Возвращаемый объект будет отправлен клиенту как JSON.
100
+ // возвращаемый объект будет отправлен клиенту как JSON
123
101
  return user;
124
102
  }
125
103
  }
@@ -133,15 +111,14 @@ import {UserController} from './user.controller';
133
111
  import {RestRouter} from '@e22m4u/ts-rest-router';
134
112
 
135
113
  async function bootstrap() {
136
- // Создание экземпляра роутера
114
+ // создание экземпляра роутера
137
115
  const router = new RestRouter();
138
- // Регистрация контроллера
116
+ // регистрация контроллера
139
117
  router.addController(UserController);
140
-
141
- // Создание HTTP-сервера с обработчиком запросов из роутера
118
+ // создание HTTP-сервера с обработчиком запросов из роутера
142
119
  const server = http.createServer(router.requestListener);
143
120
 
144
- // Запуск сервера
121
+ // запуск сервера
145
122
  server.listen(3000, () => {
146
123
  console.log('Server is running on http://localhost:3000');
147
124
  console.log('Try GET http://localhost:3000/users');
@@ -154,50 +131,74 @@ async function bootstrap() {
154
131
  bootstrap();
155
132
  ```
156
133
 
157
- ## Обработка данных запроса
134
+ ## Данные запроса
135
+
136
+ Модуль предоставляет TypeScript декораторы для инъекции данных входящего
137
+ запроса через аргументы метода контроллера.
138
+
139
+ ### URL-параметры
140
+
141
+ Извлечение динамических частей URL (например, `:id`).
142
+
143
+ Декораторы:
158
144
 
159
- Модуль предоставляет удобные и типобезопасные механизмы для доступа
160
- к данным входящего запроса.
145
+ - `@requestParam(name, schema)`
146
+ \- извлечение одного параметра;
161
147
 
162
- ### URL-параметры (`@requestParam`)
148
+ - `@requestParams(schema)`
149
+ \- извлечение всех URL-параметров в виде объекта;
163
150
 
164
- Используются для извлечения динамических частей URL (например, `:id`).
151
+ Пример:
165
152
 
166
153
  ```ts
167
- import {getAction, requestParam, DataType} from '@e22m4u/ts-rest-router';
154
+ import {
155
+ DataType,
156
+ getAction,
157
+ requestParam,
158
+ restController,
159
+ } from '@e22m4u/ts-rest-router';
168
160
 
169
161
  @restController('articles')
170
162
  class ArticleController {
171
- // Маршрут: GET /articles/42
163
+ // GET /articles/42
172
164
  @getAction(':id')
173
165
  getArticleById(
174
- // Извлечение параметра 'id' с проверкой на соответствие
175
- // типу "number"
166
+ // извлечение параметра 'id' с проверкой
167
+ // на соответствие типу "number"
176
168
  @requestParam('id', DataType.NUMBER) id: number,
177
169
  ) {
178
- // Если id не является числом, фреймворк вернет ошибку
179
- // 400 Bad Request
170
+ // если id не является числом,
171
+ // то выбрасывается ошибка 400 Bad Request
180
172
  return {articleId: id, content: '...'};
181
173
  }
182
174
  }
183
175
  ```
184
176
 
185
- **Декораторы:**
177
+ ### Query-параметры
178
+
179
+ Извлечение параметров из строки запроса (например, `?sort=desc`).
186
180
 
187
- - `@requestParam(name, schema)` - извлечение одного параметра;
188
- - `@requestParams(schema)` - извлечение всех URL-параметров в виде объекта;
181
+ Декораторы:
189
182
 
190
- ### Query-параметры (`@requestQuery`)
183
+ - `@requestQuery(name, schema)`
184
+ \- извлечение одного query-параметра;
191
185
 
192
- Применяются для извлечения параметров из строки запроса
193
- (например, `?sort=desc`).
186
+ - `@requestQueries(schema)`
187
+ \- извлечение всех query-параметров в виде объекта;
188
+
189
+ Пример:
194
190
 
195
191
  ```ts
196
- import {getAction, requestQuery, DataType} from '@e22m4u/ts-rest-router';
192
+ import {
193
+ DataType,
194
+ getAction,
195
+ requestQuery,
196
+ restController,
197
+ } from '@e22m4u/ts-rest-router';
197
198
 
198
199
  @restController('products')
199
200
  class ProductController {
200
- // Маршрут: GET /products/search?q=phone&limit=10
201
+ // GET /products/search?q=phone&limit=10
201
202
  @getAction('search')
202
203
  searchProducts(
203
204
  @requestQuery('q', {
@@ -211,19 +212,14 @@ class ProductController {
211
212
  })
212
213
  limit: number,
213
214
  ) {
214
- // searchTerm будет 'phone', limit будет 10.
215
- // При отсутствии 'q' будет ошибка. При отсутствии 'limit'
216
- // будет использовано значение по умолчанию 20.
215
+ // searchTerm будет 'phone', limit будет 10;
216
+ // при отсутствии 'q' будет ошибка;
217
+ // при отсутствии 'limit' будет использовано значение по умолчанию 20;
217
218
  return {results: [], query: {searchTerm, limit}};
218
219
  }
219
220
  }
220
221
  ```
221
222
 
222
- **Декораторы:**
223
-
224
- - `@requestQuery(name, schema)` - извлечение одного query-параметра;
225
- - `@requestQueries(schema)` - извлечение всех query-параметров в виде объекта;
226
-
227
223
  **Именование Query-декораторов**
228
224
 
229
225
  Разделение на декораторы в единственном `@requestQuery` и множественном
@@ -233,21 +229,32 @@ class ProductController {
233
229
  извлечения и валидации отдельных значений, в то время как множественное число
234
230
  служит для получения всех данных в виде единого объекта.
235
231
 
236
- ### Тело запроса (`@requestBody`, `@requestField`)
232
+ ### Тело запроса
233
+
234
+ Извлечение тела запроса POST, PUT, PATCH методов.
235
+
236
+ Декораторы:
237
+
238
+ - `@requestBody(schema)`
239
+ \- извлечение тела запроса;
237
240
 
238
- Для работы с данными, отправленными в теле POST, PUT, PATCH запросов.
241
+ - `@requestField(name, schema)`
242
+ \- извлечение отдельного поля из тела запроса;
243
+
244
+ Пример:
239
245
 
240
246
  ```ts
241
247
  import {
248
+ DataType,
242
249
  postAction,
243
250
  requestBody,
244
251
  requestField,
245
- DataType,
252
+ restController,
246
253
  } from '@e22m4u/ts-rest-router';
247
254
 
248
255
  @restController('users')
249
256
  class UserController {
250
- // Пример с @requestBody: получение и валидация всего тела запроса
257
+ // пример с @requestBody: получение и валидация всего тела запроса
251
258
  @postAction()
252
259
  createUser(
253
260
  @requestBody({
@@ -271,7 +278,7 @@ class UserController {
271
278
  return {id: 1, ...body};
272
279
  }
273
280
 
274
- // Пример с @requestField: получение только одного поля из тела
281
+ // пример с @requestField: получение только одного поля из тела
275
282
  @postAction('login')
276
283
  login(
277
284
  @requestField('username', DataType.STRING) username: string,
@@ -283,19 +290,89 @@ class UserController {
283
290
  }
284
291
  ```
285
292
 
286
- **Декораторы:**
293
+ ### Заголовки
294
+
295
+ Извлечение HTTP-заголовков запроса.
296
+
297
+ Декораторы:
298
+
299
+ - `@requestHeader(name, schema)`
300
+ \- извлечение отдельного заголовка;
301
+
302
+ - `@requestHeaders(schema)`
303
+ \- извлечение всех заголовков;
304
+
305
+ Пример:
306
+
307
+ ```ts
308
+ import {
309
+ DataType,
310
+ getAction,
311
+ requestHeader,
312
+ } from '@e22m4u/ts-rest-router';
313
+
314
+ @restController('content')
315
+ class ContentController {
316
+ // GET /content
317
+ @getAction()
318
+ getContentForLanguage(
319
+ // извлечение заголовка 'Accept-Language'
320
+ // со значением по умолчанию 'en'
321
+ @requestHeader('Accept-Language', {
322
+ type: DataType.STRING,
323
+ default: 'en',
324
+ })
325
+ lang: string,
326
+ ) {
327
+ // lang будет 'en', если заголовок отсутствует,
328
+ // или будет содержать значение заголовка, например 'ru-RU'
329
+ return {content: `Content in ${lang} language`};
330
+ }
331
+ }
332
+ ```
333
+
334
+ ### Cookies
287
335
 
288
- - `@requestBody(schema)` - извлечение тела запроса;
289
- - `@requestField(name, schema)` - извлечение отдельного поля из тела запроса;
336
+ Извлечение разобранного заголовка Cookies входящего запроса.
290
337
 
291
- ### Заголовки и Cookies
338
+ Декораторы:
292
339
 
293
- Работа с заголовками и cookies осуществляется аналогичным образом:
340
+ - `@requestCookie(name, schema)`
341
+ \- извлечение отдельного Cookie по ключу;
294
342
 
295
- - `@requestHeader(name, schema)` / `@requestHeaders(schema)`
296
- - `@requestCookie(name, schema)` / `@requestCookies(schema)`
343
+ - `@requestCookies(schema)`
344
+ \- извлечение всех Cookies;
297
345
 
298
- ### Контекст запроса (`@requestContext`)
346
+ Пример:
347
+
348
+ ```ts
349
+ import {
350
+ DataType,
351
+ getAction,
352
+ requestCookie,
353
+ } from '@e22m4u/ts-rest-router';
354
+
355
+ @restController('session')
356
+ class SessionController {
357
+ // GET /session/info
358
+ @getAction('info')
359
+ getSessionInfo(
360
+ // извлечение cookie с именем 'sessionId'
361
+ // и проверка, что значение является строкой
362
+ @requestCookie('sessionId', {
363
+ type: DataType.STRING,
364
+ required: true,
365
+ })
366
+ sessionId: string,
367
+ ) {
368
+ // если cookie 'sessionId' отсутствует,
369
+ // будет выброшена ошибка 400 Bad Request
370
+ return {sessionId, info: 'User session data...'};
371
+ }
372
+ }
373
+ ```
374
+
375
+ ### Контекст запроса
299
376
 
300
377
  Для доступа к "сырым" объектам запроса/ответа Node.js или другим частям
301
378
  контекста используются следующие декораторы:
@@ -304,11 +381,14 @@ class UserController {
304
381
  - `@requestContext('req')` - инъекция нативного `IncomingMessage`;
305
382
  - `@requestContext('res')` - инъекция нативного `ServerResponse`;
306
383
  - `@requestContext('container')` - инъекция DI-контейнера запроса;
307
- - **Алиасы:** `@httpRequest()`, `@httpResponse()`, `@requestContainer()`;
384
+ - Псевдонимы: `@httpRequest()`, `@httpResponse()`, `@requestContainer()`;
308
385
 
309
386
  ```ts
310
- import {getAction, requestContext} from '@e22m4u/ts-rest-router';
311
- import {RequestContext} from '@e22m4u/js-trie-router';
387
+ import {
388
+ getAction,
389
+ requestContext,
390
+ RequestContext,
391
+ } from '@e22m4u/ts-rest-router';
312
392
 
313
393
  @restController('system')
314
394
  class SystemController {
@@ -324,14 +404,14 @@ class SystemController {
324
404
  }
325
405
  ```
326
406
 
327
- ## Валидация и схемы данных
407
+ ## Валидация данных
328
408
 
329
409
  Модуль интегрирован с
330
410
  [@e22m4u/js-data-schema](https://www.npmjs.com/package/@e22m4u/js-data-schema)
331
411
  для гибкой проверки данных. Это дает возможность определять типы данных
332
412
  и сложные правила.
333
413
 
334
- **Базовые типы `DataType`:**
414
+ Базовые типы:
335
415
 
336
416
  - `DataType.ANY`
337
417
  - `DataType.STRING`
@@ -340,23 +420,28 @@ class SystemController {
340
420
  - `DataType.ARRAY`
341
421
  - `DataType.OBJECT`
342
422
 
343
- Для более сложных проверок используется объект `DataSchema`:
423
+ Для более сложных проверок используется объект схемы:
344
424
 
345
425
  ```ts
346
426
  type DataSchema = {
347
- type: DataType; // Обязательное поле
348
- required?: boolean; // Значение не может быть null или undefined
349
- default?: unknown; // Значение по умолчанию, если `required: false`
350
- items?: DataSchema; // Схема для элементов массива (для type: DataType.ARRAY)
351
- properties?: {[key: string]: DataSchema}; // Схема для свойств объекта
352
- validate?: (value: any) => boolean | string; // Пользовательская функция
427
+ type: DataType; // обязательное поле
428
+ required?: boolean; // значение не может быть null или undefined
429
+ default?: unknown; // значение по умолчанию, если `required: false`
430
+ items?: DataSchema; // схема для элементов массива (для type: DataType.ARRAY)
431
+ properties?: {[key: string]: DataSchema}; // схема для свойств объекта
432
+ validate?: (value: any) => boolean | string; // пользовательская функция
353
433
  };
354
434
  ```
355
435
 
356
- **Пример сложной валидации:**
436
+ Пример сложной валидации:
357
437
 
358
438
  ```ts
359
- import {postAction, requestBody, DataType} from '@e22m4u/ts-rest-router';
439
+ import {
440
+ DataType,
441
+ postAction,
442
+ requestBody,
443
+ restController,
444
+ } from '@e22m4u/ts-rest-router';
360
445
 
361
446
  @restController('orders')
362
447
  class OrderController {
@@ -372,7 +457,7 @@ class OrderController {
372
457
  products: {
373
458
  type: DataType.ARRAY,
374
459
  required: true,
375
- // Описание схемы для каждого элемента массива:
460
+ // описание схемы для каждого элемента массива
376
461
  items: {
377
462
  type: DataType.OBJECT,
378
463
  properties: {
@@ -383,7 +468,7 @@ class OrderController {
383
468
  quantity: {
384
469
  type: DataType.NUMBER,
385
470
  required: true,
386
- // Валидатор: количество должно быть больше 0
471
+ // валидатор: количество должно быть больше 0
387
472
  validate: qty => qty > 0 || 'Quantity must be positive',
388
473
  },
389
474
  },
@@ -400,42 +485,46 @@ class OrderController {
400
485
  }
401
486
  ```
402
487
 
403
- ## Хуки (`@beforeAction`, `@afterAction`)
488
+ ## Хуки
404
489
 
405
- Хуки - это функции, выполняющиеся до (`@beforeAction`) или после
406
- (`@afterAction`) основного метода контроллера. Они предназначены для
407
- сквозной логики, такой как аутентификация, логирование или модификация
408
- ответа.
490
+ Функции, выполняющиеся до или после основного метода контроллера именуются
491
+ «Хуками». Они предназначены для сквозной логики, такой как аутентификация,
492
+ логирование или модификация ответа. Применение хуков возможно как ко всему
493
+ контроллеру, так и к отдельному методу.
409
494
 
410
- Применение хуков возможно как ко всему контроллеру, так и к отдельному
411
- методу.
495
+ Декораторы:
496
+
497
+ - `@beforeAction(preHandler)` - выполняется перед методом контроллера;
498
+ - `@afterAction(postHandler)` - выполняется после метода контроллера;
499
+
500
+ Пример:
412
501
 
413
502
  ```ts
414
- import {RequestContext} from '@e22m4u/js-trie-router';
415
503
  import createError from 'http-errors';
504
+ import {RequestContext} from '@e22m4u/ts-rest-router';
416
505
 
417
- // Хук для проверки аутентификации
506
+ // хук для проверки аутентификации
418
507
  async function authHook(ctx: RequestContext) {
419
508
  const token = ctx.headers.authorization;
420
509
  if (!token /* || !isValidToken(token) */) {
421
- // Выброс ошибки прерывает выполнение и отправляет клиенту
422
- // соответствующий HTTP-статус.
510
+ // выброс ошибки прерывает выполнение и отправляет
511
+ // клиенту соответствующий HTTP-статус
423
512
  throw createError(401, 'Unauthorized');
424
513
  }
425
514
  }
426
515
 
427
- // Хук для логирования и модификации ответа
516
+ // хук для логирования и модификации ответа
428
517
  async function loggerHook(ctx: RequestContext, data: any) {
429
518
  console.log(`Response for ${ctx.pathname}:`, data);
430
- // Хуки @afterAction могут модифицировать ответ
519
+ // хуки @afterAction могут модифицировать ответ
431
520
  return {...data, timestamp: new Date()};
432
521
  }
433
522
 
434
- @beforeAction(authHook) // Применение ко всем методам контроллера
523
+ @beforeAction(authHook) // применение ко всем методам контроллера
435
524
  @restController('profile')
436
525
  class ProfileController {
437
526
  @getAction('me')
438
- @afterAction(loggerHook) // Применение только к этому методу
527
+ @afterAction(loggerHook) // применение только к этому методу
439
528
  getMyProfile() {
440
529
  return {id: 1, name: 'Current User'};
441
530
  }
@@ -447,23 +536,23 @@ class ProfileController {
447
536
  }
448
537
  ```
449
538
 
450
- ## Архитектура: Жизненный цикл контроллера и DI
539
+ ## Архитектура
451
540
 
452
541
  Понимание архитектурных принципов `ts-rest-router` является ключом
453
542
  к созданию надежных и масштабируемых приложений. Модуль построен на
454
543
  базе библиотеки `@e22m4u/js-service`, реализующей паттерн
455
544
  Service Locator / Dependency Injection.
456
545
 
457
- #### Принцип №1: Изоляция запросов
546
+ #### Изоляция запросов
458
547
 
459
- Для **каждого** входящего HTTP-запроса создается **новый, изолированный
460
- экземпляр контроллера**. Этот фундаментальный принцип гарантирует, что
548
+ Для каждого входящего HTTP-запроса создается новый, изолированный
549
+ экземпляр контроллера. Этот фундаментальный принцип гарантирует, что
461
550
  состояние одного запроса (например, данные аутентифицированного
462
551
  пользователя) никогда не будет разделено с другим, одновременно
463
552
  обрабатываемым запросом. Это устраняет целый класс потенциальных
464
553
  ошибок, связанных с состоянием гонки.
465
554
 
466
- #### Принцип №2: Request-Scoped Service Container
555
+ #### Внедрение зависимостей
467
556
 
468
557
  Каждый экземпляр контроллера создается с помощью своего собственного
469
558
  DI-контейнера, который существует только в рамках одного запроса. Чтобы
@@ -474,31 +563,31 @@ DI-контейнера, который существует только в р
474
563
 
475
564
  **Практический пример с сервисом аутентификации:**
476
565
 
477
- **Шаг 1: Создание хука для подготовки сервиса**
566
+ 1\. Создание хука для подготовки сервиса.
478
567
 
479
- Хуки - идеальное место для подготовки сервисов, специфичных для запроса.
480
- В этом примере хук `@beforeAction` создает экземпляр `AuthService`,
568
+ С помощью хука можно подготовить сервис, специфичный для каждого запроса.
569
+ В данном примере хук `@beforeAction` создает экземпляр `AuthService`,
481
570
  выполняет аутентификацию и регистрирует его в контейнере запроса.
482
571
 
483
572
  ```ts
484
573
  // src/auth.hook.ts
485
574
  import {AuthService} from './auth.service';
486
- import {RequestContext} from '@e22m4u/js-trie-router';
575
+ import {RequestContext} from '@e22m4u/ts-rest-router';
487
576
 
488
577
  export async function authHook(context: RequestContext) {
489
578
  const requestContainer = context.container;
490
- // Создание сервиса с передачей ему контейнера запроса
579
+ // создание сервиса с передачей ему контейнера запроса
491
580
  const authService = new AuthService(requestContainer);
492
- // Регистрация созданного экземпляра в контейнере.
493
- // Теперь любой другой сервис в рамках этого запроса
494
- // сможет получить этот конкретный экземпляр.
581
+ // регистрация созданного экземпляра в контейнере
582
+ // (теперь любой другой сервис в рамках текущего
583
+ // запроса сможет получить данный экземпляр)
495
584
  requestContainer.set(AuthService, authService);
496
- // Выполнение логики аутентификации
585
+ // выполнение логики аутентификации
497
586
  await authService.authenticate(context.headers.authorization);
498
587
  }
499
588
  ```
500
589
 
501
- **Шаг 2: Создание `AuthService`**
590
+ 2\. Создание `AuthService`.
502
591
 
503
592
  `AuthService` наследуется от `Service`, что позволяет ему запрашивать другие
504
593
  зависимости (например, `this.getService(DatabaseService)`).
@@ -511,7 +600,8 @@ export class AuthService extends Service {
511
600
  public currentUser?: {id: number; name: string};
512
601
 
513
602
  async authenticate(token?: string) {
514
- // Логика проверки токена и поиска пользователя в БД...
603
+ // логика проверки токена и поиска
604
+ // пользователя в базе данных...
515
605
  if (token === 'valid-token') {
516
606
  this.currentUser = {id: 1, name: 'John Doe'};
517
607
  }
@@ -519,7 +609,7 @@ export class AuthService extends Service {
519
609
  }
520
610
  ```
521
611
 
522
- **Шаг 3: Использование сервиса в контроллере**
612
+ 3\. Использование сервиса в контроллере.
523
613
 
524
614
  Контроллер, унаследованный от `Service`, теперь может получить
525
615
  доступ к предварительно настроенному экземпляру `AuthService`
@@ -531,18 +621,20 @@ import {authHook} from './auth.hook';
531
621
  import createError from 'http-errors';
532
622
  import {Service} from '@e22m4u/js-service';
533
623
  import {AuthService} from './auth.service';
534
- import {getAction, restController, beforeAction} from '@e22m4u/ts-rest-router';
624
+ import {getAction, beforeAction, restController} from '@e22m4u/ts-rest-router';
535
625
 
536
626
  @beforeAction(authHook)
537
627
  @restController('profile')
538
628
  export class ProfileController extends Service {
539
629
  @getAction('me')
540
630
  getProfile() {
541
- // Получение request-scoped экземпляра AuthService,
542
- // который был создан и зарегистрирован в хуке.
631
+ // получение request-scoped экземпляра AuthService,
632
+ // который был создан и зарегистрирован в хуке
543
633
  const authService = this.getService(AuthService);
544
634
 
545
- if (!authService.currentUser) throw createError(401, 'Unauthorized');
635
+ if (!authService.currentUser) {
636
+ throw createError(401, 'Unauthorized');
637
+ }
546
638
 
547
639
  return authService.currentUser;
548
640
  }
@@ -553,12 +645,12 @@ export class ProfileController extends Service {
553
645
  и контроллером, позволяя безопасно передавать состояние, изолированное
554
646
  в рамках одного HTTP-запроса.
555
647
 
556
- ## Производительность: Префиксное дерево
648
+ ## Производительность
557
649
 
558
650
  В основе данного модуля лежит
559
651
  [@e22m4u/js-trie-router](https://www.npmjs.com/package/@e22m4u/js-trie-router),
560
652
  который использует структуру данных
561
- **префиксного дерева** ([Trie](https://en.wikipedia.org/wiki/Trie))
653
+ *префиксного дерева ([Trie](https://en.wikipedia.org/wiki/Trie))*
562
654
  для хранения маршрутов и их сопоставления. Такое архитектурное решение
563
655
  обеспечивает высокую производительность, особенно в приложениях
564
656
  с большим количеством маршрутов.
@@ -574,41 +666,39 @@ export class ProfileController extends Service {
574
666
  маршрутизатор не перебирает все существующие маршруты. Вместо этого
575
667
  он последовательно спускается по дереву:
576
668
 
577
- 1. Находит корневой узел для метода `GET`.
578
- 2. От него переходит к дочернему узлу `users`.
579
- 3. Далее, не найдя статического узла `123`, он ищет динамический
580
- узел (`:id`) и сопоставляет его, сохраняя `123` как значение
581
- параметра `id`.
582
- 4. Наконец, он переходит к узлу `posts` и находит совпадение.
583
-
584
- #### Преимущества для производительности
585
-
586
- 1. **Эффективный поиск (O(k))**
587
- Самое главное преимущество скорость поиска. Вместо того чтобы
588
- перебирать массив из `N` маршрутов и проверять каждый с помощью
589
- регулярного выражения (сложность `O(N)`), префиксное дерево
590
- находит совпадение за время, пропорциональное количеству сегментов
591
- `k` в URL-пути (сложность `O(k)`). Это означает, что производительность
592
- поиска **не падает** с ростом общего числа маршрутов в приложении.
593
-
594
- 2. **Быстрая обработка 404 (ранний выход)**
595
- Если приходит запрос на несуществующий путь, например
596
- `/users/123/comments`, поиск по дереву прекратится сразу после
597
- того, как маршрутизатор не найдет дочерний узел `comments`
598
- у узла `:id`. Ему не нужно проверять остальные сотни маршрутов,
599
- чтобы убедиться, что совпадений нет. Это делает обработку
600
- ненайденных маршрутов (404) почти мгновенной.
601
-
602
- 3. **Оптимизация для статических и динамических маршрутов**
603
- При поиске маршрутизатор всегда отдает приоритет статическим
604
- сегментам перед динамическими. Маршрут `/users/me` всегда будет
605
- найден раньше и быстрее, чем `/users/:id` при запросе `/users/me`,
606
- поскольку не требуется проверка на соответствие шаблону.
607
-
608
- Этот подход делает `ts-rest-router` производительным решением для крупных
609
- приложений с большим количеством маршрутов.
610
-
611
- ## Полный список декораторов
669
+ 1. Находит корневой узел для метода `GET`.
670
+ 2. От него переходит к дочернему узлу `users`.
671
+ 3. Далее, не найдя статического узла `123`, он ищет динамический
672
+ узел (`:id`) и сопоставляет его, сохраняя `123` как значение
673
+ параметра `id`.
674
+ 4. Наконец, он переходит к узлу `posts` и находит совпадение.
675
+
676
+ **Эффективный поиск маршрута O(k)**
677
+
678
+ Самое главное преимущество - скорость поиска. Вместо того чтобы
679
+ перебирать массив из `N` маршрутов и проверять каждый с помощью
680
+ регулярного выражения (сложность `O(N)`), префиксное дерево
681
+ находит совпадение за время, пропорциональное количеству сегментов
682
+ `K` в URL-пути (сложность `O(K)`). Это означает, что производительность
683
+ поиска не падает с ростом общего числа маршрутов в приложении.
684
+
685
+ **Быстрая обработка 404 (ранний выход)**
686
+
687
+ Если приходит запрос на несуществующий путь, например
688
+ `/users/123/comments`, поиск по дереву прекратится сразу после
689
+ того, как маршрутизатор не найдет дочерний узел `comments`
690
+ у узла `:id`. Ему не нужно проверять остальные сотни маршрутов,
691
+ чтобы убедиться, что совпадений нет. Это делает обработку
692
+ неопределенных маршрутов (404) почти мгновенной.
693
+
694
+ **Оптимизация для статических и динамических маршрутов**
695
+
696
+ При поиске маршрутизатор всегда отдает приоритет статическим
697
+ сегментам перед динамическими. Маршрут `/users/me` всегда будет
698
+ найден раньше и быстрее, чем `/users/:id` при запросе `/users/me`,
699
+ поскольку не требуется проверка на соответствие шаблону.
700
+
701
+ ## Декораторы
612
702
 
613
703
  #### Контроллер и методы:
614
704
 
@@ -205,7 +205,7 @@ var RequestDataSource;
205
205
  RequestDataSource2["PARAMS"] = "params";
206
206
  RequestDataSource2["QUERY"] = "query";
207
207
  RequestDataSource2["HEADERS"] = "headers";
208
- RequestDataSource2["COOKIE"] = "cookie";
208
+ RequestDataSource2["COOKIES"] = "cookies";
209
209
  RequestDataSource2["BODY"] = "body";
210
210
  })(RequestDataSource || (RequestDataSource = {}));
211
211
  var REQUEST_DATA_METADATA_KEY = new import_ts_reflector5.MetadataKey("requestDataMetadataKey");
@@ -305,8 +305,8 @@ var requestQueries = createRequestDataDecoratorWithSource(RequestDataSource.QUER
305
305
  var requestQuery = createRequestDataPropertyDecoratorWithSource(RequestDataSource.QUERY);
306
306
  var requestHeaders = createRequestDataDecoratorWithSource(RequestDataSource.HEADERS);
307
307
  var requestHeader = createRequestDataPropertyDecoratorWithSource(RequestDataSource.HEADERS);
308
- var requestCookies = createRequestDataDecoratorWithSource(RequestDataSource.COOKIE);
309
- var requestCookie = createRequestDataPropertyDecoratorWithSource(RequestDataSource.COOKIE);
308
+ var requestCookies = createRequestDataDecoratorWithSource(RequestDataSource.COOKIES);
309
+ var requestCookie = createRequestDataPropertyDecoratorWithSource(RequestDataSource.COOKIES);
310
310
  var requestBody = createRequestDataDecoratorWithSource(RequestDataSource.BODY);
311
311
  var requestField = createRequestDataPropertyDecoratorWithSource(RequestDataSource.BODY);
312
312
 
@@ -896,7 +896,7 @@ var _ControllerRegistry = class _ControllerRegistry extends DebuggableService {
896
896
  case RequestDataSource.HEADERS:
897
897
  data = requestContext2.headers;
898
898
  break;
899
- case RequestDataSource.COOKIE:
899
+ case RequestDataSource.COOKIES:
900
900
  data = requestContext2.cookies;
901
901
  break;
902
902
  case RequestDataSource.BODY:
@@ -372,7 +372,7 @@ export class ControllerRegistry extends DebuggableService {
372
372
  case RequestDataSource.HEADERS:
373
373
  data = requestContext.headers;
374
374
  break;
375
- case RequestDataSource.COOKIE:
375
+ case RequestDataSource.COOKIES:
376
376
  data = requestContext.cookies;
377
377
  break;
378
378
  case RequestDataSource.BODY:
@@ -82,7 +82,7 @@ export const requestQueries = createRequestDataDecoratorWithSource(RequestDataSo
82
82
  export const requestQuery = createRequestDataPropertyDecoratorWithSource(RequestDataSource.QUERY);
83
83
  export const requestHeaders = createRequestDataDecoratorWithSource(RequestDataSource.HEADERS);
84
84
  export const requestHeader = createRequestDataPropertyDecoratorWithSource(RequestDataSource.HEADERS);
85
- export const requestCookies = createRequestDataDecoratorWithSource(RequestDataSource.COOKIE);
86
- export const requestCookie = createRequestDataPropertyDecoratorWithSource(RequestDataSource.COOKIE);
85
+ export const requestCookies = createRequestDataDecoratorWithSource(RequestDataSource.COOKIES);
86
+ export const requestCookie = createRequestDataPropertyDecoratorWithSource(RequestDataSource.COOKIES);
87
87
  export const requestBody = createRequestDataDecoratorWithSource(RequestDataSource.BODY);
88
88
  export const requestField = createRequestDataPropertyDecoratorWithSource(RequestDataSource.BODY);
@@ -126,7 +126,7 @@ describe('requestData', function () {
126
126
  ], Target.prototype, "myMethod", null);
127
127
  const res = RequestDataReflector.getMetadata(Target, 'myMethod');
128
128
  expect(res.get(0)).to.be.eql({
129
- source: RequestDataSource.COOKIE,
129
+ source: RequestDataSource.COOKIES,
130
130
  schema: { type: DataType.ANY },
131
131
  });
132
132
  });
@@ -554,7 +554,7 @@ describe('requestData', function () {
554
554
  ], Target.prototype, "myMethod", null);
555
555
  const res = RequestDataReflector.getMetadata(Target, 'myMethod');
556
556
  expect(res.get(0)).to.be.eql({
557
- source: RequestDataSource.COOKIE,
557
+ source: RequestDataSource.COOKIES,
558
558
  schema: {
559
559
  type: DataType.OBJECT,
560
560
  properties: {
@@ -580,7 +580,7 @@ describe('requestData', function () {
580
580
  ], Target.prototype, "myMethod", null);
581
581
  const res = RequestDataReflector.getMetadata(Target, 'myMethod');
582
582
  expect(res.get(0)).to.be.eql({
583
- source: RequestDataSource.COOKIE,
583
+ source: RequestDataSource.COOKIES,
584
584
  schema: {
585
585
  type: DataType.OBJECT,
586
586
  properties: {
@@ -609,7 +609,7 @@ describe('requestData', function () {
609
609
  ], Target.prototype, "myMethod", null);
610
610
  const res = RequestDataReflector.getMetadata(Target, 'myMethod');
611
611
  expect(res.get(0)).to.be.eql({
612
- source: RequestDataSource.COOKIE,
612
+ source: RequestDataSource.COOKIES,
613
613
  schema: {
614
614
  type: DataType.OBJECT,
615
615
  properties: {
@@ -637,7 +637,7 @@ describe('requestData', function () {
637
637
  ], Target.prototype, "myMethod", null);
638
638
  const mdMap = RequestDataReflector.getMetadata(Target, 'myMethod');
639
639
  const md = mdMap.get(0);
640
- expect(md.source).to.be.eq(RequestDataSource.COOKIE);
640
+ expect(md.source).to.be.eq(RequestDataSource.COOKIES);
641
641
  expect(md.schema).to.be.a('function');
642
642
  expect(md.property).to.be.eq(propertyKey);
643
643
  const res1 = md.schema;
@@ -7,7 +7,7 @@ export declare enum RequestDataSource {
7
7
  PARAMS = "params",
8
8
  QUERY = "query",
9
9
  HEADERS = "headers",
10
- COOKIE = "cookie",
10
+ COOKIES = "cookies",
11
11
  BODY = "body"
12
12
  }
13
13
  /**
@@ -7,7 +7,7 @@ export var RequestDataSource;
7
7
  RequestDataSource["PARAMS"] = "params";
8
8
  RequestDataSource["QUERY"] = "query";
9
9
  RequestDataSource["HEADERS"] = "headers";
10
- RequestDataSource["COOKIE"] = "cookie";
10
+ RequestDataSource["COOKIES"] = "cookies";
11
11
  RequestDataSource["BODY"] = "body";
12
12
  })(RequestDataSource || (RequestDataSource = {}));
13
13
  /**
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@e22m4u/ts-rest-router",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Декларативный REST-маршрутизатор на основе контроллеров для TypeScript",
5
5
  "author": "Mikhail Evstropov <e22m4u@yandex.ru>",
6
6
  "license": "MIT",
@@ -44,7 +44,7 @@
44
44
  "@e22m4u/js-data-schema": "~0.4.6",
45
45
  "@e22m4u/js-debug": "~0.3.2",
46
46
  "@e22m4u/js-format": "~0.2.0",
47
- "@e22m4u/js-service": "~0.4.4",
47
+ "@e22m4u/js-service": "~0.4.5",
48
48
  "@e22m4u/js-trie-router": "~0.3.4",
49
49
  "@e22m4u/ts-reflector": "~0.1.8",
50
50
  "http-errors": "~2.0.0"
@@ -52,25 +52,25 @@
52
52
  "devDependencies": {
53
53
  "@commitlint/cli": "~20.1.0",
54
54
  "@commitlint/config-conventional": "~20.0.0",
55
- "@eslint/js": "~9.38.0",
55
+ "@eslint/js": "~9.39.1",
56
56
  "@types/chai": "~5.2.3",
57
57
  "@types/debug": "~4.1.12",
58
58
  "@types/http-errors": "~2.0.5",
59
59
  "@types/mocha": "~10.0.10",
60
- "@types/node": "~24.9.2",
60
+ "@types/node": "~24.10.0",
61
61
  "c8": "~10.1.3",
62
62
  "chai": "~6.2.0",
63
- "esbuild": "~0.25.11",
64
- "eslint": "~9.38.0",
63
+ "esbuild": "~0.25.12",
64
+ "eslint": "~9.39.1",
65
65
  "eslint-config-prettier": "~10.1.8",
66
66
  "eslint-plugin-chai-expect": "~3.1.0",
67
67
  "eslint-plugin-mocha": "~11.2.0",
68
68
  "husky": "~9.1.7",
69
- "mocha": "~11.7.4",
69
+ "mocha": "~11.7.5",
70
70
  "prettier": "~3.6.2",
71
- "rimraf": "~6.0.1",
71
+ "rimraf": "~6.1.0",
72
72
  "tsx": "~4.20.6",
73
73
  "typescript": "~5.9.3",
74
- "typescript-eslint": "~8.46.2"
74
+ "typescript-eslint": "~8.46.3"
75
75
  }
76
76
  }
@@ -450,7 +450,7 @@ export class ControllerRegistry extends DebuggableService {
450
450
  case RequestDataSource.HEADERS:
451
451
  data = requestContext.headers;
452
452
  break;
453
- case RequestDataSource.COOKIE:
453
+ case RequestDataSource.COOKIES:
454
454
  data = requestContext.cookies;
455
455
  break;
456
456
  case RequestDataSource.BODY:
@@ -108,7 +108,7 @@ describe('requestData', function () {
108
108
  }
109
109
  const res = RequestDataReflector.getMetadata(Target, 'myMethod');
110
110
  expect(res.get(0)).to.be.eql({
111
- source: RequestDataSource.COOKIE,
111
+ source: RequestDataSource.COOKIES,
112
112
  schema: {type: DataType.ANY},
113
113
  });
114
114
  });
@@ -502,7 +502,7 @@ describe('requestData', function () {
502
502
  }
503
503
  const res = RequestDataReflector.getMetadata(Target, 'myMethod');
504
504
  expect(res.get(0)).to.be.eql({
505
- source: RequestDataSource.COOKIE,
505
+ source: RequestDataSource.COOKIES,
506
506
  schema: {
507
507
  type: DataType.OBJECT,
508
508
  properties: {
@@ -526,7 +526,7 @@ describe('requestData', function () {
526
526
  }
527
527
  const res = RequestDataReflector.getMetadata(Target, 'myMethod');
528
528
  expect(res.get(0)).to.be.eql({
529
- source: RequestDataSource.COOKIE,
529
+ source: RequestDataSource.COOKIES,
530
530
  schema: {
531
531
  type: DataType.OBJECT,
532
532
  properties: {
@@ -553,7 +553,7 @@ describe('requestData', function () {
553
553
  }
554
554
  const res = RequestDataReflector.getMetadata(Target, 'myMethod');
555
555
  expect(res.get(0)).to.be.eql({
556
- source: RequestDataSource.COOKIE,
556
+ source: RequestDataSource.COOKIES,
557
557
  schema: {
558
558
  type: DataType.OBJECT,
559
559
  properties: {
@@ -579,7 +579,7 @@ describe('requestData', function () {
579
579
  }
580
580
  const mdMap = RequestDataReflector.getMetadata(Target, 'myMethod');
581
581
  const md = mdMap.get(0) as RequestDataMetadata;
582
- expect(md.source).to.be.eq(RequestDataSource.COOKIE);
582
+ expect(md.source).to.be.eq(RequestDataSource.COOKIES);
583
583
  expect(md.schema).to.be.a('function');
584
584
  expect(md.property).to.be.eq(propertyKey);
585
585
  const res1 = md.schema as DataSchemaFactory;
@@ -118,10 +118,10 @@ export const requestHeader = createRequestDataPropertyDecoratorWithSource(
118
118
  RequestDataSource.HEADERS,
119
119
  );
120
120
  export const requestCookies = createRequestDataDecoratorWithSource(
121
- RequestDataSource.COOKIE,
121
+ RequestDataSource.COOKIES,
122
122
  );
123
123
  export const requestCookie = createRequestDataPropertyDecoratorWithSource(
124
- RequestDataSource.COOKIE,
124
+ RequestDataSource.COOKIES,
125
125
  );
126
126
  export const requestBody = createRequestDataDecoratorWithSource(
127
127
  RequestDataSource.BODY,
@@ -8,7 +8,7 @@ export enum RequestDataSource {
8
8
  PARAMS = 'params',
9
9
  QUERY = 'query',
10
10
  HEADERS = 'headers',
11
- COOKIE = 'cookie',
11
+ COOKIES = 'cookies',
12
12
  BODY = 'body',
13
13
  }
14
14