import type { OpenApiSpec, ResourceConfig, FieldConfig } from "../types"; import { extractFields, clearValidationErrors, getValidationErrors } from "./field-config"; import { extractRelationships } from "./relationship-config"; function detectPagination(pathObj: any): { limitParam: string; offsetParam: string; defaultLimit: number } | null { const params = pathObj?.get?.parameters ?? []; const limit = params.find((p: any) => p.in === "query" && p.name === "limit"); const offset = params.find((p: any) => p.in === "query" && p.name === "offset"); if (limit && offset) { return { limitParam: "limit", offsetParam: "offset", defaultLimit: limit.schema.default, }; } return null; } function hasOperation(pathObj: any, method: string): boolean { return !!pathObj?.[method]; } function sortFields(fields: FieldConfig[]): FieldConfig[] { return [...fields].sort((a, b) => { const orderDiff = a.order - b.order; if (orderDiff !== 0) return orderDiff; return a.name.localeCompare(b.name); }); } function formatDisplayName(name: string): string { return name.split(/[-_]/).map((p) => p.charAt(0).toUpperCase() + p.slice(1)).join(" "); } const SSE_RECEIVED_FIELD: FieldConfig = { name: "_received_at", label: "Received", description: "Timestamp when the event was received", type: "string", format: "date-time", order: 0, hidden: {}, filterable: false, sortable: true, readOnly: true, required: false, isArray: false, }; function getSegments(path: string): string[] { return path.split("/").filter(Boolean); } function getResponseSchemaRef(pathObj: any): string | undefined { const response = pathObj?.get?.responses?.["200"] ?? pathObj?.get?.responses?.["201"]; const content = response?.content; if (!content) return; for (const mediaType of Object.values(content) as any[]) { if (mediaType?.schema?.$ref) return mediaType.schema.$ref; if (mediaType?.schema?.items?.$ref) return mediaType.schema.items.$ref; if (mediaType?.schema?.properties?.items?.items?.$ref) return mediaType.schema.properties.items.items.$ref; } } function resolveRef(ref: string): string { return ref.split("/").pop()!; } export function buildResourceConfigs(spec: OpenApiSpec): ResourceConfig[] { clearValidationErrors(); const schemas = spec.components?.schemas ?? {}; const paths = spec.paths ?? {}; const configs: ResourceConfig[] = []; const nameMap = new Map(); const sortedPaths = Object.keys(paths).sort( (a, b) => getSegments(a).length - getSegments(b).length ); for (const path of sortedPaths) { const segments = getSegments(path); const pathObj = paths[path]; const lastSeg = segments[segments.length - 1]; const isItemPath = /^\{.*\}$/.test(lastSeg); const paramIdx = segments.findIndex( (s, i) => i < segments.length - 1 && /^\{.*\}$/.test(s) ); if (isItemPath) { const parentName = segments[segments.length - 2]; const parent = nameMap.get(parentName); if (!parent) continue; if (hasOperation(pathObj, "get")) parent.operations.get = true; if (hasOperation(pathObj, "put") || hasOperation(pathObj, "patch")) parent.operations.update = true; if (hasOperation(pathObj, "delete")) parent.operations.delete = true; if (hasOperation(pathObj, "patch") && !hasOperation(pathObj, "put")) parent.updateMethod = "patch"; continue; } if (paramIdx >= 0) { const resourceName = lastSeg; const parentName = segments[paramIdx - 1]; const pathParamName = segments[paramIdx].replace(/[{}]/g, ""); const responseRef = getResponseSchemaRef(pathObj); const schemaName = responseRef ? resolveRef(responseRef) : undefined; const schema = schemaName ? schemas[schemaName] : undefined; const fields = schema ? extractFields(schemaName!, schema, schemas) : []; const hasSSE = pathObj?.get?.["x-sse"] === true; const resource: ResourceConfig = { name: resourceName, schemaName: schemaName ?? resourceName, displayName: formatDisplayName(resourceName), path, primaryKey: schema?.["x-primary-key"] ?? "_received_at", displayFormat: schema?.["x-display-format"] ?? `{${resourceName}}`, listColumns: schema?.["x-list-columns"] ?? [], fields: hasSSE ? [SSE_RECEIVED_FIELD, ...fields.map((f) => ({ ...f, readOnly: true }))] : fields, orderedFields: [], operations: hasSSE ? { list: true, get: false, create: false, update: false, delete: false } : { list: hasOperation(pathObj, "get"), get: false, create: false, update: false, delete: false }, updateMethod: "put", pagination: hasSSE ? null : detectPagination(pathObj), relationships: [], streaming: hasSSE || undefined, parent: { resource: parentName, pathParam: pathParamName }, }; resource.orderedFields = sortFields(resource.fields); if (hasSSE) { resource.listColumns = ["_received_at", ...resource.listColumns]; resource.primaryKey = "_received_at"; } const parent = nameMap.get(parentName); if (parent) { parent.subResources = parent.subResources ?? []; parent.subResources.push(resourceName); } nameMap.set(resourceName, resource); configs.push(resource); continue; } const resourceName = lastSeg; const responseRef = getResponseSchemaRef(pathObj); const schemaName = responseRef ? resolveRef(responseRef) : undefined; const schema = schemaName ? schemas[schemaName] : undefined; const fields = schema ? extractFields(schemaName!, schema, schemas) : []; const relationships = schema ? extractRelationships(schema, schemas) : []; const hasSSE = pathObj?.get?.["x-sse"] === true; const resource: ResourceConfig = { name: resourceName, schemaName: schemaName ?? resourceName, displayName: formatDisplayName(resourceName), path, primaryKey: schema?.["x-primary-key"] ?? "id", displayFormat: schema?.["x-display-format"] ?? `{${resourceName}}`, listColumns: schema?.["x-list-columns"] ?? [], fields: hasSSE ? [SSE_RECEIVED_FIELD, ...fields.map((f) => ({ ...f, readOnly: true }))] : fields, orderedFields: [], operations: hasSSE ? { list: true, get: false, create: false, update: false, delete: false } : { list: hasOperation(pathObj, "get"), get: false, create: hasOperation(pathObj, "post"), update: false, delete: false }, updateMethod: "put", pagination: hasSSE ? null : detectPagination(pathObj), relationships, streaming: hasSSE || undefined, }; resource.orderedFields = sortFields(resource.fields); if (hasSSE) { resource.listColumns = ["_received_at", ...resource.listColumns]; resource.primaryKey = "_received_at"; } nameMap.set(resourceName, resource); configs.push(resource); } const errors = getValidationErrors(); if (errors.length > 0) { throw new Error(errors.join("\n")); } return configs; }