import SwaggerParser from "@apidevtools/swagger-parser"; import { AppConfig, ResourceConfig, ResourceField, FieldType } from "../types/config"; /** * Maps OpenAPI property types to our internal FieldType */ function mapOpenApiType(prop: any): FieldType { const type = prop.type; const format = prop.format; if (format === "date-time") return "datetime"; if (format === "date") return "date"; if (prop.enum) return "enum"; if (type === "string" && (prop.description?.toLowerCase().includes("image") || prop.name?.toLowerCase().includes("icon"))) return "image"; switch (type) { case "integer": case "number": return "number"; case "boolean": return "boolean"; case "object": return "object"; case "array": return "array"; default: return "string"; } } /** * Recursively converts OpenAPI schemas to ResourceField map */ function parseSchemaFields(schema: any): Record { const fields: Record = {}; const properties = schema.properties || {}; const required = schema.required || []; for (const [key, prop] of Object.entries(properties) as any) { fields[key] = { type: mapOpenApiType(prop), label: prop.title || key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " "), required: required.includes(key), options: prop.enum, readOnly: prop.readOnly || key === "id" || key === "created_at" || key === "updated_at", }; if (fields[key].type === "object" && prop.properties) { fields[key].schema = parseSchemaFields(prop); } } return fields; } /** * Scans paths to identify resources and their basic configuration */ export async function loadConfigFromOpenApi(baseUrl: string): Promise { // 1. Parse and dereference the spec (handles all $ref) const api = await SwaggerParser.dereference( new URL("/openapi.json", baseUrl).href ); const resources: ResourceConfig[] = []; const paths = api.paths || {}; // Group paths by base resource name (e.g., /expenses, /expenses/{id} -> expenses) const resourcePaths: Record = {}; for (const path of Object.keys(paths)) { const base = path.split("/")[1]; if (!base) continue; if (!resourcePaths[base]) resourcePaths[base] = { path, methods: [] }; const methods = Object.keys(paths[path] || {}); resourcePaths[base].methods.push(...methods); // We prefer the plural GET path for schema extraction if (!path.includes("{") && paths[path]?.get?.responses?.["200"]) { resourcePaths[base].listPath = path; } } // Generate ResourceConfig for each identified base path for (const [name, info] of Object.entries(resourcePaths)) { const listPath = info.listPath || `/${name}`; const listOp = paths[listPath]?.get; if (!listOp) continue; // Use common naming conventions or metadata from the spec const label = name.charAt(0).toUpperCase() + name.slice(1, -1); // naive singularization const pluralLabel = name.charAt(0).toUpperCase() + name.slice(1); // Extract schema from the 200 response of the list endpoint let schema: any = null; const responseSchema = listOp.responses?.["200"]?.content?.["application/json"]?.schema; if (responseSchema?.type === "array" && responseSchema.items) { schema = responseSchema.items; } else { schema = responseSchema; } if (schema) { resources.push({ name, label: schema.title || label, pluralLabel: pluralLabel, endpoint: listPath, primaryKey: "id", // assume 'id' as default or look for 'required' + 'unique' fields: parseSchemaFields(schema), }); } } return { baseUrl: import.meta.env.VITE_API_BASE_URL || (api.servers?.[0]?.url ?? ""), authBaseUrl: import.meta.env.VITE_AUTH_BASE_URL || "", resources, }; }