mirror of
https://github.com/logto-io/logto.git
synced 2025-02-17 22:04:19 -05:00
feat(core): append additional yaml responses to swagger.json (#1407)
This commit is contained in:
parent
d75dc24e33
commit
100bffbc6a
5 changed files with 248 additions and 291 deletions
|
@ -50,6 +50,7 @@
|
|||
"iconv-lite": "0.6.3",
|
||||
"inquirer": "^8.2.2",
|
||||
"jose": "^4.0.0",
|
||||
"js-yaml": "^4.1.0",
|
||||
"koa": "^2.13.1",
|
||||
"koa-body": "^5.0.0",
|
||||
"koa-compose": "^4.1.0",
|
||||
|
@ -81,6 +82,7 @@
|
|||
"@types/etag": "^1.8.1",
|
||||
"@types/inquirer": "^8.2.1",
|
||||
"@types/jest": "^27.4.1",
|
||||
"@types/js-yaml": "^4.0.5",
|
||||
"@types/koa": "^2.13.3",
|
||||
"@types/koa-compose": "^3.2.5",
|
||||
"@types/koa-logger": "^3.1.1",
|
||||
|
|
|
@ -1,3 +1,4 @@
|
|||
import { load } from 'js-yaml';
|
||||
import Koa from 'koa';
|
||||
import Router from 'koa-router';
|
||||
import request from 'supertest';
|
||||
|
@ -7,9 +8,13 @@ import koaGuard from '@/middleware/koa-guard';
|
|||
import koaPagination from '@/middleware/koa-pagination';
|
||||
import { AnonymousRouter } from '@/routes/types';
|
||||
|
||||
import swaggerRoutes, { paginationParameters } from './swagger';
|
||||
import swaggerRoutes, { defaultResponses, paginationParameters } from './swagger';
|
||||
|
||||
const createSwaggerRequest = (
|
||||
jest.mock('js-yaml', () => ({
|
||||
load: jest.fn().mockReturnValue({ paths: {} }),
|
||||
}));
|
||||
|
||||
export const createSwaggerRequest = (
|
||||
allRouters: Array<Router<unknown, any>>,
|
||||
swaggerRouter: AnonymousRouter = new Router()
|
||||
) => {
|
||||
|
@ -233,4 +238,60 @@ describe('GET /swagger.json', () => {
|
|||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe('should use correct responses', () => {
|
||||
it('should use "defaultResponses" if there is no custom "responses" from the additional swagger', async () => {
|
||||
(load as jest.Mock).mockReturnValueOnce({
|
||||
paths: { '/api/mock': { delete: {} } },
|
||||
});
|
||||
|
||||
const swaggerRequest = createSwaggerRequest([mockRouter]);
|
||||
const response = await swaggerRequest.get('/swagger.json');
|
||||
expect(response.body.paths).toMatchObject({
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
'/api/mock': expect.objectContaining({
|
||||
delete: expect.objectContaining({ responses: defaultResponses }),
|
||||
}),
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
|
||||
});
|
||||
});
|
||||
|
||||
it('should use custom "responses" from the additional swagger if it exists', async () => {
|
||||
(load as jest.Mock).mockReturnValueOnce({
|
||||
paths: {
|
||||
'/api/mock': {
|
||||
get: {
|
||||
responses: {
|
||||
'204': { description: 'No Content' },
|
||||
},
|
||||
},
|
||||
patch: {
|
||||
responses: {
|
||||
'202': { description: 'Accepted' },
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
});
|
||||
|
||||
const swaggerRequest = createSwaggerRequest([mockRouter]);
|
||||
const response = await swaggerRequest.get('/swagger.json');
|
||||
expect(response.body.paths).toMatchObject({
|
||||
/* eslint-disable @typescript-eslint/no-unsafe-assignment */
|
||||
'/api/mock': {
|
||||
get: expect.objectContaining({
|
||||
responses: {
|
||||
'204': { description: 'No Content' },
|
||||
},
|
||||
}),
|
||||
patch: expect.objectContaining({
|
||||
responses: {
|
||||
'202': { description: 'Accepted' },
|
||||
},
|
||||
}),
|
||||
},
|
||||
/* eslint-enable @typescript-eslint/no-unsafe-assignment */
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,10 +1,13 @@
|
|||
import { readFile } from 'fs/promises';
|
||||
|
||||
import { toTitle } from '@silverhand/essentials';
|
||||
import { load } from 'js-yaml';
|
||||
import Router, { IMiddleware } from 'koa-router';
|
||||
import { OpenAPIV3 } from 'openapi-types';
|
||||
import { ZodObject, ZodOptional } from 'zod';
|
||||
|
||||
import { isGuardMiddleware, WithGuardConfig } from '@/middleware/koa-guard';
|
||||
import { isPaginationMiddleware, fallbackDefaultPageSize } from '@/middleware/koa-pagination';
|
||||
import { fallbackDefaultPageSize, isPaginationMiddleware } from '@/middleware/koa-pagination';
|
||||
import assertThat from '@/utils/assert-that';
|
||||
import { zodTypeToSwagger } from '@/utils/zod';
|
||||
|
||||
|
@ -62,7 +65,7 @@ const buildParameters = (
|
|||
}));
|
||||
};
|
||||
|
||||
function buildTag(path: string) {
|
||||
const buildTag = (path: string) => {
|
||||
const root = path.split('/')[1];
|
||||
|
||||
if (root?.startsWith('.')) {
|
||||
|
@ -70,13 +73,29 @@ function buildTag(path: string) {
|
|||
}
|
||||
|
||||
return toTitle(root ?? 'General');
|
||||
}
|
||||
};
|
||||
|
||||
const buildOperation = (stack: IMiddleware[], path: string): OpenAPIV3.OperationObject => {
|
||||
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 && {
|
||||
|
@ -88,21 +107,11 @@ const buildOperation = (stack: IMiddleware[], path: string): OpenAPIV3.Operation
|
|||
},
|
||||
};
|
||||
|
||||
const pathParameters = buildParameters(guard?.config.params, 'path');
|
||||
const queryParameters = [
|
||||
...buildParameters(guard?.config.query, 'query'),
|
||||
...(hasPagination ? paginationParameters : []),
|
||||
];
|
||||
|
||||
return {
|
||||
tags: [buildTag(path)],
|
||||
parameters: [...pathParameters, ...queryParameters],
|
||||
requestBody,
|
||||
responses: {
|
||||
'200': {
|
||||
description: 'OK',
|
||||
},
|
||||
},
|
||||
responses: customResponses ?? defaultResponses,
|
||||
};
|
||||
};
|
||||
|
||||
|
@ -111,16 +120,29 @@ export default function swaggerRoutes<T extends AnonymousRouter, R extends Route
|
|||
allRouters: R[]
|
||||
) {
|
||||
router.get('/swagger.json', async (ctx, next) => {
|
||||
// Use `as` here since we'll check typing with integration tests
|
||||
const additionalSwagger = load(
|
||||
await readFile('static/yaml/additional-swagger.yaml', { encoding: 'utf-8' })
|
||||
) as OpenAPIV3.Document;
|
||||
|
||||
const routes = allRouters.flatMap<RouteObject>((router) =>
|
||||
router.stack.flatMap<RouteObject>(({ path, stack, methods }) =>
|
||||
router.stack.flatMap<RouteObject>(({ path: routerPath, stack, methods }) =>
|
||||
methods
|
||||
// There is no need to show the HEAD method.
|
||||
.filter((method) => method !== 'HEAD')
|
||||
.map((method) => ({
|
||||
path: `/api${path}`,
|
||||
method: method.toLowerCase() as OpenAPIV3.HttpMethods,
|
||||
operation: buildOperation(stack, path),
|
||||
}))
|
||||
.map((method) => {
|
||||
const path = `/api${routerPath}`;
|
||||
const httpMethod = method.toLowerCase() as OpenAPIV3.HttpMethods;
|
||||
|
||||
const additionalPathItem = additionalSwagger.paths[path] ?? {};
|
||||
const additionalResponses = additionalPathItem[httpMethod]?.responses;
|
||||
|
||||
return {
|
||||
path,
|
||||
method: httpMethod,
|
||||
operation: buildOperation(stack, routerPath, additionalResponses),
|
||||
};
|
||||
})
|
||||
)
|
||||
);
|
||||
|
||||
|
|
60
packages/core/static/yaml/additional-swagger.yaml
Normal file
60
packages/core/static/yaml/additional-swagger.yaml
Normal file
|
@ -0,0 +1,60 @@
|
|||
# The structure of `paths` SHOULD follow OpenAPI 3.0 Specification.
|
||||
# See https://swagger.io/docs/specification/paths-and-operations/
|
||||
paths:
|
||||
/api/applications/:id:
|
||||
delete:
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
/api/connectors/:id/test:
|
||||
delete:
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
/api/me/password:
|
||||
patch:
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
/api/resources/:id:
|
||||
delete:
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
/api/session/sign-in/passwordless/sms/send-passcode:
|
||||
post:
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
/api/session/sign-in/passwordless/email/send-passcode:
|
||||
post:
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
/api/session/register/passwordless/sms/send-passcode:
|
||||
post:
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
/api/session/register/passwordless/email/send-passcode:
|
||||
post:
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
/api/status:
|
||||
get:
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
/api/users/:userId:
|
||||
delete:
|
||||
responses:
|
||||
'204':
|
||||
description: No Content
|
||||
/api/.well-known/sign-in-exp:
|
||||
get:
|
||||
responses:
|
||||
'200':
|
||||
description: OK
|
||||
'304':
|
||||
description: No Modified
|
348
pnpm-lock.yaml
generated
348
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load diff
Loading…
Add table
Reference in a new issue