194 lines
7.0 KiB
TypeScript
194 lines
7.0 KiB
TypeScript
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<string, ResourceConfig>();
|
|
|
|
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;
|
|
}
|