Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
32 changes: 29 additions & 3 deletions components/workflow/config/trigger-config.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 = {
Expand All @@ -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 (
<>
<div className="space-y-2">
Expand Down Expand Up @@ -137,9 +144,28 @@ export function TriggerConfig({
value={(config?.webhookMockRequest as string) || ""}
/>
</div>
<p className="text-muted-foreground text-xs">
Enter a sample JSON payload to test the webhook trigger.
</p>
<div className="flex items-center justify-between">
<p className="text-muted-foreground text-xs">
Enter a sample JSON payload to test the webhook trigger.
</p>
<Button
disabled={disabled || !config?.webhookMockRequest}
onClick={() => {
const mockRequest = config?.webhookMockRequest as string;
if (mockRequest) {
try {
handleInferSchema(config?.webhookMockRequest as string);
} catch {
toast.error("Failed to infer schema from mock payload");
}
}
}}
size="sm"
variant="outline"
>
Infer Schema
</Button>
</div>
</div>
</>
)}
Expand Down
144 changes: 144 additions & 0 deletions components/workflow/utils/json-parser.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { nanoid } from "nanoid";
import type { SchemaField } from "@/components/workflow/config/schema-builder";

type ValidFieldType = SchemaField["type"];
type ValidItemType = NonNullable<SchemaField["itemType"]>;

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<string, unknown>) {
if (Object.hasOwn(obj, key)) {
const value = (obj as Record<string, unknown>)[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;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
field.itemType = arrayStructure.itemType as ValidItemType;
// Nested arrays case: itemType is "array"
// Since SchemaField doesn't support itemType: "array",
// represent nested arrays as itemType: "object" with fields
if (arrayStructure.itemType === "array") {
field.itemType = "object";
field.fields = []; // Empty fields for array of arrays
} else {
// Ensure only valid item types are assigned
field.itemType = arrayStructure.itemType as ValidItemType;
}

The schema inference can produce invalid SchemaField objects with itemType: "array" when processing nested arrays, but SchemaField only allows itemType to be "string" | "number" | "boolean" | "object".

View Details

Analysis

Schema inference creates invalid SchemaField with itemType: "array" for nested arrays

What fails: inferSchemaFromJSON() in components/workflow/utils/json-parser.ts produces SchemaField objects with itemType: "array", which violates the type contract defined in schema-builder.tsx (line 20) where itemType only allows "string" | "number" | "boolean" | "object".

How to reproduce:

// Call inferSchemaFromJSON with nested arrays
const schema = inferSchemaFromJSON('{"data": [[1, 2], [3, 4]]}');

// Inspect the result
console.log(schema[0].itemType); // Outputs: "array" (INVALID)

What happens: The function assigns itemType: "array" (a string value) through an unsafe as ValidItemType cast on line 101, bypassing TypeScript's type safety. This causes the schema to violate its type contract, and downstream code like template-autocomplete.tsx (line 55) generates invalid type labels like "array[]" instead of valid types.

Expected behavior: For nested arrays, the schema should use valid itemType values. The fix represents nested arrays as itemType: "object" with empty fields, which maintains type safety while properly indicating that array items are complex structures.

Fix implemented: Added a check in createArrayField() to detect when arrayStructure.itemType === "array" and handle it by setting itemType: "object" with an empty fields array, ensuring all SchemaField objects comply with the type contract.

} 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);
};