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 {