@e22m4u/js-trie-router 0.5.13 → 0.6.0
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/dist/cjs/index.cjs +41 -30
- package/package.json +6 -6
- package/src/branch/merge-router-branch-definition.spec.js +18 -4
- package/src/branch/merge-router-branch-definitions.js +6 -3
- package/src/branch/router-branch.js +1 -2
- package/src/branch/router-branch.spec.js +10 -10
- package/src/branch/validate-router-branch-definition.js +12 -6
- package/src/branch/validate-router-branch-definition.spec.js +16 -7
- package/src/hooks/router-hook-registry.d.ts +2 -0
- package/src/route/route.spec.js +18 -7
- package/src/route/validate-route-definition.js +8 -2
- package/src/route/validate-route-definition.spec.js +18 -4
- package/src/route-registry.js +1 -1
- package/src/route-registry.spec.js +7 -5
- package/src/senders/data-sender.js +29 -15
- package/src/senders/data-sender.spec.js +184 -20
- package/src/trie-router.js +1 -1
- package/src/trie-router.spec.js +1 -1
- package/src/utils/create-request-mock.d.ts +1 -1
- package/src/utils/create-request-mock.js +1 -1
- package/src/utils/create-route-mock.spec.js +2 -2
- package/src/utils/index.d.ts +0 -1
- package/src/utils/index.js +0 -1
- package/src/utils/normalize-path.d.ts +0 -12
- package/src/utils/normalize-path.js +0 -22
- package/src/utils/normalize-path.spec.js +0 -56
package/dist/cjs/index.cjs
CHANGED
|
@@ -65,7 +65,6 @@ __export(index_exports, {
|
|
|
65
65
|
isResponseSent: () => isResponseSent,
|
|
66
66
|
isWritableStream: () => isWritableStream,
|
|
67
67
|
mergeDeep: () => mergeDeep,
|
|
68
|
-
normalizePath: () => normalizePath,
|
|
69
68
|
parseContentType: () => parseContentType,
|
|
70
69
|
parseCookieString: () => parseCookieString,
|
|
71
70
|
parseJsonBody: () => parseJsonBody,
|
|
@@ -207,16 +206,6 @@ function toPascalCase(input) {
|
|
|
207
206
|
}
|
|
208
207
|
__name(toPascalCase, "toPascalCase");
|
|
209
208
|
|
|
210
|
-
// src/utils/normalize-path.js
|
|
211
|
-
function normalizePath(value, noStartingSlash = false) {
|
|
212
|
-
if (typeof value !== "string") {
|
|
213
|
-
return "/";
|
|
214
|
-
}
|
|
215
|
-
const res = value.trim().replace(/\/+/g, "/").replace(/(^\/|\/$)/g, "");
|
|
216
|
-
return noStartingSlash ? res : "/" + res;
|
|
217
|
-
}
|
|
218
|
-
__name(normalizePath, "normalizePath");
|
|
219
|
-
|
|
220
209
|
// src/utils/is-response-sent.js
|
|
221
210
|
var import_js_format3 = require("@e22m4u/js-format");
|
|
222
211
|
function isResponseSent(response) {
|
|
@@ -1006,9 +995,15 @@ function validateRouteDefinition(routeDef) {
|
|
|
1006
995
|
routeDef.method
|
|
1007
996
|
);
|
|
1008
997
|
}
|
|
1009
|
-
if (
|
|
998
|
+
if (typeof routeDef.path !== "string") {
|
|
999
|
+
throw new import_js_format12.InvalidArgumentError(
|
|
1000
|
+
'Option "path" must be a String, but %v was given.',
|
|
1001
|
+
routeDef.path
|
|
1002
|
+
);
|
|
1003
|
+
}
|
|
1004
|
+
if (!routeDef.path.startsWith("/")) {
|
|
1010
1005
|
throw new import_js_format12.InvalidArgumentError(
|
|
1011
|
-
'Option "path" must
|
|
1006
|
+
'Option "path" must start with "/", but %v was given.',
|
|
1012
1007
|
routeDef.path
|
|
1013
1008
|
);
|
|
1014
1009
|
}
|
|
@@ -1508,7 +1503,7 @@ var _RouteRegistry = class _RouteRegistry extends DebuggableService {
|
|
|
1508
1503
|
if (onDefineRouteHooks.length) {
|
|
1509
1504
|
debug('Invoking %v "onDefineRoute" hook(s).', onDefineRouteHooks.length);
|
|
1510
1505
|
for (const hook of onDefineRouteHooks) {
|
|
1511
|
-
const hookResult = hook({ ...routeDef });
|
|
1506
|
+
const hookResult = hook({ ...routeDef }, this.container);
|
|
1512
1507
|
if (hookResult !== void 0 && !(hookResult !== null && typeof hookResult === "object" && !Array.isArray(hookResult))) {
|
|
1513
1508
|
throw new import_js_format16.InvalidArgumentError(
|
|
1514
1509
|
'Hook "onDefineRoute" must return an Object or undefined, but %v was given.',
|
|
@@ -1774,16 +1769,22 @@ function validateRouterBranchDefinition(branchDef) {
|
|
|
1774
1769
|
branchDef.method
|
|
1775
1770
|
);
|
|
1776
1771
|
}
|
|
1777
|
-
if (
|
|
1772
|
+
if (branchDef.handler !== void 0) {
|
|
1773
|
+
throw new import_js_format18.InvalidArgumentError(
|
|
1774
|
+
'Option "handler" is not supported for the router branch, but %v was given.',
|
|
1775
|
+
branchDef.handler
|
|
1776
|
+
);
|
|
1777
|
+
}
|
|
1778
|
+
if (typeof branchDef.path !== "string") {
|
|
1778
1779
|
throw new import_js_format18.InvalidArgumentError(
|
|
1779
|
-
'Option "path" must be a
|
|
1780
|
+
'Option "path" must be a String, but %v was given.',
|
|
1780
1781
|
branchDef.path
|
|
1781
1782
|
);
|
|
1782
1783
|
}
|
|
1783
|
-
if (branchDef.
|
|
1784
|
+
if (!branchDef.path.startsWith("/")) {
|
|
1784
1785
|
throw new import_js_format18.InvalidArgumentError(
|
|
1785
|
-
'Option "
|
|
1786
|
-
branchDef.
|
|
1786
|
+
'Option "path" must start with "/", but %v was given.',
|
|
1787
|
+
branchDef.path
|
|
1787
1788
|
);
|
|
1788
1789
|
}
|
|
1789
1790
|
if (branchDef.preHandler !== void 0) {
|
|
@@ -1836,8 +1837,11 @@ function mergeRouterBranchDefinitions(firstDef, secondDef) {
|
|
|
1836
1837
|
validateRouterBranchDefinition(firstDef);
|
|
1837
1838
|
validateRouterBranchDefinition(secondDef);
|
|
1838
1839
|
const mergedDef = {};
|
|
1839
|
-
|
|
1840
|
-
|
|
1840
|
+
let fullPath = "/" + (firstDef.path || "");
|
|
1841
|
+
if (secondDef.path && secondDef.path !== "/") {
|
|
1842
|
+
fullPath += "/" + secondDef.path;
|
|
1843
|
+
}
|
|
1844
|
+
mergedDef.path = fullPath.replace(/\/+/g, "/");
|
|
1841
1845
|
if (firstDef.preHandler || secondDef.preHandler) {
|
|
1842
1846
|
mergedDef.preHandler = [firstDef.preHandler, secondDef.preHandler].flat().filter(Boolean);
|
|
1843
1847
|
}
|
|
@@ -1945,7 +1949,7 @@ var _RouterBranch = class _RouterBranch extends DebuggableService {
|
|
|
1945
1949
|
validateRouterBranchDefinition(branchDef);
|
|
1946
1950
|
this._definition = cloneDeep(branchDef);
|
|
1947
1951
|
}
|
|
1948
|
-
this.ctorDebug("Created a branch %v.",
|
|
1952
|
+
this.ctorDebug("Created a branch %v.", branchDef.path);
|
|
1949
1953
|
this.ctorDebug("Branch path is %v.", this._definition.path);
|
|
1950
1954
|
}
|
|
1951
1955
|
/**
|
|
@@ -2001,21 +2005,27 @@ var _DataSender = class _DataSender extends DebuggableService {
|
|
|
2001
2005
|
return;
|
|
2002
2006
|
}
|
|
2003
2007
|
if (isReadableStream(data)) {
|
|
2004
|
-
response.
|
|
2008
|
+
if (!response.getHeader("content-type")) {
|
|
2009
|
+
response.setHeader("content-type", "application/octet-stream");
|
|
2010
|
+
}
|
|
2005
2011
|
data.pipe(response);
|
|
2006
2012
|
debug("Sending response with a Stream.");
|
|
2007
2013
|
return;
|
|
2008
2014
|
}
|
|
2009
2015
|
let debugMsg;
|
|
2010
2016
|
switch (typeof data) {
|
|
2011
|
-
case "object":
|
|
2012
|
-
case "boolean":
|
|
2013
2017
|
case "number":
|
|
2018
|
+
case "boolean":
|
|
2019
|
+
case "object":
|
|
2014
2020
|
if (Buffer.isBuffer(data)) {
|
|
2015
|
-
response.
|
|
2021
|
+
if (!response.getHeader("content-type")) {
|
|
2022
|
+
response.setHeader("content-type", "application/octet-stream");
|
|
2023
|
+
}
|
|
2016
2024
|
debugMsg = "Buffer has been sent as binary data.";
|
|
2017
2025
|
} else {
|
|
2018
|
-
response.
|
|
2026
|
+
if (!response.getHeader("content-type")) {
|
|
2027
|
+
response.setHeader("content-type", "application/json");
|
|
2028
|
+
}
|
|
2019
2029
|
debugMsg = (0, import_js_format20.format)(
|
|
2020
2030
|
"%v has been sent as JSON.",
|
|
2021
2031
|
toPascalCase(typeof data)
|
|
@@ -2024,7 +2034,9 @@ var _DataSender = class _DataSender extends DebuggableService {
|
|
|
2024
2034
|
}
|
|
2025
2035
|
break;
|
|
2026
2036
|
default:
|
|
2027
|
-
response.
|
|
2037
|
+
if (!response.getHeader("content-type")) {
|
|
2038
|
+
response.setHeader("content-type", "text/plain");
|
|
2039
|
+
}
|
|
2028
2040
|
debugMsg = "Response data has been sent as plain text.";
|
|
2029
2041
|
data = String(data);
|
|
2030
2042
|
break;
|
|
@@ -2130,7 +2142,7 @@ var _TrieRouter = class _TrieRouter extends DebuggableService {
|
|
|
2130
2142
|
* ```
|
|
2131
2143
|
* const router = new TrieRouter();
|
|
2132
2144
|
* router.defineRoute({
|
|
2133
|
-
* method: HttpMethod.GET,
|
|
2145
|
+
* method: HttpMethod.GET, // Request method.
|
|
2134
2146
|
* path: '/', // Path template.
|
|
2135
2147
|
* handler: ctx => 'Hello world!', // Request handler.
|
|
2136
2148
|
* });
|
|
@@ -2330,7 +2342,6 @@ var TrieRouter = _TrieRouter;
|
|
|
2330
2342
|
isResponseSent,
|
|
2331
2343
|
isWritableStream,
|
|
2332
2344
|
mergeDeep,
|
|
2333
|
-
normalizePath,
|
|
2334
2345
|
parseContentType,
|
|
2335
2346
|
parseCookieString,
|
|
2336
2347
|
parseJsonBody,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@e22m4u/js-trie-router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "HTTP маршрутизатор для Node.js на основе префиксного дерева",
|
|
5
5
|
"author": "Mikhail Evstropov <e22m4u@yandex.ru>",
|
|
6
6
|
"license": "MIT",
|
|
@@ -39,8 +39,8 @@
|
|
|
39
39
|
},
|
|
40
40
|
"dependencies": {
|
|
41
41
|
"@e22m4u/js-debug": "~0.4.1",
|
|
42
|
-
"@e22m4u/js-format": "~0.
|
|
43
|
-
"@e22m4u/js-path-trie": "~0.
|
|
42
|
+
"@e22m4u/js-format": "~0.4.0",
|
|
43
|
+
"@e22m4u/js-path-trie": "~0.2.0",
|
|
44
44
|
"@e22m4u/js-service": "~0.5.1",
|
|
45
45
|
"debug": "~4.4.3",
|
|
46
46
|
"http-errors": "~2.0.1",
|
|
@@ -56,18 +56,18 @@
|
|
|
56
56
|
"c8": "~10.1.3",
|
|
57
57
|
"chai": "~6.2.2",
|
|
58
58
|
"chai-as-promised": "~8.0.2",
|
|
59
|
-
"esbuild": "~0.27.
|
|
59
|
+
"esbuild": "~0.27.3",
|
|
60
60
|
"eslint": "~9.39.2",
|
|
61
61
|
"eslint-config-prettier": "~10.1.8",
|
|
62
62
|
"eslint-plugin-chai-expect": "~3.1.0",
|
|
63
63
|
"eslint-plugin-import": "~2.32.0",
|
|
64
|
-
"eslint-plugin-jsdoc": "~62.
|
|
64
|
+
"eslint-plugin-jsdoc": "~62.6.0",
|
|
65
65
|
"eslint-plugin-mocha": "~11.2.0",
|
|
66
66
|
"globals": "~17.3.0",
|
|
67
67
|
"husky": "~9.1.7",
|
|
68
68
|
"mocha": "~11.7.5",
|
|
69
69
|
"prettier": "~3.8.1",
|
|
70
|
-
"rimraf": "~6.1.
|
|
70
|
+
"rimraf": "~6.1.3",
|
|
71
71
|
"typescript": "~5.9.3"
|
|
72
72
|
}
|
|
73
73
|
}
|
|
@@ -3,7 +3,7 @@ import {ROOT_PATH} from '../constants.js';
|
|
|
3
3
|
import {mergeRouterBranchDefinitions} from './merge-router-branch-definitions.js';
|
|
4
4
|
|
|
5
5
|
describe('mergeRouterBranchDefinitions', function () {
|
|
6
|
-
it('should
|
|
6
|
+
it('should require the "firstDef" parameter to be an Object', function () {
|
|
7
7
|
const throwable = () =>
|
|
8
8
|
mergeRouterBranchDefinitions(123, {path: ROOT_PATH});
|
|
9
9
|
expect(throwable).to.throw(
|
|
@@ -11,7 +11,7 @@ describe('mergeRouterBranchDefinitions', function () {
|
|
|
11
11
|
);
|
|
12
12
|
});
|
|
13
13
|
|
|
14
|
-
it('should
|
|
14
|
+
it('should require the "secondDef" parameter to be an Object', function () {
|
|
15
15
|
const throwable = () =>
|
|
16
16
|
mergeRouterBranchDefinitions({path: ROOT_PATH}, 123);
|
|
17
17
|
expect(throwable).to.throw(
|
|
@@ -20,11 +20,25 @@ describe('mergeRouterBranchDefinitions', function () {
|
|
|
20
20
|
});
|
|
21
21
|
|
|
22
22
|
it('should concatenate the "path" option with the correct order', function () {
|
|
23
|
-
const res = mergeRouterBranchDefinitions({path: 'foo'}, {path: 'bar'});
|
|
23
|
+
const res = mergeRouterBranchDefinitions({path: '/foo'}, {path: '/bar'});
|
|
24
24
|
expect(res).to.be.eql({path: '/foo/bar'});
|
|
25
25
|
});
|
|
26
26
|
|
|
27
|
-
it('should
|
|
27
|
+
it('should keep a trailing slash from the first definition', function () {
|
|
28
|
+
const res1 = mergeRouterBranchDefinitions({path: '/foo/'}, {path: '/'});
|
|
29
|
+
expect(res1).to.be.eql({path: '/foo/'});
|
|
30
|
+
const res2 = mergeRouterBranchDefinitions({path: '/foo/'}, {path: '/bar'});
|
|
31
|
+
expect(res2).to.be.eql({path: '/foo/bar'});
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
it('should keep a trailing slash from the second definition', function () {
|
|
35
|
+
const res1 = mergeRouterBranchDefinitions({path: '/'}, {path: '/foo/'});
|
|
36
|
+
expect(res1).to.be.eql({path: '/foo/'});
|
|
37
|
+
const res2 = mergeRouterBranchDefinitions({path: '/foo'}, {path: '/bar/'});
|
|
38
|
+
expect(res2).to.be.eql({path: '/foo/bar/'});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
it('should not duplicate slashes from the "path" option', function () {
|
|
28
42
|
const res = mergeRouterBranchDefinitions({path: '/'}, {path: '/'});
|
|
29
43
|
expect(res).to.be.eql({path: '/'});
|
|
30
44
|
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {mergeDeep
|
|
1
|
+
import {mergeDeep} from '../utils/index.js';
|
|
2
2
|
import {validateRouterBranchDefinition} from './validate-router-branch-definition.js';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -13,8 +13,11 @@ export function mergeRouterBranchDefinitions(firstDef, secondDef) {
|
|
|
13
13
|
validateRouterBranchDefinition(secondDef);
|
|
14
14
|
const mergedDef = {};
|
|
15
15
|
// path
|
|
16
|
-
|
|
17
|
-
|
|
16
|
+
let fullPath = '/' + (firstDef.path || '');
|
|
17
|
+
if (secondDef.path && secondDef.path !== '/') {
|
|
18
|
+
fullPath += '/' + secondDef.path;
|
|
19
|
+
}
|
|
20
|
+
mergedDef.path = fullPath.replace(/\/+/g, '/');
|
|
18
21
|
// pre-handler
|
|
19
22
|
if (firstDef.preHandler || secondDef.preHandler) {
|
|
20
23
|
mergedDef.preHandler = [firstDef.preHandler, secondDef.preHandler]
|
|
@@ -2,7 +2,6 @@ import {Route} from '../route/index.js';
|
|
|
2
2
|
import {cloneDeep} from '../utils/index.js';
|
|
3
3
|
import {TrieRouter} from '../trie-router.js';
|
|
4
4
|
import {InvalidArgumentError} from '@e22m4u/js-format';
|
|
5
|
-
import {normalizePath} from '../utils/normalize-path.js';
|
|
6
5
|
import {DebuggableService} from '../debuggable-service.js';
|
|
7
6
|
import {validateRouteDefinition} from '../route/validate-route-definition.js';
|
|
8
7
|
import {mergeRouterBranchDefinitions} from './merge-router-branch-definitions.js';
|
|
@@ -120,7 +119,7 @@ export class RouterBranch extends DebuggableService {
|
|
|
120
119
|
validateRouterBranchDefinition(branchDef);
|
|
121
120
|
this._definition = cloneDeep(branchDef);
|
|
122
121
|
}
|
|
123
|
-
this.ctorDebug('Created a branch %v.',
|
|
122
|
+
this.ctorDebug('Created a branch %v.', branchDef.path);
|
|
124
123
|
this.ctorDebug('Branch path is %v.', this._definition.path);
|
|
125
124
|
}
|
|
126
125
|
|
|
@@ -14,8 +14,8 @@ describe('RouterBranch', function () {
|
|
|
14
14
|
|
|
15
15
|
it('should merge a parent definition with a given definition', function () {
|
|
16
16
|
const router = new TrieRouter();
|
|
17
|
-
const parent = router.createBranch({path: 'foo'});
|
|
18
|
-
const S = new RouterBranch(router, {path: 'bar'}, parent);
|
|
17
|
+
const parent = router.createBranch({path: '/foo'});
|
|
18
|
+
const S = new RouterBranch(router, {path: '/bar'}, parent);
|
|
19
19
|
expect(S.getDefinition().path).to.be.eq('/foo/bar');
|
|
20
20
|
});
|
|
21
21
|
});
|
|
@@ -70,10 +70,10 @@ describe('RouterBranch', function () {
|
|
|
70
70
|
describe('defineRoute', function () {
|
|
71
71
|
it('should return a Route instance', function () {
|
|
72
72
|
const router = new TrieRouter();
|
|
73
|
-
const S = new RouterBranch(router, {path: 'foo'});
|
|
73
|
+
const S = new RouterBranch(router, {path: '/foo'});
|
|
74
74
|
const res = S.defineRoute({
|
|
75
75
|
method: HttpMethod.GET,
|
|
76
|
-
path: 'bar',
|
|
76
|
+
path: '/bar',
|
|
77
77
|
handler: () => undefined,
|
|
78
78
|
});
|
|
79
79
|
expect(res).to.be.instanceOf(Route);
|
|
@@ -81,10 +81,10 @@ describe('RouterBranch', function () {
|
|
|
81
81
|
|
|
82
82
|
it('should combine a branch path with a route path', function () {
|
|
83
83
|
const router = new TrieRouter();
|
|
84
|
-
const S = new RouterBranch(router, {path: 'foo'});
|
|
84
|
+
const S = new RouterBranch(router, {path: '/foo'});
|
|
85
85
|
const res = S.defineRoute({
|
|
86
86
|
method: HttpMethod.GET,
|
|
87
|
-
path: 'bar',
|
|
87
|
+
path: '/bar',
|
|
88
88
|
handler: () => undefined,
|
|
89
89
|
});
|
|
90
90
|
expect(res.path).to.be.eq('/foo/bar');
|
|
@@ -94,15 +94,15 @@ describe('RouterBranch', function () {
|
|
|
94
94
|
describe('createBranch', function () {
|
|
95
95
|
it('should return a RouterBranch instance', function () {
|
|
96
96
|
const router = new TrieRouter();
|
|
97
|
-
const S = new RouterBranch(router, {path: 'foo'});
|
|
98
|
-
const res = S.createBranch({path: 'bar'});
|
|
97
|
+
const S = new RouterBranch(router, {path: '/foo'});
|
|
98
|
+
const res = S.createBranch({path: '/bar'});
|
|
99
99
|
expect(res).to.be.instanceOf(RouterBranch);
|
|
100
100
|
});
|
|
101
101
|
|
|
102
102
|
it('should combine a current path with a new path', function () {
|
|
103
103
|
const router = new TrieRouter();
|
|
104
|
-
const S = new RouterBranch(router, {path: 'foo'});
|
|
105
|
-
const res = S.createBranch({path: 'bar'});
|
|
104
|
+
const S = new RouterBranch(router, {path: '/foo'});
|
|
105
|
+
const res = S.createBranch({path: '/bar'});
|
|
106
106
|
expect(res.getDefinition().path).to.be.eq('/foo/bar');
|
|
107
107
|
});
|
|
108
108
|
});
|
|
@@ -19,12 +19,6 @@ export function validateRouterBranchDefinition(branchDef) {
|
|
|
19
19
|
branchDef.method,
|
|
20
20
|
);
|
|
21
21
|
}
|
|
22
|
-
if (!branchDef.path || typeof branchDef.path !== 'string') {
|
|
23
|
-
throw new InvalidArgumentError(
|
|
24
|
-
'Option "path" must be a non-empty String, but %v was given.',
|
|
25
|
-
branchDef.path,
|
|
26
|
-
);
|
|
27
|
-
}
|
|
28
22
|
if (branchDef.handler !== undefined) {
|
|
29
23
|
throw new InvalidArgumentError(
|
|
30
24
|
'Option "handler" is not supported for the router branch, ' +
|
|
@@ -32,6 +26,18 @@ export function validateRouterBranchDefinition(branchDef) {
|
|
|
32
26
|
branchDef.handler,
|
|
33
27
|
);
|
|
34
28
|
}
|
|
29
|
+
if (typeof branchDef.path !== 'string') {
|
|
30
|
+
throw new InvalidArgumentError(
|
|
31
|
+
'Option "path" must be a String, but %v was given.',
|
|
32
|
+
branchDef.path,
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
if (!branchDef.path.startsWith('/')) {
|
|
36
|
+
throw new InvalidArgumentError(
|
|
37
|
+
'Option "path" must start with "/", but %v was given.',
|
|
38
|
+
branchDef.path,
|
|
39
|
+
);
|
|
40
|
+
}
|
|
35
41
|
if (branchDef.preHandler !== undefined) {
|
|
36
42
|
if (Array.isArray(branchDef.preHandler)) {
|
|
37
43
|
branchDef.preHandler.forEach(preHandler => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import {expect} from 'chai';
|
|
2
2
|
import {format} from '@e22m4u/js-format';
|
|
3
|
-
import {validateRouterBranchDefinition} from './validate-router-branch-definition.js';
|
|
4
3
|
import {ROOT_PATH} from '../constants.js';
|
|
4
|
+
import {validateRouterBranchDefinition} from './validate-router-branch-definition.js';
|
|
5
5
|
|
|
6
6
|
describe('validateRouterBranchDefinition', function () {
|
|
7
7
|
it('should require the "routeDef" parameter to be an Object', function () {
|
|
@@ -33,11 +33,10 @@ describe('validateRouterBranchDefinition', function () {
|
|
|
33
33
|
);
|
|
34
34
|
});
|
|
35
35
|
|
|
36
|
-
it('should require the "path" option to be a
|
|
36
|
+
it('should require the "path" option to be a String', function () {
|
|
37
37
|
const throwable = v => () => validateRouterBranchDefinition({path: v});
|
|
38
38
|
const error = v =>
|
|
39
|
-
format('Option "path" must be a
|
|
40
|
-
expect(throwable('')).to.throw(error('""'));
|
|
39
|
+
format('Option "path" must be a String, but %s was given.', v);
|
|
41
40
|
expect(throwable(10)).to.throw(error('10'));
|
|
42
41
|
expect(throwable(0)).to.throw(error('0'));
|
|
43
42
|
expect(throwable(true)).to.throw(error('true'));
|
|
@@ -47,7 +46,17 @@ describe('validateRouterBranchDefinition', function () {
|
|
|
47
46
|
expect(throwable(undefined)).to.throw(error('undefined'));
|
|
48
47
|
expect(throwable(null)).to.throw(error('null'));
|
|
49
48
|
expect(throwable(() => undefined)).to.throw(error('Function'));
|
|
50
|
-
throwable('
|
|
49
|
+
throwable('/path')();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
it('should require the "path" option to start with a forward slash', function () {
|
|
53
|
+
const throwable = v => () => validateRouterBranchDefinition({path: v});
|
|
54
|
+
const error = s =>
|
|
55
|
+
format('Option "path" must start with "/", but %s was given.', s);
|
|
56
|
+
expect(throwable('path')).to.throw(error('"path"'));
|
|
57
|
+
expect(throwable('')).to.throw(error('""'));
|
|
58
|
+
throwable('/path')();
|
|
59
|
+
throwable('/')();
|
|
51
60
|
});
|
|
52
61
|
|
|
53
62
|
it('should throw an error if the "handler" option is provided', function () {
|
|
@@ -62,7 +71,7 @@ describe('validateRouterBranchDefinition', function () {
|
|
|
62
71
|
);
|
|
63
72
|
});
|
|
64
73
|
|
|
65
|
-
it('should require the "preHandler" option to be a Function or an Array
|
|
74
|
+
it('should require the "preHandler" option to be a Function or an Array', function () {
|
|
66
75
|
const throwable = v => () =>
|
|
67
76
|
validateRouterBranchDefinition({
|
|
68
77
|
path: ROOT_PATH,
|
|
@@ -108,7 +117,7 @@ describe('validateRouterBranchDefinition', function () {
|
|
|
108
117
|
throwable(() => undefined)();
|
|
109
118
|
});
|
|
110
119
|
|
|
111
|
-
it('should require the "postHandler" option to be a Function or an Array
|
|
120
|
+
it('should require the "postHandler" option to be a Function or an Array', function () {
|
|
112
121
|
const throwable = v => () =>
|
|
113
122
|
validateRouterBranchDefinition({
|
|
114
123
|
path: ROOT_PATH,
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import {Callable} from '../types.js';
|
|
2
2
|
import {RouteDefinition} from '../route/index.js';
|
|
3
|
+
import {ServiceContainer} from '@e22m4u/js-service';
|
|
3
4
|
import {RequestContext} from '../request-context.js';
|
|
4
5
|
import {DebuggableService} from '../debuggable-service.js';
|
|
5
6
|
|
|
@@ -43,6 +44,7 @@ export type PostHandlerHook = (ctx: RequestContext, data: unknown) => unknown;
|
|
|
43
44
|
*/
|
|
44
45
|
export type OnDefineRouteHook = (
|
|
45
46
|
routeDef: RouteDefinition,
|
|
47
|
+
container: ServiceContainer,
|
|
46
48
|
) => RouteDefinition | undefined;
|
|
47
49
|
|
|
48
50
|
/**
|
package/src/route/route.spec.js
CHANGED
|
@@ -63,11 +63,7 @@ describe('Route', function () {
|
|
|
63
63
|
handler: () => 'Ok',
|
|
64
64
|
});
|
|
65
65
|
const error = v =>
|
|
66
|
-
format(
|
|
67
|
-
'Option "path" must be a non-empty String, but %s was given.',
|
|
68
|
-
v,
|
|
69
|
-
);
|
|
70
|
-
expect(throwable('')).to.throw(error('""'));
|
|
66
|
+
format('Option "path" must be a String, but %s was given.', v);
|
|
71
67
|
expect(throwable(10)).to.throw(error('10'));
|
|
72
68
|
expect(throwable(0)).to.throw(error('0'));
|
|
73
69
|
expect(throwable(true)).to.throw(error('true'));
|
|
@@ -77,7 +73,22 @@ describe('Route', function () {
|
|
|
77
73
|
expect(throwable(undefined)).to.throw(error('undefined'));
|
|
78
74
|
expect(throwable(null)).to.throw(error('null'));
|
|
79
75
|
expect(throwable(() => undefined)).to.throw(error('Function'));
|
|
80
|
-
throwable('
|
|
76
|
+
throwable('/path')();
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('should require the "path" option to start with a forward slash', function () {
|
|
80
|
+
const throwable = v => () =>
|
|
81
|
+
new Route({
|
|
82
|
+
method: HttpMethod.GET,
|
|
83
|
+
path: v,
|
|
84
|
+
handler: () => 'Ok',
|
|
85
|
+
});
|
|
86
|
+
const error = s =>
|
|
87
|
+
format('Option "path" must start with "/", but %s was given.', s);
|
|
88
|
+
expect(throwable('path')).to.throw(error('"path"'));
|
|
89
|
+
expect(throwable('')).to.throw(error('""'));
|
|
90
|
+
throwable('/path')();
|
|
91
|
+
throwable('/')();
|
|
81
92
|
});
|
|
82
93
|
|
|
83
94
|
it('should require the "handler" option to be a Function', function () {
|
|
@@ -351,7 +362,7 @@ describe('Route', function () {
|
|
|
351
362
|
|
|
352
363
|
describe('path', function () {
|
|
353
364
|
it('should return a value of the "path" option', function () {
|
|
354
|
-
const value = 'myPath';
|
|
365
|
+
const value = '/myPath';
|
|
355
366
|
const route = new Route({
|
|
356
367
|
method: HttpMethod.GET,
|
|
357
368
|
path: value,
|
|
@@ -18,9 +18,15 @@ export function validateRouteDefinition(routeDef) {
|
|
|
18
18
|
routeDef.method,
|
|
19
19
|
);
|
|
20
20
|
}
|
|
21
|
-
if (
|
|
21
|
+
if (typeof routeDef.path !== 'string') {
|
|
22
22
|
throw new InvalidArgumentError(
|
|
23
|
-
'Option "path" must be a
|
|
23
|
+
'Option "path" must be a String, but %v was given.',
|
|
24
|
+
routeDef.path,
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
if (!routeDef.path.startsWith('/')) {
|
|
28
|
+
throw new InvalidArgumentError(
|
|
29
|
+
'Option "path" must start with "/", but %v was given.',
|
|
24
30
|
routeDef.path,
|
|
25
31
|
);
|
|
26
32
|
}
|
|
@@ -51,7 +51,7 @@ describe('validateRouteDefinition', function () {
|
|
|
51
51
|
throwable(HttpMethod.GET)();
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
it('should require the "path" option to be a
|
|
54
|
+
it('should require the "path" option to be a String', function () {
|
|
55
55
|
const throwable = v => () =>
|
|
56
56
|
validateRouteDefinition({
|
|
57
57
|
method: HttpMethod.GET,
|
|
@@ -59,8 +59,7 @@ describe('validateRouteDefinition', function () {
|
|
|
59
59
|
handler: () => undefined,
|
|
60
60
|
});
|
|
61
61
|
const error = v =>
|
|
62
|
-
format('Option "path" must be a
|
|
63
|
-
expect(throwable('')).to.throw(error('""'));
|
|
62
|
+
format('Option "path" must be a String, but %s was given.', v);
|
|
64
63
|
expect(throwable(10)).to.throw(error('10'));
|
|
65
64
|
expect(throwable(0)).to.throw(error('0'));
|
|
66
65
|
expect(throwable(true)).to.throw(error('true'));
|
|
@@ -70,7 +69,22 @@ describe('validateRouteDefinition', function () {
|
|
|
70
69
|
expect(throwable(undefined)).to.throw(error('undefined'));
|
|
71
70
|
expect(throwable(null)).to.throw(error('null'));
|
|
72
71
|
expect(throwable(() => undefined)).to.throw(error('Function'));
|
|
73
|
-
throwable('
|
|
72
|
+
throwable('/path')();
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it('should require the "path" option to start with a forward slash', function () {
|
|
76
|
+
const throwable = v => () =>
|
|
77
|
+
validateRouteDefinition({
|
|
78
|
+
method: HttpMethod.GET,
|
|
79
|
+
path: v,
|
|
80
|
+
handler: () => undefined,
|
|
81
|
+
});
|
|
82
|
+
const error = s =>
|
|
83
|
+
format('Option "path" must start with "/", but %s was given.', s);
|
|
84
|
+
expect(throwable('path')).to.throw(error('"path"'));
|
|
85
|
+
expect(throwable('')).to.throw(error('""'));
|
|
86
|
+
throwable('/path')();
|
|
87
|
+
throwable('/')();
|
|
74
88
|
});
|
|
75
89
|
|
|
76
90
|
it('should require the "handler" option to be a Function', function () {
|
package/src/route-registry.js
CHANGED
|
@@ -50,7 +50,7 @@ export class RouteRegistry extends DebuggableService {
|
|
|
50
50
|
if (onDefineRouteHooks.length) {
|
|
51
51
|
debug('Invoking %v "onDefineRoute" hook(s).', onDefineRouteHooks.length);
|
|
52
52
|
for (const hook of onDefineRouteHooks) {
|
|
53
|
-
const hookResult = hook({...routeDef});
|
|
53
|
+
const hookResult = hook({...routeDef}, this.container);
|
|
54
54
|
// если возвращаемое значение хука не является
|
|
55
55
|
// объектом и undefined, то выбрасывается ошибка
|
|
56
56
|
if (
|
|
@@ -52,7 +52,8 @@ describe('RouteRegistry', function () {
|
|
|
52
52
|
expect(res.value).to.be.eq(route);
|
|
53
53
|
});
|
|
54
54
|
|
|
55
|
-
it('should invoke "onDefineRoute" hooks
|
|
55
|
+
it('should invoke "onDefineRoute" hooks with arguments and correct order', function () {
|
|
56
|
+
const S = new RouteRegistry();
|
|
56
57
|
const routeDef = {
|
|
57
58
|
method: HttpMethod.GET,
|
|
58
59
|
path: '/myPath',
|
|
@@ -61,13 +62,14 @@ describe('RouteRegistry', function () {
|
|
|
61
62
|
const order = [];
|
|
62
63
|
const onDefineRouteHook1 = (...args) => {
|
|
63
64
|
order.push(1);
|
|
64
|
-
expect(args).to.be.eql(
|
|
65
|
+
expect(args[0]).to.be.eql(routeDef);
|
|
66
|
+
expect(args[1]).to.be.eq(S.container);
|
|
65
67
|
};
|
|
66
68
|
const onDefineRouteHook2 = (...args) => {
|
|
67
69
|
order.push(2);
|
|
68
|
-
expect(args).to.be.eql(
|
|
70
|
+
expect(args[0]).to.be.eql(routeDef);
|
|
71
|
+
expect(args[1]).to.be.eq(S.container);
|
|
69
72
|
};
|
|
70
|
-
const S = new RouteRegistry();
|
|
71
73
|
const hooksRegistry = S.getService(RouterHookRegistry);
|
|
72
74
|
hooksRegistry.addHook(RouterHookType.ON_DEFINE_ROUTE, onDefineRouteHook1);
|
|
73
75
|
hooksRegistry.addHook(RouterHookType.ON_DEFINE_ROUTE, onDefineRouteHook2);
|
|
@@ -76,6 +78,7 @@ describe('RouteRegistry', function () {
|
|
|
76
78
|
});
|
|
77
79
|
|
|
78
80
|
it('should allow override the route definition by "onDefineRoute" hooks', function () {
|
|
81
|
+
const S = new RouteRegistry();
|
|
79
82
|
const routeDef = {
|
|
80
83
|
method: HttpMethod.GET,
|
|
81
84
|
path: '/myPath',
|
|
@@ -90,7 +93,6 @@ describe('RouteRegistry', function () {
|
|
|
90
93
|
order.push(2);
|
|
91
94
|
return {...def, path: def.path + '/2'};
|
|
92
95
|
};
|
|
93
|
-
const S = new RouteRegistry();
|
|
94
96
|
const hooksRegistry = S.getService(RouterHookRegistry);
|
|
95
97
|
hooksRegistry.addHook(RouterHookType.ON_DEFINE_ROUTE, onDefineRouteHook1);
|
|
96
98
|
hooksRegistry.addHook(RouterHookType.ON_DEFINE_ROUTE, onDefineRouteHook2);
|
|
@@ -15,15 +15,14 @@ export class DataSender extends DebuggableService {
|
|
|
15
15
|
*/
|
|
16
16
|
send(response, data) {
|
|
17
17
|
const debug = this.getDebuggerFor(this.send);
|
|
18
|
-
// если ответ контроллера является объектом
|
|
19
|
-
//
|
|
20
|
-
//
|
|
21
|
-
// уже отправил ответ самостоятельно
|
|
18
|
+
// если ответ контроллера является объектом ServerResponse,
|
|
19
|
+
// или имеются отправленные заголовки, то предполагается,
|
|
20
|
+
// что контроллер уже отправил ответ самостоятельно
|
|
22
21
|
if (data === response || response.headersSent) {
|
|
23
22
|
debug('Skipping response because headers have already been sent.');
|
|
24
23
|
return;
|
|
25
24
|
}
|
|
26
|
-
// если ответ контроллера пуст, то
|
|
25
|
+
// если ответ контроллера пуст, то отправляется
|
|
27
26
|
// статус 204 "No Content"
|
|
28
27
|
if (data == null) {
|
|
29
28
|
response.statusCode = 204;
|
|
@@ -32,9 +31,13 @@ export class DataSender extends DebuggableService {
|
|
|
32
31
|
return;
|
|
33
32
|
}
|
|
34
33
|
// если ответ контроллера является стримом,
|
|
35
|
-
// то
|
|
34
|
+
// то поток отправляет бинарные данные
|
|
36
35
|
if (isReadableStream(data)) {
|
|
37
|
-
|
|
36
|
+
// если заголовок "content-type" не определен ранее,
|
|
37
|
+
// то устанавливается заголовок потоковых данных
|
|
38
|
+
if (!response.getHeader('content-type')) {
|
|
39
|
+
response.setHeader('content-type', 'application/octet-stream');
|
|
40
|
+
}
|
|
38
41
|
data.pipe(response);
|
|
39
42
|
debug('Sending response with a Stream.');
|
|
40
43
|
return;
|
|
@@ -43,16 +46,25 @@ export class DataSender extends DebuggableService {
|
|
|
43
46
|
// нужного заголовка в зависимости от их типа
|
|
44
47
|
let debugMsg;
|
|
45
48
|
switch (typeof data) {
|
|
46
|
-
case 'object':
|
|
47
|
-
case 'boolean':
|
|
48
49
|
case 'number':
|
|
50
|
+
case 'boolean':
|
|
51
|
+
case 'object':
|
|
52
|
+
// для бинарных данных предусмотрен специальный "content-type",
|
|
53
|
+
// который устанавливается автоматически, если не был определен
|
|
54
|
+
// ранее (к примеру, в обработчике маршрута)
|
|
49
55
|
if (Buffer.isBuffer(data)) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
56
|
+
if (!response.getHeader('content-type')) {
|
|
57
|
+
response.setHeader('content-type', 'application/octet-stream');
|
|
58
|
+
}
|
|
53
59
|
debugMsg = 'Buffer has been sent as binary data.';
|
|
54
|
-
}
|
|
55
|
-
|
|
60
|
+
}
|
|
61
|
+
// объекты, массивы, числа и логические значения
|
|
62
|
+
// отправляются в виде JSON строки, с соответствующим
|
|
63
|
+
// заголовком "content-type" (если не был определен)
|
|
64
|
+
else {
|
|
65
|
+
if (!response.getHeader('content-type')) {
|
|
66
|
+
response.setHeader('content-type', 'application/json');
|
|
67
|
+
}
|
|
56
68
|
debugMsg = format(
|
|
57
69
|
'%v has been sent as JSON.',
|
|
58
70
|
toPascalCase(typeof data),
|
|
@@ -61,7 +73,9 @@ export class DataSender extends DebuggableService {
|
|
|
61
73
|
}
|
|
62
74
|
break;
|
|
63
75
|
default:
|
|
64
|
-
response.
|
|
76
|
+
if (!response.getHeader('content-type')) {
|
|
77
|
+
response.setHeader('content-type', 'text/plain');
|
|
78
|
+
}
|
|
65
79
|
debugMsg = 'Response data has been sent as plain text.';
|
|
66
80
|
data = String(data);
|
|
67
81
|
break;
|
|
@@ -5,7 +5,7 @@ import {createResponseMock} from '../utils/index.js';
|
|
|
5
5
|
|
|
6
6
|
describe('DataSender', function () {
|
|
7
7
|
describe('send', function () {
|
|
8
|
-
it('
|
|
8
|
+
it('should not send response when the data is the server response', function (done) {
|
|
9
9
|
const res = createResponseMock();
|
|
10
10
|
const writable = new Writable();
|
|
11
11
|
writable._write = function () {
|
|
@@ -21,7 +21,7 @@ describe('DataSender', function () {
|
|
|
21
21
|
setTimeout(() => done(), 5);
|
|
22
22
|
});
|
|
23
23
|
|
|
24
|
-
it('
|
|
24
|
+
it('should not send response when response headers already sent', function (done) {
|
|
25
25
|
const res = createResponseMock();
|
|
26
26
|
res._headersSent = true;
|
|
27
27
|
const writable = new Writable();
|
|
@@ -38,7 +38,7 @@ describe('DataSender', function () {
|
|
|
38
38
|
setTimeout(() => done(), 5);
|
|
39
39
|
});
|
|
40
40
|
|
|
41
|
-
it('
|
|
41
|
+
it('should send 204 status code when the data is undefined', function (done) {
|
|
42
42
|
const res = createResponseMock();
|
|
43
43
|
res.on('data', () => done(new Error('Should not be called')));
|
|
44
44
|
res.on('error', e => done(e));
|
|
@@ -51,7 +51,20 @@ describe('DataSender', function () {
|
|
|
51
51
|
expect(result).to.be.undefined;
|
|
52
52
|
});
|
|
53
53
|
|
|
54
|
-
it('
|
|
54
|
+
it('should send 204 status code when the data is null', function (done) {
|
|
55
|
+
const res = createResponseMock();
|
|
56
|
+
res.on('data', () => done(new Error('Should not be called')));
|
|
57
|
+
res.on('error', e => done(e));
|
|
58
|
+
res.on('end', () => {
|
|
59
|
+
expect(res.statusCode).to.be.eq(204);
|
|
60
|
+
done();
|
|
61
|
+
});
|
|
62
|
+
const S = new DataSender();
|
|
63
|
+
const result = S.send(res, null);
|
|
64
|
+
expect(result).to.be.undefined;
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('should send the readable stream as a binary data', function (done) {
|
|
55
68
|
const data = 'text';
|
|
56
69
|
const stream = new Readable();
|
|
57
70
|
stream._read = () => {};
|
|
@@ -77,9 +90,15 @@ describe('DataSender', function () {
|
|
|
77
90
|
S.send(res, stream);
|
|
78
91
|
});
|
|
79
92
|
|
|
80
|
-
it('
|
|
81
|
-
const data =
|
|
93
|
+
it('should allow override the "content-type" header for a readable stream', function (done) {
|
|
94
|
+
const data = 'text';
|
|
95
|
+
const stream = new Readable();
|
|
96
|
+
stream._read = () => {};
|
|
97
|
+
stream.push(data);
|
|
98
|
+
stream.push(null);
|
|
82
99
|
const res = createResponseMock();
|
|
100
|
+
const contentType = 'custom/type';
|
|
101
|
+
res.setHeader('content-type', contentType);
|
|
83
102
|
const writable = new Writable();
|
|
84
103
|
const chunks = [];
|
|
85
104
|
writable._write = function (chunk, encoding, done) {
|
|
@@ -87,10 +106,33 @@ describe('DataSender', function () {
|
|
|
87
106
|
done();
|
|
88
107
|
};
|
|
89
108
|
writable._final = function (callback) {
|
|
90
|
-
const sentData = Buffer.concat(chunks);
|
|
91
|
-
expect(sentData).to.be.
|
|
109
|
+
const sentData = Buffer.concat(chunks).toString('utf-8');
|
|
110
|
+
expect(sentData).to.be.eq(data);
|
|
92
111
|
const ct = res.getHeader('content-type');
|
|
93
|
-
expect(ct).to.be.eq(
|
|
112
|
+
expect(ct).to.be.eq(contentType);
|
|
113
|
+
callback();
|
|
114
|
+
done();
|
|
115
|
+
};
|
|
116
|
+
res.pipe(writable);
|
|
117
|
+
const S = new DataSender();
|
|
118
|
+
S.send(res, stream);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it('should send the number value as a JSON string', function (done) {
|
|
122
|
+
const data = 10;
|
|
123
|
+
const res = createResponseMock();
|
|
124
|
+
const writable = new Writable();
|
|
125
|
+
const chunks = [];
|
|
126
|
+
writable._write = function (chunk, encoding, done) {
|
|
127
|
+
chunks.push(chunk);
|
|
128
|
+
done();
|
|
129
|
+
};
|
|
130
|
+
writable._final = function (callback) {
|
|
131
|
+
const sentJson = Buffer.concat(chunks).toString('utf-8');
|
|
132
|
+
const sentData = JSON.parse(sentJson);
|
|
133
|
+
expect(sentData).to.be.eql(data);
|
|
134
|
+
const ct = res.getHeader('content-type');
|
|
135
|
+
expect(ct).to.be.eq('application/json');
|
|
94
136
|
callback();
|
|
95
137
|
done();
|
|
96
138
|
};
|
|
@@ -99,9 +141,11 @@ describe('DataSender', function () {
|
|
|
99
141
|
S.send(res, data);
|
|
100
142
|
});
|
|
101
143
|
|
|
102
|
-
it('
|
|
103
|
-
const data =
|
|
144
|
+
it('should allow override the "content-type" header for a number value', function (done) {
|
|
145
|
+
const data = 10;
|
|
104
146
|
const res = createResponseMock();
|
|
147
|
+
const contentType = 'custom/type';
|
|
148
|
+
res.setHeader('content-type', contentType);
|
|
105
149
|
const writable = new Writable();
|
|
106
150
|
const chunks = [];
|
|
107
151
|
writable._write = function (chunk, encoding, done) {
|
|
@@ -109,10 +153,11 @@ describe('DataSender', function () {
|
|
|
109
153
|
done();
|
|
110
154
|
};
|
|
111
155
|
writable._final = function (callback) {
|
|
112
|
-
const
|
|
113
|
-
|
|
156
|
+
const sentJson = Buffer.concat(chunks).toString('utf-8');
|
|
157
|
+
const sentData = JSON.parse(sentJson);
|
|
158
|
+
expect(sentData).to.be.eql(data);
|
|
114
159
|
const ct = res.getHeader('content-type');
|
|
115
|
-
expect(ct).to.be.eq(
|
|
160
|
+
expect(ct).to.be.eq(contentType);
|
|
116
161
|
callback();
|
|
117
162
|
done();
|
|
118
163
|
};
|
|
@@ -121,8 +166,8 @@ describe('DataSender', function () {
|
|
|
121
166
|
S.send(res, data);
|
|
122
167
|
});
|
|
123
168
|
|
|
124
|
-
it('
|
|
125
|
-
const data =
|
|
169
|
+
it('should send the boolean value as a JSON string', function (done) {
|
|
170
|
+
const data = true;
|
|
126
171
|
const res = createResponseMock();
|
|
127
172
|
const writable = new Writable();
|
|
128
173
|
const chunks = [];
|
|
@@ -144,9 +189,11 @@ describe('DataSender', function () {
|
|
|
144
189
|
S.send(res, data);
|
|
145
190
|
});
|
|
146
191
|
|
|
147
|
-
it('
|
|
192
|
+
it('should allow override the "content-type" header for a boolean value', function (done) {
|
|
148
193
|
const data = true;
|
|
149
194
|
const res = createResponseMock();
|
|
195
|
+
const contentType = 'custom/type';
|
|
196
|
+
res.setHeader('content-type', contentType);
|
|
150
197
|
const writable = new Writable();
|
|
151
198
|
const chunks = [];
|
|
152
199
|
writable._write = function (chunk, encoding, done) {
|
|
@@ -158,7 +205,7 @@ describe('DataSender', function () {
|
|
|
158
205
|
const sentData = JSON.parse(sentJson);
|
|
159
206
|
expect(sentData).to.be.eql(data);
|
|
160
207
|
const ct = res.getHeader('content-type');
|
|
161
|
-
expect(ct).to.be.eq(
|
|
208
|
+
expect(ct).to.be.eq(contentType);
|
|
162
209
|
callback();
|
|
163
210
|
done();
|
|
164
211
|
};
|
|
@@ -167,8 +214,54 @@ describe('DataSender', function () {
|
|
|
167
214
|
S.send(res, data);
|
|
168
215
|
});
|
|
169
216
|
|
|
170
|
-
it('
|
|
171
|
-
const data =
|
|
217
|
+
it('should send the Buffer as a binary data', function (done) {
|
|
218
|
+
const data = Buffer.from('text');
|
|
219
|
+
const res = createResponseMock();
|
|
220
|
+
const writable = new Writable();
|
|
221
|
+
const chunks = [];
|
|
222
|
+
writable._write = function (chunk, encoding, done) {
|
|
223
|
+
chunks.push(chunk);
|
|
224
|
+
done();
|
|
225
|
+
};
|
|
226
|
+
writable._final = function (callback) {
|
|
227
|
+
const sentData = Buffer.concat(chunks);
|
|
228
|
+
expect(sentData).to.be.eql(sentData);
|
|
229
|
+
const ct = res.getHeader('content-type');
|
|
230
|
+
expect(ct).to.be.eq('application/octet-stream');
|
|
231
|
+
callback();
|
|
232
|
+
done();
|
|
233
|
+
};
|
|
234
|
+
res.pipe(writable);
|
|
235
|
+
const S = new DataSender();
|
|
236
|
+
S.send(res, data);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should allow override the "content-type" header for a Buffer', function (done) {
|
|
240
|
+
const data = Buffer.from('text');
|
|
241
|
+
const res = createResponseMock();
|
|
242
|
+
const contentType = 'custom/type';
|
|
243
|
+
res.setHeader('content-type', contentType);
|
|
244
|
+
const writable = new Writable();
|
|
245
|
+
const chunks = [];
|
|
246
|
+
writable._write = function (chunk, encoding, done) {
|
|
247
|
+
chunks.push(chunk);
|
|
248
|
+
done();
|
|
249
|
+
};
|
|
250
|
+
writable._final = function (callback) {
|
|
251
|
+
const sentData = Buffer.concat(chunks);
|
|
252
|
+
expect(sentData).to.be.eql(sentData);
|
|
253
|
+
const ct = res.getHeader('content-type');
|
|
254
|
+
expect(ct).to.be.eq(contentType);
|
|
255
|
+
callback();
|
|
256
|
+
done();
|
|
257
|
+
};
|
|
258
|
+
res.pipe(writable);
|
|
259
|
+
const S = new DataSender();
|
|
260
|
+
S.send(res, data);
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('should send the object value as a JSON string', function (done) {
|
|
264
|
+
const data = {foo: 'bar'};
|
|
172
265
|
const res = createResponseMock();
|
|
173
266
|
const writable = new Writable();
|
|
174
267
|
const chunks = [];
|
|
@@ -189,5 +282,76 @@ describe('DataSender', function () {
|
|
|
189
282
|
const S = new DataSender();
|
|
190
283
|
S.send(res, data);
|
|
191
284
|
});
|
|
285
|
+
|
|
286
|
+
it('should allow override the "content-type" header for an object value', function (done) {
|
|
287
|
+
const data = {foo: 'bar'};
|
|
288
|
+
const res = createResponseMock();
|
|
289
|
+
const contentType = 'custom/type';
|
|
290
|
+
res.setHeader('content-type', contentType);
|
|
291
|
+
const writable = new Writable();
|
|
292
|
+
const chunks = [];
|
|
293
|
+
writable._write = function (chunk, encoding, done) {
|
|
294
|
+
chunks.push(chunk);
|
|
295
|
+
done();
|
|
296
|
+
};
|
|
297
|
+
writable._final = function (callback) {
|
|
298
|
+
const sentJson = Buffer.concat(chunks).toString('utf-8');
|
|
299
|
+
const sentData = JSON.parse(sentJson);
|
|
300
|
+
expect(sentData).to.be.eql(data);
|
|
301
|
+
const ct = res.getHeader('content-type');
|
|
302
|
+
expect(ct).to.be.eq(contentType);
|
|
303
|
+
callback();
|
|
304
|
+
done();
|
|
305
|
+
};
|
|
306
|
+
res.pipe(writable);
|
|
307
|
+
const S = new DataSender();
|
|
308
|
+
S.send(res, data);
|
|
309
|
+
});
|
|
310
|
+
|
|
311
|
+
it('should send the string value as a plain text', function (done) {
|
|
312
|
+
const data = 'text';
|
|
313
|
+
const res = createResponseMock();
|
|
314
|
+
const writable = new Writable();
|
|
315
|
+
const chunks = [];
|
|
316
|
+
writable._write = function (chunk, encoding, done) {
|
|
317
|
+
chunks.push(chunk);
|
|
318
|
+
done();
|
|
319
|
+
};
|
|
320
|
+
writable._final = function (callback) {
|
|
321
|
+
const sentData = Buffer.concat(chunks).toString('utf-8');
|
|
322
|
+
expect(sentData).to.be.eq(data);
|
|
323
|
+
const ct = res.getHeader('content-type');
|
|
324
|
+
expect(ct).to.be.eq('text/plain');
|
|
325
|
+
callback();
|
|
326
|
+
done();
|
|
327
|
+
};
|
|
328
|
+
res.pipe(writable);
|
|
329
|
+
const S = new DataSender();
|
|
330
|
+
S.send(res, data);
|
|
331
|
+
});
|
|
332
|
+
|
|
333
|
+
it('should allow override the "content-type" header for a string value', function (done) {
|
|
334
|
+
const data = 'text';
|
|
335
|
+
const res = createResponseMock();
|
|
336
|
+
const contentType = 'custom/type';
|
|
337
|
+
res.setHeader('content-type', contentType);
|
|
338
|
+
const writable = new Writable();
|
|
339
|
+
const chunks = [];
|
|
340
|
+
writable._write = function (chunk, encoding, done) {
|
|
341
|
+
chunks.push(chunk);
|
|
342
|
+
done();
|
|
343
|
+
};
|
|
344
|
+
writable._final = function (callback) {
|
|
345
|
+
const sentData = Buffer.concat(chunks).toString('utf-8');
|
|
346
|
+
expect(sentData).to.be.eq(data);
|
|
347
|
+
const ct = res.getHeader('content-type');
|
|
348
|
+
expect(ct).to.be.eq(contentType);
|
|
349
|
+
callback();
|
|
350
|
+
done();
|
|
351
|
+
};
|
|
352
|
+
res.pipe(writable);
|
|
353
|
+
const S = new DataSender();
|
|
354
|
+
S.send(res, data);
|
|
355
|
+
});
|
|
192
356
|
});
|
|
193
357
|
});
|
package/src/trie-router.js
CHANGED
|
@@ -25,7 +25,7 @@ export class TrieRouter extends DebuggableService {
|
|
|
25
25
|
* ```
|
|
26
26
|
* const router = new TrieRouter();
|
|
27
27
|
* router.defineRoute({
|
|
28
|
-
* method: HttpMethod.GET,
|
|
28
|
+
* method: HttpMethod.GET, // Request method.
|
|
29
29
|
* path: '/', // Path template.
|
|
30
30
|
* handler: ctx => 'Hello world!', // Request handler.
|
|
31
31
|
* });
|
package/src/trie-router.spec.js
CHANGED
|
@@ -32,7 +32,7 @@ describe('TrieRouter', function () {
|
|
|
32
32
|
|
|
33
33
|
it('should pass the "path" option to a router branch', function () {
|
|
34
34
|
const router = new TrieRouter();
|
|
35
|
-
const branchDef = {path: 'foo'};
|
|
35
|
+
const branchDef = {path: '/foo'};
|
|
36
36
|
const res = router.createBranch(branchDef);
|
|
37
37
|
expect(res.getDefinition().path).to.be.eq(branchDef.path);
|
|
38
38
|
});
|
|
@@ -17,8 +17,8 @@ describe('createRouteMock', function () {
|
|
|
17
17
|
});
|
|
18
18
|
|
|
19
19
|
it('sets the "path" option', function () {
|
|
20
|
-
const res = createRouteMock({path: 'test'});
|
|
21
|
-
expect(res.path).to.be.eq('test');
|
|
20
|
+
const res = createRouteMock({path: '/test'});
|
|
21
|
+
expect(res.path).to.be.eq('/test');
|
|
22
22
|
});
|
|
23
23
|
|
|
24
24
|
it('sets the "handler" option', function () {
|
package/src/utils/index.d.ts
CHANGED
|
@@ -4,7 +4,6 @@ export * from './is-promise.js';
|
|
|
4
4
|
export * from './create-error.js';
|
|
5
5
|
export * from './to-camel-case.js';
|
|
6
6
|
export * from './to-pascal-case.js';
|
|
7
|
-
export * from './normalize-path.js';
|
|
8
7
|
export * from './is-response-sent.js';
|
|
9
8
|
export * from './create-route-mock.js';
|
|
10
9
|
export * from './is-readable-stream.js';
|
package/src/utils/index.js
CHANGED
|
@@ -4,7 +4,6 @@ export * from './is-promise.js';
|
|
|
4
4
|
export * from './create-error.js';
|
|
5
5
|
export * from './to-camel-case.js';
|
|
6
6
|
export * from './to-pascal-case.js';
|
|
7
|
-
export * from './normalize-path.js';
|
|
8
7
|
export * from './is-response-sent.js';
|
|
9
8
|
export * from './create-route-mock.js';
|
|
10
9
|
export * from './is-readable-stream.js';
|
|
@@ -1,12 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Normalize path.
|
|
3
|
-
*
|
|
4
|
-
* Заменяет любые повторяющиеся слеши на один.
|
|
5
|
-
* Удаляет пробельные символы в начале и конце.
|
|
6
|
-
* Удаляет слеш в конце строки.
|
|
7
|
-
* Гарантирует слеш в начале строки (по умолчанию).
|
|
8
|
-
*
|
|
9
|
-
* @param value
|
|
10
|
-
* @param noStartingSlash
|
|
11
|
-
*/
|
|
12
|
-
export function normalizePath(value: string, noStartingSlash?: boolean): string;
|
|
@@ -1,22 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Normalize path.
|
|
3
|
-
*
|
|
4
|
-
* Заменяет любые повторяющиеся слеши на один.
|
|
5
|
-
* Удаляет пробельные символы в начале и конце.
|
|
6
|
-
* Удаляет слеш в конце строки.
|
|
7
|
-
* Гарантирует слеш в начале строки (по умолчанию).
|
|
8
|
-
*
|
|
9
|
-
* @param {string} value
|
|
10
|
-
* @param {boolean} [noStartingSlash]
|
|
11
|
-
* @returns {string}
|
|
12
|
-
*/
|
|
13
|
-
export function normalizePath(value, noStartingSlash = false) {
|
|
14
|
-
if (typeof value !== 'string') {
|
|
15
|
-
return '/';
|
|
16
|
-
}
|
|
17
|
-
const res = value
|
|
18
|
-
.trim()
|
|
19
|
-
.replace(/\/+/g, '/')
|
|
20
|
-
.replace(/(^\/|\/$)/g, '');
|
|
21
|
-
return noStartingSlash ? res : '/' + res;
|
|
22
|
-
}
|
|
@@ -1,56 +0,0 @@
|
|
|
1
|
-
import {expect} from 'chai';
|
|
2
|
-
import {normalizePath} from './normalize-path.js';
|
|
3
|
-
|
|
4
|
-
describe('normalizePath', function () {
|
|
5
|
-
describe('input validation', function () {
|
|
6
|
-
it('should return a root path "/" if value is null', function () {
|
|
7
|
-
expect(normalizePath(null)).to.equal('/');
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
it('should return a root path "/" if value is undefined', function () {
|
|
11
|
-
expect(normalizePath(undefined)).to.equal('/');
|
|
12
|
-
});
|
|
13
|
-
|
|
14
|
-
it('should return a root path "/" if value is a number', function () {
|
|
15
|
-
expect(normalizePath(123)).to.equal('/');
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
it('should return a root path "/" if value is an object', function () {
|
|
19
|
-
expect(normalizePath({})).to.equal('/');
|
|
20
|
-
});
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
describe('path normalization', function () {
|
|
24
|
-
it('should replace multiple slashes with a single slash', function () {
|
|
25
|
-
expect(normalizePath('//api///users//')).to.equal('/api/users');
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
it('should trim a given string but preserve whitespace characters', function () {
|
|
29
|
-
expect(normalizePath(' /my folder/ ')).to.equal('/my folder');
|
|
30
|
-
expect(normalizePath('path\twith\ntabs')).to.equal('/path\twith\ntabs');
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
it('should remove leading and trailing slashes before applying the final format', function () {
|
|
34
|
-
expect(normalizePath('/foo/bar/')).to.equal('/foo/bar');
|
|
35
|
-
});
|
|
36
|
-
|
|
37
|
-
it('should handle an empty string by returning "/" by default', function () {
|
|
38
|
-
expect(normalizePath('')).to.equal('/');
|
|
39
|
-
});
|
|
40
|
-
});
|
|
41
|
-
|
|
42
|
-
describe('the "noStartingSlash" option', function () {
|
|
43
|
-
it('should always prepend a leading slash when the option is false', function () {
|
|
44
|
-
expect(normalizePath('foo/bar', false)).to.equal('/foo/bar');
|
|
45
|
-
});
|
|
46
|
-
|
|
47
|
-
it('should not prepend a leading slash when the option is true', function () {
|
|
48
|
-
expect(normalizePath('/foo/bar/', true)).to.equal('foo/bar');
|
|
49
|
-
});
|
|
50
|
-
|
|
51
|
-
it('should return an empty string if the input results in an empty path', function () {
|
|
52
|
-
expect(normalizePath('', true)).to.equal('');
|
|
53
|
-
expect(normalizePath('///', true)).to.equal('');
|
|
54
|
-
});
|
|
55
|
-
});
|
|
56
|
-
});
|