From 37d95e89ebc5bda642bd72268e901197b4ed3140 Mon Sep 17 00:00:00 2001 From: Dan Date: Fri, 31 Jan 2025 14:22:28 +0100 Subject: [PATCH] Filter tags that have no associated endpoints (#629) --- packages/zudoku/src/lib/oas/graphql/index.ts | 31 +-- packages/zudoku/src/vite/plugin-api.ts | 276 +++++++++---------- 2 files changed, 152 insertions(+), 155 deletions(-) diff --git a/packages/zudoku/src/lib/oas/graphql/index.ts b/packages/zudoku/src/lib/oas/graphql/index.ts index b7edba11..19f0c4ba 100644 --- a/packages/zudoku/src/lib/oas/graphql/index.ts +++ b/packages/zudoku/src/lib/oas/graphql/index.ts @@ -87,27 +87,24 @@ const JSONObjectScalar = builder.addScalarType("JSONObject", GraphQLJSONObject); const JSONSchemaScalar = builder.addScalarType("JSONSchema", GraphQLJSONSchema); export const getAllTags = (schema: OpenAPIDocument): TagObject[] => { - const tags = schema.tags ?? []; - - // Extract tags from operations - const operationTags = Object.values(schema.paths ?? {}) - .flatMap((path) => Object.values(path ?? {})) - .flatMap((operation) => - typeof operation === "object" && "tags" in operation - ? (operation.tags ?? []) - : [], - ); - - // Remove duplicates and tags that appear in the schema - const uniqueOperationTags = [...new Set(operationTags)].filter( - (tag) => !tags.some((rootTag) => rootTag.name === tag), + const rootTags = schema.tags ?? []; + const operationTags = new Set( + Object.values(schema.paths ?? {}) + .flatMap((path) => Object.values(path ?? {})) + .flatMap((op) => (op as OperationObject).tags ?? []), ); - return [...tags, ...uniqueOperationTags.map((tag) => ({ name: tag }))]; + + return [ + // Keep root tags that are actually used in operations + ...rootTags.filter((tag) => operationTags.has(tag.name)), + // Add tags found in operations but not defined in root tags + ...[...operationTags] + .filter((tag) => !rootTags.some((rt) => rt.name === tag)) + .map((tag) => ({ name: tag })), + ]; }; const getAllOperations = (paths?: PathsObject) => { - const start = performance.now(); - const operations = Object.entries(paths ?? {}).flatMap(([path, value]) => HttpMethods.flatMap((method) => { if (!value?.[method]) return []; diff --git a/packages/zudoku/src/vite/plugin-api.ts b/packages/zudoku/src/vite/plugin-api.ts index 5485434b..bd1ccadd 100644 --- a/packages/zudoku/src/vite/plugin-api.ts +++ b/packages/zudoku/src/vite/plugin-api.ts @@ -129,158 +129,158 @@ const viteApiPlugin = async ( } }, async load(id) { - if (id === resolvedVirtualModuleId) { - const config = getConfig(); - - if (config.mode === "standalone") { - return [ - "export const configuredApiPlugins = [];", - "export const configuredApiCatalogPlugins = [];", - ].join("\n"); - } + if (id !== resolvedVirtualModuleId) return; - const apiPluginOptions = { - options: { - examplesDefaultLanguage: config.defaults?.examplesLanguage, - }, - }; - - const code = [ - `import config from "virtual:zudoku-config";`, - `import { openApiPlugin } from "zudoku/plugins/openapi";`, - `import { apiCatalogPlugin } from "zudoku/plugins/api-catalog";`, - `const configuredApiPlugins = [];`, - `const configuredApiCatalogPlugins = [];`, - `const apiPluginOptions = ${JSON.stringify(apiPluginOptions)};`, - ]; - - if (config.apis) { - const apis = Array.isArray(config.apis) ? config.apis : [config.apis]; - const apiMetadata: ApiCatalogItem[] = []; - const versionMaps: Record> = {}; - - for (const apiConfig of apis) { - if (apiConfig.type === "file" && apiConfig.navigationId) { - const schemas = processedSchemas[apiConfig.navigationId]; - if (!schemas?.length) continue; - const latestSchema = schemas[0]?.schema; - if (!latestSchema?.info) continue; - - apiMetadata.push({ - path: apiConfig.navigationId, - label: latestSchema.info.title, - description: latestSchema.info.description ?? "", - categories: apiConfig.categories ?? [], - }); - - const versionMap = Object.fromEntries( - schemas.map((processed) => [ - processed.version, - processed.inputPath, - ]), - ); - - if (Object.keys(versionMap).length > 0) { - versionMaps[apiConfig.navigationId] = versionMap; - } - } - } + const config = getConfig(); - // Generate API plugin code - for (const apiConfig of apis) { - if (apiConfig.type === "file") { - if ( - !apiConfig.navigationId || - !versionMaps[apiConfig.navigationId] - ) { - continue; - } + if (config.mode === "standalone") { + return [ + "export const configuredApiPlugins = [];", + "export const configuredApiCatalogPlugins = [];", + ].join("\n"); + } + + const apiPluginOptions = { + options: { + examplesDefaultLanguage: config.defaults?.examplesLanguage, + }, + }; + + const code = [ + `import config from "virtual:zudoku-config";`, + `import { openApiPlugin } from "zudoku/plugins/openapi";`, + `import { apiCatalogPlugin } from "zudoku/plugins/api-catalog";`, + `const configuredApiPlugins = [];`, + `const configuredApiCatalogPlugins = [];`, + `const apiPluginOptions = ${JSON.stringify(apiPluginOptions)};`, + ]; + + if (config.apis) { + const apis = Array.isArray(config.apis) ? config.apis : [config.apis]; + const apiMetadata: ApiCatalogItem[] = []; + const versionMaps: Record> = {}; + + for (const apiConfig of apis) { + if (apiConfig.type === "file" && apiConfig.navigationId) { + const schemas = processedSchemas[apiConfig.navigationId]; + if (!schemas?.length) continue; + const latestSchema = schemas[0]?.schema; + if (!latestSchema?.info) continue; + + apiMetadata.push({ + path: apiConfig.navigationId, + label: latestSchema.info.title, + description: latestSchema.info.description ?? "", + categories: apiConfig.categories ?? [], + }); + + const versionMap = Object.fromEntries( + schemas.map((processed) => [ + processed.version, + processed.inputPath, + ]), + ); - const schemas = processedSchemas[apiConfig.navigationId]; - if (!schemas?.length) continue; - - const tags = schemas - .flatMap((schema) => getAllTags(schema.schema)) - .map(({ name }) => name) - .filter((name, index, array) => array.indexOf(name) === index); - - code.push( - "configuredApiPlugins.push(openApiPlugin({", - ` ...apiPluginOptions,`, - ` type: "file",`, - ` input: ${JSON.stringify(versionMaps[apiConfig.navigationId])},`, - ` navigationId: ${JSON.stringify(apiConfig.navigationId)},`, - ` tagPages: ${JSON.stringify(tags)},`, - ` schemaImports: {`, - ...Array.from(schemaMap.entries()).map( - ([key, schemaPath]) => - ` "${key}": () => import("${schemaPath.replace(/\\/g, "/")}"),`, - ), - ` },`, - "}));", - ); - } else { - code.push( - `configuredApiPlugins.push(openApiPlugin(${JSON.stringify({ ...apiConfig, ...apiPluginOptions })}));`, - ); + if (Object.keys(versionMap).length > 0) { + versionMaps[apiConfig.navigationId] = versionMap; } } + } + + // Generate API plugin code + for (const apiConfig of apis) { + if (apiConfig.type === "file") { + if ( + !apiConfig.navigationId || + !versionMaps[apiConfig.navigationId] + ) { + continue; + } - if (config.catalogs) { - const catalogs = Array.isArray(config.catalogs) - ? config.catalogs - : [config.catalogs]; - - const categories = apis - .flatMap((api) => api.categories ?? []) - .reduce((acc, catalog) => { - if (!acc.has(catalog.label)) { - acc.set(catalog.label ?? "", new Set(catalog.tags)); - } - for (const tag of catalog.tags) { - acc.get(catalog.label ?? "")?.add(tag); - } - return acc; - }, new Map>()); - - const categoryList = Array.from(categories.entries()).map( - ([label, tags]) => ({ - label, - tags: Array.from(tags), - }), + const schemas = processedSchemas[apiConfig.navigationId]; + if (!schemas?.length) continue; + + const tags = schemas + .flatMap((schema) => getAllTags(schema.schema)) + .map(({ name }) => name) + .filter((name, index, array) => array.indexOf(name) === index); + + code.push( + "configuredApiPlugins.push(openApiPlugin({", + ` ...apiPluginOptions,`, + ` type: "file",`, + ` input: ${JSON.stringify(versionMaps[apiConfig.navigationId])},`, + ` navigationId: ${JSON.stringify(apiConfig.navigationId)},`, + ` tagPages: ${JSON.stringify(tags)},`, + ` schemaImports: {`, + ...Array.from(schemaMap.entries()).map( + ([key, schemaPath]) => + ` "${key}": () => import("${schemaPath.replace(/\\/g, "/")}"),`, + ), + ` },`, + "}));", + ); + } else { + code.push( + `configuredApiPlugins.push(openApiPlugin(${JSON.stringify({ ...apiConfig, ...apiPluginOptions })}));`, ); + } + } + + if (config.catalogs) { + const catalogs = Array.isArray(config.catalogs) + ? config.catalogs + : [config.catalogs]; - for (let i = 0; i < catalogs.length; i++) { - const catalog = catalogs[i]; - if (!catalog) { - continue; + const categories = apis + .flatMap((api) => api.categories ?? []) + .reduce((acc, catalog) => { + if (!acc.has(catalog.label)) { + acc.set(catalog.label ?? "", new Set(catalog.tags)); } - const apiCatalogConfig: ApiCatalogPluginOptions = { - ...catalog, - items: apiMetadata, - label: catalog.label, - categories: categoryList, - filterCatalogItems: catalog.filterItems, - }; - - code.push( - `configuredApiCatalogPlugins.push(apiCatalogPlugin({`, - ` ...${JSON.stringify(apiCatalogConfig, null, 2)},`, - ` filterCatalogItems: Array.isArray(config.catalogs)`, - ` ? config.catalogs[${i}].filterItems`, - ` : config.catalogs.filterItems,`, - `}));`, - ); + for (const tag of catalog.tags) { + acc.get(catalog.label ?? "")?.add(tag); + } + return acc; + }, new Map>()); + + const categoryList = Array.from(categories.entries()).map( + ([label, tags]) => ({ + label, + tags: Array.from(tags), + }), + ); + + for (let i = 0; i < catalogs.length; i++) { + const catalog = catalogs[i]; + if (!catalog) { + continue; } + const apiCatalogConfig: ApiCatalogPluginOptions = { + ...catalog, + items: apiMetadata, + label: catalog.label, + categories: categoryList, + filterCatalogItems: catalog.filterItems, + }; + + code.push( + `configuredApiCatalogPlugins.push(apiCatalogPlugin({`, + ` ...${JSON.stringify(apiCatalogConfig, null, 2)},`, + ` filterCatalogItems: Array.isArray(config.catalogs)`, + ` ? config.catalogs[${i}].filterItems`, + ` : config.catalogs.filterItems,`, + `}));`, + ); } } + } - code.push( - `export { configuredApiPlugins, configuredApiCatalogPlugins };`, - ); + code.push( + `export { configuredApiPlugins, configuredApiCatalogPlugins };`, + ); - return code.join("\n"); - } + return code.join("\n"); }, }; };