From 709f4cf946a399317a68f39d976d5a23817b2e7c Mon Sep 17 00:00:00 2001 From: Nate McGrady Date: Thu, 27 Nov 2025 01:14:45 -0600 Subject: [PATCH 1/2] add schema inference feature to trigger config --- components/workflow/config/trigger-config.tsx | 32 +++- components/workflow/utils/json-parser.ts | 144 ++++++++++++++++++ 2 files changed, 173 insertions(+), 3 deletions(-) create mode 100644 components/workflow/utils/json-parser.ts diff --git a/components/workflow/config/trigger-config.tsx b/components/workflow/config/trigger-config.tsx index ca8e6740..64c5a5b6 100644 --- a/components/workflow/config/trigger-config.tsx +++ b/components/workflow/config/trigger-config.tsx @@ -14,6 +14,7 @@ import { SelectValue, } from "@/components/ui/select"; import { TimezoneSelect } from "@/components/ui/timezone-select"; +import { inferSchemaFromJSON } from "../utils/json-parser"; import { SchemaBuilder, type SchemaField } from "./schema-builder"; type TriggerConfigProps = { @@ -40,6 +41,12 @@ export function TriggerConfig({ } }; + const handleInferSchema = (mockRequest: string) => { + const inferredSchema = inferSchemaFromJSON(mockRequest); + onUpdateConfig("webhookSchema", JSON.stringify(inferredSchema)); + toast.success("Schema inferred from mock payload"); + }; + return ( <>
@@ -137,9 +144,28 @@ export function TriggerConfig({ value={(config?.webhookMockRequest as string) || ""} />
-

- Enter a sample JSON payload to test the webhook trigger. -

+
+

+ Enter a sample JSON payload to test the webhook trigger. +

+ +
)} diff --git a/components/workflow/utils/json-parser.ts b/components/workflow/utils/json-parser.ts new file mode 100644 index 00000000..a6a9e4de --- /dev/null +++ b/components/workflow/utils/json-parser.ts @@ -0,0 +1,144 @@ +import { nanoid } from "nanoid"; +import type { SchemaField } from "@/components/workflow/config/schema-builder"; + +type ValidFieldType = SchemaField["type"]; +type ValidItemType = NonNullable; + +type ArrayStructure = { + type: "array"; + itemType: ValidFieldType | FieldsStructure; +}; + +type FieldsStructure = { + [key: string]: ValidFieldType | FieldsStructure | ArrayStructure; +}; + +const detectType = (value: unknown): ValidFieldType => { + if (value === null) { + return "string"; + } + if (Array.isArray(value)) { + return "array"; + } + const t = typeof value; + if (t === "object") { + return "object"; + } + if (t === "string") { + return "string"; + } + if (t === "number") { + return "number"; + } + if (t === "boolean") { + return "boolean"; + } + return "string"; +}; + +const processArray = (arr: unknown[]): ArrayStructure => { + if (arr.length === 0) { + return { type: "array", itemType: "string" }; + } + + const firstElement = arr[0]; + if ( + typeof firstElement === "object" && + firstElement !== null && + !Array.isArray(firstElement) + ) { + return { type: "array", itemType: extractFields(firstElement) }; + } + + // For primitive arrays, detect the type of the first element + return { type: "array", itemType: detectType(firstElement) }; +}; + +const extractFields = (obj: unknown): FieldsStructure => { + if (obj === null || typeof obj !== "object" || Array.isArray(obj)) { + return {}; + } + + const result: FieldsStructure = {}; + + for (const key in obj as Record) { + if (Object.hasOwn(obj, key)) { + const value = (obj as Record)[key]; + const valueType = detectType(value); + + if (valueType === "object") { + result[key] = extractFields(value); + } else if (valueType === "array") { + result[key] = processArray(value as unknown[]); + } else { + result[key] = valueType; + } + } + } + return result; +}; + +const createPrimitiveField = ( + key: string, + type: ValidFieldType +): SchemaField => ({ + id: nanoid(), + name: key, + type, +}); + +const createArrayField = ( + key: string, + arrayStructure: ArrayStructure +): SchemaField => { + const field: SchemaField = { + id: nanoid(), + name: key, + type: "array", + }; + + if (typeof arrayStructure.itemType === "string") { + field.itemType = arrayStructure.itemType as ValidItemType; + } else if (typeof arrayStructure.itemType === "object") { + field.itemType = "object"; + field.fields = convertToSchemaFields(arrayStructure.itemType); + } + + return field; +}; + +const createObjectField = ( + key: string, + fieldsStructure: FieldsStructure +): SchemaField => ({ + id: nanoid(), + name: key, + type: "object", + fields: convertToSchemaFields(fieldsStructure), +}); + +const convertToSchemaFields = ( + fieldsStructure: FieldsStructure +): SchemaField[] => { + const result: SchemaField[] = []; + + for (const [key, value] of Object.entries(fieldsStructure)) { + if (typeof value === "string") { + result.push(createPrimitiveField(key, value)); + } else if (typeof value === "object" && value !== null) { + if ("type" in value && value.type === "array") { + result.push(createArrayField(key, value as ArrayStructure)); + } else { + result.push(createObjectField(key, value as FieldsStructure)); + } + } + } + + return result; +}; + +export const inferSchemaFromJSON = (jsonString: string): SchemaField[] => { + const parsed = JSON.parse(jsonString); + const fieldsStructure = extractFields(parsed); + return convertToSchemaFields(fieldsStructure); +}; From 7f79fd2946f456f7b9370c006933f223614cb96e Mon Sep 17 00:00:00 2001 From: Nate McGrady Date: Mon, 1 Dec 2025 23:26:24 -0600 Subject: [PATCH 2/2] feat(workflow): add schema inference --- components/workflow/config/trigger-config.tsx | 56 +++++++++++-------- components/workflow/utils/json-parser.ts | 19 ++++++- 2 files changed, 48 insertions(+), 27 deletions(-) diff --git a/components/workflow/config/trigger-config.tsx b/components/workflow/config/trigger-config.tsx index 64c5a5b6..c1df4b7f 100644 --- a/components/workflow/config/trigger-config.tsx +++ b/components/workflow/config/trigger-config.tsx @@ -1,9 +1,15 @@ "use client"; -import { Clock, Copy, Play, Webhook } from "lucide-react"; +import { Clock, Copy, MoreVertical, Play, Webhook } from "lucide-react"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { CodeEditor } from "@/components/ui/code-editor"; +import { + DropdownMenu, + DropdownMenuContent, + DropdownMenuItem, + DropdownMenuTrigger, +} from "@/components/ui/dropdown-menu"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { @@ -125,7 +131,28 @@ export function TriggerConfig({

- +
+ + + + + + + { + handleInferSchema(config.webhookMockRequest as string); + }} + > + Infer Schema + + + +
-
-

- Enter a sample JSON payload to test the webhook trigger. -

- -
+

+ Enter a sample JSON payload to test the webhook trigger. +

)} diff --git a/components/workflow/utils/json-parser.ts b/components/workflow/utils/json-parser.ts index a6a9e4de..1b3a1f27 100644 --- a/components/workflow/utils/json-parser.ts +++ b/components/workflow/utils/json-parser.ts @@ -42,6 +42,11 @@ const processArray = (arr: unknown[]): ArrayStructure => { } const firstElement = arr[0]; + + if (Array.isArray(firstElement)) { + return { type: "array", itemType: "object" }; + } + if ( typeof firstElement === "object" && firstElement !== null && @@ -50,8 +55,11 @@ const processArray = (arr: unknown[]): ArrayStructure => { return { type: "array", itemType: extractFields(firstElement) }; } - // For primitive arrays, detect the type of the first element - return { type: "array", itemType: detectType(firstElement) }; + const detectedType = detectType(firstElement); + if (detectedType === "array") { + return { type: "array", itemType: "object" }; + } + return { type: "array", itemType: detectedType }; }; const extractFields = (obj: unknown): FieldsStructure => { @@ -98,7 +106,12 @@ const createArrayField = ( }; if (typeof arrayStructure.itemType === "string") { - field.itemType = arrayStructure.itemType as ValidItemType; + if (arrayStructure.itemType === "array") { + field.itemType = "object"; + field.fields = []; + } else { + field.itemType = arrayStructure.itemType as ValidItemType; + } } else if (typeof arrayStructure.itemType === "object") { field.itemType = "object"; field.fields = convertToSchemaFields(arrayStructure.itemType);