0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2024-12-30 20:33:54 -05:00
logto/packages/core/src/routes/swagger.ts
2022-10-21 14:03:35 +08:00

175 lines
4.8 KiB
TypeScript

import { readFile } from 'fs/promises';
import { toTitle } from '@silverhand/essentials';
import { load } from 'js-yaml';
import type { IMiddleware } from 'koa-router';
import type Router from 'koa-router';
import type { OpenAPIV3 } from 'openapi-types';
import { ZodObject, ZodOptional } from 'zod';
import type { WithGuardConfig } from '@/middleware/koa-guard';
import { isGuardMiddleware } from '@/middleware/koa-guard';
import { fallbackDefaultPageSize, isPaginationMiddleware } from '@/middleware/koa-pagination';
import assertThat from '@/utils/assert-that';
import { translationSchemas, zodTypeToSwagger } from '@/utils/zod';
import type { AnonymousRouter } from './types';
type RouteObject = {
path: string;
method: OpenAPIV3.HttpMethods;
operation: OpenAPIV3.OperationObject;
};
type MethodMap = {
[key in OpenAPIV3.HttpMethods]?: OpenAPIV3.OperationObject;
};
export const paginationParameters: OpenAPIV3.ParameterObject[] = [
{
name: 'page',
in: 'query',
required: false,
schema: {
type: 'integer',
minimum: 1,
default: 1,
},
},
{
name: 'page_size',
in: 'query',
required: false,
schema: {
type: 'integer',
minimum: 1,
default: fallbackDefaultPageSize,
},
},
];
// Parameter serialization: https://swagger.io/docs/specification/serialization
const buildParameters = (
zodParameters: unknown,
inWhere: 'path' | 'query'
): OpenAPIV3.ParameterObject[] => {
if (!zodParameters) {
return [];
}
assertThat(zodParameters instanceof ZodObject, 'swagger.not_supported_zod_type_for_params');
return Object.entries(zodParameters.shape).map(([key, value]) => ({
name: key,
in: inWhere,
required: !(value instanceof ZodOptional),
schema: zodTypeToSwagger(value),
}));
};
const buildTag = (path: string) => {
const root = path.split('/')[1];
if (root?.startsWith('.')) {
return root;
}
return toTitle(root ?? 'General');
};
export const defaultResponses: OpenAPIV3.ResponsesObject = {
'200': {
description: 'OK',
},
};
const buildOperation = (
stack: IMiddleware[],
path: string,
customResponses?: OpenAPIV3.ResponsesObject
): OpenAPIV3.OperationObject => {
const guard = stack.find((function_): function_ is WithGuardConfig<IMiddleware> =>
isGuardMiddleware(function_)
);
const pathParameters = buildParameters(guard?.config.params, 'path');
const hasPagination = stack.some((function_) => isPaginationMiddleware(function_));
const queryParameters = [
...buildParameters(guard?.config.query, 'query'),
...(hasPagination ? paginationParameters : []),
];
const body = guard?.config.body;
const requestBody = body && {
required: true,
content: {
'application/json': {
schema: zodTypeToSwagger(body),
},
},
};
return {
tags: [buildTag(path)],
parameters: [...pathParameters, ...queryParameters],
requestBody,
responses: customResponses ?? defaultResponses,
};
};
// Keep using `any` to accept various custom context types.
// eslint-disable-next-line @typescript-eslint/no-explicit-any
export default function swaggerRoutes<T extends AnonymousRouter, R extends Router<unknown, any>>(
router: T,
allRouters: R[]
) {
router.get('/swagger.json', async (ctx, next) => {
// Use `as` here since we'll check typing with integration tests
// eslint-disable-next-line no-restricted-syntax
const additionalSwagger = load(
await readFile('static/yaml/additional-swagger.yaml', { encoding: 'utf8' })
) as OpenAPIV3.Document;
const routes = allRouters.flatMap<RouteObject>((router) =>
router.stack.flatMap<RouteObject>(({ path: routerPath, stack, methods }) =>
methods
.map((method) => method.toLowerCase())
// There is no need to show the HEAD method.
.filter((method): method is OpenAPIV3.HttpMethods => method !== 'head')
.map((httpMethod) => {
const path = `/api${routerPath}`;
const additionalPathItem = additionalSwagger.paths[path] ?? {};
const additionalResponses = additionalPathItem[httpMethod]?.responses;
return {
path,
method: httpMethod,
operation: buildOperation(stack, routerPath, additionalResponses),
};
})
)
);
const pathMap = new Map<string, MethodMap>();
// Group routes by path
for (const { path, method, operation } of routes) {
pathMap.set(path, { ...pathMap.get(path), [method]: operation });
}
const document: OpenAPIV3.Document = {
openapi: '3.0.1',
info: {
title: 'Logto Core',
version: '0.1.0',
},
paths: Object.fromEntries(pathMap),
components: { schemas: translationSchemas },
};
ctx.body = document;
return next();
});
}