starlight-server 0.0.1
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/LICENSE +21 -0
- package/README.md +1 -0
- package/dist/demo/index.d.ts +1 -0
- package/dist/demo/index.js +27 -0
- package/dist/http/body/form-data.d.ts +35 -0
- package/dist/http/body/form-data.js +141 -0
- package/dist/http/body/index.d.ts +23 -0
- package/dist/http/body/index.js +47 -0
- package/dist/http/body/receive.d.ts +7 -0
- package/dist/http/body/receive.js +39 -0
- package/dist/http/http-status.d.ts +9 -0
- package/dist/http/http-status.js +64 -0
- package/dist/http/index.d.ts +9 -0
- package/dist/http/index.js +9 -0
- package/dist/http/mime-types.d.ts +14 -0
- package/dist/http/mime-types.js +764 -0
- package/dist/http/request.d.ts +25 -0
- package/dist/http/request.js +40 -0
- package/dist/http/response.d.ts +32 -0
- package/dist/http/response.js +66 -0
- package/dist/http/server.d.ts +31 -0
- package/dist/http/server.js +52 -0
- package/dist/http/types.d.ts +26 -0
- package/dist/http/types.js +12 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.js +4 -0
- package/dist/logging.d.ts +24 -0
- package/dist/logging.js +30 -0
- package/dist/router/cors.d.ts +24 -0
- package/dist/router/cors.js +35 -0
- package/dist/router/index.d.ts +38 -0
- package/dist/router/index.js +36 -0
- package/dist/router/match.d.ts +23 -0
- package/dist/router/match.js +172 -0
- package/dist/router/parameters.d.ts +51 -0
- package/dist/router/parameters.js +118 -0
- package/dist/router/router.d.ts +127 -0
- package/dist/router/router.js +97 -0
- package/dist/swagger/index.d.ts +1 -0
- package/dist/swagger/index.js +168 -0
- package/dist/swagger/openapi-spec.d.ts +261 -0
- package/dist/swagger/openapi-spec.js +5 -0
- package/dist/validators/array.d.ts +9 -0
- package/dist/validators/array.js +28 -0
- package/dist/validators/boolean.d.ts +4 -0
- package/dist/validators/boolean.js +28 -0
- package/dist/validators/common.d.ts +23 -0
- package/dist/validators/common.js +25 -0
- package/dist/validators/index.d.ts +20 -0
- package/dist/validators/index.js +38 -0
- package/dist/validators/number.d.ts +10 -0
- package/dist/validators/number.js +30 -0
- package/dist/validators/object.d.ts +13 -0
- package/dist/validators/object.js +36 -0
- package/dist/validators/string.d.ts +11 -0
- package/dist/validators/string.js +29 -0
- package/package.json +54 -0
- package/src/demo/index.ts +33 -0
- package/src/http/body/form-data.ts +164 -0
- package/src/http/body/index.ts +59 -0
- package/src/http/body/receive.ts +49 -0
- package/src/http/http-status.ts +65 -0
- package/src/http/index.ts +9 -0
- package/src/http/mime-types.ts +765 -0
- package/src/http/request.ts +44 -0
- package/src/http/response.ts +73 -0
- package/src/http/server.ts +67 -0
- package/src/http/types.ts +31 -0
- package/src/index.ts +4 -0
- package/src/logging.ts +57 -0
- package/src/router/cors.ts +54 -0
- package/src/router/index.ts +38 -0
- package/src/router/match.ts +194 -0
- package/src/router/parameters.ts +172 -0
- package/src/router/router.ts +233 -0
- package/src/swagger/index.ts +184 -0
- package/src/swagger/openapi-spec.ts +312 -0
- package/src/validators/array.ts +33 -0
- package/src/validators/boolean.ts +23 -0
- package/src/validators/common.ts +46 -0
- package/src/validators/index.ts +50 -0
- package/src/validators/number.ts +36 -0
- package/src/validators/object.ts +41 -0
- package/src/validators/string.ts +38 -0
|
@@ -0,0 +1,172 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 匹配路由路径
|
|
3
|
+
*
|
|
4
|
+
* 支持的格式:
|
|
5
|
+
* 1. /abc/ abc/ /abc => 这几种形式等价
|
|
6
|
+
* 2. /abc/:foo/bar => 命名参数(foo)
|
|
7
|
+
* 3. /abc/:foo?/bar => 可选命名参数(foo)
|
|
8
|
+
* 4. /abc/* => 后续路径(任意长度,* 只能出现在路由最后面,匹配结果不含开头的“/”)
|
|
9
|
+
*
|
|
10
|
+
* 路由不区分大小写
|
|
11
|
+
*/
|
|
12
|
+
import escapeRegExp from 'lodash/escapeRegExp.js';
|
|
13
|
+
/**
|
|
14
|
+
* 标准化路径
|
|
15
|
+
* 标准化后,相同的两个路径一定也是相同的字符串(例如 /Abc/Def 和 abc/def/ 都会变成 abc/def)。
|
|
16
|
+
*
|
|
17
|
+
* - 移除首尾和重复的 '/',完成后有 path 有这几种可能的格式: ''、'abc'、'abc/def'
|
|
18
|
+
* - 统一改为小写
|
|
19
|
+
*/
|
|
20
|
+
export function normalizePath(path) {
|
|
21
|
+
if (path.startsWith('/'))
|
|
22
|
+
path = path.slice(1);
|
|
23
|
+
if (path.endsWith('/'))
|
|
24
|
+
path = path.slice(0, -1);
|
|
25
|
+
path = path.replace(/\/+/g, '/');
|
|
26
|
+
return path.toLowerCase();
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* 解析路由路径定义
|
|
30
|
+
* '/abc/:foo?/*' => [{ type: 'text', text: 'abc' }, { type: 'named', name: 'foo', optional: true }, { type: 'rest' }]
|
|
31
|
+
*/
|
|
32
|
+
function parseRoutePath(routePath) {
|
|
33
|
+
const normalizedPath = normalizePath(routePath);
|
|
34
|
+
const nodes = [];
|
|
35
|
+
const pathParts = normalizedPath.split('/');
|
|
36
|
+
for (const [i, part] of pathParts.entries()) {
|
|
37
|
+
if (part.startsWith(':') && part !== ':' && part !== ':?') {
|
|
38
|
+
const optional = part.endsWith('?');
|
|
39
|
+
const name = optional ? part.slice(1, -1) : part.slice(1);
|
|
40
|
+
if (name === '*')
|
|
41
|
+
throw new Error('route path parameter name cannot be "*"');
|
|
42
|
+
nodes.push({ type: PathNodeType.Named, name, optional });
|
|
43
|
+
}
|
|
44
|
+
else if (part === '*' && i === pathParts.length - 1) {
|
|
45
|
+
nodes.push({ type: PathNodeType.Rest });
|
|
46
|
+
}
|
|
47
|
+
else {
|
|
48
|
+
nodes.push({ type: PathNodeType.Text, text: part });
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
return nodes;
|
|
52
|
+
}
|
|
53
|
+
var PathNodeType;
|
|
54
|
+
(function (PathNodeType) {
|
|
55
|
+
/** 纯文本 */
|
|
56
|
+
PathNodeType["Text"] = "text";
|
|
57
|
+
/** 命名参数 */
|
|
58
|
+
PathNodeType["Named"] = "named";
|
|
59
|
+
/** 后续路径 */
|
|
60
|
+
PathNodeType["Rest"] = "rest";
|
|
61
|
+
})(PathNodeType || (PathNodeType = {}));
|
|
62
|
+
/**
|
|
63
|
+
* 把路由定义转换成正则表达式
|
|
64
|
+
*
|
|
65
|
+
* Nodes | Path | RegExp
|
|
66
|
+
* -------------------------- | ------------- | ----------------------------
|
|
67
|
+
* [] | | ^$
|
|
68
|
+
* ['abc'] | abc | ^abc$
|
|
69
|
+
* ['abc', named:foo, 'xyz'] | abc/:foo/xyz | ^abc/([^/]+?)/xyz$
|
|
70
|
+
* ['abc', named:foo?, 'xyz'] | abc/:foo?/xyz | ^abc(?:/([^/]+?))?/xyz$
|
|
71
|
+
* ['abc', named:foo?, rest] | abc/:foo?/* | ^abc(?:/([^/]+?))?(?:/(.+))?$
|
|
72
|
+
*
|
|
73
|
+
* ^(?:/(.+))?$
|
|
74
|
+
*/
|
|
75
|
+
function parsedPathToRegExp(parsedPath) {
|
|
76
|
+
const regexpParts = [];
|
|
77
|
+
for (const node of parsedPath) {
|
|
78
|
+
const prefix = node === parsedPath[0] ? '' : '/';
|
|
79
|
+
if (node.type === PathNodeType.Text) {
|
|
80
|
+
regexpParts.push(prefix + escapeRegExp(node.text));
|
|
81
|
+
}
|
|
82
|
+
else if (node.type === PathNodeType.Named) {
|
|
83
|
+
regexpParts.push(node.optional ? `(?:${prefix}([^/]+?))?` : `${prefix}([^/]+?)`);
|
|
84
|
+
}
|
|
85
|
+
else {
|
|
86
|
+
regexpParts.push(`(?:${prefix}(.+))?`);
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
return new RegExp('^' + regexpParts.join('') + '$', 'i');
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* 比较两个路径的优先级(两个路由都能匹配路径时,优先级更高的生效)
|
|
93
|
+
*
|
|
94
|
+
* pathA 优先:-1
|
|
95
|
+
* pathB 优先:1
|
|
96
|
+
*
|
|
97
|
+
* 规则:
|
|
98
|
+
* 1. 挨个 node 比对
|
|
99
|
+
* 2. 先匹配各节点的类型,text > named > named optional > rest
|
|
100
|
+
* 3. 节点类型都一致,节点数量多的优先级更高
|
|
101
|
+
* 4. 节点类型、数量都一样,后出现的覆盖先出现的
|
|
102
|
+
*/
|
|
103
|
+
function pathSorter(pathA, pathB) {
|
|
104
|
+
const typePriorities = [PathNodeType.Text, PathNodeType.Named, PathNodeType.Rest];
|
|
105
|
+
const length = Math.max(pathA.length, pathB.length);
|
|
106
|
+
for (let i = 0; i < length; i++) {
|
|
107
|
+
const a = pathA[i];
|
|
108
|
+
const b = pathB[i];
|
|
109
|
+
// 若 nodes 长度不一样,长的优先
|
|
110
|
+
if (a === undefined)
|
|
111
|
+
return -1;
|
|
112
|
+
else if (b === undefined)
|
|
113
|
+
return 1;
|
|
114
|
+
// node 类型不一样,按类型排序
|
|
115
|
+
const aTypePriority = typePriorities.indexOf(a.type);
|
|
116
|
+
const bTypePriority = typePriorities.indexOf(b.type);
|
|
117
|
+
if (aTypePriority !== bTypePriority)
|
|
118
|
+
return aTypePriority - bTypePriority;
|
|
119
|
+
// 若都是 named 类型但 optional 不同
|
|
120
|
+
if (a.type === PathNodeType.Named &&
|
|
121
|
+
b.type === PathNodeType.Named &&
|
|
122
|
+
a.optional !== b.optional) {
|
|
123
|
+
return a.optional ? 1 : -1;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// 长度、类型都一样,后出现的优先
|
|
127
|
+
return 1;
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* 返回匹配的路由及匹配上的路径参数
|
|
131
|
+
*/
|
|
132
|
+
export function matchRoutes(requestPath, routes) {
|
|
133
|
+
if (!parsedCache.has(routes)) {
|
|
134
|
+
// 解析路由路径并按优先级排序
|
|
135
|
+
const parsedRoutes = routes
|
|
136
|
+
.map(route => {
|
|
137
|
+
const parsedPath = parseRoutePath(route.path);
|
|
138
|
+
const regexp = parsedPathToRegExp(parsedPath);
|
|
139
|
+
return { parsedPath, regexp, route };
|
|
140
|
+
})
|
|
141
|
+
.sort((a, b) => pathSorter(a.parsedPath, b.parsedPath));
|
|
142
|
+
parsedCache.set(routes, parsedRoutes);
|
|
143
|
+
}
|
|
144
|
+
requestPath = normalizePath(requestPath);
|
|
145
|
+
const parsedRoutes = parsedCache.get(routes);
|
|
146
|
+
return parsedRoutes
|
|
147
|
+
.map(({ parsedPath, regexp, route }) => {
|
|
148
|
+
const match = regexp.exec(requestPath);
|
|
149
|
+
if (!match)
|
|
150
|
+
return null;
|
|
151
|
+
const pathParameters = matchPathParameters(parsedPath, [...match].slice(1));
|
|
152
|
+
return { route, pathParameters };
|
|
153
|
+
})
|
|
154
|
+
.filter((matched) => matched !== null);
|
|
155
|
+
}
|
|
156
|
+
const parsedCache = new WeakMap();
|
|
157
|
+
/**
|
|
158
|
+
* 把匹配到的路径参数映射成键值对
|
|
159
|
+
* 没匹配到的内容(optional named node / rest node)值为 undefined
|
|
160
|
+
*/
|
|
161
|
+
function matchPathParameters(parsedPath, parameterNames) {
|
|
162
|
+
const parameters = {};
|
|
163
|
+
for (const node of parsedPath) {
|
|
164
|
+
if (node.type === PathNodeType.Text)
|
|
165
|
+
continue;
|
|
166
|
+
else if (node.type === PathNodeType.Named)
|
|
167
|
+
parameters[node.name] = parameterNames.shift();
|
|
168
|
+
else
|
|
169
|
+
parameters['*'] = parameterNames.shift();
|
|
170
|
+
}
|
|
171
|
+
return parameters;
|
|
172
|
+
}
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { type Request } from '../http/index.js';
|
|
2
|
+
import { type Route } from './router.js';
|
|
3
|
+
/**
|
|
4
|
+
* ----------------------------------------
|
|
5
|
+
* 类型定义
|
|
6
|
+
* ----------------------------------------
|
|
7
|
+
*/
|
|
8
|
+
/**
|
|
9
|
+
* 请求参数定义
|
|
10
|
+
*/
|
|
11
|
+
export interface Parameter {
|
|
12
|
+
/** 字段名 */
|
|
13
|
+
name: string;
|
|
14
|
+
/** 文字描述 */
|
|
15
|
+
describe?: string;
|
|
16
|
+
type: ParameterDataType;
|
|
17
|
+
/** 默认值,应与 type 匹配。若指定,则不再需要 required 规则。 */
|
|
18
|
+
defaults?: unknown;
|
|
19
|
+
/** 是否允许为 undefined */
|
|
20
|
+
required?: boolean;
|
|
21
|
+
/** 是否允许为 null */
|
|
22
|
+
nullable?: boolean;
|
|
23
|
+
/** 验证规则,会传给 type 对应的 validator */
|
|
24
|
+
validate?: Record<string, unknown>;
|
|
25
|
+
}
|
|
26
|
+
export type BasicDataType = 'string' | 'number' | 'boolean';
|
|
27
|
+
type BasicParameter = Pick<Parameter, 'type' | 'validate' | 'required' | 'nullable' | 'defaults'>;
|
|
28
|
+
/**
|
|
29
|
+
* 请求参数的数据类型定义
|
|
30
|
+
*/
|
|
31
|
+
export type ParameterDataType = BasicDataType | {
|
|
32
|
+
array: BasicParameter;
|
|
33
|
+
} | {
|
|
34
|
+
record: BasicParameter;
|
|
35
|
+
} | {
|
|
36
|
+
object: Record<string, BasicParameter>;
|
|
37
|
+
};
|
|
38
|
+
/**
|
|
39
|
+
* ----------------------------------------
|
|
40
|
+
* 与请求内容对接
|
|
41
|
+
* ----------------------------------------
|
|
42
|
+
*/
|
|
43
|
+
/**
|
|
44
|
+
* 验证 query 内容
|
|
45
|
+
*/
|
|
46
|
+
export declare function parseQuery<T>(route: Route, request: Request): T;
|
|
47
|
+
/**
|
|
48
|
+
* 验证 body 内容
|
|
49
|
+
*/
|
|
50
|
+
export declare function parseJSONBody<T>(route: Route, request: Request): Promise<T>;
|
|
51
|
+
export {};
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 实现通过 query / body parameters 的解析和验证
|
|
3
|
+
*/
|
|
4
|
+
import { success } from '@anjianshi/utils';
|
|
5
|
+
import { HTTPError } from '../http/index.js';
|
|
6
|
+
import * as validators from '../validators/index.js';
|
|
7
|
+
/**
|
|
8
|
+
* ----------------------------------------
|
|
9
|
+
* 基础功能实现
|
|
10
|
+
* ----------------------------------------
|
|
11
|
+
*/
|
|
12
|
+
/**
|
|
13
|
+
* 基于 parameter 定义验证、格式化数据
|
|
14
|
+
*/
|
|
15
|
+
function validateParameters(parameters, rawData) {
|
|
16
|
+
const result = {};
|
|
17
|
+
for (const parameter of parameters) {
|
|
18
|
+
const validator = getValidatorOfParameter(parameter);
|
|
19
|
+
const validateRes = validator.validate(parameter.name, rawData[parameter.name]);
|
|
20
|
+
if (validateRes.success)
|
|
21
|
+
result[parameter.name] = validateRes.data;
|
|
22
|
+
else
|
|
23
|
+
return validateRes;
|
|
24
|
+
}
|
|
25
|
+
return success(result);
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* 获取指定 parameter 的 validator
|
|
29
|
+
*/
|
|
30
|
+
function getValidatorOfParameter(parameter) {
|
|
31
|
+
if (!validatorCache.has(parameter)) {
|
|
32
|
+
const validatorConstructor = getValidatorConstructor(parameter.type);
|
|
33
|
+
const options = {
|
|
34
|
+
null: parameter.nullable,
|
|
35
|
+
void: typeof parameter.required === 'boolean' ? parameter.required : undefined,
|
|
36
|
+
defaults: parameter.defaults,
|
|
37
|
+
...(parameter.validate ?? {}),
|
|
38
|
+
};
|
|
39
|
+
const validator = validatorConstructor(options);
|
|
40
|
+
validatorCache.set(parameter, validator);
|
|
41
|
+
}
|
|
42
|
+
return validatorCache.get(parameter);
|
|
43
|
+
}
|
|
44
|
+
const validatorCache = new WeakMap();
|
|
45
|
+
/**
|
|
46
|
+
* 取得与参数类型对应的 validator constructor
|
|
47
|
+
*/
|
|
48
|
+
function getValidatorConstructor(type) {
|
|
49
|
+
if (typeof type === 'string')
|
|
50
|
+
return validators[type];
|
|
51
|
+
if ('array' in type) {
|
|
52
|
+
const itemValidator = getValidatorOfParameter(type.array);
|
|
53
|
+
return validators.array.bind(null, itemValidator);
|
|
54
|
+
}
|
|
55
|
+
if ('record' in type) {
|
|
56
|
+
const itemValidator = getValidatorOfParameter(type.record);
|
|
57
|
+
return validators.object.bind(null, itemValidator);
|
|
58
|
+
}
|
|
59
|
+
const struct = {};
|
|
60
|
+
for (const [key, innerParameter] of Object.entries(type.object)) {
|
|
61
|
+
struct[key] = getValidatorOfParameter(innerParameter);
|
|
62
|
+
}
|
|
63
|
+
return validators.object.bind(null, struct);
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* ----------------------------------------
|
|
67
|
+
* 与请求内容对接
|
|
68
|
+
* ----------------------------------------
|
|
69
|
+
*/
|
|
70
|
+
/**
|
|
71
|
+
* 验证 query 内容
|
|
72
|
+
*/
|
|
73
|
+
export function parseQuery(route, request) {
|
|
74
|
+
if (!route.query)
|
|
75
|
+
throw new HTTPError(500, 'Call parseQuery() need query parameter definition.');
|
|
76
|
+
const values = {};
|
|
77
|
+
for (const parameter of route.query) {
|
|
78
|
+
values[parameter.name] = getQueryParameterValue(request, parameter);
|
|
79
|
+
}
|
|
80
|
+
const res = validateParameters(route.query, values);
|
|
81
|
+
if (res.success)
|
|
82
|
+
return res.data;
|
|
83
|
+
throw new HTTPError(400, res.error);
|
|
84
|
+
}
|
|
85
|
+
function getQueryParameterValue(request, parameter) {
|
|
86
|
+
// array
|
|
87
|
+
if (typeof parameter.type === 'object' && 'array' in parameter.type) {
|
|
88
|
+
return request.query.getAll(parameter.name + '[]');
|
|
89
|
+
}
|
|
90
|
+
// undefined
|
|
91
|
+
const raw = request.query.get(parameter.name);
|
|
92
|
+
if (raw === null || raw === '')
|
|
93
|
+
return undefined;
|
|
94
|
+
// basic type
|
|
95
|
+
if (typeof parameter.type === 'string')
|
|
96
|
+
return raw;
|
|
97
|
+
// object
|
|
98
|
+
try {
|
|
99
|
+
return JSON.parse(raw);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
throw new HTTPError(400, `Invalid JSON value for query parameter "${parameter.name}".`);
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* 验证 body 内容
|
|
107
|
+
*/
|
|
108
|
+
export async function parseJSONBody(route, request) {
|
|
109
|
+
if (!route.body)
|
|
110
|
+
throw new HTTPError(500, 'call parseJSONBody() need body parameter definition.');
|
|
111
|
+
const body = await request.body.json();
|
|
112
|
+
if (typeof body !== 'object' || body === null || Array.isArray(body))
|
|
113
|
+
throw new HTTPError(400, 'Invalid JSON body, should be an object.');
|
|
114
|
+
const res = validateParameters(route.body, body);
|
|
115
|
+
if (res.success)
|
|
116
|
+
return res.data;
|
|
117
|
+
throw new HTTPError(400, res.error);
|
|
118
|
+
}
|
|
@@ -0,0 +1,127 @@
|
|
|
1
|
+
import { type RequiredFields } from '@anjianshi/utils';
|
|
2
|
+
import { type Request, type ResponseUtils } from '../http/index.js';
|
|
3
|
+
import { type CORSOptions } from './cors.js';
|
|
4
|
+
import { type PathParameters, type RouteMatch } from './match.js';
|
|
5
|
+
import { type BasicDataType, type Parameter } from './parameters.js';
|
|
6
|
+
/**
|
|
7
|
+
* -----------------------------------------
|
|
8
|
+
* 类型定义
|
|
9
|
+
* -----------------------------------------
|
|
10
|
+
*/
|
|
11
|
+
/**
|
|
12
|
+
* 请求上下文(由 Router 生成并传给请求处理函数)
|
|
13
|
+
*/
|
|
14
|
+
export interface BaseContext {
|
|
15
|
+
request: Request;
|
|
16
|
+
response: ResponseUtils;
|
|
17
|
+
parseQuery: <T>() => T;
|
|
18
|
+
parseBody: <T>() => Promise<T>;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* 请求处理函数
|
|
22
|
+
*/
|
|
23
|
+
export type RouteHandler<Ctx extends BaseContext = BaseContext, PathP extends PathParameters = PathParameters> = (context: Ctx, pathParameters: PathP) => unknown;
|
|
24
|
+
/**
|
|
25
|
+
* 路由定义
|
|
26
|
+
*
|
|
27
|
+
* 相比 RawRoute:
|
|
28
|
+
* - 有默认值的字段均填充了默认值
|
|
29
|
+
* - method 一定为全大写
|
|
30
|
+
*/
|
|
31
|
+
export type Route<Ctx extends BaseContext = BaseContext, PathP extends PathParameters = PathParameters> = RequiredFields<RawRoute<Ctx, PathP>, 'describe' | 'category' | 'method'>;
|
|
32
|
+
export interface RawRoute<Ctx extends BaseContext = BaseContext, PathP extends PathParameters = PathParameters> {
|
|
33
|
+
/** 接口路径。支持变量(/abc/:xxx/def),详见 src/router/match.ts */
|
|
34
|
+
path: string;
|
|
35
|
+
/** 接口描述 */
|
|
36
|
+
describe?: string;
|
|
37
|
+
/** 接口类别 */
|
|
38
|
+
category?: string;
|
|
39
|
+
/** 接口 HTTP Method,有 body 定义的默认为 POST,否则默认为 GET */
|
|
40
|
+
method?: string;
|
|
41
|
+
/** Query String 定义 */
|
|
42
|
+
query?: Parameter[];
|
|
43
|
+
/** JSON Request Body 定义 */
|
|
44
|
+
body?: Parameter[];
|
|
45
|
+
/**
|
|
46
|
+
* JSON 返回内容定义(返回的不是 JSON 时(如纯文本、二进制文件),不定义此项)
|
|
47
|
+
* - 若指定的是 ResponseDataType[],代表有多种可能返回的类型。例如 ['string' | 'null'] 可能返回字符串也可能返回 null
|
|
48
|
+
*/
|
|
49
|
+
response?: ResponseDataType | ResponseDataType[];
|
|
50
|
+
/**
|
|
51
|
+
* 指定此接口的 CORS 配置
|
|
52
|
+
* 不指定则由 router.getCORSOptions() 提供
|
|
53
|
+
*/
|
|
54
|
+
cors?: CORSOptions;
|
|
55
|
+
handler: RouteHandler<Ctx, PathP>;
|
|
56
|
+
}
|
|
57
|
+
/**
|
|
58
|
+
* JSON 响应内容的类型定义
|
|
59
|
+
* { array: ResponseDataType[] } 代表数组元素有多种可能的类型
|
|
60
|
+
*/
|
|
61
|
+
export type ResponseDataType = BasicDataType | 'null' | {
|
|
62
|
+
array: ResponseDataType | ResponseDataType[];
|
|
63
|
+
} | {
|
|
64
|
+
object: ResponseObjectItemType[];
|
|
65
|
+
} | {
|
|
66
|
+
ref: string;
|
|
67
|
+
summary?: string;
|
|
68
|
+
description?: string;
|
|
69
|
+
};
|
|
70
|
+
/**
|
|
71
|
+
* JSON object 相应内容的字段定义
|
|
72
|
+
*/
|
|
73
|
+
export interface ResponseObjectItemType {
|
|
74
|
+
/** 字段名 */
|
|
75
|
+
name: string;
|
|
76
|
+
/** 文字描述 */
|
|
77
|
+
describe?: string;
|
|
78
|
+
/** 值类型。ResponseDataType[] 代表有多种可能的类型 */
|
|
79
|
+
type: ResponseDataType | ResponseDataType[];
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* -----------------------------------------
|
|
83
|
+
* Router 类
|
|
84
|
+
* -----------------------------------------
|
|
85
|
+
*/
|
|
86
|
+
export declare abstract class Router<Ctx extends BaseContext = BaseContext> {
|
|
87
|
+
/**
|
|
88
|
+
* 路由列表
|
|
89
|
+
*/
|
|
90
|
+
readonly routes: Route<Ctx>[];
|
|
91
|
+
/**
|
|
92
|
+
* 注册路由
|
|
93
|
+
*/
|
|
94
|
+
register<P = Record<string, never>>(method: string, path: string, handler: RouteHandler<Ctx, PathParameters & P>): void;
|
|
95
|
+
register<P extends Record<string, string>>(raw: RawRoute<Ctx, PathParameters & P>): void;
|
|
96
|
+
/**
|
|
97
|
+
* 响应请求
|
|
98
|
+
*/
|
|
99
|
+
readonly handle: (request: Request, response: ResponseUtils) => Promise<void>;
|
|
100
|
+
/**
|
|
101
|
+
* 匹配路由,成功返回路由对象,失败抛出 HTTPError,提前结束处理返回 null
|
|
102
|
+
*/
|
|
103
|
+
protected match(request: Request, response: ResponseUtils): RouteMatch<Route<Ctx, PathParameters>> | null;
|
|
104
|
+
/**
|
|
105
|
+
* 为未指定 CORS 配置的 route 提供默认配置
|
|
106
|
+
*/
|
|
107
|
+
getCORSOptions(request: Request, routeMatch: RouteMatch<Route<Ctx>>): boolean;
|
|
108
|
+
/**
|
|
109
|
+
* 完善 context 对象并执行 route
|
|
110
|
+
* 注意:handler 在很多时候都是异步的,要用 await 等待执行完成
|
|
111
|
+
*/
|
|
112
|
+
protected abstract executeWithContext(baseContext: BaseContext, route: Route<Ctx>, pathParameters: PathParameters): void | Promise<void>;
|
|
113
|
+
/**
|
|
114
|
+
* response 定义中可引用的数据类型
|
|
115
|
+
*/
|
|
116
|
+
readonly responseReferences: Record<string, ResponseDataType>;
|
|
117
|
+
/**
|
|
118
|
+
* 注册 response 定义中引用的数据类型
|
|
119
|
+
*/
|
|
120
|
+
registerResponseReference(id: string, type: ResponseDataType): void;
|
|
121
|
+
}
|
|
122
|
+
/**
|
|
123
|
+
* 默认的 Router 实现
|
|
124
|
+
*/
|
|
125
|
+
export declare class DefaultRouter extends Router {
|
|
126
|
+
executeWithContext(baseContext: BaseContext, route: Route, pathParameters: PathParameters): Promise<void>;
|
|
127
|
+
}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { HTTPError } from '../http/index.js';
|
|
2
|
+
import { getMethodFromCORSPreflight, outputCORSHeaders } from './cors.js';
|
|
3
|
+
import { matchRoutes } from './match.js';
|
|
4
|
+
import { parseQuery, parseJSONBody } from './parameters.js';
|
|
5
|
+
/**
|
|
6
|
+
* -----------------------------------------
|
|
7
|
+
* Router 类
|
|
8
|
+
* -----------------------------------------
|
|
9
|
+
*/
|
|
10
|
+
export class Router {
|
|
11
|
+
/**
|
|
12
|
+
* 路由列表
|
|
13
|
+
*/
|
|
14
|
+
routes = [];
|
|
15
|
+
register(raw, path, handler) {
|
|
16
|
+
const rawRoute = typeof raw === 'string' ? { method: raw, path: path, handler: handler } : raw;
|
|
17
|
+
const route = {
|
|
18
|
+
describe: '',
|
|
19
|
+
category: '',
|
|
20
|
+
...rawRoute,
|
|
21
|
+
method: (rawRoute.method ?? (rawRoute.body ? 'POST' : 'GET')).toUpperCase(),
|
|
22
|
+
};
|
|
23
|
+
this.routes.push(route);
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 响应请求
|
|
27
|
+
*/
|
|
28
|
+
handle = async (request, response) => {
|
|
29
|
+
const matched = this.match(request, response);
|
|
30
|
+
if (matched === null)
|
|
31
|
+
return;
|
|
32
|
+
const baseContext = getBaseContext(matched.route, request, response);
|
|
33
|
+
await this.executeWithContext(baseContext, matched.route, matched.pathParameters);
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* 匹配路由,成功返回路由对象,失败抛出 HTTPError,提前结束处理返回 null
|
|
37
|
+
*/
|
|
38
|
+
match(request, response) {
|
|
39
|
+
const matches = matchRoutes(request.path, this.routes);
|
|
40
|
+
if (!matches.length)
|
|
41
|
+
throw new HTTPError(404);
|
|
42
|
+
const methodFromPreflight = getMethodFromCORSPreflight(request);
|
|
43
|
+
const isPreflight = methodFromPreflight !== null;
|
|
44
|
+
const method = methodFromPreflight ?? request.method;
|
|
45
|
+
const methodMatched = matches.find(match => match.route.method === method);
|
|
46
|
+
if (!methodMatched)
|
|
47
|
+
throw new HTTPError(405);
|
|
48
|
+
const corsOptions = methodMatched.route.cors ?? this.getCORSOptions(request, methodMatched);
|
|
49
|
+
outputCORSHeaders(response, corsOptions, isPreflight);
|
|
50
|
+
if (isPreflight) {
|
|
51
|
+
response.nodeResponse.end('');
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
return methodMatched;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 为未指定 CORS 配置的 route 提供默认配置
|
|
58
|
+
*/
|
|
59
|
+
// eslint-disable-next-line @typescript-eslint/no-unused-vars
|
|
60
|
+
getCORSOptions(request, routeMatch) {
|
|
61
|
+
return false;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* response 定义中可引用的数据类型
|
|
65
|
+
*/
|
|
66
|
+
responseReferences = {};
|
|
67
|
+
/**
|
|
68
|
+
* 注册 response 定义中引用的数据类型
|
|
69
|
+
*/
|
|
70
|
+
registerResponseReference(id, type) {
|
|
71
|
+
this.responseReferences[id] = type;
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* ------------------------
|
|
76
|
+
* 默认实现
|
|
77
|
+
* ------------------------
|
|
78
|
+
*/
|
|
79
|
+
/**
|
|
80
|
+
* 基础 Context 实现
|
|
81
|
+
*/
|
|
82
|
+
function getBaseContext(route, request, response) {
|
|
83
|
+
return {
|
|
84
|
+
request,
|
|
85
|
+
response,
|
|
86
|
+
parseQuery: () => parseQuery(route, request),
|
|
87
|
+
parseBody: async () => parseJSONBody(route, request),
|
|
88
|
+
};
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* 默认的 Router 实现
|
|
92
|
+
*/
|
|
93
|
+
export class DefaultRouter extends Router {
|
|
94
|
+
async executeWithContext(baseContext, route, pathParameters) {
|
|
95
|
+
await route.handler(baseContext, pathParameters);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|