0
Fork 0
mirror of https://github.com/logto-io/logto.git synced 2025-03-31 22:51:25 -05:00

refactor: remove cloud only operations when needed

This commit is contained in:
Gao Sun 2024-03-20 23:29:45 +08:00 committed by Darcy Ye
parent 77b67fbd04
commit 45a7ee17aa
No known key found for this signature in database
GPG key ID: B46F4C07EDEFC610
3 changed files with 49 additions and 56 deletions

View file

@ -191,7 +191,7 @@
},
"/api/configs/jwt-customizer/test": {
"post": {
"tags": ["cloud-only"],
"tags": ["Cloud only"],
"summary": "Test JWT customizer",
"description": "Test the JWT customizer script with the given sample context and sample token payload.",
"requestBody": {

View file

@ -7,13 +7,14 @@ import deepmerge from 'deepmerge';
import { findUp } from 'find-up';
import type { IMiddleware } from 'koa-router';
import type Router from 'koa-router';
import type { OpenAPIV3 } from 'openapi-types';
import { type OpenAPIV3 } from 'openapi-types';
import { EnvSet } from '#src/env-set/index.js';
import { isKoaAuthMiddleware } from '#src/middleware/koa-auth/index.js';
import type { WithGuardConfig } from '#src/middleware/koa-guard.js';
import { isGuardMiddleware } from '#src/middleware/koa-guard.js';
import { isPaginationMiddleware } from '#src/middleware/koa-pagination.js';
import { type DeepPartial } from '#src/test-utils/tenant.js';
import assertThat from '#src/utils/assert-that.js';
import { consoleLog } from '#src/utils/console.js';
import { translationSchemas, zodTypeToSwagger } from '#src/utils/zod.js';
@ -24,7 +25,7 @@ import {
buildTag,
findSupplementFiles,
normalizePath,
pruneSupplementPaths,
removeCloudOnlyOperations,
validateSupplement,
validateSwaggerDocument,
} from './utils/general.js';
@ -193,15 +194,14 @@ export default function swaggerRoutes<T extends AnonymousRouter, R extends Route
assertThat(routesDirectory, new Error('Cannot find routes directory.'));
const supplementPaths = await findSupplementFiles(routesDirectory);
const rawSupplementDocuments = await Promise.all(
supplementPaths.map(
// eslint-disable-next-line no-restricted-syntax
async (path) => JSON.parse(await fs.readFile(path, 'utf8')) as Record<string, unknown>
const supplementDocuments = await Promise.all(
supplementPaths.map(async (path) =>
removeCloudOnlyOperations(
// eslint-disable-next-line no-restricted-syntax -- trust the type here as we'll validate it later
JSON.parse(await fs.readFile(path, 'utf8')) as DeepPartial<OpenAPIV3.Document>
)
)
);
const supplementDocuments = rawSupplementDocuments.map((document) =>
pruneSupplementPaths(document)
);
const baseDocument: OpenAPIV3.Document = {
openapi: '3.0.1',
@ -232,8 +232,11 @@ export default function swaggerRoutes<T extends AnonymousRouter, R extends Route
tags: [...tags].map((tag) => ({ name: tag })),
};
const data = supplementDocuments.reduce(
(document, supplement) => deepmerge(document, supplement, { arrayMerge: mergeParameters }),
const data = supplementDocuments.reduce<OpenAPIV3.Document>(
(document, supplement) =>
deepmerge<OpenAPIV3.Document, DeepPartial<OpenAPIV3.Document>>(document, supplement, {
arrayMerge: mergeParameters,
}),
baseDocument
);

View file

@ -7,10 +7,14 @@ import { OpenAPIV3 } from 'openapi-types';
import { z } from 'zod';
import { EnvSet } from '#src/env-set/index.js';
import { type DeepPartial } from '#src/test-utils/tenant.js';
import { consoleLog } from '#src/utils/console.js';
const capitalize = (value: string) => value.charAt(0).toUpperCase() + value.slice(1);
/** The tag name used in the supplement document to indicate that the operation is cloud only. */
const cloudOnlyTag = 'Cloud only';
/**
* Get the root component name from the given absolute path.
* @example '/organizations/:id' -> 'organizations'
@ -106,9 +110,14 @@ const validateSupplementPaths = (
);
}
if (isKeyInObject(operations[method], 'tags')) {
const operation = operations[method];
if (
isKeyInObject(operation, 'tags') &&
Array.isArray(operation.tags) &&
(operation.tags.length > 1 || operation.tags[0] !== cloudOnlyTag)
) {
throw new TypeError(
`Cannot use \`tags\` in supplement document on path \`${path}\` and operation \`${method}\`. Define tags in the document root instead.`
`Cannot use \`tags\` in supplement document on path \`${path}\` and operation \`${method}\` except for \`${cloudOnlyTag}\`. Define tags in the document root instead.`
);
}
}
@ -125,7 +134,7 @@ const validateSupplementPaths = (
*/
export const validateSupplement = (
original: OpenAPIV3.Document,
supplement: Record<string, unknown>
supplement: DeepPartial<OpenAPIV3.Document>
) => {
if (supplement.tags) {
const supplementTags = z.array(z.object({ name: z.string() })).parse(supplement.tags);
@ -204,51 +213,32 @@ export const validateSwaggerDocument = (document: OpenAPIV3.Document) => {
};
/**
* Some path are only available in the cloud version, so we need to prune them out in the OSS.
* **CAUTION**: This function mutates the input document.
*
* Remove operations (path + method) that are tagged with `Cloud only` if the application is not
* running in the cloud. This will prevent the swagger validation from failing in the OSS
* environment.
*/
export const pruneSupplementPaths = (supplement: Record<string, unknown>) => {
if (EnvSet.values.isCloud) {
return supplement;
export const removeCloudOnlyOperations = (
document: DeepPartial<OpenAPIV3.Document>
): DeepPartial<OpenAPIV3.Document> => {
if (EnvSet.values.isCloud || !document.paths) {
return document;
}
// eslint-disable-next-line no-restricted-syntax
const supplementName = ((supplement.tags ?? []) as OpenAPIV3.TagObject[])[0]?.name;
for (const [path, pathItem] of Object.entries(document.paths)) {
for (const method of Object.values(OpenAPIV3.HttpMethods)) {
if (pathItem?.[method]?.tags?.includes(cloudOnlyTag)) {
// eslint-disable-next-line @silverhand/fp/no-delete, @typescript-eslint/no-dynamic-delete -- intended
delete pathItem[method];
}
}
if (!supplementName) {
return supplement;
if (Object.keys(pathItem ?? {}).length === 0) {
// eslint-disable-next-line @silverhand/fp/no-delete, @typescript-eslint/no-dynamic-delete -- intended
delete document.paths[path];
}
}
if (!supplement.paths) {
return supplement;
}
// eslint-disable-next-line no-restricted-syntax
const supplementPaths = supplement.paths as OpenAPIV3.PathsObject;
if (Object.entries(supplement.paths).length === 0) {
return supplement;
}
// eslint-disable-next-line no-restricted-syntax
const newPaths = Object.fromEntries(
Object.entries(supplementPaths)
.map(([path, pathBody]) => [
path,
Object.fromEntries(
// eslint-disable-next-line no-restricted-syntax
Object.entries(pathBody as OpenAPIV3.PathItemObject).filter(
([_, operationBody]) =>
// eslint-disable-next-line no-restricted-syntax
!((operationBody as OpenAPIV3.OperationObject).tags ?? []).includes('cloud-only')
)
),
])
// eslint-disable-next-line no-restricted-syntax
.filter(([_, pathBody]) => Object.entries(pathBody as OpenAPIV3.PathItemObject).length > 0)
) as OpenAPIV3.PathsObject;
return {
...supplement,
paths: newPaths,
};
return document;
};