starlight-server 0.0.1 → 1.0.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/demo/index.js +20 -3
- package/dist/router/index.d.ts +1 -1
- package/dist/router/parameters.d.ts +2 -3
- package/dist/router/router.d.ts +3 -3
- package/dist/router/router.js +1 -1
- package/dist/swagger/factory.d.ts +97 -0
- package/dist/swagger/factory.js +144 -0
- package/dist/swagger/index.d.ts +7 -0
- package/dist/swagger/index.js +67 -168
- package/dist/swagger/openapi-spec.d.ts +3 -3
- package/package.json +3 -3
- package/src/.DS_Store +0 -0
- package/src/demo/index.ts +21 -3
- package/src/router/index.ts +1 -1
- package/src/router/parameters.ts +5 -2
- package/src/router/router.ts +4 -4
- package/src/swagger/factory.ts +169 -0
- package/src/swagger/index.ts +92 -184
- package/src/swagger/openapi-spec.ts +3 -3
package/dist/demo/index.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { getFileDir } from '@anjianshi/utils/env-node/index.js';
|
|
3
3
|
import { getLogger, startHTTPServer, DefaultRouter } from '../index.js';
|
|
4
|
-
|
|
4
|
+
import { registerSwaggerRoute } from '../swagger/index.js';
|
|
5
5
|
const logger = getLogger({
|
|
6
6
|
level: 'debug',
|
|
7
7
|
debugLib: '*',
|
|
@@ -12,14 +12,31 @@ const logger = getLogger({
|
|
|
12
12
|
const router = new DefaultRouter();
|
|
13
13
|
router.registerResponseReference('hello', { object: [{ name: 'hello', type: 'string' }] });
|
|
14
14
|
router.register({
|
|
15
|
+
category: 'demo',
|
|
16
|
+
description: 'hello world',
|
|
15
17
|
method: 'GET',
|
|
16
18
|
path: '/hello',
|
|
17
|
-
|
|
19
|
+
body: [
|
|
20
|
+
{ name: 'abc', type: 'number' },
|
|
21
|
+
{ name: 'def', type: { array: { type: 'string' } } },
|
|
22
|
+
],
|
|
23
|
+
response: {
|
|
24
|
+
object: [{ name: 'key', type: 'string' }],
|
|
25
|
+
},
|
|
18
26
|
handler({ response }) {
|
|
19
27
|
response.json({ hello: 'world' });
|
|
20
28
|
},
|
|
21
29
|
});
|
|
22
|
-
|
|
30
|
+
router.register({
|
|
31
|
+
category: 'demo',
|
|
32
|
+
method: 'POST',
|
|
33
|
+
path: '/hello',
|
|
34
|
+
response: { ref: 'hello' },
|
|
35
|
+
handler({ response }) {
|
|
36
|
+
response.json({ hello: 'world post' });
|
|
37
|
+
},
|
|
38
|
+
});
|
|
39
|
+
registerSwaggerRoute(router);
|
|
23
40
|
startHTTPServer({
|
|
24
41
|
handler: router.handle,
|
|
25
42
|
logger: logger.getChild('http'),
|
package/dist/router/index.d.ts
CHANGED
|
@@ -35,4 +35,4 @@
|
|
|
35
35
|
*/
|
|
36
36
|
export * from './router.js';
|
|
37
37
|
export { type PathParameters } from './match.js';
|
|
38
|
-
export type { BasicDataType, Parameter, ParameterDataType } from './parameters.js';
|
|
38
|
+
export type { BasicDataType, Parameter, BasicParameter, ParameterDataType } from './parameters.js';
|
|
@@ -12,7 +12,7 @@ export interface Parameter {
|
|
|
12
12
|
/** 字段名 */
|
|
13
13
|
name: string;
|
|
14
14
|
/** 文字描述 */
|
|
15
|
-
|
|
15
|
+
description?: string;
|
|
16
16
|
type: ParameterDataType;
|
|
17
17
|
/** 默认值,应与 type 匹配。若指定,则不再需要 required 规则。 */
|
|
18
18
|
defaults?: unknown;
|
|
@@ -24,7 +24,7 @@ export interface Parameter {
|
|
|
24
24
|
validate?: Record<string, unknown>;
|
|
25
25
|
}
|
|
26
26
|
export type BasicDataType = 'string' | 'number' | 'boolean';
|
|
27
|
-
type BasicParameter = Pick<Parameter, 'type' | 'validate' | 'required' | 'nullable' | 'defaults'>;
|
|
27
|
+
export type BasicParameter = Pick<Parameter, 'type' | 'validate' | 'required' | 'nullable' | 'defaults' | 'description'>;
|
|
28
28
|
/**
|
|
29
29
|
* 请求参数的数据类型定义
|
|
30
30
|
*/
|
|
@@ -48,4 +48,3 @@ export declare function parseQuery<T>(route: Route, request: Request): T;
|
|
|
48
48
|
* 验证 body 内容
|
|
49
49
|
*/
|
|
50
50
|
export declare function parseJSONBody<T>(route: Route, request: Request): Promise<T>;
|
|
51
|
-
export {};
|
package/dist/router/router.d.ts
CHANGED
|
@@ -28,12 +28,12 @@ export type RouteHandler<Ctx extends BaseContext = BaseContext, PathP extends Pa
|
|
|
28
28
|
* - 有默认值的字段均填充了默认值
|
|
29
29
|
* - method 一定为全大写
|
|
30
30
|
*/
|
|
31
|
-
export type Route<Ctx extends BaseContext = BaseContext, PathP extends PathParameters = PathParameters> = RequiredFields<RawRoute<Ctx, PathP>, '
|
|
31
|
+
export type Route<Ctx extends BaseContext = BaseContext, PathP extends PathParameters = PathParameters> = RequiredFields<RawRoute<Ctx, PathP>, 'description' | 'category' | 'method'>;
|
|
32
32
|
export interface RawRoute<Ctx extends BaseContext = BaseContext, PathP extends PathParameters = PathParameters> {
|
|
33
33
|
/** 接口路径。支持变量(/abc/:xxx/def),详见 src/router/match.ts */
|
|
34
34
|
path: string;
|
|
35
35
|
/** 接口描述 */
|
|
36
|
-
|
|
36
|
+
description?: string;
|
|
37
37
|
/** 接口类别 */
|
|
38
38
|
category?: string;
|
|
39
39
|
/** 接口 HTTP Method,有 body 定义的默认为 POST,否则默认为 GET */
|
|
@@ -74,7 +74,7 @@ export interface ResponseObjectItemType {
|
|
|
74
74
|
/** 字段名 */
|
|
75
75
|
name: string;
|
|
76
76
|
/** 文字描述 */
|
|
77
|
-
|
|
77
|
+
description?: string;
|
|
78
78
|
/** 值类型。ResponseDataType[] 代表有多种可能的类型 */
|
|
79
79
|
type: ResponseDataType | ResponseDataType[];
|
|
80
80
|
}
|
package/dist/router/router.js
CHANGED
|
@@ -15,7 +15,7 @@ export class Router {
|
|
|
15
15
|
register(raw, path, handler) {
|
|
16
16
|
const rawRoute = typeof raw === 'string' ? { method: raw, path: path, handler: handler } : raw;
|
|
17
17
|
const route = {
|
|
18
|
-
|
|
18
|
+
description: '',
|
|
19
19
|
category: '',
|
|
20
20
|
...rawRoute,
|
|
21
21
|
method: (rawRoute.method ?? (rawRoute.body ? 'POST' : 'GET')).toUpperCase(),
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把路由定义转换成 swagger API 定义的工具函数
|
|
3
|
+
*/
|
|
4
|
+
import type { Route, ResponseDataType, BasicParameter } from '../router/index.js';
|
|
5
|
+
import type { Schema, Reference, PathItem } from './openapi-spec.js';
|
|
6
|
+
type AnyObject = {
|
|
7
|
+
[k: string]: unknown;
|
|
8
|
+
};
|
|
9
|
+
/**
|
|
10
|
+
* 基本数据类型
|
|
11
|
+
*/
|
|
12
|
+
export declare function string(description?: string, options?: AnyObject): {
|
|
13
|
+
type: string;
|
|
14
|
+
description: string | undefined;
|
|
15
|
+
};
|
|
16
|
+
export declare function number(description?: string, options?: AnyObject): {
|
|
17
|
+
type: string;
|
|
18
|
+
description: string | undefined;
|
|
19
|
+
};
|
|
20
|
+
export declare function boolean(description?: string, options?: AnyObject): {
|
|
21
|
+
type: string;
|
|
22
|
+
description: string | undefined;
|
|
23
|
+
};
|
|
24
|
+
export declare function object(properties: Record<string, Schema>, extra?: AnyObject): {
|
|
25
|
+
type: "object";
|
|
26
|
+
properties: Record<string, Schema>;
|
|
27
|
+
};
|
|
28
|
+
export declare function array(items: Schema, extra?: AnyObject): {
|
|
29
|
+
type: "array";
|
|
30
|
+
items: Schema;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* 对 components/schemas 中定义的数据类型的引用
|
|
34
|
+
*/
|
|
35
|
+
export declare function ref(name: string, summary?: string, description?: string): Reference;
|
|
36
|
+
/**
|
|
37
|
+
* 可放置于 components/schemas 中的数据类型定义(用于响应内容)
|
|
38
|
+
*/
|
|
39
|
+
export declare function responseSchema(type: ResponseDataType | ResponseDataType[]): Schema | Reference;
|
|
40
|
+
/**
|
|
41
|
+
* 可放置于 components/schemas 中的数据类型定义(用于请求内容)
|
|
42
|
+
*/
|
|
43
|
+
export declare function parameterSchema(parameter: BasicParameter): Schema;
|
|
44
|
+
/**
|
|
45
|
+
* 生成 JSON 形式的 content
|
|
46
|
+
*/
|
|
47
|
+
export declare function jsonMedia(schema: string | Schema): {
|
|
48
|
+
content: {
|
|
49
|
+
'application/json': {
|
|
50
|
+
schema: Schema | Reference;
|
|
51
|
+
};
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
/**
|
|
55
|
+
* 把一系列路由转换成 swagger API 定义
|
|
56
|
+
*/
|
|
57
|
+
export declare function paths(routes: Route[]): Record<string, PathItem>;
|
|
58
|
+
/**
|
|
59
|
+
* 单个路由定义
|
|
60
|
+
*/
|
|
61
|
+
export declare function operation(route: Route): {
|
|
62
|
+
tags: string[];
|
|
63
|
+
summary: string;
|
|
64
|
+
operationId: string;
|
|
65
|
+
requestBody: {
|
|
66
|
+
content: {
|
|
67
|
+
'application/json': {
|
|
68
|
+
schema: Schema | Reference;
|
|
69
|
+
};
|
|
70
|
+
};
|
|
71
|
+
} | undefined;
|
|
72
|
+
responses: {
|
|
73
|
+
default: {
|
|
74
|
+
content: {
|
|
75
|
+
'application/json': {
|
|
76
|
+
schema: Schema | Reference;
|
|
77
|
+
};
|
|
78
|
+
};
|
|
79
|
+
description: string;
|
|
80
|
+
};
|
|
81
|
+
};
|
|
82
|
+
};
|
|
83
|
+
/**
|
|
84
|
+
* 生成路由的唯一 ID
|
|
85
|
+
*/
|
|
86
|
+
export declare function operationId(route: Route): string;
|
|
87
|
+
/**
|
|
88
|
+
* 路由请求参数转为 swagger 定义
|
|
89
|
+
*/
|
|
90
|
+
export declare function requestBody(route: Route): {
|
|
91
|
+
content: {
|
|
92
|
+
'application/json': {
|
|
93
|
+
schema: Schema | Reference;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
} | undefined;
|
|
97
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 基本数据类型
|
|
3
|
+
*/
|
|
4
|
+
export function string(description, options) {
|
|
5
|
+
return { type: 'string', description, ...(options ?? {}) };
|
|
6
|
+
}
|
|
7
|
+
export function number(description, options) {
|
|
8
|
+
return { type: 'number', description, ...(options ?? {}) };
|
|
9
|
+
}
|
|
10
|
+
export function boolean(description, options) {
|
|
11
|
+
return { type: 'boolean', description, ...(options ?? {}) };
|
|
12
|
+
}
|
|
13
|
+
export function object(properties, extra) {
|
|
14
|
+
return { type: 'object', properties, ...(extra ?? {}) };
|
|
15
|
+
}
|
|
16
|
+
export function array(items, extra) {
|
|
17
|
+
return { type: 'array', items, ...(extra ?? {}) };
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* 对 components/schemas 中定义的数据类型的引用
|
|
21
|
+
*/
|
|
22
|
+
export function ref(name, summary, description) {
|
|
23
|
+
return { $ref: '#/components/schemas/' + name, summary, description };
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* 可放置于 components/schemas 中的数据类型定义(用于响应内容)
|
|
27
|
+
*/
|
|
28
|
+
export function responseSchema(type) {
|
|
29
|
+
if (Array.isArray(type)) {
|
|
30
|
+
return {
|
|
31
|
+
oneOf: type
|
|
32
|
+
.filter((subType) => subType !== 'null')
|
|
33
|
+
.map(responseSchema),
|
|
34
|
+
nullable: type.includes('null'),
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
if (typeof type === 'string')
|
|
38
|
+
return { type };
|
|
39
|
+
if ('ref' in type)
|
|
40
|
+
return ref(type.ref, type.summary, type.description);
|
|
41
|
+
if ('array' in type)
|
|
42
|
+
return array(responseSchema(type.array));
|
|
43
|
+
if ('object' in type) {
|
|
44
|
+
return object(type.object.reduce((properties, property) => ({
|
|
45
|
+
...properties,
|
|
46
|
+
[property.name]: { ...responseSchema(property.type), description: property.description },
|
|
47
|
+
}), {}));
|
|
48
|
+
}
|
|
49
|
+
return {};
|
|
50
|
+
}
|
|
51
|
+
/**
|
|
52
|
+
* 可放置于 components/schemas 中的数据类型定义(用于请求内容)
|
|
53
|
+
*/
|
|
54
|
+
export function parameterSchema(parameter) {
|
|
55
|
+
const schema = {
|
|
56
|
+
description: parameter.description,
|
|
57
|
+
nullable: parameter.nullable,
|
|
58
|
+
default: parameter.defaults,
|
|
59
|
+
};
|
|
60
|
+
if (typeof parameter.type === 'string') {
|
|
61
|
+
schema.type = parameter.type;
|
|
62
|
+
}
|
|
63
|
+
else if ('array' in parameter.type) {
|
|
64
|
+
const arraySchema = array(parameterSchema(parameter.type.array));
|
|
65
|
+
Object.assign(schema, arraySchema);
|
|
66
|
+
}
|
|
67
|
+
else if ('record' in parameter.type) {
|
|
68
|
+
const innerSchema = parameterSchema(parameter.type.record);
|
|
69
|
+
Object.assign(schema, object({}, { additionalProperties: innerSchema }));
|
|
70
|
+
}
|
|
71
|
+
else if ('object' in parameter.type) {
|
|
72
|
+
const objectSchema = object(Object.entries(parameter.type.object).reduce((properties, [name, subParameter]) => ({
|
|
73
|
+
...properties,
|
|
74
|
+
[name]: parameterSchema(subParameter),
|
|
75
|
+
}), {}));
|
|
76
|
+
Object.assign(schema, objectSchema);
|
|
77
|
+
}
|
|
78
|
+
return schema;
|
|
79
|
+
}
|
|
80
|
+
/**
|
|
81
|
+
* 生成 JSON 形式的 content
|
|
82
|
+
*/
|
|
83
|
+
export function jsonMedia(schema) {
|
|
84
|
+
return {
|
|
85
|
+
content: {
|
|
86
|
+
'application/json': {
|
|
87
|
+
schema: typeof schema === 'string' ? ref(schema) : schema,
|
|
88
|
+
},
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* 把一系列路由转换成 swagger API 定义
|
|
94
|
+
*/
|
|
95
|
+
export function paths(routes) {
|
|
96
|
+
const paths = {};
|
|
97
|
+
for (const route of routes) {
|
|
98
|
+
if (route.category === 'swagger')
|
|
99
|
+
continue;
|
|
100
|
+
if (!paths[route.path])
|
|
101
|
+
paths[route.path] = {};
|
|
102
|
+
const pathItem = paths[route.path];
|
|
103
|
+
const method = route.method.toLowerCase();
|
|
104
|
+
pathItem[method] = operation(route);
|
|
105
|
+
}
|
|
106
|
+
return paths;
|
|
107
|
+
}
|
|
108
|
+
/**
|
|
109
|
+
* 单个路由定义
|
|
110
|
+
*/
|
|
111
|
+
export function operation(route) {
|
|
112
|
+
return {
|
|
113
|
+
tags: route.category ? [route.category] : [],
|
|
114
|
+
summary: route.description,
|
|
115
|
+
operationId: operationId(route),
|
|
116
|
+
requestBody: requestBody(route),
|
|
117
|
+
responses: {
|
|
118
|
+
default: {
|
|
119
|
+
description: 'OK',
|
|
120
|
+
...jsonMedia(route.response !== undefined ? responseSchema(route.response) : object({})),
|
|
121
|
+
},
|
|
122
|
+
},
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* 生成路由的唯一 ID
|
|
127
|
+
*/
|
|
128
|
+
export function operationId(route) {
|
|
129
|
+
return `${route.method}-${route.path}`;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* 路由请求参数转为 swagger 定义
|
|
133
|
+
*/
|
|
134
|
+
export function requestBody(route) {
|
|
135
|
+
if (!route.body)
|
|
136
|
+
return;
|
|
137
|
+
return jsonMedia({
|
|
138
|
+
type: 'object',
|
|
139
|
+
properties: route.body.reduce((properties, parameter) => ({
|
|
140
|
+
...properties,
|
|
141
|
+
[parameter.name]: parameterSchema(parameter),
|
|
142
|
+
}), {}),
|
|
143
|
+
});
|
|
144
|
+
}
|
package/dist/swagger/index.d.ts
CHANGED
|
@@ -1 +1,8 @@
|
|
|
1
|
+
import type { Router } from '../router/index.js';
|
|
2
|
+
import type { OpenAPI } from './openapi-spec.js';
|
|
3
|
+
export declare function registerSwaggerRoute(router: Router, endpoint?: string, customFields?: OpenAPICustomFields, swaggerOptions?: Record<string, unknown>): void;
|
|
4
|
+
/**
|
|
5
|
+
* swagger API 路由
|
|
6
|
+
*/
|
|
7
|
+
type OpenAPICustomFields = Omit<OpenAPI, 'openapi' | 'paths' | 'components'>;
|
|
1
8
|
export {};
|
package/dist/swagger/index.js
CHANGED
|
@@ -1,168 +1,67 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
//
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
// paths: makePaths(router),
|
|
69
|
-
// components: {
|
|
70
|
-
// schemas: makeRefs(router),
|
|
71
|
-
// },
|
|
72
|
-
// }
|
|
73
|
-
// response.json(swaggerJSON)
|
|
74
|
-
// }
|
|
75
|
-
// function makeRefs(router: Router) {
|
|
76
|
-
// const schemas: Record<string, Schema> = {}
|
|
77
|
-
// for (const [id, type] of Object.entries(router.responseReferences)) {
|
|
78
|
-
// schemas[id] = responseDataType2Schema(type)
|
|
79
|
-
// }
|
|
80
|
-
// return schemas
|
|
81
|
-
// }
|
|
82
|
-
// function makePaths(router: Router) {
|
|
83
|
-
// const paths: Record<string, PathItem> = {}
|
|
84
|
-
// return paths
|
|
85
|
-
// }
|
|
86
|
-
// function responseDataType2Schema(type: ResponseDataType): Schema {
|
|
87
|
-
// if (type === 'null') return { nullable: true }
|
|
88
|
-
// if (typeof type === 'object' && 'array' in type && type.array.includes('null')) {
|
|
89
|
-
// return {
|
|
90
|
-
// nullable: true,
|
|
91
|
-
// ...responseDataType2Schema(type as ResponseDataType.filter(v => v !== 'null') as ResponseDataType),
|
|
92
|
-
// }
|
|
93
|
-
// }
|
|
94
|
-
// const nullable = type === 'null' || (Array.isArray(type) && type.includes('null'))
|
|
95
|
-
// return {
|
|
96
|
-
// nullable,
|
|
97
|
-
// }
|
|
98
|
-
// }
|
|
99
|
-
// type Obj = { [k: string]: unknown }
|
|
100
|
-
// function string(desc?: string, options?: Obj) {
|
|
101
|
-
// return { type: 'string', description: desc, ...(options ?? {}) }
|
|
102
|
-
// }
|
|
103
|
-
// function number(desc?: string, options?: Obj) {
|
|
104
|
-
// return { type: 'number', description: desc, ...(options ?? {}) }
|
|
105
|
-
// }
|
|
106
|
-
// function boolean(desc?: string, options?: Obj) {
|
|
107
|
-
// return { type: 'boolean', description: desc, ...(options ?? {}) }
|
|
108
|
-
// }
|
|
109
|
-
// function object(properties: unknown, extra?: Obj) {
|
|
110
|
-
// return { type: 'object', properties, ...(extra ?? {}) }
|
|
111
|
-
// }
|
|
112
|
-
// function array(items: unknown, extra?: Obj) {
|
|
113
|
-
// return { type: 'array', items, ...(extra ?? {}) }
|
|
114
|
-
// }
|
|
115
|
-
// function ref(name: string) {
|
|
116
|
-
// return { $ref: '#/components/schemas/' + name }
|
|
117
|
-
// }
|
|
118
|
-
// function jsonSchema(schema: string | Obj) {
|
|
119
|
-
// return {
|
|
120
|
-
// content: {
|
|
121
|
-
// 'application/json': {
|
|
122
|
-
// schema: typeof schema === 'string' ? ref(schema) : schema,
|
|
123
|
-
// },
|
|
124
|
-
// },
|
|
125
|
-
// }
|
|
126
|
-
// }
|
|
127
|
-
// function route(params: {
|
|
128
|
-
// category: string
|
|
129
|
-
// describe: string
|
|
130
|
-
// path: string
|
|
131
|
-
// body?: string | Obj
|
|
132
|
-
// response?: string | Obj
|
|
133
|
-
// }) {
|
|
134
|
-
// return {
|
|
135
|
-
// [params.path]: {
|
|
136
|
-
// post: {
|
|
137
|
-
// tags: [params.category],
|
|
138
|
-
// summary: params.describe,
|
|
139
|
-
// operationId: params.path,
|
|
140
|
-
// requestBody: params.body !== undefined ? jsonSchema(params.body) : undefined,
|
|
141
|
-
// responses: {
|
|
142
|
-
// default: {
|
|
143
|
-
// description: 'OK',
|
|
144
|
-
// ...jsonSchema(params.response ?? object({})),
|
|
145
|
-
// },
|
|
146
|
-
// },
|
|
147
|
-
// },
|
|
148
|
-
// },
|
|
149
|
-
// }
|
|
150
|
-
// }
|
|
151
|
-
// function response(schema: unknown) {
|
|
152
|
-
// return {
|
|
153
|
-
// type: 'object',
|
|
154
|
-
// properties: {
|
|
155
|
-
// success: boolean(undefined, { enum: [true] }),
|
|
156
|
-
// data: schema,
|
|
157
|
-
// },
|
|
158
|
-
// }
|
|
159
|
-
// }
|
|
160
|
-
// function pageResp(itemSchema: unknown) {
|
|
161
|
-
// return response({
|
|
162
|
-
// type: 'object',
|
|
163
|
-
// properties: {
|
|
164
|
-
// total: number(),
|
|
165
|
-
// list: array(itemSchema),
|
|
166
|
-
// },
|
|
167
|
-
// })
|
|
168
|
-
// }
|
|
1
|
+
import fsPromise from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
import { getAbsoluteFSPath } from 'swagger-ui-dist';
|
|
4
|
+
import { HTTPError } from '../http/index.js';
|
|
5
|
+
import { normalizePath } from '../router/match.js';
|
|
6
|
+
import * as factory from './factory.js';
|
|
7
|
+
export function registerSwaggerRoute(router, endpoint = '/swagger', customFields = { info: { title: 'API Document', version: '0.0.1' } }, swaggerOptions) {
|
|
8
|
+
router.register({
|
|
9
|
+
category: 'swagger',
|
|
10
|
+
method: 'GET',
|
|
11
|
+
path: normalizePath(endpoint) + '/*',
|
|
12
|
+
handler: pageRoute.bind(null, swaggerOptions),
|
|
13
|
+
});
|
|
14
|
+
router.register({
|
|
15
|
+
category: 'swagger',
|
|
16
|
+
method: 'GET',
|
|
17
|
+
path: normalizePath(endpoint) + '/api-swagger.json',
|
|
18
|
+
handler: apiRoute.bind(null, router, customFields),
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
/**
|
|
22
|
+
* swagger 页面路由
|
|
23
|
+
*/
|
|
24
|
+
async function pageRoute(swaggerOptions, // Example: { defaultModelsExpandDepth: 1, defaultModelExpandDepth: 1 }
|
|
25
|
+
{ response }, pathParameters) {
|
|
26
|
+
const file = (pathParameters['*'] ?? '') || 'index.html';
|
|
27
|
+
const swaggerDir = getAbsoluteFSPath();
|
|
28
|
+
const abspath = path.join(swaggerDir, file);
|
|
29
|
+
if (!cache.has(abspath)) {
|
|
30
|
+
try {
|
|
31
|
+
const stat = await fsPromise.stat(abspath);
|
|
32
|
+
if (!stat.isFile())
|
|
33
|
+
throw new HTTPError(404);
|
|
34
|
+
}
|
|
35
|
+
catch (e) {
|
|
36
|
+
throw new HTTPError(404);
|
|
37
|
+
}
|
|
38
|
+
const content = await fsPromise.readFile(abspath);
|
|
39
|
+
const replacemented = replacement(abspath, content, swaggerOptions);
|
|
40
|
+
cache.set(abspath, replacemented);
|
|
41
|
+
}
|
|
42
|
+
response.text(cache.get(abspath));
|
|
43
|
+
}
|
|
44
|
+
const cache = new Map();
|
|
45
|
+
function replacement(abspath, content, swaggerOptions) {
|
|
46
|
+
if (/swagger-initializer.js/.exec(abspath)) {
|
|
47
|
+
let formatted = content
|
|
48
|
+
.toString()
|
|
49
|
+
.replace('https://petstore.swagger.io/v2/swagger.json', './api-swagger.json');
|
|
50
|
+
if (swaggerOptions) {
|
|
51
|
+
formatted = formatted.replace('SwaggerUIBundle({', 'SwaggerUIBundle({\n ' + JSON.stringify(swaggerOptions).slice(1, -1) + ',\n');
|
|
52
|
+
}
|
|
53
|
+
return formatted;
|
|
54
|
+
}
|
|
55
|
+
return content;
|
|
56
|
+
}
|
|
57
|
+
function apiRoute(router, customFields, { response }) {
|
|
58
|
+
const swaggerJSON = {
|
|
59
|
+
...customFields,
|
|
60
|
+
openapi: '3.1.0',
|
|
61
|
+
paths: factory.paths(router.routes),
|
|
62
|
+
components: {
|
|
63
|
+
schemas: Object.entries(router.responseReferences).reduce((schemas, [name, type]) => ({ ...schemas, [name]: factory.responseSchema(type) }), {}),
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
response.json(swaggerJSON);
|
|
67
|
+
}
|
|
@@ -126,7 +126,7 @@ export interface Responses extends Extendable {
|
|
|
126
126
|
[HTTPStatus: string]: Response | Reference;
|
|
127
127
|
}
|
|
128
128
|
export interface Response extends Extendable {
|
|
129
|
-
|
|
129
|
+
description: CommonMark;
|
|
130
130
|
headers?: Record<string, Header | Reference>;
|
|
131
131
|
content?: Record<string, MediaType>;
|
|
132
132
|
links?: Record<string, Link | Reference>;
|
|
@@ -156,8 +156,8 @@ export interface Tag extends Extendable {
|
|
|
156
156
|
}
|
|
157
157
|
export interface Reference {
|
|
158
158
|
$ref: string;
|
|
159
|
-
summary
|
|
160
|
-
description
|
|
159
|
+
summary?: string;
|
|
160
|
+
description?: string;
|
|
161
161
|
}
|
|
162
162
|
export interface Schema extends Extendable {
|
|
163
163
|
nullable?: boolean;
|
package/package.json
CHANGED
|
@@ -1,12 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "starlight-server",
|
|
3
|
-
"version": "0.0
|
|
3
|
+
"version": "1.0.0",
|
|
4
4
|
"description": "Simple But Powerful Node.js HTTP Server",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"scripts": {
|
|
7
|
-
"
|
|
7
|
+
"dev": "npm run build && (concurrently \"tsc -w\" \"tsc-alias -w\" \"nodemon dist/demo/index.js\")",
|
|
8
8
|
"build": "rimraf dist && tsc && tsc-alias",
|
|
9
|
-
"demo": "node dist/demo/index.js",
|
|
10
9
|
"prepublishOnly": "npm run build"
|
|
11
10
|
},
|
|
12
11
|
"keywords": [
|
|
@@ -45,6 +44,7 @@
|
|
|
45
44
|
"@types/swagger-ui-dist": "^3.30.4",
|
|
46
45
|
"concurrently": "^8.2.1",
|
|
47
46
|
"eslint": "^8.54.0",
|
|
47
|
+
"nodemon": "^3.0.2",
|
|
48
48
|
"rimraf": "^4.3.0",
|
|
49
49
|
"tsc-alias": "^1.8.8",
|
|
50
50
|
"typescript": "^5.3.2"
|
package/src/.DS_Store
ADDED
|
Binary file
|
package/src/demo/index.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import path from 'node:path'
|
|
2
2
|
import { getFileDir } from '@anjianshi/utils/env-node/index.js'
|
|
3
3
|
import { getLogger, startHTTPServer, DefaultRouter } from '@/index.js'
|
|
4
|
-
|
|
4
|
+
import { registerSwaggerRoute } from '@/swagger/index.js'
|
|
5
5
|
|
|
6
6
|
const logger = getLogger({
|
|
7
7
|
level: 'debug',
|
|
@@ -16,15 +16,33 @@ const router = new DefaultRouter()
|
|
|
16
16
|
router.registerResponseReference('hello', { object: [{ name: 'hello', type: 'string' }] })
|
|
17
17
|
|
|
18
18
|
router.register({
|
|
19
|
+
category: 'demo',
|
|
20
|
+
description: 'hello world',
|
|
19
21
|
method: 'GET',
|
|
20
22
|
path: '/hello',
|
|
21
|
-
|
|
23
|
+
body: [
|
|
24
|
+
{ name: 'abc', type: 'number' },
|
|
25
|
+
{ name: 'def', type: { array: { type: 'string' } } },
|
|
26
|
+
],
|
|
27
|
+
response: {
|
|
28
|
+
object: [{ name: 'key', type: 'string' }],
|
|
29
|
+
},
|
|
22
30
|
handler({ response }) {
|
|
23
31
|
response.json({ hello: 'world' })
|
|
24
32
|
},
|
|
25
33
|
})
|
|
26
34
|
|
|
27
|
-
|
|
35
|
+
router.register({
|
|
36
|
+
category: 'demo',
|
|
37
|
+
method: 'POST',
|
|
38
|
+
path: '/hello',
|
|
39
|
+
response: { ref: 'hello' },
|
|
40
|
+
handler({ response }) {
|
|
41
|
+
response.json({ hello: 'world post' })
|
|
42
|
+
},
|
|
43
|
+
})
|
|
44
|
+
|
|
45
|
+
registerSwaggerRoute(router)
|
|
28
46
|
|
|
29
47
|
startHTTPServer({
|
|
30
48
|
handler: router.handle,
|
package/src/router/index.ts
CHANGED
|
@@ -35,4 +35,4 @@
|
|
|
35
35
|
*/
|
|
36
36
|
export * from './router.js'
|
|
37
37
|
export { type PathParameters } from './match.js'
|
|
38
|
-
export type { BasicDataType, Parameter, ParameterDataType } from './parameters.js'
|
|
38
|
+
export type { BasicDataType, Parameter, BasicParameter, ParameterDataType } from './parameters.js'
|
package/src/router/parameters.ts
CHANGED
|
@@ -21,7 +21,7 @@ export interface Parameter {
|
|
|
21
21
|
name: string
|
|
22
22
|
|
|
23
23
|
/** 文字描述 */
|
|
24
|
-
|
|
24
|
+
description?: string
|
|
25
25
|
|
|
26
26
|
type: ParameterDataType
|
|
27
27
|
|
|
@@ -40,7 +40,10 @@ export interface Parameter {
|
|
|
40
40
|
|
|
41
41
|
export type BasicDataType = 'string' | 'number' | 'boolean'
|
|
42
42
|
|
|
43
|
-
type BasicParameter = Pick<
|
|
43
|
+
export type BasicParameter = Pick<
|
|
44
|
+
Parameter,
|
|
45
|
+
'type' | 'validate' | 'required' | 'nullable' | 'defaults' | 'description'
|
|
46
|
+
>
|
|
44
47
|
|
|
45
48
|
/**
|
|
46
49
|
* 请求参数的数据类型定义
|
package/src/router/router.ts
CHANGED
|
@@ -38,7 +38,7 @@ export type RouteHandler<
|
|
|
38
38
|
export type Route<
|
|
39
39
|
Ctx extends BaseContext = BaseContext,
|
|
40
40
|
PathP extends PathParameters = PathParameters,
|
|
41
|
-
> = RequiredFields<RawRoute<Ctx, PathP>, '
|
|
41
|
+
> = RequiredFields<RawRoute<Ctx, PathP>, 'description' | 'category' | 'method'>
|
|
42
42
|
|
|
43
43
|
export interface RawRoute<
|
|
44
44
|
Ctx extends BaseContext = BaseContext,
|
|
@@ -48,7 +48,7 @@ export interface RawRoute<
|
|
|
48
48
|
path: string
|
|
49
49
|
|
|
50
50
|
/** 接口描述 */
|
|
51
|
-
|
|
51
|
+
description?: string
|
|
52
52
|
|
|
53
53
|
/** 接口类别 */
|
|
54
54
|
category?: string
|
|
@@ -96,7 +96,7 @@ export interface ResponseObjectItemType {
|
|
|
96
96
|
name: string
|
|
97
97
|
|
|
98
98
|
/** 文字描述 */
|
|
99
|
-
|
|
99
|
+
description?: string
|
|
100
100
|
|
|
101
101
|
/** 值类型。ResponseDataType[] 代表有多种可能的类型 */
|
|
102
102
|
type: ResponseDataType | ResponseDataType[]
|
|
@@ -131,7 +131,7 @@ export abstract class Router<Ctx extends BaseContext = BaseContext> {
|
|
|
131
131
|
const rawRoute: RawRoute<Ctx, PathParameters & P> =
|
|
132
132
|
typeof raw === 'string' ? { method: raw, path: path!, handler: handler! } : raw
|
|
133
133
|
const route: Route<Ctx, PathParameters & P> = {
|
|
134
|
-
|
|
134
|
+
description: '',
|
|
135
135
|
category: '',
|
|
136
136
|
...rawRoute,
|
|
137
137
|
method: (rawRoute.method ?? (rawRoute.body ? 'POST' : 'GET')).toUpperCase(),
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 把路由定义转换成 swagger API 定义的工具函数
|
|
3
|
+
*/
|
|
4
|
+
import type { Route, ResponseDataType, BasicParameter } from '@/router/index.js'
|
|
5
|
+
import type { Schema, Reference, PathItem } from './openapi-spec.js'
|
|
6
|
+
|
|
7
|
+
type AnyObject = { [k: string]: unknown }
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* 基本数据类型
|
|
11
|
+
*/
|
|
12
|
+
export function string(description?: string, options?: AnyObject) {
|
|
13
|
+
return { type: 'string', description, ...(options ?? {}) }
|
|
14
|
+
}
|
|
15
|
+
export function number(description?: string, options?: AnyObject) {
|
|
16
|
+
return { type: 'number', description, ...(options ?? {}) }
|
|
17
|
+
}
|
|
18
|
+
export function boolean(description?: string, options?: AnyObject) {
|
|
19
|
+
return { type: 'boolean', description, ...(options ?? {}) }
|
|
20
|
+
}
|
|
21
|
+
export function object(properties: Record<string, Schema>, extra?: AnyObject) {
|
|
22
|
+
return { type: 'object' as const, properties, ...(extra ?? {}) }
|
|
23
|
+
}
|
|
24
|
+
export function array(items: Schema, extra?: AnyObject) {
|
|
25
|
+
return { type: 'array' as const, items, ...(extra ?? {}) }
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* 对 components/schemas 中定义的数据类型的引用
|
|
30
|
+
*/
|
|
31
|
+
export function ref(name: string, summary?: string, description?: string): Reference {
|
|
32
|
+
return { $ref: '#/components/schemas/' + name, summary, description }
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* 可放置于 components/schemas 中的数据类型定义(用于响应内容)
|
|
37
|
+
*/
|
|
38
|
+
export function responseSchema(type: ResponseDataType | ResponseDataType[]): Schema | Reference {
|
|
39
|
+
if (Array.isArray(type)) {
|
|
40
|
+
return {
|
|
41
|
+
oneOf: type
|
|
42
|
+
.filter((subType): subType is Exclude<typeof subType, 'null'> => subType !== 'null')
|
|
43
|
+
.map(responseSchema),
|
|
44
|
+
nullable: type.includes('null'),
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
if (typeof type === 'string') return { type }
|
|
49
|
+
if ('ref' in type) return ref(type.ref, type.summary, type.description)
|
|
50
|
+
if ('array' in type) return array(responseSchema(type.array))
|
|
51
|
+
if ('object' in type) {
|
|
52
|
+
return object(
|
|
53
|
+
type.object.reduce(
|
|
54
|
+
(properties, property) => ({
|
|
55
|
+
...properties,
|
|
56
|
+
[property.name]: { ...responseSchema(property.type), description: property.description },
|
|
57
|
+
}),
|
|
58
|
+
{},
|
|
59
|
+
),
|
|
60
|
+
)
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
return {}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* 可放置于 components/schemas 中的数据类型定义(用于请求内容)
|
|
68
|
+
*/
|
|
69
|
+
export function parameterSchema(parameter: BasicParameter): Schema {
|
|
70
|
+
const schema: Schema = {
|
|
71
|
+
description: parameter.description,
|
|
72
|
+
nullable: parameter.nullable,
|
|
73
|
+
default: parameter.defaults,
|
|
74
|
+
}
|
|
75
|
+
if (typeof parameter.type === 'string') {
|
|
76
|
+
schema.type = parameter.type
|
|
77
|
+
} else if ('array' in parameter.type) {
|
|
78
|
+
const arraySchema = array(parameterSchema(parameter.type.array))
|
|
79
|
+
Object.assign(schema, arraySchema)
|
|
80
|
+
} else if ('record' in parameter.type) {
|
|
81
|
+
const innerSchema = parameterSchema(parameter.type.record)
|
|
82
|
+
Object.assign(schema, object({}, { additionalProperties: innerSchema }))
|
|
83
|
+
} else if ('object' in parameter.type) {
|
|
84
|
+
const objectSchema = object(
|
|
85
|
+
Object.entries(parameter.type.object).reduce(
|
|
86
|
+
(properties, [name, subParameter]) => ({
|
|
87
|
+
...properties,
|
|
88
|
+
[name]: parameterSchema(subParameter),
|
|
89
|
+
}),
|
|
90
|
+
{},
|
|
91
|
+
),
|
|
92
|
+
)
|
|
93
|
+
Object.assign(schema, objectSchema)
|
|
94
|
+
}
|
|
95
|
+
return schema
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
/**
|
|
99
|
+
* 生成 JSON 形式的 content
|
|
100
|
+
*/
|
|
101
|
+
export function jsonMedia(schema: string | Schema) {
|
|
102
|
+
return {
|
|
103
|
+
content: {
|
|
104
|
+
'application/json': {
|
|
105
|
+
schema: typeof schema === 'string' ? ref(schema) : schema,
|
|
106
|
+
},
|
|
107
|
+
},
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* 把一系列路由转换成 swagger API 定义
|
|
113
|
+
*/
|
|
114
|
+
export function paths(routes: Route[]) {
|
|
115
|
+
const paths: Record<string, PathItem> = {}
|
|
116
|
+
|
|
117
|
+
for (const route of routes) {
|
|
118
|
+
if (route.category === 'swagger') continue
|
|
119
|
+
|
|
120
|
+
if (!paths[route.path]) paths[route.path] = {}
|
|
121
|
+
const pathItem = paths[route.path]!
|
|
122
|
+
const method = route.method.toLowerCase() as 'get'
|
|
123
|
+
pathItem[method] = operation(route)
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
return paths
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* 单个路由定义
|
|
131
|
+
*/
|
|
132
|
+
export function operation(route: Route) {
|
|
133
|
+
return {
|
|
134
|
+
tags: route.category ? [route.category] : [],
|
|
135
|
+
summary: route.description,
|
|
136
|
+
operationId: operationId(route),
|
|
137
|
+
requestBody: requestBody(route),
|
|
138
|
+
responses: {
|
|
139
|
+
default: {
|
|
140
|
+
description: 'OK',
|
|
141
|
+
...jsonMedia(route.response !== undefined ? responseSchema(route.response) : object({})),
|
|
142
|
+
},
|
|
143
|
+
},
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* 生成路由的唯一 ID
|
|
149
|
+
*/
|
|
150
|
+
export function operationId(route: Route) {
|
|
151
|
+
return `${route.method}-${route.path}`
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* 路由请求参数转为 swagger 定义
|
|
156
|
+
*/
|
|
157
|
+
export function requestBody(route: Route) {
|
|
158
|
+
if (!route.body) return
|
|
159
|
+
return jsonMedia({
|
|
160
|
+
type: 'object',
|
|
161
|
+
properties: route.body.reduce(
|
|
162
|
+
(properties, parameter) => ({
|
|
163
|
+
...properties,
|
|
164
|
+
[parameter.name]: parameterSchema(parameter),
|
|
165
|
+
}),
|
|
166
|
+
{},
|
|
167
|
+
),
|
|
168
|
+
})
|
|
169
|
+
}
|
package/src/swagger/index.ts
CHANGED
|
@@ -1,184 +1,92 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
// }
|
|
94
|
-
|
|
95
|
-
// function responseDataType2Schema(type: ResponseDataType): Schema {
|
|
96
|
-
// if (type === 'null') return { nullable: true }
|
|
97
|
-
// if (typeof type === 'object' && 'array' in type && type.array.includes('null')) {
|
|
98
|
-
// return {
|
|
99
|
-
// nullable: true,
|
|
100
|
-
// ...responseDataType2Schema(type as ResponseDataType.filter(v => v !== 'null') as ResponseDataType),
|
|
101
|
-
// }
|
|
102
|
-
// }
|
|
103
|
-
|
|
104
|
-
// const nullable = type === 'null' || (Array.isArray(type) && type.includes('null'))
|
|
105
|
-
// return {
|
|
106
|
-
// nullable,
|
|
107
|
-
// }
|
|
108
|
-
// }
|
|
109
|
-
|
|
110
|
-
// type Obj = { [k: string]: unknown }
|
|
111
|
-
// function string(desc?: string, options?: Obj) {
|
|
112
|
-
// return { type: 'string', description: desc, ...(options ?? {}) }
|
|
113
|
-
// }
|
|
114
|
-
// function number(desc?: string, options?: Obj) {
|
|
115
|
-
// return { type: 'number', description: desc, ...(options ?? {}) }
|
|
116
|
-
// }
|
|
117
|
-
// function boolean(desc?: string, options?: Obj) {
|
|
118
|
-
// return { type: 'boolean', description: desc, ...(options ?? {}) }
|
|
119
|
-
// }
|
|
120
|
-
// function object(properties: unknown, extra?: Obj) {
|
|
121
|
-
// return { type: 'object', properties, ...(extra ?? {}) }
|
|
122
|
-
// }
|
|
123
|
-
// function array(items: unknown, extra?: Obj) {
|
|
124
|
-
// return { type: 'array', items, ...(extra ?? {}) }
|
|
125
|
-
// }
|
|
126
|
-
|
|
127
|
-
// function ref(name: string) {
|
|
128
|
-
// return { $ref: '#/components/schemas/' + name }
|
|
129
|
-
// }
|
|
130
|
-
|
|
131
|
-
// function jsonSchema(schema: string | Obj) {
|
|
132
|
-
// return {
|
|
133
|
-
// content: {
|
|
134
|
-
// 'application/json': {
|
|
135
|
-
// schema: typeof schema === 'string' ? ref(schema) : schema,
|
|
136
|
-
// },
|
|
137
|
-
// },
|
|
138
|
-
// }
|
|
139
|
-
// }
|
|
140
|
-
|
|
141
|
-
// function route(params: {
|
|
142
|
-
// category: string
|
|
143
|
-
// describe: string
|
|
144
|
-
// path: string
|
|
145
|
-
// body?: string | Obj
|
|
146
|
-
// response?: string | Obj
|
|
147
|
-
// }) {
|
|
148
|
-
// return {
|
|
149
|
-
// [params.path]: {
|
|
150
|
-
// post: {
|
|
151
|
-
// tags: [params.category],
|
|
152
|
-
// summary: params.describe,
|
|
153
|
-
// operationId: params.path,
|
|
154
|
-
// requestBody: params.body !== undefined ? jsonSchema(params.body) : undefined,
|
|
155
|
-
// responses: {
|
|
156
|
-
// default: {
|
|
157
|
-
// description: 'OK',
|
|
158
|
-
// ...jsonSchema(params.response ?? object({})),
|
|
159
|
-
// },
|
|
160
|
-
// },
|
|
161
|
-
// },
|
|
162
|
-
// },
|
|
163
|
-
// }
|
|
164
|
-
// }
|
|
165
|
-
|
|
166
|
-
// function response(schema: unknown) {
|
|
167
|
-
// return {
|
|
168
|
-
// type: 'object',
|
|
169
|
-
// properties: {
|
|
170
|
-
// success: boolean(undefined, { enum: [true] }),
|
|
171
|
-
// data: schema,
|
|
172
|
-
// },
|
|
173
|
-
// }
|
|
174
|
-
// }
|
|
175
|
-
|
|
176
|
-
// function pageResp(itemSchema: unknown) {
|
|
177
|
-
// return response({
|
|
178
|
-
// type: 'object',
|
|
179
|
-
// properties: {
|
|
180
|
-
// total: number(),
|
|
181
|
-
// list: array(itemSchema),
|
|
182
|
-
// },
|
|
183
|
-
// })
|
|
184
|
-
// }
|
|
1
|
+
import fsPromise from 'node:fs/promises'
|
|
2
|
+
import path from 'node:path'
|
|
3
|
+
import { getAbsoluteFSPath } from 'swagger-ui-dist'
|
|
4
|
+
import { HTTPError } from '@/http/index.js'
|
|
5
|
+
import type { Router, PathParameters, BaseContext } from '@/router/index.js'
|
|
6
|
+
import { normalizePath } from '@/router/match.js'
|
|
7
|
+
import * as factory from './factory.js'
|
|
8
|
+
import type { OpenAPI } from './openapi-spec.js'
|
|
9
|
+
|
|
10
|
+
export function registerSwaggerRoute(
|
|
11
|
+
router: Router,
|
|
12
|
+
endpoint: string = '/swagger',
|
|
13
|
+
customFields: OpenAPICustomFields = { info: { title: 'API Document', version: '0.0.1' } },
|
|
14
|
+
swaggerOptions?: Record<string, unknown>,
|
|
15
|
+
) {
|
|
16
|
+
router.register({
|
|
17
|
+
category: 'swagger',
|
|
18
|
+
method: 'GET',
|
|
19
|
+
path: normalizePath(endpoint) + '/*',
|
|
20
|
+
handler: pageRoute.bind(null, swaggerOptions),
|
|
21
|
+
})
|
|
22
|
+
router.register({
|
|
23
|
+
category: 'swagger',
|
|
24
|
+
method: 'GET',
|
|
25
|
+
path: normalizePath(endpoint) + '/api-swagger.json',
|
|
26
|
+
handler: apiRoute.bind(null, router, customFields),
|
|
27
|
+
})
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* swagger 页面路由
|
|
32
|
+
*/
|
|
33
|
+
async function pageRoute(
|
|
34
|
+
swaggerOptions: Record<string, unknown> | undefined, // Example: { defaultModelsExpandDepth: 1, defaultModelExpandDepth: 1 }
|
|
35
|
+
{ response }: BaseContext,
|
|
36
|
+
pathParameters: PathParameters,
|
|
37
|
+
) {
|
|
38
|
+
const file = (pathParameters['*'] ?? '') || 'index.html'
|
|
39
|
+
|
|
40
|
+
const swaggerDir = getAbsoluteFSPath()
|
|
41
|
+
const abspath = path.join(swaggerDir, file)
|
|
42
|
+
if (!cache.has(abspath)) {
|
|
43
|
+
try {
|
|
44
|
+
const stat = await fsPromise.stat(abspath)
|
|
45
|
+
if (!stat.isFile()) throw new HTTPError(404)
|
|
46
|
+
} catch (e) {
|
|
47
|
+
throw new HTTPError(404)
|
|
48
|
+
}
|
|
49
|
+
const content = await fsPromise.readFile(abspath)
|
|
50
|
+
const replacemented = replacement(abspath, content, swaggerOptions)
|
|
51
|
+
cache.set(abspath, replacemented)
|
|
52
|
+
}
|
|
53
|
+
response.text(cache.get(abspath)!)
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const cache = new Map<string, Buffer | string>()
|
|
57
|
+
|
|
58
|
+
function replacement(abspath: string, content: Buffer, swaggerOptions?: Record<string, unknown>) {
|
|
59
|
+
if (/swagger-initializer.js/.exec(abspath)) {
|
|
60
|
+
let formatted = content
|
|
61
|
+
.toString()
|
|
62
|
+
.replace('https://petstore.swagger.io/v2/swagger.json', './api-swagger.json')
|
|
63
|
+
if (swaggerOptions) {
|
|
64
|
+
formatted = formatted.replace(
|
|
65
|
+
'SwaggerUIBundle({',
|
|
66
|
+
'SwaggerUIBundle({\n ' + JSON.stringify(swaggerOptions).slice(1, -1) + ',\n',
|
|
67
|
+
)
|
|
68
|
+
}
|
|
69
|
+
return formatted
|
|
70
|
+
}
|
|
71
|
+
return content
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* swagger API 路由
|
|
76
|
+
*/
|
|
77
|
+
type OpenAPICustomFields = Omit<OpenAPI, 'openapi' | 'paths' | 'components'>
|
|
78
|
+
|
|
79
|
+
function apiRoute(router: Router, customFields: OpenAPICustomFields, { response }: BaseContext) {
|
|
80
|
+
const swaggerJSON: OpenAPI = {
|
|
81
|
+
...customFields,
|
|
82
|
+
openapi: '3.1.0',
|
|
83
|
+
paths: factory.paths(router.routes),
|
|
84
|
+
components: {
|
|
85
|
+
schemas: Object.entries(router.responseReferences).reduce(
|
|
86
|
+
(schemas, [name, type]) => ({ ...schemas, [name]: factory.responseSchema(type) }),
|
|
87
|
+
{},
|
|
88
|
+
),
|
|
89
|
+
},
|
|
90
|
+
}
|
|
91
|
+
response.json(swaggerJSON)
|
|
92
|
+
}
|
|
@@ -149,7 +149,7 @@ export interface Responses extends Extendable {
|
|
|
149
149
|
}
|
|
150
150
|
|
|
151
151
|
export interface Response extends Extendable {
|
|
152
|
-
|
|
152
|
+
description: CommonMark
|
|
153
153
|
headers?: Record<string, Header | Reference>
|
|
154
154
|
content?: Record<string, MediaType>
|
|
155
155
|
links?: Record<string, Link | Reference>
|
|
@@ -185,8 +185,8 @@ export interface Tag extends Extendable {
|
|
|
185
185
|
|
|
186
186
|
export interface Reference {
|
|
187
187
|
$ref: string
|
|
188
|
-
summary
|
|
189
|
-
description
|
|
188
|
+
summary?: string
|
|
189
|
+
description?: string
|
|
190
190
|
}
|
|
191
191
|
|
|
192
192
|
export interface Schema extends Extendable {
|