feat: ERD for components and schemas

This commit is contained in:
Patrick Balsiger
2025-10-31 15:49:53 +01:00
parent cccc152edc
commit 16ef2059cd
4 changed files with 426 additions and 12 deletions

102
server.js
View File

@@ -151,6 +151,100 @@ function buildGraphFromPaths(pathsObject, spec) {
return { nodes, links, endpoints, operations };
}
function buildERDGraph(spec) {
const components = spec.components || {};
const schemas = components.schemas || {};
const nodes = [];
const links = [];
const nodeMap = new Map();
// Create nodes for each schema
for (const [schemaName, schema] of Object.entries(schemas)) {
const resolved = resolveSchema(spec, schema) || {};
const properties = resolved.properties || {};
const requiredSet = new Set(Array.isArray(resolved.required) ? resolved.required : []);
const fields = [];
for (const [propName, propSchema] of Object.entries(properties)) {
const resolvedProp = resolveSchema(spec, propSchema) || {};
const isRequired = requiredSet.has(propName);
const propType = resolvedProp.type || 'object';
let typeInfo = propType;
// Handle $ref in properties
if (propSchema.$ref) {
const refPath = propSchema.$ref.replace('#/components/schemas/', '');
typeInfo = refPath;
} else if (resolvedProp.$ref) {
const refPath = resolvedProp.$ref.replace('#/components/schemas/', '');
typeInfo = refPath;
} else if (resolvedProp.items && resolvedProp.items.$ref) {
const refPath = resolvedProp.items.$ref.replace('#/components/schemas/', '');
typeInfo = `[${refPath}]`;
} else if (resolvedProp.items && resolvedProp.items.type) {
typeInfo = `[${resolvedProp.items.type}]`;
} else if (Array.isArray(resolvedProp.enum)) {
typeInfo = `enum(${resolvedProp.enum.length})`;
}
fields.push({
name: propName,
type: typeInfo,
required: isRequired
});
}
nodes.push({
id: schemaName,
name: schemaName,
fields: fields,
description: resolved.description || ''
});
nodeMap.set(schemaName, nodes[nodes.length - 1]);
}
// Create links based on $ref relationships
for (const node of nodes) {
for (const field of node.fields) {
// Check if field type references another schema
const typeStr = String(field.type);
if (typeStr && !typeStr.startsWith('[') && !typeStr.includes('enum') && nodeMap.has(typeStr)) {
// Check if link already exists (avoid duplicates)
const linkExists = links.some(l =>
(l.source === node.id && l.target === typeStr) ||
(l.target === node.id && l.source === typeStr)
);
if (!linkExists) {
links.push({
source: node.id,
target: typeStr,
field: field.name
});
}
}
// Handle array types like [SchemaName]
const arrayMatch = typeStr.match(/^\[(.+)\]$/);
if (arrayMatch && nodeMap.has(arrayMatch[1])) {
const targetSchema = arrayMatch[1];
const linkExists = links.some(l =>
(l.source === node.id && l.target === targetSchema) ||
(l.target === node.id && l.source === targetSchema)
);
if (!linkExists) {
links.push({
source: node.id,
target: targetSchema,
field: field.name,
relationship: 'array'
});
}
}
}
}
return { nodes, links };
}
app.post('/api/parse', (req, res) => {
try {
const { yaml: yamlText } = req.body || {};
@@ -164,8 +258,14 @@ app.post('/api/parse', (req, res) => {
const pathsObject = spec.paths || {};
const graph = buildGraphFromPaths(pathsObject, spec);
const erdGraph = buildERDGraph(spec);
const servers = Array.isArray(spec.servers) ? spec.servers.map(s => s && s.url).filter(Boolean) : [];
res.json({ graph, info: { title: spec.info && spec.info.title, version: spec.info && spec.info.version }, servers });
res.json({
graph,
erdGraph,
info: { title: spec.info && spec.info.title, version: spec.info && spec.info.version },
servers
});
} catch (err) {
res.status(400).json({ error: err.message || 'Failed to parse YAML' });
}