245 lines
8.0 KiB
JavaScript
245 lines
8.0 KiB
JavaScript
/**
|
|
* Fetches and filters the OpenAPI specs for fumadocs.
|
|
* Outputs two separate spec files:
|
|
* - public/openapi.json (v3.1 — latest, clean operationIds)
|
|
* - public/openapi-v3.json (v3.0)
|
|
*
|
|
* Run: bun run scripts/fetch-openapi.mjs
|
|
*/
|
|
|
|
import { writeFileSync } from 'node:fs';
|
|
import { fileURLToPath } from 'node:url';
|
|
import { dirname, join } from 'node:path';
|
|
|
|
const OPENAPI_V3_URL = process.env.OPENAPI_SPEC_URL || 'https://backend.composio.dev/api/v3/openapi.json';
|
|
const OPENAPI_V31_URL = process.env.OPENAPI_V31_SPEC_URL || 'https://backend.composio.dev/api/v3.1/openapi.json';
|
|
|
|
// Tags to ignore (internal/admin)
|
|
const IGNORED_TAGS = [
|
|
'CLI',
|
|
'Admin',
|
|
'Profiling',
|
|
'User',
|
|
'x-internal',
|
|
];
|
|
|
|
async function fetchSpec(url) {
|
|
console.log(`Fetching OpenAPI spec from ${url}...`);
|
|
const response = await fetch(url);
|
|
if (!response.ok) {
|
|
throw new Error(`Failed to fetch ${url}: ${response.status}`);
|
|
}
|
|
return response.json();
|
|
}
|
|
|
|
/**
|
|
* Filter paths: remove ignored/internal tags, keep first tag only.
|
|
*/
|
|
function filterPaths(paths) {
|
|
const filteredPaths = {};
|
|
let removedCount = 0;
|
|
|
|
for (const [path, methods] of Object.entries(paths)) {
|
|
const filteredMethods = {};
|
|
|
|
for (const [method, operation] of Object.entries(methods)) {
|
|
const tags = operation.tags || [];
|
|
const hasValidTag = tags.some(tag => !IGNORED_TAGS.includes(tag));
|
|
|
|
if (!hasValidTag && tags.length > 0) {
|
|
removedCount++;
|
|
continue;
|
|
}
|
|
|
|
if (operation['x-internal'] === true || tags.includes('x-internal')) {
|
|
removedCount++;
|
|
continue;
|
|
}
|
|
|
|
if (tags.length > 1) {
|
|
operation.tags = [tags[0]];
|
|
}
|
|
|
|
filteredMethods[method] = operation;
|
|
}
|
|
|
|
if (Object.keys(filteredMethods).length > 0) {
|
|
filteredPaths[path] = filteredMethods;
|
|
}
|
|
}
|
|
|
|
return { filteredPaths, removedCount };
|
|
}
|
|
|
|
/**
|
|
* Strip version prefixes from operationIds (e.g. getV3_1Tools → getTools).
|
|
*/
|
|
function cleanOperationIds(paths) {
|
|
for (const methods of Object.values(paths)) {
|
|
for (const operation of Object.values(methods)) {
|
|
if (operation.operationId) {
|
|
// Remove V3_1, V3_0, etc. prefixes from operationId
|
|
operation.operationId = operation.operationId.replace(/V\d+_\d+/g, '');
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Post-process a spec: remove CookieAuth, normalize unions, fix nullable.
|
|
*/
|
|
function postProcessSpec(spec) {
|
|
// Pin the server to production. The published docs must always show the
|
|
// production base URL in their curl examples, regardless of which environment
|
|
// the source spec was fetched from (a staging fetch would otherwise bake a
|
|
// staging server URL into the committed reference).
|
|
spec.servers = [
|
|
{
|
|
url: 'https://backend.composio.dev',
|
|
description: 'PRODUCTION API',
|
|
},
|
|
];
|
|
|
|
// Filter tags list
|
|
if (spec.tags) {
|
|
spec.tags = spec.tags.filter(tag => !IGNORED_TAGS.includes(tag.name));
|
|
}
|
|
|
|
// Remove CookieAuth from security schemes
|
|
if (spec.components?.securitySchemes?.CookieAuth) {
|
|
delete spec.components.securitySchemes.CookieAuth;
|
|
}
|
|
|
|
// Remove CookieAuth from all endpoint security arrays
|
|
for (const methods of Object.values(spec.paths)) {
|
|
for (const operation of Object.values(methods)) {
|
|
if (operation.security) {
|
|
operation.security = operation.security.filter(sec => !('CookieAuth' in sec));
|
|
if (operation.security.length === 0) {
|
|
delete operation.security;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Normalize overly complex anyOf/oneOf schemas
|
|
const mergePropertySchemas = (existing, incoming) => {
|
|
if (!existing) return JSON.parse(JSON.stringify(incoming));
|
|
const merged = JSON.parse(JSON.stringify(existing));
|
|
if (existing.enum && incoming.enum) {
|
|
merged.enum = [...new Set([...existing.enum, ...incoming.enum])];
|
|
}
|
|
if (existing.properties && incoming.properties) {
|
|
merged.properties = { ...existing.properties };
|
|
for (const [key, val] of Object.entries(incoming.properties)) {
|
|
merged.properties[key] = mergePropertySchemas(merged.properties[key], val);
|
|
}
|
|
}
|
|
return merged;
|
|
};
|
|
|
|
const normalizeUnionSchemas = (obj) => {
|
|
if (!obj || typeof obj !== 'object') return;
|
|
for (const unionKey of ['anyOf', 'oneOf']) {
|
|
if (obj[unionKey] && Array.isArray(obj[unionKey]) && obj[unionKey].length > 5) {
|
|
const objectSchemas = obj[unionKey].filter(s => s.type === 'object' && s.properties);
|
|
if (objectSchemas.length > 5 && objectSchemas.length >= obj[unionKey].length * 0.8) {
|
|
const mergedProperties = {};
|
|
const allRequired = new Set();
|
|
for (const schema of objectSchemas) {
|
|
for (const [propName, propSchema] of Object.entries(schema.properties || {})) {
|
|
mergedProperties[propName] = mergePropertySchemas(mergedProperties[propName], propSchema);
|
|
}
|
|
if (schema.required) {
|
|
for (const req of schema.required) allRequired.add(req);
|
|
}
|
|
}
|
|
const universallyRequired = [...allRequired].filter(req =>
|
|
objectSchemas.every(s => s.required && s.required.includes(req))
|
|
);
|
|
delete obj[unionKey];
|
|
obj.type = 'object';
|
|
obj.properties = mergedProperties;
|
|
if (universallyRequired.length > 0) obj.required = universallyRequired;
|
|
obj.additionalProperties = true;
|
|
}
|
|
}
|
|
}
|
|
for (const val of Object.values(obj)) {
|
|
if (Array.isArray(val)) val.forEach(item => normalizeUnionSchemas(item));
|
|
else normalizeUnionSchemas(val);
|
|
}
|
|
};
|
|
normalizeUnionSchemas(spec);
|
|
|
|
// Fix invalid OpenAPI 3.0: "nullable: true" without "type"
|
|
const fixNullableWithoutType = (obj, parentKey = '') => {
|
|
if (!obj || typeof obj !== 'object') return;
|
|
if (obj.nullable === true && !obj.type && !obj.$ref && !obj.oneOf && !obj.anyOf && !obj.allOf) {
|
|
if (parentKey === 'additionalProperties') {
|
|
delete obj.nullable;
|
|
} else if (obj.example && typeof obj.example === 'object' && !Array.isArray(obj.example)) {
|
|
obj.type = 'object';
|
|
} else if (obj.example && Array.isArray(obj.example)) {
|
|
obj.type = 'array';
|
|
} else {
|
|
obj.type = 'object';
|
|
}
|
|
}
|
|
for (const [key, val] of Object.entries(obj)) {
|
|
if (Array.isArray(val)) val.forEach(item => fixNullableWithoutType(item, key));
|
|
else fixNullableWithoutType(val, key);
|
|
}
|
|
};
|
|
fixNullableWithoutType(spec);
|
|
}
|
|
|
|
async function fetchAndFilterSpec() {
|
|
// Fetch both specs in parallel
|
|
const [v3Raw, v31Raw] = await Promise.all([
|
|
fetchSpec(OPENAPI_V3_URL),
|
|
fetchSpec(OPENAPI_V31_URL),
|
|
]);
|
|
|
|
// --- v3.1 spec (latest, default) ---
|
|
const v31Spec = JSON.parse(JSON.stringify(v31Raw));
|
|
const v31Filtered = filterPaths(v31Spec.paths);
|
|
v31Spec.paths = v31Filtered.filteredPaths;
|
|
// Clean operationIds: getV3_1Tools → getTools (so URLs are clean)
|
|
cleanOperationIds(v31Spec.paths);
|
|
// Annotate all operations with version
|
|
for (const methods of Object.values(v31Spec.paths)) {
|
|
for (const op of Object.values(methods)) {
|
|
op['x-api-version'] = '3.1';
|
|
}
|
|
}
|
|
postProcessSpec(v31Spec);
|
|
console.log(`v3.1: ${Object.keys(v31Spec.paths).length} paths`);
|
|
|
|
// --- v3.0 spec ---
|
|
const v3Spec = JSON.parse(JSON.stringify(v3Raw));
|
|
const v3Filtered = filterPaths(v3Spec.paths);
|
|
v3Spec.paths = v3Filtered.filteredPaths;
|
|
cleanOperationIds(v3Spec.paths);
|
|
for (const methods of Object.values(v3Spec.paths)) {
|
|
for (const op of Object.values(methods)) {
|
|
op['x-api-version'] = '3.0';
|
|
}
|
|
}
|
|
postProcessSpec(v3Spec);
|
|
console.log(`v3.0: ${Object.keys(v3Spec.paths).length} paths`);
|
|
|
|
// Write both spec files
|
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
|
|
const v31Path = join(__dirname, '../public/openapi.json');
|
|
writeFileSync(v31Path, JSON.stringify(v31Spec, null, 2));
|
|
console.log(`Written v3.1 spec to ${v31Path}`);
|
|
|
|
const v3Path = join(__dirname, '../public/openapi-v3.json');
|
|
writeFileSync(v3Path, JSON.stringify(v3Spec, null, 2));
|
|
console.log(`Written v3.0 spec to ${v3Path}`);
|
|
}
|
|
|
|
fetchAndFilterSpec().catch(console.error);
|