mirror of
https://github.com/Comfy-Org/ComfyUI_frontend.git
synced 2026-04-30 11:11:53 +00:00
Validate node def from /object_info endpoint (#159)
* Validate node def * nit * nit * More tests
This commit is contained in:
@@ -6,7 +6,7 @@
|
|||||||
"scripts": {
|
"scripts": {
|
||||||
"dev": "vite",
|
"dev": "vite",
|
||||||
"build": "npm run typecheck && vite build",
|
"build": "npm run typecheck && vite build",
|
||||||
"deploy": "node scripts/deploy.js",
|
"deploy": "npm run build && node scripts/deploy.js",
|
||||||
"zipdist": "node scripts/zipdist.js",
|
"zipdist": "node scripts/zipdist.js",
|
||||||
"typecheck": "tsc --noEmit",
|
"typecheck": "tsc --noEmit",
|
||||||
"format": "prettier --write 'src/**/*.{js,ts,tsx,vue}'",
|
"format": "prettier --write 'src/**/*.{js,ts,tsx,vue}'",
|
||||||
|
|||||||
@@ -4,6 +4,7 @@ import {
|
|||||||
PendingTaskItem,
|
PendingTaskItem,
|
||||||
RunningTaskItem,
|
RunningTaskItem,
|
||||||
ComfyNodeDef,
|
ComfyNodeDef,
|
||||||
|
validateComfyNodeDef,
|
||||||
} from "@/types/apiTypes";
|
} from "@/types/apiTypes";
|
||||||
|
|
||||||
interface QueuePromptRequestBody {
|
interface QueuePromptRequestBody {
|
||||||
@@ -240,7 +241,17 @@ class ComfyApi extends EventTarget {
|
|||||||
*/
|
*/
|
||||||
async getNodeDefs(): Promise<Record<string, ComfyNodeDef>> {
|
async getNodeDefs(): Promise<Record<string, ComfyNodeDef>> {
|
||||||
const resp = await this.fetchApi("/object_info", { cache: "no-store" });
|
const resp = await this.fetchApi("/object_info", { cache: "no-store" });
|
||||||
return await resp.json();
|
const objectInfoUnsafe = await resp.json();
|
||||||
|
const objectInfo: Record<string, ComfyNodeDef> = {};
|
||||||
|
for (const key in objectInfoUnsafe) {
|
||||||
|
try {
|
||||||
|
objectInfo[key] = validateComfyNodeDef(objectInfoUnsafe[key]);
|
||||||
|
} catch (e) {
|
||||||
|
console.warn("Ignore node definition: ", key);
|
||||||
|
console.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return objectInfo;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { ZodType, z } from "zod";
|
import { ZodType, z } from "zod";
|
||||||
import { zComfyWorkflow } from "./comfyWorkflow";
|
import { zComfyWorkflow } from "./comfyWorkflow";
|
||||||
|
import { fromZodError } from "zod-validation-error";
|
||||||
|
|
||||||
const zNodeId = z.number();
|
const zNodeId = z.number();
|
||||||
const zNodeType = z.string();
|
const zNodeType = z.string();
|
||||||
@@ -124,9 +125,21 @@ export type TaskItem = z.infer<typeof zTaskItem>;
|
|||||||
|
|
||||||
// TODO: validate `/history` `/queue` API endpoint responses.
|
// TODO: validate `/history` `/queue` API endpoint responses.
|
||||||
|
|
||||||
function inputSpec(spec: [ZodType, ZodType]): ZodType {
|
function inputSpec(
|
||||||
|
spec: [ZodType, ZodType],
|
||||||
|
allowUpcast: boolean = true
|
||||||
|
): ZodType {
|
||||||
const [inputType, inputSpec] = spec;
|
const [inputType, inputSpec] = spec;
|
||||||
return z.union([z.tuple([inputType, inputSpec]), z.tuple([inputType])]);
|
// e.g. "INT" => ["INT", {}]
|
||||||
|
const upcastTypes: ZodType[] = allowUpcast
|
||||||
|
? [inputType.transform((type) => [type, {}])]
|
||||||
|
: [];
|
||||||
|
|
||||||
|
return z.union([
|
||||||
|
z.tuple([inputType, inputSpec]),
|
||||||
|
z.tuple([inputType]).transform(([type]) => [type, {}]),
|
||||||
|
...upcastTypes,
|
||||||
|
]);
|
||||||
}
|
}
|
||||||
|
|
||||||
const zIntInputSpec = inputSpec([
|
const zIntInputSpec = inputSpec([
|
||||||
@@ -173,15 +186,18 @@ const zStringInputSpec = inputSpec([
|
|||||||
]);
|
]);
|
||||||
|
|
||||||
// Dropdown Selection.
|
// Dropdown Selection.
|
||||||
const zComboInputSpec = inputSpec([
|
const zComboInputSpec = inputSpec(
|
||||||
z.array(z.any()),
|
[
|
||||||
z.object({
|
z.array(z.any()),
|
||||||
default: z.any().optional(),
|
z.object({
|
||||||
control_after_generate: z.boolean().optional(),
|
default: z.any().optional(),
|
||||||
image_upload: z.boolean().optional(),
|
control_after_generate: z.boolean().optional(),
|
||||||
forceInput: z.boolean().optional(),
|
image_upload: z.boolean().optional(),
|
||||||
}),
|
forceInput: z.boolean().optional(),
|
||||||
]);
|
}),
|
||||||
|
],
|
||||||
|
/* allowUpcast=*/ false
|
||||||
|
);
|
||||||
|
|
||||||
const zCustomInputSpec = inputSpec([
|
const zCustomInputSpec = inputSpec([
|
||||||
z.string(),
|
z.string(),
|
||||||
@@ -210,6 +226,9 @@ const zComfyNodeDef = z.object({
|
|||||||
input: z.object({
|
input: z.object({
|
||||||
required: z.record(zInputSpec).optional(),
|
required: z.record(zInputSpec).optional(),
|
||||||
optional: z.record(zInputSpec).optional(),
|
optional: z.record(zInputSpec).optional(),
|
||||||
|
// Frontend repo is not using it, but some custom nodes are using the
|
||||||
|
// hidden field to pass various values.
|
||||||
|
hidden: z.record(z.any()).optional(),
|
||||||
}),
|
}),
|
||||||
output: zComfyOutputSpec,
|
output: zComfyOutputSpec,
|
||||||
output_is_list: z.array(z.boolean()),
|
output_is_list: z.array(z.boolean()),
|
||||||
@@ -227,4 +246,15 @@ export type ComfyInputSpec = z.infer<typeof zInputSpec>;
|
|||||||
export type ComfyOutputSpec = z.infer<typeof zComfyOutputSpec>;
|
export type ComfyOutputSpec = z.infer<typeof zComfyOutputSpec>;
|
||||||
export type ComfyNodeDef = z.infer<typeof zComfyNodeDef>;
|
export type ComfyNodeDef = z.infer<typeof zComfyNodeDef>;
|
||||||
|
|
||||||
// TODO: validate `/object_info` API endpoint responses.
|
export function validateComfyNodeDef(data: any): ComfyNodeDef {
|
||||||
|
const result = zComfyNodeDef.safeParse(data);
|
||||||
|
if (!result.success) {
|
||||||
|
const zodError = fromZodError(result.error);
|
||||||
|
const error = new Error(
|
||||||
|
`Invalid ComfyNodeDef: ${JSON.stringify(data)}\n${zodError.message}`
|
||||||
|
);
|
||||||
|
error.cause = zodError;
|
||||||
|
throw error;
|
||||||
|
}
|
||||||
|
return result.data;
|
||||||
|
}
|
||||||
|
|||||||
77
tests-ui/tests/apiTypes.test.ts
Normal file
77
tests-ui/tests/apiTypes.test.ts
Normal file
@@ -0,0 +1,77 @@
|
|||||||
|
import { ComfyNodeDef, validateComfyNodeDef } from "@/types/apiTypes";
|
||||||
|
const fs = require("fs");
|
||||||
|
const path = require("path");
|
||||||
|
|
||||||
|
const EXAMPLE_NODE_DEF: ComfyNodeDef = {
|
||||||
|
input: {
|
||||||
|
required: {
|
||||||
|
ckpt_name: [["model1.safetensors", "model2.ckpt"]],
|
||||||
|
},
|
||||||
|
},
|
||||||
|
output: ["MODEL", "CLIP", "VAE"],
|
||||||
|
output_is_list: [false, false, false],
|
||||||
|
output_name: ["MODEL", "CLIP", "VAE"],
|
||||||
|
name: "CheckpointLoaderSimple",
|
||||||
|
display_name: "Load Checkpoint",
|
||||||
|
description: "",
|
||||||
|
python_module: "nodes",
|
||||||
|
category: "loaders",
|
||||||
|
output_node: false,
|
||||||
|
};
|
||||||
|
|
||||||
|
describe("validateNodeDef", () => {
|
||||||
|
it("Should accept a valid node definition", () => {
|
||||||
|
expect(() => validateComfyNodeDef(EXAMPLE_NODE_DEF)).not.toThrow();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
[{ ckpt_name: "foo" }, ["foo", {}]],
|
||||||
|
[{ ckpt_name: ["foo"] }, ["foo", {}]],
|
||||||
|
[{ ckpt_name: ["foo", { default: 1 }] }, ["foo", { default: 1 }]],
|
||||||
|
])(
|
||||||
|
"validateComfyNodeDef with various input spec formats",
|
||||||
|
(inputSpec, expected) => {
|
||||||
|
it(`should accept input spec format: ${JSON.stringify(inputSpec)}`, () => {
|
||||||
|
expect(
|
||||||
|
validateComfyNodeDef({
|
||||||
|
...EXAMPLE_NODE_DEF,
|
||||||
|
input: {
|
||||||
|
required: inputSpec,
|
||||||
|
},
|
||||||
|
}).input.required.ckpt_name
|
||||||
|
).toEqual(expected);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
describe.each([
|
||||||
|
[{ ckpt_name: { "model1.safetensors": "foo" } }],
|
||||||
|
[{ ckpt_name: ["*", ""] }],
|
||||||
|
[{ ckpt_name: ["foo", { default: 1 }, { default: 2 }] }],
|
||||||
|
])(
|
||||||
|
"validateComfyNodeDef rejects with various input spec formats",
|
||||||
|
(inputSpec) => {
|
||||||
|
it(`should accept input spec format: ${JSON.stringify(inputSpec)}`, () => {
|
||||||
|
expect(() =>
|
||||||
|
validateComfyNodeDef({
|
||||||
|
...EXAMPLE_NODE_DEF,
|
||||||
|
input: {
|
||||||
|
required: inputSpec,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
).toThrow();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
it("Should accept all built-in node definitions", async () => {
|
||||||
|
const nodeDefs = Object.values(
|
||||||
|
JSON.parse(
|
||||||
|
fs.readFileSync(path.resolve("./tests-ui/data/object_info.json"))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
nodeDefs.forEach((nodeDef) => {
|
||||||
|
expect(() => validateComfyNodeDef(nodeDef)).not.toThrow();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user