@e22m4u/js-trie-router 0.7.2 → 0.7.4
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 +94 -0
- package/dist/cjs/index.cjs +33 -81
- package/package.json +1 -1
- package/src/parsers/request-body-parser.d.ts +2 -2
- package/src/parsers/request-body-parser.js +15 -14
- package/src/parsers/request-body-parser.spec.js +2 -2
- package/src/trie-router-options.d.ts +7 -21
- package/src/trie-router-options.js +19 -71
- package/src/trie-router-options.spec.js +10 -189
- package/src/trie-router.spec.js +2 -2
- package/src/utils/create-response-mock.js +3 -1
- package/src/utils/create-response-mock.spec.js +16 -0
package/README.md
CHANGED
|
@@ -20,6 +20,7 @@ HTTP маршрутизатор для Node.js на основе
|
|
|
20
20
|
- [Параметры маршрутизатора](#параметры-маршрутизатора)
|
|
21
21
|
- [Контекст запроса](#контекст-запроса)
|
|
22
22
|
- [Отправка ответа](#отправка-ответа)
|
|
23
|
+
- [Парсинг тела запроса](#парсинг-тела-запроса)
|
|
23
24
|
- [Жизненный цикл](#жизненный-цикл)
|
|
24
25
|
- [Хуки маршрута](#хуки-маршрута)
|
|
25
26
|
- [Глобальные хуки](#глобальные-хуки)
|
|
@@ -219,6 +220,99 @@ router.defineRoute({
|
|
|
219
220
|
});
|
|
220
221
|
```
|
|
221
222
|
|
|
223
|
+
### Парсинг тела запроса
|
|
224
|
+
|
|
225
|
+
Для разбора тела входящего запроса отслеживается заголовок `Content-Type`,
|
|
226
|
+
определяющий формат передаваемых данных. По умолчанию маршрутизатор включает
|
|
227
|
+
парсеры для следующих форматов:
|
|
228
|
+
|
|
229
|
+
- `application/json` разбирается как *JSON*;
|
|
230
|
+
- `text/plain` преобразуется в строку;
|
|
231
|
+
|
|
232
|
+
Если входящий запрос содержит данные, но для его формата не найден подходящий
|
|
233
|
+
парсер, маршрутизатор прервет обработку запроса и вернет ошибку
|
|
234
|
+
*415 Unsupported Media Type*.
|
|
235
|
+
|
|
236
|
+
```json
|
|
237
|
+
{
|
|
238
|
+
"error": {
|
|
239
|
+
"message": "Media type \"application/octet-stream\" is not supported."
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
```
|
|
243
|
+
|
|
244
|
+
Чтобы избежать появления ошибки для форматов, которые предполагается
|
|
245
|
+
обрабатывать особым способом, предусмотрен параметр маршрутизатора
|
|
246
|
+
`ignoredMediaTypes` для игнорирования указанных медиа-типов. Параметр
|
|
247
|
+
позволяет пропустить встроенный парсинг, как это сделано в примере ниже.
|
|
248
|
+
|
|
249
|
+
```js
|
|
250
|
+
import {TrieRouter, HttpMethod} from '@e22m4u/js-trie-router';
|
|
251
|
+
|
|
252
|
+
// создание экземпляра маршрутизатора
|
|
253
|
+
// с указанием исключаемых медиа-типов
|
|
254
|
+
const router = new TrieRouter({
|
|
255
|
+
ignoredMediaTypes: [
|
|
256
|
+
'application/octet-stream',
|
|
257
|
+
'multipart/form-data'
|
|
258
|
+
],
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
// регистрация маршрута для обработки файлов
|
|
262
|
+
router.defineRoute({
|
|
263
|
+
method: HttpMethod.POST,
|
|
264
|
+
path: '/upload',
|
|
265
|
+
handler(ctx) {
|
|
266
|
+
// свойство body остается пустым, далее
|
|
267
|
+
// выполняется доступ к нативному потоку
|
|
268
|
+
const stream = ctx.request;
|
|
269
|
+
return 'OK';
|
|
270
|
+
}
|
|
271
|
+
});
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
#### Регистрация пользовательского парсера
|
|
275
|
+
|
|
276
|
+
Для расширения поддерживаемых форматов предусмотрена возможность регистрации
|
|
277
|
+
пользовательской функции парсинга для определенного медиа-типа. Управление
|
|
278
|
+
такими функциями выполняется сервисом `RequestBodyParser`, который доступен
|
|
279
|
+
через глобальный контейнер маршрутизатора.
|
|
280
|
+
|
|
281
|
+
```js
|
|
282
|
+
// доступ к сервису через маршрутизатор
|
|
283
|
+
const bodyParser = router.getService(RequestBodyParser);
|
|
284
|
+
// bodyParser.defineParser(mediaType, parserFn); см. далее
|
|
285
|
+
```
|
|
286
|
+
|
|
287
|
+
Регистрируемая функция принимает извлеченные данные в виде строки и возвращает
|
|
288
|
+
преобразованный результат. Итоговое значение впоследствии будет доступно
|
|
289
|
+
в контексте обработки запроса.
|
|
290
|
+
|
|
291
|
+
```js
|
|
292
|
+
import queryString from 'querystring';
|
|
293
|
+
import {TrieRouter, HttpMethod, RequestBodyParser} from '@e22m4u/js-trie-router';
|
|
294
|
+
|
|
295
|
+
const router = new TrieRouter();
|
|
296
|
+
const bodyParser = router.getService(RequestBodyParser);
|
|
297
|
+
|
|
298
|
+
// регистрация парсера для обработки данных формы
|
|
299
|
+
bodyParser.defineParser(
|
|
300
|
+
'application/x-www-form-urlencoded',
|
|
301
|
+
(input) => queryString.parse(input),
|
|
302
|
+
);
|
|
303
|
+
|
|
304
|
+
// определение маршрута
|
|
305
|
+
router.defineRoute({
|
|
306
|
+
method: HttpMethod.POST,
|
|
307
|
+
path: '/submit',
|
|
308
|
+
handler(ctx) {
|
|
309
|
+
// свойство содержит результат
|
|
310
|
+
// работы новой парсер-функции
|
|
311
|
+
return ctx.body;
|
|
312
|
+
}
|
|
313
|
+
});
|
|
314
|
+
```
|
|
315
|
+
|
|
222
316
|
### Жизненный цикл
|
|
223
317
|
|
|
224
318
|
Для понимания того, как маршрутизатор обрабатывает входящий запрос, ниже
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -881,7 +881,7 @@ function patchHeaders(response) {
|
|
|
881
881
|
);
|
|
882
882
|
}
|
|
883
883
|
const key = name.toLowerCase();
|
|
884
|
-
this._headers[key] = String(value);
|
|
884
|
+
this._headers[key] = Array.isArray(value) ? [...value.map(String)] : String(value);
|
|
885
885
|
return this;
|
|
886
886
|
}, "value")
|
|
887
887
|
});
|
|
@@ -2071,12 +2071,28 @@ var _TrieRouterOptions = class _TrieRouterOptions {
|
|
|
2071
2071
|
*/
|
|
2072
2072
|
_requestBodyBytesLimit = 512 * 1024;
|
|
2073
2073
|
// 512kb
|
|
2074
|
+
/**
|
|
2075
|
+
* Request body bytes limit.
|
|
2076
|
+
*
|
|
2077
|
+
* @type {number}
|
|
2078
|
+
*/
|
|
2079
|
+
get requestBodyBytesLimit() {
|
|
2080
|
+
return this._requestBodyBytesLimit;
|
|
2081
|
+
}
|
|
2074
2082
|
/**
|
|
2075
2083
|
* Ignored media types.
|
|
2076
2084
|
*
|
|
2077
2085
|
* @type {string[]}
|
|
2078
2086
|
*/
|
|
2079
2087
|
_ignoredMediaTypes = [];
|
|
2088
|
+
/**
|
|
2089
|
+
* Ignored media types.
|
|
2090
|
+
*
|
|
2091
|
+
* @type {string[]}
|
|
2092
|
+
*/
|
|
2093
|
+
get ignoredMediaTypes() {
|
|
2094
|
+
return this._ignoredMediaTypes;
|
|
2095
|
+
}
|
|
2080
2096
|
/**
|
|
2081
2097
|
* Constructor.
|
|
2082
2098
|
*
|
|
@@ -2118,74 +2134,9 @@ var _TrieRouterOptions = class _TrieRouterOptions {
|
|
|
2118
2134
|
this._ignoredMediaTypes.push(mediaTypeLc);
|
|
2119
2135
|
}
|
|
2120
2136
|
});
|
|
2137
|
+
Object.freeze(this._ignoredMediaTypes);
|
|
2121
2138
|
}
|
|
2122
2139
|
}
|
|
2123
|
-
/**
|
|
2124
|
-
* Get request body bytes limit.
|
|
2125
|
-
*
|
|
2126
|
-
* @param {number} limit
|
|
2127
|
-
* @returns {this}
|
|
2128
|
-
*/
|
|
2129
|
-
setRequestBodyBytesLimit(limit) {
|
|
2130
|
-
if (typeof limit !== "number" || limit < 0) {
|
|
2131
|
-
throw new import_js_format18.InvalidArgumentError(
|
|
2132
|
-
'Parameter "limit" must be a positive Number or 0, but %v was given.',
|
|
2133
|
-
limit
|
|
2134
|
-
);
|
|
2135
|
-
}
|
|
2136
|
-
this._requestBodyBytesLimit = limit;
|
|
2137
|
-
return this;
|
|
2138
|
-
}
|
|
2139
|
-
/**
|
|
2140
|
-
* Get request body bytes limit.
|
|
2141
|
-
*
|
|
2142
|
-
* @returns {number}
|
|
2143
|
-
*/
|
|
2144
|
-
getRequestBodyBytesLimit() {
|
|
2145
|
-
return this._requestBodyBytesLimit;
|
|
2146
|
-
}
|
|
2147
|
-
/**
|
|
2148
|
-
* Get ignored media types.
|
|
2149
|
-
*
|
|
2150
|
-
* @param {string} mediaType
|
|
2151
|
-
* @returns {this}
|
|
2152
|
-
*/
|
|
2153
|
-
addIgnoredMediaType(mediaType) {
|
|
2154
|
-
if (!mediaType || typeof mediaType !== "string") {
|
|
2155
|
-
throw new import_js_format18.InvalidArgumentError(
|
|
2156
|
-
'Parameter "mediaType" must be a non-empty String, but %v was given.',
|
|
2157
|
-
mediaType
|
|
2158
|
-
);
|
|
2159
|
-
}
|
|
2160
|
-
const mediaTypeLc = mediaType.toLowerCase();
|
|
2161
|
-
if (!this._ignoredMediaTypes.includes(mediaTypeLc)) {
|
|
2162
|
-
this._ignoredMediaTypes.push(mediaTypeLc);
|
|
2163
|
-
}
|
|
2164
|
-
return this;
|
|
2165
|
-
}
|
|
2166
|
-
/**
|
|
2167
|
-
* Has ignored media type.
|
|
2168
|
-
*
|
|
2169
|
-
* @param {string} mediaType
|
|
2170
|
-
* @returns {boolean}
|
|
2171
|
-
*/
|
|
2172
|
-
hasIgnoredMediaType(mediaType) {
|
|
2173
|
-
if (!mediaType || typeof mediaType !== "string") {
|
|
2174
|
-
throw new import_js_format18.InvalidArgumentError(
|
|
2175
|
-
'Parameter "mediaType" must be a non-empty String, but %v was given.',
|
|
2176
|
-
mediaType
|
|
2177
|
-
);
|
|
2178
|
-
}
|
|
2179
|
-
return this._ignoredMediaTypes.includes(mediaType.toLowerCase());
|
|
2180
|
-
}
|
|
2181
|
-
/**
|
|
2182
|
-
* Get ignored media types.
|
|
2183
|
-
*
|
|
2184
|
-
* @returns {string[]}
|
|
2185
|
-
*/
|
|
2186
|
-
getIgnoredMediaTypes() {
|
|
2187
|
-
return this._ignoredMediaTypes.slice();
|
|
2188
|
-
}
|
|
2189
2140
|
};
|
|
2190
2141
|
__name(_TrieRouterOptions, "TrieRouterOptions");
|
|
2191
2142
|
var TrieRouterOptions = _TrieRouterOptions;
|
|
@@ -2205,23 +2156,23 @@ var _RequestBodyParser = class _RequestBodyParser extends DebuggableService {
|
|
|
2205
2156
|
* Set parser.
|
|
2206
2157
|
*
|
|
2207
2158
|
* @param {string} mediaType
|
|
2208
|
-
* @param {Function}
|
|
2159
|
+
* @param {Function} parserFn
|
|
2209
2160
|
* @returns {this}
|
|
2210
2161
|
*/
|
|
2211
|
-
defineParser(mediaType,
|
|
2162
|
+
defineParser(mediaType, parserFn) {
|
|
2212
2163
|
if (!mediaType || typeof mediaType !== "string") {
|
|
2213
2164
|
throw new import_js_format19.InvalidArgumentError(
|
|
2214
2165
|
'Parameter "mediaType" must be a non-empty String, but %v was given.',
|
|
2215
2166
|
mediaType
|
|
2216
2167
|
);
|
|
2217
2168
|
}
|
|
2218
|
-
if (!
|
|
2169
|
+
if (!parserFn || typeof parserFn !== "function") {
|
|
2219
2170
|
throw new import_js_format19.InvalidArgumentError(
|
|
2220
|
-
'Parameter "
|
|
2221
|
-
|
|
2171
|
+
'Parameter "parserFn" must be a Function, but %v was given.',
|
|
2172
|
+
parserFn
|
|
2222
2173
|
);
|
|
2223
2174
|
}
|
|
2224
|
-
this._parsers[mediaType.toLowerCase()] =
|
|
2175
|
+
this._parsers[mediaType.toLowerCase()] = parserFn;
|
|
2225
2176
|
return this;
|
|
2226
2177
|
}
|
|
2227
2178
|
/**
|
|
@@ -2252,14 +2203,14 @@ var _RequestBodyParser = class _RequestBodyParser extends DebuggableService {
|
|
|
2252
2203
|
mediaType
|
|
2253
2204
|
);
|
|
2254
2205
|
}
|
|
2255
|
-
const
|
|
2256
|
-
if (!
|
|
2206
|
+
const parserFn = this._parsers[mediaType.toLowerCase()];
|
|
2207
|
+
if (!parserFn) {
|
|
2257
2208
|
throw new import_js_format19.InvalidArgumentError(
|
|
2258
2209
|
"Media type %v does not have a parser.",
|
|
2259
2210
|
mediaType
|
|
2260
2211
|
);
|
|
2261
2212
|
}
|
|
2262
|
-
return
|
|
2213
|
+
return parserFn;
|
|
2263
2214
|
}
|
|
2264
2215
|
/**
|
|
2265
2216
|
* Remove parser.
|
|
@@ -2307,26 +2258,27 @@ var _RequestBodyParser = class _RequestBodyParser extends DebuggableService {
|
|
|
2307
2258
|
);
|
|
2308
2259
|
}
|
|
2309
2260
|
const options = this.getService(TrieRouterOptions);
|
|
2310
|
-
const
|
|
2261
|
+
const mediaTypeLc = mediaType.toLowerCase();
|
|
2262
|
+
const isMediaTypeIgnored = options.ignoredMediaTypes.includes(mediaTypeLc);
|
|
2311
2263
|
if (isMediaTypeIgnored) {
|
|
2312
2264
|
debug("Media type %v is ignored.", mediaType);
|
|
2313
2265
|
return;
|
|
2314
2266
|
}
|
|
2315
|
-
const
|
|
2316
|
-
if (!
|
|
2267
|
+
const parserFn = this._parsers[mediaTypeLc];
|
|
2268
|
+
if (!parserFn) {
|
|
2317
2269
|
throw createError(
|
|
2318
2270
|
import_http_errors2.default.UnsupportedMediaType,
|
|
2319
2271
|
"Media type %v is not supported.",
|
|
2320
2272
|
mediaType
|
|
2321
2273
|
);
|
|
2322
2274
|
}
|
|
2323
|
-
const bodyBytesLimit = options.
|
|
2275
|
+
const bodyBytesLimit = options.requestBodyBytesLimit;
|
|
2324
2276
|
debug("Fetching a request body.");
|
|
2325
2277
|
debug("Body limit is %v bytes.", bodyBytesLimit);
|
|
2326
2278
|
return fetchRequestBody(request, bodyBytesLimit).then((rawBody) => {
|
|
2327
2279
|
if (rawBody != null) {
|
|
2328
2280
|
debug("Read %v bytes.", Buffer.byteLength(rawBody, "utf8"));
|
|
2329
|
-
return
|
|
2281
|
+
return parserFn(rawBody);
|
|
2330
2282
|
}
|
|
2331
2283
|
debug("Request body has no content.");
|
|
2332
2284
|
return rawBody;
|
package/package.json
CHANGED
|
@@ -15,9 +15,9 @@ export declare class RequestBodyParser extends DebuggableService {
|
|
|
15
15
|
* Define parser.
|
|
16
16
|
*
|
|
17
17
|
* @param mediaType
|
|
18
|
-
* @param
|
|
18
|
+
* @param parserFn
|
|
19
19
|
*/
|
|
20
|
-
defineParser(mediaType: string,
|
|
20
|
+
defineParser(mediaType: string, parserFn: BodyParserFunction): this;
|
|
21
21
|
|
|
22
22
|
/**
|
|
23
23
|
* Has parser.
|
|
@@ -29,10 +29,10 @@ export class RequestBodyParser extends DebuggableService {
|
|
|
29
29
|
* Set parser.
|
|
30
30
|
*
|
|
31
31
|
* @param {string} mediaType
|
|
32
|
-
* @param {Function}
|
|
32
|
+
* @param {Function} parserFn
|
|
33
33
|
* @returns {this}
|
|
34
34
|
*/
|
|
35
|
-
defineParser(mediaType,
|
|
35
|
+
defineParser(mediaType, parserFn) {
|
|
36
36
|
if (!mediaType || typeof mediaType !== 'string') {
|
|
37
37
|
throw new InvalidArgumentError(
|
|
38
38
|
'Parameter "mediaType" must be a non-empty String, ' +
|
|
@@ -40,13 +40,13 @@ export class RequestBodyParser extends DebuggableService {
|
|
|
40
40
|
mediaType,
|
|
41
41
|
);
|
|
42
42
|
}
|
|
43
|
-
if (!
|
|
43
|
+
if (!parserFn || typeof parserFn !== 'function') {
|
|
44
44
|
throw new InvalidArgumentError(
|
|
45
|
-
'Parameter "
|
|
46
|
-
|
|
45
|
+
'Parameter "parserFn" must be a Function, but %v was given.',
|
|
46
|
+
parserFn,
|
|
47
47
|
);
|
|
48
48
|
}
|
|
49
|
-
this._parsers[mediaType.toLowerCase()] =
|
|
49
|
+
this._parsers[mediaType.toLowerCase()] = parserFn;
|
|
50
50
|
return this;
|
|
51
51
|
}
|
|
52
52
|
|
|
@@ -81,14 +81,14 @@ export class RequestBodyParser extends DebuggableService {
|
|
|
81
81
|
mediaType,
|
|
82
82
|
);
|
|
83
83
|
}
|
|
84
|
-
const
|
|
85
|
-
if (!
|
|
84
|
+
const parserFn = this._parsers[mediaType.toLowerCase()];
|
|
85
|
+
if (!parserFn) {
|
|
86
86
|
throw new InvalidArgumentError(
|
|
87
87
|
'Media type %v does not have a parser.',
|
|
88
88
|
mediaType,
|
|
89
89
|
);
|
|
90
90
|
}
|
|
91
|
-
return
|
|
91
|
+
return parserFn;
|
|
92
92
|
}
|
|
93
93
|
|
|
94
94
|
/**
|
|
@@ -147,15 +147,16 @@ export class RequestBodyParser extends DebuggableService {
|
|
|
147
147
|
// если текущий медиа тип исключен
|
|
148
148
|
// настройками, то парсинг пропускается
|
|
149
149
|
const options = this.getService(TrieRouterOptions);
|
|
150
|
-
const
|
|
150
|
+
const mediaTypeLc = mediaType.toLowerCase();
|
|
151
|
+
const isMediaTypeIgnored = options.ignoredMediaTypes.includes(mediaTypeLc);
|
|
151
152
|
if (isMediaTypeIgnored) {
|
|
152
153
|
debug('Media type %v is ignored.', mediaType);
|
|
153
154
|
return;
|
|
154
155
|
}
|
|
155
156
|
// если парсер для текущего медиа типа
|
|
156
157
|
// не определен, то выбрасывается ошибка
|
|
157
|
-
const
|
|
158
|
-
if (!
|
|
158
|
+
const parserFn = this._parsers[mediaTypeLc];
|
|
159
|
+
if (!parserFn) {
|
|
159
160
|
throw createError(
|
|
160
161
|
HttpErrors.UnsupportedMediaType,
|
|
161
162
|
'Media type %v is not supported.',
|
|
@@ -164,7 +165,7 @@ export class RequestBodyParser extends DebuggableService {
|
|
|
164
165
|
}
|
|
165
166
|
// определение максимального количества
|
|
166
167
|
// байт, извлекаемых из тела запроса
|
|
167
|
-
const bodyBytesLimit = options.
|
|
168
|
+
const bodyBytesLimit = options.requestBodyBytesLimit;
|
|
168
169
|
debug('Fetching a request body.');
|
|
169
170
|
debug('Body limit is %v bytes.', bodyBytesLimit);
|
|
170
171
|
// извлечение тела запроса для последующего
|
|
@@ -172,7 +173,7 @@ export class RequestBodyParser extends DebuggableService {
|
|
|
172
173
|
return fetchRequestBody(request, bodyBytesLimit).then(rawBody => {
|
|
173
174
|
if (rawBody != null) {
|
|
174
175
|
debug('Read %v bytes.', Buffer.byteLength(rawBody, 'utf8'));
|
|
175
|
-
return
|
|
176
|
+
return parserFn(rawBody);
|
|
176
177
|
}
|
|
177
178
|
debug('Request body has no content.');
|
|
178
179
|
return rawBody;
|
|
@@ -30,11 +30,11 @@ describe('RequestBodyParser', function () {
|
|
|
30
30
|
throwable('text/plain')();
|
|
31
31
|
});
|
|
32
32
|
|
|
33
|
-
it('should require the parameter "
|
|
33
|
+
it('should require the parameter "parserFn" to be a Function', function () {
|
|
34
34
|
const S = new RequestBodyParser();
|
|
35
35
|
const throwable = v => () => S.defineParser('str', v);
|
|
36
36
|
const error = v =>
|
|
37
|
-
format('Parameter "
|
|
37
|
+
format('Parameter "parserFn" must be a Function, but %s was given.', v);
|
|
38
38
|
expect(throwable('str')).to.throw(error('"str"'));
|
|
39
39
|
expect(throwable('')).to.throw(error('""'));
|
|
40
40
|
expect(throwable(10)).to.throw(error('10'));
|
|
@@ -11,33 +11,19 @@ export type TrieRouterOptionsInput = {
|
|
|
11
11
|
*/
|
|
12
12
|
export declare class TrieRouterOptions {
|
|
13
13
|
/**
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* @param limit
|
|
17
|
-
*/
|
|
18
|
-
setRequestBodyBytesLimit(limit: number): this;
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Get request body bytes limit.
|
|
14
|
+
* Request body bytes limit.
|
|
22
15
|
*/
|
|
23
|
-
|
|
16
|
+
get requestBodyBytesLimit(): number;
|
|
24
17
|
|
|
25
18
|
/**
|
|
26
|
-
*
|
|
27
|
-
*
|
|
28
|
-
* @param mediaType
|
|
19
|
+
* Request body bytes limit.
|
|
29
20
|
*/
|
|
30
|
-
|
|
21
|
+
get ignoredMediaTypes(): string[];
|
|
31
22
|
|
|
32
23
|
/**
|
|
33
|
-
*
|
|
24
|
+
* Constructor.
|
|
34
25
|
*
|
|
35
|
-
* @param
|
|
36
|
-
*/
|
|
37
|
-
hasIgnoredMediaType(mediaType: string): boolean;
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Get ignored media types.
|
|
26
|
+
* @param options
|
|
41
27
|
*/
|
|
42
|
-
|
|
28
|
+
constructor(options?: TrieRouterOptionsInput);
|
|
43
29
|
}
|
|
@@ -11,6 +11,15 @@ export class TrieRouterOptions {
|
|
|
11
11
|
*/
|
|
12
12
|
_requestBodyBytesLimit = 512 * 1024; // 512kb
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* Request body bytes limit.
|
|
16
|
+
*
|
|
17
|
+
* @type {number}
|
|
18
|
+
*/
|
|
19
|
+
get requestBodyBytesLimit() {
|
|
20
|
+
return this._requestBodyBytesLimit;
|
|
21
|
+
}
|
|
22
|
+
|
|
14
23
|
/**
|
|
15
24
|
* Ignored media types.
|
|
16
25
|
*
|
|
@@ -18,6 +27,15 @@ export class TrieRouterOptions {
|
|
|
18
27
|
*/
|
|
19
28
|
_ignoredMediaTypes = [];
|
|
20
29
|
|
|
30
|
+
/**
|
|
31
|
+
* Ignored media types.
|
|
32
|
+
*
|
|
33
|
+
* @type {string[]}
|
|
34
|
+
*/
|
|
35
|
+
get ignoredMediaTypes() {
|
|
36
|
+
return this._ignoredMediaTypes;
|
|
37
|
+
}
|
|
38
|
+
|
|
21
39
|
/**
|
|
22
40
|
* Constructor.
|
|
23
41
|
*
|
|
@@ -66,77 +84,7 @@ export class TrieRouterOptions {
|
|
|
66
84
|
this._ignoredMediaTypes.push(mediaTypeLc);
|
|
67
85
|
}
|
|
68
86
|
});
|
|
87
|
+
Object.freeze(this._ignoredMediaTypes);
|
|
69
88
|
}
|
|
70
89
|
}
|
|
71
|
-
|
|
72
|
-
/**
|
|
73
|
-
* Get request body bytes limit.
|
|
74
|
-
*
|
|
75
|
-
* @param {number} limit
|
|
76
|
-
* @returns {this}
|
|
77
|
-
*/
|
|
78
|
-
setRequestBodyBytesLimit(limit) {
|
|
79
|
-
if (typeof limit !== 'number' || limit < 0) {
|
|
80
|
-
throw new InvalidArgumentError(
|
|
81
|
-
'Parameter "limit" must be a positive Number or 0, but %v was given.',
|
|
82
|
-
limit,
|
|
83
|
-
);
|
|
84
|
-
}
|
|
85
|
-
this._requestBodyBytesLimit = limit;
|
|
86
|
-
return this;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
/**
|
|
90
|
-
* Get request body bytes limit.
|
|
91
|
-
*
|
|
92
|
-
* @returns {number}
|
|
93
|
-
*/
|
|
94
|
-
getRequestBodyBytesLimit() {
|
|
95
|
-
return this._requestBodyBytesLimit;
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
* Get ignored media types.
|
|
100
|
-
*
|
|
101
|
-
* @param {string} mediaType
|
|
102
|
-
* @returns {this}
|
|
103
|
-
*/
|
|
104
|
-
addIgnoredMediaType(mediaType) {
|
|
105
|
-
if (!mediaType || typeof mediaType !== 'string') {
|
|
106
|
-
throw new InvalidArgumentError(
|
|
107
|
-
'Parameter "mediaType" must be a non-empty String, but %v was given.',
|
|
108
|
-
mediaType,
|
|
109
|
-
);
|
|
110
|
-
}
|
|
111
|
-
const mediaTypeLc = mediaType.toLowerCase();
|
|
112
|
-
if (!this._ignoredMediaTypes.includes(mediaTypeLc)) {
|
|
113
|
-
this._ignoredMediaTypes.push(mediaTypeLc);
|
|
114
|
-
}
|
|
115
|
-
return this;
|
|
116
|
-
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Has ignored media type.
|
|
120
|
-
*
|
|
121
|
-
* @param {string} mediaType
|
|
122
|
-
* @returns {boolean}
|
|
123
|
-
*/
|
|
124
|
-
hasIgnoredMediaType(mediaType) {
|
|
125
|
-
if (!mediaType || typeof mediaType !== 'string') {
|
|
126
|
-
throw new InvalidArgumentError(
|
|
127
|
-
'Parameter "mediaType" must be a non-empty String, but %v was given.',
|
|
128
|
-
mediaType,
|
|
129
|
-
);
|
|
130
|
-
}
|
|
131
|
-
return this._ignoredMediaTypes.includes(mediaType.toLowerCase());
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Get ignored media types.
|
|
136
|
-
*
|
|
137
|
-
* @returns {string[]}
|
|
138
|
-
*/
|
|
139
|
-
getIgnoredMediaTypes() {
|
|
140
|
-
return this._ignoredMediaTypes.slice();
|
|
141
|
-
}
|
|
142
90
|
}
|
|
@@ -87,210 +87,31 @@ describe('TrieRouterOptions', function () {
|
|
|
87
87
|
it('should set the option "requestBodyBytesLimit" to the current instance', function () {
|
|
88
88
|
const value = 10;
|
|
89
89
|
const inst = new TrieRouterOptions({requestBodyBytesLimit: value});
|
|
90
|
-
expect(inst.
|
|
90
|
+
expect(inst.requestBodyBytesLimit).to.be.eq(value);
|
|
91
91
|
});
|
|
92
92
|
|
|
93
93
|
it('should add elements of the option "ignoredMediaTypes" to the current instance', function () {
|
|
94
94
|
const value = ['text/plain', 'text/html'];
|
|
95
95
|
const inst = new TrieRouterOptions({ignoredMediaTypes: value});
|
|
96
|
-
expect(inst.
|
|
97
|
-
expect(inst.getIgnoredMediaTypes()).to.include(value[1]);
|
|
96
|
+
expect(inst.ignoredMediaTypes).to.be.eql(['text/plain', 'text/html']);
|
|
98
97
|
});
|
|
99
98
|
|
|
100
99
|
it('should add elements of the option "ignoredMediaTypes" without duplicates', function () {
|
|
101
100
|
const value = ['text/plain', 'text/html', 'text/plain'];
|
|
102
101
|
const inst = new TrieRouterOptions({ignoredMediaTypes: value});
|
|
103
|
-
expect(inst.
|
|
104
|
-
'text/plain',
|
|
105
|
-
'text/html',
|
|
106
|
-
]);
|
|
102
|
+
expect(inst.ignoredMediaTypes).to.be.eql(['text/plain', 'text/html']);
|
|
107
103
|
});
|
|
108
104
|
|
|
109
|
-
it('should
|
|
110
|
-
const inst = new TrieRouterOptions({ignoredMediaTypes: ['TEXT/PLAIN']});
|
|
111
|
-
expect(inst.getIgnoredMediaTypes()).to.be.eql(['text/plain']);
|
|
112
|
-
});
|
|
113
|
-
});
|
|
114
|
-
|
|
115
|
-
describe('setRequestBodyBytesLimit', function () {
|
|
116
|
-
it('should require the parameter "value" to be a positive number or zero', function () {
|
|
117
|
-
const throwable = v => () => {
|
|
118
|
-
const S = new TrieRouterOptions();
|
|
119
|
-
S.setRequestBodyBytesLimit(v);
|
|
120
|
-
};
|
|
121
|
-
const error = s =>
|
|
122
|
-
format(
|
|
123
|
-
'Parameter "limit" must be a positive Number or 0, but %s was given.',
|
|
124
|
-
s,
|
|
125
|
-
);
|
|
126
|
-
expect(throwable('str')).to.throw(error('"str"'));
|
|
127
|
-
expect(throwable('')).to.throw(error('""'));
|
|
128
|
-
expect(throwable(-1)).to.throw(error('-1'));
|
|
129
|
-
expect(throwable(true)).to.throw(error('true'));
|
|
130
|
-
expect(throwable(false)).to.throw(error('false'));
|
|
131
|
-
expect(throwable([])).to.throw(error('Array'));
|
|
132
|
-
expect(throwable({})).to.throw(error('Object'));
|
|
133
|
-
expect(throwable(undefined)).to.throw(error('undefined'));
|
|
134
|
-
expect(throwable(null)).to.throw(error('null'));
|
|
135
|
-
throwable(10)();
|
|
136
|
-
throwable(0)();
|
|
137
|
-
});
|
|
138
|
-
|
|
139
|
-
it('should set the option value to the current instance', function () {
|
|
140
|
-
const S = new TrieRouterOptions();
|
|
141
|
-
const customValue = 10;
|
|
142
|
-
S.setRequestBodyBytesLimit(customValue);
|
|
143
|
-
const res = S.getRequestBodyBytesLimit();
|
|
144
|
-
expect(res).to.be.eq(customValue);
|
|
145
|
-
});
|
|
146
|
-
|
|
147
|
-
it('should return the current instance to allow chaining', function () {
|
|
148
|
-
const S = new TrieRouterOptions();
|
|
149
|
-
const res = S.setRequestBodyBytesLimit(10);
|
|
150
|
-
expect(res).to.be.eq(S);
|
|
151
|
-
});
|
|
152
|
-
});
|
|
153
|
-
|
|
154
|
-
describe('getRequestBodyBytesLimit', function () {
|
|
155
|
-
it('should return a default value when the option is not specified', function () {
|
|
156
|
-
const defaultValue = 512 * 1024; // 512kb
|
|
157
|
-
const inst = new TrieRouterOptions();
|
|
158
|
-
expect(inst.getRequestBodyBytesLimit()).to.be.eq(defaultValue);
|
|
159
|
-
});
|
|
160
|
-
|
|
161
|
-
it('should return a value specified in the constructor', function () {
|
|
162
|
-
const customValue = 10;
|
|
163
|
-
const inst = new TrieRouterOptions({requestBodyBytesLimit: customValue});
|
|
164
|
-
expect(inst.getRequestBodyBytesLimit()).to.be.eq(customValue);
|
|
165
|
-
});
|
|
166
|
-
});
|
|
167
|
-
|
|
168
|
-
describe('addIgnoredMediaType', function () {
|
|
169
|
-
it('should require the parameter "mediaType" to be a non-empty String', function () {
|
|
170
|
-
const throwable = v => () => {
|
|
171
|
-
const S = new TrieRouterOptions();
|
|
172
|
-
S.addIgnoredMediaType(v);
|
|
173
|
-
};
|
|
174
|
-
const error = s =>
|
|
175
|
-
format(
|
|
176
|
-
'Parameter "mediaType" must be a non-empty String, ' +
|
|
177
|
-
'but %s was given.',
|
|
178
|
-
s,
|
|
179
|
-
);
|
|
180
|
-
expect(throwable('')).to.throw(error('""'));
|
|
181
|
-
expect(throwable(10)).to.throw(error('10'));
|
|
182
|
-
expect(throwable(0)).to.throw(error('0'));
|
|
183
|
-
expect(throwable(true)).to.throw(error('true'));
|
|
184
|
-
expect(throwable(false)).to.throw(error('false'));
|
|
185
|
-
expect(throwable([])).to.throw(error('Array'));
|
|
186
|
-
expect(throwable({})).to.throw(error('Object'));
|
|
187
|
-
expect(throwable(undefined)).to.throw(error('undefined'));
|
|
188
|
-
expect(throwable(null)).to.throw(error('null'));
|
|
189
|
-
throwable('text/plain')();
|
|
190
|
-
});
|
|
191
|
-
|
|
192
|
-
it('should add an ignored media type to the current instance', function () {
|
|
193
|
-
const S = new TrieRouterOptions();
|
|
194
|
-
const mediaType = 'text/plain';
|
|
195
|
-
expect(S.hasIgnoredMediaType(mediaType)).to.be.false;
|
|
196
|
-
S.addIgnoredMediaType(mediaType);
|
|
197
|
-
expect(S.hasIgnoredMediaType(mediaType)).to.be.true;
|
|
198
|
-
});
|
|
199
|
-
|
|
200
|
-
it('should return the current instance to allow chaining', function () {
|
|
201
|
-
const S = new TrieRouterOptions();
|
|
202
|
-
const res = S.addIgnoredMediaType('text/plain');
|
|
203
|
-
expect(res).to.be.eq(S);
|
|
204
|
-
});
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
describe('hasIgnoredMediaType', function () {
|
|
208
|
-
it('should require the parameter "mediaType" to be a non-empty String', function () {
|
|
209
|
-
const throwable = v => () => {
|
|
210
|
-
const S = new TrieRouterOptions();
|
|
211
|
-
S.hasIgnoredMediaType(v);
|
|
212
|
-
};
|
|
213
|
-
const error = s =>
|
|
214
|
-
format(
|
|
215
|
-
'Parameter "mediaType" must be a non-empty String, ' +
|
|
216
|
-
'but %s was given.',
|
|
217
|
-
s,
|
|
218
|
-
);
|
|
219
|
-
expect(throwable('')).to.throw(error('""'));
|
|
220
|
-
expect(throwable(10)).to.throw(error('10'));
|
|
221
|
-
expect(throwable(0)).to.throw(error('0'));
|
|
222
|
-
expect(throwable(true)).to.throw(error('true'));
|
|
223
|
-
expect(throwable(false)).to.throw(error('false'));
|
|
224
|
-
expect(throwable([])).to.throw(error('Array'));
|
|
225
|
-
expect(throwable({})).to.throw(error('Object'));
|
|
226
|
-
expect(throwable(undefined)).to.throw(error('undefined'));
|
|
227
|
-
expect(throwable(null)).to.throw(error('null'));
|
|
228
|
-
throwable('text/plain')();
|
|
229
|
-
});
|
|
230
|
-
|
|
231
|
-
it('should return true when the given type is registered', function () {
|
|
232
|
-
const S = new TrieRouterOptions();
|
|
233
|
-
const mediaType = 'text/plain';
|
|
234
|
-
expect(S.hasIgnoredMediaType(mediaType)).to.be.false;
|
|
235
|
-
S.addIgnoredMediaType(mediaType);
|
|
236
|
-
expect(S.hasIgnoredMediaType(mediaType)).to.be.true;
|
|
237
|
-
});
|
|
238
|
-
|
|
239
|
-
it('should lookup the media type in case-insensitive mode', function () {
|
|
240
|
-
const S = new TrieRouterOptions();
|
|
241
|
-
S.addIgnoredMediaType('TeXt/PlAiN');
|
|
242
|
-
expect(S.hasIgnoredMediaType('tExT/pLaIn')).to.be.true;
|
|
243
|
-
});
|
|
244
|
-
|
|
245
|
-
it('should not add duplicates even with different cases', function () {
|
|
246
|
-
const S = new TrieRouterOptions();
|
|
247
|
-
S.addIgnoredMediaType('text/plain');
|
|
248
|
-
S.addIgnoredMediaType('TEXT/PLAIN');
|
|
249
|
-
S.addIgnoredMediaType('text/plain');
|
|
250
|
-
const res = S.getIgnoredMediaTypes();
|
|
251
|
-
expect(res).to.have.lengthOf(1);
|
|
252
|
-
expect(res[0]).to.be.eq('text/plain');
|
|
253
|
-
});
|
|
254
|
-
});
|
|
255
|
-
|
|
256
|
-
describe('getIgnoredMediaTypes', function () {
|
|
257
|
-
it('should return media types specified in the constructor', function () {
|
|
258
|
-
const mediaTypes = ['text/plain', 'text/html'];
|
|
259
|
-
const S = new TrieRouterOptions({ignoredMediaTypes: mediaTypes});
|
|
260
|
-
const res = S.getIgnoredMediaTypes();
|
|
261
|
-
expect(res).to.be.eql(mediaTypes);
|
|
262
|
-
});
|
|
263
|
-
|
|
264
|
-
it('should return media types added by the "addIgnoredMediaType" method', function () {
|
|
265
|
-
const mediaTypes = ['text/plain', 'text/html'];
|
|
266
|
-
const S = new TrieRouterOptions();
|
|
267
|
-
S.addIgnoredMediaType(mediaTypes[0]);
|
|
268
|
-
S.addIgnoredMediaType(mediaTypes[1]);
|
|
269
|
-
const res = S.getIgnoredMediaTypes();
|
|
270
|
-
expect(res).to.be.eql(mediaTypes);
|
|
271
|
-
});
|
|
272
|
-
|
|
273
|
-
it('should combine media types specified by the constructor and the "addIgnoredMediaType" method', function () {
|
|
105
|
+
it('should freeze the option "ignoredMediaTypes" to prevent mutations', function () {
|
|
274
106
|
const mediaTypes = ['text/plain', 'text/html'];
|
|
275
|
-
const
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
expect(res).to.be.eql(mediaTypes);
|
|
107
|
+
const inst = new TrieRouterOptions({ignoredMediaTypes: mediaTypes});
|
|
108
|
+
const res = inst.ignoredMediaTypes;
|
|
109
|
+
expect(Object.isFrozen(res)).to.be.true;
|
|
279
110
|
});
|
|
280
111
|
|
|
281
|
-
it('should
|
|
282
|
-
const
|
|
283
|
-
|
|
284
|
-
S.addIgnoredMediaType(mediaTypes[0]);
|
|
285
|
-
S.addIgnoredMediaType(mediaTypes[1]);
|
|
286
|
-
const res1 = S.getIgnoredMediaTypes();
|
|
287
|
-
const res2 = S.getIgnoredMediaTypes();
|
|
288
|
-
expect(res1).to.be.eql(mediaTypes);
|
|
289
|
-
expect(res2).to.be.eql(mediaTypes);
|
|
290
|
-
expect(res1).to.be.not.eq(res2);
|
|
291
|
-
res1[0] = '123';
|
|
292
|
-
const res3 = S.getIgnoredMediaTypes();
|
|
293
|
-
expect(res3).to.be.eql(mediaTypes);
|
|
112
|
+
it('should convert ignored media types to lower case', function () {
|
|
113
|
+
const inst = new TrieRouterOptions({ignoredMediaTypes: ['TEXT/PLAIN']});
|
|
114
|
+
expect(inst.ignoredMediaTypes).to.be.eql(['text/plain']);
|
|
294
115
|
});
|
|
295
116
|
});
|
|
296
117
|
});
|
package/src/trie-router.spec.js
CHANGED
|
@@ -28,7 +28,7 @@ describe('TrieRouter', function () {
|
|
|
28
28
|
const optionsInput = {requestBodyBytesLimit: 10};
|
|
29
29
|
const router = new TrieRouter(optionsInput);
|
|
30
30
|
const options = router.getService(TrieRouterOptions);
|
|
31
|
-
expect(options.
|
|
31
|
+
expect(options.requestBodyBytesLimit).to.be.eq(10);
|
|
32
32
|
});
|
|
33
33
|
|
|
34
34
|
it('should use the service container and the router options from parameters', function () {
|
|
@@ -37,7 +37,7 @@ describe('TrieRouter', function () {
|
|
|
37
37
|
const router = new TrieRouter(container, optionsInput);
|
|
38
38
|
const options = router.getService(TrieRouterOptions);
|
|
39
39
|
expect(router.container).to.be.eq(container);
|
|
40
|
-
expect(options.
|
|
40
|
+
expect(options.requestBodyBytesLimit).to.be.eq(10);
|
|
41
41
|
});
|
|
42
42
|
});
|
|
43
43
|
|
|
@@ -79,6 +79,22 @@ describe('createResponseMock', function () {
|
|
|
79
79
|
expect(ret).to.be.eq(res);
|
|
80
80
|
expect(res._headers['num']).to.be.eq('10');
|
|
81
81
|
});
|
|
82
|
+
|
|
83
|
+
it('should not stringify an array value', function () {
|
|
84
|
+
const res = createResponseMock();
|
|
85
|
+
expect(res._headers['key']).to.be.eq(undefined);
|
|
86
|
+
const ret = res.setHeader('key', ['foo', 'bar']);
|
|
87
|
+
expect(ret).to.be.eq(res);
|
|
88
|
+
expect(res._headers['key']).to.be.eql(['foo', 'bar']);
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('should stringify an array elements', function () {
|
|
92
|
+
const res = createResponseMock();
|
|
93
|
+
expect(res._headers['key']).to.be.eq(undefined);
|
|
94
|
+
const ret = res.setHeader('key', [1, 2]);
|
|
95
|
+
expect(ret).to.be.eq(res);
|
|
96
|
+
expect(res._headers['key']).to.be.eql(['1', '2']);
|
|
97
|
+
});
|
|
82
98
|
});
|
|
83
99
|
|
|
84
100
|
describe('getHeader', function () {
|