Skip to content

Commit 78ad279

Browse files
authored
chore: openapi doc generator (#2644)
* chore: extract the type and comment from apis * chore: template code * feat: openapi * pref: openapi generator. send into public/openapi folder
1 parent 5f3c8e9 commit 78ad279

File tree

12 files changed

+2221
-31
lines changed

12 files changed

+2221
-31
lines changed

package.json

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,7 @@
1010
"postinstall": "sh ./scripts/postinstall.sh",
1111
"initIcon": "node ./scripts/icon/init.js",
1212
"previewIcon": "node ./scripts/icon/index.js",
13-
"i18n:delete-unused-keys": "node ./scripts/i18n/delete-unused-keys.js",
14-
"i18n:query": "node ./scripts/i18n/query.js"
13+
"api:gen": "tsc ./scripts/openapi/index.ts && node ./scripts/openapi/index.js && npx @redocly/cli build-docs ./scripts/openapi/openapi.json -o ./projects/app/public/openapi/index.html"
1514
},
1615
"devDependencies": {
1716
"@chakra-ui/cli": "^2.4.1",

pnpm-lock.yaml

Lines changed: 1218 additions & 27 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

projects/app/public/openapi/index.html

Lines changed: 443 additions & 0 deletions
Large diffs are not rendered by default.

projects/app/src/pages/api/support/outLink/list.ts

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,13 +4,25 @@ import { ManagePermissionVal } from '@fastgpt/global/support/permission/constant
44
import type { ApiRequestProps } from '@fastgpt/service/type/next';
55
import { NextAPI } from '@/service/middleware/entry';
66
import { OutLinkSchema } from '@fastgpt/global/support/outLink/type';
7+
8+
export const ApiMetadata = {
9+
name: '获取应用内所有 Outlink',
10+
author: 'Finley',
11+
version: '0.1.0'
12+
};
13+
14+
// Outlink
715
export type OutLinkListQuery = {
8-
appId: string;
9-
type: string;
16+
appId: string; // 应用 ID
17+
type: string; // 类型
1018
};
19+
1120
export type OutLinkListBody = {};
21+
22+
// 响应: 应用内全部 Outlink
1223
export type OutLinkListResponse = OutLinkSchema[];
1324

25+
// 查询应用内全部 Outlink
1426
async function handler(
1527
req: ApiRequestProps<OutLinkListBody, OutLinkListQuery>
1628
): Promise<OutLinkListResponse> {

scripts/openapi/.env

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
SEARCH_PATH=support

scripts/openapi/.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
*.js
2+
openapi.json
3+
openapi.out

scripts/openapi/index.ts

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
import { parseAPI } from './utils';
2+
import * as fs from 'fs';
3+
import * as path from 'path';
4+
import { convertOpenApi } from './openapi';
5+
6+
const rootPath = 'projects/app/src/pages/api';
7+
const exclude = ['/admin', '/proApi'];
8+
9+
function getAllFiles(dir: string) {
10+
let files: string[] = [];
11+
const stat = fs.statSync(dir);
12+
if (stat.isDirectory()) {
13+
const list = fs.readdirSync(dir);
14+
list.forEach((item) => {
15+
const fullPath = path.join(dir, item);
16+
if (!exclude.some((excluded) => fullPath.includes(excluded))) {
17+
files = files.concat(getAllFiles(fullPath));
18+
}
19+
});
20+
} else {
21+
files.push(dir);
22+
}
23+
return files;
24+
}
25+
26+
const searchPath = process.env.SEARCH_PATH || '';
27+
28+
const files = getAllFiles(path.join(rootPath, searchPath));
29+
// console.log(files)
30+
const apis = files.map((file) => {
31+
return parseAPI({ path: file, rootPath });
32+
});
33+
34+
const openapi = convertOpenApi({
35+
apis,
36+
openapi: '3.0.0',
37+
info: {
38+
title: 'FastGPT OpenAPI',
39+
version: '1.0.0',
40+
author: 'FastGPT'
41+
},
42+
servers: [
43+
{
44+
url: 'http://localhost:4000'
45+
}
46+
]
47+
});
48+
49+
const json = JSON.stringify(openapi, null, 2);
50+
51+
fs.writeFileSync('./scripts/openapi/openapi.json', json);
52+
fs.writeFileSync('./scripts/openapi/openapi.out', JSON.stringify(apis, null, 2));
53+
54+
console.log('Total APIs:', files.length);

scripts/openapi/openapi.ts

Lines changed: 181 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,181 @@
1+
import { ApiType } from './type';
2+
3+
type OpenAPIParameter = {
4+
name: string;
5+
in: string;
6+
description: string;
7+
required: boolean;
8+
schema: {
9+
type: string;
10+
};
11+
};
12+
13+
type OpenAPIResponse = {
14+
[code: string]: {
15+
description?: string;
16+
content: {
17+
[mediaType: string]: {
18+
schema: {
19+
type: string;
20+
properties?: {
21+
[key: string]: {
22+
type: string;
23+
description?: string;
24+
};
25+
};
26+
};
27+
};
28+
};
29+
};
30+
};
31+
32+
type PathType = {
33+
[method: string]: {
34+
description: string;
35+
parameters: OpenAPIParameter[];
36+
responses: OpenAPIResponse;
37+
};
38+
};
39+
40+
type PathsType = {
41+
[url: string]: PathType;
42+
};
43+
44+
type OpenApiType = {
45+
openapi: string;
46+
info: {
47+
title: string;
48+
version: string;
49+
author: string;
50+
};
51+
paths: PathsType;
52+
servers?: {
53+
url: string;
54+
}[];
55+
};
56+
57+
export function convertPath(api: ApiType): PathType {
58+
const method = api.method.toLowerCase();
59+
const parameters: any[] = [];
60+
if (api.query) {
61+
if (Array.isArray(api.query)) {
62+
api.query.forEach((item) => {
63+
parameters.push({
64+
name: item.key,
65+
description: item.comment,
66+
in: 'query',
67+
required: item.required,
68+
schema: {
69+
type: item.type
70+
}
71+
});
72+
});
73+
} else {
74+
parameters.push({
75+
description: api.query.comment,
76+
name: api.query.key,
77+
in: 'query',
78+
required: api.query.required,
79+
schema: {
80+
type: api.query.type
81+
}
82+
});
83+
}
84+
} else if (api.body) {
85+
if (Array.isArray(api.body)) {
86+
api.body.forEach((item) => {
87+
parameters.push({
88+
description: item.comment,
89+
name: item.key,
90+
in: 'body',
91+
required: item.required,
92+
schema: {
93+
type: item.type
94+
}
95+
});
96+
});
97+
}
98+
}
99+
100+
const responses: OpenAPIResponse = (() => {
101+
if (api.response) {
102+
if (Array.isArray(api.response)) {
103+
const properties: {
104+
[key: string]: {
105+
type: string;
106+
description?: string;
107+
};
108+
} = {};
109+
110+
api.response.forEach((item) => {
111+
properties[item.type] = {
112+
type: item.key ?? item.type,
113+
description: item.comment
114+
};
115+
});
116+
const res: OpenAPIResponse = {
117+
'200': {
118+
description: api.description ?? '',
119+
content: {
120+
'application/json': {
121+
schema: {
122+
type: 'object',
123+
properties
124+
}
125+
}
126+
}
127+
}
128+
};
129+
return res;
130+
} else {
131+
return {
132+
'200': {
133+
description: api.response.comment ?? '',
134+
content: {
135+
'application/json': {
136+
schema: {
137+
type: api.response.type
138+
}
139+
}
140+
}
141+
}
142+
};
143+
}
144+
} else {
145+
return {
146+
'200': {
147+
description: api.description ?? '',
148+
content: {
149+
'application/json': {
150+
schema: {
151+
type: 'object'
152+
}
153+
}
154+
}
155+
}
156+
};
157+
}
158+
})();
159+
return {
160+
[method]: {
161+
description: api.description ?? '',
162+
parameters,
163+
responses
164+
}
165+
};
166+
}
167+
export function convertOpenApi({
168+
apis,
169+
...rest
170+
}: {
171+
apis: ApiType[];
172+
} & Omit<OpenApiType, 'paths'>): OpenApiType {
173+
const paths: PathsType = {};
174+
apis.forEach((api) => {
175+
paths[api.url] = convertPath(api);
176+
});
177+
return {
178+
paths,
179+
...rest
180+
};
181+
}

scripts/openapi/package.json

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
{
2+
"name": "test",
3+
"module": "index.js",
4+
"scripts": {
5+
"build": "tsc index.ts"
6+
},
7+
"devDependencies": {
8+
"@babel/types": "^7.25.6",
9+
"@types/babel__generator": "^7.6.8",
10+
"@types/babel__traverse": "^7.20.6"
11+
},
12+
"peerDependencies": {
13+
"typescript": "^5.0.0"
14+
},
15+
"dependencies": {
16+
"@babel/generator": "^7.25.6",
17+
"@babel/parser": "^7.25.6",
18+
"@babel/traverse": "^7.25.6",
19+
"babel": "^6.23.0"
20+
}
21+
}

scripts/openapi/template.md

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
```ts
2+
import type { ApiRequestProps, ApiResponseType } from '@fastgpt/service/type/next';
3+
import { NextAPI } from '@/service/middleware/entry';
4+
5+
// This should be at the top of the file after the imports
6+
export const ApiMetadata = {
7+
name: 'template example api',
8+
author: 'Finley',
9+
version: '0.1.0',
10+
}
11+
12+
export type TemplateQuery = {
13+
// The App's ID
14+
appId?: string[],
15+
// The App's Name
16+
name: string,
17+
// The App's Description
18+
description: string | Something<AppDetailType>,
19+
};
20+
21+
export type TemplateBody = {
22+
// The App's Name
23+
name: string,
24+
};
25+
26+
// This is the response type for the API
27+
export type TemplateResponse = AppDetailType;
28+
29+
// This is the template API for FASTGPT NextAPI
30+
async function handler(
31+
req: ApiRequestProps<TemplateBody, TemplateQuery>,
32+
res: ApiResponseType<any>,
33+
): Promise<TemplateResponse> {
34+
35+
return {}
36+
}
37+
38+
export default NextAPI(handler);
39+
40+
```

scripts/openapi/type.d.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
export type ApiMetaData = {
2+
name?: string;
3+
author?: string;
4+
version?: string;
5+
};
6+
7+
export type ApiType = {
8+
description?: string;
9+
path: string;
10+
url: string;
11+
query?: itemType | itemType[];
12+
body?: itemType | itemType[];
13+
response?: itemType | itemType[];
14+
method: 'GET' | 'POST' | 'PUT' | 'DELETE';
15+
} & ApiMetaData;
16+
17+
export type itemType = {
18+
comment?: string;
19+
key?: string;
20+
type: string;
21+
required?: boolean;
22+
};

0 commit comments

Comments
 (0)