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, resourceName: string, schemaToResourceMap: Map, configuration: Record = {} ): Record { const fields: Record = {}; const properties = schema.properties || {}; const required = schema.required || []; const overrides = configuration[resourceName]?.fields || {}; for (const [key, prop] of Object.entries(properties) as [string, any]) { const type = mapOpenApiType(prop); const override = overrides[key]; // Explicitly skip 'id' as it's the primary key and handled elsewhere if (key === "id" || override?.display === false) continue; fields[key] = { type, label: prop.title || key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " "), required: required.includes(key), options: prop.enum, readOnly: prop.readOnly || key === "created_at" || key === "updated_at", ...override, }; // STRICT RELATION DETECTION // A field is a relation ONLY if its schema object (or items schema) // exactly matches a schema that is defined as a resource. let targetSchema = prop; if (type === "array" && prop.items) { targetSchema = prop.items; } // Check if this schema object is registered as a resource const relation = schemaToResourceMap.get(targetSchema); if (relation) { fields[key].relation = relation; } // Recursively parse nested objects (only if not a relation) if (fields[key].type === "object" && prop.properties && !relation) { fields[key].schema = parseSchemaFields(prop, resourceName, schemaToResourceMap, configuration); } } return fields; } /** * Scans paths to identify resources and their basic configuration */ export async function loadConfigFromOpenApi(baseUrl: string, configuration: Record = {}, profileConfiguration: any = {}): Promise { // Use SwaggerParser to dereference the spec. // Dereferencing preserves object identity for $ref targets. const api = await SwaggerParser.dereference( new URL("/openapi.json", baseUrl).href ); const resources: ResourceConfig[] = []; const paths = api.paths || {}; // Group paths by base resource name 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); // Identify the list endpoint for this resource if (!resourcePaths[base].listPath && !path.includes("{") && paths[path]?.get?.responses?.["200"]) { resourcePaths[base].listPath = path; } } // 1. Identify which schema objects correspond to which resources const schemaToResourceMap = new Map(); for (const [name, info] of Object.entries(resourcePaths)) { const listPath = info.listPath || `/${name}`; const listOp = paths[listPath]?.get; if (!listOp) continue; // @ts-ignore const responseSchema = listOp.responses?.["200"]?.content?.["application/json"]?.schema; let schemaObj = responseSchema; if (responseSchema?.type === "array" && responseSchema.items) { schemaObj = responseSchema.items; } if (schemaObj) { schemaToResourceMap.set(schemaObj, name); resourcePaths[name].schemaObj = schemaObj; } } // 2. Generate ResourceConfig for each identified resource for (const [name, info] of Object.entries(resourcePaths)) { const listPath = info.listPath || `/${name}`; const listOp = paths[listPath]?.get; if (!listOp || !info.schemaObj) continue; const schema = info.schemaObj; const label = name.charAt(0).toUpperCase() + name.slice(1, -1); const pluralLabel = name.charAt(0).toUpperCase() + name.slice(1); const fields = parseSchemaFields(schema, name, schemaToResourceMap, configuration); const resourceOverride = configuration[name] || {}; resources.push({ name, label: schema.title || label, pluralLabel: pluralLabel, endpoint: listPath, primaryKey: "id", // Strict default, no heuristics fields, pagination: resourceOverride.pagination, }); } // @ts-ignore const serverBaseUrl = import.meta.env.VITE_API_BASE_URL || (api.servers?.[0]?.url ?? "") // @ts-ignore const authBaseUrl = import.meta.env.VITE_AUTH_BASE_URL || "" return { baseUrl: serverBaseUrl, authBaseUrl: authBaseUrl, resources, profile: profileConfiguration, }; }