From e35d099c35cfc9e86c97353f356356069f38ff01 Mon Sep 17 00:00:00 2001 From: Gao Sun Date: Tue, 12 Sep 2023 14:27:40 +0800 Subject: [PATCH] chore(core): add supplementary openapi json (#4472) * chore(core): support supplementary openapi json * refactor(core): copy json files to build --- .vscode/settings.json | 13 +++++- packages/core/package.json | 2 +- packages/core/src/routes/status.openapi.json | 15 +++++++ packages/core/src/routes/swagger.test.ts | 2 - packages/core/src/routes/swagger.ts | 45 ++++++++++++++++++-- 5 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 packages/core/src/routes/status.openapi.json diff --git a/.vscode/settings.json b/.vscode/settings.json index 131a4bbb9..1c0626205 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -5,7 +5,10 @@ "source.fixAll.stylelint": true } }, - "stylelint.validate": ["css", "scss"], + "stylelint.validate": [ + "css", + "scss" + ], "eslint.workingDirectories": [ { "pattern": "./packages/*", @@ -23,6 +26,14 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": true, }, + "json.schemas": [ + { + "fileMatch": [ + "packages/core/src/routes/*.openapi.json" + ], + "url": "https://raw.githubusercontent.com/OAI/OpenAPI-Specification/main/schemas/v3.0/schema.json" + } + ], "cSpell.words": [ "Alipay", "CIAM", diff --git a/packages/core/package.json b/packages/core/package.json index 20069c9e8..988cba4ac 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -12,7 +12,7 @@ }, "scripts": { "precommit": "lint-staged", - "copyfiles": "copyfiles -u 1 src/**/*.md build", + "copyfiles": "copyfiles -u 1 src/routes/**/*.openapi.json build/", "build": "rm -rf build/ && tsc -p tsconfig.build.json && pnpm run copyfiles", "build:test": "rm -rf build/ && tsc -p tsconfig.test.json --sourcemap", "lint": "eslint --ext .ts src", diff --git a/packages/core/src/routes/status.openapi.json b/packages/core/src/routes/status.openapi.json new file mode 100644 index 000000000..9fcd46bb1 --- /dev/null +++ b/packages/core/src/routes/status.openapi.json @@ -0,0 +1,15 @@ +{ + "paths": { + "/api/status": { + "get": { + "summary": "Health check", + "description": "The traditional health check API. No authentication needed.\n\n> **Note**\n> Even if 204 is returned, it does not guarantee all the APIs are working properly since they may depend on additional resources or external services.", + "responses": { + "204": { + "description": "The Logto core service is healthy." + } + } + } + } + } +} diff --git a/packages/core/src/routes/swagger.test.ts b/packages/core/src/routes/swagger.test.ts index c973c7cc6..3d3fe566c 100644 --- a/packages/core/src/routes/swagger.test.ts +++ b/packages/core/src/routes/swagger.test.ts @@ -51,8 +51,6 @@ describe('GET /swagger.json', () => { it('should contain the specific paths', async () => { const response = await mockSwaggerRequest.get('/swagger.json'); - // eslint-disable-next-line @typescript-eslint/no-unsafe-argument - expect(Object.entries(response.body.paths)).toHaveLength(2); expect(response.body.paths).toMatchObject({ /* eslint-disable @typescript-eslint/no-unsafe-assignment */ '/api/mock': { diff --git a/packages/core/src/routes/swagger.ts b/packages/core/src/routes/swagger.ts index ffe99d7a1..45aeabba7 100644 --- a/packages/core/src/routes/swagger.ts +++ b/packages/core/src/routes/swagger.ts @@ -1,5 +1,10 @@ +import fs from 'node:fs/promises'; +import path from 'node:path'; +import { fileURLToPath } from 'node:url'; + import { httpCodeToMessage } from '@logto/core-kit'; import { conditionalArray, deduplicate, toTitle } from '@silverhand/essentials'; +import deepmerge from 'deepmerge'; import type { IMiddleware } from 'koa-router'; import type Router from 'koa-router'; import type { OpenAPIV3 } from 'openapi-types'; @@ -146,6 +151,27 @@ const isManagementApiRouter = ({ stack }: Router) => .filter(({ path }) => !path.includes('.*')) .some(({ stack }) => stack.some((function_) => isKoaAuthMiddleware(function_))); +/** + * Recursively find all supplement files (files end with `.openapi.json`) for the given + * directory. + */ +/* eslint-disable @silverhand/fp/no-mutating-methods, no-await-in-loop */ +const findSupplementFiles = async (directory: string) => { + const result: string[] = []; + + for (const file of await fs.readdir(directory)) { + const stats = await fs.stat(path.join(directory, file)); + if (stats.isDirectory()) { + result.push(...(await findSupplementFiles(path.join(directory, file)))); + } else if (file.endsWith('.openapi.json')) { + result.push(path.join(directory, file)); + } + } + + return result; +}; +/* eslint-enable @silverhand/fp/no-mutating-methods, no-await-in-loop */ + // Keep using `any` to accept various custom context types. // eslint-disable-next-line @typescript-eslint/no-explicit-any export default function swaggerRoutes>( @@ -185,17 +211,30 @@ export default function swaggerRoutes JSON.parse(await fs.readFile(path, 'utf8')) as Record + ) + ); + + const baseDocument: OpenAPIV3.Document = { openapi: '3.0.1', info: { title: 'Logto Core', - version: '0.1.0', + description: 'Management APIs for Logto core service.', + version: 'Cloud', }, paths: Object.fromEntries(pathMap), components: { schemas: translationSchemas }, }; - ctx.body = document; + ctx.body = supplementDocuments.reduce( + (document, supplement) => deepmerge(document, supplement), + baseDocument + ); return next(); });