Skip to content

Commit

Permalink
Filter tags that have no associated endpoints (#629)
Browse files Browse the repository at this point in the history
  • Loading branch information
dan-lee authored Jan 31, 2025
1 parent 800c6c5 commit 37d95e8
Show file tree
Hide file tree
Showing 2 changed files with 152 additions and 155 deletions.
31 changes: 14 additions & 17 deletions packages/zudoku/src/lib/oas/graphql/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 [];
Expand Down
276 changes: 138 additions & 138 deletions packages/zudoku/src/vite/plugin-api.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<string, Record<string, string>> = {};

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<string, Record<string, string>> = {};

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<string, Set<string>>());

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<string, Set<string>>());

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");
},
};
};
Expand Down

0 comments on commit 37d95e8

Please sign in to comment.