diff --git a/src_generic/components/EnhancedTable.tsx b/src_generic/components/EnhancedTable.tsx index 290ae9d..f1a7120 100644 --- a/src_generic/components/EnhancedTable.tsx +++ b/src_generic/components/EnhancedTable.tsx @@ -52,7 +52,7 @@ export default function EnhancedTable({ // 2. Relational Link if (field.relation && value) { - const relationId = typeof value === 'object' ? value.id : value; + const relationId = typeof value === 'object' ? (value.id || value._id || value.pk) : value; if (relationId) { return ( { e.stopPropagation(); - onNavigateToResource?.(field.relation!, relationId); + onNavigateToResource?.(field.relation!, String(relationId)); }} > {relationId} diff --git a/src_generic/utils/openapi_loader.ts b/src_generic/utils/openapi_loader.ts index 6a64606..9572bbc 100644 --- a/src_generic/utils/openapi_loader.ts +++ b/src_generic/utils/openapi_loader.ts @@ -40,52 +40,51 @@ function mapOpenApiType(prop: any): FieldType { function parseSchemaFields( schema: any, resourceName: string, - allResources: string[] + schemaToResourceMap: Map ): 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 any) { + for (const [key, prop] of Object.entries(properties) as [string, any]) { const type = mapOpenApiType(prop); const override = overrides[key]; - console.log("key", key, "type", type, "prop", prop, "override", override); - if (key !== "id" && override?.display !== false) { - 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, - }; - } else continue; + // Explicitly skip 'id' as it's the primary key and handled elsewhere + if (key === "id" || override?.display === false) continue; - // Schema-based Relation Detection - // If it's an object/string and matches a resource name, it might be a relation - const potentialRelation = allResources.find( - (res) => - key === res || - key === `${res}_id` || - prop.title?.toLowerCase() === res || - prop["x-resource"] === res - ); + 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, + }; - if (potentialRelation) { - if (type === "string" || (type === "object" && prop.properties?.id)) { - fields[key].relation = potentialRelation; - } + // 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; } - if (fields[key].type === "object" && prop.properties) { - fields[key].schema = parseSchemaFields(prop, resourceName, allResources); + // 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); } } @@ -96,7 +95,8 @@ function parseSchemaFields( * 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) + // Use SwaggerParser to dereference the spec. + // Dereferencing preserves object identity for $ref targets. const api = await SwaggerParser.dereference( new URL("/openapi.json", baseUrl).href ); @@ -104,9 +104,8 @@ export async function loadConfigFromOpenApi(baseUrl: string): Promise const resources: ResourceConfig[] = []; const paths = api.paths || {}; - // Group paths by base resource name (e.g., /expenses, /expenses/{id} -> expenses) + // Group paths by base resource name const resourcePaths: Record = {}; - for (const path of Object.keys(paths)) { const base = path.split("/")[1]; if (!base) continue; @@ -115,45 +114,51 @@ export async function loadConfigFromOpenApi(baseUrl: string): Promise const methods = Object.keys(paths[path] || {}); resourcePaths[base].methods.push(...methods); - // We prefer the plural GET path for schema extraction + // Identify the list endpoint for this resource if (!path.includes("{") && paths[path]?.get?.responses?.["200"]) { resourcePaths[base].listPath = path; } } - const allResourceNames = Object.keys(resourcePaths); - - // Generate ResourceConfig for each identified base 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; - // Use common naming conventions or metadata from the spec - const label = name.charAt(0).toUpperCase() + name.slice(1, -1); // naive singularization + 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); - // Extract schema from the 200 response of the list endpoint - let schema: any = null; - const responseSchema = - listOp.responses?.["200"]?.content?.["application/json"]?.schema; + const fields = parseSchemaFields(schema, name, schemaToResourceMap); - 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, name, allResourceNames), - }); - } + resources.push({ + name, + label: schema.title || label, + pluralLabel: pluralLabel, + endpoint: listPath, + primaryKey: "id", // Strict default, no heuristics + fields, + }); } return {