relation fixes

This commit is contained in:
2026-04-02 20:30:21 +05:30
parent a8581325fa
commit 71f7ee83f1
2 changed files with 69 additions and 64 deletions

View File

@@ -40,52 +40,51 @@ function mapOpenApiType(prop: any): FieldType {
function parseSchemaFields(
schema: any,
resourceName: string,
allResources: string[]
schemaToResourceMap: Map<any, string>
): Record<string, ResourceField> {
const fields: Record<string, ResourceField> = {};
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<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(
new URL("/openapi.json", baseUrl).href
);
@@ -104,9 +104,8 @@ export async function loadConfigFromOpenApi(baseUrl: string): Promise<AppConfig>
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<string, any> = {};
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<AppConfig>
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<any, string>();
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 {