feat: render all fields in the forms
This commit is contained in:
111
server.js
111
server.js
@@ -13,7 +13,62 @@ app.get('/api/health', (req, res) => {
|
||||
res.json({ status: 'ok' });
|
||||
});
|
||||
|
||||
function buildGraphFromPaths(pathsObject) {
|
||||
function resolveLocalRef(spec, ref) {
|
||||
if (typeof ref !== 'string' || !ref.startsWith('#/')) return null;
|
||||
const parts = ref.slice(2).split('/').map(p => p.replace(/~1/g, '/').replace(/~0/g, '~'));
|
||||
let cur = spec;
|
||||
for (const p of parts) {
|
||||
if (cur && Object.prototype.hasOwnProperty.call(cur, p)) {
|
||||
cur = cur[p];
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
return cur || null;
|
||||
}
|
||||
|
||||
function resolveSchema(spec, schema, depth = 0) {
|
||||
if (!schema || typeof schema !== 'object' || depth > 20) return schema || null;
|
||||
if (schema.$ref) {
|
||||
const target = resolveLocalRef(spec, schema.$ref);
|
||||
if (!target) return schema;
|
||||
return resolveSchema(spec, target, depth + 1);
|
||||
}
|
||||
// Handle allOf by merging shallowly (best-effort)
|
||||
if (Array.isArray(schema.allOf)) {
|
||||
return schema.allOf.reduce((acc, s) => ({ ...acc, ...resolveSchema(spec, s, depth + 1) }), {});
|
||||
}
|
||||
return schema;
|
||||
}
|
||||
|
||||
function extractBodyFields(spec, rootSchema) {
|
||||
const schema = resolveSchema(spec, rootSchema) || {};
|
||||
const fields = [];
|
||||
|
||||
function walk(obj, pathPrefix = '', parentRequired = []) {
|
||||
const resolved = resolveSchema(spec, obj) || {};
|
||||
if (resolved.type === 'object' || (resolved.properties && typeof resolved.properties === 'object')) {
|
||||
const props = resolved.properties || {};
|
||||
const requiredSet = new Set(Array.isArray(resolved.required) ? resolved.required : parentRequired);
|
||||
for (const [name, propSchema] of Object.entries(props)) {
|
||||
const fullName = pathPrefix ? `${pathPrefix}.${name}` : name;
|
||||
const rProp = resolveSchema(spec, propSchema) || {};
|
||||
const isRequired = requiredSet.has(name);
|
||||
if ((rProp.type === 'object') || rProp.properties || rProp.$ref) {
|
||||
walk(rProp, fullName, Array.from(requiredSet));
|
||||
} else {
|
||||
const enumValues = Array.isArray(rProp.enum) ? rProp.enum : null;
|
||||
fields.push({ name: fullName, type: rProp.type || 'string', required: !!isRequired, description: rProp.description || '', enum: enumValues });
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
walk(schema, '', []);
|
||||
return fields;
|
||||
}
|
||||
|
||||
function buildGraphFromPaths(pathsObject, spec) {
|
||||
const httpMethods = new Set(['get','put','post','delete','options','head','patch','trace']);
|
||||
const nodeIdSet = new Set();
|
||||
const linkKeySet = new Set();
|
||||
@@ -37,9 +92,25 @@ function buildGraphFromPaths(pathsObject) {
|
||||
const summary = op.summary || '';
|
||||
const description = op.description || '';
|
||||
const opParams = [].concat(pathItem.parameters || [], op.parameters || []).filter(Boolean);
|
||||
const parameters = opParams.map(p => ({ name: p.name, in: p.in, required: !!p.required, schema: p.schema || null }));
|
||||
const hasJsonBody = !!(op.requestBody && op.requestBody.content && op.requestBody.content['application/json']);
|
||||
operations.push({ method: lower.toUpperCase(), path: pathKey, summary, description, parameters, hasJsonBody });
|
||||
const parameters = opParams.map(p => {
|
||||
const base = { name: p.name, in: p.in, required: !!p.required };
|
||||
const sch = p.schema ? resolveSchema(spec, p.schema) : null;
|
||||
return { ...base, schema: sch || null };
|
||||
});
|
||||
const jsonContent = op.requestBody && op.requestBody.content && op.requestBody.content['application/json'];
|
||||
const formContent = op.requestBody && op.requestBody.content && op.requestBody.content['application/x-www-form-urlencoded'];
|
||||
const bodyContent = jsonContent || formContent;
|
||||
const hasBody = !!bodyContent;
|
||||
const bodyContentType = jsonContent ? 'application/json' : (formContent ? 'application/x-www-form-urlencoded' : null);
|
||||
let bodyFields = [];
|
||||
if (bodyContent && bodyContent.schema) {
|
||||
try {
|
||||
bodyFields = extractBodyFields(spec, bodyContent.schema);
|
||||
} catch (_) {
|
||||
bodyFields = [];
|
||||
}
|
||||
}
|
||||
operations.push({ method: lower.toUpperCase(), path: pathKey, summary, description, parameters, hasJsonBody: hasBody, bodyFields, bodyContentType });
|
||||
endpoints.push({ method: lower.toUpperCase(), path: pathKey });
|
||||
}
|
||||
}
|
||||
@@ -92,7 +163,7 @@ app.post('/api/parse', (req, res) => {
|
||||
}
|
||||
|
||||
const pathsObject = spec.paths || {};
|
||||
const graph = buildGraphFromPaths(pathsObject);
|
||||
const graph = buildGraphFromPaths(pathsObject, 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 });
|
||||
} catch (err) {
|
||||
@@ -101,10 +172,10 @@ app.post('/api/parse', (req, res) => {
|
||||
});
|
||||
|
||||
// Simple proxy endpoint to call target server
|
||||
// Body: { baseUrl, method, path, headers, query, body }
|
||||
// Body: { baseUrl, method, path, headers, query, body, contentType }
|
||||
app.post('/api/proxy', async (req, res) => {
|
||||
try {
|
||||
const { baseUrl, method, path: relPath, headers = {}, query = {}, body } = req.body || {};
|
||||
const { baseUrl, method, path: relPath, headers = {}, query = {}, body, contentType } = req.body || {};
|
||||
if (!baseUrl || !method || !relPath) {
|
||||
return res.status(400).json({ error: 'Missing baseUrl, method, or path' });
|
||||
}
|
||||
@@ -114,15 +185,33 @@ app.post('/api/proxy', async (req, res) => {
|
||||
}
|
||||
const fetchOpts = { method: method.toUpperCase(), headers: { ...headers } };
|
||||
if (body !== undefined && body !== null && method.toUpperCase() !== 'GET') {
|
||||
const contentTypeHeader = contentType || fetchOpts.headers['content-type'] || fetchOpts.headers['Content-Type'] || 'application/json';
|
||||
if (!fetchOpts.headers['content-type'] && !fetchOpts.headers['Content-Type']) {
|
||||
fetchOpts.headers['Content-Type'] = 'application/json';
|
||||
fetchOpts.headers['Content-Type'] = contentTypeHeader;
|
||||
}
|
||||
if (contentTypeHeader === 'application/x-www-form-urlencoded') {
|
||||
// Flatten body object into URLSearchParams
|
||||
const params = new URLSearchParams();
|
||||
const flatten = (obj, prefix = '') => {
|
||||
for (const [k, v] of Object.entries(obj || {})) {
|
||||
const key = prefix ? `${prefix}.${k}` : k;
|
||||
if (v !== null && v !== undefined && typeof v !== 'object') {
|
||||
params.append(key, String(v));
|
||||
} else if (typeof v === 'object') {
|
||||
flatten(v, key);
|
||||
}
|
||||
}
|
||||
};
|
||||
flatten(body);
|
||||
fetchOpts.body = params.toString();
|
||||
} else {
|
||||
fetchOpts.body = typeof body === 'string' ? body : JSON.stringify(body);
|
||||
}
|
||||
fetchOpts.body = typeof body === 'string' ? body : JSON.stringify(body);
|
||||
}
|
||||
const upstream = await fetch(url.toString(), fetchOpts);
|
||||
const contentType = upstream.headers.get('content-type') || '';
|
||||
const upstreamContentType = upstream.headers.get('content-type') || '';
|
||||
res.status(upstream.status);
|
||||
if (contentType.includes('application/json')) {
|
||||
if (upstreamContentType.includes('application/json')) {
|
||||
const data = await upstream.json().catch(() => null);
|
||||
res.json({ status: upstream.status, headers: Object.fromEntries(upstream.headers), data });
|
||||
} else {
|
||||
|
||||
Reference in New Issue
Block a user