0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-01-20 21:32:31 -05:00

feat: auto-gen api doc

This commit is contained in:
Gao Sun 2021-07-23 23:10:54 +08:00 committed by Gao Sun
parent 2c0aa72bd8
commit e77d2d9414
14 changed files with 179 additions and 16 deletions

View file

@ -10,6 +10,18 @@ Copy proper `.env` to project root. (TBD: design the config process)
yarn && yarn dev
```
## Notes
## OpenAPI Doc
Upgrade `xo` after [this issue](https://github.com/SamVerschueren/vscode-linter-xo/issues/91) is solved.
OpenAPI (Swagger) json is available on `http(s)://your-domain/api/swagger.json`. If you are running locally, the default URL will be `http://localhost:3001/api/swagger.json`. Consume it in the way you like.
### Using ReDoc
The doc website can be served by [redoc-cli](https://github.com/Redocly/redoc/blob/master/cli/README.md) in an extremely easy way:
```bash
npx redoc-cli serve http://localhost:3001/api/swagger.json
```
### Using Swagger Editor
Copy the API output and paste it in the [Swagger Editor](https://editor.swagger.io/).

View file

@ -7,9 +7,9 @@
"license": "UNLICENSED",
"private": true,
"scripts": {
"build": "tsc",
"build": "rm -rf build/ && tsc",
"lint": "eslint --format pretty --ext .ts src",
"dev": "tsc-watch --onSuccess \"node ./build/index.js\"",
"dev": "rm -rf build/ && tsc-watch --onSuccess \"node ./build/index.js\"",
"prepare": "husky install"
},
"dependencies": {
@ -50,6 +50,7 @@
"eslint-formatter-pretty": "^4.1.0",
"husky": "^6.0.0",
"lint-staged": "^11.0.0",
"openapi-types": "^9.1.0",
"prettier": "^2.3.2",
"tsc-watch": "^4.4.0",
"typescript": "^4.3.5"

View file

@ -0,0 +1,7 @@
export enum SwaggerErrorCode {
InvalidZodType = 'swagger.invalid_zod_type',
}
export const swaggerErrorMessage: Record<SwaggerErrorCode, string> = {
[SwaggerErrorCode.InvalidZodType]: 'Invalid Zod type, please check route guard config.',
};

View file

@ -2,9 +2,11 @@ import { RequestErrorCode } from './types';
import { guardErrorMessage } from './collection/guard-errors';
import { oidcErrorMessage } from './collection/oidc-errors';
import { registerErrorMessage } from './collection/register-errors';
import { swaggerErrorMessage } from './collection/swagger-errors';
export const requestErrorMessage: Record<RequestErrorCode, string> = {
...guardErrorMessage,
...oidcErrorMessage,
...registerErrorMessage,
...swaggerErrorMessage,
};

View file

@ -1,10 +1,15 @@
import { GuardErrorCode } from './collection/guard-errors';
import { OidcErrorCode } from './collection/oidc-errors';
import { RegisterErrorCode } from './collection/register-errors';
import { SwaggerErrorCode } from './collection/swagger-errors';
export { GuardErrorCode, OidcErrorCode, RegisterErrorCode };
export { GuardErrorCode, OidcErrorCode, SwaggerErrorCode, RegisterErrorCode };
export type RequestErrorCode = GuardErrorCode | OidcErrorCode | RegisterErrorCode;
export type RequestErrorCode =
| GuardErrorCode
| OidcErrorCode
| RegisterErrorCode
| SwaggerErrorCode;
export type RequestErrorMetadata = {
code: RequestErrorCode;

View file

@ -1,21 +1,23 @@
import Koa from 'koa';
import Router from 'koa-router';
import { Provider } from 'oidc-provider';
import createSignInRoutes from '@/routes/sign-in';
import createUIProxy from '@/proxies/ui';
import createRegisterRoutes from '@/routes/register';
import signInRoutes from '@/routes/sign-in';
import registerRoutes from '@/routes/register';
import uiProxy from '@/proxies/ui';
import swaggerRoutes from '@/routes/swagger';
const createRouter = (provider: Provider): Router => {
const router = new Router({ prefix: '/api' });
router.use(createSignInRoutes(provider));
router.use(createRegisterRoutes());
router.use(signInRoutes(provider));
router.use(registerRoutes());
router.use(swaggerRoutes());
return router;
};
export default function initRouter(app: Koa, provider: Provider): Router {
const router = createRouter(provider);
app.use(router.routes()).use(createUIProxy()).use(router.allowedMethods());
app.use(router.routes()).use(uiProxy()).use(router.allowedMethods());
return router;
}

View file

@ -1,7 +1,8 @@
import RequestError, { GuardErrorCode } from '@/errors/RequestError';
import { has } from '@logto/essentials';
import { Middleware } from 'koa';
import koaBody from 'koa-body';
import { IRouterParamContext } from 'koa-router';
import { IMiddleware, IRouterParamContext } from 'koa-router';
import { ZodType } from 'zod';
export type GuardConfig<QueryT, BodyT, ParametersT> = {
@ -34,6 +35,11 @@ export type WithGuardConfig<
config: GuardConfig<GuardQueryT, GuardBodyT, GuardParametersT>;
};
export const isGuardMiddleware = <Type extends IMiddleware>(
function_: Type
): function_ is WithGuardConfig<Type> =>
function_.name === 'guardMiddleware' && has(function_, 'config');
export default function koaGuard<
StateT,
ContextT extends IRouterParamContext,

View file

@ -1,7 +1,7 @@
import proxy from 'koa-proxies';
// CAUTION: this is for testing only
export default function createUIProxy() {
export default function uiProxy() {
return proxy(/^\/(?!api|oidc).*$/, {
target: 'http://localhost:3000',
changeOrigin: true,

View file

@ -22,7 +22,7 @@ const generateUserId = async (maxRetries = 500) => {
throw new Error('Cannot generate user ID in reasonable retries');
};
export default function createRegisterRoutes() {
export default function registerRoutes() {
const router = new Router();
router.post(

View file

@ -8,7 +8,7 @@ import { conditional } from '@logto/essentials';
import koaGuard from '@/middleware/koa-guard';
import { OidcErrorCode } from '@/errors/RequestError';
export default function createSignInRoutes(provider: Provider) {
export default function signInRoutes(provider: Provider) {
const router = new Router();
router.post(

View file

@ -0,0 +1,63 @@
import Router, { IMiddleware } from 'koa-router';
import { OpenAPIV3 } from 'openapi-types';
import { isGuardMiddleware, WithGuardConfig } from '@/middleware/koa-guard';
import { toTitle } from '@/utils/string';
import { zodTypeToSwagger } from '@/utils/zod';
export default function swaggerRoutes() {
const router = new Router();
router.get('/swagger.json', async (ctx) => {
const routes = ctx.router.stack.map(({ path, stack, methods }) => {
const guard = stack.find((function_): function_ is WithGuardConfig<IMiddleware> =>
isGuardMiddleware(function_)
);
return { path, methods, guard };
});
const paths = Object.fromEntries(
routes.map<[string, OpenAPIV3.PathItemObject]>(({ path, methods, guard }) => {
const trimmedPath = path.slice(4);
const body = guard?.config.body;
return [
trimmedPath,
Object.fromEntries(
methods.map<[string, OpenAPIV3.OperationObject]>((method) => [
method.toLowerCase(),
{
tags: [toTitle(trimmedPath.split('/')[1] ?? 'General')],
requestBody: body && {
required: true,
content: {
'application/json': {
schema: zodTypeToSwagger(body),
},
},
},
responses: {
'200': {
description: 'OK',
},
},
},
])
),
];
})
);
const document: OpenAPIV3.Document = {
openapi: '3.0.1',
info: {
title: 'Logto Core',
version: '0.1.0',
},
paths,
};
ctx.body = document;
});
return router.routes();
}

View file

@ -0,0 +1,10 @@
export function toTitle(string: string) {
if (typeof string !== 'string') {
throw new TypeError('Expected a string');
}
return string
.toLowerCase()
.replace(/(?:^|\s|-)\S/g, (x) => x.toUpperCase())
.replace(/-/g, ' ');
}

View file

@ -0,0 +1,50 @@
import { OpenAPIV3 } from 'openapi-types';
import { ZodArray, ZodBoolean, ZodNumber, ZodObject, ZodOptional, ZodString } from 'zod';
import RequestError, { SwaggerErrorCode } from '@/errors/RequestError';
import { conditional } from '@logto/essentials';
export const zodTypeToSwagger = (config: unknown): OpenAPIV3.SchemaObject => {
if (config instanceof ZodOptional) {
return zodTypeToSwagger(config._def.innerType);
}
if (config instanceof ZodObject) {
const entries = Object.entries(config.shape);
const required = entries
.filter(([, value]) => !(value instanceof ZodOptional))
.map(([key]) => key);
return {
type: 'object',
required: conditional(required.length > 0 && required),
properties: Object.fromEntries(entries.map(([key, value]) => [key, zodTypeToSwagger(value)])),
};
}
if (config instanceof ZodArray) {
return {
type: 'array',
items: zodTypeToSwagger(config._def.type),
};
}
if (config instanceof ZodString) {
return {
type: 'string',
};
}
if (config instanceof ZodNumber) {
return {
type: 'number',
};
}
if (config instanceof ZodBoolean) {
return {
type: 'boolean',
};
}
throw new RequestError(SwaggerErrorCode.InvalidZodType, config);
};

View file

@ -3320,6 +3320,11 @@ only@~0.0.2:
resolved "https://registry.yarnpkg.com/only/-/only-0.0.2.tgz#2afde84d03e50b9a8edc444e30610a70295edfb4"
integrity sha1-Kv3oTQPlC5qO3EROMGEKcCle37Q=
openapi-types@^9.1.0:
version "9.1.0"
resolved "https://registry.yarnpkg.com/openapi-types/-/openapi-types-9.1.0.tgz#a2ab0acc5198c1725f83d7cbe063efd1bcd0479e"
integrity sha512-mhXh8QN8sbErlxfxBeZ/pzgvmDn443p8CXlxwGSi2bWANZAFvjLPI0PoGjqHW+JdBbXg6uvmvM81WXaweh/SVA==
optionator@^0.9.1:
version "0.9.1"
resolved "https://registry.yarnpkg.com/optionator/-/optionator-0.9.1.tgz#4f236a6373dae0566a6d43e1326674f50c291499"