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.
@@ -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
- // import { registerSwaggerRoute } from '../swagger/index.js'
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
- response: { ref: 'hello' },
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
- // registerSwaggerRoute(router)
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'),
@@ -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
- describe?: string;
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 {};
@@ -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>, 'describe' | 'category' | 'method'>;
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
- describe?: string;
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
- describe?: string;
77
+ description?: string;
78
78
  /** 值类型。ResponseDataType[] 代表有多种可能的类型 */
79
79
  type: ResponseDataType | ResponseDataType[];
80
80
  }
@@ -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
- describe: '',
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
+ }
@@ -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 {};
@@ -1,168 +1,67 @@
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 {
6
- // type Router,
7
- // type PathParameters,
8
- // type BaseContext,
9
- // type ResponseDataType,
10
- // } from '../router/index.js'
11
- // import { normalizePath } from '../router/match.js'
12
- // import type { OpenAPI, Schema, PathItem } from './openapi-spec.js'
13
- export {};
14
- // export function registerSwaggerRoute(
15
- // router: Router,
16
- // endpoint: string = '/swagger',
17
- // customFields: OpenAPICustomFields = { info: { title: 'API Document', version: '0.0.1' } },
18
- // ) {
19
- // router.register('GET', normalizePath(endpoint) + '/*', pageRoute)
20
- // router.register(
21
- // 'GET',
22
- // normalizePath(endpoint) + '/api-swagger.json',
23
- // apiRoute.bind(null, router, customFields),
24
- // )
25
- // }
26
- // /**
27
- // * ---------------------------------
28
- // * swagger 页面路由
29
- // * ---------------------------------
30
- // */
31
- // async function pageRoute({ response }: BaseContext, pathParameters: PathParameters) {
32
- // const file = (pathParameters['*'] ?? '') || 'index.html'
33
- // const swaggerDir = getAbsoluteFSPath()
34
- // const abspath = path.join(swaggerDir, file)
35
- // if (!cache.has(abspath)) {
36
- // try {
37
- // const stat = await fsPromise.stat(abspath)
38
- // if (!stat.isFile()) throw new HTTPError(404)
39
- // } catch (e) {
40
- // throw new HTTPError(404)
41
- // }
42
- // const content = await fsPromise.readFile(abspath)
43
- // const replacemented = replacement(abspath, content)
44
- // cache.set(abspath, replacemented)
45
- // }
46
- // response.text(cache.get(abspath)!)
47
- // }
48
- // const cache = new Map<string, Buffer | string>()
49
- // function replacement(abspath: string, content: Buffer) {
50
- // if (/swagger-initializer.js/.exec(abspath)) {
51
- // return content
52
- // .toString()
53
- // .replace('https://petstore.swagger.io/v2/swagger.json', './api-swagger.json')
54
- // // .replace('SwaggerUIBundle({', 'SwaggerUIBundle({\n defaultModelsExpandDepth: 1,\n defaultModelExpandDepth: 1,')
55
- // }
56
- // return content
57
- // }
58
- // /**
59
- // * ---------------------------------
60
- // * swagger API 路由
61
- // * ---------------------------------
62
- // */
63
- // type OpenAPICustomFields = Omit<OpenAPI, 'openapi' | 'paths' | 'components'>
64
- // function apiRoute(router: Router, customFields: OpenAPICustomFields, { response }: BaseContext) {
65
- // const swaggerJSON: OpenAPI = {
66
- // ...customFields,
67
- // openapi: '3.1.0',
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
- descriptoin: CommonMark;
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: string;
160
- description: string;
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.1",
3
+ "version": "1.0.0",
4
4
  "description": "Simple But Powerful Node.js HTTP Server",
5
5
  "type": "module",
6
6
  "scripts": {
7
- "watch": "rimraf dist && tsc && (concurrently \"tsc -w\" \"tsc-alias -w\")",
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
- // import { registerSwaggerRoute } from '@/swagger/index.js'
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
- response: { ref: 'hello' },
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
- // registerSwaggerRoute(router)
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,
@@ -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'
@@ -21,7 +21,7 @@ export interface Parameter {
21
21
  name: string
22
22
 
23
23
  /** 文字描述 */
24
- describe?: string
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<Parameter, 'type' | 'validate' | 'required' | 'nullable' | 'defaults'>
43
+ export type BasicParameter = Pick<
44
+ Parameter,
45
+ 'type' | 'validate' | 'required' | 'nullable' | 'defaults' | 'description'
46
+ >
44
47
 
45
48
  /**
46
49
  * 请求参数的数据类型定义
@@ -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>, 'describe' | 'category' | 'method'>
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
- describe?: string
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
- describe?: string
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
- describe: '',
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
+ }
@@ -1,184 +1,92 @@
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 {
6
- // type Router,
7
- // type PathParameters,
8
- // type BaseContext,
9
- // type ResponseDataType,
10
- // } from '@/router/index.js'
11
- // import { normalizePath } from '@/router/match.js'
12
- // import type { OpenAPI, Schema, PathItem } from './openapi-spec.js'
13
-
14
- // export function registerSwaggerRoute(
15
- // router: Router,
16
- // endpoint: string = '/swagger',
17
- // customFields: OpenAPICustomFields = { info: { title: 'API Document', version: '0.0.1' } },
18
- // ) {
19
- // router.register('GET', normalizePath(endpoint) + '/*', pageRoute)
20
- // router.register(
21
- // 'GET',
22
- // normalizePath(endpoint) + '/api-swagger.json',
23
- // apiRoute.bind(null, router, customFields),
24
- // )
25
- // }
26
-
27
- // /**
28
- // * ---------------------------------
29
- // * swagger 页面路由
30
- // * ---------------------------------
31
- // */
32
- // async function pageRoute({ response }: BaseContext, pathParameters: PathParameters) {
33
- // const file = (pathParameters['*'] ?? '') || 'index.html'
34
-
35
- // const swaggerDir = getAbsoluteFSPath()
36
- // const abspath = path.join(swaggerDir, file)
37
- // if (!cache.has(abspath)) {
38
- // try {
39
- // const stat = await fsPromise.stat(abspath)
40
- // if (!stat.isFile()) throw new HTTPError(404)
41
- // } catch (e) {
42
- // throw new HTTPError(404)
43
- // }
44
- // const content = await fsPromise.readFile(abspath)
45
- // const replacemented = replacement(abspath, content)
46
- // cache.set(abspath, replacemented)
47
- // }
48
- // response.text(cache.get(abspath)!)
49
- // }
50
-
51
- // const cache = new Map<string, Buffer | string>()
52
-
53
- // function replacement(abspath: string, content: Buffer) {
54
- // if (/swagger-initializer.js/.exec(abspath)) {
55
- // return content
56
- // .toString()
57
- // .replace('https://petstore.swagger.io/v2/swagger.json', './api-swagger.json')
58
- // // .replace('SwaggerUIBundle({', 'SwaggerUIBundle({\n defaultModelsExpandDepth: 1,\n defaultModelExpandDepth: 1,')
59
- // }
60
- // return content
61
- // }
62
-
63
- // /**
64
- // * ---------------------------------
65
- // * swagger API 路由
66
- // * ---------------------------------
67
- // */
68
- // type OpenAPICustomFields = Omit<OpenAPI, 'openapi' | 'paths' | 'components'>
69
-
70
- // function apiRoute(router: Router, customFields: OpenAPICustomFields, { response }: BaseContext) {
71
- // const swaggerJSON: OpenAPI = {
72
- // ...customFields,
73
- // openapi: '3.1.0',
74
- // paths: makePaths(router),
75
- // components: {
76
- // schemas: makeRefs(router),
77
- // },
78
- // }
79
- // response.json(swaggerJSON)
80
- // }
81
-
82
- // function makeRefs(router: Router) {
83
- // const schemas: Record<string, Schema> = {}
84
- // for (const [id, type] of Object.entries(router.responseReferences)) {
85
- // schemas[id] = responseDataType2Schema(type)
86
- // }
87
- // return schemas
88
- // }
89
-
90
- // function makePaths(router: Router) {
91
- // const paths: Record<string, PathItem> = {}
92
- // return paths
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
- descriptoin: CommonMark
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: string
189
- description: string
188
+ summary?: string
189
+ description?: string
190
190
  }
191
191
 
192
192
  export interface Schema extends Extendable {