relation fixes
This commit is contained in:
@@ -52,7 +52,7 @@ export default function EnhancedTable({
|
|||||||
|
|
||||||
// 2. Relational Link
|
// 2. Relational Link
|
||||||
if (field.relation && value) {
|
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) {
|
if (relationId) {
|
||||||
return (
|
return (
|
||||||
<Link
|
<Link
|
||||||
@@ -60,7 +60,7 @@ export default function EnhancedTable({
|
|||||||
variant="body2"
|
variant="body2"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
onNavigateToResource?.(field.relation!, relationId);
|
onNavigateToResource?.(field.relation!, String(relationId));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{relationId}
|
{relationId}
|
||||||
|
|||||||
@@ -40,52 +40,51 @@ function mapOpenApiType(prop: any): FieldType {
|
|||||||
function parseSchemaFields(
|
function parseSchemaFields(
|
||||||
schema: any,
|
schema: any,
|
||||||
resourceName: string,
|
resourceName: string,
|
||||||
allResources: string[]
|
schemaToResourceMap: Map<any, string>
|
||||||
): Record<string, ResourceField> {
|
): Record<string, ResourceField> {
|
||||||
const fields: Record<string, ResourceField> = {};
|
const fields: Record<string, ResourceField> = {};
|
||||||
const properties = schema.properties || {};
|
const properties = schema.properties || {};
|
||||||
const required = schema.required || [];
|
const required = schema.required || [];
|
||||||
const overrides = configuration[resourceName]?.fields || {};
|
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 type = mapOpenApiType(prop);
|
||||||
const override = overrides[key];
|
const override = overrides[key];
|
||||||
|
|
||||||
console.log("key", key, "type", type, "prop", prop, "override", override);
|
// Explicitly skip 'id' as it's the primary key and handled elsewhere
|
||||||
if (key !== "id" && override?.display !== false) {
|
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,
|
|
||||||
};
|
|
||||||
} else continue;
|
|
||||||
|
|
||||||
// Schema-based Relation Detection
|
fields[key] = {
|
||||||
// If it's an object/string and matches a resource name, it might be a relation
|
type,
|
||||||
const potentialRelation = allResources.find(
|
label:
|
||||||
(res) =>
|
prop.title ||
|
||||||
key === res ||
|
key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " "),
|
||||||
key === `${res}_id` ||
|
required: required.includes(key),
|
||||||
prop.title?.toLowerCase() === res ||
|
options: prop.enum,
|
||||||
prop["x-resource"] === res
|
readOnly:
|
||||||
);
|
prop.readOnly ||
|
||||||
|
key === "created_at" ||
|
||||||
|
key === "updated_at",
|
||||||
|
...override,
|
||||||
|
};
|
||||||
|
|
||||||
if (potentialRelation) {
|
// STRICT RELATION DETECTION
|
||||||
if (type === "string" || (type === "object" && prop.properties?.id)) {
|
// A field is a relation ONLY if its schema object (or items schema)
|
||||||
fields[key].relation = potentialRelation;
|
// 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) {
|
// Check if this schema object is registered as a resource
|
||||||
fields[key].schema = parseSchemaFields(prop, resourceName, allResources);
|
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
|
* Scans paths to identify resources and their basic configuration
|
||||||
*/
|
*/
|
||||||
export async function loadConfigFromOpenApi(baseUrl: string): Promise<AppConfig> {
|
export async function loadConfigFromOpenApi(baseUrl: string): Promise<AppConfig> {
|
||||||
// 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(
|
const api = await SwaggerParser.dereference(
|
||||||
new URL("/openapi.json", baseUrl).href
|
new URL("/openapi.json", baseUrl).href
|
||||||
);
|
);
|
||||||
@@ -104,9 +104,8 @@ export async function loadConfigFromOpenApi(baseUrl: string): Promise<AppConfig>
|
|||||||
const resources: ResourceConfig[] = [];
|
const resources: ResourceConfig[] = [];
|
||||||
const paths = api.paths || {};
|
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<string, any> = {};
|
const resourcePaths: Record<string, any> = {};
|
||||||
|
|
||||||
for (const path of Object.keys(paths)) {
|
for (const path of Object.keys(paths)) {
|
||||||
const base = path.split("/")[1];
|
const base = path.split("/")[1];
|
||||||
if (!base) continue;
|
if (!base) continue;
|
||||||
@@ -115,45 +114,51 @@ export async function loadConfigFromOpenApi(baseUrl: string): Promise<AppConfig>
|
|||||||
const methods = Object.keys(paths[path] || {});
|
const methods = Object.keys(paths[path] || {});
|
||||||
resourcePaths[base].methods.push(...methods);
|
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"]) {
|
if (!path.includes("{") && paths[path]?.get?.responses?.["200"]) {
|
||||||
resourcePaths[base].listPath = path;
|
resourcePaths[base].listPath = path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const allResourceNames = Object.keys(resourcePaths);
|
// 1. Identify which schema objects correspond to which resources
|
||||||
|
const schemaToResourceMap = new Map<any, string>();
|
||||||
// Generate ResourceConfig for each identified base path
|
|
||||||
for (const [name, info] of Object.entries(resourcePaths)) {
|
for (const [name, info] of Object.entries(resourcePaths)) {
|
||||||
const listPath = info.listPath || `/${name}`;
|
const listPath = info.listPath || `/${name}`;
|
||||||
const listOp = paths[listPath]?.get;
|
const listOp = paths[listPath]?.get;
|
||||||
if (!listOp) continue;
|
if (!listOp) continue;
|
||||||
|
|
||||||
// Use common naming conventions or metadata from the spec
|
const responseSchema = listOp.responses?.["200"]?.content?.["application/json"]?.schema;
|
||||||
const label = name.charAt(0).toUpperCase() + name.slice(1, -1); // naive singularization
|
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 pluralLabel = name.charAt(0).toUpperCase() + name.slice(1);
|
||||||
|
|
||||||
// Extract schema from the 200 response of the list endpoint
|
const fields = parseSchemaFields(schema, name, schemaToResourceMap);
|
||||||
let schema: any = null;
|
|
||||||
const responseSchema =
|
|
||||||
listOp.responses?.["200"]?.content?.["application/json"]?.schema;
|
|
||||||
|
|
||||||
if (responseSchema?.type === "array" && responseSchema.items) {
|
resources.push({
|
||||||
schema = responseSchema.items;
|
name,
|
||||||
} else {
|
label: schema.title || label,
|
||||||
schema = responseSchema;
|
pluralLabel: pluralLabel,
|
||||||
}
|
endpoint: listPath,
|
||||||
|
primaryKey: "id", // Strict default, no heuristics
|
||||||
if (schema) {
|
fields,
|
||||||
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),
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return {
|
return {
|
||||||
|
|||||||
Reference in New Issue
Block a user