@mintlify/validation
Version:
Validates mint.json files
295 lines (294 loc) • 13.8 kB
JavaScript
/**
* Converts a SchemaGraphData back to an OpenAPI specification.
* Optionally filters to a specific operation if path and method are provided.
*
* This is the inverse of openApiToSchemaGraph - it reconstructs the original
* OpenAPI document from the graph representation.
*/
export function schemaGraphToOpenApi({ schemaGraphData, targetPath, targetMethod, }) {
const { spec, documentId } = schemaGraphData;
const { uuidObjectHashMap, hashedNodeMap, refUuidMap } = spec;
// Build reverse map: UUID -> $ref path
const uuidRefMap = new Map();
for (const [refPath, uuid] of Object.entries(refUuidMap)) {
uuidRefMap.set(uuid, refPath);
}
// Track which component refs are actually used
const usedComponents = new Set();
// Helper to resolve a node by UUID
const resolveNode = (uuid) => {
const hash = uuidObjectHashMap[uuid];
if (!hash)
return undefined;
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return hashedNodeMap[hash];
};
// Track which security schemes are used
const usedSecuritySchemes = new Set();
// Helper to transform security requirements from internal format to OpenAPI format
const transformSecurityRequirement = (securityReq) => {
const transformed = {};
for (const [key, value] of Object.entries(securityReq)) {
// Track this security scheme as used
usedSecuritySchemes.add(key);
if (typeof value === 'object' && !Array.isArray(value) && 'scopes' in value) {
// Extract just the scopes array from internal format
// Create a new array to avoid YAML anchor references
transformed[key] = value.scopes ? [...value.scopes] : [];
}
else if (Array.isArray(value)) {
// Already in correct format - create a new array to avoid shared references
transformed[key] = [...value];
}
}
return transformed;
};
// Helper to convert UUID references back to $ref or inline objects
const convertValue = (value, depth = 0) => {
// Prevent infinite recursion - return empty object as fallback
if (depth > 50)
return {};
if (value == null)
return value;
// If it's a UUID string, try to resolve it
if (typeof value === 'string') {
const refPath = uuidRefMap.get(value);
// If this UUID corresponds to a component ref, use $ref
if (refPath && refPath.startsWith('#/components/')) {
usedComponents.add(refPath);
return { $ref: refPath };
}
// Otherwise try to resolve and inline it
const resolved = resolveNode(value);
if (resolved && typeof resolved === 'object') {
return convertValue(resolved, depth + 1);
}
return value;
}
// Handle arrays
if (Array.isArray(value)) {
return value.map((item) => convertValue(item, depth + 1));
}
// Handle objects
if (typeof value === 'object') {
// Special handling for $ref objects (OpenAPI 3.1 allows sibling properties)
if ('$ref' in value) {
const refValue = value.$ref;
// Convert the $ref value (which may be a UUID) to the actual ref path
let convertedRef;
if (typeof refValue === 'string') {
const refPath = uuidRefMap.get(refValue);
if (refPath && refPath.startsWith('#/components/')) {
usedComponents.add(refPath);
convertedRef = refPath;
}
else {
convertedRef = refValue;
}
}
else {
convertedRef = convertValue(refValue, depth + 1);
}
// If there are only $ref property, return just the $ref object
if (Object.keys(value).length === 1) {
return { $ref: convertedRef };
}
// OpenAPI 3.1 allows sibling properties with $ref
// Process all properties, with special handling for $ref
const result = { $ref: convertedRef };
for (const [key, val] of Object.entries(value)) {
if (key !== '$ref') {
result[key] = convertValue(val, depth + 1);
}
}
return result;
}
const result = {};
for (const [key, val] of Object.entries(value)) {
result[key] = convertValue(val, depth + 1);
}
return result;
}
return value;
};
// Get the document root
const docObj = resolveNode(documentId);
if (!docObj) {
throw new Error('Could not resolve document root');
}
// Start building the OpenAPI document
const openApiDoc = {
openapi: '3.0.0', // Default, will be overwritten if present in document
info: {
title: 'API Documentation',
version: '1.0.0',
},
};
// Copy over document-level properties
if (docObj.openapi)
openApiDoc.openapi = docObj.openapi;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (docObj.info) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
openApiDoc.info = convertValue(docObj.info);
}
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (docObj.servers) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
openApiDoc.servers = convertValue(docObj.servers);
}
// Security requirements need special handling - they should be resolved inline, not as $refs
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (docObj.security && Array.isArray(docObj.security)) {
const securityArray = [];
for (const securityUuid of docObj.security) {
if (typeof securityUuid === 'string') {
const securityReq = resolveNode(securityUuid);
if (securityReq) {
securityArray.push(transformSecurityRequirement(securityReq));
}
}
}
// Always set security, even if empty - security: [] has specific meaning (public endpoint)
openApiDoc.security = securityArray;
}
if (docObj.tags) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
openApiDoc.tags = convertValue(docObj.tags);
}
if (docObj.externalDocs) {
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
openApiDoc.externalDocs = convertValue(docObj.externalDocs);
}
// Build paths
openApiDoc.paths = {};
const pathUuids = docObj.paths;
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
if (pathUuids && Array.isArray(pathUuids)) {
for (const pathUuid of pathUuids) {
const pathObj = resolveNode(pathUuid);
if (!pathObj || !('path' in pathObj))
continue;
const pathKey = pathObj.path;
// If filtering by path, skip non-matching paths
if (targetPath && pathKey !== targetPath)
continue;
const pathItem = {};
// Copy path-level properties (with type guard)
if ('summary' in pathObj && pathObj.summary)
pathItem.summary = pathObj.summary;
if ('description' in pathObj && pathObj.description)
pathItem.description = pathObj.description;
if ('servers' in pathObj && pathObj.servers)
pathItem.servers = convertValue(pathObj.servers);
if ('parameters' in pathObj && pathObj.parameters)
pathItem.parameters = convertValue(pathObj.parameters);
// Process operations - they're stored directly on the path object as method properties
const httpMethods = ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'];
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const pathObjRecord = pathObj;
for (const method of httpMethods) {
const operationUuid = pathObjRecord[method];
if (!operationUuid || typeof operationUuid !== 'string')
continue;
// If filtering by method, skip non-matching methods
if (targetMethod && method.toLowerCase() !== targetMethod.toLowerCase())
continue;
const operation = resolveNode(operationUuid);
if (!operation)
continue;
const operationObj = {};
// Copy operation properties
if (operation.tags)
operationObj.tags = convertValue(operation.tags);
if (operation.summary)
operationObj.summary = operation.summary;
if (operation.description)
operationObj.description = operation.description;
if (operation.operationId)
operationObj.operationId = operation.operationId;
if (operation.parameters)
operationObj.parameters = convertValue(operation.parameters);
if (operation.requestBody)
operationObj.requestBody = convertValue(operation.requestBody);
if (operation.responses)
operationObj.responses = convertValue(operation.responses);
if (operation.callbacks)
operationObj.callbacks = convertValue(operation.callbacks);
if (operation.deprecated !== undefined)
operationObj.deprecated = operation.deprecated;
// Security requirements need special handling at operation level too
if (operation.security && Array.isArray(operation.security)) {
const securityArray = [];
for (const securityUuid of operation.security) {
if (typeof securityUuid === 'string') {
const securityReq = resolveNode(securityUuid);
if (securityReq) {
securityArray.push(transformSecurityRequirement(securityReq));
}
}
}
// Always set security, even if empty - security: [] has specific meaning (public endpoint)
operationObj.security = securityArray;
}
if (operation.servers)
operationObj.servers = convertValue(operation.servers);
if (operation.externalDocs)
operationObj.externalDocs = convertValue(operation.externalDocs);
pathItem[method] = operationObj;
}
// Only add the path if it has content (at least one operation)
const hasOperations = Object.keys(pathItem).some((key) => ['get', 'put', 'post', 'delete', 'options', 'head', 'patch', 'trace'].includes(key));
if (hasOperations) {
openApiDoc.paths[pathKey] = pathItem;
}
}
}
// Build components section with only used components and security schemes
if (usedComponents.size > 0 || usedSecuritySchemes.size > 0) {
openApiDoc.components = {};
// Cast to allow dynamic indexing
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
const components = openApiDoc.components;
// Add used schema/parameter/response components (referenced via $ref)
for (const refPath of usedComponents) {
// Parse the ref path: #/components/{type}/{name}
const match = refPath.match(/^#\/components\/([^/]+)\/(.+)$/);
if (!match)
continue;
const [, componentType, componentName] = match;
if (!componentType || !componentName)
continue;
const uuid = refUuidMap[refPath];
if (!uuid)
continue;
const component = resolveNode(uuid);
if (!component)
continue;
// Initialize the component type object if it doesn't exist
if (!components[componentType]) {
components[componentType] = {};
}
// Add the component (recursively converting any nested UUIDs)
components[componentType][componentName] = convertValue(component);
}
// Add used security schemes (referenced by name in security requirements)
for (const schemeName of usedSecuritySchemes) {
const refPath = `#/components/securitySchemes/${schemeName}`;
const uuid = refUuidMap[refPath];
if (!uuid)
continue;
const scheme = resolveNode(uuid);
if (!scheme)
continue;
// Initialize security schemes object if it doesn't exist
if (!components.securitySchemes) {
components.securitySchemes = {};
}
// Add the security scheme
components.securitySchemes[schemeName] = convertValue(scheme);
}
}
// eslint-disable-next-line @typescript-eslint/consistent-type-assertions
return openApiDoc;
}