diff --git a/src/lib/faker-options.ts b/src/lib/faker-options.ts index 9417b93..b152fe0 100644 --- a/src/lib/faker-options.ts +++ b/src/lib/faker-options.ts @@ -274,13 +274,32 @@ export function fieldsFromSchema(schema: unknown, prefix = ""): SchemaField[] { return fields; } -export function buildSchema(schemaFields: SchemaField[]): Record { - const formattedSchema: Record = {}; +export function buildSchema(schemaFields: SchemaField[]): Record { + const result: Record = {}; + schemaFields.forEach((field) => { - if (field.fieldName.trim()) { - formattedSchema[field.fieldName.trim()] = field.dataType; + const name = field.fieldName.trim(); + if (!name) return; + + const parts = name.split("."); + let current: Record = result; + + for (let i = 0; i < parts.length - 1; i++) { + const part = parts[i]!; + // If the key already exists but is not an object, overwrite it + if ( + typeof current[part] !== "object" || + current[part] === null || + Array.isArray(current[part]) + ) { + current[part] = {}; + } + current = current[part] as Record; } + + const lastPart = parts[parts.length - 1]!; + current[lastPart] = field.dataType; }); - return formattedSchema; + return result; } diff --git a/src/modules/workspace/ui/components/dashboard/schemaBuilder.tsx b/src/modules/workspace/ui/components/dashboard/schemaBuilder.tsx index 3a8264b..e64a0b6 100644 --- a/src/modules/workspace/ui/components/dashboard/schemaBuilder.tsx +++ b/src/modules/workspace/ui/components/dashboard/schemaBuilder.tsx @@ -82,6 +82,9 @@ export function SchemaBuilder({ const [activeTab, setActiveTab] = useState<"schema" | "auth">(initialTab); const [resourceName, setResourceName] = useState(endpoint.name); const [nameError, setNameError] = useState(null); + const [schemaPath, setSchemaPath] = useState(endpoint.path); + const [pathError, setPathError] = useState(null); + const [httpMethod, setHttpMethod] = useState(endpoint.method); const [delayMs, setDelayMs] = useState(endpoint.delayMs); const [failureRate, setFailureRate] = useState(endpoint.failureRate); const [responseCount, setResponseCount] = useState(endpoint.responseCount); @@ -101,6 +104,9 @@ export function SchemaBuilder({ useEffect(() => { setResourceName(endpoint.name); setNameError(null); + setSchemaPath(endpoint.path); + setPathError(null); + setHttpMethod(endpoint.method); setDelayMs(endpoint.delayMs); setFailureRate(endpoint.failureRate); setResponseCount(endpoint.responseCount); @@ -166,11 +172,27 @@ export function SchemaBuilder({ }, [endpoint.responseSchema]); const handleSubmit = () => { + let hasError = false; + if (!resourceName.trim()) { setNameError("Resource name is required."); - return; + hasError = true; + } else { + setNameError(null); } - setNameError(null); + + const trimmedPath = schemaPath.trim(); + if (!trimmedPath) { + setPathError("Path is required."); + hasError = true; + } else if (!trimmedPath.startsWith("/")) { + setPathError("Path must start with /"); + hasError = true; + } else { + setPathError(null); + } + + if (hasError) return; let formattedSchema: unknown; if (editorMode === "raw") { @@ -189,6 +211,8 @@ export function SchemaBuilder({ { id: endpoint.id, name: resourceName, + path: trimmedPath, + method: httpMethod, responseSchema: formattedSchema as Record, delayMs: delayMs, failureRate: failureRate, @@ -266,6 +290,66 @@ export function SchemaBuilder({ )} + {/* Schema path + HTTP method */} +
+ +
+ {/* Method pill-toggle */} +
+ {( + [ + "GET", + "POST", + "PUT", + "PATCH", + "DELETE", + ] as HttpMethod[] + ).map((m) => ( + + ))} +
+ + {/* Path input */} +
+ { + setSchemaPath(e.target.value); + if (pathError) setPathError(null); + }} + aria-invalid={!!pathError} + className="h-11 border-0 bg-muted font-mono text-sm shadow-none focus-visible:ring-2 focus-visible:ring-ring aria-invalid:ring-2 aria-invalid:ring-destructive/50" + /> +
+
+ {pathError && ( +

+ {pathError} +

+ )} +
+ {/* Behavior config */} diff --git a/src/modules/workspace/ui/components/dashboard/schemaPreview.tsx b/src/modules/workspace/ui/components/dashboard/schemaPreview.tsx index 440df00..4f334c3 100644 --- a/src/modules/workspace/ui/components/dashboard/schemaPreview.tsx +++ b/src/modules/workspace/ui/components/dashboard/schemaPreview.tsx @@ -1,7 +1,7 @@ "use client"; import { useCallback, useEffect, useRef, useState } from "react"; -import { Code2, RotateCw, ShieldAlert, LayoutTemplate } from "lucide-react"; +import { Code2, RotateCw, ShieldAlert, LayoutTemplate, Copy, Check } from "lucide-react"; import { Card, CardContent, CardHeader } from "~/components/ui/card"; import { CodeBlock } from "~/components/code-block"; import { Button } from "~/components/ui/button"; @@ -34,12 +34,22 @@ export function SchemaPreview({ endpoint, fetchUrl, bearerToken }: SchemaPreview const [liveData, setLiveData] = useState(null); const [error, setError] = useState(null); const [isLoading, setIsLoading] = useState(false); + const [copied, setCopied] = useState(false); const [progress, setProgress] = useState(0); const animationFrameRef = useRef(null); const requiresAuth = !!endpoint?.authConfig?.requiresAuth; const isLoginEndpoint = !!endpoint?.authConfig?.isLoginEndpoint; + const copySchema = useCallback(() => { + if (!endpoint?.responseSchema) return; + const text = JSON.stringify(endpoint.responseSchema, null, 2); + void navigator.clipboard.writeText(text).then(() => { + setCopied(true); + setTimeout(() => setCopied(false), 2000); + }); + }, [endpoint?.responseSchema]); + const fields = endpoint?.responseSchema && typeof endpoint.responseSchema === "object" ? Object.entries(endpoint.responseSchema as Record) @@ -132,6 +142,19 @@ export function SchemaPreview({ endpoint, fetchUrl, bearerToken }: SchemaPreview Generated schema +