118 lines
3.8 KiB
TypeScript
118 lines
3.8 KiB
TypeScript
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): Record<string, ResourceField> {
|
|
const fields: Record<string, ResourceField> = {};
|
|
const properties = schema.properties || {};
|
|
const required = schema.required || [];
|
|
|
|
for (const [key, prop] of Object.entries(properties) as any) {
|
|
fields[key] = {
|
|
type: mapOpenApiType(prop),
|
|
label: prop.title || key.charAt(0).toUpperCase() + key.slice(1).replace(/_/g, " "),
|
|
required: required.includes(key),
|
|
options: prop.enum,
|
|
readOnly: prop.readOnly || key === "id" || key === "created_at" || key === "updated_at",
|
|
};
|
|
|
|
if (fields[key].type === "object" && prop.properties) {
|
|
fields[key].schema = parseSchemaFields(prop);
|
|
}
|
|
}
|
|
|
|
return fields;
|
|
}
|
|
|
|
/**
|
|
* 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)
|
|
const api = await SwaggerParser.dereference(
|
|
new URL("/openapi.json", baseUrl).href
|
|
);
|
|
|
|
const resources: ResourceConfig[] = [];
|
|
const paths = api.paths || {};
|
|
|
|
// Group paths by base resource name (e.g., /expenses, /expenses/{id} -> expenses)
|
|
const resourcePaths: Record<string, any> = {};
|
|
|
|
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);
|
|
|
|
// We prefer the plural GET path for schema extraction
|
|
if (!path.includes("{") && paths[path]?.get?.responses?.["200"]) {
|
|
resourcePaths[base].listPath = path;
|
|
}
|
|
}
|
|
|
|
// Generate ResourceConfig for each identified base path
|
|
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 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;
|
|
|
|
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),
|
|
});
|
|
}
|
|
}
|
|
|
|
return {
|
|
baseUrl: import.meta.env.VITE_API_BASE_URL || (api.servers?.[0]?.url ?? ""),
|
|
authBaseUrl: import.meta.env.VITE_AUTH_BASE_URL || "",
|
|
resources,
|
|
};
|
|
}
|