starlight-server 1.2.0 → 1.5.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/README.md +11 -1
- package/dist/demo/index.js +41 -27
- package/dist/http/cors.d.ts +33 -0
- package/dist/http/cors.js +33 -0
- package/dist/http/index.d.ts +1 -0
- package/dist/http/index.js +1 -0
- package/dist/http/mime-types.js +1 -20
- package/dist/http/request.d.ts +18 -2
- package/dist/http/request.js +28 -7
- package/dist/http/response.d.ts +9 -5
- package/dist/http/response.js +16 -8
- package/dist/http/server.d.ts +1 -1
- package/dist/http/server.js +1 -1
- package/dist/index.d.ts +3 -1
- package/dist/index.js +3 -2
- package/dist/logging.d.ts +4 -3
- package/dist/logging.js +7 -19
- package/dist/router/helpers.d.ts +6 -0
- package/dist/router/helpers.js +24 -0
- package/dist/router/index.d.ts +67 -38
- package/dist/router/index.js +107 -36
- package/dist/router/match-path.d.ts +13 -0
- package/dist/router/match-path.js +160 -0
- package/dist/swagger/factories.d.ts +143 -0
- package/dist/swagger/factories.js +231 -0
- package/dist/swagger/index.d.ts +54 -6
- package/dist/swagger/index.js +94 -58
- package/dist/swagger/specification.d.ts +802 -0
- package/dist/swagger/specification.js +130 -0
- package/package.json +10 -10
- package/src/demo/index.ts +45 -42
- package/src/http/cors.ts +59 -0
- package/src/http/index.ts +1 -0
- package/src/http/mime-types.ts +1 -20
- package/src/http/request.ts +38 -10
- package/src/http/response.ts +18 -17
- package/src/http/server.ts +2 -2
- package/src/index.ts +3 -2
- package/src/logging.ts +7 -21
- package/src/router/helpers.ts +27 -0
- package/src/router/index.ts +141 -38
- package/src/router/match-path.ts +178 -0
- package/src/swagger/factories.ts +345 -0
- package/src/swagger/index.ts +113 -80
- package/src/swagger/specification.ts +823 -0
- package/dist/router/cors.d.ts +0 -24
- package/dist/router/cors.js +0 -35
- package/dist/router/match.d.ts +0 -23
- package/dist/router/match.js +0 -172
- package/dist/router/parameters.d.ts +0 -50
- package/dist/router/parameters.js +0 -118
- package/dist/router/router.d.ts +0 -128
- package/dist/router/router.js +0 -97
- package/dist/swagger/factory.d.ts +0 -97
- package/dist/swagger/factory.js +0 -144
- package/dist/swagger/openapi-spec.d.ts +0 -261
- package/dist/swagger/openapi-spec.js +0 -5
- package/dist/validators/array.d.ts +0 -9
- package/dist/validators/array.js +0 -28
- package/dist/validators/boolean.d.ts +0 -4
- package/dist/validators/boolean.js +0 -28
- package/dist/validators/common.d.ts +0 -23
- package/dist/validators/common.js +0 -25
- package/dist/validators/index.d.ts +0 -20
- package/dist/validators/index.js +0 -38
- package/dist/validators/number.d.ts +0 -10
- package/dist/validators/number.js +0 -30
- package/dist/validators/object.d.ts +0 -13
- package/dist/validators/object.js +0 -36
- package/dist/validators/string.d.ts +0 -11
- package/dist/validators/string.js +0 -29
- package/src/.DS_Store +0 -0
- package/src/router/cors.ts +0 -54
- package/src/router/match.ts +0 -194
- package/src/router/parameters.ts +0 -175
- package/src/router/router.ts +0 -234
- package/src/swagger/factory.ts +0 -169
- package/src/swagger/openapi-spec.ts +0 -312
- package/src/validators/array.ts +0 -33
- package/src/validators/boolean.ts +0 -23
- package/src/validators/common.ts +0 -46
- package/src/validators/index.ts +0 -50
- package/src/validators/number.ts +0 -36
- package/src/validators/object.ts +0 -41
- package/src/validators/string.ts +0 -38
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { Validator, type CommonOptions } from './common.js';
|
|
2
|
-
export interface NumberOptions {
|
|
3
|
-
min?: number;
|
|
4
|
-
max?: number;
|
|
5
|
-
float: boolean;
|
|
6
|
-
}
|
|
7
|
-
export declare class NumberValidator extends Validator<NumberOptions> {
|
|
8
|
-
constructor(options?: Partial<NumberOptions> & Partial<CommonOptions>);
|
|
9
|
-
validate(fieldName: string, value: unknown): import("@anjianshi/utils").Success<unknown> | import("@anjianshi/utils").Failed<string>;
|
|
10
|
-
}
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { success, failed } from '@anjianshi/utils';
|
|
2
|
-
import { Validator } from './common.js';
|
|
3
|
-
export class NumberValidator extends Validator {
|
|
4
|
-
constructor(options) {
|
|
5
|
-
super({
|
|
6
|
-
float: false,
|
|
7
|
-
...(options ?? {}),
|
|
8
|
-
});
|
|
9
|
-
}
|
|
10
|
-
validate(fieldName, value) {
|
|
11
|
-
const superResult = super.validate(fieldName, value);
|
|
12
|
-
if (!superResult.success)
|
|
13
|
-
return superResult;
|
|
14
|
-
value = superResult.data;
|
|
15
|
-
if (value === null || value === undefined)
|
|
16
|
-
return superResult;
|
|
17
|
-
const opt = this.options;
|
|
18
|
-
if (typeof value === 'string')
|
|
19
|
-
value = parseFloat(value);
|
|
20
|
-
if (typeof value !== 'number' || !isFinite(value))
|
|
21
|
-
return failed(`${fieldName} must be a valid number`);
|
|
22
|
-
if (!opt.float && value % 1 !== 0)
|
|
23
|
-
return failed(`${fieldName} must be a integer`);
|
|
24
|
-
if (typeof opt.min === 'number' && value < opt.min)
|
|
25
|
-
return failed(`${fieldName} must >= ${opt.min}`);
|
|
26
|
-
if (typeof opt.max === 'number' && value > opt.max)
|
|
27
|
-
return failed(`${fieldName} must <= ${opt.max}`);
|
|
28
|
-
return success(value);
|
|
29
|
-
}
|
|
30
|
-
}
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
import { Validator } from './common.js';
|
|
2
|
-
/**
|
|
3
|
-
* struct 用于有明确键值对结构的对象;value 用于有任意 key 的对象
|
|
4
|
-
* use `struct` is used for objects with an explicit key-value structure; `value` is used for objects with arbitrary keys.
|
|
5
|
-
*/
|
|
6
|
-
export type ObjectOptions = {
|
|
7
|
-
struct: Record<string, Validator>;
|
|
8
|
-
} | {
|
|
9
|
-
value: Validator;
|
|
10
|
-
};
|
|
11
|
-
export declare class ObjectValidator extends Validator<ObjectOptions> {
|
|
12
|
-
validate(fieldName: string, value: unknown): import("@anjianshi/utils").Success<unknown> | import("@anjianshi/utils").Failed<string>;
|
|
13
|
-
}
|
|
@@ -1,36 +0,0 @@
|
|
|
1
|
-
import { success, failed } from '@anjianshi/utils';
|
|
2
|
-
import isPlainObject from 'lodash/isPlainObject.js';
|
|
3
|
-
import { Validator } from './common.js';
|
|
4
|
-
export class ObjectValidator extends Validator {
|
|
5
|
-
validate(fieldName, value) {
|
|
6
|
-
const superResult = super.validate(fieldName, value);
|
|
7
|
-
if (!superResult.success)
|
|
8
|
-
return superResult;
|
|
9
|
-
value = superResult.data;
|
|
10
|
-
if (value === null || value === undefined)
|
|
11
|
-
return superResult;
|
|
12
|
-
const opt = this.options;
|
|
13
|
-
if (!isPlainObject(value))
|
|
14
|
-
return failed(`${fieldName} should be a plain object`);
|
|
15
|
-
const formatted = {};
|
|
16
|
-
if ('struct' in opt) {
|
|
17
|
-
for (const [key, itemValidator] of Object.entries(opt.struct)) {
|
|
18
|
-
const itemResult = itemValidator.validate(`${fieldName}["${key}"]`, value[key]);
|
|
19
|
-
if (itemResult.success)
|
|
20
|
-
formatted[key] = itemResult.data;
|
|
21
|
-
else
|
|
22
|
-
return itemResult;
|
|
23
|
-
}
|
|
24
|
-
}
|
|
25
|
-
else {
|
|
26
|
-
for (const [key, itemValue] of Object.entries(value)) {
|
|
27
|
-
const itemResult = opt.value.validate(`${fieldName}["${key}"]`, itemValue);
|
|
28
|
-
if (itemResult.success)
|
|
29
|
-
formatted[key] = itemResult.data;
|
|
30
|
-
else
|
|
31
|
-
return itemResult;
|
|
32
|
-
}
|
|
33
|
-
}
|
|
34
|
-
return success(formatted);
|
|
35
|
-
}
|
|
36
|
-
}
|
|
@@ -1,11 +0,0 @@
|
|
|
1
|
-
import { Validator, type CommonOptions } from './common.js';
|
|
2
|
-
export interface StringOptions {
|
|
3
|
-
min?: number;
|
|
4
|
-
max?: number;
|
|
5
|
-
pattern?: RegExp;
|
|
6
|
-
trim: boolean;
|
|
7
|
-
}
|
|
8
|
-
export declare class StringValidator extends Validator<StringOptions> {
|
|
9
|
-
constructor(options?: Partial<StringOptions> & Partial<CommonOptions>);
|
|
10
|
-
validate(fieldName: string, value: unknown): import("@anjianshi/utils").Success<unknown> | import("@anjianshi/utils").Failed<string>;
|
|
11
|
-
}
|
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import { success, failed } from '@anjianshi/utils';
|
|
2
|
-
import { Validator } from './common.js';
|
|
3
|
-
export class StringValidator extends Validator {
|
|
4
|
-
constructor(options) {
|
|
5
|
-
super({
|
|
6
|
-
trim: true,
|
|
7
|
-
...(options ?? {}),
|
|
8
|
-
});
|
|
9
|
-
}
|
|
10
|
-
validate(fieldName, value) {
|
|
11
|
-
const superResult = super.validate(fieldName, value);
|
|
12
|
-
if (!superResult.success)
|
|
13
|
-
return superResult;
|
|
14
|
-
value = superResult.data;
|
|
15
|
-
if (value === null || value === undefined)
|
|
16
|
-
return superResult;
|
|
17
|
-
const opt = this.options;
|
|
18
|
-
if (typeof value !== 'string')
|
|
19
|
-
return failed(`${fieldName} should be a string`);
|
|
20
|
-
const formatted = opt.trim ? value.trim() : value;
|
|
21
|
-
if (typeof opt.min === 'number' && formatted.length < opt.min)
|
|
22
|
-
return failed(`${fieldName}'s length must >= ${opt.min}`);
|
|
23
|
-
if (typeof opt.max === 'number' && formatted.length > opt.max)
|
|
24
|
-
return failed(`${fieldName}'s length must <= ${opt.max}`);
|
|
25
|
-
if (opt.pattern && !opt.pattern.exec(formatted))
|
|
26
|
-
return failed(`${fieldName} does not match the pattern.`);
|
|
27
|
-
return success(formatted);
|
|
28
|
-
}
|
|
29
|
-
}
|
package/src/.DS_Store
DELETED
|
Binary file
|
package/src/router/cors.ts
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { type Request, type ResponseUtils } from '@/http/index.js'
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* CORS 配置
|
|
5
|
-
*
|
|
6
|
-
* - false: 不处理跨域
|
|
7
|
-
* - true: 支持且不设限(等同于 { allowOrigin: '*', allowHeaders: '*', exposeHeaders: '*' })
|
|
8
|
-
* - { allowOrigin, allowHeaders, exposeHeaders }: 手动配置
|
|
9
|
-
*
|
|
10
|
-
* allowHeaders 仅对 Preflight 请求有效
|
|
11
|
-
* exposeHeaders 仅对非 Preflight 请求有效
|
|
12
|
-
*/
|
|
13
|
-
export type CORSOptions =
|
|
14
|
-
| boolean
|
|
15
|
-
| { allowOrigin?: string; allowHeaders?: string; exposeHeaders?: string }
|
|
16
|
-
|
|
17
|
-
/**
|
|
18
|
-
* 若当前请求是 CORS Preflight 请求,返回实际请求的 method,否则返回 null
|
|
19
|
-
*/
|
|
20
|
-
export function getMethodFromCORSPreflight(request: Request) {
|
|
21
|
-
if (
|
|
22
|
-
request.method === 'OPTIONS' &&
|
|
23
|
-
request.headers['Access-Control-Request-Methods'] !== undefined
|
|
24
|
-
) {
|
|
25
|
-
const rawMethod = request.headers['Access-Control-Request-Methods']
|
|
26
|
-
return Array.isArray(rawMethod) ? rawMethod[0]! : rawMethod
|
|
27
|
-
}
|
|
28
|
-
return null
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* 输出 CORS 相关 headers
|
|
33
|
-
*/
|
|
34
|
-
export function outputCORSHeaders(
|
|
35
|
-
response: ResponseUtils,
|
|
36
|
-
cors: CORSOptions,
|
|
37
|
-
isPreflight: boolean,
|
|
38
|
-
) {
|
|
39
|
-
if (cors === true) {
|
|
40
|
-
response.headers(['Access-Control-Allow-Origin', '*'])
|
|
41
|
-
if (isPreflight) response.headers(['Access-Control-Allow-Headers', '*'])
|
|
42
|
-
else response.headers(['Access-Control-Expose-Headers', '*'])
|
|
43
|
-
} else if (cors !== false) {
|
|
44
|
-
if (cors.allowOrigin !== undefined)
|
|
45
|
-
response.header('Access-Control-Allow-Origin', cors.allowOrigin)
|
|
46
|
-
if (isPreflight && cors.allowHeaders !== undefined)
|
|
47
|
-
response.header('Access-Control-Allow-Headers', cors.allowHeaders)
|
|
48
|
-
if (!isPreflight && cors.exposeHeaders !== undefined)
|
|
49
|
-
response.header('Access-Control-Expose-Headers', cors.exposeHeaders)
|
|
50
|
-
} else {
|
|
51
|
-
// 若指定为“不处理跨域”,则不输出 headers,浏览器会默认服务端不允许跨域请求
|
|
52
|
-
// (后续 route handler 仍有机会自行设置 CORS headers)
|
|
53
|
-
}
|
|
54
|
-
}
|
package/src/router/match.ts
DELETED
|
@@ -1,194 +0,0 @@
|
|
|
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
|
-
* 标准化路径
|
|
16
|
-
* 移除首尾和重复的 '/',完成后有 path 有这几种可能的格式: ''、'abc'、'abc/def'
|
|
17
|
-
* 例如 /abc/def 和 abc/def/ 都会变成 abc/def
|
|
18
|
-
*
|
|
19
|
-
* 注意:此操作不会统一大小写,因此不保证标准化后两个字符串在代码层面 ===
|
|
20
|
-
*/
|
|
21
|
-
export function normalizePath(path: string) {
|
|
22
|
-
if (path.startsWith('/')) path = path.slice(1)
|
|
23
|
-
if (path.endsWith('/')) path = path.slice(0, -1)
|
|
24
|
-
path = path.replace(/\/+/g, '/')
|
|
25
|
-
return path
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* 解析路由路径定义
|
|
30
|
-
* '/abc/:foo?/*' => [{ type: 'text', text: 'abc' }, { type: 'named', name: 'foo', optional: true }, { type: 'rest' }]
|
|
31
|
-
*/
|
|
32
|
-
function parseRoutePath(routePath: string): ParsedPath {
|
|
33
|
-
const normalizedPath = normalizePath(routePath)
|
|
34
|
-
|
|
35
|
-
const nodes: PathNode[] = []
|
|
36
|
-
const pathParts = normalizedPath.split('/')
|
|
37
|
-
for (const [i, part] of pathParts.entries()) {
|
|
38
|
-
if (part.startsWith(':') && part !== ':' && part !== ':?') {
|
|
39
|
-
const optional = part.endsWith('?')
|
|
40
|
-
const name = optional ? part.slice(1, -1) : part.slice(1)
|
|
41
|
-
if (name === '*') throw new Error('route path parameter name cannot be "*"')
|
|
42
|
-
nodes.push({ type: PathNodeType.Named, name, optional })
|
|
43
|
-
} else if (part === '*' && i === pathParts.length - 1) {
|
|
44
|
-
nodes.push({ type: PathNodeType.Rest })
|
|
45
|
-
} else {
|
|
46
|
-
nodes.push({ type: PathNodeType.Text, text: part })
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return nodes
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
enum PathNodeType {
|
|
54
|
-
/** 纯文本 */
|
|
55
|
-
Text = 'text',
|
|
56
|
-
/** 命名参数 */
|
|
57
|
-
Named = 'named',
|
|
58
|
-
/** 后续路径 */
|
|
59
|
-
Rest = 'rest',
|
|
60
|
-
}
|
|
61
|
-
type PathNode =
|
|
62
|
-
| { type: PathNodeType.Text; text: string }
|
|
63
|
-
| { type: PathNodeType.Named; name: string; optional: boolean }
|
|
64
|
-
| { type: PathNodeType.Rest }
|
|
65
|
-
type ParsedPath = PathNode[]
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
* 把路由定义转换成正则表达式
|
|
69
|
-
*
|
|
70
|
-
* Nodes | Path | RegExp
|
|
71
|
-
* -------------------------- | ------------- | ----------------------------
|
|
72
|
-
* [] | | ^$
|
|
73
|
-
* ['abc'] | abc | ^abc$
|
|
74
|
-
* ['abc', named:foo, 'xyz'] | abc/:foo/xyz | ^abc/([^/]+?)/xyz$
|
|
75
|
-
* ['abc', named:foo?, 'xyz'] | abc/:foo?/xyz | ^abc(?:/([^/]+?))?/xyz$
|
|
76
|
-
* ['abc', named:foo?, rest] | abc/:foo?/* | ^abc(?:/([^/]+?))?(?:/(.+))?$
|
|
77
|
-
*
|
|
78
|
-
* ^(?:/(.+))?$
|
|
79
|
-
*/
|
|
80
|
-
function parsedPathToRegExp(parsedPath: ParsedPath) {
|
|
81
|
-
const regexpParts: string[] = []
|
|
82
|
-
for (const node of parsedPath) {
|
|
83
|
-
const prefix = node === parsedPath[0] ? '' : '/'
|
|
84
|
-
if (node.type === PathNodeType.Text) {
|
|
85
|
-
regexpParts.push(prefix + escapeRegExp(node.text))
|
|
86
|
-
} else if (node.type === PathNodeType.Named) {
|
|
87
|
-
regexpParts.push(node.optional ? `(?:${prefix}([^/]+?))?` : `${prefix}([^/]+?)`)
|
|
88
|
-
} else {
|
|
89
|
-
regexpParts.push(`(?:${prefix}(.+))?`)
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
return new RegExp('^' + regexpParts.join('') + '$', 'i')
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
/**
|
|
96
|
-
* 比较两个路径的优先级(两个路由都能匹配路径时,优先级更高的生效)
|
|
97
|
-
*
|
|
98
|
-
* pathA 优先:-1
|
|
99
|
-
* pathB 优先:1
|
|
100
|
-
*
|
|
101
|
-
* 规则:
|
|
102
|
-
* 1. 挨个 node 比对
|
|
103
|
-
* 2. 先匹配各节点的类型,text > named > named optional > rest
|
|
104
|
-
* 3. 节点类型都一致,节点数量多的优先级更高
|
|
105
|
-
* 4. 节点类型、数量都一样,后出现的覆盖先出现的
|
|
106
|
-
*/
|
|
107
|
-
function pathSorter(pathA: ParsedPath, pathB: ParsedPath) {
|
|
108
|
-
const typePriorities = [PathNodeType.Text, PathNodeType.Named, PathNodeType.Rest]
|
|
109
|
-
|
|
110
|
-
const length = Math.max(pathA.length, pathB.length)
|
|
111
|
-
for (let i = 0; i < length; i++) {
|
|
112
|
-
const a = pathA[i]
|
|
113
|
-
const b = pathB[i]
|
|
114
|
-
|
|
115
|
-
// 若 nodes 长度不一样,长的优先
|
|
116
|
-
if (a === undefined) return -1
|
|
117
|
-
else if (b === undefined) return 1
|
|
118
|
-
|
|
119
|
-
// node 类型不一样,按类型排序
|
|
120
|
-
const aTypePriority = typePriorities.indexOf(a.type)
|
|
121
|
-
const bTypePriority = typePriorities.indexOf(b.type)
|
|
122
|
-
if (aTypePriority !== bTypePriority) return aTypePriority - bTypePriority
|
|
123
|
-
|
|
124
|
-
// 若都是 named 类型但 optional 不同
|
|
125
|
-
if (
|
|
126
|
-
a.type === PathNodeType.Named &&
|
|
127
|
-
b.type === PathNodeType.Named &&
|
|
128
|
-
a.optional !== b.optional
|
|
129
|
-
) {
|
|
130
|
-
return a.optional ? 1 : -1
|
|
131
|
-
}
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
// 长度、类型都一样,后出现的优先
|
|
135
|
-
return 1
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
/**
|
|
139
|
-
* 返回匹配的路由及匹配上的路径参数
|
|
140
|
-
*/
|
|
141
|
-
export function matchRoutes<R extends BasicRoute>(requestPath: string, routes: R[]) {
|
|
142
|
-
if (!parsedCache.has(routes)) {
|
|
143
|
-
// 解析路由路径并按优先级排序
|
|
144
|
-
const parsedRoutes = routes
|
|
145
|
-
.map(route => {
|
|
146
|
-
const parsedPath = parseRoutePath(route.path)
|
|
147
|
-
const regexp = parsedPathToRegExp(parsedPath)
|
|
148
|
-
return { parsedPath, regexp, route }
|
|
149
|
-
})
|
|
150
|
-
.sort((a, b) => pathSorter(a.parsedPath, b.parsedPath))
|
|
151
|
-
parsedCache.set(routes, parsedRoutes)
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
requestPath = normalizePath(requestPath)
|
|
155
|
-
const parsedRoutes = parsedCache.get(routes)!
|
|
156
|
-
return parsedRoutes
|
|
157
|
-
.map(({ parsedPath, regexp, route }) => {
|
|
158
|
-
const match = regexp.exec(requestPath)
|
|
159
|
-
if (!match) return null
|
|
160
|
-
|
|
161
|
-
const pathParameters = matchPathParameters(parsedPath, [...match].slice(1))
|
|
162
|
-
return { route, pathParameters } as RouteMatch<R>
|
|
163
|
-
})
|
|
164
|
-
.filter((matched): matched is Exclude<typeof matched, null> => matched !== null)
|
|
165
|
-
}
|
|
166
|
-
const parsedCache = new WeakMap<
|
|
167
|
-
BasicRoute[], // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
168
|
-
{ parsedPath: ParsedPath; regexp: RegExp; route: BasicRoute }[] // eslint-disable-line @typescript-eslint/no-explicit-any
|
|
169
|
-
>()
|
|
170
|
-
|
|
171
|
-
export type BasicRoute = { path: string }
|
|
172
|
-
|
|
173
|
-
export interface RouteMatch<R> {
|
|
174
|
-
route: R
|
|
175
|
-
pathParameters: PathParameters
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
/**
|
|
179
|
-
* 把匹配到的路径参数映射成键值对
|
|
180
|
-
* 没匹配到的内容(optional named node / rest node)值为 undefined
|
|
181
|
-
*/
|
|
182
|
-
function matchPathParameters(parsedPath: ParsedPath, parameterNames: (string | undefined)[]) {
|
|
183
|
-
const parameters: PathParameters = {}
|
|
184
|
-
for (const node of parsedPath) {
|
|
185
|
-
if (node.type === PathNodeType.Text) continue
|
|
186
|
-
else if (node.type === PathNodeType.Named) parameters[node.name] = parameterNames.shift()!
|
|
187
|
-
else parameters['*'] = parameterNames.shift()!
|
|
188
|
-
}
|
|
189
|
-
return parameters
|
|
190
|
-
}
|
|
191
|
-
export type PathParameters = {
|
|
192
|
-
[name: string]: string | undefined
|
|
193
|
-
'*'?: string
|
|
194
|
-
}
|
package/src/router/parameters.ts
DELETED
|
@@ -1,175 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* 实现通过 query / body parameters 的解析和验证
|
|
3
|
-
*/
|
|
4
|
-
import { success } from '@anjianshi/utils'
|
|
5
|
-
import { type Request, HTTPError } from '@/http/index.js'
|
|
6
|
-
import { type Validator, type CommonOptions } from '@/validators/common.js'
|
|
7
|
-
import * as validators from '@/validators/index.js'
|
|
8
|
-
import { type Route } from './router.js'
|
|
9
|
-
|
|
10
|
-
/**
|
|
11
|
-
* ----------------------------------------
|
|
12
|
-
* 类型定义
|
|
13
|
-
* ----------------------------------------
|
|
14
|
-
*/
|
|
15
|
-
|
|
16
|
-
/**
|
|
17
|
-
* 请求参数定义
|
|
18
|
-
*/
|
|
19
|
-
export interface Parameter {
|
|
20
|
-
/** 字段名 */
|
|
21
|
-
name: string
|
|
22
|
-
|
|
23
|
-
/** 文字描述 */
|
|
24
|
-
description?: string
|
|
25
|
-
|
|
26
|
-
type: ParameterDataType
|
|
27
|
-
|
|
28
|
-
/** 默认值,应与 type 匹配。若指定,则不再需要 required 规则。 */
|
|
29
|
-
defaults?: unknown
|
|
30
|
-
|
|
31
|
-
/** 是否允许为 undefined */
|
|
32
|
-
required?: boolean
|
|
33
|
-
|
|
34
|
-
/** 是否允许为 null */
|
|
35
|
-
nullable?: boolean
|
|
36
|
-
|
|
37
|
-
/** 验证规则,会传给 type 对应的 validator */
|
|
38
|
-
validate?: Record<string, unknown>
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export type BasicDataType = 'string' | 'number' | 'boolean'
|
|
42
|
-
|
|
43
|
-
export type BasicParameter = Pick<
|
|
44
|
-
Parameter,
|
|
45
|
-
'type' | 'validate' | 'required' | 'nullable' | 'defaults' | 'description'
|
|
46
|
-
>
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* 请求参数的数据类型定义
|
|
50
|
-
*/
|
|
51
|
-
export type ParameterDataType =
|
|
52
|
-
| BasicDataType
|
|
53
|
-
| { array: BasicParameter }
|
|
54
|
-
| { record: BasicParameter } // 对应 object validator 的 value 类型
|
|
55
|
-
| { object: Record<string, BasicParameter> } // 对应 object validator 的 struct 类型
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* ----------------------------------------
|
|
59
|
-
* 基础功能实现
|
|
60
|
-
* ----------------------------------------
|
|
61
|
-
*/
|
|
62
|
-
|
|
63
|
-
/**
|
|
64
|
-
* 基于 parameter 定义验证、格式化数据
|
|
65
|
-
*/
|
|
66
|
-
function validateParameters<T>(parameters: Parameter[], rawData: Record<string, unknown>) {
|
|
67
|
-
const result = {} as Record<string, unknown>
|
|
68
|
-
for (const parameter of parameters) {
|
|
69
|
-
const validator = getValidatorOfParameter(parameter)
|
|
70
|
-
const validateRes = validator.validate(parameter.name, rawData[parameter.name])
|
|
71
|
-
if (validateRes.success) result[parameter.name] = validateRes.data
|
|
72
|
-
else return validateRes
|
|
73
|
-
}
|
|
74
|
-
return success(result as T)
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* 获取指定 parameter 的 validator
|
|
79
|
-
*/
|
|
80
|
-
function getValidatorOfParameter(parameter: BasicParameter) {
|
|
81
|
-
if (!validatorCache.has(parameter)) {
|
|
82
|
-
const validatorConstructor = getValidatorConstructor(parameter.type) as typeof validators.any
|
|
83
|
-
const options = {
|
|
84
|
-
null: parameter.nullable,
|
|
85
|
-
void: typeof parameter.required === 'boolean' ? !parameter.required : undefined,
|
|
86
|
-
defaults: parameter.defaults,
|
|
87
|
-
...(parameter.validate ?? {}),
|
|
88
|
-
}
|
|
89
|
-
const validator = validatorConstructor(options)
|
|
90
|
-
validatorCache.set(parameter, validator)
|
|
91
|
-
}
|
|
92
|
-
return validatorCache.get(parameter)!
|
|
93
|
-
}
|
|
94
|
-
const validatorCache = new WeakMap<BasicParameter, ReturnType<typeof validators.any>>()
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* 取得与参数类型对应的 validator constructor
|
|
98
|
-
*/
|
|
99
|
-
function getValidatorConstructor(type: ParameterDataType): (options: CommonOptions) => Validator {
|
|
100
|
-
if (typeof type === 'string') return validators[type]
|
|
101
|
-
|
|
102
|
-
if ('array' in type) {
|
|
103
|
-
const itemValidator = getValidatorOfParameter(type.array)
|
|
104
|
-
return validators.array.bind(null, itemValidator)
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
if ('record' in type) {
|
|
108
|
-
const itemValidator = getValidatorOfParameter(type.record)
|
|
109
|
-
return validators.object.bind(null, itemValidator)
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
const struct: Record<string, Validator> = {}
|
|
113
|
-
for (const [key, innerParameter] of Object.entries(type.object)) {
|
|
114
|
-
struct[key] = getValidatorOfParameter(innerParameter)
|
|
115
|
-
}
|
|
116
|
-
return validators.object.bind(null, struct)
|
|
117
|
-
}
|
|
118
|
-
|
|
119
|
-
/**
|
|
120
|
-
* ----------------------------------------
|
|
121
|
-
* 与请求内容对接
|
|
122
|
-
* ----------------------------------------
|
|
123
|
-
*/
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* 验证 query 内容
|
|
127
|
-
*/
|
|
128
|
-
export function parseQuery<T>(route: Route, request: Request) {
|
|
129
|
-
if (!route.query) throw new HTTPError(500, 'Call parseQuery() need query parameter definition.')
|
|
130
|
-
|
|
131
|
-
const values: Record<string, unknown> = {}
|
|
132
|
-
for (const parameter of route.query) {
|
|
133
|
-
values[parameter.name] = getQueryParameterValue(request, parameter)
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
const res = validateParameters<T>(route.query, values)
|
|
137
|
-
if (res.success) return res.data
|
|
138
|
-
throw new HTTPError(400, res.error)
|
|
139
|
-
}
|
|
140
|
-
|
|
141
|
-
function getQueryParameterValue(request: Request, parameter: Parameter) {
|
|
142
|
-
// array
|
|
143
|
-
if (typeof parameter.type === 'object' && 'array' in parameter.type) {
|
|
144
|
-
return request.query.getAll(parameter.name + '[]')
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// undefined
|
|
148
|
-
const raw = request.query.get(parameter.name)
|
|
149
|
-
if (raw === null || raw === '') return undefined
|
|
150
|
-
|
|
151
|
-
// basic type
|
|
152
|
-
if (typeof parameter.type === 'string') return raw
|
|
153
|
-
|
|
154
|
-
// object
|
|
155
|
-
try {
|
|
156
|
-
return JSON.parse(raw) as unknown
|
|
157
|
-
} catch (error) {
|
|
158
|
-
throw new HTTPError(400, `Invalid JSON value for query parameter "${parameter.name}".`)
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
/**
|
|
163
|
-
* 验证 body 内容
|
|
164
|
-
*/
|
|
165
|
-
export async function parseJSONBody<T>(route: Route, request: Request) {
|
|
166
|
-
if (!route.body) throw new HTTPError(500, 'call parseJSONBody() need body parameter definition.')
|
|
167
|
-
|
|
168
|
-
const body = await request.body.json()
|
|
169
|
-
if (typeof body !== 'object' || body === null || Array.isArray(body))
|
|
170
|
-
throw new HTTPError(400, 'Invalid JSON body, should be an object.')
|
|
171
|
-
|
|
172
|
-
const res = validateParameters<T>(route.body, body as Record<string, unknown>)
|
|
173
|
-
if (res.success) return res.data
|
|
174
|
-
throw new HTTPError(400, res.error)
|
|
175
|
-
}
|